1

So I have this:

export ti_arg='';

if [[ -t 1 ]] ; then
   # allow us to kill container if attached to terminal
   export ti_arg='-ti'
fi

(
  cd "$(dirname "$BASH_SOURCE")"


  docker build <...> '.'


  docker run "$ti_arg" --rm  "test-$commit_id"
)

the problem is that even if $ti_arg is empty, the shell still sees it as an argument, and I get:

docker : invalid reference format

The only solution I can think of is and if/else to run basically the same docker command except for that one arg, or to find some pass through argument that I can use with docker, like this:

export ti_arg='--noop';

if [[ -t 1 ]] ; then
   # allow us to kill container if attached to terminal
   export ti_arg='-ti'
fi

anyone have a good solution? I guess a CLI design would always provide a --noop flag for this use case ...now go update all your own CLI tools

2 Answers2

5

There are a number of ways to handle it. The basic way is to use the ability of the shell to test for a parameter being set. so you could have

docker run ${ti_arg:+"$ti_arg"} --rm  "test-$commit_id"

This tests to see if ti_arg is set, and if so puts in the "$ti_arg".

Of course starting the script with export ti_arg='' is not the best, this explicitly says that the value of the parameter is the empty string. Using unset ti_arg to say the value is unset would be better, although it would not solve the problem in this case. It would allow you however to use ${ti_arg+"$ti_arg"} (note the lack of :) to better represent the case where you do not have the value set.

A trick using "$@"

In the thankfully long ago past, people would use

subprog ${1:"$@"}

in their scripts to handle the expansion of parameters to work around essentially just this problem. All modern implementations of Bourne like shells have this bug fixed, so you can just say "$@" (with the double quotes) and it expands correctly to nothing if there are no values set. So instead of using a named variable, you could use

set -- # Clear the $@ array
if [[ -t 1 ]] ; then
    set -- "-ti"
fi
...
docker run "$@" --rm  ....

Sidenote for C programmers

For people familiar with C, the difference between a variable being unset and it being set to an empty string is like

  1. char *var = NULL; /* unset case. var is 4 or 8 bytes all of which are 0 */
  2. char *var = strdup(""); /* set to the empty string. var is still 4 or 8 8 bytes, but now contains the address of an item in the heap, which could be as small as 1 byte. The first byte of this heap item will be 0, to indicate the end of string */
icarus
  • 17,920
  • since there is no whitespace, we can do this without quotes: docker run $ti_arg --rm ... – Alexander Mills May 04 '20 at 01:28
  • @AlexanderMills since there is no whitespace and no shell meta characters you can do this without quotes. In this case you know that ti_arg is either empty or has "-ti" in it and so you are fine. Once you allow the user to provide the contents you should always use quotes. It is simpler to have a rule always use quotes than to have one always use quotes unless you know you don't need to! – icarus May 04 '20 at 02:54
4

You can push the item into an array and use array substitution. This has the wonderful advantage that an array with no elements gets optimised away completely. Here's a modified part of your own code:

(
  cd "$(dirname "$BASH_SOURCE")"

  docker build --build-arg commit_id -t "test-$commit_id" .

  set --
  test -n "$ti_arg" && set -- "$ti_arg"

  docker run "$@" --rm  "test-$commit_id"
)

If you're using bash you can avoid using $@ and simplify things a litle more

(
  cd "${BASH_SOURCE%/*}"

  docker build --build-arg commit_id -t "test-$commit_id" .

  args=()
  [[ -n "$ti_arg" ]] && args+=("$ti_arg")

  docker run "${args[@]}" --rm "test-$commit_id"
)

Both these solutions double-quote "$ti_arg" so that its protected against any inadvertent shell evaluation. In this particular case you could probably also simply leave $ti_arg unquoted in the final docker run so that the shell either intepolates -ti or just evaluates it away, but as a general solution it could break given unexpected input either to the variable or its context (including IFS), which is why I prefer the more complex and complete solution above.

docker run $ti_arg --rm "test-$commit_id"
Chris Davies
  • 116,213
  • 16
  • 160
  • 287
  • Leaving $ti_arg unquoted wouldn't work in contexts where $IFS contains -, t or i. – Stéphane Chazelas May 01 '20 at 16:36
  • (except for $BASH_SOURCE) your bash version would also work in zsh (where that syntax come from), ksh93 and mksh – Stéphane Chazelas May 01 '20 at 16:38
  • Hi @StéphaneChazelas, (1) Leaving $ti_arg unquoted... You're right and I probably should have tried to include more exceptions; I'll adjust my "unexpected input" to include IFS. (2) The OP references $BASH_SOURCE and without being able to see the remainder of their script I thought it best not to try and replace it. – Chris Davies May 01 '20 at 16:52