0

I have bash script I only want to support long options ("--option"). An --option can optionally have one or more arguments. All words (anything delimited by whitespace) up to but not including the first '--' OR the end of the command line string are considered "--option-arguments". A trailing space in the result is okay. Maximum performance is desired since this function is called by other functions for each possible option. Thus, trying to avoid bash loops and external commands.

Fought for many hours with the "first occurrence" issue until I found this answer that reminded me that POSIX (and thus bash) does not support nongreedy/lazy regex operators.

What to do?

  • Why use regexs here at all? A simple test for ${1#--} != $1 will do the trick. You can extract the arg name with ${1#--}. Speaking of efficiency, it's far better to loop through all arguments over a big case ...esac switch. In your case, you'd have a double case..esac. One to parse the argument names, and another to find the arguments. – Otheus Feb 15 '16 at 14:52
  • Thank you @Otheus. I wish to avoid for loops as they are quite slow in bash. Letting the C code within bash do iterations performs much better. Regex and Parameter Expansion accomplish that here. If I only needed to do this at script startup (the usual case), this would not be worth the effort. But this function is called by any other function that takes options (of which there are many) so performance is important. – DocSalvager Feb 15 '16 at 16:46
  • 1
    You're building this for functions as well...? Hrm. The whole idea of iteratively calling the function for each parameter seems wasteful to me, but you might be right: the looping in interprative code is slower than regex in C – Otheus Feb 15 '16 at 22:06
  • Is there any reason why you can't or don't want to use /usr/bin/getopt from the util-linux package? It supports short and/or long options. The bash built-in getopts only supports short options, so you need to either write your own option processing or use getopt if you want long options. IMO unless you are on a system where /usr/bin/getopt is not available, you are much better off just using that....reinventing the wheel almost invariably ends up with something triangular in shape rather than round and missing important features. – cas Feb 16 '16 at 2:56
  • @cas ... In addition to avoiding external programs due to performance issues, getopt is designed to be used with a case statement which works best for mutually exclusive and independent options. Case statements almost always involve setting variables in order to use values repeatedly ("persistence"?). That gets messy. – DocSalvager Feb 16 '16 at 4:08
1

You cannot parse options with regular expressions the way you seem to want because options are not passed in a string, but in a list of strings. myscript --option foo bar -- qux has myscript, --option, foo, bar, -- and qux as separate arguments, none of them containing whitespace.

A loop is the way to go in bash.

case "$1" in
  --option1)
    shift
    while [[ $# -ne 0 && "$1" != "--" ]]; do
      option1_args+=("$1")
      shift
    done
    (($# == 0)) || shift
done

If performance is that much of a problem, you shouldn't be using bash. Try ksh instead — it's free, available pretty much everywhere even if it isn't installed by default, and it's often significantly faster than bash. If that's still too slow, you need a fancier programming language such as Perl, Python or Ruby.

-1

Found this fairly simple solution...

function optionArg () {
  local _find="$1"; shift 1
  local _optarg=""
  local _reBeg=""
  #
  _reBeg="${_find}"'[= ]+(.*?)( --)?'
  ### no regex nongreedy operator support in POSIX
  ### will have to just truncate after first match
  #
  if [[ "$*" =~ $_reBeg ]]
  then
    _optarg="${BASH_REMATCH[1]}"
    ### all arguments following --option[= ]
    #
    _optarg="${_optarg%%--*}"
    ### limit to just arguments up to next --option (no lazy support in POSIX)
    #
    return 0
  else
    return 1
  fi

Given a script or function call with options followed by other options such as...

otherfunction --option1 arg1 arg2 --option2 -- file1 /home/me/file2

For each option accepted by otherfunction(), optionArg() would be called like...

_optarg1="$(optionArg --option1 "$@")"
_optarg2="$(optionArg --option2 "$@")"
_optarg3="$(optionArg --        "$@")"

The results would be...

_optarg1="arg1 arg2 "
_optarg2=""
_optarg3="file1 /home/me/file2"
  • This doesn't work (except in some specialized contexts where arguments can't meaningfully contain whitespace) because you're erasing the distinction between separate arguments and arguments containing whitespace. As a rule of thumb, if you're using $*, you're doing it wrong. – Gilles Feb 16 '16 at 1:25
  • A good point @Gilles. But not a problem in this use case. – DocSalvager Feb 16 '16 at 3:23
  • To whoever down-voted... Please consider not down-voting everything you disagree with. Just because you don't understand the logic, doesn't make it wrong. Please limit down-votes to grossly misleading approaches. I've been writing shell scripts for over 30 years in many different shells as well as programming in about 40 different languages including C and assembly. My low rep here is because I seldom post... which is because every time I do, someone more concerned with quick answers than thinking through problems starts down-voting. – DocSalvager Feb 16 '16 at 3:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.