This is my best attempt so far at a bash script argument parser written without GNU getopt or bash getopts
the first two functions, usage and err can be more or less ignored, but I plan on adding the ability to specify an exit code when calling err.
Now for the first section of code in the main function:
shopt -s extglob
args=()
for (( i = 1; i <= "$#"; i++ )); do
arg="${!i}"
case "${arg}" in
-[[:alpha:]?]+([[:alpha:]?]))
for (( j = 1; j < "${#arg}"; j++ )); do
args+=("-${arg:j:1}")
done ;;
-[[:alpha:]]=*|--*=*)
args+=("${arg%%=*}")
args+=("${arg#*=}") ;;
*)
args+=("${arg}") ;;
esac
done
set -- "${args[@]}"
shopt -u extglob
This section de-concatenates flags and separates flags joined to their values with an =.
as an example, ./script -xYz --test=value would pop out as ./script -x -Y -z --test value. Most of this could likely have been combined with the second part, but I think there's at least a little value in being able to access/save the intermediate form, and it made debugging easier. Single letter flags with = can also be processed, but as I understand it, this isn't an extremely common thing to see anyway. Flags that are already in the correct format, flags that could not be reformatted due to user error (./script -xYz=value, for instance), and positional parameters would both be passed to the second part without any modification. This hasn't caused any issues yet, but I am considering trying to further differentiate between good/bad input. at the very end, the reformatted args are set for later use.
Part two of the main function:
args=()
for (( i = 1; i <= "$#"; i++ )); do
arg="${!i}"
case "${arg}" in
--)
break ;;
-*)
case "${arg}" in
-h|--help|-\?)
usage ;;
-x|-Y|-z) ;;
-t|--type)
arg2="${!i+1}"
if [[ -n "${arg2}" ]] && [[ "${arg2:0:1}" != "-" ]]; then
type="${arg2}"
(( i++ ))
else
err "Invalid option: ${arg} requires an argument"
fi ;;
-*)
err "Invalid option: ${arg}" ;;
esac ;;
*)
args+=("${arg}")
esac
done
set -- "${args[@]}"
This is where flags and their values actually get processed. Flags (and their arguments, if applicable) are checked one at a time, but only unused positional parameters are put back in the array to be set once again. For example ./script -x -Y -z --test value hello world would pop out as ./script hello world
I did what I could to prevent any special cases slipping through, and to account for as many common formats as possible, but I couldn't find a list of either, so I'd really appreciate advice on both of those issues.
I also have very little experience writing bash scripts, so general bash scripting advice would also be greatly appreciated.
Full code:
#!/bin/bash
usage() {
echo "help me"
exit 0
}
err() {
echo "$*" >&2
exit 1
}
shopt -s extglob
main() {
shopt -s extglob
args=()
for (( i = 1; i <= "$#"; i++ )); do
arg="${!i}"
case "${arg}" in
-[[:alpha:]?]+([[:alpha:]?]))
for (( j = 1; j < "${#arg}"; j++ )); do
args+=("-${arg:j:1}")
done ;;
-[[:alpha:]]=*|--*=*)
args+=("${arg%%=*}")
args+=("${arg#*=}") ;;
*)
args+=("${arg}") ;;
esac
done
set -- "${args[@]}"
shopt -u extglob
echo "$0 $@"
args=()
for (( i = 1; i <= "$#"; i++ )); do
arg="${!i}"
case "${arg}" in
--)
break ;;
-*)
case "${arg}" in
-h|--help|-\?)
usage ;;
-x|-Y|-z) ;;
-t|--type)
arg2="${!i+1}"
if [[ -n "${arg2}" ]] && [[ "${arg2:0:1}" != "-" ]]; then
type="${arg2}"
(( i++ ))
else
err "Invalid option: ${arg} requires an argument"
fi ;;
-*)
err "Invalid option: ${arg}" ;;
esac ;;
*)
args+=("${arg}")
esac
done
set -- "${args[@]}"
echo "$0 ${@:-No positional parameters set}"
echo "test: ${test:-Test not set}"
}
shopt -u extglob
main "$@"