2

In Bash I can set $@ this way:

set -- a b c

Then, I can inspect the content of $@:

printf '%s\n' "$@"

which would show:

a
b
c

However, if I do this in a function:

f() {
    set d e f
}

set a b c
f
printf '%s\n' "$@"

I still get

a
b
c

and not

d
e
f

How can I make my function update caller's $@? I tried with BASH_ARGV, but it didn't work.

I am trying to write a function that processes the command line arguments and removes certain items from there (while setting a variable) so that the caller doesn't need to bother about them. For example, I want all my scripts to turn on their debug logging if I invoke them with --debug without having to write the code to process that in each script and placing that logic in a common "sourced" function instead.

Note: I don't want to fork a subshell.

codeforester
  • 39,467
  • 16
  • 112
  • 140

3 Answers3

0

You cannot change the values of arguments, as they are passed by reference in bash functions.

The best you can do is to pass the arguments you want to process, and return the ones not processed yet.

Something in the lines of:

process_arguments() {
    # process the arguments
    echo "original arguments : $@"
    local new_arguments=(a c)

    echo ${new_arguments[@])
}

new_arguments=$(process_arguments a b c)
set -- $new_arguments

If you don't want the trouble of a "subshell", you can use a global var:

arguments=""

process_arguments() {
    # process the arguments
    echo "original arguments : $@"
    local new_arguments=(a c)
    arguments="${new_arguments[@]}"
}

process_arguments a b c # no subshell
set -- $arguments        

As suggested by @ruakh, you can also use arguments as an array, like this:

arguments=()

process_arguments() {
    # process the arguments
    echo "original arguments : $@"
    local new_arguments=(a c)
    arguments=( "${new_arguments[@]}" )
}

process_arguments a b c # no subshell
set -- "${arguments[@]}"
ruakh
  • 175,680
  • 26
  • 273
  • 307
vfalcao
  • 332
  • 1
  • 3
  • 12
  • I think your second suggestion is the best one; but `arguments` should be an array. (Perhaps with a different name.) – ruakh Mar 14 '19 at 17:01
  • If you use `argument` as an array, you are going to do `set -- ${arguments}`, at the risk of getting only the first array item as the new positional parameters. – vfalcao Mar 14 '19 at 23:54
  • Re: "If you use argument as an array, you are going to do `set -- ${arguments}`": Why on Earth would you do that??? – ruakh Mar 15 '19 at 00:15
  • My mistake, @ruakh. I have update the answer, with your suggestion. – vfalcao Mar 16 '19 at 01:18
0

This is a matter of scope: Functions each have their own parameter array, independently of the script:

$ cat test.bash 
#!/usr/bin/env bash
f() {
    printf '%s\n' "Function arguments:" "$@"
}
printf '%s\n' "Script arguments:" "$@"
f 'a b' 'c d'
$ chmod u+x test.bash
$ ./test.bash 'foo bar' baz
Script arguments:
foo bar
baz
Function arguments:
a b
c d

So when you set the parameter array, that only applies within the current scope. If you want to change the script parameter array, you need to set it outside of any function. Hacks like set -- $(f) are not going to work in general, because it can't handle whitespace in arguments.

A general solution gets much uglier: you'd need to printf '%s\0' "$parameter" in the function and while IFS= read -r -d'' -u9 in the script to put the returned values into an array, and then set -- "${arguments[@]}".

I hope this is possible to do reliably some other way, but that's all I got.

l0b0
  • 55,365
  • 30
  • 138
  • 223
0

vfalcao's approach is good, though the code in that answer wouldn't handle whitespaces accurately.

Here is the code based on the same idea that does handle whitespaces well:

wrapper() {
  # filter args
  args=()
  for arg; do
    if [[ $arg = arg1 ]]; then
      # process arg1
    elif [[ $arg = arg2 ]]; then
      # process arg2
    elif ...
      # process other args
    else
      args+=("$arg")
    fi
  
    # call function with filtered args
    wrapped_function "$args[@]"
  done
}

wrapper "$@"

An example implementation here: base-wrapper

codeforester
  • 39,467
  • 16
  • 112
  • 140