5

In a bash script, I call another programme, but I want to configure that programme with a command line option. The following works:

AREA_ARG=""
if __SOME_SETTING__ ; then
  AREA_ARG=" --area us,ca "
fi

process_data -i /some/path $AREA_ARG

i.e. bash either executes process_data -i /some/path, or process_data -i /some/path --area us,ca .

However shellcheck complains!

$ shellcheck test.sh 

In test.sh line 7:
process_data -i /some/path $AREA_ARG
                           ^-------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean: 
process_data -i /some/path "$AREA_ARG"

For more information:
  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...

I understand the principle, but I want/need the variable to split on the space so that process_data gets 2 arguments.

What's the Proper Way™ to do this in bash?

muru
  • 69,900
  • 13
  • 192
  • 292
Amandasaurus
  • 1,186
  • 2
  • 12
  • 21

4 Answers4

13

Use arrays

Here is your code re-written using arrays. Also to be a working example (ls in place of your command), and using correct case for variables (coding standard says that capitalised names are reserved for system).

#!/bin/bash

area_args=()
if true ; then
  area_args=(-l -a)
fi

ls "${area_args[@]}"
Toby Speight
  • 8,460
  • 3
  • 26
  • 50
ctrl-alt-delor
  • 27,473
  • 9
  • 58
  • 102
  • Worth clarifying that although `bash`, `ksh` and (I think) `zsh` support arrays, `sh` (and `dash`) don't? – roaima May 18 '22 at 09:56
  • 2
    @roaima, ...well, `sh` supports _exactly one_ array in each context, the argument list. To use it here might be something like: `set --` as an equivalent to `area_args=( )`, and `set -- -l -a` vs `areas_args=( -l -a )`, then `ls "$@"` to actually pass them through. – Charles Duffy May 18 '22 at 18:44
  • @roaima, `zsh` supported arrays long before bash, that `array=(foo bar)` is actually from zsh. csh (the first in the late 70s), tcsh, rc, es, yash, fish all support arrays / lists. Shells that don't are the exception. Arrays / lists should be the primary data type in shells (and is in many) as the main thing a shell is meant to do is run commands with their list of arguments. The main reason why POSIX didn't specify arrays for sh is (I believe) because it's based on ksh, and the ksh array design (which bash copied unfortunately) is by far the worst. – Stéphane Chazelas May 18 '22 at 19:43
  • @Stéphane thank you for the info. I'm not (yet) a `zsh` user so my comment could only be definitive about the shells I know. The question is tagged `bash` so there's nothing wrong in expecting an array type to be present; I thought it was worth clarifying which common shells do/don't support it – roaima May 18 '22 at 22:02
6

For your specific use case, there's a much easier answer:

unset -v area
if __SOME_SETTING__; then
  area="ca,us"
fi

process_data -i /some/path ${area+ --area "$area" }

If for some reason you can't use arrays, and you need more control than the above practice offers, you might consider using a function wrapper.

withOptionalArea() {
  if __SOME_SETTING__; then
    "$@" --area us,ca
  else
    "$@"
  fi
}

withOptionalArea process_data -i /some/path

...but this should only be necessary in exceptional circumstances.


Really, use an array.

Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
Charles Duffy
  • 1,651
  • 15
  • 22
2

The Proper Way for storing arbitrary commands or arguments in a variable would be to use an array, see How can we run a command stored in a variable?

But you can just tell shellcheck you like it just the way it is, as long as you're sure it's ok (i.e. you don't have whitespace that should stay intact, or glob characters that could cause issues, and didn't change IFS to something that would trash this):

AREA_ARG=""
if __SOME_SETTING__ ; then
  AREA_ARG=" --area us,ca "
fi


# shellcheck disable=SC2086 # split on purpose
process_data -i /some/path $AREA_ARG

See https://www.shellcheck.net/wiki/Directive

ilkkachu
  • 133,243
  • 15
  • 236
  • 397
  • 1
    You also need to make sure that at the time `process_data -i /some/path $AREA_ARG` is run, `$IFS` contains the space character and none of the characters of `$AREA_ARG` that you don't want it to be split on. You may want to do something like `(IFS=' '; set -o noglob; exec process_data -i /some/path $AREA_ARG)` as an approximation of what you'd do with languages with proper splitting operators (like `zsh`'s `${(s[ ])AREA_ARG}`). – Stéphane Chazelas May 19 '22 at 05:48
2

Many argument parsing libraries let you use an = sign to join a long option name and its value. (This includes Gnu libc's parsers and Python's standard argparse library.) If you're calling such a program, and you have no need for more generality, you can use:

AREA_ARG=--area=us,ca
cjs
  • 640
  • 6
  • 14