2

So I've been reading around about getopts, getopt, etc. but I haven't found an exact solution to my problem.

The basic idea of the usage of my script is:

./program [-u] [-s] [-d] <TEXT>

Except TEXT is not required if -d is passed. Note that TEXT is usually a paragraph of text.

My main problem is that once getopts finishing parsing the flags, I have no way of knowing the position of the TEXT parameter. I could just assume that TEXT is the last argument, however, if a user messes up and does something like:

./program -u "sentence 1" "sentence 2"

then the program will not realize that the usage is incorrect.

The closest I've come is using getopt and IFS by doing

ARGS=$(getopt usd: $*)
IFS=' ' read -a array <<< "$ARGS"

The only problem is that TEXT might be a long paragraph of text and this method splits every word of text because of the spaces.

I'm thinking my best bet is to use a regular expression to ensure the usage is correctly formed and then extract the arguments with getopts, but it would be nice if there was a simpler solution

3

3 Answers 3

3

It's quite simple with getopts:

#!/bin/bash
u_set=0
s_set=0
d_set=0
while getopts usd OPT; do
  case "$OPT" in
    u) u_set=1;;
    s) s_set=1;;
    d) d_set=1;;
    *) # getopts produces error
       exit 1;;
  esac
done
if ((!d_set && OPTIND>$#)); then
  echo You must provide text or use -d >>/dev/stderr
  exit 1
fi
# The easiest way to get rid of the processed options:
shift $((OPTIND-1))
# This will run all of the remaining arguments together with spaces between them:
TEXT="$*"
Sign up to request clarification or add additional context in comments.

Comments

0

This is what I typically do:

local badflag=""
local aflag=""
local bflag=""
local cflag=""
local dflag=""

while [[ "$1" == -* ]]; do
  case $1 in
    -a)
      aflag="-a"
      ;;

    -b)
      bflag="-b"
      ;;

    -c)
      cflag="-c"
      ;;

    -d)
      dflag="-d"
      ;;

    *)
      badflag=$1
      ;;
  esac
  shift
done

if [ "$badflag" != "" ]; do
    echo "ERROR CONDITION"
fi

if [ "$1" == "" ] && [ "$dflag" == "" ]; do
    echo "ERROR CONDITION"
fi

local remaining_text=$@

1 Comment

The (dis)advantage of this is that you cannot group single-letter options together; you must use each option explicitly on its own, with any option arguments in the next argument to the script. Thus, you can't simulate ls -ls, or sort -ooutput whereas using getopts, you can.
0

Here is a CLI parser generator I wrote that supports bash and other common languages from a common command line argument definition file, see climeta. It also supports collapsed flags etc. without getopts dependency. This is an example of output that can be used as a template if not interested in the tool itself.

# Usage function
usage() {
    echo "Usage: $0 [options]"
    echo ""
    echo "Example CLI Parser using TOML"
    echo ""
    echo "positional arguments:"
    echo "  input INPUT                   : input file path (required)"
    echo ""
    echo "options:"
    echo '  -h, --help                    : show this help message and exit'
    echo '  --output OUTPUT               : output file path (required)'
    echo '  -v VERBOSE, --verbose VERBOSE : enable verbose mode (default "0")'
    echo '  --disable DISABLE             : disable something (default "1")'
    echo '  -i INT, --int INT             : just an integer number (required)'
    echo '  -f FLOAT, --float FLOAT       : just a float number (default "7.0")'
    echo ""
    echo "Example: sample0 input.txt --output output.txt --verbose -i 1 -f 2.0"
    exit "$1"
}

# check if a valid argument follows
check_valid_arg() {
    case "$2" in
        -*|'')
            echo "ERROR: $1 requires a value." >&2
            usage 1
            ;;
    esac
}

# Argument parsing function
parse_args() {
    # split --a=xx -b=yy -cde into --a xx -b yy -c -d -e
    # for more unified processing later on
    local i ch arg
    local -a new_args
    for arg in "$@"; do
        case "$arg" in
            --*=*) # convert --aa=xx into --aa xx
                right=${arg#*=}  # remove up to first =
                left=${arg%="$right"}  # remove right hand side
                new_args+=("$left" "$right")
                ;;
            --*)
                new_args+=("$arg")
                ;;
            -*) # convert -abc=yy into -a -b -c yy
                i=1
                while [ "$i" -lt "${#arg}" ]; do
                    # Get character at position i (0 based)
                    ch=$(expr "$arg" : "^.\{$i\}\(.\)")
                    case "${ch}" in
                        =) rest=$(expr "$arg" : "^..\{$i\}\(.*\)")
                           new_args+=("$rest"); break ;;
                        *) new_args+=("-${ch}") ;;
                    esac
                    i=$((i+1))
                done
                ;;
            *)
                new_args+=("$arg")
                ;;
        esac
    done
    set -- "${new_args[@]}"

    remaining_args=""
    local positional_idx=0
    while [ "$#" -gt 0 ]; do
        case "$1" in
            --output)
                check_valid_arg "$1" "$2"
                output="$2"
                shift;;
            --verbose|-v)
                verbose="1"
                ;;
            --disable)
                enable="0"
                ;;
            --int|-i)
                check_valid_arg "$1" "$2"
                int_="$2"
                shift;;
            --float|-f)
                check_valid_arg "$1" "$2"
                float_="$2"
                shift;;
            --help|-h)
                usage 0
                ;;
            --)
                shift
                remaining_args="$*"
                break
                ;;
            -*)
                echo "ERROR: Unknown option: $1" >&2
                usage 1
                ;;
            *) # handle positional arguments
                if [ $positional_idx -eq 0 ]; then
                    input="$1"
                else
                    echo "ERROR: Unexpected positional argument: $1" >&2
                    usage 1
                fi
                positional_idx=$(( positional_idx + 1 ))
                ;;
        esac
        shift
    done
}

# Validate arguments
validate_args() {
    if [ -z "$input" ]; then
        echo "ERROR: input is required" >&2
        usage 1
    fi
    if [ -z "$output" ]; then
        echo "ERROR: --output is required" >&2
        usage 1
    fi
    if [ -z "$int_" ]; then
        echo "ERROR: --int is required" >&2
        usage 1
    fi
}

# Dump argument values for debug
dump_args() {
    echo "Parsed arguments:"
    echo "input: $input"
    echo "output: $output"
    echo "verbose: $verbose"
    echo "enable: $enable"
    echo "int_: $int_"
    echo "float_: $float_"
    echo "remaining_args:"
    for arg in $remaining_args; do
        echo "  $arg"
    done
}

# Main entry point, parse CLI
get_cli_args() {
    # set defaults
    verbose="0"
    enable="1"
    float_="7.0"
    parse_args "$@"
    validate_args
}

# Example of use:
# get_cli_args "$@"
# dump_args

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.