Skip to content

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 -p "Enter your name: " name
echo "Hello, $name"
Enter your name: Nikhil
Hello, Nikhil

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

name="Nikhil Sharma"
echo "${#name}"         # 14

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)
count=0
count=$(( count + 1 ))
echo "$count"           # 1

# Shorthand
(( count++ ))
(( count += 5 ))

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)

  1. Try ${#PATH}. What does it tell you?
  2. Assign file="photo_2024-01-15.jpg". Use parameter expansion to extract just the date part (2024-01-15).
  3. 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

  1. 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.
  2. Using only parameter expansion (no sed or awk), write a function that converts a string to snake_case (spaces replaced with underscores, all lowercase).
  3. Research REPLY — the default variable used by read when no variable name is given. When is this useful?

Interview Questions

  1. 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.

  1. 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 }'.

  1. What does read -r do 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).


day03-part1-variables-quoting | day04-part1-control-flow