VMware ESXi and vSphere Cluster Management

read: Read User Input in Shell Scripts

Learn how Bash and POSIX shell scripts use read to accept, split, validate, hide, time out, and safely process interactive and file input.

The read builtin receives data from standard input and stores it in shell variables. Standard input is the input stream a command normally reads, such as keyboard input, a redirected file, or another command's output.

read is a shell builtin, not a general-purpose command shared by every programming language. Its exact options depend on the shell. This lesson focuses on Bash-compatible scripts and identifies options that are not broadly portable.

Common uses include interactive prompts, configuration values, menu choices, passwords, and line-by-line file processing. For related shell concepts, see shell input with read.

Basic syntax and variable assignment

read variable_name

This command waits for one line from standard input, removes the terminating newline, and assigns the result to variable_name.

#!/usr/bin/env bash

read name
printf 'Hello, %s!\n' "$name"

Always quote a variable expansion when inserting input into another command or message. Quoting prevents spaces and wildcard characters from being interpreted as separate words or filename patterns.

Multiple variables

read first last

By default, the input line is split into fields using the shell's IFS setting, then fields are assigned from left to right.

  • With input Ada Lovelace, first becomes Ada and last becomes Lovelace.
  • If there are fewer fields than variables, remaining variables become empty.
  • If there are more fields than variables, the final variable receives the remaining fields, joined with the first character of IFS.
printf '%s\n' 'Ada Byron Lovelace' | read first last
# first is Ada; last receives Byron Lovelace in a shell that runs read directly

The default variable: REPLY

If no variable name is supplied, Bash stores the input in REPLY.

read -r
printf 'You entered: %s\n' "$REPLY"

Prompting for input

In Bash, -p displays a prompt without printing a newline before reading.

read -r -p "Name: " name
printf '\nHello, %s!\n' "$name"

Quote prompt text so spaces, punctuation, and shell metacharacters remain literal. A prompt usually includes a trailing space, and interactive scripts commonly print a newline afterward when input does not echo one.

For a portable prompt-and-read pattern, print the prompt separately and then call read:

printf 'Name: '
IFS= read -r name
printf 'Hello, %s!\n' "$name"

This separate pattern avoids relying on Bash's -p option. Use a shebang that declares the intended shell when a script needs Bash-specific behavior:

#!/usr/bin/env bash

Input splitting and IFS

IFS means Internal Field Separator. It is a shell setting that tells read which characters separate fields. With the normal default, spaces, tabs, and newlines participate in whitespace splitting. Leading and trailing IFS whitespace is removed during ordinary field splitting.

Preserving a complete line

Use an empty temporary IFS together with -r when the complete line matters:

IFS= read -r line

This preserves leading and trailing whitespace and prevents backslashes from being treated specially. The assignment affects this invocation of read, not the rest of the script.

Parsing a delimiter-separated record

IFS=, read -r field1 field2 field3

This splits one input line at commas. It is useful for simple records, but it is not a complete CSV parser: quoted commas, escaped quotes, and multiline CSV fields need a CSV-aware tool.

Backslashes, quoting, and literal input

Without -r, read treats backslashes as escape characters. A backslash can remove the special meaning of the next character or join lines. This can alter Windows paths, regular expressions, and other literal text.

IFS= read -r path

Use -r by default when input should be treated literally. After reading input, quote expansions:

printf 'Selected path: %s\n' "$path"
cp -- "$path" /tmp/backup/

Do not use raw input as shell code. Never pass untrusted input to eval, and do not construct a command string and execute it merely because it came from a variable. Use fixed commands, validation, and quoted arguments.

Silent and character-limited input

In Bash, -s suppresses terminal echo, which is useful for passwords. The value is still stored in a variable, so avoid printing it or exposing it unnecessarily.

read -r -s -p "Password: " password
printf '\nPassword received.\n'

The prompt's input line may not end with a visible newline, so print one after the read when appropriate.

-n reads after a specified number of characters instead of waiting for a complete line:

printf 'Press y or n: '
read -r -n 1 answer
printf '\n'

-n is Bash-specific and counts characters according to the shell's input handling. It is commonly used for one-key choices, but a terminal may still need a newline printed afterward.

Timeouts, file descriptors, and status codes

In Bash, -t sets a timeout in seconds, and -u selects a numbered file descriptor. A file descriptor is a numbered handle for an open input or output stream.

if read -r -t 5 answer; then
  printf 'Input received: %s\n' "$answer"
else
  printf 'No complete response arrived in time.\n' >&2
fi

A command's exit status is its success or failure code. For read, a successful read normally returns status zero. A nonzero status indicates that the read did not complete normally, such as end-of-file, a timeout, or interruption. Exact nonzero values and timeout details can vary by shell version, so scripts should test success versus failure and, when needed, use shell-specific documentation for finer distinctions.

  • Input received: process the variable.
  • Empty input: the read can succeed while the variable is empty; validate it if the value is required.
  • EOF: no more input is available. Ctrl-D on a terminal can produce EOF.
  • Timeout: stop waiting and choose a documented default or retry path.
  • Interruption: handle a nonzero result and avoid using an incomplete value.

Scripts must not assume standard input is an interactive terminal. Input may be closed, redirected, unavailable in a service, or already consumed by another command.

Reading from an explicit descriptor

#!/usr/bin/env bash

exec 3< settings.txt
while IFS= read -r -u 3 line; do
  printf 'Setting: %s\n' "$line"
done
exec 3<&-

Using descriptor 3 keeps file input separate from descriptor 0, which is standard input and can remain available for user interaction.

Reading files and streams safely

The standard line-processing pattern is:

while IFS= read -r line || [ -n "$line" ]; do
  printf 'Processing: %s\n' "$line"
done < input.txt

The redirection supplies input.txt to the loop's standard input. IFS= read -r line preserves each line literally. The additional [ -n "$line" ] condition handles a final line that does not end with a newline. Without it, that final unterminated line can be skipped because read reports EOF while returning the partial data.

When variables assigned inside the loop must still exist afterward, avoid piping into the loop:

count=0
while IFS= read -r line; do
  count=$((count + 1))
done < input.txt
printf 'Lines: %s\n' "$count"

A pipeline-created loop commonly runs in a subshell, a child shell environment whose variable changes may not affect the parent shell. File redirection keeps this loop in the current shell in common Bash usage. Bash process substitution is another option when appropriate:

while IFS= read -r line; do
  printf '%s\n' "$line"
done < <(some_command)

Validation and defensive scripting

Reading input is only the first step. Validate before using it. A robust prompt usually handles required values, numeric ranges, allowed choices, empty input, and EOF.

Required input

while :; do
  printf 'Project name: '
  if ! IFS= read -r project; then
    printf '\nInput cancelled.\n' >&2
    exit 1
  fi
  if [ -n "$project" ]; then
    break
  fi
  printf 'A project name is required.\n' >&2
done

Numeric and bounded input

while :; do
  printf 'Choose a number from 1 to 10: '
  if ! IFS= read -r number; then
    printf '\nInput cancelled.\n' >&2
    exit 1
  fi
  case $number in
    1|2|3|4|5|6|7|8|9|10) break ;;
    *) printf 'Enter a whole number from 1 to 10.\n' >&2 ;;
  esac
done

For more complex numeric validation in Bash, use arithmetic tests only after deciding how to handle nonnumeric text. Do not assume that arbitrary input is a valid arithmetic expression.

Choice-based validation

while :; do
  printf 'Action [c]ontinue, [q]uit: '
  if ! IFS= read -r choice; then
    printf '\nInput cancelled.\n' >&2
    exit 1
  fi
  case $choice in
    c|C) printf 'Continuing.\n'; break ;;
    q|Q) printf 'Goodbye.\n'; exit 0 ;;
    *) printf 'Enter c or q.\n' >&2 ;;
  esac
done

Common read options

Option: -pPurpose: display an inline prompt. Portability: Bash-specific; print with printf separately for a portable pattern. Example: read -p "Name: " name.

Option: -rPurpose: do not interpret backslashes. Portability: broadly available in POSIX-style shells. Example: IFS= read -r line.

Option: -sPurpose: suppress terminal echo. Portability: common Bash feature, not portable POSIX behavior. Example: read -s password.

Option: -nPurpose: read a specified number of characters. Portability: Bash-specific. Example: read -n 1 answer.

Option: -tPurpose: stop waiting after a timeout. Portability: Bash-specific. Example: read -t 5 answer.

Option: -uPurpose: read from a selected file descriptor. Portability: common Bash feature but not required by POSIX. Example: read -u 3 line.

Useful input-handling patterns

Read one normal response: read -r answer. Use a named variable and preserve backslashes.

Preserve an entire line: IFS= read -r line. Prevents field splitting and backslash processing.

Read delimited fields: IFS=, read -r first second third. Splits one record at commas.

Read a password: read -r -s -p "Password: " password. Hides terminal echo in Bash.

Read file lines: while IFS= read -r line || [ -n "$line" ]; do ...; done < file. Preserves lines and handles a missing final newline.

Handle a timeout: if read -r -t 5 answer; then ...; else ...; fi. Branches on the exit status.

Result conditions at a glance

Input received: read normally succeeds. Validate and process the value.

Empty input: the command may still succeed with an empty variable. Reject it when required or assign a documented default.

End-of-file: read returns nonzero. Exit, use a default, or report cancellation.

Timeout: read -t returns nonzero when the wait expires. Retry or continue with a defined fallback.

Interrupted input: treat the nonzero result as incomplete input and provide a safe recovery path.

Shell compatibility

read is a shell builtin, so invoke it directly inside the target shell rather than assuming an external executable is available. POSIX behavior centers on reading variables, IFS, and -r. Options such as -p, -s, -n, -t, and -u are Bash or implementation-specific.

Choose the shell deliberately. A Bash script should begin with #!/usr/bin/env bash and may use Bash-specific options. A script intended for broad POSIX shell compatibility should use a POSIX shebang and avoid those options, using patterns such as printf followed by read -r.

Troubleshooting

  • Leading spaces disappear or a line splits unexpectedly: default IFS processing is active. Use IFS= read -r line.
  • Backslashes are altered: add -r.
  • The final file line is skipped: the line lacks a terminating newline. Use while IFS= read -r line || [ -n "$line" ].
  • Loop variables are empty afterward: the loop likely ran in a pipeline-created subshell. Redirect input into the loop instead.
  • Ctrl-D causes unexpected behavior: EOF was not checked. Test read's status and define an EOF path.
  • A prompt option fails under another shell: it is shell-specific. Declare Bash or print the prompt separately.
  • Input appears to execute as commands: raw input was evaluated unsafely. Quote expansions and never use eval on user input.

Exam-relevant notes

  • read reads standard input and assigns shell variables.
  • REPLY is Bash's destination when no variable name is supplied.
  • IFS= read -r line is the key pattern for preserving a complete line.
  • A loop fed by input redirection avoids the common pipeline-subshell variable problem.
  • A missing final newline requires || [ -n "$line" ] in a line-reading loop.
  • Always distinguish a successful empty response from EOF, timeout, or interruption by checking the exit status and validating the value.