-1

Without introducing getopts, I'd like to parse expressions like so:

./cli.sh data i -f -f="./path/to/file.txt" --flags="--a --b"

And variations/combinations thereof:

./cli.sh -file="./path/to/file.txt" data --flags="--a --b" -f i

Unfortunately, I am stuck with reading all optional values of the long options submitted, for example: --flags="--a --b" only returns --a as value for the long option --flags.

I have created a minimalized version of a parser which exhibits my current issue:

#!/usr/bin/env bash

__init_system () {
    CMD_SED="sed"
    CMD_SED_EXT="sed -E"
}

__init_system_Darwin () {
    __init_system
    CMD_SED="gsed"
    CMD_SED_EXT="gsed -E"
}

invoke_func () {
    SYSTEM=$(uname)
    FUNC=$1 && shift 1
    declare -f ${FUNC}_${SYSTEM} >/dev/null
    [ $? -eq 0 ] && { ${FUNC}_${SYSTEM} "$@"; return $?; } || { ${FUNC} "$@"; return $?; }
}

handle_cli_harmonizing () {
    cli_adjusted=$(echo "$@" | ${CMD_SED_EXT} \
                    -e 's@([ ])+@ @g'\
                    -e 's@\<h\>@help@g'\
                    -e 's@\<e\>@enable@g'\
                    -e 's@\<d\>@disable@g'\
                    -e 's@\<i\>@import@g'\
                    -e 's@\<sl\>@showlog@g'\
                    -e 's@\<st\>@status@g'\
                    | tr ' ' '\n' | sort -u | xargs
                )
    #echo ">>>  Original: <$@>"
    #echo ">>>  Adjusted: <$cli_adjusted>"
}

handle_cli_parsing () {
    for param in $@; do
        case ${param} in
            start|stop|status|enable|disable|data|showlog)
                CMD=$param
                ;;
            help|app|stats|import|export|dbinfo|exec)
                SPEC=$param
                ;;
            --*|-*)
                #echo ">>> Found param: <$param>"
                OPT="$OPT $param"
                ;;
            *)
                echo ">>> Parsing mismatch: $param"
                ;;
        esac
    done
}

handle_cli_optparams () {
    for opt in $OPT; do
        case "$opt" in
            --force|-f)
                OPT_FORCE=1
                ;;
            --file=*|-f=*)
                OPT_FILE=1
                FILE=${opt#*=}
                ;;
            --flags=*)
                OPT_FLAGS=1
                FLAGS=${opt#*=}
                ;;
            *)
                echo ">>> Unknown/unimplemented option specifier: $opt"
                ;;
        esac
    done
}

invoke_func __init_system
invoke_func handle_cli_harmonizing "$@"
invoke_func handle_cli_parsing ${cli_adjusted}
invoke_func handle_cli_optparams

echo "DEBUG[   CLI]: CMD=$CMD SPEC=$SPEC"
echo "DEBUG[TOGGLE]: FORCE=$OPT_FORCE FILE=$OPT_FILE FLAGS=$OPT_FLAGS"
echo "DEBUG[  OPTS]: FILE=$FILE FLAGS=$FLAGS"

Expected ouput:

$ ./cli.sh data i -f -f="./path/to/file.txt" --flags="--a --b"
DEBUG[   CLI]: CMD=data SPEC=import
DEBUG[TOGGLE]: FORCE=1 FILE=1 FLAGS=1
DEBUG[  OPTS]: FILE=./path/to/file.txt FLAGS=--a --b

Current output:

$ ./cli.sh data i -f -f="./path/to/file.txt" --flags="--a --b"
>>> Unknown/unimplemented option specifier: --b
DEBUG[   CLI]: CMD=data SPEC=import
DEBUG[TOGGLE]: FORCE=1 FILE=1 FLAGS=1
DEBUG[  OPTS]: FILE=./path/to/file.txt FLAGS=--a

It's probably some quoting issue or a missing set, however I seem to be stuck here, and I'd really prefer to keep the simplistic approach I have taken.

I'd be especially stocked if someone would additionally offer a shell-only (no bashims) version for portability concerns, but that's icing on the cake. Mind you, this is only a very simple part of the whole parser I have written; enough to exhibit my current challenge.

I have read all the suggested linked issues while writing this question, and I have pondered for a while on How do I parse command line arguments in Bash?.


Update (2020/09/12): Even though this question has prematurely been downvoted, I found an elegant and more flexible solution, and have posted it as an answer below. It still beats getopt or any other approach I have seen concerning my specific requirements to command line parsing.

ikaerom
  • 538
  • 5
  • 27

2 Answers2

1

The root of the problem is here:

OPT="$OPT $param"

This is where the spaces embedded in $param become indistinguishable from the space used to append to the end of $OPT.

You can avoid this problem by making OPT an array, append values to it correctly:

OPT+=("$param")

... and then use array syntax to iterate over the values of the array:

for opt in "${OPT[@]}"; do

... and so on, consistently, everywhere, and watch out for correct double-quoting.

janos
  • 120,954
  • 29
  • 226
  • 236
  • Thank you for taking your time to actually tackle the question. I have posted the solution which I was after below: https://stackoverflow.com/a/60890883/619961. I still accepted yours for at least trying to help out. – ikaerom Mar 27 '20 at 17:15
0

Even after years of abandonment, I'd like to describe the solution to the question asked above.

While janos gave me the pointer, the solution is actually much simpler and maintains the original flexibility envisioned:

#!/usr/bin/env bash

handle_cli_parsing () {
    while [ $# -ne 0 ]; do
        param=$1
        [ x"$param" = x"sl" ] && param=showlog
        [ x"$param" = x"sh" ] && param=showlog
        [ x"$param" = x"h" ]  && param=help
        [ x"$param" = x"i" ]  && param=import
        [ x"$param" = x"e" ]  && param=enable
        [ x"$param" = x"d" ]  && param=disable
        case ${param} in
            start|stop|status|enable|disable|data|showlog)
                CMD=$param
                ;;
            help|app|stats|import|export|dbinfo|exec)
                SPEC=$param
                ;;
            --force|-f)
                OPT_FORCE=1
                ;;
            --file=*|-f=*)
                OPT_FILE=1
                FILE=${param#*=}
                ;;
            --flags=*)
                OPT_FLAGS=1
                FLAGS=${param#*=}
                ;;
            --*|-*)
                echo ">>> New param: <$param>"
                ;;
            *)
                echo ">>> Parsing mismatch: $param"
                ;;
        esac
        shift 1
    done
}

handle_cli_parsing "$@"

echo "DEBUG[   CLI]: CMD=$CMD SPEC=$SPEC"
echo "DEBUG[TOGGLE]: FORCE=$OPT_FORCE FILE=$OPT_FILE FLAGS=$OPT_FLAGS"
echo "DEBUG[  OPTS]: FILE=$FILE FLAGS=$FLAGS"

The output is now as expected and the parsing as flexible as intended:

$ ./cli.sh data i -f -f="./path/to/file.txt" --flags="--a --b"
DEBUG[   CLI]: CMD=data SPEC=import
DEBUG[TOGGLE]: FORCE=1 FILE=1 FLAGS=1
DEBUG[  OPTS]: FILE=./path/to/file.txt FLAGS=--a --b
ikaerom
  • 538
  • 5
  • 27