12

I'm encountering an issue passing an argument to a command in a Bash script.

poc.sh:

#!/bin/bash

ARGS='"hi there" test'
./swap ${ARGS}

swap:

#!/bin/sh
echo "${2}" "${1}"

The current output is:

there" "hi

Changing only poc.sh (as I believe swap does what I want it to correctly), how do I get poc.sh to pass "hi there" and test as two arguments, with "hi there" having no quotes around it?

codeforester
  • 39,467
  • 16
  • 112
  • 140
Joel T.
  • 121
  • 1
  • 3
  • This is the topic of BashFAQ #50: http://mywiki.wooledge.org/BashFAQ/050 – Charles Duffy Sep 26 '14 at 21:22
  • inverse operation: [Correctly quote array that is being passed indirectly via another command](https://unix.stackexchange.com/questions/518012/correctly-quote-array-that-is-being-passed-indirectly-via-another-command) – milahu Apr 19 '22 at 16:42

4 Answers4

24

A Few Introductory Words

If at all possible, don't use shell-quoted strings as an input format.

  • It's hard to parse consistently: Different shells have different extensions, and different non-shell implementations implement different subsets (see the deltas between shlex and xargs below).
  • It's hard to programmatically generate. ksh and bash have printf '%q', which will generate a shell-quoted string with contents of an arbitrary variable, but no equivalent exists to this in the POSIX sh standard.
  • It's easy to parse badly. Many folks consuming this format use eval, which has substantial security concerns.

NUL-delimited streams are a far better practice, as they can accurately represent any possible shell array or argument list with no ambiguity whatsoever.


xargs, with bashisms

If you're getting your argument list from a human-generated input source using shell quoting, you might consider using xargs to parse it. Consider:

array=( )
while IFS= read -r -d ''; do
  array+=( "$REPLY" )
done < <(xargs printf '%s\0' <<<"$ARGS")

swap "${array[@]}"

...will put the parsed content of $ARGS into the array array. If you wanted to read from a file instead, substitute <filename for <<<"$ARGS".


xargs, POSIX-compliant

If you're trying to write code compliant with POSIX sh, this gets trickier. (I'm going to assume file input here for reduced complexity):

# This does not work with entries containing literal newlines; you need bash for that.
run_with_args() {
  while IFS= read -r entry; do
    set -- "$@" "$entry"
  done
  "$@"
}
xargs printf '%s\n' <argfile | run_with_args ./swap

These approaches are safer than running xargs ./swap <argfile inasmuch as it will throw an error if there are more or longer arguments than can be accommodated, rather than running excess arguments as separate commands.


Python shlex -- rather than xargs -- with bashisms

If you need more accurate POSIX sh parsing than xargs implements, consider using the Python shlex module instead:

shlex_split() {
  python -c '
import shlex, sys
for item in shlex.split(sys.stdin.read()):
    sys.stdout.write(item + "\0")
'
}
while IFS= read -r -d ''; do
  array+=( "$REPLY" )
done < <(shlex_split <<<"$ARGS")
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • This doesn't seem to work with `ARGS="\"a\\\"b\" c"`? Error reported *xargs: unmatched double quote; by default quotes are special to xargs unless you use the -0 option* – Cyker Dec 25 '16 at 22:03
  • Fair point -- `xargs` indeed doesn't honor POSIX sh behavior in that case. Updating with an alternative using the Python 2's `shlex.split()` function instead. – Charles Duffy Dec 26 '16 at 18:51
  • The best thing to do would be to put the OP back on the right track: if OP needs such a thing, then their design is obviously wrong. Unless OP is writing a shell (or a cli), in which case the choice of language is wrong. Either way, OP is doing it wrong. – gniourf_gniourf Dec 26 '16 at 19:03
  • @gniourf_gniourf, ...in theory, I agree. In practice, folks tend to be given requirements that don't make sense (or need compatibility with longstanding systems/practices -- there are a *ton* of legacy startup scripts for Java apps floating around that rely on `eval`ing argument lists given in shell-quoted form), and there's value in being able to make the best of a bad situation... though certainly, "don't do that" ought to be part of the advice given. – Charles Duffy Dec 26 '16 at 19:06
  • 1
    @gniourf_gniourf, ...I've edited in an appropriate introduction. – Charles Duffy Dec 26 '16 at 19:09
  • @CharlesDuffy curious what you would do if the command (`./swap` in the above example) needed to read from stdin as well, as you have captured it for `run_with_args`? – deitch Jul 24 '18 at 19:40
  • @deitch, the easy thing is to use a different FD. `3< <(...)` will feed the input on FD 3 rather than 0, then one can `IFS= read -r entry <&3`. With bash 4.1 or later, FD numbers can be dynamically assigned and used from variables (thus also passed on command line arguments &c), making this even easier. – Charles Duffy Jul 24 '18 at 19:52
  • Oh, I didn't think of that. Nice. I am using posix sh, so the bashism won't work. I ended up doing a variant on your solution, see this gist https://gist.github.com/deitch/e93f3bab0f1d535f9b8bcec8459f7f79 – deitch Jul 25 '18 at 04:12
  • That's buggy, unfortunately, but almost necessarily so with POSIX sh. `args="$@"` is storing your array in a string, so it necessarily loses information about argument boundaries. See `OPTS='a "*" d'` for a pathological case. – Charles Duffy Jul 25 '18 at 12:08
  • (If you *intend* to collapse an argument list to a string, better to use `args="$*"` to be more explicit about that intent). – Charles Duffy Jul 25 '18 at 12:13
  • ...Similarly, `OPTS='-name "*.txt"'` will differ in its behavior based on which files are in your local directory and locally set shell options (any equivalent to `nullglob` or `failglob` will cause the `*.txt` to be removed from the option list entirely). – Charles Duffy Jul 25 '18 at 12:14
4

Embedded quotes do not protect whitespace; they are treated literally. Use an array in bash:

args=( "hi there" test)
./swap "${args[@]}"

In POSIX shell, you are stuck using eval (which is why most shells support arrays).

args='"hi there" test'
eval "./swap $args"

As usual, be very sure you know the contents of $args and understand how the resulting string will be parsed before using eval.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • 1
    I would tend to argue that there are more options that `eval` here. Even in POSIX, one could read into `"$@"` from xargs. – Charles Duffy Jul 17 '15 at 23:07
2

Ugly Idea Alert: Pure Bash Function

Here's a quoted-string parser written in pure bash (what terrible fun)!

Caveat: just like the xargs example above, this errors in the case of an escaped quote. This could be fixed... but much better to do in an actual programming language.

Example Usage

MY_ARGS="foo 'bar baz' qux * "'$(dangerous)'" sudo ls -lah"

# Create array from multi-line string
IFS=$'\r\n' GLOBIGNORE='*' args=($(parseargs "$MY_ARGS"))

# Show each of the arguments array
for arg in "${args[@]}"; do
    echo "$arg"
done

Example Output

foo
bar baz
qux
*

Parse Argument Function

This literally goes character-by-character and either adds to the current string or the current array.

set -u
set -e

# ParseArgs will parse a string that contains quoted strings the same as bash does
# (same as most other *nix shells do). This is secure in the sense that it doesn't do any
# executing or interpreting. However, it also doesn't do any escaping, so you shouldn't pass
# these strings to shells without escaping them.
parseargs() {
    notquote="-"
    str=$1
    declare -a args=()
    s=""

    # Strip leading space, then trailing space, then end with space.
    str="${str## }"
    str="${str%% }"
    str+=" "

    last_quote="${notquote}"
    is_space=""
    n=$(( ${#str} - 1 ))

    for ((i=0;i<=$n;i+=1)); do
        c="${str:$i:1}"

        # If we're ending a quote, break out and skip this character
        if [ "$c" == "$last_quote" ]; then
            last_quote=$notquote
            continue
        fi

        # If we're in a quote, count this character
        if [ "$last_quote" != "$notquote" ]; then
            s+=$c
            continue
        fi

        # If we encounter a quote, enter it and skip this character
        if [ "$c" == "'" ] || [ "$c" == '"' ]; then
            is_space=""
            last_quote=$c
            continue
        fi

        # If it's a space, store the string
        re="[[:space:]]+" # must be used as a var, not a literal
        if [[ $c =~ $re ]]; then
            if [ "0" == "$i" ] || [ -n "$is_space" ]; then
                echo continue $i $is_space
                continue
            fi
            is_space="true"
            args+=("$s")
            s=""
            continue
        fi

        is_space=""
        s+="$c"
    done

    if [ "$last_quote" != "$notquote" ]; then
        >&2 echo "error: quote not terminated"
        return 1
    fi

    for arg in "${args[@]}"; do
        echo "$arg"
    done
    return 0
}

I may or may not keep this updated at:

Seems like a rather stupid thing to do... but I had the itch... oh well.

coolaj86
  • 74,004
  • 20
  • 105
  • 125
0

This might not be the most robust approach, but it is simple, and seems to work for your case:

## demonstration matching the question
$ ( ARGS='"hi there" test' ; ./swap ${ARGS} )
there" "hi

## simple solution, using 'xargs'
$ ( ARGS='"hi there" test' ; echo ${ARGS} |xargs ./swap )
test hi there
Brent Bradburn
  • 51,587
  • 17
  • 154
  • 173
  • Commentary: Bash string processing is extremely confusing. `xargs` removes the guesswork by passing parameters directly via [`exec`](https://en.wikipedia.org/wiki/Exec_(system_call)), which bypasses the normal, capricious, string processing that Bash would otherwise perform as part of string-based command-line processing. – Brent Bradburn Apr 01 '17 at 18:13
  • 2
    I can't be sure about my assertion regarding `exec`. But I stand by my statement that **"Bash string processing is extremely confusing"**. – Brent Bradburn Apr 01 '17 at 18:23
  • 1
    The processing done by `xargs` (when used without `-0` or the GNU extension `-d`) is _also_ fairly capricious, and while it's mostly shell-compatible, it's not 100% so. Also, `echo ${XARGS}` [has its own bugs](https://stackoverflow.com/questions/29378566), which vary depending on exactly which version of `echo` you're using; `printf '%s\n' "$XARGS"` [is more reliable](https://unix.stackexchange.com/questions/65803/why-is-printf-better-than-echo). – Charles Duffy Jan 28 '22 at 22:07
  • ("not 100% so" -- f/e, a backslash before a newline in xargs causes an error, whereas in shell it either makes the newline be ignored or makes it literal data, depending on context) – Charles Duffy Jan 28 '22 at 22:14