8

Say I have a variable $ARGS which contains the following:

file1.txt "second file.txt" file3.txt

How can I pass the contents of $ARGS as arguments to a command (say cat $ARGS, for example), treating "second file.txt" as one argument and not splitting it into "second and file.txt"?

Ideally, I'd like to be able to pass arguments to any command exactly as they are stored in a variable (read from a text file, but I don't think that's pertinent).

Thanks!

  • 10
    Don't use a variable; use an array: `ARGS=( "file1.txt" "second file.txt" "file3.txt" )` and then pass it around: `"${ARGS[@]}"`. – Jonathan Leffler Sep 20 '16 at 04:30
  • If the settings are in a text file and the separator between arguments is a space, I don't see why you regard the behavior you are seeing as incorrect. I mean I can see what you would like to do, but then reading space-separated arguments from a text file is probably the wrong solution. Maybe use some sort of structured file format if your needs are complex. – tripleee Sep 20 '16 at 06:03

3 Answers3

9

It's possible to do this without either bash arrays or eval: This is one of the few places where the behavior of xargs without either -0 or -d extensions (a behavior which mostly creates bugs) is actually useful.

# this will print each argument on a different line
# ...note that it breaks with arguments containing literal newlines!
xargs printf '%s\n' <<<"$ARGS"

...or...

# this will emit arguments in a NUL-delimited stream
xargs printf '%s\0' <<<"$ARGS"

# in bash 4.4, you can read this into an array like so:
readarray -t -d '' args < <(xargs printf '%s\0' <<<"$ARGS")
yourprog "${args[@]}" # actually run your programs

# in bash 3.x or newer, it's just a bit longer:
args=( );
while IFS= read -r -d '' arg; do
    args+=( "$arg" )
done < <(xargs printf '%s\0' <<<"$ARGS")
yourprog "${args[@]}" # actually run your program

# in POSIX sh, you can't safely handle arguments with literal newlines
# ...but, barring that, can do it like this:
set --
while IFS= read -r arg; do
    set -- "$@" "$arg"
done < <(printf '%s\n' "$ARGS" | xargs printf '%s\n')
yourprog "$@" # actually run your program

...or, letting xargs itself do the invocation:

# this will call yourprog with ARGS given
# ...but -- beware! -- will cause bugs if there are more arguments than will fit on one
# ...command line invocation.
printf '%s\n' "$ARGS" | xargs yourprog
alecov
  • 4,882
  • 2
  • 29
  • 55
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
3

As mentioned by Jonathan Leffler you can do this with an array.

my_array=( "file1.txt" "second file.txt" "file3.txt" )
cat "${my_array[1]}"

An array's index starts at 0. So if you wanted to cat the first file in your array you would use the index number 0. "${my_array[0]}". If you wanted to run your command on all elements, replace the index number with @ or *. For instance instead of "${my_arryay[0]}" you would use "${my_array[@]}"Make sure you quote the array or it will treat any filename with spaces as separate files.

Alternatively if for some reason quoting the array is a problem, you can set IFS (which stands for Internal Field Separator) to equal a newline. If you do this, it's a good idea to save the default IFS to a variable before changing it so you can set it back to the way it was once the script completes. For instance:

# save IFS to a variable    
old_IFS=${IFS-$' \t\n'}
#set IFS to a newline
IFS='$\n'

# run your script
my_array=( "file1.txt" "second file.txt" "file3.txt" )
cat ${my_array[1]}

# restore IFS to its default state
IFS=$old_IFS

It's probably better to not mess around with IFS unless you have to. If you can quote the array to make your script work then you should do that.

For a much more in depth look into using arrays see:

I0_ol
  • 1,054
  • 1
  • 14
  • 28
  • Please consider using a source other than the ABS -- it's [rather notorious](http://wooledge.org/~greybot/meta/abs) for using bad practices in its examples. There's [the bash-hackers page on arrays](http://wiki.bash-hackers.org/syntax/arrays), [the BashGuide on arrays](http://mywiki.wooledge.org/BashGuide/Arrays), and [BashFAQ #5](http://mywiki.wooledge.org/BashFAQ/005). – Charles Duffy Sep 20 '16 at 15:32
  • 1
    Also, if your original IFS value is unset, the `old_IFS` shuffle you're doing here won't behave as you intend. Consider `old_IFS=${IFS-$' \t\n'}`, to store the default value in `old_IFS` if the original value is unset (as opposed to set-to-an-empty-string). – Charles Duffy Sep 20 '16 at 15:34
  • Thanks for the information. I have updated the answer. – I0_ol Sep 21 '16 at 00:00
  • BTW, `IFS='$\n'` is very different from `IFS=$'\n'`. In the former case, your `IFS` is three characters, `$`, the backslash, and `n`. In the latter, it's one character, a literal newline. – Charles Duffy Mar 15 '17 at 16:13
  • I stumbled here, found this answer, and was able to solve a long-standing bug. Why the myriad of other SO answers which change `IFS` don't caution about it restoring is baffling. Thankyou! – Anti Earth Nov 30 '19 at 15:53
  • @AntiEarth, not so baffling: the better-written subset of answers on Stack Overflow that advise changing IFS use formulations that don't _require_ you to restore the old value. For example, in `IFS=, read -r -a array <<<"$string_with_commas"` doesn't _require_ you to restore `IFS`, because the change is scoped to the `read` command and automatically undone when that command is complete. – Charles Duffy Feb 23 '22 at 22:40
  • @CharlesDuffy appreciate it. – I0_ol Feb 25 '22 at 04:36
0

Without bashisms, plain shell code might need an eval:

# make three temp files and list them.
cd /tmp ;  echo ho > ho ; echo ho ho > "ho ho" ; echo ha > ha ; 
A='ho "ho ho" ha' ; eval grep -n '.' $A

Output:

ho:1:ho
ho ho:1:ho ho
ha:1:ha

Note that eval is powerful, and if not used responsibly can lead to mischief...

Community
  • 1
  • 1
agc
  • 7,973
  • 2
  • 29
  • 50