read is Bash’s built-in for getting input, either typed by a user or read from a file or pipe. It’s how install scripts ask questions, how scripts confirm dangerous actions, and how loops process files line by line.
Basic prompt
read -rp "Domain name: " domain
echo "Configuring $domain"
-p "text"shows a prompt.-rkeeps backslashes literal. Always use-runless you have a reason not to.
Without a variable name, the input goes into $REPLY.
Hidden input for passwords
read -rsp "Database password: " db_pass
echo # move to a new line, since -s also hides the Enter
-s (silent) doesn’t echo what’s typed. Don’t pass passwords as command-line arguments. Other users can see those in ps.
Defaults
read -rp "Port [3000]: " port
port="${port:-3000}"
Or pre-fill an editable default with readline:
read -e -i "/var/www" -rp "Web root: " webroot
Yes/no confirmation
read -rp "Delete all backups older than 30 days? [y/N] " answer
if [[ "$answer" =~ ^[Yy]([Ee][Ss])?$ ]]; then
find /var/backups -mtime +30 -delete
else
echo "Aborted."
fi
A capital letter in [y/N] signals the default. Treating anything other than an explicit yes as “no” is the safe choice for destructive actions.
Read a single key without needing Enter:
read -n 1 -rp "Continue? (y/n) " key; echo
Timeouts
if read -t 10 -rp "Reboot now? [y/N] " ans; then
[[ "$ans" == [Yy] ]] && reboot
else
echo; echo "No answer, skipping."
fi
read -t returns a non-zero status when it times out. That’s useful for scripts that sometimes run unattended.
Several values at once
read -rp "First and last name: " first last
Words are split on $IFS (spaces, tabs, newlines). The last variable gets everything left over.
Split on a different delimiter:
IFS=: read -r user _ uid gid _ home shell <<< "$(getent passwd www-data)"
echo "$user has UID $uid and home $home"
IFS=, read -r name email role <<< "Alice,[email protected],admin"
<<< is a “here string”: it feeds a string as input.
Into an array:
read -ra parts <<< "web1 web2 web3"
echo "${parts[1]}" # web2
Reading files line by line
while IFS= read -r line; do
echo "→ $line"
done < domains.txt
IFS=preserves leading and trailing spaces.-rpreserves backslashes.- The
< fileredirect at the end keeps the loop in the current shell, so variables set inside it survive.
If the last line lacks a trailing newline, it gets skipped. Handle that with:
while IFS= read -r line || [[ -n "$line" ]]; do …; done < file
Skip comments and blank lines:
while IFS= read -r line; do
[[ -z "$line" || "$line" == \#* ]] && continue
…
done < config.txt
More loop patterns are in Bash for loops.
Scripts that are piped or run non-interactively
When a script runs from curl … | bash, cron or CI, stdin isn’t a keyboard:
- In
curl | bash,readwould consume the script itself. Read from the terminal explicitly:read -rp "…" ans < /dev/tty. - In cron and CI there’s no terminal at all. Give every prompt a non-interactive way out: environment variables, flags like
--yes, or a check with[[ -t 0 ]].
if [[ -t 0 ]]; then
read -rp "Domain: " DOMAIN
fi
: "${DOMAIN:?set DOMAIN or run interactively}"
Input validation
Check the input before using it, especially when it ends up in commands or config files:
until [[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )); do
read -rp "Port (1-65535): " port
done
Quick reference
| Task | Command |
|---|---|
| Prompt | read -rp "Q: " var |
| Hidden | read -rsp "Pass: " var; echo |
| Timeout | read -t 10 -rp "…" var |
| Single key | read -n 1 -rp "…" key |
| Pre-filled default | read -e -i default -rp "…" var |
| Split fields | IFS=, read -r a b c <<< "$str" |
| Into array | read -ra arr <<< "$str" |
| File line by line | while IFS= read -r l; do …; done < f |
| From terminal in curl | bash | read … < /dev/tty |
Related: Bash variables and Bash functions.