5

What I'm trying to achieve is to read command line arguments from a file and invoke a command using them. So essentially I need to pass the arguments through a bash variable. The problem is that some arguments have spaces in them. How can I do that?

Not working code, to illustrate the problem:

file.txt contents (quotes are there just to show two different arguments):

"aaa bbb" "xxx yyy"

shell script:

ARGS=$(cat file.txt)
/some/command $ARGS

/some/command should receive two arguments: aaa bbb and xxx yyy. I do have control of generated file.txt and the shell script itself.

Smilediver
  • 1,738
  • 23
  • 26
  • i think that the file contents influence a lot the correct answer. Can you append some more data in file.txt (i.e a couple of more lines)? – George Vasiliou Feb 08 '17 at 12:03
  • Could you please clarify if you are intending `"aaa bbb" "xxx yyy"` to be one single argument passed (with literal quotes) to your command, or if this is supposed to be interpreted as two arguments with respective values `aaa bbb` and `xxx yyy`? – Fred Feb 08 '17 at 12:11
  • @GeorgeVasiliou As I've wrote, I have full control of generated file and script. I can write anything to them. – Smilediver Feb 08 '17 at 12:48
  • @Fred Quotes are there just to illustrate two different arguments. – Smilediver Feb 08 '17 at 12:49
  • You are starting from the false premise that this type of configuration file is a good idea; it is not. – chepner Feb 08 '17 at 14:37

2 Answers2

4

If you want to execute your command once for each line found in file.txt, so each line is a separate argument set, you can do this :

xargs /some/command <file.txt

The xargs utility takes each line it receives on standard input and uses its content as arguments to be provided to the command that is called. If the file contains only one line, it will work and execute the command only once.

The following solution does the same, but works with functions too:

while IFS= read -r line
do
  eval args=\("$line"\)
  command_or_function "${args[@]}"
done<file.txt

Please note that this uses eval, which means that if file.txt contains malicious content, arbitrary code execution could result. You must be 100% certain that the data contained in the file is safe.

The idea with this technique is that you explode each line into an array (one array element is one argument), and then use an array expansion ("${args[@]}") that expands to a list of all its elements, properly quoted (the quotes around the expansion are important here).

As an aside, the eval line could be replaced with :

declare -a args=\($line\)

But $line still gets expanded, so this is no safer than eval.

Fred
  • 6,590
  • 9
  • 20
  • The update fixed it cheers! but `eval` is not needed for this trivial task, could be fixed simply as in my answer. – Inian Feb 08 '17 at 11:52
  • @Inian Please explain which quotes get stripped? If you are talking about the array-based solution, try it : the array-splitting will obey quoting in the file (e.g. the sample data will create an array with two elements), and the array expansion will also produce two arguments to the command. – Fred Feb 08 '17 at 11:52
  • I meant for the logic in `xargs`, try `xargs echo < file.txt` – Inian Feb 08 '17 at 11:54
  • @Fred I think you don't need to use eval here. Most probably can be donw with (one of my beloved commands) : `readarray -t args <<<"$line"` – George Vasiliou Feb 08 '17 at 11:55
  • @GeorgeVasiliou `readarray` will work if there is no quoting involved in the data file, but my understanding is that, to the OP, `"aaa bbb" "xxx yyy"` means two arguments, one being "aaa bbb", and the other one being "xxx yyy", so readarray will not work there (does not handle quoting). – Fred Feb 08 '17 at 12:08
  • 1
    @Fred Yes, readarray will now work correctly, i test it. You still can avoid to use eval and avoid malicious code execution , by using `declare -a args=\(......\)`. I tested and has the same result. – George Vasiliou Feb 08 '17 at 13:51
  • @GeorgeVasiliou I got it to work with `declare -a args=\($line\)`. I routinely use this kind of declaration (or `printf -v`) precisely to avoid using `eval`, but not for arrays with a single-variable expanding to a string with quotes inside. I am not sure it sidesteps code injection issues, will look into it and update my answer when I am confident about it. Thanks for the idea! – Fred Feb 08 '17 at 14:09
  • 1
    @Fred Welcome :-) To be honest was a proposal by a high rated guy in SO (under a different question) just to avoid using eval . I also tried `declare -a args +=\(...\)` for appending array and still works fine in my tests. – George Vasiliou Feb 08 '17 at 14:31
  • @GeorgeVasiliou In `file.txt`, try putting a line containing `$(ls /)`. Both with `eval` and with `declare`, you will be able to see `ls` is executed. `declare` does not protect you from code injection, because the content of the variable is expanded. To avoid that while still allowing quotes in the file, what would be required is a way to block expansion while still allowing word splitting. – Fred Feb 08 '17 at 15:53
  • @FRed Really? Oh god, i never tried to execute hidden code with eval and/or declare. I thought declare would do just what it's name dictates: declare a variable. OK, good to know since you tested this! – George Vasiliou Feb 08 '17 at 15:58
2

Use command substitution, to expand the file contents to the command,

/some/command "$(<file.txt)"

As an example,

cat file
"aaa bbb" "xxx yyy"

using printf on it INCORRECTLY with cat will produce

printf "%s\n" $(cat file)
"aaa
bbb"
"xxx
yyy"

With proper quoting present, arguments are sent as such without getting split.

printf "%s\n" "$(<file)"
"aaa bbb" "xxx yyy"
Inian
  • 80,270
  • 14
  • 142
  • 161
  • OP wants to pass arguments containing spaces, but I think he also wants to enclose arguments in the data file with quotes (therefore allowing many arguments on the line). My understanding is therefore that double quotes are not part of the argument values, only separators (like in a normal command). Your method passes the quotes as part of one big string for the whole line, there is no splitting involved. – Fred Feb 08 '17 at 12:04
  • @Fred: The fact `he also wants to enclose arguments in the data file with quotes (therefore allowing many arguments on the line).` is just a plain assumption which OP has never suggested, please stick to the requirement that is plain enough – Inian Feb 08 '17 at 12:06
  • @Fred: Would you be good enough to remove your comment `It does not really work` – Inian Feb 08 '17 at 12:42