Linux Basic — Case Conversion & Splitting
Case Conversion & Splitting
Two of the most common string chores: change case, and split a string on a delimiter. Bash 4+ handles both in pure parameter expansion plus the read builtin — no fork, no external tools.
Upper / Lower / Toggle
| Form | Effect |
|---|---|
${var^^} |
every char to UPPER |
${var,,} |
every char to lower |
${var^} |
first char to UPPER |
${var,} |
first char to lower |
${var^^[abc]} |
uppercase only matches of pattern |
${var,,[ABC]} |
lowercase only matches |
name="alice"
echo "${name^^}" # ALICE
echo "${name^}" # Alice
shout="HELLO WORLD"
echo "${shout,,}" # hello world
echo "${shout,}" # hELLO WORLD
Targeted case change:
s="hello world"
echo "${s^^[w]}" # hello World (only w → W)
echo "${s,,[H]}" # hello world (lowercase H if present)
Title Case (Not Built-In — Easy Compose)
There’s no ${var~} title-case operator, but the prefix-only ^ makes it a one-liner per word:
title() {
local out=""
for word in $1; do
out+="${word^} "
done
echo "${out% }"
}
title "the quick brown fox"
# The Quick Brown Fox
Case-Insensitive Comparison
The cleanest pattern is “normalise both sides”:
a="Hello"
b="hello"
[[ "${a,,}" == "${b,,}" ]] && echo equal # equal
Or use Bash’s shopt -s nocasematch for the rest of the block:
shopt -s nocasematch
[[ "Hello" == "hello" ]] && echo equal
shopt -u nocasematch
The ,,/^^ approach is usually safer — no global state to remember.
Splitting a String on a Delimiter — IFS=… read -ra
line="alice,42,admin"
IFS=, read -ra parts <<< "$line"
echo "${parts[0]}" # alice
echo "${parts[1]}" # 42
echo "${parts[2]}" # admin
echo "${#parts[@]}" # 3
Anatomy:
IFS=,— set the input field separator for this command only (no save/restore needed)read -ra parts— read fields into arrayparts<<< "$line"— feed the variable as stdin (here-string)
Multiple delimiters at once? Put them all in IFS:
line="a:b/c d"
IFS=':/ ' read -ra parts <<< "$line"
printf '[%s]\n' "${parts[@]}"
# [a]
# [b]
# [c]
# [d]
Bash treats IFS as a set of separator chars, not a multi-char string. So IFS='->' means “split on - OR >”, not on the literal ->.
For multi-char delimiters, use a different technique:
line="apple<->banana<->cherry"
arr=()
while [[ "$line" == *"<->"* ]]; do
arr+=("${line%%<->*}") # everything before the first delimiter
line="${line#*<->}" # everything after it
done
arr+=("$line")
printf '[%s]\n' "${arr[@]}"
# [apple]
# [banana]
# [cherry]
Joining an Array With a Delimiter
arr=(alice bob carol)
# Using IFS for one expansion
(IFS=,; echo "${arr[*]}")
# alice,bob,carol
"${arr[*]}" joins elements using the first char of IFS. Wrapping the assignment in () runs it in a subshell so IFS isn’t polluted.
A reusable function:
join_by() {
local sep=$1
shift
local IFS=$sep
echo "$*"
}
join_by , alice bob carol
# alice,bob,carol
join_by ' | ' first second third
# first | second | third (multi-char OK because $* uses sep AS-IS, not as a char set)
Wait — that last one really does work! When joining via "$*", IFS is used as a literal separator string, not as a char set. Splitting and joining have asymmetric IFS semantics. Useful to know.
IFS Scope — Always Local
Setting IFS permanently is a great way to break unrelated code. Two safe idioms:
# 1. Per-command (the `IFS=… cmd` form)
IFS=, read -ra parts <<< "$csv"
# 2. Subshell isolation
(IFS=,; echo "${arr[*]}")
# 3. Save / restore
old_ifs=$IFS
IFS=,
# ... use $IFS ...
IFS=$old_ifs
The first form (per-command) is the cleanest — IFS exists only for that one read.
Reading Stdin into an Array (One Line)
Use mapfile for full files (covered in Reading Files). For a single delimited line that’s already a string:
csv="apple,banana,cherry"
IFS=, read -ra fruits <<< "$csv"
echo "${fruits[1]}"
# banana
For null-delimited records (the safest for filenames):
mapfile -d '' -t files < <(find . -type f -print0)
echo "${#files[@]}"
-d '' uses a NUL byte as the delimiter — impossible for any filename to contain.
A Real-World Example — Parse key=value Config
config=(
"host=example.com"
"port=8080"
"debug=true"
)
declare -A cfg
for entry in "${config[@]}"; do
IFS='=' read -r key value <<< "$entry"
cfg["$key"]="$value"
done
echo "${cfg[host]}:${cfg[port]}"
# example.com:8080
Native parsing, no cut, no awk.
Common Mistakes
| ❌ Wrong | ✅ Right | Issue |
|---|---|---|
IFS=, ; read -a parts <<< "$line" |
IFS=, read -ra parts <<< "$line" |
first form leaves IFS polluted for the rest of the script |
IFS='->' for multi-char split |
use ${line%%->*} loop |
IFS is a char-set, not a string |
echo "${arr[*]}" for join with , |
(IFS=', '; echo "${arr[*]}") and remember it’s joined by IFS as-is via $* |
order of operations matters |
${var^^A} to upper-case only a |
the pattern is matched against the target chars, so use ${var^^[aA]} for both |
case matching is literal |
tr a-z A-Z in tight loop |
"${var^^}" |
one fork per call |
Next → Practice Exercises