📖 read Command

Reading Input and Data in Bash Shell Scripts

About the read Command

The read command is a bash builtin used to read input from standard input (keyboard) or from files, and assign the values to shell variables. It's fundamental for creating interactive shell scripts that accept user input, process file contents line by line, and parse structured data. The read command is one of the most versatile tools in bash scripting, supporting features like timeouts, prompts, arrays, delimiters, and silent (password) input.

Key capabilities: Interactive user input, file processing line-by-line, parsing CSV and delimited data, reading into arrays, password input without echo, setting timeouts for input, reading specific number of characters, and processing command output. Read is a builtin command (part of bash itself) rather than an external program, making it very fast and efficient.

Common use cases: Menu systems in scripts, configuration file processing, user confirmation prompts, password input, parsing log files, reading API responses, processing command output, and creating interactive installation scripts.

Basic Syntax: read [OPTIONS] [VARIABLE...] Common Usage: read variable # Read input into variable read var1 var2 var3 # Read space-separated values read -p "Prompt: " variable # Display prompt before reading read -s password # Silent mode (no echo, for passwords) read -t 5 variable # Timeout after 5 seconds read -n 1 key # Read single character read -a array # Read into array read -d ':' variable # Use custom delimiter read -r line # Raw mode (don't interpret backslashes) Common Options: -p prompt Display prompt without newline -s Silent mode (no echo to terminal) -t timeout Timeout in seconds -n nchars Return after reading nchars characters -N nchars Return after reading exactly nchars characters -d delim Continue until delim is read (not newline) -r Raw mode (backslash doesn't escape) -a array Read into array (split on IFS) -i text Use text as initial value (with readline) -e Use readline for line editing -u fd Read from file descriptor fd

Detailed Examples

1Basic User Input

The simplest use of read - getting input from the user into a variable.

#!/bin/bash echo "What is your name?" read name echo "Hello, $name! Welcome to Linux."
$ ./script.sh What is your name? Craig Hello, Craig! Welcome to Linux.
What's happening: The script uses echo to display a prompt, then read name waits for user input. When the user types "Craig" and presses Enter, that text is stored in the $name variable. The script then uses that variable in the welcome message. Key behavior: Read waits indefinitely until the user presses Enter (newline character). Everything typed before Enter is captured. If the user just presses Enter without typing anything, the variable becomes empty. Leading and trailing whitespace is preserved in the variable. This is the most basic form - simple but requires two lines (prompt + read). The next example shows a more efficient approach.
If no variable name is provided to read, the input is stored in the special variable $REPLY. This is useful for quick scripts: read; echo "You entered: $REPLY"

2Read with Inline Prompt

Using the -p flag to display a prompt without needing a separate echo command.

#!/bin/bash read -p "Enter your username: " username read -p "Enter your email: " email echo "" echo "Account Details:" echo "Username: $username" echo "Email: $email"
$ ./script.sh Enter your username: craig Enter your email: craig@example.com Account Details: Username: craig Email: craig@example.com
What's happening: The -p flag (prompt) displays the provided text and then waits for input on the same line. This is much cleaner than using separate echo commands. The prompt text "Enter your username: " is displayed, the cursor waits immediately after the colon and space, and the user's input appears right there. This creates a professional, interactive feel. Technical detail: The -p prompt is sent to standard error (stderr), not standard output (stdout), which means it won't be captured if you redirect the script's output. This is intentional design - prompts should go to the terminal even when output is redirected. This is more efficient and readable than the previous example's two-line approach, and it's the standard way to prompt for input in bash scripts.
Always include a space at the end of your prompt text (after the colon) so the user's input doesn't run directly into the prompt. Compare "Name:" vs "Name: " - the space makes it much more readable.

3Silent Input for Passwords

Using -s flag to hide input, essential for password entry and sensitive data.

#!/bin/bash read -sp "Enter password: " password echo "" # Move to new line after password entry read -sp "Confirm password: " password2 echo "" if [ "$password" = "$password2" ]; then echo "Password set successfully!" else echo "Passwords do not match!" exit 1 fi
$ ./script.sh Enter password: Confirm password: Password set successfully!
What's happening: The -s (silent) flag prevents echoing characters to the terminal as they're typed. Combined with -p, this creates a secure password prompt. When the user types their password, nothing appears on screen - this prevents shoulder surfing and keeps passwords out of terminal history. After each password entry, we echo an empty line to move to the next line (because read -s doesn't automatically print a newline when Enter is pressed). The script then compares both password entries to ensure they match. Important security note: While -s prevents the password from appearing on screen, it doesn't encrypt it or prevent it from appearing in process lists if passed as command-line arguments. Passwords in variables are stored in memory in plaintext. For production systems, consider using proper authentication libraries instead of storing passwords in shell scripts.
Never echo passwords back to the screen, even for confirmation. Use comparison logic like shown here. Also, avoid storing passwords in files or passing them as command-line arguments where they appear in process listings (ps aux). Consider using password managers or authentication tokens for production systems.
After using read -s, always output a newline (echo "") so subsequent output doesn't appear on the same line as the password prompt. This is a common mistake that makes scripts look broken.

4Reading Multiple Values

Split input into multiple variables using whitespace as delimiter.

#!/bin/bash read -p "Enter first name and last name: " first last echo "First name: $first" echo "Last name: $last" echo "" read -p "Enter three numbers: " num1 num2 num3 sum=$((num1 + num2 + num3)) echo "Sum: $sum"
$ ./script.sh Enter first name and last name: Craig Johnson First name: Craig Last name: Johnson Enter three numbers: 10 20 30 Sum: 60
What's happening: When multiple variable names are provided to read, the input is split on whitespace (by default, spaces and tabs). The first word goes to the first variable, second word to the second variable, and so on. In the first example, "Craig" goes to $first and "Johnson" goes to $last. In the second example, three numbers are split into three variables. Important behavior: If the user enters more words than variables, all remaining words go into the last variable. For example, if the user entered "Craig Michael Johnson" with only two variables, $first would be "Craig" and $last would be "Michael Johnson". If fewer words are entered than variables, the extra variables become empty. The splitting is controlled by the IFS (Internal Field Separator) variable, which defaults to space, tab, and newline.
This behavior (extra input going into the last variable) is actually useful for reading "first rest" patterns. For example: read first rest would put the first word in $first and everything else in $rest, which is perfect for parsing commands with arguments.

5Reading with Timeout

Set a timeout to prevent scripts from waiting indefinitely for input.

#!/bin/bash echo "Quick! You have 5 seconds to enter your name!" if read -t 5 -p "Name: " name; then echo "Hello, $name!" else echo "" echo "Too slow! Using default name: Guest" name="Guest" fi echo "Continuing with name: $name"
$ ./script.sh Quick! You have 5 seconds to enter your name! Name: Craig Hello, Craig! Continuing with name: Craig $ ./script.sh Quick! You have 5 seconds to enter your name! Name: Too slow! Using default name: Guest Continuing with name: Guest
What's happening: The -t 5 flag sets a 5-second timeout. If the user doesn't press Enter within 5 seconds, read returns with exit status 1 (failure) and the variable remains empty or unchanged. By using read in an if statement, we can detect whether the user provided input in time (exit status 0) or timed out (exit status 1). If timeout occurs, we set a default value. Practical uses: Automated scripts that shouldn't hang indefinitely, default value prompts where you want to continue even if user doesn't respond, countdown timers, and interactive menus with auto-proceed. The timeout value can be a decimal (e.g., -t 0.5 for half a second). A timeout of 0 tests if input is available without waiting at all.
Use timeouts in automation scripts that might run unattended. For example, during system updates or deployments, you might want to prompt for confirmation but automatically proceed after 10 seconds if no one responds.
When read times out, it returns exit status > 128 in bash 4.0+. Always check the exit status rather than just checking if the variable is empty, because the user might have entered nothing before the timeout.

6Reading Single Character Input

Use -n to read a specific number of characters without waiting for Enter.

#!/bin/bash echo "=== Simple Menu ===" echo "1) Option One" echo "2) Option Two" echo "3) Option Three" echo "Q) Quit" echo "" read -n 1 -p "Select option: " choice echo "" # Move to new line case $choice in 1) echo "You selected Option One" ;; 2) echo "You selected Option Two" ;; 3) echo "You selected Option Three" ;; q|Q) echo "Exiting..." exit 0 ;; *) echo "Invalid option: $choice" ;; esac
$ ./script.sh === Simple Menu === 1) Option One 2) Option Two 3) Option Three Q) Quit Select option: 2 You selected Option Two
What's happening: The -n 1 flag tells read to return immediately after reading exactly 1 character. The user doesn't need to press Enter - as soon as they press any key, that character is captured and the script continues. This creates responsive, menu-driven interfaces where pressing a single key makes a selection. The case statement then processes the choice. Difference between -n and -N: -n 1 returns after reading 1 character OR when Enter is pressed (whichever comes first), while -N 1 waits for exactly 1 character and ignores Enter. For menus, -n is usually preferred because it's more flexible. This technique is perfect for yes/no confirmations, menu systems, and interactive prompts where typing full words would be tedious.
For yes/no prompts, a common pattern is: read -n 1 -p "Continue? (y/n): " answer; echo ""; [[ $answer =~ ^[Yy]$ ]] && echo "Continuing..."

7Reading Lines from a File

Process a file line-by-line using read in a while loop.

#!/bin/bash # Create sample file cat > users.txt << EOF craig:admin:1001 alice:developer:1002 bob:developer:1003 eve:manager:1004 EOF echo "Processing user file..." echo "" while IFS=':' read -r username role userid; do echo "User: $username" echo " Role: $role" echo " User ID: $userid" echo "" done < users.txt echo "Processing complete."
$ ./script.sh Processing user file... User: craig Role: admin User ID: 1001 User: alice Role: developer User ID: 1002 User: bob Role: developer User ID: 1003 User: eve Role: manager User ID: 1004 Processing complete.
What's happening: This demonstrates file processing with custom delimiters. The pattern while read ... done < file reads the file line by line. IFS=':' temporarily sets the Internal Field Separator to colon, so each line is split on colons instead of spaces. The three fields are assigned to username, role, and userid variables. The -r flag prevents backslash interpretation (raw mode), which is crucial for processing files that might contain backslashes. Why this pattern works: The while loop continues as long as read successfully reads a line (exit status 0). When read reaches end-of-file, it returns exit status 1, ending the loop. The < redirection at the end feeds the file into the loop. This is the standard idiom for processing files in bash and handles files of any size efficiently because it reads one line at a time rather than loading the entire file into memory.
Always use -r when reading files unless you specifically need backslash interpretation. Without -r, a backslash at the end of a line is treated as a line continuation, joining it with the next line, which is rarely what you want for data processing.
For CSV files, set IFS=',' to split on commas. For tab-delimited files, use IFS=$'\t'. The IFS assignment before read only affects that single command, so you don't need to save and restore the original value.

8Reading into an Array

Use -a flag to read input into an indexed array.

#!/bin/bash read -p "Enter names separated by spaces: " -a names echo "You entered ${#names[@]} names:" for i in "${!names[@]}"; do echo " [$i] ${names[$i]}" done echo "" echo "First name: ${names[0]}" echo "Last name: ${names[-1]}" echo "All names: ${names[*]}"
$ ./script.sh Enter names separated by spaces: Craig Alice Bob Eve You entered 4 names: [0] Craig [1] Alice [2] Bob [3] Eve First name: Craig Last name: Eve All names: Craig Alice Bob Eve
What's happening: The -a names flag tells read to split the input on IFS (whitespace by default) and store each piece as an element in the names array. ${#names[@]} gives the number of elements. ${!names[@]} gives the array indices (0, 1, 2, 3). Individual elements are accessed with ${names[0]}, ${names[1]}, etc. Negative indices count from the end: ${names[-1]} is the last element. ${names[*]} expands to all elements. Array advantages: Unlike reading into multiple variables where leftover input goes to the last variable, arrays capture all input elements separately. This is perfect for unknown numbers of inputs, processing lists, and iterating over values. Arrays can be processed with loops, filtered, sorted, and manipulated in various ways that aren't possible with simple variables.
When iterating over array elements that might contain spaces, always use quotes: for name in "${names[@]}"; do ... done. The @ expands each element as a separate word, while * would join them into a single word.

9Custom Delimiter Reading

Use -d to specify a custom delimiter instead of newline.

#!/bin/bash # Create a file with colon-separated records on one line echo "craig:admin:alice:developer:bob:manager" > data.txt echo "Reading records with custom delimiter..." echo "" while read -r -d ':' person; do [ -n "$person" ] && echo "Person: $person" done < data.txt echo "" echo "---" echo "" # Reading null-terminated strings (common with find -print0) echo "Simulating null-terminated filenames..." printf "file1.txt\0file2.txt\0file 3.txt\0" | while read -r -d '' filename; do echo "Found: '$filename'" done
$ ./script.sh Reading records with custom delimiter... Person: craig Person: admin Person: alice Person: developer Person: bob --- Simulating null-terminated filenames... Found: 'file1.txt' Found: 'file2.txt' Found: 'file 3.txt'
What's happening: The -d ':' flag sets the delimiter to colon instead of newline. Read now returns each time it encounters a colon, not when it finds a newline. This allows processing records separated by characters other than newlines. In the first example, we process colon-separated values on a single line. The null delimiter trick: Using -d '' (empty string) sets the delimiter to null byte (\0), which is the safest way to handle filenames because null bytes cannot appear in Unix filenames. This is critical when processing output from find -print0 or sort -z, which use null bytes to separate results. The third filename "file 3.txt" contains a space, which would cause problems with space-delimited reading but works perfectly with null-delimited reading. This technique is essential for safely handling files with spaces, newlines, or other special characters in their names.
Always use null-delimited output (find -print0) and null-delimited reading (read -d '') when processing filenames in scripts. Filenames can contain any character except null and forward slash, including newlines, spaces, tabs, and control characters. Space or newline delimited processing will fail on such files.
Common pattern for safe file processing: find /path -type f -print0 | while read -r -d '' file; do process "$file"; done

10Advanced Interactive Script

Combining multiple read techniques in a complete interactive script.

#!/bin/bash # Color codes for better UI RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color echo -e "${GREEN}=== User Registration System ===${NC}" echo "" # Get username with validation while true; do read -p "Enter username (3-20 chars): " username if [[ ${#username} -ge 3 && ${#username} -le 20 ]]; then break else echo -e "${RED}Invalid length. Try again.${NC}" fi done # Get email with basic validation while true; do read -p "Enter email: " email if [[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then break else echo -e "${RED}Invalid email format. Try again.${NC}" fi done # Get password securely while true; do read -sp "Enter password (min 8 chars): " password echo "" if [ ${#password} -lt 8 ]; then echo -e "${RED}Password too short.${NC}" continue fi read -sp "Confirm password: " password2 echo "" if [ "$password" = "$password2" ]; then break else echo -e "${RED}Passwords don't match. Try again.${NC}" fi done # Get interests as array read -p "Enter interests (space-separated): " -a interests # Confirmation with timeout echo "" echo -e "${YELLOW}Please review your information:${NC}" echo "Username: $username" echo "Email: $email" echo "Interests: ${interests[*]}" echo "" if read -t 10 -n 1 -p "Confirm? (y/n): " confirm; then echo "" if [[ $confirm =~ ^[Yy]$ ]]; then echo -e "${GREEN}Registration successful!${NC}" # Save to file echo "$username:$email:${interests[*]}" >> users.db echo "Account saved to database." else echo -e "${RED}Registration cancelled.${NC}" fi else echo "" echo -e "${RED}Timeout. Registration cancelled.${NC}" fi
$ ./script.sh === User Registration System === Enter username (3-20 chars): craig Enter email: craig@example.com Enter password (min 8 chars): Confirm password: Enter interests (space-separated): linux scripting automation Please review your information: Username: craig Email: craig@example.com Interests: linux scripting automation Confirm? (y/n): y Registration successful! Account saved to database.
What's happening: This comprehensive script demonstrates multiple read techniques in a realistic scenario: (1) Validation loops: Username and email are validated with regex patterns and length checks, looping until valid input is received. (2) Silent password input: Using read -sp for secure password entry with length validation and confirmation. (3) Array input: Interests are captured into an array with -a flag. (4) Color output: ANSI escape codes make the interface more user-friendly. (5) Timeout confirmation: The final confirmation uses -t 10 -n 1 to give users 10 seconds to confirm with a single keypress. (6) Data persistence: Valid registrations are appended to a users.db file in a colon-delimited format. Real-world improvements: In production, you'd add password hashing (bcrypt/argon2), SQL injection protection if using a database, email verification, and proper error handling. This example demonstrates input validation patterns that prevent many common issues in interactive scripts.
Always validate user input in loops. Never trust that users will enter data in the expected format. Provide clear error messages and allow them to retry. This creates a much better user experience than cryptic error messages or script failures.
This example stores passwords in plain text, which is never acceptable in production. Always use proper password hashing (bcrypt, argon2, scrypt) and never store passwords reversibly. The validation regex patterns shown are basic - implement comprehensive validation for production systems.

Complete Options Reference

Option Description Example
-p prompt Display prompt on standard error before reading read -p "Name: " name
-s Silent mode - do not echo input (for passwords) read -sp "Password: " pass
-t timeout Timeout in seconds (can be decimal) read -t 5 response
-n nchars Return after reading nchars (or newline) read -n 1 key
-N nchars Return after reading exactly nchars (ignore delimiters) read -N 4 code
-d delim Use delim as delimiter instead of newline read -d ':' field
-r Raw mode - backslash doesn't escape characters read -r line
-a array Read words into indexed array read -a items
-u fd Read from file descriptor fd instead of stdin read -u 3 line
-i text Use text as initial value (requires -e) read -ei "default" value
-e Use readline for line editing (arrow keys, history) read -e command

Special Variables and IFS

The $REPLY Variable

If no variable name is provided to read, input is stored in $REPLY:
read -p "Enter something: " echo "You entered: $REPLY"

The IFS Variable

IFS (Internal Field Separator) controls how read splits input into variables. Default is space, tab, and newline. Examples:
# Default behavior (splits on whitespace) read first last # Input: "John Doe" → first="John", last="Doe" # Custom delimiter (colon) IFS=':' read user pass # Input: "john:secret123" → user="john", pass="secret123" # Multiple delimiters (comma or colon) IFS=':,' read -a fields # Input: "a:b,c:d" → fields=([0]="a" [1]="b" [2]="c" [3]="d") # Tab delimiter IFS=$'\t' read col1 col2 col3 # Input: "A B C" → col1="A", col2="B", col3="C"

Exit Status

Read returns different exit codes:
  • 0 - Success: input was successfully read
  • 1 - End-of-file reached (or timeout occurred)
  • >128 - Timeout occurred (bash 4.0+)
# Using exit status for control flow if read -t 5 response; then echo "Got input: $response" else echo "No input (timeout or EOF)" fi # Detecting EOF in file processing while read line; do process "$line" done < file.txt # Loop exits when read returns 1 (EOF)

Best Practices and Patterns

General Best Practices

  • Always use -r for file processing: Prevents backslash interpretation which can corrupt data
  • Quote variable expansions: Use "$variable" not $variable to handle spaces and special characters
  • Validate input: Never trust user input - always validate format, length, and content
  • Provide clear prompts: Tell users exactly what format you expect (e.g., "Enter date (YYYY-MM-DD):")
  • Use -p for prompts: More efficient than echo + read and keeps code cleaner
  • Set appropriate timeouts: Use -t in automated scripts to prevent hanging
  • Handle empty input: Check if variables are empty and provide defaults or re-prompt
  • Use -s for sensitive data: Always use silent mode for passwords and other secrets
  • Add newline after -s: Always echo "" after read -s to move to next line
  • Use arrays for lists: When reading multiple items, -a is cleaner than multiple variables

File Processing Patterns

  • Standard line processing: while IFS= read -r line; do ... done < file
  • CSV processing: while IFS=',' read -r field1 field2 field3; do ... done < file.csv
  • Safe filename handling: find . -print0 | while read -r -d '' file; do ... done
  • Reading command output: command | while read -r line; do ... done
  • Skipping first line (header): { read; while read -r line; do ... done; } < file
  • Processing last field: while read -r first rest; do ... done (rest gets everything after first word)

Interactive Script Patterns

  • Yes/No confirmation: read -n 1 -p "Continue? (y/n): " answer; [[ $answer =~ ^[Yy]$ ]]
  • Default values: read -p "Name [$USER]: " name; name=${name:-$USER}
  • Validation loop: while true; do read input; [[ $input =~ pattern ]] && break; done
  • Menu selection: read -n 1 -p "Select (1-5): " choice; case $choice in ...
  • Press any key: read -n 1 -s -p "Press any key to continue..."
  • Password with confirmation: See example 3 pattern

Security Considerations

  • Never echo passwords: Use read -s and never display password values
  • Sanitize input: Remove or escape special characters before using in commands
  • Validate before execution: Never pass unvalidated input to eval, system(), or command execution
  • Limit input length: Prevent buffer overflow-style attacks by validating length
  • Use whitelists not blacklists: Define what's allowed, don't try to block what's not
  • Escape for SQL/shell: Properly escape input before using in SQL queries or shell commands
  • Avoid eval: Never use eval with user input - find safer alternatives
  • Use readonly for constants: Mark variables readonly after reading if they shouldn't change

Common Use Cases

Configuration File Processing

#!/bin/bash # Read config file with KEY=VALUE format while IFS='=' read -r key value; do # Skip comments and empty lines [[ $key =~ ^#.*$ || -z $key ]] && continue # Remove quotes from value if present value="${value%\"}" value="${value#\"}" # Declare variable declare "$key=$value" done < config.conf echo "Loaded config: SERVER=$SERVER PORT=$PORT"

Progress Indicator

#!/bin/bash total=100 for ((i=0; i<=total; i+=10)); do echo -ne "Progress: $i% \r" sleep 0.5 done echo -e "\nComplete! Press Enter to continue..." read

Multi-line Input (Here Document)

#!/bin/bash echo "Enter message (Ctrl+D when done):" message=$(cat) echo "You wrote:" echo "$message" # Alternative using read in loop echo "Enter lines (empty line to finish):" while read -r line; do [ -z "$line" ] && break lines+=("$line") done echo "You entered ${#lines[@]} lines"

Troubleshooting Common Issues

Issue: Read not waiting for input / immediately returns
Cause: Input is being redirected from somewhere (file, pipe, here-doc). Solution: Check for redirections. If in a loop reading from a file, you need to read from a different file descriptor: while ... done < file vs read -p "prompt: " var < /dev/tty
Issue: Backslashes disappearing from input
Cause: Not using -r flag, so backslashes are interpreted as escape characters. Solution: Always use read -r unless you specifically need backslash interpretation.
Issue: Last line of file not processed
Cause: File doesn't end with newline, so read returns 1 and loop exits. Solution: while read line || [ -n "$line" ]; do ... done
Issue: Variables empty after while loop
Cause: Loop runs in subshell (when using pipes: cat file | while read...). Changes to variables don't persist. Solution: Use redirection instead: while read line; do ... done < file
Issue: Read timing out immediately
Cause: Timeout value is 0 or very small. Solution: Check -t value. Remember -t 0 tests for input without waiting.
Issue: Prompt not showing or appearing in wrong place
Cause: Output buffering or redirections. Solution: Use -p which sends prompt to stderr, or add echo "" before prompts when using read -s.

Related Commands

  • readarray / mapfile: Read lines from stdin into an indexed array (more efficient than loop for entire files)
  • select: Generate simple menus automatically from a list
  • getopt / getopts: Parse command-line options and arguments
  • dialog / whiptail: Create sophisticated text-based user interfaces
  • zenity / kdialog: Create GUI dialog boxes from shell scripts
  • expect: Automate interactive applications by providing input programmatically
  • cat: Can read and concatenate files
  • head / tail: Read first or last lines of files
  • sed / awk: More powerful text processing for complex parsing

Bash Version Differences

Bash 4.0+:
  • Timeout exit status changed to >128
  • Better handling of -N option
  • Improved array handling with -a
Bash 4.4+:
  • Fixed issues with -t 0 behavior
  • Better signal handling during read
Check version: bash --version