Skip to content

Reading User Input


You’ve used read twice already in this course, both times in the same narrow form: while IFS= read -r line. That pattern is safe, but it’s only a fraction of what read actually does. This chapter covers it properly — prompts, silent input for passwords, timeouts, reading several values at once, reading straight into an array, and exactly why that -r flag matters the most while working with files.

Basic read

read name
echo "You entered: $name"

Typing Alice and pressing Enter stores Alice in name. This is the same builtin you’ve been using since Chapter 8 to read files line by line — here, stdin is just the keyboard instead of a redirected file.

Prompting With -p

Rather than a separate echo before every read, -p displays a prompt directly, without a trailing newline:

read -p "Enter your name: " name
echo "Hello, $name"
Enter your name: Alice
Hello, Alice

Silent Input With -s

-s suppresses terminal echo entirely — the standard way to prompt for a password or other sensitive value without displaying it as it’s typed:

read -s -p "Password: " password
echo
echo "Password captured (length: ${#password})"
Password: 
Password captured (length: 12)

Note the extra echo on its own line right after read — since -s suppresses the newline the person’s own Enter keypress would normally produce visually, without that extra echo your next line of output would appear jammed onto the same line as the prompt.

Timeouts With -t

-t seconds makes read give up and return after the specified time if nothing was entered. Check read’s own exit status afterward to tell whether it actually got input or timed out:

if read -t 5 -p "Answer within 5 seconds: " answer; then
    echo "You answered: $answer"
else
    echo "Timed out waiting for input."
fi
Answer within 5 seconds: 
Timed out waiting for input.

read returns a nonzero exit status on timeout, which is exactly what makes it usable directly as an if condition here, the same pattern covered back in Chapter 7.

Reading Into Multiple Variables

read can populate several variables from one line at once, splitting on IFS the same way word splitting does everywhere else in Bash:

read first last <<< "Alice Smith"
echo "First: $first"
echo "Last: $last"
First: Alice
Last: Smith

Here’s the behavior worth knowing explicitly: if the input has more words than you gave variables, the last variable absorbs everything remaining, not just one more word:

read first last <<< "Alice Middle Names Smith"
echo "First: $first"
echo "Last: $last"
First: Alice
Last: Middle Names Smith

last didn’t get just "Middle" — it got the entire remainder, "Middle Names Smith". This is deliberate, standard read behavior, not a bug, but it’s a common source of confusion the first time you see more variables’ worth of data arrive than expected.

Reading Into Arrays With -a

-a reads a full line and splits it directly into an indexed array, again governed by IFS:

read -a words <<< "one two three four"
echo "${#words[@]}"
echo "${words[2]}"
4
three

This is the natural choice when you don’t know in advance how many fields a line will contain, unlike the fixed multiple-variable form above.

Custom Delimiters With -d

By default, read stops at a newline. -d changes that to any character you specify:

read -d ':' field <<< "first:second:third"
echo "$field"
first

The most important special case here is -d '' — an empty delimiter, which tells read to split on null bytes (\0) instead. Combined with a command that null-delimits its own output, this is the single most robust way to handle filenames in Bash, including ones containing newlines, which even the while IFS= read -r line pattern from Chapter 8 can’t fully protect against:

while IFS= read -r -d '' file; do
    echo "Found: $file"
done < <(find . -maxdepth 1 -type f -print0)

find -print0 separates each result with a null byte instead of a newline — a character that can never legally appear inside a filename — and read -d '' reads up to each null byte instead of each newline. Combined with the process substitution from the previous chapter, this is the most bulletproof filename-handling pattern available in Bash, closing out the “safe filename handling” thread this course has followed since Chapter 8.

Reading A Fixed Number Of Characters With -n

-n count reads exactly that many characters and returns immediately, without waiting for Enter — useful for single-keypress confirmations:

read -n 1 -p "Continue? (y/n) " answer
echo
echo "You pressed: $answer"
Continue? (y/n) y
You pressed: y

Word Splitting With read

Let’s understand the role of IFS first. By default, it contains:

  • Space ( )
  • Tab (\t)
  • Newline (\n)

If you don’t trust me, see it yourself:

printf '%q\n' "$IFS"

Output for default IFS: $' \t\n' (representing space, horizontal tab, and newline).

What read does is:

  • reads till the end of unescaped newline or end of the input
  • it processes the backslashes too
  • splits input into words on the basis of IFS
  • assigns splitted words to given variables a/c to rules

Example1: read With Default IFS

Start with a simple exmple of:

read a b c <<< "i love linux bro"
  • Since no unescaped newline found, read reads the whole input as "i love linux bro"
  • It doesn’t have backslashes to process
  • It splits the whole input into three parts: "i", "love", "linux", "bro" because space ( ) is the default seperator present in the input string

This means:

  • $ai
  • $blove
  • $clinux bro

Important

The last variable will get all the remaining splitted words that’s why bro is also the part of $c which is the last variable.

Example2: read With Backslashes

Let’s see an example involving backslashes:

read a b c <<< "foo\ bar blah\ bla\
blah whatever bro"

This means:

  • $afoo bar (space after foo is discarded by \ , doesn’t undergo word splitting)
  • $bblah bla blah (same reason)
  • $cwhatever bro (obvious, the remaining)

Let’s change a scenario:

read a b c <<< "foo\ bar blah\ bla\
    blah whatever bro"

This time there are spaces before the start of blah whatever bro in continued line. This means:

  • $afoo bar (space after foo is discarded by \ , doesn’t undergo word splitting)
  • $bblah bla (space after blah is discarded, a single space after bla is also discarded but there are more spaces which can’t be discarded with single \ hence undergoes word splitting)
  • $cblah whatever bro (obvious, the remaining)

Example3: read Multiline Without \ Ending

Let’s go one step further:

read a b c <<< "foo\ bar blah\ bla
blah whatever bro"

This time no \ after the first line:

  • $afoo bar (space after foo is discarded by \ , doesn’t undergo word splitting)
  • $bblah bla (same reason)
  • $c ⇐ Empty (newline found after bla unescaped i.e. no \ ending)

You can verify that with hexdump:

echo -n "foo\ bar blah\ bla
blah whatever bro"
00000000  66 6f 6f 5c 20 62 61 72  20 62 6c 61 68 5c 20 62  |foo\ bar blah\ b|
00000010  6c 61 0a 62 6c 61 68 20  77 68 61 74 65 76 65 72  |la.blah whatever|
00000020  20 62 72 6f                                       | bro|
00000024

See the hex 0a representing the newline (LF \n) which is represented as . at the right side. This means read finishes it’s reading and stop right there. whatever bro were never subject to word splitting, in a nutshell.

Now, let’s read the same thing with the -r option where \ are not processed:

read -r a b c <<< "foo\ bar blah\ bla
blah whatever bro"

Even with the -r, only this part (foo\ bar blah\ bla) will be subject for the read to work further. Without \ processing:

  • $afoo\ (anything before first delimeter — space )
  • $bbar (same reason)
  • $cblah\ bla (the remaining before LF)

If you wish to read the whole input, use null delimiter -d '':

read -r -d '' a b c <<< "foo\ bar blah\ bla
blah whatever bro"

Now $c will get the whole remaining part from the input:

blah\ bla
blah whatever bro

Example4: read With Different IFS

Let’s change the default rule for word splitting with custom IFS value scoped to read. I am choosing : as such IFS=:. So:

IFS=: read a b c <<< ":alice::bob::"

The values assigned to the variables are:

  • $a"" (The input string :alice::bob:: starts immediately with a colon (:). Because IFS is set to :, read sees an empty field before the first colon and assigns it to a)
  • $balice (b receives the content between the first and second colons)
  • $c:bob:: (Because c is the last variable supplied to read, it captures all remaining unconsumed text after the second colon — including any internal field separators (:) and trailing colons)

For more on IFS, see this unix stackexchange answer, also baeldung’s examples.

Best Practices

  • Always use -r, in every call to read, unless you have a specific reason not to — this chapter’s Shell-Safety Considerations section demonstrates exactly what goes wrong without it.
  • Use -p instead of a separate echo before read — it’s shorter, and it correctly avoids a trailing newline between the prompt and the person’s typed response.
  • Follow every -s read with a plain echo to restore the newline suppressed by silent mode, before printing anything else.
  • Check read’s exit status when using -t — don’t assume the variable actually contains meaningful input just because the line ran without error.
  • Reach for -d '' with find -print0 when a script’s correctness genuinely depends on handling every possible filename, including ones with embedded newlines — the plain IFS= read -r line pattern is sufficient for the vast majority of everyday scripts, but this is the fully robust version when it matters.

Shell-Safety Considerations

Chapter 8 introduced -r as part of a fixed pattern without fully explaining what breaks without it. Here’s the concrete demonstration.

Naive (broken) version — reading a value containing backslashes, without -r:

read path <<< 'C:\Users\test\notes'
echo "$path"
C:Userstestnotes

Every backslash vanished. Without -r, read treats backslash as an escape character exactly the way unquoted shell text does elsewhere — it removes the special meaning of (and is itself stripped from) whatever character follows it. Since none of the characters following those backslashes had any special meaning to strip, the net effect was simply deleting every backslash from the input.

Corrected version — with -r:

read -r path <<< 'C:\Users\test\notes'
echo "$path"
C:\Users\test\notes

Every backslash survives intact. Any input that might legitimately contain a backslash — Windows-style paths, regular expressions, escaped characters in general — needs -r to come through read unmodified, which is exactly why this course has used -r in every read call since it was first introduced.

Hands-On: Prompts, Silence, Timeouts, And Robust Reading

1. Set up a working directory.

mkdir -p ~/shell-course/ch13
cd ~/shell-course/ch13

2. Practice a basic prompted read.

read -p "Enter your name: " name
echo "Hello, $name"

3. Try silent input for a password-style prompt.

read -s -p "Password: " password
echo
echo "Captured ${#password} character(s)."

4. Try a timed read, and check its exit status.

if read -t 5 -p "Quick — type anything: " answer; then
    echo "Got: $answer"
else
    echo "Too slow."
fi

5. Reproduce the last-variable-absorbs-everything behavior.

read first last <<< "Alice Middle Names Smith"
echo "First: [$first]"
echo "Last: [$last]"

6. Read a line into an array.

read -a words <<< "The quick brown fox"
echo "${#words[@]}"
echo "${words[@]}"

7. Reproduce the missing--r backslash bug, then fix it.

echo "--- broken ---"
read path <<< 'C:\Users\test\notes'
echo "$path"

echo "--- fixed ---"
read -r path <<< 'C:\Users\test\notes'
echo "$path"

8. Use -d '' with find -print0 for fully robust filename handling.

mkdir -p /tmp/shell-course-demo
cd /tmp/shell-course-demo
touch "quarterly report.txt" notes.txt

while IFS= read -r -d '' file; do
    echo "Found: $file"
done < <(find . -maxdepth 1 -type f -print0)

9. Clean up.

cd ~
rm -rf ~/shell-course/ch13
rm -rf /tmp/shell-course-demo

You’ve now prompted for input, captured a password silently, handled a timeout by checking read’s own exit status, seen exactly how extra fields get absorbed into a final variable, read a full line into an array, and confirmed precisely why -r matters using a real backslash-containing value. That completes this section’s coverage of parameters, expansion, and input. The next section moves into control flow and structure, starting with command grouping and subshells — the exact mechanism behind the pipeline variable-loss problem this course has flagged since Chapter 8.

Last updated on