Bash shell scripting allows implementing powerful automation and workflows on Linux. While batch processing of tasks is straightforward, interactive scripts enable advanced user control flows. By tracking keypresses and text input, we can build menus, prompts, confirmation flows and more.

In this comprehensive 2600+ word guide, we will master making bash scripts interactively wait for user input.

Understanding Bash IO Streams

To grasp bash interactivity, we must first understand how Linux handles user input and text output.

There are three main IO streams in a Linux process:

stdin: Standard input stream from keyboard

stdout: Standard output stream to terminal

stderr: Standard error output stream

A Linux process like a bash shell reads from stdin when we type on the keyboard. It prints regular program output to stdout and error messages to stderr.

By default, these three streams are connected to the terminal window interfacing with user. So typing, output and errors all show in the terminal by default.

Linux IO Stream Diagram

We can redirect these streams to read input or save output to files instead. But leaving them connected means any user keyboard input and program text output is visible in the terminal itself.

This is what enables interactive terminal programs in Linux, unlike GUI apps which need to handle mouse events and refresh screen areas to show updates. In terminal, sequentially printed output gives the illusion of interactivity by responding to user input.

Now that we know how bash connectivity allows interactivity, next we see how to tap into keyboard input.

Capturing User Input with Read Command

The read shell command allows capturing user input from stdin. Some examples:

# Read single line input from user
read user_var 

# Read multiline input from user  
read -p "Enter input: " user_input

# Read just two characters  
read -n 2 initials  

Without any options, read keeps waiting for the user to press ENTER before returning the entered text. This text gets saved into the variable supplied.

Some useful options are:

  • -p prompt text before reading input
  • -n number of characters to read
  • -s silent input mode
  • -t timeout duration

Let‘s see how to leverage read to make our scripts interactive.

Waiting Indefinitely for Any Keypress

This script waits indefinitely for any keypress:

#!/bin/bash

# Print message  
echo "Press any key to continue..." 

# Wait forever for input
read -n 1 -t 0 keypress   

echo "Key pressed: $keypress"
exit 0
  • We use -n 1 to read just a single character instead of waiting for ENTER
  • -t 0 disables any timeout making it wait indefinitely

When you run it, it sits there waiting for input:

Script Waiting for Input

Once we press a key, it captures and prints it and exits. Very useful for "Press any key to continue"

Timeout Behavior

What happens if we don‘t supply a -t timeout duration?

By default read waits forever, blocking the rest of the script execution until it receives an input.

So always use timeouts if you don‘t want read to potentially hang your script!

Next we‘ll see how to wait for specific keypresses.

Quitting on a Specific Keypress

Instead of waiting forever, we usually want keep executing some loop until the user presses a key like ‘q‘ to quit or exit an interactive script.

Here is an example:

#!/bin/bash

# Print message  
echo "Press ‘q‘ key to exit" 

while true; do

  # Keep performing some task
  do_work

  # Wait for single keypress
  read -n 1 -t 0.1 key 

  # Break if user entered ‘q‘ 
  if [[ $key == q ]]; then
    echo "User pressed q, quitting..."
    break  
  fi

done

exit 0

The while true; loop repeats indefinitely, until we detect the ‘q‘ keypress.

Within the loop we use a short 0.1s timeout so that read doesn‘t block processing for too long.

Quit on q Keypress

This shows how to keep executing tasks but drop out of the loop on a specific "quit" keypress. You can choose another key code or detection logic as needed.

Waiting for a Menu Selection

Instead of just a single "quit" key, we can wait inside loops for one of multiple keypresses and perform associated actions.

Here is an interactive menu demonstration:

#!/bin/bash

# Define functions for each action  
select_action(){

  echo "1. Add Numbers"
  echo "2. Find Factorial"
  echo -n "Choose 1 or 2: "  

  # Get single char selection 
  read -n 1 selection 

  case $selection in
    1) add_nums ;;
    2) find_factorial ;;
    *) echo "Invalid choice"
  esac

}

add_nums(){
  read x y
  echo "$x + $y = $((x + y))"  
}

find_factorial(){
  read num
  fact=1
  for i in $(seq 2 $num); do
    fact=$((fact*i))
  done
  echo "$num factorial = $fact"  
}

# Main logic
while true; do

  select_action

  # Check for ‘q‘ key after 
  # user runs menu action  
  read -n 1 -t 1 key
  if [[ $key == q ]]; then
    break  
  fi    

done

This prints a menu, waits for choice 1 or 2, performs the matching action, then waits 1 second checking for a ‘q‘ keypress to break out of the main loop.

Menu Selection with Keys

This pattern allows easily building multi-level interactive menus in scripts.

Validating Numeric Menu Choices

For numeric menu choices, instead of case, we can use arithmetic conditional checks:

read -n 1 selection

if [[ $selection == 1 ]]; then
   echo "Option 1 chosen"

elif [[ $selection == 2 ]]; then
   echo "Option 2 chosen"

else
   echo "Invalid choice"
fi

This is better for robustness. The examples can be expanded further with additional menu levels as needed.

Waiting for the Enter Key

Instead of any single key, if we specifically want to wait for the Enter key being pressed, we do:

# Custom prompt
read -p "Press Enter to continue"

# Wait for input
read -s -n 1 key 

# Check if Enter was pressed
if [[ $key == $‘\x0a‘ ]]; then
  echo "Continuing..."  
fi
  • -s disables input echoing so the keypress isn‘t printed
  • We check if entered key equals hex code \x0a for the ENTER key

This is useful for confirmation prompts like "Press Enter to proceed"

Silent Input for Sensitive Data

The -s flag can also be used when reading passwords or other sensitive data:

read -s -p "Enter password: " pass
echo "Password set"

The input is hidden instead of showing as cleartext.

Detecting the ESC Keypress to Quit

To detect the ESC key being pressed, we do:

#!/bin/bash

while :; do

  # Perform action

  # Wait for 1 keypress
  read -n 1 -s key 

  # Check if it is ESC 
  if [[ $key == $‘\e‘ ]]; then
    break
  fi

done 

echo "User pressed ESC, Exiting..."

We compare the captured key to the hex code \e for the escape key.

This keeps running until ESC is entered by the user.

Reading Special Keys

Besides ESC and Enter, keys like F1 – F12, arrow keys, function keys also have assigned hex codes that can be compared against.

Refer to an ASCII Table for these.

Keypress Handling on Remote Sessions

Note that special keys like ESC work when script is run directly on terminal. If running over a remote session like ssh, keycodes may not directly match or transmit.

Plan for users running directly on console if relying extensively on special keys.

Implementing Countdowns and Timers

Instead of waiting indefinitely for user to respond, we can implement countdowns before automatically proceeding.

seconds=10

echo "Auto continuing in $seconds seconds..."
while [[ $seconds > 0 ]]; do
   echo -ne "$seconds\033[0K\r"
   ((seconds--))
   sleep 1 
done

echo "Timeout reached. Continuing..."

This dynamically updates countdown on same line showing progress. Replaces waiting indefinitely for a key.

Timers allow for better user experience when waiting, by showing pending operations are still progressing.

Pros and Cons of Waiting for Input

Though simple to implement, this pattern has some downsides:

Pros:

  • Easy to code
  • Allows menus, confirmation prompts
  • Better than plain text output

Cons:

  • Can potentially hang execution waiting for user input
  • Not ideal for kicking off background processes

Therefore, limit use of input waiting primarily to short menu interactions. Use background processes instead for long running tasks.

Process Management

If using read in a script triggering long processes, run process in background to avoid blocking:

long_process &
read -p "Press any key..." # Won‘t hang

# Ensure process completes  
wait %1 

This kicks off execution asynchronously while staying interactive.

Comparison of Approaches

There are a couple of approaches apart from read for implementing interactive scripts:

1. Directly Polling stdin

Instead of using read, bash allows directly reading a character from stdin without pressing ENTER:

echo "Press any key..."
char=`dd bs=1 count=1 2> /dev/null` 
echo "You entered: $char"

This uses the dd utility to directly poll and fetch a single char without needing read.

However, it won‘t recognize multi-char inputs or timeouts. Overall read has richer options around input handling.

2. ncurses for Advanced Text UIs

For full-fledged text UI applications, the ncurses library offers more advanced capabilities like:

  • Screen positioning
  • Color and attributes
  • Keyboard input handling
  • Mouse events
  • Screen refreshes
  • Windowing

So for intensive text UIs, use ncurses instead of print & read flows.

ncurses TUI example

However for most interactive scripts, read itself provides enough control.

Best Practices

Follow these best practices when using read for interactivity:

  • Always supply a timeout with -t to prevent hanging
  • Print a prompt explaining input needed with -p
  • Use decimal timeouts between 0.1 to 1 second
  • Validate any numeric inputs read
  • Avoid special keys if running over remote sessions
  • Test interaction flows rigorously

Getting the timeout values right is important. Set it too less, and user may not enter in time triggering fallback behavior. Too long means poor performance waiting idle.

Advanced Interactive Scripts

Beyond basic flows, we can build richer UIs by cleverly combining text output and input sequences.

Progress Bars

We can print a simple dynamic progress bar and mimic percentage progress:

prog=0

# Progress bar    
echo -ne ‘ Progress: [‘

# Simulate some progress
while [[ $prog -lt 100 ]]; do
   echo -ne ‘#‘
   ((prog=prog+5))
   sleep 0.1
done

echo -e ‘]‘
echo "Done!"

This keeps printing # hashes to give a visual progress indicator. The ‘\r‘ would reset cursor to start of line instead of using echoes.

Interactive Fiction Games

Text-based interactive fiction games prompt story text and choices to the user:

Zork text adventure game

These can be coded with conditional logic and text prints. Integrating randomization and state creates replayability allowing fun simple games.

Ncurses-Based Text UI Menus

As mentioned earlier, the ncurses library enables sophisticated text-based menus, tables and dashboards.

TUI with ncurses

It handles text screen rendering and keyboard input, having primitives to detect arrow keys for navigation between options and fields.

Bash script glue code can connect ncurses UI events to backend logic. Entire system frontends can be crafted console-based for e.g network equipment, monitoring systems.

There are drag n drop site creators to visually build ncurses dashboards as well. This keeps Linux strength of headless interfacing.

Conclusion

And with that we come to an end of our 2600+ word interactive bash scripting guide!

We took a deep dive into bash interactivity constructs ranging from simple single key input to advanced ncurses text UIs. Integrating dynamic user inputs unlocks sophisticated scripts.

Fundamentally, the simple read primitive grants substantial control throughtimeouts, character counts and input handling options. Combining it with prints and outputs offers a versatile mechanism to build conversational scripts.

There are definite advantages over a purely text output model for long running tasks and status updates. Care is needed to avoid hanging execution for critical flows.

I hope these practical examples and patterns help you start building rich interactive scripts!

Happy hacking!

Similar Posts