Reading User Input (read)

The read command is Bash’s built-in tool for capturing input from a user, from a file, or from another command’s output while a script is running. Instead of hard-coding values into a script, read pauses execution, waits for a line of text to arrive on standard input, and stores it in one or more shell variables. It’s the foundation of every interactive Bash script: prompts, password entry, confirmation dialogs, and line-by-line file processing all build on this one builtin.

Overview / How it works

read is a Bash builtin, not a separate program on disk like /bin/ls. That distinction matters: because it runs inside the current shell process rather than forking a new one, any variables it sets are visible to the rest of your script immediately, in the same shell. This becomes important later when we talk about pipelines and subshells.

Every process starts with three open file descriptors: 0 (standard input, stdin), 1 (standard output, stdout), and 2 (standard error, stderr). When you run a script from an interactive terminal, stdin is connected to your keyboard. read reads from file descriptor 0 by default — one line at a time, up to a newline character (or a custom delimiter set with -d).

Once a line is read, Bash splits it into words using the characters in the IFS (Internal Field Separator) variable, which defaults to space, tab, and newline. Each word is assigned to the variable names you list after read, in order. If you supply more words than variable names, every leftover word is stuffed into the last variable, whitespace and all. If you supply only one variable name (the common case), the entire line lands in it with no splitting effect the user would notice.

read returns an exit status like any command: 0 if a line was read successfully, and non-zero (typically 1) if it hit end-of-file (EOF) before a newline — for example, when the input stream runs out inside a while read loop. This return value is what lets loops know when to stop.

Syntax

read [options] [variable_name...]

If no variable name is given, the whole line is stored in the special variable REPLY. The most useful options:

Option Meaning
-p "text" Display text as a prompt before reading, without a trailing newline.
-r Raw mode: don’t treat a backslash (\) in the input as an escape character. Almost always what you want.
-s Silent: don’t echo the typed characters to the terminal (for passwords/secrets).
-a array Split the input line into an indexed array named array instead of scalar variables.
-n N Read at most N characters and return, even without a newline.
-t N Time out after N seconds and return non-zero if no input arrives in time.
-d delim Use delim as the line terminator instead of newline.
-e Use Bash’s readline for interactive editing (arrow keys, history) while typing.

Examples

Example 1: A basic prompt

#!/usr/bin/env bash
read -r -p "Enter your name: " name
echo "Hello, $name! Welcome to the script."

Output:

Enter your name: Priya
Hello, Priya! Welcome to the script.

The -p flag prints the prompt text without a newline, so the cursor waits right after it. read -r then blocks until the user types a line and presses Enter, storing it in name. Note the prompt and typed text share one line in the real terminal — that’s shown here as two lines only because the second line is the program’s own echo output on the next line.

Example 2: Silent password entry with confirmation

#!/usr/bin/env bash
read -r -s -p "Enter a new password: " password
printf "\n"
read -r -s -p "Confirm password: " password_confirm
printf "\n"

if [[ "$password" == "$password_confirm" ]]; then
    echo "Passwords match."
else
    echo "Passwords do not match." >&2
    exit 1
fi

Output:

Enter a new password: 
Confirm password: 
Passwords match.

With -s, nothing typed is echoed to the terminal — the blank space after each prompt is where the (invisible) password was typed. Because -s also suppresses the newline the Enter key would normally show, we print one manually with printf "\n" so the next prompt doesn’t jam onto the same line. The two values are compared with [[ ... ]], always quoting the variables.

Example 3: Splitting input into an array, and a timeout

#!/usr/bin/env bash
echo "Enter up to three favorite fruits, separated by spaces:"
read -r -a fruits

echo "You listed ${#fruits[@]} fruit(s):"
for fruit in "${fruits[@]}"; do
    echo " - $fruit"
done

if read -r -t 5 -p "Quick, type anything within 5 seconds: " reply; then
    echo "You said: $reply"
else
    echo
    echo "Too slow — timed out."
fi

Output:

Enter up to three favorite fruits, separated by spaces:
mango banana kiwi
You listed 3 fruit(s):
 - mango
 - banana
 - kiwi
Quick, type anything within 5 seconds: 
Too slow — timed out.

The -a flag tells read to split the whole line on IFS and load each word into the fruits array instead of a single variable. The final read -t 5 demonstrates a timeout: if no line arrives within 5 seconds, read returns a non-zero exit status, the if takes the else branch, and the script continues instead of hanging forever — important for any script that might run unattended.

How it works step by step

Consider reading a file line by line, which is one of the most common uses of read in real scripts:

#!/usr/bin/env bash
line_number=1
while IFS= read -r line; do
    echo "$line_number: $line"
    ((line_number++))
done < "/var/log/app.log"
  1. Bash opens /var/log/app.log for reading and connects it to the while loop's standard input via the < "/var/log/app.log" redirection — this happens once, before the loop starts.
  2. Each iteration, read -r line reads one line from that file descriptor into the variable line, up to (and consuming) the next newline.
  3. Setting IFS= (empty) just for this read disables field splitting on leading/trailing whitespace, so a log line like " warning: disk full" keeps its leading spaces intact instead of having them trimmed.
  4. The loop body runs with that line available in $line, prints it with a counter, and increments the counter.
  5. When read hits end-of-file with no more data, it returns a non-zero exit status, the while condition fails, and the loop ends.

Because the redirection (< file) feeds the file straight into the loop's stdin in the current shell, every variable set inside the loop (like line_number) is still visible after the loop finishes. That's the detail the next section's first mistake gets wrong.

Common Mistakes

Mistake 1: Piping into while read and losing the variables afterward

# WRONG: variables set inside the loop vanish afterward
count=0
cat "/etc/hosts" | while read -r line; do
    ((count++))
done
echo "Lines processed: $count"

Output:

Lines processed: 0

A pipeline (cmd1 | cmd2) runs each side in its own subshell. The while loop after the pipe executes in a child process, so count gets incremented there, but that copy disappears when the subshell exits — the parent shell's count was never touched. The fix is to avoid the pipe and redirect the file directly into the loop instead, which keeps everything in one shell:

# RIGHT: no subshell, variables persist
count=0
while read -r line; do
    ((count++))
done < "/etc/hosts"
echo "Lines processed: $count"

Mistake 2: Forgetting -r

# WRONG: backslashes get silently eaten
read -p "Enter a Windows-style path: " path
echo "$path"

If the user types C:\Users\Priya\notes, without -r each backslash acts as an escape character and either gets stripped or merges with the next character, corrupting the value. Always default to read -r unless you have a specific reason to allow escape processing.

Mistake 3: Using the value unquoted

# WRONG: breaks on spaces and glob characters
read -r -p "Enter a file name: " filename
rm $filename

If the user enters my notes.txt, the unquoted $filename splits into two words (my and notes.txt), and rm tries to delete two different files — neither of which is the one intended. Always quote: rm "$filename".

Best Practices

  • Use read -r by default; only drop -r if you deliberately want backslash escapes interpreted.
  • Always quote variables set by read when you use them later: "$name", not $name.
  • Use -p for prompts instead of a separate echo -n followed by read — it's shorter and clearer.
  • Use -s for passwords or tokens, and print a manual newline afterward since -s suppresses the one Enter would normally produce.
  • Redirect a file into a while read loop with < file instead of piping with cat file |, so variables set in the loop survive after it ends.
  • Set IFS= for the duration of a read when processing files line by line, to preserve leading/trailing whitespace exactly.
  • Use -t with a sensible timeout for any script that might run unattended (cron, CI) so it can't hang forever waiting for input that will never come.
  • Validate what you read (non-empty, expected format) before trusting it — read itself does not check the content, only that a line arrived.

Practice Exercises

  1. Write a script called greet.sh that prompts for a first name and an age on two separate read -p prompts, then prints a sentence like "Priya is 29 years old." Make sure both variables are quoted when used.
  2. Write a script that asks the user to type a comma-separated list of city names on one line (e.g. Delhi,Mumbai,Pune), reads it with IFS=',' read -r -a cities, and then prints each city on its own numbered line using a for loop over the array.
  3. Write a script that reads /etc/passwd line by line with a while read loop redirected from the file (not piped), counts how many lines there are in a variable, and prints the total after the loop. Confirm the count is correct by comparing it to wc -l /etc/passwd.

Summary

  • read is a Bash builtin that reads one line from standard input (or a file/timeout/character count) and stores it in variables, splitting on IFS by default.
  • Key options: -p for a prompt, -s for silent (password) input, -a to fill an array, -t for a timeout, -r to disable backslash escaping.
  • Always use -r and always quote the resulting variables ("$var") to avoid word-splitting and glob expansion bugs.
  • Redirect files into a while read loop with < file rather than piping, because a pipeline runs the loop in a subshell and any variables it sets are lost once the subshell exits.
  • read's exit status is 0 on a successful read and non-zero on EOF or timeout — that's what drives loop termination and timeout handling.