How to substitute quoted, multi-word strings as ar

2020-01-28 19:30发布

问题:

I'm trying to substitute a string variable, containing multiple quoted words, as a parameter to a command.

Thus, given the following example script (Note the -x in the shebang, which causes the output to be logged to stderr),

#!/bin/bash -x

myArg="\"hello\" \"world\""
echo "string is:" $myArg

exit

Which gives us,

+ myArg='"hello" "world"'
+ echo 'string is:' '"hello"' '"world"'
string is: "hello" "world"
+ exit


Line two shows what is actually passed to the command; bash has added single quotes to each word in the string. If I instead, thusly, quote "$myArg", the same occurs but for the whole string rather than each word.

Now, imagine that instead of echo, we are passing the string to a program where some of the arguments need to be quoted patterns, such as "*" (which mustn't be expanded by the shell).

To clarify, I don't want the single quotes added at all during the expansion. How might I achieve this?

回答1:

Don't use quotes, use an array (see BashFAQ #050):

$ myArgs=("hello" "world" "multiword arg with * ?")
+ myArgs=("hello" "world" "multiword arg with * ?")
$ echo "${myArgs[@]}"
+ echo hello world 'multiword arg with * ?'
hello world multiword arg with * ?

If it really needs to be in the form of quoted strings within a string, you're either going to have to use something like eval "echo $myArg" (which can cause some really nasty bugs, if you aren't careful) or parse it yourself (which is going to be difficult).



回答2:

If you want to pass a variable value as a parameter (99% of cases on SO), simply use proper quoting:

arg="foo bar"
command "$arg"

If you want to pass several arguments, use arrays:

args=("foo bar" "baz ban" bay)
command "${args[@]}"


回答3:

I don't think it is doing what you think it is doing.

[~]$ myArg="\"hello\" \"world\""
[~]$ echo "string is:" $myArg
string is: "hello" "world"

I see no extra quotes of any kind- echo gets three argument strings.

[~]$ cargs(){ echo $#; }
[~]$ cargs "string is:" $myArg
3

Bash will expand the variable first, so

cargs "string is:" $myArg

becomes (though without the literal backslashes- this is why string escaping is a PITA)

cargs "string is:" "\"hello\"" "\"world\""

And the args array is:

0x00:string is:0
0x0B:"hello"0
0x13:"world"0
0x1B:0

Now, if you add the *, or glob path expansion in one of those, Bash will at this point expand it, unless you escape it, or use single quotes in your literal command.

[~]$ cargs "string is:" $myArg *
19
[~]$ cargs "string is:" $myArg "\*"
4
[~]$ cargs "string is:" $myArg '*'
4


回答4:

There is a portable way to split expand a variable but keep spaces. Bash arrays are not needed. Dash (Ubuntu's /bin/sh) would work too.

Use some character to separate arguments that is definitely not used inside the arguments. The below example uses semicolon, but it could be a newline or another character. Change the IFS variable to a newline temporarily when the list of arguments is expanded. Restore IFS to the original value as soon as possible, even if it means doing it in the loop. If the loop is not guaranteed to run at least once, do it after the loop as well.

#! /bin/sh
arg_list='hello world;have a nice day'
save_IFS="$IFS"
IFS=';'
for i in $arg_list; do
  IFS="$save_IFS"
  echo "$i"
done
IFS="$save_IFS"

Note that every expanded argument is printed individually.

$ ./test.sh
hello world
have a nice day