3

I'm trying to move some getopts logic to a funciton, so I can use it more than once, giving users more flexibility in the order in which they specify arguments:

print-usage() {
echo "myssh [options] <host> [options] [-- ssh-options...]" >&2
exit 1
}

extra_args=()

parse-args() {
  while getopts ":hvV:" opt; do
    case ${opt} in
      (h)  print-usage ;;
      (v)  extra_args+=('-L 5900:localhost:5900')    ;;
      (V)  extra_args+=("-L $OPTARG:localhost:5900") ;;
      (\?) echo "Invalid option: -$OPTARG" >&2       ;;
      (:)  echo "Invalid option: -$OPTARG requires an argument" >&2 ;;
    esac
  done
  echo $((OPTIND -1))
}

shift $(parse-args $@)

host=$1
shift

shift $(parse-args $@)

ssh $host $extra_args $@

My problem is that parse-args() { ... extra_args+=(...) } doesn't affect the global variable extra_args. I get that a sub-shell can't write to parent-scoped variables, and we should normally use stdout, but I'm already using stdout for the shift integer.

How would one normally address this problem?

Stewart
  • 12,628
  • 1
  • 37
  • 80
  • 1
    Use a global `optind` variable that you set in the function and then use in the main code, avoiding the command substitution entierly? Remember that `OPTIND` needs to be set to 1 at the start of the function, and that using `$@` unquoted would split and glob all arguments. Also, since `-L` and e.g. `5900:localhost:5900` are two separate arguments, should you not add them as separate elements in `extra_args`? Not really a real answer as I haven't tested anything and I'm just throwing ideas around. – Kusalananda Jun 28 '21 at 10:48

2 Answers2

3

Since you're using bash (as tagged) you can use array variables. What you can't do is affect global variables from a call such as shift $(parse_args "$@"), which places parse_args in a subshell. Here's an alternate solution, which passes the array of values out of the function

#!/bin/bash
#
extra_args1=()
extra_args2=()

usage() {
    echo "myssh [options] <host> [options] [-- ssh-options...]" >&2
    exit 1
}
    
parse_args() {
    local OPT OPTIND=1 OPTARG args=()
    while getopts ":hvV:" OPT
    do
        case "$OPT" in
            (h)   usage ;;
            (v)   args+=('-L' '5900:localhost:5900') ;;
            (V)   args+=('-L' "$OPTARG:localhost:5900") ;;
            (\?)  echo "Invalid option: -$OPTARG" >&2 ;;
            (:)   echo "Invalid option: -$OPTARG requires an argument" >&2 ;;
        esac
    done
    echo $((OPTIND -1)) "${args[@]}"
}

# Get number to shift plus set of arguments
extra_args1=($(parse_args "$@"))
shift ${extra_args1[0]}
unset extra_args1[0]

# Position dependent value
host="$1"
shift

# Get number to shift plus set of arguments
extra_args2=($(parse_args "$@"))
unset extra_args2[0]

# "${x[@]}" values are interpolated as quoted and vanish if fully empty
ssh "$host" "${extra_args1[@]}" "${extra_args2[@]}" "$@"

By double-quoting "$@" we get to use its magic, which means not only that the separate values themselves are treated as being double-quoted, but that the entire expansion disappears if there are no values to expand. We can use this same feature to handle the expansion of "${extra_args1[@]}" and "${extra_args2[@]}"

A potential disadvantage of this method is that a=($(function)) is subject to globbing itself, so if function returns any wildcards they will be expanded as part of the assignment to array a. I don't have a good solution to this concern.

roaima
  • 107,089
  • 14
  • 139
  • 261
2

Possible solution using FIFO pipe. Instead of defining extra_args in the function, use the function to write to a pipe and read it into extra_args after the function was called.

#!/bin/bash

trap "rm -rf extraargpipe" ERR EXIT
mkfifo extraargpipe
parseargs() {
 echo 1 > extraargpipe &
 echo 2 > extraargpipe &
}
parseargs
extra_args=$(cat extraargpipe)

echo ${extra_args}

Returns 2 1 due to FIFO-logic (or actually is a race condition, if I am not mistaken), but that scrambled order should not be a problem with the ssh options you defined. It would be if you wanted to created something like -o opt1,opt2,opt3.

More or less the same could be done with a temporary file, too.

FelixJN
  • 12,616
  • 2
  • 27
  • 48
  • I'm guessing `echo 1 > extraargpipe ; echo 2 >> extraargpipe` would solve the race condition and inverted order. I'm guessing you already considered that but discarded it. Performance perhaps? – Stewart Jun 28 '21 at 19:42
  • @Stewart No, this would hang the function. See [this problem](https://unix.stackexchange.com/q/207487/123460) and the accepted answer - I however wanted to avoid using the read-write circumvention, because order does not matter here. – FelixJN Jun 28 '21 at 21:20