Shell Scripting Bash Input Arguments May 2026

Shell Scripting User Input & Arguments

Read interactive input with the read command, handle positional and optional arguments with shift and getopts, validate all inputs, and build interactive menus — essential for every real-world script.

Every useful script needs to accept input — either interactively from a user typing at the terminal or programmatically from arguments passed on the command line. Handling both correctly, with proper validation, is what makes a script safe to run in production.

The read builtin reads a line from stdin into a variable. It has many flags for prompts, timeouts, silent input (passwords), and character limits.

BASH
# Basic read
read -r name
echo "You entered: ${name}"

# -p : inline prompt (no newline before input)
read -rp "Enter your name: " name

# -s : silent mode (for passwords — input not echoed)
read -rsp "Password: " password
echo                    # Add newline after silent input

# -t : timeout in seconds
if ! read -rt 10 -p "Continue? [y/N]: " answer; then
  echo "Timed out — defaulting to No"
  answer="n"
fi

# -n : read exactly N characters (no Enter needed)
read -rn1 -p "Press any key to continue..."
echo

# -a : read into an array
read -ra fruits -p "Enter fruits (space-separated): "
for f in "${fruits[@]}"; do
  echo "  Fruit: ${f}"
done

# -d : custom delimiter (read until that character)
read -rd $'\0' content < file.txt   # Read entire file into var
Key User input Prompt Timeout
bash — read demo
vriddh@prod-01:~/scripts$./read_demo.sh
Enter your name: Vriddh
Password: ******** (hidden)
Continue? [y/N]: y
You said: yes
Enter fruits (space-separated): mango apple banana
Fruit: mango
Fruit: apple
Fruit: banana

Never trust input. Validate argument count, type, and range before any processing. Exit early with a clear error message and non-zero exit code.

BASH
#!/usr/bin/env bash
# backup.sh — validate all inputs before proceeding

set -euo pipefail

usage() {
  echo "Usage: $(basename "$0") <source_dir> <dest_dir> [--compress]"
  echo "  source_dir : directory to back up"
  echo "  dest_dir   : where to store the backup"
  echo "  --compress : gzip the backup (optional)"
  exit 1
}

# Check argument count
[[ $# -lt 2 ]] && { echo "ERROR: Missing required arguments"; usage; }

SOURCE="${1}"
DEST="${2}"
COMPRESS="false"
[[ "${3:-}" == "--compress" ]] && COMPRESS="true"

# Validate source exists and is a directory
[[ -d "${SOURCE}" ]] || { echo "ERROR: Source is not a directory: ${SOURCE}"; exit 1; }

# Validate source is readable
[[ -r "${SOURCE}" ]] || { echo "ERROR: Cannot read source: ${SOURCE}"; exit 1; }

# Create dest if it doesn't exist
mkdir -p "${DEST}" || { echo "ERROR: Cannot create destination: ${DEST}"; exit 1; }

echo "✔ Source    : ${SOURCE}"
echo "✔ Dest      : ${DEST}"
echo "✔ Compress  : ${COMPRESS}"
echo "Starting backup..."

shift removes the first positional argument, shifting all remaining ones down by one position. It is the traditional way to process arguments in a loop without getopts.

BASH
#!/usr/bin/env bash
# deploy.sh — manual argument parsing with shift

ENV="development"
VERSION=""
DRY_RUN="false"
VERBOSE="false"

while [[ $# -gt 0 ]]; do
  case "${1}" in
    -e|--env)
      ENV="${2}"
      shift 2          # Consume flag AND its value
      ;;
    -v|--version)
      VERSION="${2}"
      shift 2
      ;;
    --dry-run)
      DRY_RUN="true"
      shift             # Boolean flag — no value follows
      ;;
    --verbose|-V)
      VERBOSE="true"
      shift
      ;;
    --help|-h)
      echo "Usage: deploy.sh [-e env] [-v version] [--dry-run] [--verbose]"
      exit 0
      ;;
    --)
      shift             # End of options
      break
      ;;
    -*)
      echo "Unknown option: ${1}" >&2
      exit 1
      ;;
    *)
      break             # Positional argument — stop parsing flags
      ;;
  esac
done

echo "ENV      = ${ENV}"
echo "VERSION  = ${VERSION:-latest}"
echo "DRY_RUN  = ${DRY_RUN}"
echo "VERBOSE  = ${VERBOSE}"
bash — deploy.sh
vriddh@prod-01:~/scripts$./deploy.sh -e production -v 2.4.1 --dry-run
ENV = production
VERSION = 2.4.1
DRY_RUN = true
VERBOSE = false
vriddh@prod-01:~/scripts$./deploy.sh --unknown-flag
Unknown option: --unknown-flag

getopts is the POSIX-standard builtin for parsing short options like -e, -v, -h. It handles combined flags like -eV automatically. For long options like --env, use the while/case/shift pattern from Section 3.

BASH
#!/usr/bin/env bash
# monitor.sh — getopts for short option parsing

CPU_THRESHOLD=80
MEM_THRESHOLD=90
VERBOSE="false"
LOG_FILE="/var/log/monitor.log"

# Option string: c: = -c requires argument, v = flag only
while getopts ":c:m:l:vh" opt; do
  case "${opt}" in
    c) CPU_THRESHOLD="${OPTARG}" ;;
    m) MEM_THRESHOLD="${OPTARG}" ;;
    l) LOG_FILE="${OPTARG}"      ;;
    v) VERBOSE="true"            ;;
    h) echo "Usage: monitor.sh [-c cpu%] [-m mem%] [-l logfile] [-v]"; exit 0 ;;
    :) echo "Option -${OPTARG} requires an argument" >&2; exit 1 ;;
    \?) echo "Invalid option: -${OPTARG}" >&2; exit 1 ;;
  esac
done

# Skip past parsed options to reach positional args
shift $(( OPTIND - 1 ))

echo "CPU threshold : ${CPU_THRESHOLD}%"
echo "MEM threshold : ${MEM_THRESHOLD}%"
echo "Log file      : ${LOG_FILE}"
echo "Verbose       : ${VERBOSE}"
[[ $# -gt 0 ]] && echo "Remaining args: $*"
bash — monitor.sh
vriddh@prod-01:~/scripts$./monitor.sh -c 75 -m 85 -v
CPU threshold : 75%
MEM threshold : 85%
Log file : /var/log/monitor.log
Verbose : true
vriddh@prod-01:~/scripts$./monitor.sh -c
Option -c requires an argument
BASH
#!/usr/bin/env bash
# menu.sh — interactive menu with select

PS3="Select an action: "   # Prompt for select

options=("Start service" "Stop service" "Check status" "View logs" "Quit")

select choice in "${options[@]}"; do
  case "${choice}" in
    "Start service")
      echo "Starting myapp service..."
      systemctl start myapp
      ;;
    "Stop service")
      echo "Stopping myapp service..."
      systemctl stop myapp
      ;;
    "Check status")
      systemctl status myapp
      ;;
    "View logs")
      journalctl -u myapp -n 50
      ;;
    "Quit")
      echo "Goodbye!"
      break
      ;;
    *)
      echo "Invalid option: ${REPLY}. Choose 1-${#options[@]}"
      ;;
  esac
done
BASH
# Read line by line from a file
while IFS= read -r line; do
  echo "Processing: ${line}"
done < /etc/hosts

# Read from a command's output
while IFS= read -r user; do
  echo "User: ${user}"
done < <(getent passwd | cut -d: -f1)

# Read CSV — split on comma
while IFS=',' read -r host port service; do
  echo "Checking ${service} at ${host}:${port}"
done < services.csv

# Check if script is receiving piped input
if [[ ! -t 0 ]]; then
  echo "Reading from pipe or file..."
  while IFS= read -r line; do
    echo "  ${line}"
  done
else
  echo "Running interactively"
fi
bash — file input demo
vriddh@prod-01:~/scripts$cat services.csv | ./check_services.sh
Reading from pipe or file...
Checking mysql at db-01:3306
Checking redis at cache-01:6379
Checking nginx at web-01:80
✔ Input handling rules — Always use read -r (raw mode prevents backslash interpretation). Always use IFS= before read when reading lines to preserve leading/trailing whitespace. Always validate every argument before using it. These three rules prevent the most common input-handling bugs.
💡 getopts vs manual shift — Use getopts for simple short options (-c -v -h). Use the manual while/case/shift pattern when you need long options (--cpu --verbose --help) or a mix of both. For very complex CLIs, consider a dedicated argument parsing library.