7

Preamble. I know that in the C language, the printf function allows to do this:

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

Output: Hello Hello World World

But in GNU Bash it seems this feature is not supported:

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

Output: bash: printf: $': invalid format character

I also tried using the local /usr/bin/printf:

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

Output: /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.

2
  • There is currently a discussion about including this in the POSIX standard for the printf utility. This, obviously, doesn't magically make it work in bash.
    – Kusalananda
    Feb 10, 2020 at 10:56
  • @Kusalananda can you link that discussion? Feb 11, 2020 at 11:14

2 Answers 2

7

You can't with bash. the POSIX specification of the printf utility doesn't support it either. You'd have to re-order the arguments by hand.

The printf (or print -f) builtins of ksh93 and zsh support them though:

$ printf '%2$s%1$s\n' a b
ba

GNU awk or perl also support it, so if you have any of those installed, in bash, you could redefine printf as a function like:

printf() { zsh -c '"$0" "$@"' printf "$@"; }

or:

printf() { ksh93 -c '"$0" "$@"' printf "$@"; }

It would be more effort with gawk or perl however as gawk won't let you pass ARGV as is and neither gawk nor perl would expand the \x sequences (unless passed literally in their code in double quotes) and they don't support %b (an extension of the printf utility used to emulate the SysV echo).

0
3

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.

8
  • I appreciate this native approach but it does not work to me testing with printf '%2$s %2$s %1%s %1%s' 'World' 'Hello' Jan 27, 2020 at 10:55
  • 1
    Try printf '%2$s %2$s %1$s %1$s' 'World' 'Hello' instead; when I do that I see output Hello Hello World World as expected. %1%s (possibly a typo?) isn't a valid conversion specifier. %s is, but I'm not sure the builtin printf parser will find it behind the %1 and even if it did you'd be mixing numbered and unnumbered specifiers, which POSIX says is undefined.
    – flabdablet
    Feb 4, 2020 at 19:57
  • Changed my solution to this answer because it's effective and 100% just Bash. Jun 3, 2020 at 7:33
  • 1
    @ValerioBozz, it's got a few issues though: won't work with -v if $IFS contains non-whitespace ($opt handling should be done with an array, expansions should be quoted). It won't work for things like printf '%2$s,%1$s\n' a b c d (c, d ignored as already noted in the answer). It doesn't handle escaped %'s as in printf '%%0$x %2$s,%1$s\n' a b (unlikely to be a problem in practice though). Jun 3, 2020 at 8:18
  • Thanks, Stéphane. I've edited the answer and modified the regex handling to fix the %% bug.
    – flabdablet
    Jun 9, 2020 at 16:57

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .