How to pass quoted arguments from variable to bash script [duplicate] - bash

This question already has answers here:
Why does shell ignore quoting characters in arguments passed to it through variables? [duplicate]
(3 answers)
Closed 2 years ago.
I tried building a set of arguments in a variable and passing that to a script but the behavior different from what I expected.
test.sh
#!/bin/bash
for var in "$#"; do
echo "$var"
done
input
usr#host$ ARGS="-a \"arg one\" -b \"arg two\""
usr#host$ ./test.sh $ARGS
output
-a
"arg
one"
-b
"arg
two"
expected
-a
arg one
-b
arg two
Note if you pass the quoted arguments directly to the script it works. I also can work around this with eval but I wanted to understand why the first approach failed.
workaround
ARGS="./test.sh -a "arg one" -b "arg two""
eval $ARGS

You should use an array, which in some sense provides a 2nd level of quoting:
ARGS=(-a "arg one" -b "arg two")
./test.sh "${ARGS[#]}"
The array expansion produces one word per element of the array, so that the whitespace you quoted when the array was created is not treated as a word separator when constructing the list of arguments that are passed to test.sh.
Note that arrays are not supported by the POSIX shell, but this is the precise shortcoming in the POSIX shell that arrays were introduced to correct.

Related

How to execute a command that is the arguments to the script? [duplicate]

This question already has answers here:
What is the difference between $* and $#
(4 answers)
How do I pass on script arguments that contain quotes/spaces?
(2 answers)
Closed last month.
I want a bash script, call it args, to execute a command that is the arguments to the script.
In particular, I would like this command (note multiple blanks):
$./args echo 'foobar *0x0'
to execute this precise command:
echo 'foobar *0x0'
I tried this in args:
#!/bin/bash
set -x
$*
but it doesn't work:
./args echo 'foobar *0x0'
+ echo foobar '*0x0'
foobar *0x0
Witness the single space, as well as moved single quotes.
With $#, the result is exactly the same, so please don't close the question on the account of differences between $* and $#. Also, blanks are not my only problem, there is the *0x0.
#!/bin/bash
"$#"
This expands to all of the command-line arguments with spacing and quoting intact. $*, by contrast, is subject to unwanted word splitting and globbing since it's not quoted.

How can I create and use a backup copy of all input args ("$#") in bash?

I need the ability to back up and then later use (both to read and print and to pass to another command as arguments) all input args to a bash program, but can't figure it out. Here is my attempt:
back_up_all_input_args.sh:
#!/usr/bin/env bash
all_args1="$#"
all_args2="$(printf "%q " "$#")"
# Simulate parsing the input args here
# - see: https://stackoverflow.com/a/14203146/4561887
shift # remove 1st arg
shift # remove 2nd arg
echo "$#"
echo "$all_args1"
echo "$all_args2"
# Do another program call with all input args here
# rg "$all_args1" # FAILS
# rg "$all_args2" # FAILS
Sample run and output:
$ ./backup_all_input_args.sh arg1 "arg 2" "arg 3"
arg 3
arg1 arg 2 arg 3
arg1 arg\ 2 arg\ 3
My first approach was to back up the arguments with all_args1="$#". This fails because I lose the quotes and get arg1 arg 2 arg 3. My 2nd approach was to back up the arguments with all_args2="$(printf "%q " "$#")". This fails because again, I lose the quotes, and it just so happens the first argument doesn't just need to be stuck together with the backslash like a file path, but rather, it is a regular expression argument, so it needs to remain truly unmodified. This, therefore, is not equal to the original input: arg1 arg\ 2 arg\ 3 .
References I've already studied to get this far
How to keep quotes in Bash arguments?
How can I preserve quotes in printing a bash script's arguments
How do I parse command line arguments in Bash?
Bash demo
Update: this Q&A, now that I have an answer, contributed to these bash demos I just wrote:
back_up_all_input_args.sh
array_practice.sh
...and culminated in this rapid find-and-replace tool wrapper I wrote around Ripgrep.
Add parentheses and store them in an array. This will preserve each of the arguments as separate words and avoids all the backslash escaping complexities.
all_args=("$#")
You can then pass the arguments to another command:
cmd "${all_args[#]}"
Or print them out:
printf '[%s]\n' "${all_args[#]}"
Or assign them to another array:
args_copy=("${all_args[#]}")
You can also use set restore the script's original $1, $2, etc., arguments:
set -- "${all_args[#]}"

Expanded variable as argument of another command via concatenation in Bash [duplicate]

This question already has answers here:
Bash doesn't parse quotes when converting a string to arguments
(5 answers)
Closed 1 year ago.
Imagine I want to call some command some-command via $() with an argument stored in another variable argument, the latter containing space.
With my current understanding, the fact that result="$(some-command $argument)" (e.g. expansion) leads to passing two arguments is as expected.
Question part: why does the result="$(some-command "$argument")" (e.g. concatenation) lead to the desired result of passing one single argument?
More details:
./some-command:
#!/usr/bin/env bash
echo "Arg 1: $1"
echo "Arg 2: $2"
./test-script:
#!/usr/bin/env bash
export PATH="`pwd -P`:$PATH"
argument="one two"
echo "Calling with expansion"
res="$(some-command $argument)"
echo $res
echo "Calling with concatenation"
res="$(some-command "$argument")"
echo $res
Calling test-script leads to the following output:
Calling with expansion
Arg 1: one Arg 2: two
Calling with concatenation
Arg 1: one two Arg 2:
I seem to not grasp when smth is expanded / evaluated and how the expanded variables are grouped into arguments passed to scripts.
Thank you!
P.S. Bonus curiosity is why result="$(some-command \"$argument\")" does not work at all.
That's how quoting and expansions work in bash. In fact, double quotes after = are not needed, as word-splitting is not performed, so
result=$(some-command "$argument")
should work the same way.
There's no "concatenation" going on. Bash treats the string inside $() as a command and runs all the expansions on it before running it.
So, what happens with some-command "$argument"? First, the parameter expansion expands $argument into a string containing spaces. When word-splitting happens, it notes the string was enclosed in double quotes, so it keeps it as a single string.

quotation marks in bash script around $#? [duplicate]

This question already has answers here:
Parsing/passing command line arguments to a bash script - what is the difference between "$#" and "$*"?
(3 answers)
When to wrap quotes around a shell variable?
(5 answers)
How to preserve double quotes in $# in a shell script?
(6 answers)
Closed 5 years ago.
I found the following snippet of bash script from How can I run a function from a script in command line?
$ cat test.sh
testA() {
echo "TEST A $1";
}
testB() {
echo "TEST B $2";
}
"$#"
This works well.
One of the response of this answer is Use "$#" in most cases. $# is not safe in some cases
I'm wondering why $# needs quotation marks, "$#" , in the last line.
What makes it difference with or without quotation marks around $# in the bash script?
"$#" quotes each positional paremeter that is expanded. For example:
# script.sh
cat "$#"
cat $#
If you do script "a b" the first cat will try to emit a file literally named a b. Since the positional parameters aren't quoted for the second cat, it will try to emit individual files named a and b. This can cause all kinds of problems since the user quoted the value passed to script and expected it to be handled as one word/unit.
Note that "$#" is a bit special since if you do script "a b" "c d" this expands to: cat "a b" "c d" in the script rather than cat "a b c d". The second line would do cat a b c d which tries to concatenate four different files rather than two files with spaces in the name.

How to keep quotes in Bash arguments? [duplicate]

This question already has answers here:
How can I preserve quotes in printing a bash script's arguments
(7 answers)
Closed 3 years ago.
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 re-use 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?
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?
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
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.
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.
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.)
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
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
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.
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! :)
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"
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"
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.
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.
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

Resources