Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable long, GNU style options RE: issue-1 #3

Merged
merged 2 commits into from
Feb 16, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 64 additions & 32 deletions main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,27 @@
### Configuration
#####################################################################

# Exit on error. Append ||true if you expect an error.
# `set` is safer than relying on a shebang like `#!/bin/bash -e` because that is neutralized
# when someone runs your script as `bash yourscript.sh`
set -o errexit
set -o nounset

# Bash will remember & return the highest exitcode in a chain of pipes.
# This way you can catch the error in case mysqldump fails in `mysqldump |gzip`
set -o pipefail

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More robust error checking from the beginning

# Environment variables and their defaults
LOG_LEVEL="${LOG_LEVEL:-6}" # 7 = debug -> 0 = emergency

# Commandline options. This defines the usage page, and is used to parse cli
# opts & defaults from. The parsing is unforgiving so be precise in your syntax
read -r -d '' usage <<-'EOF'
-f [arg] Filename to process. Required.
-t [arg] Location of tempfile. Default="/tmp/bar"
-d Enables debug mode
-h This page
read -r -d '' usage <<-'EOF' || true # exits non-zero when EOF encountered
-f --file [arg] Filename to process. Required.
-t --temp [arg] Location of tempfile. Default="/tmp/bar"
-v Enable verbose mode, print script as it is executed
-d --debug Enables debug mode
-h --help This page
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second field is examined for leading -- if found, that is determined to be a long version of the option. N.B. All long options must have corresponding short options, but not vice-versa. This could be changed without too much work, but for now, this is more consistent with the existing code and intentions, IMO.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A very acceptable compromise imo too

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

EOF

# Set magic variables for current file and its directory.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to delete the whitespace at EOL error... because OCD

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes of course : ) I think I have turned it off in my editor for markdown trailing space preservation. I should really make a case-by-case exception going forward 😄

Expand All @@ -56,7 +67,7 @@ function _fmt () {
fi

local color_reset="\x1b[0m"
if [[ "${TERM}" != "xterm"* ]] || [ -t 1 ]; then
if [[ "${TERM:-}" != "xterm"* ]] || [ -t 1 ]; then
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow some variables to be unset, without causing errors, mostly those provided by the system and built-ins that should be set in most reasonable environments, but if not, they won't cause problems.

# Don't use colors on pipes or non-recognized terminals
color=""; color_reset=""
fi
Expand Down Expand Up @@ -91,14 +102,26 @@ trap cleanup_before_exit EXIT

# Translate usage string -> getopts arguments, and set $arg_<flag> defaults
while read line; do
opt="$(echo "${line}" |awk '{print $1}' |sed -e 's#^-##')"
# fetch single character version of option sting
opt="$(echo "${line}" |awk 'match($1,"^-.",a){print substr(a[0],2,1)}')"

# fetch long version if present
long_opt="$(echo "${line}" |awk 'match($2,"^--.*",a){print substr(a[0],3)}')"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get the long option name from the second field, if it exists

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be cool to use one method for long/short opt parsing. E.g. go awk only for both?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure I can do that

# map long name back to short name
varname="short_opt_${long_opt}"
eval "${varname}=\"${opt}\""
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll use this later to make long options get processed as if the short option was used


# check if option takes an argument
varname="has_arg_${opt}"
if ! echo "${line}" |egrep '\[.*\]' >/dev/null 2>&1; then
init="0" # it's a flag. init with 0
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line was unused, dead code so I removed it.

eval "${varname}=0"
else
opt="${opt}:" # add : if opt has arg
init="" # it has an arg. init with ""
eval "${varname}=1"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need some additional means of determining whether the long options have required arguments or not; the : doesn't apply in the normal sense to them

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that even before these changes, if a required option argument was missing, subsequent options/text would get parsed as the required argument. It might be sensible to check that required arguments aren't just being populated with additional options/arguments. See also #5

fi
opts="${opts}${opt}"
opts="${opts:-}${opt}"

varname="arg_${opt:0:1}"
if ! echo "${line}" |egrep '\. Default=' >/dev/null 2>&1; then
Expand All @@ -109,30 +132,44 @@ while read line; do
fi
done <<< "${usage}"

# Allow long options like --this
opts="${opts}-:"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the trick. I saw it here: http://mywiki.wooledge.org/ComplexOptionParsing (second code block)

# Reset in case getopts has been used previously in the shell.
OPTIND=1

# Overwrite $arg_<flag> defaults with the actual CLI options
while getopts "${opts}" opt; do
line="$(echo "${usage}" |grep "\-${opt}")"
[ "${opt}" = "?" ] && help "Invalid use of script: ${@} "

if [ "${opt}" = "-" ]; then #OPTARG is long-option-name or long-option=value
if [[ "${OPTARG}" =~ .*=.* ]]; then # --key=value format
long=${OPTARG/=*/}
eval "opt=\"\${short_opt_${long}}\"" # Set opt to the short option corresponding to the long option
OPTARG=${OPTARG#*=}
else # --key value format
eval "opt=\"\${short_opt_${OPTARG}}\"" # Map long name to short version of option
eval "OPTARG=\"\${@:OPTIND:\${has_arg_${opt}}}\"" # Only assign OPTARG if option takes an argument
((OPTIND+=has_arg_${opt})) # shift over the argument if argument is expected
fi
# we have set opt/OPTARG to the short value and the argument as OPTARG if it exists
else
varname="arg_${opt:0:1}"
default="${!varname}"

[ "${opt}" = "?" ] && help "Invalid use of script: ${@} "
varname="arg_${opt:0:1}"
default="${!varname}"
value="${OPTARG:-}"
if [ -z "${OPTARG:-}" ] && [ "${default}" = "0" ]; then
value="1"
fi

value="${OPTARG}"
if [ -z "${OPTARG}" ] && [ "${default}" = "0" ]; then
value="1"
eval "${varname}=\"${value}\""
debug "cli arg ${varname} = ($default) -> ${!varname}"
fi

eval "${varname}=\"${value}\""
debug "cli arg ${varname} = ($default) -> ${!varname}"
done

shift $((OPTIND-1))

[ "$1" = "--" ] && shift
[ "${1:-}" = "--" ] && shift


Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move to beginning for better error handling

### Switches (like -d for debugmode, -h for showing helppage)
Expand All @@ -144,6 +181,11 @@ if [ "${arg_d}" = "1" ]; then
LOG_LEVEL="7"
fi

# verbose mode
if [ "${arg_v}" = "1" ]; then
set -o verbose
fi

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hunk is just related to the additional -v flag.... could easily remove this and the extra definition in the usage string

# help mode
if [ "${arg_h}" = "1" ]; then
# Help exists with code 1
Expand All @@ -154,24 +196,14 @@ fi
### Validation (decide what's required for running your script and error out)
#####################################################################

[ -z "${arg_f}" ] && help "Setting a filename with -f is required"
[ -z "${LOG_LEVEL}" ] && emergency "Cannot continue without LOG_LEVEL. "
[ -z "${arg_f:-}" ] && help "Setting a filename with -f or --file is required"
[ -z "${LOG_LEVEL:-}" ] && emergency "Cannot continue without LOG_LEVEL. "


### Runtime
#####################################################################

# Exit on error. Append ||true if you expect an error.
# `set` is safer than relying on a shebang like `#!/bin/bash -e` because that is neutralized
# when someone runs your script as `bash yourscript.sh`
set -o errexit
set -o nounset

# Bash will remember & return the highest exitcode in a chain of pipes.
# This way you can catch the error in case mysqldump fails in `mysqldump |gzip`
set -o pipefail

if [[ "${OSTYPE}" == "darwin"* ]]; then
if [[ "${OSTYPE:-}" == "darwin"* ]]; then
info "You are on OSX"
else
info "You are on Linux"
Expand Down
9 changes: 5 additions & 4 deletions test/fixture/help.stdio
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ B3BP:STDIO_REPLACE_DATETIMES

Help using {root}/main.sh

-f [arg] Filename to process. Required.
-t [arg] Location of tempfile. Default="{tmpdir}/bar"
-d Enables debug mode
-h This page
-f --file [arg] Filename to process. Required.
-t --temp [arg] Location of tempfile. Default="{tmpdir}/bar"
-v Enable verbose mode, print script as it is executed
-d --debug Enables debug mode
-h --help This page

{datetime} UTC [ info] Cleaning up. Done