132

I have a Bash script where I want to keep quotes in the arguments passed.

Example:

./test.sh this is "some test"

Then I want to use those arguments, and reuse them, including quotes and quotes around the whole argument list.

I tried using \"$@\", but that removes the quotes inside the list.

How do I accomplish this?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
zlack
  • 1,937
  • 4
  • 15
  • 15
  • 5
    `\"$@\"` is wrong -- it adds **literal** quotes to the first and last arguments. `"$@"`, without the backslashes, is correct: in it, the quotes are purely syntactic. See [BashFAQ #50](http://mywiki.wooledge.org/BashFAQ/050) for an explanation of why passing argument lists around as strings is innately incorrect in shell. – Charles Duffy Mar 09 '17 at 22:54
  • 2
    Charles is right. When you want to pass all the arguments to another script, or function, use "$@" (without escaping your quotes). – CompEng88 Oct 09 '17 at 21:54
  • See this answer: [Difference between single and double quotes in Bash](https://stackoverflow.com/a/42082956/6862601). – codeforester Dec 19 '17 at 05:45
  • 1
    Related: [How do I use a Bash variable (string) containing quotes in a command?](https://superuser.com/q/360966/11574) –  May 30 '18 at 16:19
  • https://unix.stackexchange.com/a/445477/162125 – Sergey Ponomarev Jan 17 '22 at 16:43
  • Related: *[How to escape single quotes within single quoted strings](https://stackoverflow.com/questions/1250079/)* – Peter Mortensen Aug 16 '23 at 18:29

14 Answers14

142

using "$@" will substitute the arguments as a list, without re-splitting them on whitespace (they were split once when the shell script was invoked), which is generally exactly what you want if you just want to re-pass the arguments to another program.

Note that this is a special form and is only recognized as such if it appears exactly this way. If you add anything else in the quotes the result will get combined into a single argument.

What are you trying to do and in what way is it not working?

Chris Dodd
  • 119,907
  • 13
  • 134
  • 226
  • 7
    I think he means the quotes are part of the argument string, not part of the bash syntax. – Alex Brown May 12 '14 at 17:46
  • To clarify, this answer is perfect: it is just like the other answers that tries to re-add quote around each argument. This DOES NOT just add two quotes around the whole argument list, but around each individual argument. – qwertzguy May 15 '14 at 12:28
  • 4
    @qwertzguy is incorrect. The double quotes will be stripped before the variable enters the arguments list. – Donal Lafferty Nov 03 '14 at 09:35
  • 11
    This is the top-rated answer. While it is correct per se, it doesn't address the question really. – Torsten Bronger Jan 25 '16 at 22:10
  • 2
    @TorstenBronger: Since the question is completely unclear, this is answering my best guess for what the OP actually wants. – Chris Dodd Jan 26 '16 at 03:17
  • 1
    Yes, I agree that it is unclear. And that your answer has its value in its own right. And I really do not grudge you any upvote. But still ... this is the answer to another question. :) – Torsten Bronger Jan 26 '16 at 07:23
  • @TorstenBronger: the upvotes indicate that this is the asnwer to the question that most people have when they search for their question and hit this page, even if its not the answer to what the OP was trying to ask. – Chris Dodd Feb 14 '16 at 20:47
  • how would you use the "$@" format for passing the params to `xargs` ? would it work without the re-quoting trick? i do not think so – Hofi Apr 13 '16 at 14:49
  • @Hofi: `xargs` doesn't reinterpret or reparse its arguments in any way, so there should be no need for additional quoting (unless you're doing something like `xargs sh `...) – Chris Dodd Apr 13 '16 at 23:22
  • @ChrisDodd i'm confused now, will you plz clarify, how would xargs do that in the following case `echo "$@" | xargs -t defaults write "${BUNDLEID}" "${KEY}" -dict` in this case if one of the input parameters is like "pararm with spaces" than it would not work without the extra quoting trick – Hofi Apr 14 '16 at 07:56
  • @Hofi: You can do something like `find | xargs program "$@"` and it will pass the arguments of the original script or function on to `program` with no reinterpretation. If you want to xargs to not split its INPUT on spaces, you need something like `{ for a in "$@"; do echo -en "$a\\x0"; done; } | xargs -0 program` – Chris Dodd Apr 14 '16 at 18:01
  • @ChrisDodd sorry, but neither above will work if you have to just pass strings with spaces inside, try these `echo "nospace" "yes space" | xargs ls "$@"` `echo "nospace" "yes space" | xargs -0 ls ` `echo '"nospace"' '"yes space"' | xargs ls ` In the first case 3 separate name passed to `ls` by `xargs` and in the 2nd case only one (as expected) only the 3rd case working correctly (not surprisingly) – Hofi Apr 15 '16 at 11:29
  • 2
    @Hofi: Those are all because you need the correct quoting for `echo` -- `xargs` and `"$@"` are irrelevant. Try `echo -ne "nospace\x0yes space\x0"` as I suggested. – Chris Dodd Apr 15 '16 at 14:25
  • 1
    @TorstenBronger, it absolutely does address the question, if what you're doing is passing through arguments you received -- which is the only context where "keeping" quotes makes sense. If your script was run with `./yourscript this is "some test"`, then running `./anotherscript "$@"` will pass `this`, `is`, and `some test` as three distinct arguments through to `anotherscript`, just as if a user had run `./anotherscript this is "some test"`. – Charles Duffy Mar 09 '17 at 22:54
  • @Hofi, `xargs` is a special case, and it's generally accepted that it's broken-by-design (inasmuch as it tries to perform shell-like string-splitting on its literal input). Indeed, if you look at reputable references such as [the Wooledge wiki](http://mywiki.wooledge.org/UsingFind#Actions_in_bulk:_xargs.2C_-print0_and_-exec_.2B-), you find warnings akin to: "*xargs by default is **extremely** dangerous, and should never be used without the nonstandard `-0` extension.*". That said, xargs only dequotes its stdin, **not** its argument vector. – Charles Duffy Mar 09 '17 at 22:59
  • @Hofi, ...thus, it's completely valid to run `xargs something "$@"` -- it's when you're feeding content to **stdin** of `xargs` that precautions such as `printf '%s\0' "$@" | xargs -0 something` are needed. – Charles Duffy Mar 09 '17 at 23:02
  • This is definitely not the answer we are looking for even with top vote. A example why we need this is, for example, I need to use ssh host -t "remoteCmd $@", The remote command expect some argument with a string with spaces in between which we do need to keep quote. – Jianwu Chen Jan 26 '21 at 06:47
  • @JianwuChen: you want the quotes just around the `$@` -- `ssh host -t remoteCmd "$@"`. The special expansion doesn't apply if you include anything else in the quotes. – Chris Dodd Jan 26 '21 at 18:07
71

There are two safe ways to do this:

1. Shell parameter expansion: ${variable@Q}:

When expanding a variable via ${variable@Q}:

The expansion is a string that is the value of parameter quoted in a format that can be reused as input.

Example:

$ expand-q() { for i; do echo ${i@Q}; done; }  # Same as for `i in "$@"`...
$ expand-q word "two words" 'new
> line' "single'quote" 'double"quote'
word
'two words'
$'new\nline'
'single'\''quote'
'double"quote'

2. printf %q "$quote-me"

printf supports quoting internally. The manual's entry for printf says:

%q Causes printf to output the corresponding argument in a format that can be reused as shell input.

Example:

$ cat test.sh 
#!/bin/bash
printf "%q\n" "$@"
$ 
$ ./test.sh this is "some test" 'new                                                                                                              
>line' "single'quote" 'double"quote'
this
is
some\ test
$'new\nline'
single\'quote
double\"quote
$

Note the 2nd way is a bit cleaner if displaying the quoted text to a human.

Related: For bash, POSIX sh and zsh: Quote string with single quotes rather than backslashes

Tom Hale
  • 40,825
  • 36
  • 187
  • 242
  • 1
    This worked for me. In a function I used: local _quoted=$( printf ' "%q"' "$@" ) This answer looks like the most robust. – Laurent Caillette Nov 07 '17 at 08:50
  • Interesting, but it does add backslashes to spaces and characters like bang. In some cases this doesn't matter, but it converts a single unquoted ! to /! which doesn't work for me. – Elliptical view Feb 21 '18 at 04:31
  • `!` needs to be quoted: Unquoted: `$ echo !foo` `-bash: !foo: event not found` Quoted: `$ echo \!foo` `!foo` – Tom Hale Feb 26 '18 at 14:36
  • 8
    The 1st method `expand-q` has an error: `${i@Q}` should be `${i:Q}` otherwise it fails with `bash: ${i@Q}: bad substitution` – arielf May 31 '18 at 00:49
  • 3
    @arielf What version of `bash`? It works as shown on `$BASH_VERSION 4.4.19(1)-release`. What you're doing is probably a side effect of `${parameter:offset}` where `offset` is `0`, (possibly because `atoi()` of `Q` gives `0`). – Tom Hale May 31 '18 at 08:30
  • 2
    That may be the reason. I'm on Ubuntu 16.04.4 (LTS). Latest bash on this system is `4.3.48(1)-release` – arielf Jun 02 '18 at 04:47
  • 9
    I found `${*@Q}` to be very useful for passing all command line arguments to a subsequent shell invocation, in my case one due to a `sg` call to change group. Thanks for this answer! – MvG Aug 30 '19 at 21:25
  • @MvG I wish I'd run into a comment like yours sooner; I'm doing the *exact* same thing! I can confirm that `${*@Q}` is the perfect solution at the end of my long search – forresthopkinsa Jul 10 '21 at 02:03
  • In example one, my bash (version 5.1.8) also puts the first line `word` in quotes, as `'word'`, which is different from yours. It look bad in my case because I see a lot of unnecessary quotes. How do you achieve your result? (no quote for `word`) – doraemon Jan 16 '22 at 12:13
48

Yuku's answer only works if you're the only user of your script, while Dennis Williamson's is great if you're mainly interested in printing the strings, and expect them to have no quotes-in-quotes.

Here's a version that can be used if you want to pass all arguments as one big quoted-string argument to the -c parameter of bash or su:

#!/bin/bash
C=''
for i in "$@"; do 
    i="${i//\\/\\\\}"
    C="$C \"${i//\"/\\\"}\""
done
bash -c "$C"

So, all the arguments get a quote around them (harmless if it wasn't there before, for this purpose), but we also escape any escapes and then escape any quotes that were already in an argument (the syntax ${var//from/to} does global substring substitution).

You could of course only quote stuff which already had whitespace in it, but it won't matter here. One utility of a script like this is to be able to have a certain predefined set of environment variables (or, with su, to run stuff as a certain user, without that mess of double-quoting everything).


Update: I recently had reason to do this in a POSIX way with minimal forking, which lead to this script (the last printf there outputs the command line used to invoke the script, which you should be able to copy-paste in order to invoke it with equivalent arguments):

#!/bin/sh
C=''
for i in "$@"; do
    case "$i" in
        *\'*)
            i=`printf "%s" "$i" | sed "s/'/'\"'\"'/g"`
            ;;
        *) : ;;
    esac
    C="$C '$i'"
done
printf "$0%s\n" "$C"

I switched to '' since shells also interpret things like $ and !! in ""-quotes.

unhammer
  • 4,306
  • 2
  • 39
  • 52
  • Use array instead? I'll post an example below. – JohnMudd Apr 01 '15 at 18:45
  • 1
    This is almost perfect, but I would suggest one minor modification - escaping all `\` before appending to `$C` - i.e. adding `i="${i//\\/\\\\}"` otherwise arguments like `"\\\"asdf"` will fail. – Matěj Hrazdíra Jul 24 '16 at 16:32
  • Good catch! Thanks, edited and added. I can now do `$ ./escape-args-to-bash echo "\\\"asdf"` to get `\"asdf` just like `$ echo "\\\"asdf"` gives `\"asdf`. (In case the above comment isn't editable, the single ` was probably meant to be a backslash in backticks that ended up escaping the backtick instead – is there a corollary to Muphry's Law here? :)) – unhammer Jul 25 '16 at 07:27
  • 1
    @unhammer Thank you so much, this works awesome! – Pieter Vogelaar Jan 10 '19 at 20:51
  • 2
    ℹ️ Same as just: `"${@@Q}"` – Alberto Salvia Novella May 19 '21 at 01:09
  • Oh that's interesting, what do you call that? It doesn't seem to do the same thing though, since it splits it up into several params (much like `"$@"`), so you can't use it as the single argument of `-c` – also bash-only. – unhammer May 19 '21 at 09:43
  • 1
    `man bash` says it's a **Parameter transformation**. Could use that in the bash-version instead of the regex replace, giving `C=''; for i in "$@"; do C="$C ${i@Q}"; done; bash -c "$C"` – unhammer May 19 '21 at 10:35
37

If it's safe to make the assumption that an argument that contains white space must have been (and should be) quoted, then you can add them like this:

#!/bin/bash
whitespace="[[:space:]]"
for i in "$@"
do
    if [[ $i =~ $whitespace ]]
    then
        i=\"$i\"
    fi
    echo "$i"
done

Here is a sample run:

$ ./argtest abc def "ghi jkl" $'mno\tpqr' $'stu\nvwx'
abc
def
"ghi jkl"
"mno    pqr"
"stu
vwx"

You can also insert literal tabs and newlines using Ctrl-V Tab and Ctrl-V Ctrl-J within double or single quotes instead of using escapes within $'...'.

A note on inserting characters in Bash: If you're using Vi key bindings (set -o vi) in Bash (Emacs is the default - set -o emacs), you'll need to be in insert mode in order to insert characters. In Emacs mode, you're always in insert mode.

Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • But how to check if the user has quoted say, the 1st argument? – Tony Barganski Nov 09 '18 at 16:59
  • 2
    @TonyBarganski: The shell strips quotes when arguments are processed before they're passed to the program receiving them. It's not possible to tell whether there were _outer_ quotes on an argument. – Dennis Williamson Nov 09 '18 at 18:27
23

I needed this for forwarding all arguments to another interpreter. What ended up right for me is:

bash -c "$(printf ' %q' "$@")"

Example (when named as forward.sh):

$ ./forward.sh echo "3 4"
3 4
$ ./forward.sh bash -c "bash -c 'echo 3'"
3

(Of course the actual script I use is more complex, involving in my case nohup and redirections etc., but this is the key part.)

isarandi
  • 3,120
  • 25
  • 35
14

Like Tom Hale said, one way to do this is with printf using %q to quote-escape.

For example:

send_all_args.sh

#!/bin/bash
if [ "$#" -lt 1 ]; then
 quoted_args=""
else
 quoted_args="$(printf " %q" "${@}")"
fi

bash -c "$( dirname "${BASH_SOURCE[0]}" )/receiver.sh${quoted_args}"

send_fewer_args.sh

#!/bin/bash
if [ "$#" -lt 2 ]; then
 quoted_last_args=""
else
 quoted_last_args="$(printf " %q" "${@:2}")"
fi

bash -c "$( dirname "${BASH_SOURCE[0]}" )/receiver.sh${quoted_last_args}"

receiver.sh

#!/bin/bash
for arg in "$@"; do
  echo "$arg"
done

Example usage:

$ ./send_all_args.sh
$ ./send_all_args.sh a b
a
b
$ ./send_all_args.sh "a' b" 'c "e '
a' b
c "e
$ ./send_fewer_args.sh
$ ./send_fewer_args.sh a
$ ./send_fewer_args.sh a b
b
$ ./send_fewer_args.sh "a' b" 'c "e '
c "e
$ ./send_fewer_args.sh "a' b" 'c "e ' 'f " g'
c "e
f " g
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Gary S. Weaver
  • 7,966
  • 4
  • 37
  • 61
  • This is a great example because it contains a reproducible test for forwarding the arguments through something like `sh -c`. Plus, it works ;-). Not all of the others do. – Dave Abrahams Sep 11 '20 at 17:32
9

Just use:

"${@}"

For example:

# cat t2.sh
for I in "${@}"
do
   echo "Param: $I"
done
# cat t1.sh
./t2.sh "${@}"

# ./t1.sh "This is a test" "This is another line" a b "and also c"
Param: This is a test
Param: This is another line
Param: a
Param: b
Param: and also c
FrameGrace
  • 585
  • 4
  • 4
6

Changed unhammer's example to use array.

printargs() { printf "'%s' " "$@"; echo; };  # http://superuser.com/a/361133/126847

C=()
for i in "$@"; do
    C+=("$i")  # Need quotes here to append as a single array element.
done

printargs "${C[@]}"  # Pass array to a program as a list of arguments.                               
Community
  • 1
  • 1
JohnMudd
  • 13,607
  • 2
  • 26
  • 24
6

My problem was similar and I used mixed ideas posted here.

We have a server with a PHP script that sends e-mails. And then we have a second server that connects to the 1st server via SSH and executes it.

The script name is the same on both servers and both are actually executed via a bash script.

On server 1 (local) bash script we have just:

/usr/bin/php /usr/local/myscript/myscript.php "$@"

This resides on /usr/local/bin/myscript and is called by the remote server. It works fine even for arguments with spaces.

But then at the remote server we can't use the same logic because the 1st server will not receive the quotes from "$@". I used the ideas from JohnMudd and Dennis Williamson to recreate the options and parameters array with the quotations. I like the idea of adding escaped quotations only when the item has spaces in it.

So the remote script runs with:

CSMOPTS=()
whitespace="[[:space:]]"
for i in "$@"
do
    if [[ $i =~ $whitespace ]]
    then
        CSMOPTS+=(\"$i\")
    else
        CSMOPTS+=($i)
    fi
done

/usr/bin/ssh "$USER@$SERVER" "/usr/local/bin/myscript ${CSMOPTS[@]}"

Note that I use "${CSMOPTS[@]}" to pass the options array to the remote server.

Thanks for eveyone that posted here! It really helped me! :)

Gus Neves
  • 470
  • 6
  • 7
  • the above slightly fails: -opt="value" becomes "-opt=value" – sdive Feb 06 '17 at 13:58
  • I am not using `=` in my scripts, but since the `\"` are only added when spaces are detected, `-opt="value"` will actually become `-opt=value`. something like `-opt="another value"` would be sent as `"-opt=another value"` but since my setup sends it to another script on another machine it is still interpreted as a single token as intended. The remote script receives `-opt=another value` as a single token without the quotations. Things are very different if running locally though. You wouldn't even need to escape the `\"` for example. – Gus Neves Apr 19 '17 at 14:55
3

Quotes are interpreted by bash and are not stored in command line arguments or variable values.

If you want to use quoted arguments, you have to quote them each time you use them:

val="$3"
echo "Hello World" > "$val"
mouviciel
  • 66,855
  • 13
  • 106
  • 140
1

As Gary S. Weaver shown in his source code tips, the trick is to call bash with parameter '-c' and then quote the next.

e.g.

bash -c "<your program> <parameters>"

or

docker exec -it <my docker> bash -c "$SCRIPT $quoted_args"
Lars
  • 3,576
  • 2
  • 15
  • 13
1

If you need to pass all arguments to bash from another programming language (for example, if you'd want to execute bash -c or emit_bash_code | bash), use this:

  • escape all single quote characters you have with '\''.
  • then, surround the result with singular quotes

The argument of abc'def will thus be converted to 'abc'\''def'. The characters '\'' are interpreted as following: the already existing quoting is terminated with the first first quote, then the escaped singular single quote \' comes, then the new quoting starts.

VasiliNovikov
  • 9,681
  • 4
  • 44
  • 62
  • 2
    No. Don't escape arguments as text embedded in the code; instead, use an interface from your other programming language that lets you pass an *array* of arguments, and pass each item in its own array element. This means folks shouldn't ever use `system()`; fortunately, its deficiencies have been recognized for decades, so it's rarely the only choice. – Charles Duffy Jun 09 '20 at 13:00
  • In Python, for example, one can pass an array rather than a string to a `subprocess`-family call. In C, one can use `execv` or `posix_spawn`. In Java, one can use one of the array-based interfaces in `Runtime.exec()`; etc, etc. – Charles Duffy Jun 09 '20 at 13:03
  • 1
    @CharlesDuffy Sorry, but my answer starts from the word "if". See "for example" if it's not clear enough... If you can just call a CLI by using an array of arguments, of course that's better. In the situation I describe, however, it's just not possible (`bash -c` or `another_program | bash`). – VasiliNovikov Jun 09 '20 at 19:01
  • 1
    It's *absolutely* possible. `subprocess.Popen(['bash', '-c', 'code that uses "$1" and "$2"', "_", "value for $1", "value for $2"])`, f/e, starts bash from Python while passing two values in a way that keeps them out-of-band from code. The pipe-to-bash pattern simply shouldn't be followed at all, but one can get code that way while similarly passing data out-of-band with any of the available equivalents to `another_program | bash -s 'value for $1' 'value for $2'` – Charles Duffy Jun 09 '20 at 19:11
  • Even if you're using a language that provides nothing but `system()`, you can use `setenv()` to store your literal values as environment variables and then pass the code as a constant, which refers to those variables from the body. – Charles Duffy Jun 09 '20 at 19:13
  • 1
    `The pipe-to-bash pattern simply shouldn't be followed at all` -- you could be right. That's what my answer is about still. I guess it might be wrong in a sense that, I should give more warnings on the preferred approach. Personally, I used it to print a CLI instruction that _can_ be copy-pasted by the user to a terminal (again something that you should never really encourage, to be fair). – VasiliNovikov Jun 10 '20 at 09:30
0

Yes, seems that it is not possible to ever preserve the quotes, but for the issue I was dealing with it wasn't necessary.

I have a bash function that will search down folder recursively and grep for a string, the problem is passing a string that has spaces, such as "find this string". Passing this to the bash script will then take the base argument $n and pass it to grep, this has grep believing these are different arguments. The way I solved this by using the fact that when you quote bash to call the function it groups the items in the quotes into a single argument. I just needed to decorate that argument with quotes and pass it to the grep command.

If you know what argument you are receiving in bash that needs quotes for its next step you can just decorate with with quotes.

Kalamarico
  • 5,466
  • 22
  • 53
  • 70
Tom Orsi
  • 51
  • 4
-18

Just use single quotes around the string with the double quotes:

./test.sh this is '"some test"'

So the double quotes of inside the single quotes were also interpreted as string.

But I would recommend to put the whole string between single quotes:

 ./test.sh 'this is "some test" '

In order to understand what the shell is doing or rather interpreting arguments in scripts, you can write a little script like this:

#!/bin/bash

echo $@
echo "$@"

Then you'll see and test, what's going on when calling a script with different strings

Sedat Kilinc
  • 2,843
  • 1
  • 22
  • 20
Randy Sugianto 'Yuku'
  • 71,383
  • 57
  • 178
  • 228
  • 60
    How can this be the accepted answer? This requires the caller of the script to know that he meeds double quotation although this can be handled in the script without altering the way the caller builds the parameter list – Patrick Cornelissen Mar 02 '16 at 07:09
  • 4
    If you have no control over `./test.sh` then this is the correct answer, but if you are authoring `./test.sh`, then [the answer from Chris Dodd](http://stackoverflow.com/a/1669548/185101) is the better solution. – sanscore Mar 29 '16 at 08:26
  • 1
    Not just if you have no control over `test.sh`, but only in the scenario where `test.sh` is **written to use `eval` unsafely** is this appropriate. And frankly, if something is using `eval` unsafely, that's probably a pretty good reason to stop using that thing. – Charles Duffy Mar 09 '17 at 23:03
  • 2
    Wtf is wrong with people here. How is this an accepted answer. It passes the responsibility to the caller. – Arunav Sanyal May 12 '17 at 07:19
  • Although this is correct, it is basically "you don't"-type answer. It doesn't really answer the question. Nor does it adhere to the SO rules for answers. If anything, a more informative explanation of why you don't can be found here https://stackoverflow.com/a/10836225/4687565 – Dimitry Jun 30 '17 at 11:41
  • I've extended the answer because it wasn't visible on first sight where the differences of the both calls were. It took a while until I've seen the single quotes at @yuku answer – Sedat Kilinc Jun 27 '18 at 18:18
  • I take that back, for an instant i read it as `$*` instead of `$@`, indeed this is the right answer. The link here clarifies it [Bash $* and $@](https://eklitzke.org/bash-%24*-and-%24%40). – Claudiu Jun 01 '20 at 18:15