5

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
  • 13,677
  • 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.

Chris Davies
  • 116,213
  • 16
  • 160
  • 287
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
  • 13,566
  • 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 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