Instantly share code, notes, and snippets.
Created
September 14, 2023 23:03
-
Save sellmerfud/158f0a6444c4520f3023c11667c6b965 to your computer and use it in GitHub Desktop.
Bourne shell function to prompt using a menu
This file contains 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 sh | |
# Present a menu and prompt user to select one of the given options. | |
# | |
# usage: | |
# prompt_menu "<choices>" "<cols>:<width>" prompt="Choose one: " delim=":" | |
# $1 - choices | |
# contains the values to be seleted separated by newlines | |
# Optionally, each value can be followed by a User friendly label that will | |
# be displayed for that value. The label if present must be appened to the value | |
# following a delimiter. The default delimiter is colon ':' | |
# $2 - columns and width | |
# By default the menu options are show in a single column. | |
# For large menus is it sometimes better to display the choices in | |
# more than one column. | |
# This argument may consist of one or two integers separated by a colon ':' | |
# The first (and possible only) integer is the number of columns desired. | |
# The second integer is the amount of screen width that the entire menu | |
# will use. The colums widths will be evenly divided among this screen | |
# width. If the <width> column is not given, then the scren width | |
# as reported by `tput cols` is used and if that fails then a value | |
# of 80 is used as a last resort. | |
# Normally, you will want to supply the width so that the menu does | |
# not look spread out on wide monitors. | |
# $3 - prompt | |
# The prompt string show to the user. If not given a default prompt | |
# of "Choose one: " is used. | |
# $4 - delim | |
# delimiter used to separate values from their associated labels. | |
# This must be a single character. | |
# If not given, a default delimiter of ':' is used. | |
# | |
# return | |
# The selected value is written to stdout | |
# Use command substitution to save the return value | |
# | |
# example: | |
# choices=' | |
# international:YYYY-MM-DD (ISO) | |
# us:MM-DD-YYYY (United States) | |
# europe:DD-MM-YYYY (Europe) | |
# ' | |
promptMenu() { | |
choices="$1" | |
prompt="${3:-"Choose one:"}" | |
delim="${4:-":"}" | |
default_width="`tput cols`" || default_width=80 | |
# Set up columns | |
if [ -z "$2" ]; then | |
num_cols="1" | |
menu_width="$default_width" | |
else | |
# Putting the IFS= before set was not working! | |
save_IFS="$IFS" | |
IFS=':' | |
# Do not quote $2 here! | |
set $2 | |
IFS="$save_IFS" | |
num_cols="${1}" | |
menu_width="${2:-$default_width}" | |
fi | |
separator="" | |
choices_file="/tmp/menu_choices.$$" | |
choice=-1 | |
# Trim leading and trailing whitespace | |
# from stdin and write to stdout. | |
trim() { | |
sed -e 's/^[ \t\n]*//' -e 's/[ \t\n]*$//' | |
} | |
# Return 0 if the first argument is all digits | |
is_numeric() | |
{ | |
echo "$1" | grep -E -e '^[0-9]+$' >/dev/null 2>&1 | |
} | |
# Pass in name of function to call for each value/label pair | |
# $1 - the number of the chosen value | |
# Writes the value to stdout | |
value_for_index() { | |
while IFS="$delim" read index value label; do | |
if [ "$index" -eq "$1" ]; then | |
printf "%s" "$value" | |
return 0 | |
fi | |
done <"$choices_file" | |
return 1 | |
} | |
iterate_choices() { | |
lambda="$1" | |
shift | |
while IFS="$delim" read index value label; do | |
[ -z "$label" ] && label="$value" | |
"$lambda" "$index" "$value" "$label" "$@" | |
done <"$choices_file" | |
} | |
get_max_label_len() { | |
max_len=0 | |
get_max() { | |
this_max="`printf "$3" | wc -c | trim`" | |
[ "$this_max" -gt "$max_len" ] && max_len="$this_max" | |
} | |
iterate_choices "get_max" | |
unset get_max | |
printf "%s" $max_len | |
} | |
# Write the choices to stdout | |
show_choices() { | |
print_one() { | |
printf "%${num_width}d) %s\n" "$1" "$3" | |
} | |
# Use pr to handle the display of columns | |
iterate_choices "print_one" | pr -t "-$num_cols" "-w$menu_width" | |
unset print_one | |
} | |
if [ -z "$choices" ]; then | |
printf "promptMenu: first argument (choices) is required\n" >&2 | |
return 1 | |
fi | |
# Put the choices in a file. | |
# We first normalize the entries: | |
# - Skip blank lines | |
# - Remove leading whitespace | |
# - Remove whitespace before and after the delimiter (so the value is clean) | |
# - Prepend a choice number field to each entry | |
echo "$choices" | | |
awk 'NF > 0 { print $0 }' | | |
sed -e "s/^[\t ]*//" -e "s/[\t ]*${delim}[ \t]*/${delim}/" | | |
awk -vdelim="$delim" '{ printf "%s%s%s\n", NR, delim, $0 }' > "$choices_file" | |
num_choices="`cat "$choices_file" | wc -l | trim`" | |
max_label_len="`get_max_label_len`" | |
num_width=`printf "%d" "$num_choices" | wc -c | trim` | |
# plus two to account for ") " | |
idx=0 | |
idx_limit="`expr "$max_label_len" + "$num_width" + 2`" | |
while [ "$idx" -lt "$idx_limit" ]; do | |
separator="$separator-" | |
idx="`expr "$idx" + 1`" | |
done | |
# Redirect all stdout to stderr for the menu display | |
# We use stdout to return the chosen value. | |
printf "\n" >&2 | |
show_choices >&2 | |
while [ "$choice" -eq -1 ]; do | |
printf -- "%s\n%s " "$separator" "$prompt" | |
IFS=' ' read response rest | |
if is_numeric "$response" && [ "$response" -ge 1 ] && [ "$response" -le "$num_choices" ]; then | |
choice="$response" | |
else | |
printf "'%s' is not a valid choice\n" "$response" | |
fi | |
done >&2 | |
printf "%s" "`value_for_index "$choice"`" | |
rm -f "$choices_file" >/dev/null 2>/dev/null | |
unset trim is_numeric value_for_index iterate_choices get_max_label_len show_choices | |
return 0 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment