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"
- Bash opens
/var/log/app.logfor reading and connects it to thewhileloop's standard input via the< "/var/log/app.log"redirection — this happens once, before the loop starts. - Each iteration,
read -r linereads one line from that file descriptor into the variableline, up to (and consuming) the next newline. - Setting
IFS=(empty) just for thisreaddisables field splitting on leading/trailing whitespace, so a log line like" warning: disk full"keeps its leading spaces intact instead of having them trimmed. - The loop body runs with that line available in
$line, prints it with a counter, and increments the counter. - When
readhits end-of-file with no more data, it returns a non-zero exit status, thewhilecondition 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 -rby default; only drop-rif you deliberately want backslash escapes interpreted. - Always quote variables set by
readwhen you use them later:"$name", not$name. - Use
-pfor prompts instead of a separateecho -nfollowed byread— it's shorter and clearer. - Use
-sfor passwords or tokens, and print a manual newline afterward since-ssuppresses the one Enter would normally produce. - Redirect a file into a
while readloop with< fileinstead of piping withcat file |, so variables set in the loop survive after it ends. - Set
IFS=for the duration of areadwhen processing files line by line, to preserve leading/trailing whitespace exactly. - Use
-twith 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 —
readitself does not check the content, only that a line arrived.
Practice Exercises
- Write a script called
greet.shthat prompts for a first name and an age on two separateread -pprompts, then prints a sentence like"Priya is 29 years old."Make sure both variables are quoted when used. - 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 withIFS=',' read -r -a cities, and then prints each city on its own numbered line using aforloop over the array. - Write a script that reads
/etc/passwdline by line with awhile readloop 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 towc -l /etc/passwd.
Summary
readis a Bash builtin that reads one line from standard input (or a file/timeout/character count) and stores it in variables, splitting onIFSby default.- Key options:
-pfor a prompt,-sfor silent (password) input,-ato fill an array,-tfor a timeout,-rto disable backslash escaping. - Always use
-rand always quote the resulting variables ("$var") to avoid word-splitting and glob expansion bugs. - Redirect files into a
while readloop with< filerather 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 is0on a successful read and non-zero on EOF or timeout — that's what drives loop termination and timeout handling.
