How to pass bash script arguments to a subshell

  • Ralf Holly

    I have a wrapper script that does some work and then passes the original parameters on to another tool:

    # ...
    other_tool -a -b "$@"

    This works fine, unless the "other tool" is run in a subshell:

    # ...
    bash -c "other_tool -a -b $@"

    If I call my wrapper script like this: -x "blah blup"

    then, only the first orginal argument (-x) is handed to "other_tool". In reality, I do not create a subshell, but pass the original arguments to a shell on an Android phone, which shouldn't make any difference:

    # ...
    adb sh -c "other_tool -a -b $@"
  • Answers
  • Gordon Davisson

    Bash's printf command has a feature that'll quote/escape/whatever a string, so as long as both the parent and subshell are actually bash, this should work:

    quoted_args="$(printf " %q" "$@")" # Note: this will have a leading space before the first arg
    # echo "Quoted args:$quoted_args" # Uncomment this to see what it's doing
    bash -c "other_tool -a -b$quoted_args"

    Note that you can also do it in a single line: bash -c "other_tool -a -b$(printf " %q" "$@")"

  • Daniel Andersson

    Change $@ to $*. I did a small local test and it works in my case.

    bash -c "echo $*"
    bash -c "echo $@"

    Saving as and making it executable gives

    $ ./ foo bar
    foo bar

    There is a subtle difference between $* and $@, as you can see. See e.g.

    For the follow-up question in the comments: you need to escape e.g. white-space "twice" to pass a string with a separator as a combined argument, e.g. with modified to a wc wrapper:

    bash -c "wc $*"

    This works:

    $ touch test\ file
    $ ./ -l "test\ file"
    0 test file


    $ ./ -l "test file"
    wc: test: No such file or directory
    wc: file: No such file or directory
    0 total
  • glenn jackman

    It's failing because you're coercing an array (the positional parameters) into a string. "$@" is magical because it gives you each separate paramter as a properly quoted string. Adding additional text breaks the magic: "blah $@" is just a single string.

    This may get you closer:

    cmd="other_tool -a -b"
    for parm in "$@"; do cmd+=" '$parm'"; done
    adb sh -c "$cmd"

    Of course, any parameter that contains a single quote will cause trouble.

  • jcayzac

    None of the solutions work well. Just pass x/\ \ \"b\"/aaaaa/\'xxx\ yyyy\'/zz\"offf\" as parameter and they fail.

    Here is a simple wrapper that handles every case. Note how it escapes each argument twice.

    #!/usr/bin/env bash
    declare -a ARGS
    for ((INDEX=0; INDEX<COUNT; ++INDEX))
        ARG="$(printf "%q" "$1")"
        ARGS[INDEX]="$(printf "%q" "$ARG")"
    ls -l ${ARGS[*]}

  • Related Question

    Writing this Bash Script to accept Arguments?
  • Urda

    How would I go about converting this bash script:

    mkdir /store/sftp/%USERNAME%
    sudo useradd -d /incoming %USERNAME%
    sudo passwd %USERNAME%
    ## Password needs to be typed or passed in here
    sudo usermod -g sftp %USERNAME%
    sudo usermod -s /bin/false %USERNAME%
    sudo chmod 755 /store/sftp/%USERNAME%
    sudo chown root:root /store/sftp/%USERNAME%
    sudo mkdir /store/sftp/%USERNAME%/incoming
    sudo chown %USERNAME%:sftp /store/sftp/%USERNAME%/incoming

    To accept a username and a password?

  • Related Answers
  • petersohn

    First, you should avoid that lots of sudo calls. Instead, you should run the script with sudo. The final version would look like this:

    # first check for root user
    if [ ! $UID -eq 0 ]; then
        echo "This script must be run as root."
        exit 1
    # check if the user provided an argument
    if [ -z $1 ]; then
        echo "No username provided. Usage: $0 username"
        exit 2
    username=$1 # set the first argument as the username
    mkdir "/store/sftp/$username"
    useradd -d /incoming "$username"
    passwd "$username"
    ## Password needs to be typed or passed in here
    usermod -g sftp "$username"
    usermod -s /bin/false "$username"
    chmod 755 "/store/sftp/$username"
    mkdir "/store/sftp/$username/incoming"
    chown "$username:sftp" "/store/sftp/$username/incoming"

    Quoting is necessary because the user name may contain spaces (but sure, it usually doesn't).

    Edited to work.