3

Say I have a bash function, which is supposed to remove all arguments that start with "-" until it gets to an argument that does not start with "-".

gmx(){

  local options=( );

  while [ "${1:0:1}" == "-"  ]; do
    options+=("${1}")
    shift 1;
  done

  echo "first legit arg: $1"
  "$@" # omg will be executed here, like `omg --rolo`
}

gmx -a -f -c omg --rolo

this seems to work, but I am wondering if this is a good generic solution to always get 'omg' to be the first "legit" argument. Are there any edge cases that might fail?

In other words -a, -f, -c are all arguments to gmx. Whereas omg and everything that follows, will be run in a child process.

6
  • Is the intention to write a wrapper function around an existing command and to determine which is the first non-option argument? Then, no that's not the right way unless the wrapped command only accepts options that don't take arguments. More generally, the only reliable way is to parse the options the same way the wrapped command does. Jun 24, 2018 at 20:27
  • the wrapped command should be the first argument that does not start with -. my methodology will work unless you can explain why it wouldnt? Jun 24, 2018 at 20:31
  • i dont parse the arguments of the wrapped command, i pass the unparsed arguments to the wrapped command. Jun 24, 2018 at 20:32
  • 1
    For instance, it you wrapped sort, in sort -o out -n file1 file2 (same as sort -oout -n file1 file2), the first non-option argument is file1, not out which is the argument of the -o option Jun 24, 2018 at 20:33
  • Maybe I'm missing something or the function's body is incomplete but why would you run the function with options that are not going to be used (-a --foo -c)?. And how is gmx supposed to be a wrapper function if it basically runs its args as commands? I mean, instead of running gmx --foo omg --rolo (where --foo seems to do nothing) just run omg --rolo. I get the feeling that this is an XY problem, but it's possible that I don't clearly understand the purpose of the question.
    – nxnev
    Jun 25, 2018 at 0:39

3 Answers 3

8

The official and best way is t use the getopts builtin to parse the command line options.

See the man page for more information.

A note may be important: bash does not support long options.

If you like scripts to deal with long options, you have two shells that support them: ksh93 and bosh. Both shells support long options the way they are supported by the getopt(3) function in libc on Solaris. See the bosh man page (currently starting at page 43:

http://schilytools.sourceforge.net/man/man1/bosh.1.html

getopts "f:(file)(input-file)o:(output-file)" OPT

supports e.g an option -f with an argument and that option has a long option alias --file and a second alias for this option --input-file

ksh93 supports this as well, even though it is not documented in the ksh93 man page.

3
  • zsh also has a zparseopts to handle long options. I doesn't support the full GNU getopt_long() syntax though (not abbreviations for instance). There's a getopt in util-linux that can be used in any shell to parse GNU getopt_long style options. Jun 24, 2018 at 20:30
  • Thank you for the zsh hint. BTW: The getopt command is deprecated since 1986.
    – schily
    Jun 24, 2018 at 20:37
  • I'm talking of the util-linux getopt command, not the traditional one which can't be used reliably. See getopts --man in ksh93 for the documentation. Jun 25, 2018 at 5:29
5
while getopts ':' opt; do
    :    # This is where ordinarily a case statement would be,
         # case $opt in ... esac
         # But we use : as a no-op
done

shift "$(( OPTIND - 1 ))"
printf 'arg: %s\n' "$@"

This uses getopts to parse the command line options. The while loop will terminate as soon as the first non-option is found, and the shift will shift the processed options off of $@ leaving only the non-option operands in $@.

The script

#!/bin/sh

gmx () {
    while getopts ':' opt; do
        :
    done

    shift "$(( OPTIND - 1 ))"
    printf 'arg: %s\n' "$@"
}

gmx -a --foo -c omg lol

would output

arg: omg
arg: lol

Since you're not interested in what the real options are, you may obviously do a simple loop like

for opt do
    case $opt in
        -*) shift ;;
        *)  break
     esac
done

printf 'arg: %s\n' "$@"

This loops over all arguments, shifting off each that starts with a dash and ending at the first one that does not start with a dash.

2
  • Would while [[ $1 == -* ]]; do shift; done also work? Not sure which is more efficient. Jun 25, 2018 at 4:48
  • @PeterCordes That would work in bash, yes. When it comes to making a handful of pattern matches, I don't think efficiency really is the main issue.
    – Kusalananda
    Jun 25, 2018 at 5:31
3

A much simpler solution, if you have control over the way gmx is called, is simply to separate arguments to the wrapper from the wrapped command using --, as such

gmx garg garg garg -- warg warg warg

where

  • garg means an argument used by gmx.
  • warg means an argument sent to the wrapper.

If you want the convenience of omitting --, you could make it optional, and search for it before trying the "smart" check.

3
  • I have always wondered if -- is an official bash thing or more of a convention. Jun 25, 2018 at 0:22
  • 2
    It's more like a GNU convention, which got picked up by other command authors as being useful. Strictly speaking, it has nothing to do with the shell the command is being run in --- bash simply sees the -- as another argument.
    – jpaugh
    Jun 25, 2018 at 0:23
  • yeah I figured, thanks for the info, I upvoted your answer Jun 25, 2018 at 0:32

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.