How to pass bash script arguments to a subshell

24
2014-04
  • Ralf Holly

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

    #!/bin/bash
    # ...
    other_tool -a -b "$@"
    

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

    #!/bin/bash
    # ...
    bash -c "other_tool -a -b $@"
    

    If I call my wrapper script like this:

    wrapper.sh -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:

    #!/bin/bash
    # ...
    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:

    #!/bin/bash
    
    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.

    #!/bin/sh
    bash -c "echo $*"
    bash -c "echo $@"
    

    Saving as test.sh and making it executable gives

    $ ./test.sh foo bar
    foo bar
    foo
    

    There is a subtle difference between $* and $@, as you can see. See e.g. http://ss64.com/bash/syntax-parameters.html


    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 test.sh modified to a wc wrapper:

    #!/bin/sh
    bash -c "wc $*"
    

    This works:

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

    but:

    $ ./test.sh -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
    COUNT=$#
    for ((INDEX=0; INDEX<COUNT; ++INDEX))
    do
        ARG="$(printf "%q" "$1")"
        ARGS[INDEX]="$(printf "%q" "$ARG")"
        shift
    done
    
    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:

    #!/bin/bash
    
    # first check for root user
    if [ ! $UID -eq 0 ]; then
        echo "This script must be run as root."
        exit 1
    fi
    
    # check if the user provided an argument
    if [ -z $1 ]; then
        echo "No username provided. Usage: $0 username"
        exit 2
    fi 
    
    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.