bashshellargumentsposixvariable-assignment

Is there a way to store "$@" into a variable in pure POSIX shell?


When I do something like ARGS="$@", the shell just concatenates the arguments stored in "$@". Is there a way to store "$@" into a variable in pure POSIX shell to be able to use it to call another program or function with the passed arguments?


Solution

  • The only array-like structure in a POSIX shell is the $@ list whose elements can be accessed independently with $1,$2,$3,... The number of elements available is given by $#.

    $@ elements can be modified using set and shift.

    The top-level and each function call has its own separate $@ array which is initialised to the arguments received by the script/function when it is called. Using set or shift inside a function does not affect the top-level $@.

    It is possible to save all the original elements of $@ somewhere but I don't believe there is way to use the result in a form as simple as bash's "${arr[@]}" syntax. (Individual elements may accessed without too much effort but not, trivially, the array as a whole.)

    However, by appropriately reloading/manipulating the elements of $@ it can be used directly, although performing the manipulation is likely to be rather tedious.

    A quick search for ways to accomplish the saving found these approaches:

    Rich's code from the second link is probably the simplest and looks like:

    save(){
        for i do
            printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"   
        done
        echo " "
    }
    

    and is used as:

    myarray=$(save "$@")
    
    set -- foo bar baz boo
    # ... do stuff with new $@ ...
    
    eval "set -- $myarray"
    

    The eval is safe since $myarray expands to a list of single-quoted strings.

    See how-to-use-pseudo-arrays-in-posix-shell-script for a more efficient and arguably easier to understand awk implementation of this idea. Here's the performance difference between the 2 implementations (timed using bash):

    $ cat tst.sh
    #!/usr/bin/env bash
    
    save_sed(){
        for i do
            printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"
        done
        echo " "
    }
    
    save_awk() {
        LC_ALL=C awk -v q=\' '
            BEGIN {
                for ( i=1; i<ARGC; i++ ) {
                    gsub(q, q "\\" q q, ARGV[i])
                    printf "%s ", q ARGV[i] q
                }
                print ""
            }
        ' "$@"
    }
    
    echo "calling sed in a loop:"
    time save_sed >/dev/null $(seq 100)
    echo ""
    echo "calling awk once:"
    time save_awk >/dev/null $(seq 100)
    

    $ ./tst.sh
    calling sed in a loop:
    
    real    0m3.403s
    user    0m1.014s
    sys     0m2.615s
    
    calling awk once:
    
    real    0m0.042s
    user    0m0.015s
    sys     0m0.031s
    

    The awk versions performance hardly changes when the number of arguments change while the loop+sed version increases/decreases by about a factor of 10 when the number of arguments changes by the same factor of 10.