Skip to content

Instantly share code, notes, and snippets.

@sellmerfud
Created September 14, 2023 23:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sellmerfud/158f0a6444c4520f3023c11667c6b965 to your computer and use it in GitHub Desktop.
Save sellmerfud/158f0a6444c4520f3023c11667c6b965 to your computer and use it in GitHub Desktop.
Bourne shell function to prompt using a menu
#! /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