Bash – Handling arguments in specified order in /usr/bin/printf or Bash printf

bashprintf

You know that the printf function written in C allows to do:

printf('%2$s %2$s %1$s %1$s', 'World', 'Hello');

Hello Hello World World

But in GNU Bash:

printf '%2$s %2$s %1$s %1$s' 'World' 'Hello'

bash: printf: $': invalid format character

Also for /usr/bin/printf:

/usr/bin/printf '%2$s %2$s %1$s %1$s' 'World' 'Hello'

/usr/bin/printf: %2$: invalid conversion specification

How to obtain the C behavior in Bash? Thanks.

Edited:

I was curious about this behaviour, I can't accept a workaround that changes the order of the arguments. It should work just playing with the format string.

Edited:

E.g. think about GNU Bash source code internationalization. Very improbable without this feature.

Best Answer

You can make bash do it without invoking other processes by redefining printf as a function:

printf() {  # add support for numbered conversion specifiers
    local -a args
    local opt=
    case $1 in
    -v) opt=-v$2; shift 2;;
    -*) opt=$1; shift;;
    esac
    local format=$1; shift
    while [[ $format =~ ((^|.*[^%])%)([0-9]+)\$(.*) ]]
    do
        args=("${!BASH_REMATCH[3]}" "${args[@]}")
        format=${BASH_REMATCH[1]}${BASH_REMATCH[4]}
    done
    let ${#args[@]} && set -- "${args[@]}"
    builtin printf $opt "$format" "$@"
}

This works by replacing numbered conversion specifiers in the format string with unnumbered ones while using the numbers to select arguments for a modified argument list. If no numbered conversion specifiers are present, the printf builtin will see the function's arguments as-is. Should be a drop-in replacement with very little surprising behaviour.

The possibly surprising ordering in the statement that builds the new argument list, with new arguments pushed onto the front of the args[] array instead of being appended, is needed because bash regex matching is greedy; each time around the while loop, the numbered conversion specifier it finds will be the last one remaining in the format string.

Also note that the format string recycling that the printf builtin normally performs when passed more arguments than the format string contains conversion specifiers for doesn't happen when numbered conversion specifiers are present; arguments that don't correspond to a numbered specifier are simply ignored. Given that POSIX describes the mixing of numbered and unnumbered conversion specifiers as undefined, I think that's reasonable.