9

In a shell script, my understanding is that "$@" expands to the script arguments, quoting them as needed. For instance this forwards the script arguments to gcc:

gcc -fPIC "$@"

When using the bash pass-to-stdin syntax <<< though, "@$" doesn't work as I would expect it to.

#!/bin/bash
cat <<< "$@"

Calling the script as ./test.sh foo "bar baz" gives

foo bar baz

I would expect

foo "bar baz"

Is there a way to write a shell script that prints it's arguments as you would write them on the shell prompt? For instance: a hint as to what command to use next, including the script arguments in the hint.

0

4 Answers 4

5

Well, "$@" expands to the list of positional parameters, one argument per positional parameter.

When you do:

set '' 'foo bar' $'blah\nblah'
cmd "$@"

cmd is being invoked with those 3 arguments: the empty string, foo bar and blah<newline>blah. The shell will call the execve() system call with something like:

execve("/path/to/cmd", ["cmd", "", "foo bar", "blah\nblah"], [envvars...]);

If you want to reconstruct a shell command line (that is code in the shell language) that would reproduce that same invocation, you could do something like:

awk -v q="'" '
  function shellquote(s) {
    gsub(q, q "\\" q q, s)
    return q s q
  }
  BEGIN {
    for (i = 1; i < ARGC; i++) {
      printf "%s", sep shellquote(ARGV[i])
      sep = " "
    }
    printf "\n"
  }' cmd "$@"

Or with zsh, asking for different types of quotes:

$ set '' 'foo bar' $'blah\nblah'
$ print -r -- cmd "${(q)@}"
cmd '' foo\ bar blah$'\n'blah
$ print -r -- cmd "${(qq)@}"
cmd '' 'foo bar' 'blah
blah'
$ print -r -- cmd "${(qqq)@}"
cmd "" "foo bar" "blah
blah"
$ print -r -- cmd "${(qqqq)@}"
cmd $'' $'foo bar' $'blah\nblah'

Or with zsh, bash or ksh93 (here for bash, YMMV with other shells):

$ set '' 'foo bar' $'blah\nblah'
$ printf cmd; printf ' %q' "$@"; printf '\n'
cmd '' foo\ bar $'blah\nblah'

You could also use the shell's xtrace option that causes the shell to print what it's going to execute:

$ (PS4=; set -x; : cmd "$@")
: cmd '' 'foo bar' 'blah
blah'

Above, we ran the : no-op command with cmd and the positional parameters as argument. My shell printed them in a nice quoted fashion suitable for reinput to the shell. Not all shells do that.

5
`"$@"` expands to the script arguments, quoting them as needed

No, this is not what happens. Calling a program takes a list of arguments, each argument being a string. When you run the shell program ./test.sh foo "bar baz", this builds a call with three arguments: ./test.sh, foo, and bar baz. (The zeroth argument is the program name; this allows programs to know under what name they are called.) Quoting is a feature of the shell, not a feature of program calls. The shell builds this list when it makes the call.

"$@" directly copies the list of arguments passed to the script or function to the list of arguments in the call where it's used. There is no quoting involved since there is no shell parsing done on those lists.

In cat <<< "$@", you're using "$@" in a context where a single string is required. The <<< operator` requires a string, not a list of strings. In this context, bash takes the elements of the list and joins them with a space in between.

For script debugging, if you run set -x (set +x to turn off), that activates a trace mode where each command is printed before is executed. In bash, that trace has quotes which make it possible to paste the command back into a shell (this isn't true of every sh implementation).

If you have a string and you want to turn it into shell source syntax that parses back into the original string, you can surround it with single quotes, and replace every single quote inside the string with '\''.

for x do
  printf %s "'${x//\'/\'\\\'\'}' "
done
echo

The string replacement syntax is ksh93/bash/zsh/mksh-specific. In plain sh, you need to loop over the string.

for raw do
  quoted=
  while case "$raw" in *\'*) true;; *) false;; esac; do
    quoted="$quoted'\\''${raw%%\'*}"
    raw="${raw#*\'}"
  done
  printf %s "'$quoted$raw' "
done
echo
2

"$@" expands to the script arguments, quoting them as needed

Well, sort of. For practical purposes that should be close enough, and the reference manual does say that "$@" is equivalent to "$1" "$2" ...

So, with the two parameters foo and bar baz, these would be alike:

echo "$@"
echo "$1" "$2"
echo "foo" "bar baz"

(Except that if the parameters contained special characters instead of just plain strings, they wouldn't be expanded again after expanding $@ and $1...)

But even if we consider $@ replaced by the parameters in quotes, the quotes wouldn't be there for echo to see, similarly to that gcc doesn't get the quotes either.

<<< is a bit of an exception to the "$@" == "$1" "$2" ... rule, it's explicitly mentioned that The result is supplied as a single string to the command on its standard input after going through parameter and variable expansion and quote removal among others. So as usual, <<< "foo" just gives foo as input, in the same way somecmd "foo" only gives foo as an argument.

Calling the script as ./test.sh foo "bar baz" [...] I would expect foo "bar baz"

If the quotes remained, it would still have to be "foo" "bar baz". The shell or any running command doesn't have any idea what the quoting was when the command was ran. Or if there even was any quoting to talk of, the system call just receives a list of null-terminated strings and quotes are only a feature of the shell language. Other languages may have other conventions.

0

An alternate solution for bash

q='"'; t=( "${@/#/$q}" ); u=( "${t[@]/%/$q}" ); echo ${u[@]}

Bash doesn't support nested substitutions, so thanks to https://stackoverflow.com/questions/12303974/assign-array-to-variable#12304017 for showing how to reassign an array. See man bash (https://linux.die.net/man/1/bash) for details on arrays, expansion, and pattern substitution (under parameter expansion).

Analysis

Bash puts the command line parameters as an array in $@

q holds the quoting character.

Double quotes around parameter expansion ${ ... } preserves the individual parameters as distinct elements and wrapping them in ( ) allows you to assign them as an array to a variable.

/#/$q in a parameter expansion substitutes the beginning of the pattern (like regex ^) with the quoting char.

/%/$q in a parameter expansion substitutes the end of the pattern (like regex $) with the quoting char.

Use case: querying MySQL for a list of email addresses from the command line

There are a few changes to the statements above to use a different quoting char, add commas between the parameters, and strip off the final comma. And of course I'm being bad by putting the password in the mysql invocation. So sue me.

q="'"; t=( "${@/#/$q}" ); u="${t[@]/%/$q,}"; v="u.email in( ${u%,} )"
mysql -uprod_program -h10.90.2.11 -pxxxxxxxxxxxx my_database <<END
select ...
from users u
join ....
where $v # <<<<<<<<<<<<<<<<<< here is where all the hard work pays off :-)
group by user_id, prog_id
;
END
2
  • Just note that quoting with double-quotes isn't very helpful in protecting backslashes, $-expansions and other double-quotes. That's why the other answers use single quotes while going to some lengths to handle single quotes inside the string, or use the shell's own features for producing a quoted copy of the string.
    – ilkkachu
    Commented Jun 3, 2018 at 15:39
  • @ilkkachu Duly noted! That's why (among other reasons) that I upvoted all of the previous answers. Also why I added this use case. Hopefully the perfect is not the enemy of the good.
    – Jeff
    Commented Jun 3, 2018 at 15:53

You must log in to answer this question.

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