2

I am trying to build a program with 2 modes, each mode takes a different set of arguments. I have tried to implement nested case statements as described by @geekosaur in here, with case "$1" going through modes and case "$2" going through arguments.

I have a general --help option for the program describing the modes which are available, and a separate --help for each mode describing the arguments.

If I run:

./program.sh --help works

./program.sh mode1 --help works

./program.sh mode1 --unknown works (it correctly catches the unknown option and calls the usage function for mode1)

However:

./program.sh mode1 --option1 file

ERROR: unknown parameter file

I.e., it is recognizing "file" as a parameter instead of a value for key "option1".

How can I make this work? Am I messing up with my "shifts", or do I need nested loops?

(Only showing "mode1" in the code below for simplicity)

#!/bin/bash

usage_general() {
    echo "Usage: ProgramName [modes]"
    echo ""
    printf "%-10s %s\n" "mode1" "Run subprogram 1."
    printf "%-10s %s\n" "mode2" "Run subprogram 2."
}

usage_mode1() {
    echo "Usage: ProgramName mode1 [options]"
    echo ""
    printf "%-10s %s\n" "-1 | --option1" "Arg 1 for mode1"
    printf "%-10s %s\n" "-2 | --option2" "Arg 2 for mode1."
}


while [[ $# -gt 0 ]]
do
    case "$1" in
    -h | --help)
        usage_general
        exit
        ;;

    mode1)

        case "$2" in
        -h | --help)
            usage_mode1
            exit
            ;;
        -1 | --option1)
            val1="$3"
            shift 2
            ;;
        -2 | --option2)
            val2="$3"
            shift 2
            ;;
        *)
            echo "ERROR: unknown parameter $2"
            usage_mode1
            exit 1
            ;;
        esac
        ;;

    *)
        echo "ERROR: unknown parameter $1"
        usage_general
        exit 1
        ;;
    esac
done
1

When you use shift 2 in the inner case, you shift off $1 and $2, but not $3, the filename.

You would need to do a separate loop for parsing the options to the mode1 and mode2 sub command.

Assuming that when a sub command is found in the command line argument of the script, the rest of the options and option-arguments belongs to that sub command, you could split up the parsing like so:

run_mode1 () {
    while [ "$#" -gt 0 ]; do
        case "$1" in
        -h | --help)
            usage_mode1
            exit
            ;;
        -1 | --option1)
            val1="$2"
            shift 2
            ;;
        -2 | --option2)
            val2="$2"
            shift 2
            ;;
        *)
            echo "ERROR: unknown parameter $1"
            usage_mode1
            exit 1
            ;;
        esac
    done

    # code for actually running mode1 goes here
}

while [[ $# -gt 0 ]]
do
    case "$1" in
    -h | --help)
        usage_general
        exit
        ;;

    mode1)
        shift
        run_mode1 "$@"
        break
        ;;

    *)
        echo "ERROR: unknown parameter $1"
        usage_general
        exit 1
        ;;
    esac
done

Here, we run a shell function as soon as we encounter mode1 amongst the command line arguments. That function takes care of parsing the rest of the argument list.

This would obviously only work in the specific circumstance that all options to the script (not to a sub command) occurs before any given sub command on the command line, and that there may only be a single sub command with options given in any one invocation of the script.

It would not be impossible to implement the command line parsing in such a way that the script would be able to accept two or more sub commands with options at once, as in

./script.sh ...options... mode1 ...options... mode2 ...options...

but I'll leave that for now.

  • Thank you. In your first approach, it works for ./program.sh mode1 --option1 file. However, if I attempt to use both --option1 and --option2 as in ./program.sh mode1 --option1 file1 --option2 file2, I get ERROR: unknown parameter --option2. – Vitor Rezende da Costa Aguiar May 20 at 22:01
  • @VitorRezendedaCostaAguiar Ah, yes. That's actually a case where the second approach will work for you. If all of the options after mode1 are for that mode, parsing them in a separate shell function may be what you want to do. Alternatively do an inner loop inside the case statement. I'm on an awkward machine at the moment and can't do testing. I'll come back to this tomorrow (it's midnight). – Kusalananda May 20 at 22:05
  • 1
    Thank you very much. The second approach seems to work great, and I agree that it is nicer. I tried it with a mode2 as well, and it works. – Vitor Rezende da Costa Aguiar May 20 at 22:08
  • Another problem which I ran into is the following. In the command ./program.sh mode1 --option1 file1 --option2 file2, if user doesn't give a value for --option1 (e.g., ./program.sh mode1 --option1 --option2 file2), I get ERROR: unknown parameter file2. In this case, I cannot test if --option1 value is empty, because it takes "--option2" as its value! How can I avoid that please? @Kusalananda – Vitor Rezende da Costa Aguiar May 24 at 18:31
  • @VitorRezendedaCostaAguiar Take sed as an example of a standard tool that also takes a filename as an argument (to its -f option). If you forget to give it a filename and follow up with -e (for an expression), you get sed: -e: so such file or directory. Taking this as the pattern to follow, you should check for the existence of the argument to both --option1 and --option2 immediately and give the appropriate error message if the given name does not exist (I would use if [ ! -e "$argument" ]; then ...error message and exit 1...; fi). – Kusalananda May 24 at 19:31

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.