Instantly share code, notes, and snippets.
Last active
March 26, 2025 23:13
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save sellmerfud/158f0a6444c4520f3023c11667c6b965 to your computer and use it in GitHub Desktop.
Bourne shell function to prompt using a menu
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 sh | |
# Trim leading and trailing spaces | |
# from stdin and write to stdout. | |
# Useful when getting numbers from wc -c, etc. | |
trim() { | |
sed -e 's/^[ ]*//' -e 's/[ ]*$//' | |
} | |
# Return 0 if the first argument is all digits | |
is_numeric() | |
{ | |
echo "$1" | grep '^[0-9][0-9]*$' >/dev/null 2>&1 | |
} | |
# 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 | |
# | |
# examples: | |
# choices=' | |
# international:YYYY-MM-DD (ISO) | |
# us:MM-DD-YYYY (United States) | |
# europe:DD-MM-YYYY (Europe) | |
# ' | |
# date_format=`prompt_menu "$choices" | |
# | |
# # With two column layout: | |
# date_format=`prompt_menu "$choices" "2:80" | |
# | |
# # With a custom prompt: | |
# date_format=`prompt_menu "$choices" "1" "Select date format:"` | |
prompt_menu() { | |
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="$TMPDIR/menu_choices.$$" | |
choice=-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 "prompt_menu: first argument (choices) is required\n" >&2 | |
return 1 | |
fi | |
AWK="awk" | |
# On SunOS we must expliclty use Posix version of awk | |
[ `uname -s` = SunOS ] && AWK=/usr/xpg4/bin/awk | |
# Put the choices in a file with each | |
# entry normalized: | |
# <number>:value:display | |
echo "$choices" | | |
$AWK 'NF > 0 { print $0 }' | # Remove blank lines | |
$AWK -F"$delim" ' | |
{ | |
OFS=delim | |
gsub(/^ *| *$/, "", $1) # trim leading and trailer spaces from field 1 | |
sub(/^ */, "", $2) # trim leading spaces from field 2 | |
print NR, $1, $2 # Ouput <number>:value:display | |
}' delim="$delim" > "$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" >&2 | |
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" >&2 | |
fi | |
done | |
printf "%s" `value_for_index "$choice"` | |
rm -f "$choices_file" >/dev/null 2>/dev/null | |
unset 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