Bash Almanac
Introduction
The Bash Almanac is a reference for building practical GNU Bash shell scripts. It provides reusable functions and examples for creating robust command-line tools and interactive CLI programs. All examples are tested under Linux and macOS. The snippets are self-contained and can be copied directly into your own scripts.
Topics include:
-
Prompting and validating user input.
-
Generating color output compatible with Linux console and terminals.
-
Drawing text boxes using Unicode line-drawing characters.
-
Advanced parsing of command-line options and arguments.
-
Other features commonly used in advanced CLI scripts.
All examples are tested under Linux and macOS. The snippets are self-contained and can be copied directly into your own scripts.
Colors and Styles
The term_init function defines reusable foreground and background color variables, cursor movement sequences, and additional terminal control sequences.
It adjusts the terminal color palette, adding colors such as orange to improve visibility and maintain compatibility across Linux VGA consoles, SSH sessions, and modern terminal emulators.
term_init also provides a demo mode to display all available color options and a cleanup mode that restores the original shell environment if the script is source-executed.
#!/usr/bin/env bash
function term_init {
#
# Assign useful terminal sequences that are compatible with any
# 256-color VGA terminal, if available. There is, however, no
# ill-effect if the terminal does not - in which case variables
# will simply be empty and produce no output. Requires Bash >= 3.
#
# Usage:
# term_init : Create variables.
# term_init demo : Demonstrate (verify).
# term_init cleanup : Remove variables if source exectued.
#
# Define arrays of variable names and appropriate tput arguments.
local -a tput_fg=( 'C0:setaf 0' 'C1:setaf 1' 'C2:setaf 2' 'C3:setaf 3'
'C4:setaf 4' 'C5:setaf 5' 'C6:setaf 6' 'C7:setaf 7' )
local -a tput_bfg=( 'B0:setaf 0' 'B1:setaf 1' 'B2:setaf 2' 'B3:setaf 3'
'B4:setaf 4' 'B5:setaf 5' 'B6:setaf 6' 'B7:setaf 7' )
local -a tput_bg=( 'R0:setab 0' 'R1:setab 1' 'R2:setab 2' 'R3:setab 3'
'R4:setab 4' 'R5:setab 5' 'R6:setab 6' 'R7:setab 7' )
local -a tput_misc=( 'BD:bold' 'RG:bel' 'BL:blink' 'T0:sgr0' 'U1:cuu1'
'RV:rev' 'ED:ed' 'EL:el' 'HC:civis' 'RC:cnorm' )
local i i1 i2
#
case "$1" in
demo)
echo -e "\nConsole and terminal compatible colors:\n"
for i in "${tput_fg[@]}" "${tput_bfg[@]}" BD "${tput_bg[@]}" RV; do
i1="${i%:*}"
echo -n "${!i1}$i1${T0} "
case $i1 in C7|BD|RV) printf "\n";; esac
done
for i1 in "${tput_fg[@]}"; do
i1="${i1%:*}"
for i2 in "${tput_bg[@]}"; do
i1="${i1%:*}" i2="${i2%:*}"
[[ ${i1#C} == ${i2#R} ]] && continue
case $i1$i2 in C6R2|C2R6|C5R1|C1R5|C7R3|C0R4) continue;; esac
echo -n "${!i1}${!i2}$i1$i2${T0} "
done; echo
done
for i1 in "${tput_bfg[@]}"; do
for i2 in "${tput_bg[@]}"; do
i1="${i1%:*}" i2="${i2%:*}"
case $i1$i2 in B3RV|B4R4) continue;; esac
echo -n "${!i1}${!i2}$i1$i2${T0} "
done; echo
done
;;
cleanup)
# Remove variables defined by term_init. This is useful to clean up
# the shell (ENV) if term_init was source executed.
# script has been source executed.
local -a all=( "${tput_fg[@]}" "${tput_bfg[@]}" "${tput_bg[@]}"
"${tput_misc[@]}" )
for i in "${all[@]}"; do unset "${i%:*}"; done
# Restore default Linux 16-color terminal palette and $TERM.
if [[ "${TERM}" == linux ]]; then
setvtrgb vga
elif [[ -n ${TERM_INIT_OLD_TERM} ]]; then
export TERM="${TERM_INIT_OLD_TERM}"
unset TERM_INIT_OLD_TERM
fi
unset -f term_init
;;
*)
# Define variables that produce the same or similar color output
# under Linux VGA-style 16-color (console, TERM=linux) and terminal
# emulators (SSH, xterm-256color).
for i in "${tput_fg[@]}" "${tput_bg[@]}" "${tput_misc[@]}"; do
printf -v "${i%:*}" '%s' "$(tput ${i#*:} 2>/dev/null)"
done
# Bright colors, add bold.
for i in "${tput_bfg[@]}"; do
printf -v "${i%:*}" '%s' "${BD}$(tput ${i#*:} 2>/dev/null)"
done
# Alter the Linux console color map to replace dark yellow with a
# more visible orange, thereby providing an additional color and
# improving compatiblity with modern terminal emulation.
# Note that bold orange (BD+C3) becomes bright yellow.
if [[ "${TERM}" == linux ]]; then
# Match Linux console and xterm-256 colors.
echo "0,170,0,255,0,170,0,192,85,255,85,255,85,255,85,255
0,0,170,199,0,0,170,192,85,85,255,255,85,85,255,255
0,0,0,6,176,170,170,192,85,85,85,85,255,255,255,255" \
| setvtrgb -
else
# Any SSH client/terminal worth mentioning supports xterm-256color.
# Some installations may not be configured properly. Nevertheless,
# restore the original TERM value when running term_init cleanup.
# Override default orange and blue to match Linux console colors.
TERM_INIT_OLD_TERM="${TERM}"
export TERM=xterm-256color
C3=$(tput setaf 214) # Orange
R3=$(tput setab 214) # Orange background
C4=$(tput setaf 25) # Blue
fi
;;
esac
}
## END
Boxes and Borders
The box function generates text boxes using Unicode box-drawing characters, optional color schemes, and flexible line formatting modes. These are useful for displaying messages that require special user attention.
The function draws the box line by line and overlays the desired text, giving the user full control over the final appearance. Long lines can be split manually to match the preferred console width or programming style.
The companion mute function can suppress keyboard echo and disable control-key functions that might otherwise interfere with screen rendering.
#!/usr/bin/env bash
function mute {
# Optional feature to suppress keyboard and cursor output
# when it can interfere with proper box and content rendering.
#
# $1 on: Hide cursor, disable terminal echo, disable ctrl/s/q/c/d.
# Stop user/keyboard interference while rendering screen output.
# $1 off: Restore previously saved terminal state.
#
local hc=$( tput civis ); local rc=$( tput cnorm )
#
case $1 in
on) SAV=$( stty -g </dev/tty ) # Save tty settings. Global scope.
stty -echo -icanon -ixoff intr '?' eof '?' </dev/tty
printf ${hc} ;;
off) stty ${SAV} </dev/tty
printf ${rc} ;;
esac
}
function box {
# $1 = Color scheme.
# $2 = Text.
# $3 = Format (optional):
# 0 = standard newline (default).
# 1 = do not move the cursor.
# 2 = move the cursor to the beginning of the line (\r).
# Example: box r --top
# box r "The rabbit jumps over the fox" 1
# box r "and escapes."
# box r --middle
# box r "http://maxjot.github.io/maxJOT/bash/bash-almanac.html"
# box r --bottom
#
local maxlen background indent fb t0
local box_top box_middle box_bottom box1 box2 box3 box4 box5 box6 box7 box8
t0=$( tput sgr0 )
maxlen=65
box1=$'\u250C' box2=$'\u2500' box3=$'\u2510' box4=$'\u2502'
box5=$'\u2514' box6=$'\u2518' box7=$'\u251C' box8=$'\u2524'
# Generate horizontal line.
box2=$( eval printf "${box2}%.0s" {1..${maxlen}} )
# Generate top middle and bottom box-lines.
box_top=$( printf "${box1}${box2}${box3}" )
box_middle=$( printf "${box7}${box2}${box8}" )
box_bottom=$( printf "${box5}${box2}${box6}" )
# Used for white space with background.
# printf -v background '%*s' ${maxlen} ''
background=$( eval printf -- '\ %.0s' {1..${maxlen}} )
# Set forground and background color.
case $1 in
4) fb=$( tput setaf 7; tput setab 4 ) ;; # white/blue
0) fb=$( tput setaf 7; tput setab 0 ) ;; # black/white
r) fb=$( tput rev ) ;; # Reverse
*) fb= ;; # No color
esac
indent=" ${fb}${box4}${t0}"
if [[ "$2" == --top ]]; then
printf " ${fb}${box_top}${t0}\n"
lastarg=0
elif [[ "$2" == --bottom ]]; then
printf " ${fb}${box_bottom}${t0}\n"
lastarg=0
elif [[ "$2" == --middle ]]; then
printf " ${fb}${box_middle}${t0}\n"
lastarg=0
else
# Insert indent depending on previous lastarg.
case ${lastarg} in
0) printf " ${fb}${background}${box4}${t0}\r" ;;
1) unset indent ;;
2) ;;
*) printf " ${fb}${background}${box4}${t0}\r" ;;
esac
case "${3}" in
0) printf "${indent}${fb} %s${t0}\n" "$2"
lastarg=0 ;;
1) printf "${indent}${fb} %s${t0}" "$2"
lastarg=1 ;;
2) printf "${indent}${fb} %s${t0}\r" "$2"
lastarg=2 ;;
*) printf "${indent}${fb} %s${t0}\n" "$2"
lastarg=0 ;;
esac
fi
}
## END
User Input Control
The get_reply function provides single-key user input with optional validation and default handling. When a list of valid options is supplied, the first item is automatically treated as the default and chosen if the user presses Return.
Invalid input is handled internally with automatic re-prompts and without visible screen redraw artifacts. The function returns the selected value via REPLY along with a meaningful exit status.
#!/usr/bin/env bash
function get_reply {
# Arguments:
# $1=prompt.
# $2=valid options (optional).
# Examples:
# get_reply "Press menu option:" "E 1 2 3 4"
# get_reply "Press any key to continue..."
# get_reply "Hit (y)es or (n)o, or (a)bort:" "Y N A"
#
# Note: $2 is optional. When specified, the first item
# is automatically shown as the default answer. e.g. [E].
# Any leading indentation in $1 automatically aligns the feedback.
#
# `read -t 0.1' causes an invalid timeout specification error
# if not Bash 4 or later. Use 1 under Bash 3, which will still work
# to flush the keyboard buffer, but cause a > 1 second delay.
#
local tries=0 option prompt answer indent
local sav=$(stty -g </dev/tty)
local hc=$(tput civis) rc=$(tput cnorm) u1=$(tput cuu1) ed=$(tput ed)
local b1=$(tput bold; tput setaf 1) t0=$(tput sgr0)
#
if [[ ! ${BASH_VERSINFO:-0} -ge 4 ]]; then
printf "\n${b1} \`get_reply' requires Bash 4 or later.${t0}\n"
timeout=1
fi
# Restore cursor and cleanup prior to exiting the menu.
opt_cleanup() {
stty ${sav} </dev/tty; printf ${rc}; unset -f opt_msg opt_cleanup; }
# Hide cursor, disable terminal echo, and show error message.
opt_msg() {
stty -echo </dev/tty; echo -e "${hc}\n${b1}$1${t0}"; sleep 1; }
# Provide a dummy prompt when $1 is missing. Use any leading white
# space when specified as indent, and align messages accordingly.
if [[ -z $1 ]]; then
prompt="?:"
else
indent=${1%%[!$' \t']*}
prompt="$1"
fi
# Convert $2 to uppercase and make it an array for easier processing.
# Adjust the prompt accordingly, using the first specified character
# as default. Otherwise leave $1 as is (any key to continue).
if [[ -n $2 ]]; then
options=( $(printf '%s' "$2" | tr '[:lower:]' '[:upper:]') )
default=${options[0]}
prompt="$1 [${default}]"
fi
#
while true; do
# Flush the keyboard buffer.
stty -icanon -echo </dev/tty
read -r -t ${timeout:-0.1} -s --
# Disable ctrl/s/q/c/d and set stdin to interactive mode.
stty icanon echo -ixoff intr '?' eof '?' </dev/tty
echo -en "${rc}${prompt}${ed}"
read -r -e -n 1 -p " " answer
answer=$(printf '%s' "${answer}" | tr '[:lower:]' '[:upper:]')
# No valid options = any key to continue.
[[ -z ${2} ]] && { opt_cleanup; REPLY=${answer}; return 0; }
# Apply default if the input is a Return.
[[ -z ${answer} ]] && answer="${default}" # Default.
for item in "${options[@]}"; do
[[ "${answer}" == ${item} ]] \
&& { opt_cleanup; REPLY=${answer}; return 0; }
done
if (( tries++ == 2 )); then
opt_msg "${indent}Aborting after 3 invalid answers."
printf "\r${u1}${u1}${ed}"
opt_cleanup
return 3
else
opt_msg "${indent}Invalid input - please try again."
printf "${u1}${u1}${u1}"
fi
stty ${sav} </dev/tty
done
}
## END
Arguments and Options
Parsing command-line arguments in a flexible and user-friendly way can quickly become complex when aiming for professional-grade behavior. Beyond basic option handling, a capable parser must manage option bundles, parameterized options, mutual exclusivity rules, diagnostics, and meaningful error reporting.
This implementation demonstrates structured argument parsing in Bash. It is designed to be order-agnostic and tolerant of redundant options, allowing users to supply arguments in any sequence while handling repeated specifications gracefully.
The demonstration script serves as a practical reference that can be adapted, simplified, or extended according to application requirements.