Skip to content

Instantly share code, notes, and snippets.

@fareedst
Last active December 26, 2022 19:02
Show Gist options
  • Save fareedst/8979cc7bd9c811ecd450b9be6d511cdf to your computer and use it in GitHub Desktop.
Save fareedst/8979cc7bd9c811ecd450b9be6d511cdf to your computer and use it in GitHub Desktop.
#!/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
@fareedst
Copy link
Author

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 a PROCESS environment variable.

./script.sh --process 1
PROCESS=1 ./script.sh

source env_opt_cli.sh
eoc_init_options "+opt/val/fix preview/1/0 process/0/1"
eoc_parse_input $*
rc=$?
while :; do
  (( rc == eoc_parse_satisfied )) && echo "COMPLETED by env_opt_cli" && break
  (( rc != 0 )) && echo "FAILED env_opt_cli" && break

  if (( preview == 1 )); then
    : # execute preview
  fi

  if (( process == 1 )); then
    : # execute process
  fi

  break
done

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