2 releases
0.1.1 | Apr 11, 2024 |
---|---|
0.1.0 | Apr 11, 2024 |
#1779 in Command line utilities
63KB
1K
SLoC
ArgParse-sh
Utility for parsing arguments to shell scripts and providing the results as environment variables.
Installation
Cargo Install
The preferred method of install uses cargo
, the Rust tool. If you have Rust installed on your
local machine, run:
cargo install argparse-sh
The binary will be built and put in your ~/.cargo/bin/
folder. Add that to your PATH
or
reference it directly, and you are all set.
Building From Source
You can download and build the application from source code. This also requires Rust to be installed on your local machine, as well as Git. To do this, run:
git clone git@github.com:Hounshell/argparse-sh.git
cd argparse-sh
cargo build --release
The compiled binary will be in target/release/argparse-sh
. Put this wherever you like and add it
to your PATH
or reference it directly.
Usage
ArgParse-sh takes two sets of arguments. The first set defines all of the arguments that will be parsed. The second set is the arguments to parse. This is probably best clarified via a minimal demonstration:
$ argparse-sh --string text -- --text "Hello, world!"
TEXT="Hello, world!"
What did this do? It defined a set of arguments (one string argument called "text") and then parsed a set of argument values. The output is a script that sets environment variables for the arguments provided.
This is a minimal example, but it isn't a very useful one. Instead lets drop this into a script and pass arguments on the script through to ArgParse-sh:
demo.sh
eval "$(argparse-sh --string text -- "$@")";
echo "$TEXT"
Let's break this script up into various pieces:
-
argparse-sh
- This invokes the ArgParse-sh program -
--string text
- This indicates to ArgParse-sh that we can accept an optional argument called "text". The value is a string and there is no default value. -
--
- This indicates to ArgParse-sh that we are done defining arguments. All remaining command line parameters should be treated as argument values. -
"$@"
- This forwards all of the parameters that the script was called with to ArgParse-sh. Because this is after the--
, ArgParse-sh will interpret these parameters. -
eval "$(...)";
- The output of ArgParse-sh is script commands.eval
executes these commands in the current shell. -
echo "$TEXT"
- The script commands that ArgParse-sh will output set environment variables with the value of the arguments. This will print the value of whatever was passed in for the "text" argument.
If we run demo.sh
we can see this all work in action:
$ ./demo.sh --text "Hello, world!"
Hello, world!
ArgParse-sh recognizes the --text
argument and expects a string. That string ("Hello, world!") is
written to an environment variable called "TEXT" (based on the argument's name), which can then be
used by the remainder of the script.
In several examples below we will omit the eval
part of the argparse-sh command. This will cause the
ArgParse-sh output to dump to the screen, allowing us to see what is happening more clearly.
Argument Types
There are several supported argument types. Generally they share the same set of options (where it makes sense for them to do so), and some have additional options or forms.
Common Argument Parameters
There is a common pattern to defining arguments, as well as a number of common parameters that can be provided:
--<type> [<name / key 1> <key 2> <key 3> ...]
To begin defining an argument we need to specify the argument type. After the type we may provide shorthand for the name and the keys that can be used to provide this argument. This shorthand covers the most common cases, but you are not required to use it; the name and flags can be defined individually.
The types available are:
- --boolean or --bool - A "true" or "false" value.
- --choice or --pick - One selection from a list of options.
- --float or --number - A 64 bit floating point number.
- --integer or --int - A 64 bit signed integer.
- --string or --str - Free-form text.
Example:
$ argparse-sh --string given-name first-name name -- --first-name "Alice"
GIVEN_NAME="Alice"
This creates a single string argument called "GIVEN_NAME" that can be set using either
--given-name
, --first-name
, or --name
. This is the shorthand form of:
$ argparse-sh --string --name GIVEN_NAME \
--flag "--given-name" \
--flag "--first-name" \
--flag "--name" \
-- --first-name "Alice"
GIVEN_NAME="Alice"
--name <name>
Provide the name of the environment variable the value for this argument will be stored in.
If the name is not provided, the first flag defined (either using --flag or the shorthand above) will be normalized and used. Normalization removes any preceeding or trailing non-alphanumeric characters. All other sequences of non-alphanumeric sequences will be replaced with "_" and the entire string will be capitalized. For example, if the first flag is "--first-name" the normalized name would be "FIRST_NAME".
Example:
$ argparse-sh --string name --name FIRST_NAME -- --name "Alice"
FIRST_NAME="Alice"
This will configure a string argument that can be set using --name "Name"
, but will be stored in
the FIRST_NAME
environment variable.
--flag <flag>
Provides a flag that can be used to specify this argument's value. The flag will be used as-is, with capitalization and hyphens. This is a good way to support shorter flags. You are allowed to specify a flag name without a hyphen at all.
Example:
$ argparse-sh --string name --flag "--first-name" --flag "-n" -- -n Alice
NAME="Alice"
This defines a string argument called "NAME" using the shorthand method, but alternate flags ("--first-name" and "-n") are also defined as available for use.
--default <default>
Provide the default value to use if this argument is not specified.
Warning: This default value is not parsed or validated; invalid values will be passed on through.
Example:
$ argparse-sh --string name --default "Alice"
NAME="Alice"
If -- --name "Bob"
had been provided then NAME
would have been set to "Bob" instead of "Alice".
--desc[ription] <description>
Provide a description to use for this argument when generating help text.
Example:
$ eval "$(argparse-sh --string name --desc "The user's first name." --autohelp -- --help)"
OPTIONS
--name <name>
The user's first name.
We used eval
in this example because it's hard to see the output otherwise. Help was generated
for all of the arguments (in this case only --name
was specified), and the help text was used in
the description of that argument.
--repeated
Indicates that this argument may be repeated. If an argument is repeated then the environment variable will be set to the number of values that exist, and each value will be set as its own environment variable, with a 0-based suffix for the index.
Example:
$ argparse-sh --string name --repeated -- --name "Alice" --name "Bob" --name "Carol"
NAME="3"
NAME_0="Alice"
NAME_1="Bob"
NAME_2="Carol"
Here we can see that three names were supplied. Each value for --name
was included in order.
--required
Indicates that this argument is required. If not provided ArgParse-sh will fail.
Example:
$ argparse-sh --string name --required
echo ""
echo "!!! ArgParse-sh Error: Value for argument NAME is missing !!!"
echo ""
$ echo $?
2
An error message is shown indicating that the "name" argument wasn't supplied. The exit code from ArgParse-sh when there is an error parsing the arguments is 2.
--secret
Marks an argument for non-inclusion in generated help text.
Example:
$ eval "$(target/debug/argparse-sh --string name --secret --string age --autohelp -- --help)"
OPTIONS
--age <age>
No details available.
Again we use eval
for clarity. Note that help text is generated for the "age" argument, but not
for the "name" argument.
--catch-all
This is used to mark an argument that will be get any unrecognized values. This is particularly useful for repeated arguments where you don't want to require the user to specify the flag name.
Note that catch-all arguments don't require any flags, but it is advised that you still
provide some to the user so that they can use the --flag=value
syntax. If not using any flags
then the argument must have a name.
Example:
$ argparse-sh --string name --catch-all -- "Bob"
NAME="Bob"
--ordinal <order>
Makes this argument act like a catch-all argument, except it will only take a single value, and
only if a value hasn't been explicitly provided. The order
is an integer that provides the order
that ordinal arguments are filled. The lowest argument that does not already have a value (e.g. the
user hasn't explicitly provided a value for this argument via a flag) will be used next. This means
that ordinals can start at whatever number you like, and can have gaps between the numbers.
Example:
$ argparse-sh \
--string first_name --ordinal 1 --required \
--string middle_name --ordinal 2 --required \
--string last_name --ordinal 3 --required \
-- --middle_name "Quincy" "Alice" "Smith"
FIRST_NAME="Alice"
MIDDLE_NAME="Qunicy"
LAST_NAME="Smith"
String Arguments (--string or --str)
String arguments do not perform any validation or re-writing of their values. These are simply passed through to the environment variable. String arguments support all of the common argument parameters.
Example:
$ argparse-sh \
--string first-name --required \
--string last-name --default "Doe" \
--string nickname --repeated --catch-all \
-- \
--first-name "John" \
--nickname "Sticky Fingers" \
--nickname "Tight Lips"
FIRST_NAME="John"
LAST_NAME="Doe"
NICKNAME="2"
NICKNAME_0="Sticky Fingers"
NICKNAME_1="Tight Lips"
Integer Arguments (--integer or --int)
Integer arguments are validated. The value provided must be parseable as a 64 bit signed integer. If an invalid argument is provided then argparse-sh will fail with a message and an error code of 2. Integer arguments support all of the common argument parameters.
Important: If a default value is provided it is not validated. You are responsible for ensuring that the provided value resolves to an integer, or your script is able to handle non-integer values.
Example:
$ argparse-sh \
--integer age --required \
--integer children --default 0 \
--integer pockets --required --catch-all \
-- \
--age 42 \
7
AGE="42"
CHILDREN="0"
POCKETS="7"
Float Arguments (--float or --number)
Float arguments are also validated. The value provided must be parseable as a 64 bit floating point number. If an invalid argument is provided then argparse-sh will fail with a message and an error code of 2. Float arguments support all of the common argument parameters.
Important: If a default value is provided it is not validated. You are responsible for ensuring that the provided value resolves to a number, or your script is able to handle non-numeric values.
Example:
$ argparse-sh \
--float height --required \
--float weight --default 0 \
--float cash-on-hand --catch-all \
-- \
--height 180.4 \
72.34
HEIGHT="180.4"
WEIGHT="0"
CASH_ON_HAND="72.34"
Choice Arguments (--choice or --pick)
Choice arguments are a little different than other argument types, but they are most similar to String arguments. With Choice arguments you supply a list of valid choices and alternate mappings. If an unrecognized value is provided then argparse-sh will fail with a message and an error code of 2.
Important: If a default value is provided it is not validated and it is not mapped. You are responsible for ensuring that the provided value resolves to one of your choices, or your script is able to handle this value.
--option <name> [<help_text>]
Choice arguments expect one or more option parameters. After --option
you must include the option
name. You may also provide help text that is shown after that option.
--map <from> <to>
Maps from one option to another. This provides an easy way to have multiple names for a specific option.
Important: You can map to an option that does not exist. You are responsible for ensuring that the option that you map to exists. Mappings are not chained; if you map from "a" to "b" and "b" to "c" and the user provides "a", the value will be "b".
Example:
$ argparse-sh \
--choice gender --default "none" \
--option male "Person identifies as male" \
--option female "Person identifies as female" \
--map boy male \
--map girl female \
--option other "Person identifies as something else" \
--option none "Person declines to identify" \
-- \
--gender boy
GENDER="male"
$ eval "$(argparse-sh \
--choice gender --default "none" \
--option male "Person identifies as male" \
--option female "Person identifies as female" \
--map boy male \
--map girl female \
--option other "Person identifies as something else" \
--option none "Person declines to identify" \
--autohelp \
-- \
--help)"
OPTIONS
--gender <gender>
No details available.
The possible options are:
• male - Person identifies as male
• female - Person identifies as female
• boy - Identical to 'male'
• girl - Identical to 'female'
• other - Person identifies as something else
• none - Person declines to identify
When this option is not provided it will default to 'none'.
Boolean Arguments (--boolean or --bool)
Boolean arguments, like Choice arguments, have additional behavior.
By default boolean arguments do not have a value. If the user does not specify the argument then
the variable will not be set. If the user provides the flag with --flag-name
then the value of
the boolean argument will be "true". However, users can also explicitly specify the value by using
--flag-name=false
. The user input will only accept "true" and "false". You can utilize the
--default
flag to ensure that this is always set.
Boolean arguments can not be repeated, can not have any ordinals, and can not be a catch-all. If you attempt to define one with any of these characteristics you will get a definition error.
Example:
$ argparse-sh --boolean happy -- --happy
HAPPY="true"
--negative-flag <flag>
Boolean arguments allow defining negative flags. These are flags that force the value to "false". Negation flags are explicitly defined.
Example:
$ argparse-sh --boolean happy --negative-flag "--not-happy" -- --not-happy
HAPPY="false"
This technique is particular useful when combined with either a --default
paramater or a
--required
parameter:
$ argparse-sh --boolean happy --negative-flag "--sad" --required -- --sad
HAPPY="false"
$ argparse-sh --boolean --name "HAPPY" --negative-flag "--sad" --default "true" --
HAPPY="true"
The first line requires that you include either --happy
or --sad
. If you don't include either
you will get a user error. If you include both you will get an error due to having multiple values
for "HAPPY".
The second line defines "HAPPY" as a boolean that defaults to "true", but can be made "false" by
including the --sad
argument.
Other Runtime Options
There are a handful of other options that can be used when running argparse-sh. These can be included
anywhere in the list of arguments, but it is recommended that you put these at the beginning or end
of the arguments. They can not be placed inside an argument definition (e.g.
argparse-sh --bool --debug --name bad-example
is not allowed).
--debug
Writes debugging information out via echo. This is useful when trying to determine why an argument is not behaving the way you expected.
Example:
$ eval "$(argparse-sh \
--choice gender --default "none" \
--option male "Person identifies as male" \
--option female "Person identifies as female" \
--map boy male \
--map girl female \
--option other "Person identifies as something else" \
--option none "Person declines to identify" \
--debug \
-- \
--gender boy)"
[ArgParse-sh] ArgParse-sh debugging enabled with --debug flag
[ArgParse-sh] Arguments are not exported to child processes
[ArgParse-sh]
[ArgParse-sh] Definition - type: Choice; name: GENDER; flags: gender; default: none; options: male, female, boy -> male, girl -> female, other, none
[ArgParse-sh]
[ArgParse-sh] Parsing argument values
[ArgParse-sh]
[ArgParse-sh] Parsed argument GENDER = 'male'
[ArgParse-sh]
[ArgParse-sh] Setting GENDER = "male"
[ArgParse-sh]
[ArgParse-sh] ArgParse-sh completed successfully
--auto-help
--export
--prefix <arg_prefix>
--program-name <name>
--program-summary <summary>
--program-description <description>
Exit Codes
-
0 - Success
When ArgParse-sh completes successfully the exit code will be 0. If this happens you can be sure that all required arguments have been set, all provided arguments have been parsed, and all type checks completed successfully.
-
1 - Help
If the
--autohelp
flag was used and the user passed in--help
then help text will be written to screen (using the user's PAGER if set) and ArgParse-sh will exit with a code of 1. -
2 - Definition Error
If there's an issue with the definition of the arguments the exit code will be 2. For example, not including an argument name after
--name
would generate this error. -
3 - User Error
This error code is returned if there is a problem with the arguments that the user provided. An omitted argument that is marked as required, or multiple values for arguments that are not repeated are examples of this.
If you run set -e
before calling argparse-sh, your script will automatically exit if ArgParse-sh
returns an exit code other than 0. You can also trap this error to recover more gracefully.
Putting it all together.
This is an example of a script using a wide variety of functionality along with best practices.
demo.sh
# Set shell to exit immediately after failed command.
set -e;
# The description is long, so we pulled it out into a variable for clarity.
PROGRAM_DESCRIPTION="This demo program provides a number of examples of how to use ArgParse-sh.
You can provide a number of arguments that are parsed and sent back to the wrapper script as
environment variables.
Feel free to save this script and run it with a variety of parameters to test things out.";
# Run ArgParse-sh with argument definitions and pass command line through.
eval "$(argparse-sh \
--string given-name first-name \
--description "Name given to you. In western cultures this is usually your first name." \
--required \
--string family-name last-name \
--description "Name inherited from your family. In western cultures this is usually your last name." \
--required \
--string nickname \
--name NICKNAMES \
--description "Nicknames that you are commonly known by." \
--repeated \
--integer age \
--description "Your age in years." \
--required \
--integer children \
--default 0 \
--secret \
--choice gender \
--description "The gender that you identify as." \
--option male "Person identifies as male" \
--map boy male \
--option female "Person identifies as female" \
--map girl female \
--option other "Person identifies as something else" \
--option none "Person declines to identify" \
--default none \
--boolean basic-data one-line single-line \
--description "Include this argument if you only want to see the first line in the output." \
--string quote \
--name QUOTES \
--description "Include one or more quotes that you find inspirational." \
--repeated \
--required \
--catch-all \
--auto-help \
--prefix "DEMO_" \
--program-name "$(basename "$0")" \
--program-summary "Sample script that uses argparse-sh to parse command line arguments." \
--program-description "$PROGRAM_DESCRIPTION" \
-- "$@")";
# Dump some of the basic variables to the screen.
echo "Hello $DEMO_GIVEN_NAME. I see you are $DEMO_AGE years old and have $DEMO_CHILDREN children.";
# We can test for existence of boolean variables
if [ "$DEMO_BASIC_DATA" = "true" ]; then
exit
fi
# We can use if statements to switch logic based on the results of a flag.
if [ "$DEMO_GENDER" = "male" ]; then
echo "You identify as male."
elif [ "$DEMO_GENDER" = "female" ]; then
echo "You identify as female."
elif [ "$DEMO_GENDER" = "other" ]; then
echo "You identify as something other than strictly male or female."
else
echo "You have declined to provide your gender identity."
fi
# We can test to see if variables are set, even for repeated arguments.
if [ -n "$DEMO_NICKNAMES" ]; then
echo ""
echo "You have $DEMO_NICKNAMES nickname(s):";
for (( i=0; i<$DEMO_NICKNAMES; i++ )); do
# We need to do this expansion to iterate over all of the values in the list.
NICKNAME=DEMO_NICKNAMES_$i
echo " ${!NICKNAME}";
done
fi
# If an argument is required you can be guaranteed that the value is set, even for repeated args.
echo ""
echo "You have $DEMO_QUOTES favorite quote(s):";
for (( i=0; i<$DEMO_QUOTES; i++ )); do
QUOTE=DEMO_QUOTES_$i
echo " ${!QUOTE}";
done
You can run this script with --help
to get the help text
Dependencies
~3.5–5MB
~77K SLoC