6

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
  • 72,889
Amandasaurus
  • 1,256

4 Answers4

14

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,678
  • Worth clarifying that although bash, ksh and (I think) zsh support arrays, sh (and dash) don't? – Chris Davies 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 – Chris Davies 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.

Charles Duffy
  • 1,732
  • 15
  • 22
3

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
  • 138,973
  • 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
  • 670