Skip to content

Instantly share code, notes, and snippets.

@jehiah
Created March 4, 2011 16:56
Show Gist options
  • Save jehiah/855086 to your computer and use it in GitHub Desktop.
Save jehiah/855086 to your computer and use it in GitHub Desktop.
a simple way to parse shell script arguments
#!/bin/sh
#
# a simple way to parse shell script arguments
#
# please edit and use to your hearts content
#
ENVIRONMENT="dev"
DB_PATH="/data/db"
function usage()
{
echo "if this was a real script you would see something useful here"
echo ""
echo "./simple_args_parsing.sh"
echo "\t-h --help"
echo "\t--environment=$ENVIRONMENT"
echo "\t--db-path=$DB_PATH"
echo ""
}
while [ "$1" != "" ]; do
PARAM=`echo $1 | awk -F= '{print $1}'`
VALUE=`echo $1 | awk -F= '{print $2}'`
case $PARAM in
-h | --help)
usage
exit
;;
--environment)
ENVIRONMENT=$VALUE
;;
--db-path)
DB_PATH=$VALUE
;;
*)
echo "ERROR: unknown parameter \"$PARAM\""
usage
exit 1
;;
esac
shift
done
echo "ENVIRONMENT is $ENVIRONMENT";
echo "DB_PATH is $DB_PATH";
@hypersoft
Copy link

👎 You've complicated a really simple task..

printfx() {

    TEMP=`getopt -o hbrkwels:c:v:f:p: --long help,write,keep,reset,bold,format:,script:,color:,print: -n "$FUNCNAME" -- "$@"`

    if [ $? != 0 ] ; then return 1 ; fi

    eval set -- "$TEMP";

    local format='%s\n' escape='-E' line='-n' script clear='tput sgr0';

    while [[ ${1:0:1} == - ]]; do
        [[ $1 =~ ^-h|--help ]] && {
            cat <<-EOF
            USAGE: $FUNCNAME [OPTIONS] [TEXT]

            OPTIONS

              -s  Adds a line of text to tput script
              -c  Adds a line of text to tput script that sets the foreground
                  color
              -b  Adds a line of text to tput script that activates emboldened
                  text
              -f  Sets the final printf format
              -w  Forces an immediate write of tput script
              -p  Immediately prints data to standard out
              -e  Enabled backslash interpretation in immediate print operations
              -l  Enables line output in immediate print operations
              -k  disables terminal reset on return
              -r  Forces an immediate reset of terminal settings
              -v  Adds multiple lines of script from a shell variable label

              --script  Same as -s
              --color   Same as -c
              --bold    Same as -b
              --format  Same as -f
              --write   Same as -w
              --print   Same as -p
              --keep    Same as -k
              --reset   Same as -r

            EOF
            return;
        };

        [[ $1 == -- ]] && { shift; break; };
        [[ $1 == -l ]] && { line=''; shift 1; continue; };
        [[ $1 =~ ^-s|--script$ ]] && { script+="$2"$'\n'; shift 2; continue; };
        [[ $1 =~ ^-c|--color$ ]] && { script+="setf $2"$'\n'; shift 2; continue; };
        [[ $1 =~ ^-b|--bold$ ]] && { script+="bold"$'\n'; shift 1; continue; };
        [[ $1 =~ ^-f|--format$ ]] && { format="${2}"; shift 2; continue; };
        [[ $1 =~ ^-w|--write$ ]] && { tput -S <<<"$script"; script=''; shift 1; continue; };
        [[ $1 =~ ^-p|--print$ ]] && { echo $escape $line "${2}"; escape='-E' line='-n'; shift 2; continue; };
        [[ $1 =~ ^-k|--keep$ ]] && { clear='true'; shift 1; continue; };
        [[ $1 =~ ^-r|--reset$ ]] && { tput sgr0; shift 1; continue; };
        [[ $1 =~ ^-v ]] && { script+="${!2}"$'\n'; shift 2; continue; };
        [[ $1 =~ ^-e ]] && { escape=$1; shift 1; continue; };

        break;
    done

    echo $format;
    tput -S <<<"$script";
    (( $# )) && printf "$format" "$@";
    $clear;

}

@perepechaev
Copy link

I think that it is better to replace https://gist.github.com/jehiah/855086#file-simple_args_parsing-sh-L26

VALUE=`echo $1 | sed 's/^[^=]*=//g'`

sample:
/bin/sh myscript --host=localhost --uri="/some/uri/?debug=1"

@geoff-nixon
Copy link

@hypersoft: I really don't see how your code is any simpler... it is much less readable; and significantly less portable. This gist would appear to be intended as a shell script; not a bash script; and while the original does have one bashism, it seems like a trivial oversight (should be just usage() rather than function usage(). Your code includes using GNU getopt; herestrings, and many, many bash-only features that make it completely unsuited for any type of portable shell script.

@brunodix
Copy link

brunodix commented Feb 4, 2014

Thanks, works even on Git Bash on Windows.

@cfollet
Copy link

cfollet commented May 24, 2016

Thank you for this. But why you didn't use$0 in order to retrieve the script name ?

@diemol
Copy link

diemol commented Sep 30, 2016

Shouldn't line https://gist.github.com/jehiah/855086#file-simple_args_parsing-sh-L44 be shift 2?
I am trying it and the parameters are shifted one by one, when (I think) the loop expects them to move two by two.

Anyway, thanks for this, saves a bunch of time 👍

@matejak
Copy link

matejak commented Dec 7, 2016

Better than writing the parsing code is having one generated and that's the reason why I have started the argbash project here on Github. Keeping the usage function up-to-date and having support for positional arguments are some of the obvious benefits.
There is even an online bash 3.0+ compatible parsing code generator at https://argbash.io/generate

@stancufm
Copy link

I've started the run-sections project if you need more than just parsing arguments. With run-sections you can chose what part of you code do you want to run and much more. You can simply mark sections inside your script and run just that sections. If you are interested on this idea, please have a look on https://github.com/stancufm/run-sections . It's very easy to work with this function.

@tchalvak
Copy link

./simple_args_parsing.sh
./simple_args_parsing.sh: 13: ./simple_args_parsing.sh: Syntax error: "(" unexpected

Soo... not great.

@gperreymond
Copy link

gperreymond commented Sep 28, 2017

Hotfix this

function usage()
{

With :

usage() {

@migaes
Copy link

migaes commented Apr 9, 2018

Nice gist conception!
Just a small detail, add -e to echo commands in the help menu, so it will format special tab characters ...

@justme-justus
Copy link

echo "\t-h --help"

shouldn't it be echo -e? Otherwise it prints \t in my case.

Or better use printf?

@elliotboney
Copy link

@geoff-codes thanks for the snippet! @hypersoft's solution was hella less readable

@LeeYunhang
Copy link

Thank you a lot

@LeeYunhang
Copy link

@gperreymond is right, without function keyword is better than with it.

@BatmanAoD
Copy link

@hypersoft 👎 You've complicated a really simple task.

@kthoms
Copy link

kthoms commented Jan 22, 2019

Thanks, works fine. Use echo -e "\t-h --help" as @bestjejust suggested for printing out tab.

@Yann-Situ
Copy link

I have difficulties for parsing "-e" with that, I think it interferes with the 'echo $1 line 25.

@Pagliacii
Copy link

This works fine with or without an equal symbol.

while [ "$1" != "" ]; do
    PARAM=`printf "%s\n" $1 | awk -F= '{print $1}'`
    VALUE=`printf "%s\n" $1 | sed 's/^[^=]*=//g'`
    if [[ $VALUE == $PARAM ]]; then
        shift
        VALUE=$1
    fi
    ...
done

@Ivancaminal72
Copy link

@perepechaev An even better way would be

awk -F= '{OFS="=";$1=""; printf substr($0,2)}'

In case that $VALUE is not provided returns an empty string

@cslycord
Copy link

cslycord commented Apr 30, 2020

@Pagliacii

This works fine with or without an equal symbol.

while [ "$1" != "" ]; do
    PARAM=`printf "%s\n" $1 | awk -F= '{print $1}'`
    VALUE=`printf "%s\n" $1 | sed 's/^[^=]*=//g'`
    if [[ $VALUE == $PARAM ]]; then
        shift
        VALUE=$1
    fi
    ...
done

If you change if [[ $VALUE == $PARAM ]]" to if [ "$VALUE" = "$PARAM" ] it will be more portable.
The code in the OP was written to be run as /bin/sh, so the [[ ]] and == parts could be a problem since they're not POSIX.

Also, I think it's better to change PARAM=printf "%s\n" $1 | awk -F= '{print $1}' to PARAM="$(printf "%s\n" $1 | awk -F= '{print $1}')"
and VALUE=printf "%s\n" $1 | sed 's/^[^=]*=//g' to VALUE="$(printf "%s\n" $1 | sed 's/^[^=]*=//g')"

@unfor19
Copy link

unfor19 commented Jul 23, 2020

I wrote this - https://github.com/unfor19/bargs - exactly for this need, so you don't need to write down while case esac shift thingy, a very simple and flexible implementation

@ko1nksm
Copy link

ko1nksm commented Nov 8, 2020

I wrote another optional parser and generator that POSIX-compliant and works with all POSIX shells (e.g. dash, bash 2.0+, ksh88, zsh 3.1+, busybox ash). It is a pure shell function and aims to be compatible with POSIX and GNU option syntax. Its code is as fast and small as possible, and the CC0 license allows you to embed it freely in your scripts. https://github.com/ko1nksm/getoptions

2020-11-19: Add support for abbreviated long options and subcommands.

@alanmur
Copy link

alanmur commented Nov 8, 2021

I used the work of @hypersoft for my own script and made some bug fixes and improvements to meet my needs. Giving back the results, which I have simplified for the sake of clarity.

#!/bin/bash

DIRNAME=$(dirname -- $0)
BASENAME=$(basename -- $0)

usage()
{
    echo "USAGE: $BASENAME [OPTIONS]"
    echo
    echo "OPTIONS"
    echo "  -r, --require-approval   Ask user for security approval when deploying CDK stacks"
    echo
    echo "  Features deployed will be the sum \(union\) of the following options, if none"
    echo "  is specified then all features will be deployed by default. \(Specify each"
    echo "  option separately, as a group like -yld will not be understood\)"
    echo "  -a, --all                Deploy all features"
    echo "  -y, --analytics          Deploy the analytics features"
    echo
    echo "  -v, --verbose [level]    Output additional info. (can use -v=2 form also)"
    echo "                           Possible levels:"
    echo "                               0 \(default\)    essential info"
    echo "                               1              more info"
    echo "                               2              even more info"
    echo
    echo "POSITIONAL PARAMETERS"
    echo "  none"
    exit
}

# default values for flags. always use 1 for true and 0 for false. use strings for qualifiers.
REQUIREAPPROVAL="--require-approval never"
DEFAULT=1
ALL=0
ANALYTICS=0
VERBOSELEVEL="0"

while [[ $# > 0 ]]
do
    echo num $#
    [[ $1 =~ ^-h$|^--help$ ]] && {
        usage
    };
    # -- marks end of options and start of positional parameters
    [[ $1 == -- ]] && { shift; break; };

    # options without parameters
    [[ $1 =~ ^-r$|^--require-approval$ ]] && { REQUIREAPPROVAL=; shift 1; continue; };
    [[ $1 =~ ^-a$|^--all$ ]] && { ALL=1; DEFAULT=0; shift 1; continue; };
    [[ $1 =~ ^-y$|^--analytics$ ]] && { ANALYTICS=1; DEFAULT=0; shift 1; continue; };

    # verbose option with optional value. e.g. -v, -v 0, -v 1, --verbose, --verbose 1
    [[ $1 =~ ^-v$|^--verbose$ ]] && {
        shift 1
        [[ $# == 0 ]] && { continue; };
        VERBOSELEVEL="$1"
    };

    # verbose option with = value. e.g. -v=0, -v=1, -v=2, --verbose=1 etc.
    [[ $1 =~ ^-v=|^--verbose= ]] && {
        VERBOSELEVEL="${1#*=}"
        shift 1
        continue;
    };

    break;
done

# validate string parameters
[[ ! $VERBOSELEVEL =~ ^[0-2]$ ]] && {
    echo illegal --verbose parameter $VERBOSELEVEL
    usage
};

# if verbose 1 or 2
[[ ! $VERBOSELEVEL =~ ^[1-2]$ ]] && {
    echo DEFAULT == $DEFAULT
    echo ALL == $ALL
    echo ANALYTICS == $ANALYTICS
};

# the default is to deploy all stacks
ALL=$(( $ALL || $DEFAULT ))

# Logic for which stacks are deployed based on the features needed
CFNMAIN=$(( $ALL ))
CFNANALYTICS=$(( $ALL || $ANALYTICS ))

# if verbose 2 only
[[ ! $VERBOSELEVEL =~ ^2$ ]] && {
    echo CFNMAIN = $CFNMAIN
    echo CFNANALYTICS = $CFNANALYTICS
};

[[ $CFNMAIN ]] && {
    echo cdk synth cfnMain
    echo cdk deploy $REQUIREAPPROVAL cfnMain
};

[[ $CFNANALYTICS ]] && {
    echo cdk synth cfnAnalytics
    echo cdk deploy $REQUIREAPPROVAL cfnAnalytics
};

echo ":) Deployment complete"

@slycordinator
Copy link

#!/bin/sh

DIRNAME=$(dirname -- $0)
BASENAME=$(basename -- $0)

usage()
{
echo "USAGE: $BASENAME [OPTIONS]"
echo
echo "OPTIONS"
echo " -r, --require-approval Ask user for security approval when deploying CDK stacks"
echo
echo " Features deployed will be the sum (union) of the following options, if none"
echo " is specified then all features will be deployed by default. (Specify each"
echo " option separately, as a group like -yld will not be understood)"
echo " -a, --all Deploy all features"
echo " -y, --analytics Deploy the analytics features"
echo
echo " -v, --verbose [level] Output additional info. (can use -v=2 form also)"
echo " Possible levels:"
echo " 0 (default) essential info"
echo " 1 more info"
echo " 2 even more info"
echo
echo "POSITIONAL PARAMETERS"
echo " none"
exit
}

default values for flags. always use 1 for true and 0 for false. use strings for qualifiers.

REQUIREAPPROVAL="--require-approval never"
DEFAULT=1
ALL=0
ANALYTICS=0
VERBOSELEVEL="0"

while [[ $# > 0 ]]
do
echo num $#
[[ $1 =~ ^-h$|^--help$ ]] && {
usage
};
# -- marks end of options and start of positional parameters
[[ $1 == -- ]] && { shift; break; };

# options without parameters
[[ $1 =~ ^-r$|^--require-approval$ ]] && { REQUIREAPPROVAL=; shift 1; continue; };
[[ $1 =~ ^-a$|^--all$ ]] && { ALL=1; DEFAULT=0; shift 1; continue; };
[[ $1 =~ ^-y$|^--analytics$ ]] && { ANALYTICS=1; DEFAULT=0; shift 1; continue; };

# verbose option with optional value. e.g. -v, -v 0, -v 1, --verbose, --verbose 1
[[ $1 =~ ^-v$|^--verbose$ ]] && {
    shift 1
    [[ $# == 0 ]] && { continue; };
    VERBOSELEVEL="$1"
};

# verbose option with = value. e.g. -v=0, -v=1, -v=2, --verbose=1 etc.
[[ $1 =~ ^-v=|^--verbose= ]] && {
    VERBOSELEVEL="${1#*=}"
    shift 1
    continue;
};

break;

done

validate string parameters

[[ ! $VERBOSELEVEL =~ ^[0-2]$ ]] && {
echo illegal --verbose parameter $VERBOSELEVEL
usage
};

if verbose 1 or 2

[[ ! $VERBOSELEVEL =~ ^[1-2]$ ]] && {
echo DEFAULT == $DEFAULT
echo ALL == $ALL
echo ANALYTICS == $ANALYTICS
};

the default is to deploy all stacks

ALL=$(( $ALL || $DEFAULT ))

Logic for which stacks are deployed based on the features needed

CFNMAIN=$(( $ALL ))
CFNANALYTICS=$(( $ALL || $ANALYTICS ))

if verbose 2 only

[[ ! $VERBOSELEVEL =~ ^2$ ]] && {
echo CFNMAIN = $CFNMAIN
echo CFNANALYTICS = $CFNANALYTICS
};

[[ $CFNMAIN ]] && {
echo cdk synth cfnMain
echo cdk deploy $REQUIREAPPROVAL cfnMain
};

[[ $CFNANALYTICS ]] && {
echo cdk synth cfnAnalytics
echo cdk deploy $REQUIREAPPROVAL cfnAnalytics
};

echo ":) Deployment complete"

It's marked as /bin/sh, but will not work in posix compliant shells (such as dash).
The "[[ ]]" for comparisons, "~=" regex matching, and "==" instead of "=" are all not supported.

If you prefer using those to the posix equivalents, the easiest thing to do would be to just change it to /bin/bash since those are bashisms.

:)

@alanmur
Copy link

alanmur commented Nov 9, 2021

/bin/bash it is, then!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment