0

I am unable to parse flags in a function in a bash script.

I have a shell script admin.sh that I'm sourcing (i.e. source ./admin.sh). In that script I have function called command_to_do. The code below shows what I'm trying to accomplish. I want to be able to call command_to_do from the command line and process all arguments

#!/bin/bash

function command_to_do() {
  echo "Args $@"

  SYSTEM=false
  UNINSTALL=false
  PUSH=false
  VERSION="debug"

  while getopts "sup:" opt; do
    printf "\n$opt\n"
    case $opt in
      s) echo "Setting SYSTEM to true"
         SYSTEM=true
         ;;
      p) echo "Setting VERSION to $OPTARG"
         PUSH=true
         VERSION=$OPTARG
         ;;
      u) echo "Setting UNINSTALL to true"
         UNINSTALL=true
         ;;
    esac
  done

  printf "\nSystem: $SYSTEM\nVersion: $VERSION\n"

  if [[ $UNINSTALL = true ]]; then
    if [[ $SYSTEM = true ]]; then
      echo "system uninstall"
    else
      echo "non-system uninstall"
    fi
  fi

}

I'm getting very strange results. The first time I run the command_to_do -s -p release, it correctly processes the flags and prints the expected results

Args -s -p release

s
Setting SYSTEM to true

p
Setting VERSION to release

System: true
Version: release

Every time after that, it doesn't seem to capture the flags. For instance, running that same command again gives:

Args -s -p release

System: false
Version: debug

If I remove the function declaration (function command_to_do() {}) and just have the code in the admin.sh file, everything seems to work fine every time, but I have to call the script with the dot syntax (./admin -s -p release). The reasoning behind why I don't want to use the dot syntax is not important. I just want to know if it's possible to do what I'm attempting and how.

I've read about passing the arguments to the function with command_to_do $@, but, as I'm trying to call the function by it's name from the command prompt, this is not applicable and it appears by the echo $@ statement that the arguments are being passed to the function. I'm new to writing bash scripts so any help would be appreciated.

James B
  • 447
  • 3
  • 15
  • 1
    FYI, using the `function` keyword is not particularly good practice; see https://wiki.bash-hackers.org/scripting/obsolete. On its own, it's a ksh compatibility feature; combined with POSIX-y `()` syntax, it's compatible with neither legacy ksh function declaration syntax *or* POSIX function syntax. – Charles Duffy May 17 '19 at 00:34
  • As another aside, `command_to_do $@` is just as buggy as `command_to_do $*`, and in exactly the same ways; to pass everything through *correctly*, you need quotes: `command_to_do "$@"`. – Charles Duffy May 17 '19 at 00:36
  • ...to speak directly to the question, though -- it's *very* unclear how you're getting the function declaration into your active shell, and *which* command line you want it to parse. If you mean the active command line in the calling shell, this might actually be one of the rare cases when an alias is the right tool. `command_to_do_fn() { ... }; alias command_to_do='command_to_do_fn "$@"'` -- but that's only true in very specific circumstances, and it's unclear to me whether those circumstances apply. – Charles Duffy May 17 '19 at 00:37
  • @CharlesDuffy to clarify, I'm using the mac terminal. I'm using the command `source ./admin.sh` to be able to call the `command_to_do` function directly. – James B May 17 '19 at 00:46

1 Answers1

1

The trick here is resetting OPTIND, which tells getopts where it is in the current argument list. As it is, your first call leaves OPTIND at a value telling getopts not to process the arguments it already saw.

See a demonstration at https://ideone.com/UvUyn2

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Thanks for the code sample! I added the line `unset OPTIND` to the beginning of the function and it works perfect. Any clarification as to why I don't need to unset the index if this code is not in a function? – James B May 17 '19 at 01:28
  • If it's a separate script, it has its own variables. – Charles Duffy May 17 '19 at 02:30
  • 1
    In a function, it's better to declare `OPTIND` as local, so you won't interfere with the top-level script (or other functions, or...). See: [Using getopts inside a Bash function](https://stackoverflow.com/questions/16654607/using-getopts-inside-a-bash-function). – Gordon Davisson May 17 '19 at 06:41
  • Oh, shoot -- that's a proper duplicate. – Charles Duffy May 17 '19 at 11:55