User Input & Expansion¶
Scripts that accept arguments and respond to user input are the difference between a one-off hack and a reusable tool that others can actually run.
Learning Objectives¶
- Read user input at runtime with
read - Use parameter expansion to manipulate variable values
- Perform arithmetic in bash
- Use command substitution in real scripts
- Validate input and provide sensible defaults
Reading User Input with read¶
read -s -p "Enter password: " password # -s: silent (no echo)
echo "" # newline after silent input
echo "Password length: ${#password}"
read -t 10 -p "Answer within 10 seconds: " answer # -t: timeout
read -r line # -r: no backslash escaping
Always use -r with read
Without -r, backslashes in input are treated as escape characters. With -r, input is read literally. Unless you specifically want backslash processing, always use read -r.
Parameter Expansion¶
Parameter expansion lets you manipulate variable values inline without spawning a subprocess.
String Length¶
Substrings¶
str="hello world"
echo "${str:0:5}" # hello (start at 0, length 5)
echo "${str:6}" # world (start at 6, to end)
echo "${str: -5}" # world (last 5 characters — note the space)
Prefix and Suffix Removal¶
filename="backup_2024-01-15.tar.gz"
echo "${filename#backup_}" # 2024-01-15.tar.gz (remove shortest prefix match)
echo "${filename##*.}" # gz (remove longest prefix match)
echo "${filename%.*}" # backup_2024-01-15.tar (remove shortest suffix)
echo "${filename%%.*}" # backup_2024-01-15 (remove longest suffix)
This is useful for extracting extensions or basenames:
file="report.pdf"
echo "${file%.*}" # report (filename without extension)
echo "${file##*.}" # pdf (extension only)
Case Conversion (bash 4+)¶
name="hello world"
echo "${name^^}" # HELLO WORLD (uppercase)
echo "${name,,}" # hello world (lowercase)
echo "${name^}" # Hello world (capitalize first character)
Find and Replace¶
str="hello hello hello"
echo "${str/hello/world}" # world hello hello (replace first)
echo "${str//hello/world}" # world world world (replace all)
Arithmetic¶
$(( )) — Arithmetic Expansion¶
x=10
y=3
echo $(( x + y )) # 13
echo $(( x * y )) # 30
echo $(( x / y )) # 3 (integer division — no decimals)
echo $(( x % y )) # 1 (remainder)
echo $(( x ** 2 )) # 100 (exponentiation)
Bash arithmetic is integer only
echo $(( 10 / 3 )) gives 3, not 3.33. For floating-point arithmetic, use bc: echo "scale=2; 10/3" | bc gives 3.33.
let and expr (older alternatives)¶
let "x = 5 + 3" # older syntax, avoid in new scripts
result=$(expr 5 + 3) # very old, POSIX — avoid; use $(( ))
Command Substitution in Scripts¶
#!/usr/bin/env bash
set -euo pipefail
# Capture command output into variables
hostname=$(hostname)
ip_address=$(hostname -I | awk '{print $1}')
disk_usage=$(df -h / | awk 'NR==2 {print $5}')
today=$(date +%Y-%m-%d)
echo "Report for $hostname ($ip_address) — $today"
echo "Disk usage: $disk_usage"
Input Validation Pattern¶
#!/usr/bin/env bash
set -euo pipefail
read -rp "Enter a number between 1 and 10: " input
# Check it is a number
if ! [[ "$input" =~ ^[0-9]+$ ]]; then
echo "Error: '$input' is not a number" >&2
exit 1
fi
# Check range
if (( input < 1 || input > 10 )); then
echo "Error: $input is not between 1 and 10" >&2
exit 1
fi
echo "You entered: $input"
Common Mistakes¶
Spaces inside $(( ))
$(( x+y )) and $(( x + y )) both work. But $((x+y)) also works. The issue is with =: $(( count =5 )) is valid but confusing. Be consistent and use spaces for readability.
Using expr in new scripts
expr is a POSIX relic. Every guide using expr 5 + 3 is outdated. Use $(( 5 + 3 )) instead — it is faster, built into bash, and cleaner.
Practice Exercises¶
Warm-Up (run and observe)¶
- Try
${#PATH}. What does it tell you? - Assign
file="photo_2024-01-15.jpg". Use parameter expansion to extract just the date part (2024-01-15). - Run
echo $(( 2 ** 10 )). What is the result?
Main (write a short script)¶
Create ~/scripts/rename_preview.sh that shows what files would be renamed (dry run):
#!/usr/bin/env bash
set -euo pipefail
read -rp "Enter directory path: " dir
read -rp "Replace string: " old_str
read -rp "With string: " new_str
echo ""
echo "Preview of changes in: $dir"
for f in "$dir"/*"$old_str"*; do
[[ -e "$f" ]] || continue
base="${f##*/}"
new_name="${base//$old_str/$new_str}"
echo " $base -> $new_name"
done
Stretch¶
- Write a script that reads a sentence from the user and prints: the number of words, the number of characters, and the sentence reversed word by word.
- Using only parameter expansion (no
sedorawk), write a function that converts a string to snake_case (spaces replaced with underscores, all lowercase). - Research
REPLY— the default variable used byreadwhen no variable name is given. When is this useful?
Interview Questions¶
- What is the difference between
${var%.*}and${var%%.*}?
Show answer
Both remove a suffix matching .* (a dot followed by anything). % removes the shortest matching suffix, so ${file%.*} on archive.tar.gz gives archive.tar. %% removes the longest matching suffix, so ${file%%.*} gives archive.
- How do you perform floating-point arithmetic in bash?
Show answer
Bash arithmetic ($(( ))) is integer-only. For decimals, pipe an expression to bc: echo "scale=4; 22/7" | bc gives 3.1428. The scale setting controls decimal places. awk is another option: awk 'BEGIN { printf "%.4f\n", 22/7 }'.
- What does
read -rdo and why should you use it?
Show answer
-r disables backslash interpretation during input. Without it, typing C:\Users\name would have the backslashes treated as escape characters. With -r, the input is read literally. Always use read -r unless you specifically want backslash processing (which is rare).