Search code examples
bashshellargumentsparameter-passingposix

Is $(printf '%q ' "${@:1}") equivalent to "${*}"?


Is $(printf '%q ' "${@:1}") equivalent to "${*}"?

If they are, then, doing $(printf '%q ' "${@:2}") (note the 2 instead of 1 as before) is not possible with pure bash $*?

Related questions:

  1. POSIX sh equivalent for Bash’s printf %q
  2. How to use printf "%q " in bash?
  3. Bash printf %q invalid directive
  4. How to make runuser correctly forward all command line arguments, instead of trying to interpret them?
  5. How to portability use "${@:2}"?

Solution

  • No, it is not equivalent, because words are splitted. Ex. the following code:

    check_args() {
      echo "\$#=$#"
      printf "%s\n" "$@";
    }
    
    # setting arguments
    set -- "space notspace" "newline"$'\n'"newline"
    echo '1: ---------------- "$*"'
    check_args "$*"
    
    echo '2: ---------------- $(printf '\''%q '\'' "${@:1}")'
    check_args $(printf '%q ' "${@:1}")
    
    echo '3: ---------------- "$(printf '\''%q '\'' "${@:1}")"'
    check_args "$(printf '%q ' "${@:1}")"
    
    echo '4: ---------------- IFS=@ and "$*"'
    ( IFS=@; check_args "$*"; )
    
    echo "5: ---------------- duplicating quoted"
    check_args "$(printf '%s'"${IFS:0:1}" "${@:1}" | sed 's/'"${IFS:0:1}"'$//')"
    
    echo "6: ---------------- duplicating quoted IFS=@"
    ( IFS=@; check_args "$(printf '%s'"${IFS:0:1}" "${@:1}" | sed 's/'"${IFS:0:1}"'$//')"; )
    
    echo "7: ---------------- duplicating eval unquoted"
    eval check_args $(printf '%q"'"${IFS:0:1}"'"' "${@:1}" | sed 's/'"${IFS:0:1}"'$//')
    
    echo "8: ---------------- duplicating eval unquoted IFS=@"
    ( eval check_args $(IFS=@ ; printf '%q"'"${IFS:0:1}"'"' "${@:1}" | sed 's/"'"${IFS:0:1}"'"$//'); )
    

    will output:

    1: ---------------- "$*"
    $#=1
    space notspace newline
    newline
    2: ---------------- $(printf '%q ' "${@:1}")
    $#=3
    space\
    notspace
    $'newline\nnewline'
    3: ---------------- "$(printf '%q ' "${@:1}")"
    $#=1
    space\ notspace $'newline\nnewline'
    4: ---------------- IFS=@ and "$*"
    $#=1
    space notspace@newline
    newline
    5: ---------------- duplicating quoted
    $#=1
    space notspace newline
    newline
    6: ---------------- duplicating quoted IFS=@
    $#=1
    space notspace@newline
    newline
    7: ---------------- duplicating eval unquoted
    $#=1
    space notspace newline
    newline
    8: ---------------- duplicating eval unquoted IFS=@
    $#=1
    space notspace@newline
    newline
    

    tested on repl.

    The "$*" outputs the arguments delimetered by IFS. So, shown in test 4, if delimeter is not unset or set to space, then the output of $* will be delimetered by IFS, @ in this example.

    Also when IFS is unset or set to space, the output of $* does not include a terminating space, while printf '%q ' will always print a trailing space on the end of the string.

    The output of $(printf '%q ' "${@:1}") is still splitted on space. So the test case 2 receives 3 arguments, because the space notspace string is separated by space and splitted to two arguments. When enclosing the printf inside " will not help - printf substitutes ex. newlines for \n characters.

    Cases 5, 6, 7, 8 are my tries to replicate the behavior of "$*" using printf. It can be seen with cases 7 and 8 I used eval, with cases 5 and 6 I quoted the command substitution. The output of cases ( 5 and 6 ) and ( 7 and 8 ) should match the output of cases 1 and 4 respectively.

    For duplicating the behavior of "$*" special care needs to be taken for IFS to properly delimeter the strings. I used sed 's/'"${IFS:0:1}"'$//' to remove the trailing IFS separator from the printf output. The 5 and 6 cases are unquoted $(printf ...) tries, with 6 using IFS=@ to show the separating works. The 7 and 8 cases use eval with special handling on the IFS, cause the IFS character itself needs to be enclosed with quotes, so the shell will not split on it again, that's why printf '%q"'"${IFS:0:1}"'"'.

    doing $(printf '%q ' "${@:2}") (note the 2 instead of 1 as before) is not possible with pure bash $*?

    You probably could just shift the arguments inside the substitution $(shift; printf "%s\n" "$*"), but as shown above, they are not equivalent anyway.