Last active
December 26, 2022 19:02
-
-
Save fareedst/8979cc7bd9c811ecd450b9be6d511cdf to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
if [[ -n ${EOC_DEBUG:-} && ${-} != *x* ]] | |
then | |
export PS4='+${BASH_SOURCE[0]##*/}($LINENO)/${FUNCNAME[0]}> ' | |
# shellcheck disable=SC2120 | |
:() { | |
[[ ${1:--} != ::* ]] && return 0 | |
printf '%s\n' "${*}" >&2 | |
} | |
fi | |
# Add dead-simple command-line options to your shell script. | |
# | |
## Default values | |
# | |
# If your script `my.sh` starts like this, | |
# | |
# export FIRST=1 | |
# export LAST=10 | |
# | |
# or, | |
# | |
# first=1 | |
# last=10 | |
# | |
# Replace them with: | |
# | |
# source env_opt_cli.sh | |
# eoc_init_options "+option,value first,1 last,10" | |
# eoc_parse_input $* | |
# | |
# To accept calls such as: | |
# | |
# ./my.sh -> runs with first=1, last=10 | |
# ./my.sh --first 2 --last 9 -> runs with first=2, last=9 | |
# FIRST=3 LAST=8 ./my.sh -> runs with first=3, last=8 | |
# ./my.sh --help -> displays options for script | |
# | |
# HELP output: | |
# | |
# Options: | |
# parameter | env var | shell var | value | description | |
# ---------- | ------------ | ------------ | ------------- | ------------------------ | |
# --first | FIRST | first | 1 | | |
# --last | LAST | last | 10 | | |
# --help | HELP | help | 1 | Print options | |
# | |
# Access options either as shell variables or as environment variables: | |
# | |
# (( first == 1 )) | |
# [[ $FIRST == 1 ]] | |
# | |
## Enabled options | |
# | |
# The automatic `--help` command is an option that does not take a parameter. | |
# This type of option is defined with an initial default value and a fixed value set when the option is specified. | |
# | |
# source env_opt_cli.sh | |
# eoc_init_options "+option,value,fixed command,0,1" | |
# eoc_parse_input $* | |
# (( command == 0 )) && echo "--command not used" | |
# (( command == 1 )) && echo "--command was used" | |
# | |
## Complete properties | |
# | |
# Each option has the following possible customized properties. | |
# v value (default) default value for option | |
# d description description for option | |
# e envvar_name name of environment variable | |
# f fixed_value value set when option is called | |
# o option_name name of option | |
# s shlvar_name name of shell variable | |
# | |
# Options are named in the template by the first letter. Extra characters are ignored. | |
# The separator between properties and the separator between options is flexible | |
# and computed from the input template. | |
# Example templates: +option,value +opt,val,fix +v;f;o;d +o/v/f/d/e/s | |
# | |
## Global namespaces | |
# | |
# Collision with existing environment or shell variables can be prevented with: | |
# eoc_set_prefix_envvar_name | |
# eoc_set_prefix_shlvar_name | |
# | |
## Testing | |
# | |
# This script can be used for basic exploration. | |
# Call without parameters to see a Help table | |
# ./env_opt_cli.sh | |
# ./env_opt_cli.sh --help | |
# Test setting option values | |
# ./env_opt_cli.sh --first 3 --help | |
# LAST=4 ./env_opt_cli.sh --enable --help | |
# return codes | |
# | |
eoc_parse_satisfied=1 | |
eoc_option_not_recognized=2 | |
eoc_incomplete_option=3 | |
eoc_error=4 | |
# default parser settings | |
# | |
eoc_opt_sep=$'\n' | |
eoc_prop_sep=";" | |
gpi_default_value=1 | |
gpi_description=3 | |
gpi_envvar_name=-1 | |
gpi_fixed_value=2 | |
gpi_option_name=0 | |
gpi_shlvar_name=-1 | |
# prefixes for global names | |
# | |
gpp_envvar_name='' | |
gpp_shlvar_name='' | |
# show_stack output filtering | |
# | |
declare -a prior_bash_lineno=() | |
prior_bash_lineno_size=0 | |
# parsed options | |
# | |
declare -a goptions=() | |
declare -a gprops=() | |
# print error and return | |
# | |
die () { | |
echo >&2 "$0 (${FUNCNAME[1]} ${BASH_LINENO[0]}) ERROR: $*" | |
return $eoc_error | |
} | |
# print debug output | |
# | |
info () { | |
[[ -z $EOC_DEBUG ]] && return 0 | |
show_stack | |
echo "$(strnrepeat . ${#FUNCNAME[@]}) $*" 2>/dev/tty | |
} | |
# # print function positional parameter | |
# # $1 position | |
# # | |
# info_pp () { | |
# [[ -z $EOC_DEBUG ]] && return 0 | |
# (( #* == 0 )) && return 0 | |
# echo "$(strnrepeat . ${#FUNCNAME[@]}) info_pp: $*" >/dev/stderr | |
# } | |
# print call stack | |
# | |
show_stack () { | |
local bl_size="${#BASH_LINENO[@]}" | |
local new=0 | |
local bl_ind bl_off indent | |
for (( bl_off = 2; bl_off < bl_size; bl_off++ )); do | |
bl_ind=$(( bl_size - bl_off )) | |
if (( prior_bash_lineno_size == 0 )) || (( new == 1 )) || [[ "${BASH_LINENO[$(( bl_ind ))]}" != "${prior_bash_lineno[$(( prior_bash_lineno_size - bl_off ))]}" ]]; then | |
new=1 | |
indent=$(( bl_size - bl_ind - 1 )) | |
echo "$(strnrepeat + $indent) ${FUNCNAME[(( bl_ind + 1 ))]}(): ${BASH_LINENO[$bl_ind]}" | |
fi | |
done | |
(( new == 1 )) && prior_bash_lineno=("${BASH_LINENO[@]}") && prior_bash_lineno_size=("${#BASH_LINENO[@]}") | |
} | |
# access global option properties | |
# | |
gp_default_value () { | |
if (( gpi_default_value != -1 )); then | |
echo -n "${gprops[$gpi_default_value]}" | |
fi | |
} | |
gp_description () { | |
if (( gpi_description != -1 )); then | |
echo -n "${gprops[$gpi_description]}" | |
fi | |
} | |
gp_envvar_name () { | |
if (( gpi_envvar_name == -1 )) || [[ -z ${gprops[$gpi_envvar_name]} ]]; then | |
echo -n "${gpp_envvar_name}${gprops[$gpi_option_name]}" | tr '[:lower:]' '[:upper:]' | |
else | |
echo -n "${gpp_envvar_name}${gprops[$gpi_envvar_name]}" | |
fi | |
} | |
gp_fixed_value () { | |
if (( gpi_fixed_value != -1 )); then | |
echo -n "${gprops[$gpi_fixed_value]}" | |
fi | |
} | |
gp_option_name () { | |
echo -n "${gprops[$gpi_option_name]}" | |
} | |
gp_shlvar_name () { | |
if (( gpi_shlvar_name == -1 )) || [[ -z ${gprops[$gpi_shlvar_name]} ]]; then | |
echo -n "${gpp_shlvar_name}${gprops[$gpi_option_name]}" | tr '[:upper:]' '[:lower:]' | |
else | |
echo -n "${gpp_shlvar_name}${gprops[$gpi_shlvar_name]}" | |
fi | |
} | |
# add default HELP option | |
# | |
eoc_add_help_option () { | |
local i | |
local str="" | |
for i in {0..9}; do | |
(( gpi_default_value == i )) && str+="0${eoc_prop_sep}" | |
(( gpi_description == i )) && str+="Print options${eoc_prop_sep}" | |
(( gpi_envvar_name == i )) && str+="${gpp_envvar_name}HELP${eoc_prop_sep}" | |
(( gpi_fixed_value == i )) && str+="1${eoc_prop_sep}" | |
(( gpi_shlvar_name == i )) && str+="${gpp_shlvar_name}help${eoc_prop_sep}" | |
(( gpi_option_name == i )) && str+="help${eoc_prop_sep}" | |
done | |
goptions+=("$str") | |
} | |
# add default TRACE option | |
# | |
eoc_add_trace_option () { | |
local i | |
local str="" | |
for i in {0..9}; do | |
(( gpi_default_value == i )) && str+="0${eoc_prop_sep}" | |
(( gpi_description == i )) && str+="Start trace${eoc_prop_sep}" | |
(( gpi_envvar_name == i )) && str+="${gpp_envvar_name}TRACE${eoc_prop_sep}" | |
(( gpi_fixed_value == i )) && str+="1${eoc_prop_sep}" | |
(( gpi_shlvar_name == i )) && str+="${gpp_shlvar_name}trace${eoc_prop_sep}" | |
(( gpi_option_name == i )) && str+="trace${eoc_prop_sep}" | |
done | |
goptions+=("$str") | |
} | |
# read environment variable | |
# $1 env var name | |
# | |
eoc_fetch_envvar_value () { | |
printenv "$1" | |
} | |
# set options to current environment value or default value | |
# | |
eoc_initial_value () { | |
local currenv option | |
for option in "${goptions[@]}"; do | |
info "option: $option" | |
IFS="${eoc_prop_sep}" gprops=($option) | |
currenv=$(eoc_fetch_envvar_value "$(gp_envvar_name)") | |
if [[ -z $currenv ]]; then | |
eoc_publish_option_value "$(gp_default_value)" | |
else | |
eoc_publish_option_value "$currenv" | |
fi | |
done | |
} | |
# find option name in goptions | |
# publish input or fixed value | |
# $1 option name with hyphens | |
# $2 value (not for fixed option) | |
# return: # to shift; 0 indicates failure | |
# | |
eoc_match_option () { | |
local option | |
for option in "${goptions[@]}"; do #; echo "option: $option" | |
IFS="${eoc_prop_sep}" gprops=($option) | |
if [[ $1 == --$(gp_option_name) ]]; then | |
if [[ -z $(gp_fixed_value) ]]; then | |
eoc_publish_option_value "${2}" | |
return 2 | |
else | |
eoc_publish_option_value "$(gp_fixed_value)" | |
return 1 | |
fi | |
fi | |
done | |
return 0 | |
} | |
# parse input string for options | |
# value precendence: option default < environment value < input value | |
# publish new value | |
# store non-option input | |
# | |
# $@ input strings | |
# | |
eoc_parse_input () { | |
eoc_initial_value | |
# set option to input value | |
# | |
# - loop over $@ | |
# - match $1, $2 as option, value | |
# - abort if option is not recognized | |
# - consumes $@ with shift | |
# | |
local rest=() | |
eoc_rest=() | |
while :; do | |
if [[ "$*" == "" ]]; then break ; fi | |
eoc_match_option "$1" "$2" | |
local shift_num=$? | |
if (( shift_num > 0 )); then | |
if ! shift $shift_num; then | |
die "Missing parameter" | |
return $eoc_incomplete_option | |
fi | |
elif [[ $1 =~ ^--.+ ]]; then | |
# unmatched option | |
# | |
die "Option not recognized: $*" | |
return $eoc_option_not_recognized | |
else | |
# non-option input | |
# | |
eoc_rest+=($1) | |
shift | |
fi | |
done | |
if (( $(eoc_fetch_envvar_value "${gpp_envvar_name}HELP") == 1 )); then | |
# print final values and exit | |
# | |
show_help | |
return $eoc_parse_satisfied | |
fi | |
if (( $(eoc_fetch_envvar_value "${gpp_envvar_name}TRACE") == 1 )); then | |
trace_start | |
fi | |
} | |
# formatting for options help | |
# | |
w0=10 | |
w1=12 | |
w2=12 | |
w3=13 | |
w4=24 | |
fmt="%-${w0}s | %-${w1}s | %-${w2}s | %-${w3}s | %-${w4}s\n" | |
# print options in table | |
# | |
show_help () { | |
echo "Options:" | |
printf "$fmt" parameter 'env var' 'shell var' value description | |
printf "$fmt" \ | |
"$(strnrepeat - $w0)" \ | |
"$(strnrepeat - $w1)" \ | |
"$(strnrepeat - $w2)" \ | |
"$(strnrepeat - $w3)" \ | |
"$(strnrepeat - $w4)" | |
local option | |
for option in "${goptions[@]}"; do | |
IFS="${eoc_prop_sep}" gprops=($option) | |
printf "$fmt" \ | |
"--$(gp_option_name)" \ | |
"$(gp_envvar_name)" \ | |
"$(gp_shlvar_name)" \ | |
"$(eoc_fetch_envvar_value "$(gp_envvar_name)")" \ | |
"$(gp_description)" | |
done | |
} | |
trace_start () { | |
export PS4='+${BASH_SOURCE[0]##*/}($LINENO)/${FUNCNAME[0]}> ' | |
set -x | |
} | |
trace_stop () { | |
set +x | |
} | |
# # print options in table | |
# # | |
# print_options () { | |
# echo "print_options()" | |
# local option | |
# for option in "${goptions[@]}"; do | |
# IFS="${eoc_prop_sep}" gprops=($option) | |
# printf "$fmt" "$(gp_option_name)" "$(gp_envvar_name)" "$(eoc_fetch_envvar_value "$(gp_envvar_name)")" "$(gp_description)" | |
# done | |
# } | |
# read new separators from template | |
# 1: template with property definition | |
# | |
eoc_read_seps () { | |
if [[ ${1:$i:1} == + ]]; then | |
eoc_opt_sep='' | |
eoc_prop_sep='' | |
local i | |
for (( i=1; i<${#1}; i++ )); do | |
if [[ ${1:$i:1} =~ [^a-z] ]]; then | |
if [[ -z $eoc_prop_sep ]]; then | |
eoc_prop_sep="${1:$i:1}" | |
elif [[ "$eoc_prop_sep" != "${1:$i:1}" ]]; then | |
eoc_opt_sep="${1:$i:1}" | |
break | |
fi | |
fi | |
done | |
fi | |
} | |
# parse options | |
# $1 options specification | |
# options joined with $2, | |
# option properties joined with $3 | |
# properties: option name, environment name, default value, enabled value | |
# in options string: --option_name=default | |
# in environment: ENVIRONMENT_NAME=default | |
# | |
eoc_init_options () { | |
# info_pp 1 $* | |
info "#1: $1" | |
eoc_read_seps "$1" | |
IFS="${eoc_opt_sep}" goptions=($1) | |
if [[ ${goptions[0]} =~ ^\+ ]]; then | |
eoc_set_template "${goptions[0]:1}" | |
# shellcheck disable=SC2184 | |
unset goptions[0] | |
fi | |
eoc_add_help_option | |
eoc_add_trace_option | |
} | |
# publish option value to environment and shell variables | |
# $1 option value | |
# | |
eoc_publish_option_value () { | |
info "gp_envvar_name: $(gp_envvar_name) | |
gp_shlvar_name: $(gp_shlvar_name) | |
value: $1" | |
export "$(gp_envvar_name)"="$1" | |
eval "$(gp_shlvar_name)='${1}'" | |
} | |
# set environment variable prefix | |
# $1 prefix for environment variable name with current option value | |
# | |
eoc_set_prefix_envvar_name () { | |
gpp_envvar_name="$1" | |
} | |
# set shell variable prefix | |
# $1 prefix for variable name with current option value | |
# | |
eoc_set_prefix_shlvar_name () { | |
gpp_shlvar_name="$1" | |
} | |
# set separator for options | |
# $1 option sep | |
# | |
eoc_set_opt_sep () { | |
eoc_opt_sep="$1" | |
} | |
# set separator for properties | |
# $1 prop sep | |
# | |
eoc_set_prop_sep () { | |
eoc_prop_sep="$1" | |
} | |
# set gpi_* according to template | |
# $1 option template | |
# expects only first letter of property name (d,e,f,o,s,v) but allows any name | |
# invalid if not option name is included | |
# append description and fixed value columns for help command | |
# | |
eoc_set_template () { | |
gpi_default_value=-1 | |
gpi_description=-1 | |
gpi_envvar_name=-1 | |
gpi_fixed_value=-1 | |
gpi_shlvar_name=-1 | |
gpi_option_name=-1 | |
local ind prop temps | |
IFS="${eoc_prop_sep}" temps=($1) | |
ind=0 | |
for prop in "${temps[@]}"; do | |
case "$prop" in | |
d*) gpi_description=$ind ;; | |
e*) gpi_envvar_name=$ind ;; | |
f*) gpi_fixed_value=$ind ;; | |
o*) gpi_option_name=$ind ;; | |
s*) gpi_shlvar_name=$ind ;; | |
v*) gpi_default_value=$ind ;; | |
*) die "invalid property: $1" | |
break | |
;; | |
esac | |
(( ++ind )) | |
done | |
(( gpi_description == -1 )) && gpi_description=$ind && (( ind++ )) | |
(( gpi_fixed_value == -1 )) && gpi_fixed_value=$ind && (( ind++ )) | |
info "gpi_default_value: $gpi_default_value | |
gpi_description: $gpi_description | |
gpi_envvar_name: $gpi_envvar_name | |
gpi_fixed_value: $gpi_fixed_value | |
gpi_shlvar_name: $gpi_shlvar_name | |
gpi_option_name: $gpi_option_name" | |
(( gpi_option_name == -1 )) && die "invalid template: $1" | |
} | |
# print `str` on current line `count` times | |
# 1: str | |
# 2: count | |
# | |
strnrepeat () { | |
local i | |
for (( i = 1; i <= $2; i++ )); do | |
echo -n "$1" | |
done | |
} | |
# script self-execute | |
# *: parameters [--help] | |
# | |
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then | |
test_main () { | |
info "test_main()" | |
eoc_init_options "+o/v/f first/1 last/10 enable/0/1" | |
# shellcheck disable=SC2048 | |
# shellcheck disable=SC2086 | |
eoc_parse_input $* | |
} | |
# shellcheck disable=SC2048 | |
# shellcheck disable=SC2086 | |
test_main ${*:---help} | |
# shellcheck disable=SC2181 | |
if (( $? > 0 )); then | |
exit 1 | |
fi | |
fi | |
# 2022-12-26-19-00 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Basic example: the script below accepts
--preview
and--process
parameters.If no options are used, only the preview will be executed.
The process is executed by providing either a
process
option or aPROCESS
environment variable../script.sh --process 1
PROCESS=1 ./script.sh