Learn Interactive Shell Scripting the Easy Way (Part 8 of 34)






How to Write Interactive Shell Scripts in Linux | LinuxTeck


Writing interactive shell scripts on Linux is a skill that can take simple automation and turn it into something truly valuable. Interactive shell scripting allows you to create scripts that can ask for user input, display menus, prompt for confirmation before performing dangerous actions, or even securely accept passwords without hardcoding them into the script. This guide covers everything from the basics of the read command to creating full-fledged terminal menu-style interfaces using whiptail.

Why Interactive Scripts Matter:

Most beginner bash scripts break in real environments because they assume everything is known upfront. The reality is different. Interactive scripts solve real problems like:

  • Asking the operator which service to restart before doing it
  • Prompting for a password without echoing it to the terminal
  • Letting a user pick from a menu instead of editing variables inside the script
  • Preventing accidental deletions with a yes/no confirmation gate

Once you learn this, you will stop writing single-use scripts and start building tools people actually want to run.

Before diving into interactivity, make sure you are comfortable with how bash scripts are structured. If you are just getting started, the guide on what is bash scripting in Linux is a solid starting point that covers the basics of how scripts work before you add user input into the mix.

#01

Interactive vs Non-Interactive Shell: What Is the Difference?

This is one of the most searched questions around this topic and most articles give a half answer. So here is the full picture.

An interactive shell is one that talks back and forth with a user. When you open a terminal on Ubuntu or Rocky Linux and type commands, that is an interactive session. The shell shows you a prompt ($ or #), reads what you type, runs it, and shows output. Startup files like ~/.bashrc are loaded. The $PS1 variable is set because there is a prompt to display.

A non-interactive shell does not talk to anyone. When a cron job runs a script at 2am, or you pipe commands through bash -c, no human is sitting there. The .bashrc file is skipped. There is no prompt. The script just runs from top to bottom.

An interactive shell script is something in between. The shell itself is non-interactive (it is running a file), but the script is designed to pause and ask the user for input using commands like read. This is the kind of script this article teaches you to build.

How to Check Which Shell Type You Are In:

Run this one-liner in your terminal to check:

bash
LinuxTeck.com
[[ $- == *i* ]] && echo "Interactive" || echo "Non-interactive"
OUTPUT
Interactive

If you paste that into a running script file and execute it, it will say Non-interactive, because scripts run as non-interactive shells even when they contain read commands. The script interacts with a user, but the shell process itself is not interactive. That distinction matters when you are debugging why some scripts behave differently in cron versus the terminal.

Common Mistake:

Many beginners add read commands to cron jobs expecting them to wait for user input. A cron job has no terminal attached. The read command will immediately receive EOF and assign an empty value, causing the script to behave unexpectedly or fail silently.

Fix: Never use interactive prompts inside cron jobs. Use command-line arguments or config files for non-interactive automation instead. See the cron command guide for how to pass variables safely to scheduled scripts.

#02

The read Command: How to Write Interactive Shell Scripts in Linux

The read command is the foundation of interactive shell scripting in Linux. Every other tool builds on top of what read does. Most tutorials show you only the basic usage. This section covers every useful flag with a real example for each.

The basic syntax is:

bash
LinuxTeck.com
read [OPTIONS] VARIABLE_NAME

-p : Inline prompt text

Instead of using a separate echo before every read, the -p flag lets you add a prompt inline. Cleaner and one line shorter.

bash
LinuxTeck.com
read -p "Enter your username: " username
echo "Hello, $username"
OUTPUT
Enter your username: john
Hello, john

-s : Silent input (passwords)

The -s flag suppresses what the user types. Nothing appears on screen as they type. This is how you collect passwords in shell scripts without showing them in plaintext.

bash
LinuxTeck.com
read -s -p "Enter password: " pass
echo ""
echo "Password stored (not shown)."

Note:

After using -s, always print an empty echo "" on the next line. Otherwise the next output appears on the same line as the password prompt which looks broken in the terminal.

Also add this trap before any read -s call so the terminal is restored if the script is force-killed mid-input:

bash
LinuxTeck.com
trap 'stty echo; echo ""; exit 1' INT TERM

-t : Timeout in seconds

If you want the script to move on automatically after a few seconds if no input is given, use -t. Very useful in automated deployment scripts that need a confirmation window but should not hang forever.

bash
LinuxTeck.com
if read -t 10 -p "Continue? (yes/no) [auto-yes in 10s]: " answer; then
  echo "You entered: $answer"
else
  echo ""
  echo "Timeout reached. Continuing with defaults."
fi

-n : Limit character input

With -n NUM, read stops after exactly NUM characters. No need to press Enter. Good for single-key yes/no prompts.

bash
LinuxTeck.com
read -n 1 -p "Press any key to continue..." key
echo ""
echo "Key pressed: $key"

-r : Raw mode (no backslash escaping)

By default, read treats backslash as an escape character. If you are collecting file paths or regex patterns, always use -r to get the literal string.

bash
LinuxTeck.com
read -r -p "Enter file path: " filepath
echo "Path: $filepath"

-a : Read into an array

When a user enters space-separated values, -a splits them into array elements automatically.

bash
LinuxTeck.com
read -r -a servers -p "Enter server names (space-separated): "
for s in "${servers[@]}"; do
  echo "Processing: $s"
done
OUTPUT
Enter server names (space-separated): web01 db01 cache01
Processing: web01
Processing: db01
Processing: cache01

Pro Tip:

Combine flags freely. read -r -s -p "Enter API key: " apikey gives you a raw, silent, prompted read in one line. The flags are additive. For more practical examples of how echo and read work together in scripts, check out the guide on using echo effectively in shell scripts.

#03

Yes/No Prompts and Input Validation Patterns

One of the most practical things in interactive bash scripting is asking the user to confirm a dangerous action before executing it. A yes/no prompt done right should handle uppercase, lowercase, and invalid input without crashing.

Reusable confirm() function for production scripts

bash
LinuxTeck.com
#!/bin/bash

confirm() {
  while true; do
    read -r -p "$1 [y/n]: " choice
    case "$choice" in
      y|Y|yes|YES) return 0 ;;
      n|N|no|NO)  return 1 ;;
      *) echo "Please enter y or n." ;;
    esac
  done
}

if confirm "Delete all logs in /var/log/app?"; then
  echo "Deleting logs..."
  # rm -rf /var/log/app/*.log
else
  echo "Cancelled. No files deleted."
fi

This function loops until valid input is given. You can reuse it anywhere in the script just by calling confirm "Your question here".

Numeric input validation

bash
LinuxTeck.com
while true; do
  read -r -p "Enter a number between 1 and 10: " num
  if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le 10 ]; then
    echo "Valid input: $num"
    break
  else
    echo "Invalid. Please enter a number from 1 to 10."
  fi
done

Empty input guard

bash
LinuxTeck.com
while true; do
  read -r -p "Enter a hostname (required): " host
  if [ -z "$host" ]; then
    echo "Hostname cannot be empty. Try again."
  else
    break
  fi
done
echo "Hostname set to: $host"

Common Mistake:

Forgetting to quote variables in conditions causes word-splitting bugs. if [ $var == "yes" ] will throw an error if $var is empty because the shell sees if [ == "yes" ].

Fix: Always quote: if [ "$var" == "yes" ]. This pattern is covered thoroughly in the bash exit codes and error handling guide.

#04

Bash select Statement: Building Terminal Menus

The select statement is the cleanest way to build numbered menus in bash. It handles all the display and loop logic for you. Most articles show only the surface-level example. Here is a proper deep-dive with real patterns you can use.

Basic select menu

bash
LinuxTeck.com
#!/bin/bash

PS3="Choose an action: "
options=("Start Service" "Stop Service" "Check Status" "Exit")

select opt in "${options[@]}"; do
  case $opt in
    "Start Service")  echo "Starting service..."; break ;;
    "Stop Service")   echo "Stopping service..."; break ;;
    "Check Status")   echo "Checking status..."; break ;;
    "Exit")           echo "Goodbye."; break ;;
    *) echo "Invalid option. Try again." ;;
  esac
done

OUTPUT
1) Start Service
2) Stop Service
3) Check Status
4) Exit
Choose an action: 1
Starting service...

Dynamic option list from an array

Instead of hardcoding menu items, you can build the list from a directory listing, a file, or a command output.

Note:

The mapfile command requires Bash 4.0 or higher. Most Linux distros ship with Bash 4+ so this is fine on Ubuntu, Rocky Linux, and RHEL. However macOS ships with Bash 3.x by default and mapfile will not work there without upgrading Bash first.

bash
LinuxTeck.com
#!/bin/bash

PS3="Select a config file to edit: "
mapfile -t configs < <(ls /etc/*.conf 2>/dev/null)
options=("${configs[@]}" "Cancel")

select choice in "${options[@]}"; do
  if [ "$choice" = "Cancel" ]; then
    echo "Cancelled."
    break
  elif [[ -n "$choice" ]]; then
    echo "Opening $choice..."
    # vi "$choice"
    break
  else
    echo "Invalid selection."
  fi
done

Looping menu (return after each action)

Remove the break from all non-exit branches and the menu keeps showing after every selection until the user explicitly chooses to quit. This is useful for server maintenance scripts where the operator might need to run several actions in sequence.

bash
LinuxTeck.com
#!/bin/bash

PS3="Server Menu > "

while true; do
  select action in "Disk Usage" "Memory Info" "Running Processes" "Quit"; do
    case $action in
      "Disk Usage")         df -h; break ;;
      "Memory Info")        free -h; break ;;
      "Running Processes")  ps aux | head -20; break ;;
      "Quit")               echo "Exiting."; exit 0 ;;
      *) echo "Invalid." ;;
    esac
  done
done

Note on PS3:

PS3 is a special bash variable that sets the prompt text for select menus. The default is #? which looks confusing to users who are not developers. Always set PS3 to something readable. For related shell prompt variables, the shell scripting environment setup guide has a good breakdown of PS1, PS2, PS3, and PS4.

#05

whiptail and dialog: GUI-Style Menus in the Terminal

This is the section nobody else covers. If you want your interactive shell script to look more polished, with real dialog boxes, checkboxes, and radio buttons right inside the terminal, whiptail and dialog are what you need.

Installing whiptail and dialog

On Ubuntu or Debian:

bash
LinuxTeck.com
sudo apt install whiptail dialog -y

On Rocky Linux, RHEL, or CentOS Stream:

bash
LinuxTeck.com
sudo dnf install newt dialog -y

Note:

On Rocky Linux and RHEL, whiptail comes from the newt package. The command is still called whiptail after install. If you are setting up Rocky Linux for the first time, the Rocky Linux 8 installation guide covers initial package setup including enabling repos.

Message box (msgbox)

bash
LinuxTeck.com
whiptail --title "Welcome" --msgbox "Script is starting. Press OK to continue." 8 50

Input box

bash
LinuxTeck.com
NAME=$(whiptail --inputbox "Enter your full name:" 8 50 3>&1 1>&2 2>&3)
echo "Hello, $NAME"

The 3>&1 1>&2 2>&3 Pattern:

whiptail writes the user's choice to stderr, not stdout. So to capture it in a variable, you have to swap file descriptors: save stdout to fd3, redirect stdout to stderr, redirect stderr (which has the result) back to stdout. It looks strange but this is the standard pattern for all whiptail value captures.

Yes/No dialog box

bash
LinuxTeck.com
if whiptail --yesno "Do you want to reboot now?" 8 40; then
  echo "Rebooting..."
  # sudo reboot
else
  echo "Reboot cancelled."
fi

Checklist (multi-select)

bash
LinuxTeck.com
PACKAGES=$(whiptail --checklist "Select packages to install:" 15 50 5 \
  "nginx"   "Web server"   OFF \
  "mariadb" "Database"     OFF \
  "php"     "PHP runtime"  OFF \
  "redis"   "Cache layer"  OFF \
  3>&1 1>&2 2>&3)

echo "Installing: $PACKAGES"

Radio list (single select)

bash
LinuxTeck.com
ENV=$(whiptail --radiolist "Select environment:" 12 40 3 \
  "dev"  "Development" ON \
  "stag" "Staging"     OFF \
  "prod" "Production"  OFF \
  3>&1 1>&2 2>&3)

echo "Deploying to: $ENV"

Tip: whiptail vs dialog:

whiptail is lighter, has no color themes, and is available by default on most Debian/Ubuntu systems. dialog supports more widget types (calendar, file browser, progress bar) and allows color customization. For basic scripts on enterprise Linux, stick with whiptail. For more complex installer-style UIs, use dialog. Both use the same syntax structure.

#06

Real-World Interactive Script Examples

Theory is only useful when you can see it in action. Here are three production-ready scripts that combine everything covered above.

Script 1: Server Maintenance Menu

This script presents a looping menu to a sysadmin. They can restart, stop, or check the status of nginx without running individual commands manually.

bash
LinuxTeck.com
#!/bin/bash
# server-menu.sh - Interactive service management

SERVICE="nginx"
PS3="Choose action for $SERVICE: "

while true; do
  echo ""
  select action in "Start" "Stop" "Restart" "Status" "Quit"; do
    case $action in
      "Start")   sudo systemctl start $SERVICE;   echo "$SERVICE started.";  break ;;
      "Stop")    sudo systemctl stop $SERVICE;    echo "$SERVICE stopped.";  break ;;
      "Restart") sudo systemctl restart $SERVICE; echo "$SERVICE restarted."; break ;;
      "Status")  sudo systemctl status $SERVICE;                             break ;;
      "Quit")    echo "Exiting menu."; exit 0 ;;
      *)         echo "Invalid choice." ;;
    esac
  done
done

Script 2: Backup Confirmation Wizard

Before running a backup job, this script asks for confirmation and lets the user choose a destination directory. It guards against empty input and confirms the final action.

bash
LinuxTeck.com
#!/bin/bash
# backup-wizard.sh

echo "=== Backup Wizard ==="

while true; do
  read -r -p "Enter source directory to backup: " SRC
  [ -n "$SRC" ] && break
  echo "Source cannot be empty."
done

while true; do
  read -r -p "Enter backup destination path: " DEST
  [ -n "$DEST" ] && break
  echo "Destination cannot be empty."
done

echo ""
echo "Summary:"
echo "  Source : $SRC"
echo "  Dest   : $DEST"
echo ""

read -r -p "Proceed with backup? [y/n]: " confirm
case $confirm in
  y|Y)
    mkdir -p "$DEST"
    cp -r "$SRC" "$DEST" && echo "Backup complete." || echo "Backup failed."
    ;;
  *)
    echo "Backup cancelled."
    ;;
esac

For a deeper look at Linux backup strategies that you can wrap with scripts like this, the Linux server backup solutions guide covers rsync, tar-based, and cloud backup approaches in detail.

Script 3: New Linux User Creation with Prompts

This script collects a username, sets a password silently, and creates the user with one confirmation step.

bash
LinuxTeck.com
#!/bin/bash
# create-user.sh - Interactive user creation

echo "=== New User Setup ==="

while true; do
  read -r -p "Enter new username: " NEWUSER
  if id "$NEWUSER" >/dev/null 2>&1; then
    echo "User $NEWUSER already exists. Try another."
  elif [ -z "$NEWUSER" ]; then
    echo "Username cannot be empty."
  else
    break
  fi
done

while true; do
  read -s -p "Set password for $NEWUSER: " NEWPASS
  echo ""
  read -s -p "Confirm password: " NEWPASS2
  echo ""
  if [ "$NEWPASS" = "$NEWPASS2" ]; then
    break
  else
    echo "Passwords do not match. Try again."
  fi
done

read -r -p "Create user $NEWUSER? [y/n]: " confirm
if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
  sudo useradd -m "$NEWUSER"
  printf "%s:%s\n" "$NEWUSER" "$NEWPASS" | sudo chpasswd
  echo "User $NEWUSER created successfully."
else
  echo "Cancelled."
fi

#07

Troubleshooting Interactive Shell Scripts

Interactive scripts fail in ways that are not always obvious. These are the most common issues and how to fix them properly.

Problem 1: read not waiting for input

If your script's read command exits immediately without waiting, you are likely running the script through something that closes stdin, like a pipe or process substitution.

bash
LinuxTeck.com
# Wrong - stdin is replaced by pipe output
cat file.txt | while read line; do
  read -p "Continue? " ans   # This wont wait
done

# Correct - redirect file to loop, keep stdin for read
while read -r line; do
  read -r -p "Continue? " ans < /dev/tty
done < file.txt

Using < /dev/tty forces read to take input from the actual terminal even when stdin is redirected.

Problem 2: select loop not exiting

If your select menu keeps looping after a valid selection, you forgot to add break inside the matching case branch. Every valid action branch needs a break to exit the select loop. The loop only exits with an explicit break or exit.

Problem 3: Script behaves differently under sudo

When you run a script with sudo, it runs as root in a fresh environment. Variables from your user environment, including PATH entries and exported variables, may not be available. Always use full paths in scripts that will run as root:

bash
LinuxTeck.com
# Instead of:
nginx -t

# Use full path:
/usr/sbin/nginx -t

The guide on configuring sudo in Linux explains how to set up sudo rules so scripts get the right environment when elevated privileges are needed.

Problem 4: whiptail not found error

bash
LinuxTeck.com
# Check if whiptail is installed
if ! command -v whiptail >/dev/null 2>&1; then
  echo "whiptail not found. Installing..."
  # Ubuntu/Debian:
  sudo apt install whiptail -y
  # Rocky/RHEL:
  # sudo dnf install newt -y
fi

Problem 5: Script behaves differently in cron

Cron has no terminal. Any read prompt in a cron script will get EOF immediately and the variable will be empty. If your script has interactive prompts but also needs to support scheduled runs, add a flag check at the top:

bash
LinuxTeck.com
#!/bin/bash
# Run interactively:  ./script.sh
# Run from cron:     ./script.sh --auto

AUTO=false
[ "$1" = "--auto" ] && AUTO=true

if [ "$AUTO" = false ]; then
  read -r -p "Enter target server: " SERVER
else
  SERVER="default-server"
fi

echo "Connecting to $SERVER..."

Common Mistake:

Running a script with bash script.sh instead of ./script.sh can ignore the shebang line. If your script relies on bash-specific features like select, always execute with bash script.sh or make it executable with chmod +x and run it directly. The bash hello world beginner guide covers shebang lines and execution modes properly.

#08

Best Practices: When to Use read vs select vs whiptail

Picking the right tool matters. Using whiptail for a one-line confirmation is overkill. Using plain read when you have 10 options makes the script hard to use. Here is a simple decision guide:

Tool Selection Guide:

Use read when: you need freeform text input, a password, a file path, or a simple yes/no answer. It is POSIX-compatible and works everywhere.

Use select when: you have 3 to 8 fixed options and want a clean numbered menu. Pure bash, no dependencies, works over SSH and in minimal containers.

Use whiptail when: you are building an interactive installer or maintenance tool where visual polish matters. Requires the newt library. Does not work in non-terminal environments (cron, CI/CD pipelines).

Additional best practices for production-ready interactive scripts:

  • Always add set -e at the top so the script exits on any error rather than continuing with bad data
  • Trap Ctrl+C cleanly with trap 'echo "Cancelled."; exit 1' INT to avoid leaving things in a half-done state
  • Log what the user chose: append to a log file so there is an audit trail of who ran what interactively
  • Test your script with an empty terminal (SSH session with minimal env) before deploying it to servers
  • Keep interactive prompts near the top of long scripts so all input is collected before heavy operations begin

For more automation patterns that combine bash scripting with real-world Linux workflows, the Linux bash scripting automation guide for 2026 covers scheduled tasks, logging, and deployment script patterns.

FAQ

Frequently Asked Questions

What is the difference between an interactive and a non-interactive shell script?

An interactive shell is a live session where you type commands and see results. A non-interactive shell is what runs when you execute a script file. The shell process is non-interactive, but a script can still be interactive if it uses read to collect user input during execution. The key difference is that cron jobs, pipes, and process substitutions are fully non-interactive and will not pause for user input at all. You can detect the shell mode inside a bash script with:

bash
LinuxTeck.com
[[ $- == *i* ]] && echo "Interactive" || echo "Non-interactive"
How do I validate user input in a bash script?

The cleanest approach is to wrap your read in a while true loop and only break when the input passes your validation check. For numeric ranges, use regex to confirm digits and then compare. For empty input, use [ -z "$var" ]. For email or path patterns, test with [[ "$var" =~ PATTERN ]]. Always quote your variables and use -r with read to avoid backslash surprises. See Section #03 in this article for copy-paste validation patterns including numeric guards, empty checks, and retry loops. The shell scripting interview questions guide also has validation-related questions that test this knowledge.

Can I use whiptail on Rocky Linux or RHEL?

Yes. On Rocky Linux and RHEL, whiptail is provided by the newt package. Install it with:

bash
LinuxTeck.com
sudo dnf install newt -y

After install, the whiptail command is available. All the examples in Section #05 work identically on Rocky Linux 8/9 and RHEL 8/9. On Ubuntu and Debian, the package name is simply whiptail.

Why does my select menu keep repeating after I choose an option?

The select loop continues until it sees a break or exit statement. If your case branch does not include break, the loop restarts. The fix is to add break at the end of each valid action branch. If you want the menu to loop intentionally and only stop on a quit option, add break only to the quit branch and wrap the select in a while true loop. Section #04 in this article shows both patterns with working code.

How do I collect a password in a bash script without showing it on screen?

Use the -s flag with read. This suppresses terminal echo so nothing appears as the user types. Always follow it with an empty echo to move the cursor to the next line:

bash
LinuxTeck.com
read -s -p "Enter password: " PASS
echo ""

Never store passwords in plaintext script files or print them with echo $PASS. For Linux security hardening that includes credential handling in scripts, see the Linux server hardening checklist.

Is whiptail available in minimal Docker or container environments?

Not by default. Most minimal container images do not include whiptail or even a proper terminal. More importantly, whiptail requires a terminal (TTY) to render its interface. If your script runs inside a container without an attached TTY, whiptail will fail. In that case, fall back to plain read prompts or pass all values as environment variables or arguments. Use docker run -it if you need a TTY attached to the container session.

What is the bash select statement and how is it different from read?

read takes freeform text input. The user types whatever they want and the script stores it in a variable. select generates a numbered list from an array and forces the user to pick one of the listed options by number. If the user enters an invalid number, select sets the variable to empty and loops again. select is better when you have a fixed set of choices. read is better when the user needs to enter a custom value like a file path or hostname.

END

Summary

Learning how to write interactive shell scripts in Linux opens up a whole category of tools you simply cannot build with static scripts. Starting from the full set of read flags, through validated prompts and select menus, all the way to proper TUI dialog boxes with whiptail, this guide has covered every layer of interactivity that matters for real sysadmin and DevOps work.

The troubleshooting section and real-world script examples are there so you can adapt these patterns directly to your environment without starting from scratch. For a deeper dive into where interactive scripts fit in a broader automation strategy, the Linux shell scripting command cheat sheet is a good quick reference to keep open while building.

Related Articles


LinuxTeck — A Complete Linux Learning Blog
Learn step-by-step how to automate Linux tasks with real-world scripts and practical examples.

About Sharon J

Sharon J is a Linux System Administrator with strong expertise in server and system management. She turns real-world experience into practical Linux guides on Linux Teck.

View all posts by Sharon J →

Leave a Reply

Your email address will not be published.

L