From 5dd8b08cce0f4b89edf97d8c3933061e5cc284d5 Mon Sep 17 00:00:00 2001 From: Benjamin Lupton Date: Sat, 19 Aug 2023 07:37:02 +0800 Subject: [PATCH] bash: clearer names and docs for the various helpers --- commands/choose-option | 2 +- commands/echo-style | 6 +- commands/is-inside | 4 +- commands/setup-dns | 14 ++-- commands/setup-linux | 2 +- commands/setup-mac | 2 +- commands/setup-mac-brew | 2 +- commands/setup-system | 2 +- commands/setup-util | 2 +- commands/what-is-my-dns | 2 +- commands/what-is-my-ip | 2 +- sources/bash.bash | 140 ++++++++++++++++++++++++---------------- 12 files changed, 106 insertions(+), 74 deletions(-) diff --git a/commands/choose-option b/commands/choose-option index ee5b48688..8534a7844 100755 --- a/commands/choose-option +++ b/commands/choose-option @@ -254,7 +254,7 @@ function choose_option() ( label="${labels[i]}" visual="${visuals[i]}" # compare their lowercase forms - if [[ "$(lc "$label")" == *"$(lc "$option_filter")"* ]] || [[ "$(lc "$value")" == *"$(lc "$option_filter")"* ]]; then + if [[ "$(lowercase_string "$label")" == *"$(lowercase_string "$option_filter")"* ]] || [[ "$(lowercase_string "$value")" == *"$(lowercase_string "$option_filter")"* ]]; then # there was a partial match, add it filtered_values+=("$value") filtered_labels+=("$label") diff --git a/commands/echo-style b/commands/echo-style index 6684249b2..be8de4a61 100755 --- a/commands/echo-style +++ b/commands/echo-style @@ -141,12 +141,12 @@ function echo_style() ( esac # if it don't exist, it may be a foreground shorthand - if ! testv "$style" && testv "foreground_$style"; then + if ! is_var_set "$style" && is_var_set "foreground_$style"; then style="foreground_$style" fi # return the color value - if testv "$style"; then + if is_var_set "$style"; then echo -n "${!style}" # determine the disable style @@ -160,7 +160,7 @@ function echo_style() ( else # custom disable, check if it exits disable_style="disable_$style" - if testv "$disable_style"; then + if is_var_set "$disable_style"; then # if it does, use it DISABLE+="${!disable_style}" else diff --git a/commands/is-inside b/commands/is-inside index 74a81651d..feea52a98 100755 --- a/commands/is-inside +++ b/commands/is-inside @@ -7,10 +7,10 @@ function is_inside() ( # convert to lower case local haystack needle - haystack="$(lc "$1")" + haystack="$(lowercase_string "$1")" for needle in "${@:2}"; do # convert to lowercase - needle="$(lc "$needle")" + needle="$(lowercase_string "$needle")" # check if it is present if [[ $haystack == *"$needle"* ]]; then diff --git a/commands/setup-dns b/commands/setup-dns index e24b7308a..2432ceb27 100755 --- a/commands/setup-dns +++ b/commands/setup-dns @@ -768,7 +768,7 @@ function setup_dns() ( # determine servers and action local servers action action_title action="$1" - action_title="$(ucf "$action")" + action_title="$(uppercase_first_letter "$action")" if test "$action" = 'enable'; then servers=( "${ipv4_servers[@]}" @@ -1020,7 +1020,7 @@ function setup_dns() ( if aghome_exists; then action='upgrade' fi - action_title="$(ucf "$action")" + action_title="$(uppercase_first_letter "$action")" # check if test -z "$aghome_installer"; then @@ -1092,7 +1092,7 @@ function setup_dns() ( function aghome_configure { local action action_title upstream_servers server pattern replace action="$1" # enable/disable - action_title="$(ucf "$action")" + action_title="$(uppercase_first_letter "$action")" # check if ! aghome_exists; then @@ -1184,7 +1184,7 @@ function setup_dns() ( if dnscrypt_exists; then action='upgrade' fi - action_title="$(ucf "$action")" + action_title="$(uppercase_first_letter "$action")" # check if test -z "$dnscrypt_installer"; then @@ -1275,7 +1275,7 @@ function setup_dns() ( function dnscrypt_configure { local action action_title temp_conf_file dnscrypt_options action="$1" # enable/disable - action_title="$(ucf "$action")" + action_title="$(uppercase_first_letter "$action")" # check if ! dnscrypt_exists; then @@ -1373,7 +1373,7 @@ function setup_dns() ( if cloudflared_exists; then action='upgrade' fi - action_title="$(ucf "$action")" + action_title="$(uppercase_first_letter "$action")" # check if test -z "$cloudflared_installer"; then @@ -1447,7 +1447,7 @@ function setup_dns() ( function cloudflared_configure { local action action_title upstream_servers upstream_section upstream_args server action="$1" # enable/disable - action_title="$(ucf "$action")" + action_title="$(uppercase_first_letter "$action")" upstream_section='' upstream_args='' diff --git a/commands/setup-linux b/commands/setup-linux index bd8a7708a..028ac6a6f 100755 --- a/commands/setup-linux +++ b/commands/setup-linux @@ -52,7 +52,7 @@ function setup_linux() ( # generate log title local title - title="$(ucf "$action") Linux" + title="$(uppercase_first_letter "$action") Linux" # ===================================== # Configuration diff --git a/commands/setup-mac b/commands/setup-mac index b355f06db..6975cc282 100755 --- a/commands/setup-mac +++ b/commands/setup-mac @@ -50,7 +50,7 @@ function setup_mac() ( # generate log title local title - title="$(ucf "$action") macOS" + title="$(uppercase_first_letter "$action") macOS" # ===================================== # Action diff --git a/commands/setup-mac-brew b/commands/setup-mac-brew index 06c88c677..64b7e5a65 100755 --- a/commands/setup-mac-brew +++ b/commands/setup-mac-brew @@ -198,7 +198,7 @@ function setup_mac_brew() ( # generate log title local title - title="Setup/$(ucf "$action") Homebrew" + title="Setup/$(uppercase_first_letter "$action") Homebrew" # ===================================== # Action Helpers diff --git a/commands/setup-system b/commands/setup-system index 5c191e3dc..d2c83d621 100755 --- a/commands/setup-system +++ b/commands/setup-system @@ -45,7 +45,7 @@ function setup_system() ( # generate log title local title - title="$(ucf "$action") System" + title="$(uppercase_first_letter "$action") System" # ===================================== # Action diff --git a/commands/setup-util b/commands/setup-util index 0bcf5063b..40d88a888 100755 --- a/commands/setup-util +++ b/commands/setup-util @@ -1816,7 +1816,7 @@ function setup_util() ( while read -r id name; do # trim version from the name, it doesn't work in the `read` arguments, as spaces inside name will be considered the version name="${name%% *}" - if test "$(lc "$name")" = "$(lc "$arg")"; then + if test "$(lowercase_string "$name")" = "$(lowercase_string "$arg")"; then exact+=("$id" "$name") else options+=("$id" "$name") diff --git a/commands/what-is-my-dns b/commands/what-is-my-dns index e01db330d..cdc469194 100755 --- a/commands/what-is-my-dns +++ b/commands/what-is-my-dns @@ -122,7 +122,7 @@ function what_is_my_dns() ( "get_dns_${__types[0]}" else for __type in "${__types[@]}"; do - printf '%s: ' "$(ucf "$__type")" + printf '%s: ' "$(uppercase_first_letter "$__type")" "get_dns_$__type" done fi diff --git a/commands/what-is-my-ip b/commands/what-is-my-ip index 59ada9c06..9b1de7021 100755 --- a/commands/what-is-my-ip +++ b/commands/what-is-my-ip @@ -93,7 +93,7 @@ function what_is_my_ip() ( "get_ip_${__types[0]}" else for __type in "${__types[@]}"; do - printf '%s: ' "$(ucf "$__type")" + printf '%s: ' "$(uppercase_first_letter "$__type")" "get_ip_$__type" done fi diff --git a/sources/bash.bash b/sources/bash.bash index fc3aa66d5..873f7c23a 100755 --- a/sources/bash.bash +++ b/sources/bash.bash @@ -1,11 +1,24 @@ #!/usr/bin/env bash # trunk-ignore-all(shellcheck/SC2034) -# Thread that discusses and tracks bash version compatibility: -# https://github.com/bevry/dorothy/discussions/151 - -# ------------------------------------- -# Version Extraction +# For bash version compatibility and changes, see: +# See for documentation about signficant changes between bash versions. +# See for documentation on changes from bash v2 and above. + +# For bash configuration options, see: +# https://www.gnu.org/software/bash/manual/bash.html#The-Set-Builtin +# https://www.gnu.org/software/bash/manual/bash.html#The-Shopt-Builtin + +# ============================================================================= +# Determine the bash version information, which is used to determine if we can use certain features or not. +# +# require_upgraded_bash -- BASH_VERSION_CURRENT != BASH_VERSION_LATEST, fail. +# BASH_VERSION_CURRENT -- 5.2.15(1)-release => 5.2.15 +# BASH_VERSION_MAJOR -- 5 +# BASH_VERSION_MINOR -- 2 +# BASH_VERSION_PATCH -- 15 +# BASH_VERSION_LATEST -- 5.2.15 +# IS_BASH_VERSION_OUTDATED -- yes/no if test -z "${BASH_VERSION_CURRENT-}"; then # 5.2.15(1)-release => 5.2.15 @@ -29,12 +42,13 @@ if test -z "${BASH_VERSION_CURRENT-}"; then fi fi -# ------------------------------------- -# SHELL OPTIONS - -# https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html -# https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html -# https://github.com/bminor/bash/blob/master/CHANGES +# ============================================================================= +# Configure bash for Dorothy best practices. +# +# require_lastpipe -- if lastpipe not supported, fail. +# eval_capture -- capture or ignore exit status, without disabling errexit, and without a subshell. +# require_globstar -- if globstar not supported, fail. +# require_extglob -- if extglob not supported, fail. # Disable completion (not needed in scripts) # bash v2: progcomp: If set, the programmable completion facilities (see Programmable Completion) are enabled. This option is enabled by default. @@ -88,8 +102,9 @@ function eval_capture { '--help') cat <<-EOF >/dev/stderr ABOUT: - Capture or ignore exit codes and outputs, without disabling errexit. + Capture or ignore exit status, without disabling errexit, and without a subshell. Copyright 2023+ Benjamin Lupton (https://balupton.com) + Written for Dorothy (https://github.com/bevry/dorothy) Licensed under the CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/) For more information: https://gist.github.com/balupton/21ded5cefc26dc20833e6ed606209e1b @@ -292,103 +307,112 @@ fi # basg v1?: localvar_unset: If set, calling unset on local variables in previous function scopes marks them so subsequent lookups find them unset until that function returns. This is identical to the behavior of unsetting local variables at the current function scope. # shopt -s localvar_unset 2>/dev/null || : -# ------------------------------------- -# Shell Paramater Expansions +# ============================================================================= +# Shim bash functionality that is inconsistent between bash versions. +# Shim Paramater Expansions # https://www.gnu.org/software/bash/manual/bash.html#Shell-Parameter-Expansion -# ucf = upper case first letter -# lc = lower case all +# +# uppercase_first_letter +# lowercase_string + if test "$BASH_VERSION_MAJOR" -eq 5 -a "$BASH_VERSION_MINOR" -ge 1; then # >= bash v5.1 - function ucf { + function uppercase_first_letter { echo "${1@u}" } - function lc { + function lowercase_string { echo "${1@L}" } elif test "$BASH_VERSION_MAJOR" -eq 4; then # >= bash v4.0 - function ucf { + function uppercase_first_letter { echo "${1^}" } - function lc { + function lowercase_string { echo "${1,,}" } else # < bash v4.0 - function ucf { + function uppercase_first_letter { echo "$1" # not important, implement later } - function lc { + function lowercase_string { echo "$1" # not important, implement later } fi -# ------------------------------------- -# test a variable is defined: test -v +# Shim Conditional Expressions +# -v varname: True if the shell variable varname is set (has been assigned a value). +# https://www.gnu.org/software/bash/manual/bash.html#Bash-Conditional-Expressions +# +# is_var_set if test "$BASH_VERSION_MAJOR" -ge 5 || test "$BASH_VERSION_MAJOR" -eq 4 -a "$BASH_VERSION_MINOR" -ge 2; then # >= bash v4.2 - function testv { + function is_var_set { test -v "$1" } else # < bash v4.2 - function testv { + function is_var_set { test -n "${!1-}" } fi -# ------------------------------------- -# Arrays -# This was some amazing work by balupton, if you extract it, be sure to thank him - -function has_array_support { +# Shim Array Support +# Bash v4 has the following capabilities, which must be shimmed in earlier versions: +# - `readarray` and `mapfile` +# - our shim provides a workaround +# - associative arrays +# - no workaround, you are out of luck +# - iterating empty arrays: +# - broken: `arr=(); for item in "${arr[@]}"; do ...` +# - broken: `arr=(); for item in "${!arr[@]}"; do ...` +# - use: `test "${#array[@]}" -ne 0 && for ...` +# - or if you don't care for empty elements, use: `test -n "$arr" && for ...` +# +# BASH_ARRAY_CAPABILITIES -- string that stores the various capaibilities: mapfile[native] mapfile[shim] readarray[native] empty[native] empty[shim] associative +# has_array_capability -- check if a capability is provided by the current bash version +# require_array -- require a capability to be provided by the current bash version, otherwise fail +# mapfile -- shim [mapfile] for bash versions that do not have it + +function has_array_capability { for arg in "$@"; do - if [[ $ARRAYS != *" $arg"* ]]; then + if [[ $BASH_ARRAY_CAPABILITIES != *" $arg"* ]]; then return 1 fi done } function require_array { - if ! has_array_support "$@"; then + if ! has_array_capability "$@"; then echo-style --error='Array support insufficient, required:' ' ' --code="$*" require_upgraded_bash fi } -ARRAYS='' +BASH_ARRAY_CAPABILITIES='' if test "$BASH_VERSION_MAJOR" -ge '5'; then - ARRAYS+=' mapfile[native] readarray[native] empty[native]' + BASH_ARRAY_CAPABILITIES+=' mapfile[native] readarray[native] empty[native]' if test "$BASH_VERSION_MINOR" -ge '1'; then - ARRAYS+=' associative' + BASH_ARRAY_CAPABILITIES+=' associative' fi elif test "$BASH_VERSION_MAJOR" -ge '4'; then - ARRAYS+=' mapfile[native] readarray[native]' + BASH_ARRAY_CAPABILITIES+=' mapfile[native] readarray[native]' if test "$BASH_VERSION_MINOR" -ge '4'; then - ARRAYS+=' empty[native]' + BASH_ARRAY_CAPABILITIES+=' empty[native]' else - ARRAYS+=' empty[shim]' + BASH_ARRAY_CAPABILITIES+=' empty[shim]' set +u # disable nounset to prevent crashes on empty arrays fi elif test "$BASH_VERSION_MAJOR" -ge '3'; then - ARRAYS+=' mapfile[shim] empty[shim]' + BASH_ARRAY_CAPABILITIES+=' mapfile[shim] empty[shim]' set +u # disable nounset to prevent crashes on empty arrays - # bash v4 features: - # - `readarray` and `mapfile` - # - our shim provides a workaround - # - associative arrays - # - no workaround, you are out of luck - # - iterating empty arrays: - # - broken: `arr=(); for item in "${arr[@]}"; do ...` - # - broken: `arr=(); for item in "${!arr[@]}"; do ...` - # - use: `test "${#array[@]}" -ne 0 && for ...` - # - or if you don't care for empty elements, use: `test -n "$arr" && for ...` function mapfile { - # if you copy and paste this, please give credit: - # written by Benjamin Lupton https://balupton.com - # written for Dorothy https://github.com/bevry/dorothy + # Copyright 2021+ Benjamin Lupton (https://balupton.com) + # Written for Dorothy (https://github.com/bevry/dorothy) + # Licensed under the CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/) local delim=$'\n' item if test "$1" = '-t'; then shift @@ -403,4 +427,12 @@ elif test "$BASH_VERSION_MAJOR" -ge '3'; then done } fi -ARRAYS+=' ' +BASH_ARRAY_CAPABILITIES+=' ' + +# ============================================================================= +# Additional helpers to work around bash pecularities. +# +# print -- echo has a few flaws, notably if the string argument is actually a echo argument, then it will not be output, e.g. [echo '-n'] will not output [-n] +function print { + printf '%s\n' "$*" +}