Building scripts that communicate two-way with the user: mastering the read command with the -p, -s, -t, -n, -a flags, building interactive menus with select, and input validation patterns that are immune to empty input, wrong formats, and password confirmation.

In episode 5 we covered command line arguments & special parameters — how a script receives data through $1, $2, $@, up to $# — so in this episode we open the second communication door: direct input from the user while the script is running.
Command line arguments fit data that's already known before the script runs. But what if the data is only known while the script runs — for example a user name, a menu choice, or a password? Demanding all of that through arguments would make your command line long and rigid. The answer is the read command, which makes the script pause briefly, ask a question, and store the answer in a variable. This is what separates a mute script that only receives commands from an interactive script that can "hold a dialogue" with its user.
read Command: Listening to What the User TypesThe read command reads one line of input from the keyboard (or from a file/pipe) and stores it in a variable. The simplest example:
#!/usr/bin/env bash
read nama
echo "Halo, $nama!"./read_sederhana.shArman
Halo, Arman!There's no prompt at all — the cursor just blinks and waits. A beginner user will be confused: "what am I supposed to type?". That's why we need the -p flag: to tell the user what's being asked before they type.
read -p — Displaying a Prompt#!/usr/bin/env bash
read -p "Siapa nama kamu? " nama
echo "Senang bertemu, $nama!"Siapa nama kamu? Arman
Senang bertemu, Arman!-p displays the prompt text before reading input. Notice the space at the end of the prompt (? ) — without it, what the user types will "stick" directly to the end of the question mark. Small details like this make a script feel professional.
read -s — Silent Mode for PasswordsWhen a user types a password, we don't want the characters visible on screen (people behind could peek). The -s flag hides the input — but it's still stored in the variable:
#!/usr/bin/env bash
read -s -p "Masukkan password: " password
echo
echo "Password diterima (${#password} karakter)."Masukkan password: ••••••••
Password diterima (8 karakter).There are two important details here. First, -s doesn't display input — the cursor keeps blinking, and no keystroke appears. Second, note the empty echo on the line after read: because read -s doesn't produce a new line after the user presses Enter (similar to echo -n), without the extra echo, the next prompt would appear on the same line as the hidden input. That empty line "closes" the line.
Important
read -s hides the display, not secures the data. The password still sits in a variable (visible to other processes that can read memory) and can leak into shell history if you write it as an argument. read -s is not a substitute for proper secret management — it only prevents shoulder surfing. For real secrets, use tools like Vault, which we'll cover in another series.
read -t — Timeout: Don't Let a Script Wait ForeverIn automation (cron jobs, CI/CD), a script that waits for input forever is a disaster — the pipeline hangs indefinitely. The -t flag limits the wait time in seconds:
#!/usr/bin/env bash
if read -t 5 -p "Konfirmasi deploy (y/n): " jawaban; then
echo "Jawaban: $jawaban"
else
echo
echo "Tidak ada jawaban dalam 5 detik — membatalkan deploy."
exit 1
fiWhat's interesting: read itself returns an exit status. If the user answers before the timeout, read succeeds (exit status 0); if the timeout runs out, read fails (non-zero exit status). That's why we can directly use if read ... as the condition — elegantly leveraging the $? lesson from episode 5. This pattern is ideal for automation scripts that must cancel themselves if no human is operating them.
read -n — Reading a Number of Characters (Single Key)Sometimes we only need one character — like a "y" or "n" answer. The -n 1 flag makes read stop after one character, without waiting for Enter:
#!/usr/bin/env bash
read -n 1 -p "Hapus cache? (y/N) " jawaban
echo
case "$jawaban" in
y|Y) echo "Menghapus cache...";;
*) echo "Dibatalkan.";;
esacHapus cache? (y/N) y
Menghapus cache...Note the (y/N) pattern with a capital N — a Unix convention meaning "the default is No". The user just presses one key, no Enter needed. It feels light and fast, exactly like how installer applications interact with users.
read -a — Reading Into an ArrayThe -a flag reads the entire input line and splits it into array elements by spaces (IFS). Very useful for accepting a list of values in one prompt:
#!/usr/bin/env bash
read -a packages -p "Daftar paket (pisahkan spasi): "
for pkg in "${packages[@]}"; do
echo " - $pkg"
doneDaftar paket (pisahkan spasi): nginx redis postgres
- nginx
- redis
- postgresWe'll cover arrays in depth in the indexed & associative arrays episode. For now, just understand that read -a nama_array is a short way to turn one input line into a list of loopable elements.
read Flags Table| Flag | Function | When to use |
|---|---|---|
-p "text" | Display a prompt before reading | Almost always — the user must know what's being asked |
-s | Don't display input (silent mode) | Passwords, tokens, sensitive input |
-t N | Timeout after N seconds; read fails if it expires | Automation scripts that must cancel themselves |
-n N | Stop after N characters (no Enter) | Single-key y/n confirmation |
-a arr | Store input as an array (split on spaces) | Accepting a list of values in one prompt |
-r | Don't treat \ as an escape | Almost always — prevents backslashes from disappearing |
Tip
Get used to always writing read -r alongside the other flags (e.g. read -rp "Nama: " nama). Without -r, bash treats backslash as an escape character — so a Windows path like C:\Users\Arman will lose \U, \A. Rule of thumb: if you don't know why you need the -r flag, you still need -r.
The first law of interactive programming: input from users can't be trusted. A user can press Enter without typing anything, type a number when text was requested, or type a wrong format. A good script validates, and if the input is bad, asks again — rather than continuing with wrong data.
The most basic pattern: loop until the user gives a non-empty answer. The key is the loop — validating once isn't enough, because a user can give bad input many times.
#!/usr/bin/env bash
while [ -z "$nama" ]; do
read -rp "Nama project (wajib): " nama
if [ -z "$nama" ]; then
echo "Nama tidak boleh kosong. Coba lagi."
fi
done
echo "Project: $nama"The loop keeps running as long as $nama is empty. -z "$nama" means "true if the string is empty" — we'll study this test operator in more depth in episode 9. For now, understand the pattern: check → if wrong, tell the user → repeat. This is the universal validation cycle.
Empty validation only filters out missing input. To ensure the shape, use a regex pattern — bash has the =~ operator, which we briefly introduce here:
#!/usr/bin/env bash
while ! [[ "$npm" =~ ^[0-9]{9}$ ]]; do
read -rp "NPM (9 digit angka): " npm
[[ "$npm" =~ ^[0-9]{9}$ ]] || echo "Format salah — harus 9 digit angka."
done
echo "NPM valid: $npm"The pattern ^[0-9]{9}$ reads: start from the beginning, nine digits, then end — meaning the whole string must be exactly 9 digits, no more no less. In episode 9 we'll dissect [[ ]] and regex in more detail. What matters now: format validation is the second line of defense after the empty check.
For passwords, the standard practice is to enter twice and compare — preventing typos that are invisible because the input is hidden. Combine it with -s and a loop:
#!/usr/bin/env bash
while true; do
read -s -p "Buat password: " pw1
echo
read -s -p "Ulangi password: " pw2
echo
if [ -z "$pw1" ]; then
echo "Password tidak boleh kosong."
elif [ "$pw1" != "$pw2" ]; then
echo "Password tidak cocok — coba lagi."
else
break
fi
done
echo "Password berhasil disimpan."Why double confirmation? Because on a silent screen, typos are invisible — a user can be "sure" they typed correctly when they didn't. The second confirmation is a cheap safety net that saves you from big problems later (for example, a locked account because of a wrong password at deploy time).
selectBuilding a menu with read + case (like in the -n 1 example above) is possible, but bash provides a more convenient tool: the select loop. It automatically displays a numbered menu, accepts a choice, and stores it in a variable. Perfect for scripts offering a limited set of choices — exactly like a self-service kiosk menu.
#!/usr/bin/env bash
PS3="Pilih opsi (1-3): "
select opsi in "Cek status" "Tampilkan log" "Keluar"; do
case "$opsi" in
"Cek status") echo "→ Menjalankan status check...";;
"Tampilkan log") echo "→ Menampilkan log terakhir...";;
"Keluar") echo "Sampai jumpa!"; break;;
*) echo "Opsi tidak valid.";;
esac
done1) Cek status
2) Tampilkan log
3) Keluar
Pilih opsi (1-3): 2
→ Menampilkan log terakhir...
Pilih opsi (1-3): 3
Sampai jumpa!Two things to note:
PS3 is select's special prompt. Without setting PS3, the default prompt is #? , which confuses users. Rule of thumb: always set PS3 — this is the most common question in select scripts.select loop keeps showing the menu again after every choice. To exit, we need break (or exit). Without break, the user is stuck in the menu forever.Warning
Categorically reject invalid menu input. select accepts a number choice, not text — if the user types a letter, $opsi becomes empty and case falls into the * branch. Always make sure there's a *) branch as a safety net, exactly like the example above. A menu without a * branch will stay "silent" with no explanation when the user types a number outside the range — a confusing behavior.
Let's combine everything into one realistic script: a maintenance menu that accepts a password with confirmation, validates input, and offers different actions:
#!/usr/bin/env bash
set -euo pipefail
# 1. Password confirmation (twice, matching, non-empty)
while true; do
read -s -p "Buat password maintenance: " pw1; echo
read -s -p "Ulangi password: " pw2; echo
if [ -z "$pw1" ]; then
echo "Password tidak boleh kosong."
elif [ "$pw1" != "$pw2" ]; then
echo "Tidak cocok — coba lagi."
else
echo "Password diterima."
break
fi
done
# 2. Ask for the target name with validation
while [ -z "$server" ]; do
read -rp "Nama server target: " server
done
# 3. Action menu
PS3="Aksi untuk $server: "
select aksi in "Status" "Restart" "Keluar"; do
case "$aksi" in
"Status") echo "→ Mengecek status $server...";;
"Restart") echo "→ Me-restart $server...";;
"Keluar") echo "Selesai."; break;;
*) echo "Pilihan tidak valid.";;
esac
doneLet's break down the flow — a real example of how the three patterns work together:
| Stage | Pattern | Reason |
|---|---|---|
| 1 | Password confirmation (read -s twice + compare) | Prevents typos on hidden input |
| 2 | Non-empty validation (while [ -z ] + re-prompt) | The server name must not be empty |
| 3 | select menu + PS3 + * branch + break | Safe action navigation that can exit |
This script can now be run in the terminal with no arguments — all data is obtained through dialog. Compare it with the episode 5 script, which got everything through arguments. Both are valid — which one you use depends on context: arguments for data known in advance and automatable; read/select for data only a human can provide while interacting.
read inside a pipe → variable lostThis is the most classic and confusing trap:
echo "Arman" | read nama
echo "Halo, $nama" # KOSONG!Why does it happen? Each side of a | pipeline runs in a subshell — a separate process. read reads the input in that subshell, and its variable dies with the subshell when the pipeline finishes. The nama variable in the main shell is never touched. The solution: read directly from a file (< file), use process substitution (< <(cmd)), or — if possible — read with a while loop in the same construct. As long as you write ... | while read, remember: the variables inside that loop won't escape it.
In cron jobs or CI/CD, no human is waiting at the terminal. If a script calls read without -t, and stdin doesn't provide data, the script will hang forever and the pipeline stops entirely. Always ask: will this script run without a human? If so, use read -t N and prepare a default path (like the read -t example above, which cancels the deploy).
PS3 not set → #? promptA select that doesn't set PS3 displays the mysterious #? . The user has no idea what to type. Set PS3 every time you use select.
echo after read -sread -s doesn't print a new line after Enter. The next prompt will stick to the same line as the hidden input — looking like the script "hung". Add an echo after it.
"..."[ "$pw1" != "$pw2" ] with quotes is correct. Without quotes — [ $pw1 != $pw2 ] — a password containing spaces will split into several words and error with "unary operator expected". Always quote. This detail is covered thoroughly in episode 9.
In episode 6, we transformed scripts from mute tools into tools that can hold a dialogue: the read command with the -p (prompt), -s (silent for passwords), -t (timeout), -n (character count), and -a (array) flags; input validation patterns that are immune to empty and wrongly formatted input through the re-prompt cycle; the double password confirmation pattern; and interactive menus with select and PS3.
The takeaways:
read -rp for a clear prompt, and always -r to prevent backslashes from disappearing.read -s for passwords — but remember, it hides the display, not secures the data.read -t is mandatory in automation scripts so they don't hang.select + PS3 + * branch + break is the recipe for a safe menu.| while read — variables won't escape the subshell.Now that you can accept data from the command line and from interactive users, there's one more data domain that's often forgotten: the files on the filesystem. How do you grab a list of files matching a certain pattern — *.log, report_2026*, all .png files? In episode 7, we'll cover Globbing & Pathname Expansion — the *, ?, [...] wildcards, brace expansion, globstar **, plus why we must always use globs and never parse ls output. See you there!