11

Is there a way to set the positional parameters of a bash script from within a function?

In the global scope one can use set -- <arguments> to change the positional arguments, but it doesn't work inside a function because it changes the function's positional parameters.

A quick illustration:

# file name: script.sh
# called as: ./script.sh -opt1 -opt2 arg1 arg2

function change_args() {
    set -- "a" "c" # this doesn't change the global args
}

echo "original args: $@" # original args: -opt1 -opt2 arg1 arg2
change_args
echo "changed args: $@" # changed args: -opt1 -opt2 arg1 arg2
# desired outcome:        changed args: a c
2
  • 3
    AFAIK, the answer is "no; a function cannot change the script's global positional parameters". Commented Mar 25, 2015 at 14:41
  • 2
    The only way I can think to do this would be to have the function echo the set command and run $(change_args) at the toplevel. If this was javascript there would be eval games that could be played here I believe but I don't think things like that exist in the shell. Commented Mar 25, 2015 at 14:43

3 Answers 3

5

As stated before, the answer is no, but if someone need this there's the option of setting an external array (_ARRAY), modifying it from within the function and then using set -- ${_ARRAY[@]} after the fact. For example:

#!/bin/bash

_ARGS=()

shift_globally () {
  shift
  _ARGS=$@
}

echo "before: " "$@"
shift_globally "$@"
set -- "${_ARGS[@]}"
echo "after: " "$@"

If you test it:

./test.sh a b c d
> before:  a b c d
> after:  b c d

It's not technically what you're asking for but it's a workaround that might help someone who needs a similar behaviour.

Sign up to request clarification or add additional context in comments.

2 Comments

Invoking set -- "..." will result there being just single argument ("b c d") (and $# of 1) i.e. $@ converted to string. Leaving out the double quotes will give the wanted/expected result -- This is also what you used in your answer above, and probably meant to write in the example code as well?
@huoneusto Actually double-quoting $@ will cause it to expand each argument into its own word but not perform any word-splitting or globbing on them. Same when double-quoting a bash array with the [@] subscript. So set -- "$@" will work as intended. You can see this for yourself if you replace the use of echo in this answer with printf.
1

Not really. A function actually has its own scope. A parameter assigned in a function is global by default, but if you explicitly declare it it has local scope:

foo() {
    x=3 # global
    declare y # local
    ...
}

The positional parameters are always local to the function, so anything you do to manipulate them will only take effect within the function scope.

Technically, you can always use recursion to solve this issue. If you can confidently call the original script again, you can reorder the arguments that way. E.g.

declare -i depth

outer() {
    if (( depth++ < 1 )); then
        outer "${@:4:$#}" "${@:1:3}"
        return
    fi
    main "$@"
}

main() {
    printf '%s\n' "$@"
}

outer "$@"

This seems like an error prone and confusing solution to a problem I don't understand, but it essentially works.

Comments

0

I was searching for this myself and came up with the below, i.e. return a space separated string of the array (sorry, positional parameters) from the function by printing it to stdout and capture it with command substitution, then parse the string word for word and append it back into the positional arguments. Clear as mud!

Obviously won't work for args with spaces in them below but that's of course possible to workaround too.

#!/bin/sh  
fn() {
    set -- d e f
    res=""
    for i; do
        res="${res:+${res} }${i}"
    done
    printf "%s\n" "$res"
}
set -- a b c
rv=$(fn)
set --
for word in $rv; do
    set -- "$@" "$word"
done
for i; do
    echo positional parms "$i"
done

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.