Jon
Jon

Reputation: 2133

Passing arguments to bash -c with spaces

I have the command line arguments to a program stored in a bash array and one of the arguments can have spaces in it. X=(A B "C D" E)

I have another constraint that I need to execute my program through a bash -c statement. (Technically it is a ... | xargs bash -c "./a.out ..." statement)

When I do this, I end up mangling the spaces in my command. How can I do this correctly?

I wrote a quick c++ program to output command line arguments to represent my program

#include <iostream>
int main(int argc, char** argv)
{
    for (int i = 1; i < argc; ++i)
        std::cout << '"' << argv[i] << '"' << std::endl;
    return 0;
}

I am looking to get the output from ./a.out "${X[@]}"

"A"
"B"
"C D"
"E"

but when I test this bash -c "./a.out ${X[@]}" I just get just get

"A"

and when I test this bash -c "./a.out ${X[*]}" it mangles the spaces and I get

"A"
"B"
"C"
"D"
"E"

What can Ido?

Upvotes: 2

Views: 1353

Answers (1)

Charles Duffy
Charles Duffy

Reputation: 295659

The Right Way(s)

Passing data out-of-band from code

Since you're using xargs, consider passing data through the argument list of the shell itself, not inside the script passed with -c:

xargs bash -c './a.out "$@"' _

Because the "$@" is itself in single quotes, it isn't expanded by the parent shell, but instead by the child shell that's passed other arguments by xargs. (The _ is there to act as a placeholder for $0).

This is actually a best practice even in cases where the number of arguments is fixed: It the potential for shell injection attacks via malicious data substituted directly into your script text.


Using printf %q to create eval-safe content

ksh, bash, and zsh include a %q operator to printf which quotes content in an eval-safe manner:

printf -v out_str '%q ' "${X[@]}"
bash -c "./a.out $out_str"

Why The Wrong Ways Didn't Work

bash -c './a.out "${X[@]}"'

When "${foo[@]}" is expanded inside a string, the result is an array with contents before that expansion prepended to the first element, additional elements prior to the last one emitted standalone, and any contents of the string after the last one appended to the last element. Thus bash -c "./a.out begin${X[@]}end" expands to:

bash -c "./a.out beginA" "B" "C D" "Eend"

...thus, B, C D and E are all passed to your shell, but outside of the -c argument; they can be accessed with "$@", or looking at $1, $2, etc.


bash -c './a.out ${X[*]}'

By contrast, with "${foo[*]}, the result is the first character of IFS (by default, a simple space) being substituted between each array element. Thus:

bash -c "./a.out ${X[*]}"

...becomes...

bash -c "./a.out A B C D E"

...such that the space literal between C and D becomes indistinguishable from the literal spaces placed between other characters by the expansion process.

Upvotes: 4

Related Questions