bashprintfshellcheck

resolving shellcheck printf warnings have me stuck with either SC2059 or SC2089


For the following code, I'm getting SC2089 "Quotes/backslashes will be treated literally, use an array"

# Quotes/backslashes will be treated literally, use an array
CMD_AVAIL_MSG='%s is required for this script but the "%s" command is missing. Exiting...'
if [ -z "$(which valet)" ]; then
    printf -v err $CMD_AVAIL_MSG "Laravel Valet" "valet";
    echo $err; # there's other stuff I do here, but using echo for conciseness
    exit 1;
fi

However, converting this to an array gives me shellcheck warning SC2059 "Don't use variables in the printf format string, use '...%s...' $foo"

CMD_AVAIL_MSG=("%s" 'is required for this script but the "' '%s' '" command is missing. Exiting...')
if [ -z "$(which valet)" ]; then
    # Don't use variables in the printf format string, use '...%s...' "$foo"
    printf -v err "${CMD_AVAIL_MSG[@]}" "Laravel Valet" "valet";
    echo $err; # there's other stuff I do here, but using echo for conciseness
    exit 1;
fi

How do I resolve both of these warnings?


Solution

  • Warning SC2059 is intended to prevent people from passing data in the format-string position. You aren't doing that (your "data" is in fact a format string), so it isn't intended for you and can be safely disabled.

    On the other hand, you are still misusing printf: Because each invocation of printf accepts only a single format string, CMD_AVAIL_MSG should be a regular string, not an array. (It also should be lowercase; all-caps names are reserved by POSIX-specified convention for variables that reflect or modify behavior of the shell or other POSIX-specified tools).

    cmd_avail_msg='"%s" is required for this script but the "%s" command is missing. Exiting...'
    if ! command -v valet >/dev/null 2>&1; then
        # shellcheck disable=SC2059
        printf -v err "$cmd_avail_msg" 'Laravel Valet' valet
        echo "$err" >&2
        exit 1;
    fi
    

    Note the use of if ! command -v valet >/dev/null 2>&1 instead of if [ -z "$(which valet)" ]. This is both more efficient and more portable: which is a separate executable being run as a subprocess, whereas command -v is a POSIX-specified command internal to the shell itself.


    The above being said, this looks like a place where you can, and perhaps should, use a function instead of a variable with a format string -- that way you can reuse the entire code block, not just the single message.

    check_for_cmd() {
      local cmd=$1 name=$2 err
      command -v "$cmd" >/dev/null 2>&1 && return 0
      printf -v err \
        '"%s" is required for this script but the "%s" command is missing. Exiting...' \
        "$name" "$cmd"
      # do other stuff here
      echo "$err" >&2
      exit 1
    }
    
    check_for_cmd valet "Laravel Valet"
    check_for_cmd other "Something Else"