Module 9 — Parameter Expansion and String Manipulation

Updated 3 September 2026

Module 9 — Parameter Expansion and String Manipulation. Trimming extensions, default values, extracting pieces of paths and versions — pure-bash string surgery that replaces a dozen external commands.

🧠 concept → 🧩 analogy → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (hidden)

Before you start. You need Modules 1–8 — especially ${var} braces and quoting (Module 2), glob patterns (Modules 2 and 5 — the trim operators reuse them), and functions (Module 8 — this module's tools live inside helpers). Two earlier previews get their full story here: ${var:-default} (Module 7) and ${0##*/} (Module 8). Tools: nothing new.

Part A — Defaults and guards: the :- family

A1. Four operators for "what if it's empty?"

Everything in this module happens inside the ${ } braces you met in Module 2 — the braces are not just a name boundary; they are a tiny workshop where the value can be transformed on the way out. The first family handles bash's oldest hazard, the silent empty expansion (Module 2 A1), with four one-character policies:

SpellingIf var is unset or empty…Production use
${var:-fallback}expand to fallback; var itself unchangedread-time defaults: "${TIMEOUT:-30}"
${var:=fallback}expand to fallback and assign it to varset-once defaults early in a script
${var:?message}print message to stderr and abort (non-zero)required variables: : "${API_KEY:?API_KEY is required}"
${var:+word}the reverse: expand to word only if var has a valueconditional flags: ${VERBOSE:+--verbose}

The :? guard usually rides on the do-nothing command : (a colon — it ignores its arguments and succeeds), giving the one-line required-variable check above: if API_KEY is missing the script stops with your message; if present, the line does nothing at all. Dropping the colon inside the braces (${var-fallback}) changes "unset or empty" to strictly "unset" — a distinction Module 2's unset-vs-empty section planted, now with a use.

Real-world analogy — the form with printed defaults. A well-designed order form has policies for blank fields, printed right on it: "leave blank for standard shipping" (:-), "blank = we fill in standard and it becomes your choice" (:=), "THIS FIELD IS REQUIRED — form rejected if blank" (:?), and the odd one, "check here ONLY IF the field above is filled" (:+). The clerk applies the policy without asking anyone.

Where the analogy stops working. A human clerk distinguishes "field left blank" from "field filled with whitespace" from "form has no such field." Bash's colon-forms lump unset and empty together — a variable set to "" triggers the same policy as one that never existed — and a value of a single space counts as filled. When those distinctions matter (they occasionally do in config handling), the colonless forms and manual -z checks are the precision tools.

🧪 Exercise 9.1 — four policies, felt

The last line aborts on purpose.

bash
unset region
echo "read-default: [${region:-eu-west-1}] — and region is still: [$region]"
echo "set-default:  [${region:=eu-west-1}] — and region is now:  [$region]"
count=0
echo "zero is a value: [${count:-5}]"
flag=1
echo "flag set: [${flag:+--verbose}]  flag unset: [${nope:+--verbose}]"
: "${MUST_EXIST:?is required}"
echo "you will not see this"
Expected result — click to reveal (contains a deliberate abort)
plain text
read-default: [eu-west-1] — and region is still: []
set-default:  [eu-west-1] — and region is now:  [eu-west-1]
zero is a value: [0]
flag set: [--verbose]  flag unset: []
bash: MUST_EXIST: is required

What to read out of it, line by line: :- supplied the value but left the jar empty (the probe brackets prove it); := supplied and filled the jar — the difference between the two is entirely in that side effect. Line 3 kills a common misreading: 0 is not "empty" — the default did not fire; :- is about missing values, not falsy ones. Line 4 shows :+'s party trick, building a flag out of a variable's mere presence. And the finale: the :? guard printed your message on stderr and — in a script — stops execution dead: the final echo never runs (from a file, the message also gains a line N: prefix). Typed at an interactive prompt, your shell itself survives the failure and the last echo will print — the abort kills scripts, not your terminal. One line, and a whole class of "ran half a deploy with a blank API key" incidents becomes impossible.

Interview questions — Part A

🎯 "How do I set a default value if the user just presses Enter?" — asked verbatim in LinuxTeck's FAQ (Module 7 answered the read half; here is the expansion half in full)

The direct answer: "${var:-default}" wherever the value is used, or : "${var:=default}" once near the top to settle it permanently. For read prompts: read -r ans; ans="${ans:-staging}".

Going deeper: lay out the full family as policies — :- read-time, := assign-time, :? required, :+ presence-flag — and the colon rule (with colon: unset or empty; without: strictly unset). The canonical production opener is a block of : "${VAR:?...}" and : "${VAR:=...}" lines that is the script's configuration documentation, self-enforcing.

The details that separate candidates: the ${VERBOSE:+--verbose} idiom for building optional command flags without an if-block; knowing := cannot assign to positional parameters (${1:=x} errors — they're read-only that way); and the subtle honesty that 0 and "0" and a lone space are all values, so the colon family never fires on them — "default" means absent, not falsy.

Part B — Trimming: # and %, the path surgeons

B1. Strip from the front, strip from the back

Four operators remove a matching piece from a value's edge, and they reuse the glob language you already know (*, ? — Module 2 B3, Module 5's case patterns):

${var#pattern} — strip the shortest match from the front. ${var##pattern} — the longest from the front. ${var%pattern} — shortest from the back. ${var%%pattern} — longest from the back. The memory hook everyone eventually adopts: on a US keyboard, # sits left of % on the number row the way the front of a string sits left of its back — # trims fronts, % trims backs; doubling the character means "be greedy."

Two of these four are so common they have names in other tools: ${path##*/} (drop everything up to the last slash) is basename; ${path%/*} (drop the last slash and after) is dirname. The expansions do in-process what those commands do in a child process — in a loop over ten thousand paths, that difference is measurable minutes.

Real-world analogy — the deli slicer's two settings. The value is a loaf; the pattern says what a "slice" looks like; #/% choose which end of the loaf meets the blade, and single-vs-double is the thickness dial: single = the thinnest slice that satisfies the pattern, double = keep slicing while the pattern still holds. ${file%%.*} is "cut from the back, greedily, everything from the first dot" — the loaf that remains is the bare name.

Where the analogy stops working. A slicer cuts what you see. These operators cut what the pattern matches, and glob patterns are greedy in non-obvious ways: with archive.tar.gz, %.* takes only .gz (shortest), %%.* takes .tar.gz (longest) — both reasonable, different answers to "remove the extension." Multi-dot names are where extension-stripping one-liners quietly disagree with their authors; decide which behavior you mean, then pick the operator, not the other way round.

🧪 Exercise 9.2 — surgery on paths and versions
bash
file="backups/db-prod.sql.gz"
echo "shortest from back:  ${file%.gz}"
echo "longest from back:   ${file%%.*}"
echo "shortest from front: ${file#*/}"
echo "longest from front:  ${file##*/}"
path="/opt/app/releases/v2.1.0"
echo "basename-style: ${path##*/}"
echo "dirname-style:  ${path%/*}"
ver="release-2.14.7"
echo "version number: ${ver#release-}"
Expected result — click to reveal
plain text
shortest from back:  backups/db-prod.sql
longest from back:   backups/db-prod
shortest from front: db-prod.sql.gz
longest from front:  db-prod.sql.gz
basename-style: v2.1.0
dirname-style:  /opt/app/releases
version number: 2.14.7

What to read out of it: lines 1–2 are the multi-dot lesson live — same intent ("remove the extension"), two different results; know which you meant. Lines 3–4 happen to match because this path has one slash — with /a/b/c.txt, #*/ yields b/c.txt while ##*/ yields c.txt; the difference reappears the moment paths deepen. The last three lines are the daily bread: pure-bash basename, dirname, and prefix-stripping — no child processes, no quoting adventures, no sed.

Now imagine this at 500 hosts. In one script, $(basename "$f") versus ${f##*/} is a style choice. In a log-processing loop touching 200,000 paths per run per host, each $( ) spawns a process (Module 12 explains exactly what that costs) — and expansions run at memory speed inside bash. Fleet log processors and provisioning loops that switched from command substitution to parameter expansion have cut run times from hours to minutes with no logic change. The habit: inside loops, reach for the expansion first; reserve basename/dirname for one-off calls and for their edge-case handling (trailing slashes, bare names) when paths are hostile.

Interview questions — Part B

🎯 "How do you get a filename without its extension in bash?" — a whole tutorial genre exists on this exact task (Java2Blog, LinuxHint, Sentry all cover it), though question lists rarely print it verbatim — the substance is the trim operators

The direct answer: "${file%.*}" — strip the shortest match of .anything from the back. For the extension itself: "${file##*.}". For the name without directory and without extension: two steps — name="${file##*/}"; name="${name%.*}".

Going deeper: the multi-dot decision — %.* on app.tar.gz leaves app.tar (usually what you want: one extension off), %%.* leaves app (everything after the first dot gone, which mangles version-dotted names like v2.1.0-notes.txt). And the no-dot edge: ${file%.*} on Makefile returns Makefile unchanged — the pattern must match to cut, which is usually the right failure mode.

The details that separate candidates: explaining # / % by mechanism (front/back, shortest/longest, glob patterns) rather than reciting one memorized incantation; the basename/dirname equivalences and their per-call process cost in loops; and one honest boundary — hidden files like .bashrc fool naive extension logic (${f##*.} yields bashrc), so hostile input deserves a guard.

Part C — Replace, measure, slice

C1. ${var/…}, ${#var}, ${var:off:len}, and case flips

The rest of the workshop, tour-style. Replacement: ${var/pattern/replacement} swaps the first match; double the slash — ${var//pattern/replacement} — for all matches; leave the replacement empty to delete (${csv//,/} strips every comma). The pattern is again a glob, not a regex. Length: ${#var} counts characters (recognize the shape: # before the name is length; after the brace with a pattern it's front-trim — position is everything). Substring: ${var:offset:length} slices by position, counting from 0 — ${host:0:6} is the first six characters. Case: ${var^^} uppercases, ${var,,} lowercases (bash 4+; the everyday normalizer for user-typed values before a case).

🧪 Exercise 9.3 — the rest of the toolbox
bash
host="web-01.prod.example.com"
echo "first dot only: ${host/./-}"
echo "every dot:      ${host//./-}"
echo "length:         ${#host}"
echo "first six:      ${host:0:6}"
csv="a,b,c"
echo "commas deleted: ${csv//,/}"
answer="YeS"
echo "normalized:     ${answer,,}"
Expected result — click to reveal
plain text
first dot only: web-01-prod.example.com
every dot:      web-01-prod-example-com
length:         23
first six:      web-01
commas deleted: abc
normalized:     yes

What to read out of it: single vs double slash is first-vs-all — the metrics-name conversion on line 2 (dots to dashes, wholesale) is a real daily task in monitoring pipelines. The length and slice lines read a value by position, which is honest for fixed-format data (the first six characters of a host naming convention) and brittle for anything humans type — prefer pattern-based trims when structure, not position, is what you know. The last line is the production idiom before every case "$answer" in yes) …: normalize once, match simply.

Interview questions — Part C

🎯 "How to get part of string variable with echo command only?" — asked verbatim at Edureka; PlacementPreparation asks "How do you check the length of a string in shell scripting?"

The direct answer: substring expansion — echo "${var:x:y}": from offset x (zero-based), take y characters. Length: "${#var}". Both are pure expansions — the "echo command only" in the question is a hint that no cut/awk is needed.

Going deeper: negative offsets count from the end but need a space — ${var: -3} is the last three characters (without the space, :- is Part A's default operator — a genuinely nasty collision worth naming aloud in an interview). Omitting length takes everything to the end: ${var:5}.

The details that separate candidates: the :--collision detail is the separator on this exact question — it proves you know both operators rather than one; and pairing position-slicing with its honest limits (byte/character subtleties with multibyte text, brittleness on human input) shows judgment, not just syntax.

🎯 "How do you perform string manipulation in Bash?" — asked verbatim at Zero To Mastery

The direct answer, as a toolbox tour: defaults ${v:-d}; trims ${v#p} ${v##p} ${v%p} ${v%%p} (front/back, shortest/longest, glob patterns); replace ${v/p/r} and ${v//p/r}; length ${#v}; slice ${v:off:len}; case ${v^^} ${v,,} — all in-process, no child commands.

Going deeper: organize by task, the way their answer does — extension handling (trims), sanitizing names for metrics/DNS (global replace), normalizing input (case flips), validation (length + Module 5's =~) — and state the boundary honestly: patterns here are globs; the moment you need real regex captures or multi-line edits, that is sed/awk territory (Module 11), and knowing where the line is is part of the answer.

The details that separate candidates: the performance story (expansion vs $(sed …) in loops — no fork, orders of magnitude in bulk); quoting discipline surviving inside the braces ("${v//p/r}" still gets quoted as a whole); and mentioning that replacement patterns can anchor — ${v/#p/r} start, ${v/%p/r} end — a corner even strong candidates rarely know.

Part D — Composing the workshop

D1. A real task, assembled: bulk-renaming safely

Every operator in this module earns its keep the day you must rename a directory of files. The task: turn report draft v2.txt-style names into clean report_draft_v2.txt — spaces to underscores. Watch four modules compose:

bash
cd ~/bash-course
mkdir -p renames && cd renames
touch "report draft v2.txt" "meeting notes.txt" clean.txt
for f in *.txt; do                       # glob loop: split-safe (Module 6)
    new="${f// /_}"                      # this module: every space → underscore
    [ "$f" = "$new" ] && continue        # already clean? skip (Modules 5+6)
    echo "would rename: [$f] -> [$new]"  # probe first, act later (Module 2)
done

The echo-first shape is deliberate and professional: destructive loops get a dry-run pass — you read the plan, then swap echo "would rename: …" for mv -- "$f" "$new" (the -- says "no options follow", armoring mv against names that begin with -; a Module 7 idea protecting a Module 9 rename).

🧪 Exercise 9.4 — the dry run
bash
cd ~/bash-course/renames
for f in *.txt; do
    new="${f// /_}"
    [ "$f" = "$new" ] && continue
    echo "would rename: [$f] -> [$new]"
done
Expected result — click to reveal
plain text
would rename: [meeting notes.txt] -> [meeting_notes.txt]
would rename: [report draft v2.txt] -> [report_draft_v2.txt]

What to read out of it: two renames planned, one file rightly skipped (clean.txt — the continue guard), names shown in probe brackets so any surprise is visible before anything moves. Glob order is alphabetical, hence meeting before report. When the plan reads right, promote the echo to mv -- "$f" "$new" and run once more. This tiny script is the module in miniature: expansion did the string work, and everything around it — glob list, guard, quoting, dry-run — is the earlier track keeping the operation safe.

D2. Which string tool? — the decision

Note: the diagram below is a Mermaid code block. Notion does not render it automatically — click the block and switch it from "Code" to "Preview" (or "Split") to see the flowchart.
Diagram source
flowchart TD
    A["I need to transform<br>a string in bash"] --> B{"What kind<br>of job?"}
    B -->|"missing-value<br>policy"| C["${var:-} family<br>:- := :? :+"]
    B -->|"cut from an edge<br>(path, extension, prefix)"| D["trims: # ## % %%<br>glob patterns"]
    B -->|"swap or delete<br>occurrences"| E["${var/p/r} first<br>${var//p/r} all"]
    B -->|"measure / slice<br>by position"| F["${#var} and<br>${var:off:len}"]
    B -->|"normalize case"| G["${var,,} ${var^^}"]
    B -->|"regex captures,<br>multi-line, files"| H["sed / awk<br>(Module 11)"]
    D --> I{"inside a<br>hot loop?"}
    I -->|"yes"| J["expansion — no fork"]
    I -->|"one-off, hostile paths"| K["basename / dirname<br>fine too"]

Interview questions — Part D

🎯 "When do you use parameter expansion versus sed or awk?" — the judgment question behind every string-manipulation screen; posed in follow-up form rather than published lists

The direct answer: expansion for one value in a variable — defaults, edge-trims, glob replace, slices, case — in-process and fork-free. sed/awk for streams and files: many lines, regex with captures and classes, field-based work.

Going deeper: the two boundaries that decide it — pattern power (expansion speaks glob only; the moment you need [0-9]+ captures or backreferences, that's regex country) and data location (a variable vs a stream; $(sed …ing a variable…) round-trips through a child process for work bash could do in place). The hot-loop rule from Part B's fleet callout: expansions in loops, external tools on streams.

The details that separate candidates: naming the collision cases where both work and one is clearly better (strip an extension: expansion; edit a config file in place: sed) rather than treating the tools as rivals; and the team-code angle — a chain of three cryptic expansions is sometimes worth trading for one readable awk even at fork cost, because maintenance is also a cost. Judgment, stated as trade-offs, is the interview answer.

Part E — Production

E1. 🏭 Production practices

These are the practices in production — how the things this module taught are actually done on real systems:

Scripts open with a self-enforcing config block. A run of : "${VAR:?message}" and : "${VAR:=default}" lines documents every tunable and secret the script accepts — readable as documentation, fatal when violated.

Path surgery is expansion first. ${f##*/}, ${f%/*}, ${f%.*} are the house spellings inside loops; basename/dirname appear for one-offs and hostile-path edge cases, with the choice deliberate.

Extension logic states its multi-dot policy. %.* vs %%.* on *.tar.gz-shaped names is decided and commented, not discovered in production.

Destructive string-driven loops ship with a dry-run. The echo-the-plan pass is kept in the script behind a flag (${DRY_RUN:+echo} is the one-line trick: prefix the action with a variable that expands to echo only when DRY_RUN is set).

User input is normalized before matching. "${answer,,}" before the case; hostnames and metric names sanitized with global replace (${name//./-}) at the boundary where they enter the system.

The glob/regex boundary is respected. Expansions do glob-shaped work; the first regex capture or multi-line edit goes to sed/awk — no heroic expansion chains that the next engineer cannot read.

E2. Production-practice table

SymptomWhat is really happeningWhat to runThe fix
Deploy ran with a blank API key / empty target and did half a jobNo required-variable guard — empty expansion sailed through (Module 2's silence)printf '[%s]\n' "$API_KEY"[]Top-of-script : "${API_KEY:?API_KEY is required}" per required variable
A default "didn't work" — variable still empty later in the script:- was used (read-time only, no assignment) where := was meantProbe the variable after the line that "set" it: "${VAR:=default}" when the value should persist
Default fired even though the variable was set… to 0 — or refused to fire on a lone spaceColon-family semantics: unset-or-empty, and whitespace is a valuev=0; echo "${v:-5}" (prints 0) and v=" "; echo "[${v:-x}]" (prints the space)Colon forms fire on absent, not falsy; trim/validate first when that distinction matters
"Remove the extension" mangled v2.1.0-notes.txt into v2%%.* (longest-from-back) applied where %.* (shortest) was meantTest both operators on a multi-dot sample nameDecide the multi-dot policy; comment it at the use site
Substring produced a default value instead of the last N characters${var:-3} — the missing space made it the default operator, not a negative offsetCompare ${var:-3} and ${var: -3} on a sample${var: -3} — space before the minus, always; comment it, it will look like a typo
Loop over thousands of paths is mysteriously slow$(basename …)/$(sed …) forking a process per iterationtime the loop; count $( ) per iterationReplace with ${f##*/}-style expansions; keep externals out of hot loops

E3. 🎓 Capstone — four tickets from the queue

Work each ticket yourself before opening the answer. Everything needed was taught in Modules 1–9.
🎓 Ticket 1 — "Rollout script takes IMAGE_TAG from the environment. Someone ran it on a box where that variable didn't exist; it deployed the image myapp: — colon, nothing — and the registry treated it as latest. Production got last night's build."

Diagnosis. The unguarded empty expansion composing a valid-looking wrong value: myapp:$IMAGE_TAG with the variable absent became myapp: — not an error, a different artifact. Worse than a crash: the failure wore a working costume all the way to production.

Work the steps: reproduce with the variable unset and probe the composed string: printf '[%s]\n' "myapp:$IMAGE_TAG"[myapp:].

Fix and prevention: the one-liner this module exists for: : "${IMAGE_TAG:?IMAGE_TAG is required (e.g. 2026-09-03-a1b2c3)}" at the top — the message even teaches the format. Optionally stack a shape guard ([[ "$IMAGE_TAG" =~ ^[0-9a-zA-Z._-]+$ ]], Module 5). Fleet rule: any variable that becomes part of an artifact name, path, or URL gets :? — absence must be louder than wrongness.

🎓 Ticket 2 — "A cleanup script archives logs as ${name%%.*}.tar.gz. Since the app team started shipping logs named like api.2026-09-03.log, the archives keep overwriting each other — everything becomes api.tar.gz."

Diagnosis. B1's multi-dot disagreement, weaponized by a naming change: %%.* eats from the first dot, so api.2026-09-03.logapi — every dated file collapses to the same archive name, and >-style overwrite semantics did the rest. The operator was defensible when names had one dot; the data changed, the greedy trim did not.

Work the steps: run both trims on the new name shape: ${name%.*}api.2026-09-03 (distinct per day) vs ${name%%.*}api (collision). One echo each settles it.

Fix and prevention: shortest-match %.* — and state the policy in a comment ("strip ONE extension; dated stems must survive"). Add a collision guard before archiving: [ -e "$out" ] && { echo "refusing to overwrite $out" >&2; exit 1; }. The reusable lesson: greedy trims encode an assumption about dot-count; assumptions about other teams' filenames need guards, not faith.

🎓 Ticket 3 — "Monitoring rejects half our metric names. We build them as cpu.${HOSTNAME}.load — and hosts are named like web-01.prod.example.com, so the metric arrives as cpu.web-01.prod.example.com.load — five dots where the schema allows three fields."

Diagnosis. Unsanitized data crossing a boundary with its own grammar: the hostname's dots are structure to the metrics system, but content to you. Nothing is broken in bash — the composition simply forwarded hostile characters into a namespace that assigns them meaning (the same disease as Module 2's *-in-data, new costume).

Work the steps: probe the composed name; count dots. Then apply the fix candidate and re-count: h="${HOSTNAME//./-}"web-01-prod-example-com.

Fix and prevention: sanitize at the boundary, once, in a named helper — metric_safe() { local s="${1//./-}"; printf '%s' "${s// /_}"; } (Module 8's function discipline meeting this module's replaces) — and route every externally-sourced fragment through it. Boundary sanitizers beat scattered fixes: the next hostile character (a space, an underscore policy) changes one function, not forty call sites.

🎓 Ticket 4 — "Code review flagged our new log rotator: 'this loop calls basename and two seds per file; on the ingest host that's 300k files.' The author asks — does it actually matter?"

Diagnosis. The hot-loop fork tax (B's fleet callout): three child processes per file × 300k files ≈ a million forks per run, each costing far more than the string work itself. On the laptop sample of 50 files, invisible; on the ingest host, the difference between a minute and the better part of an hour of pure overhead.

Work the steps: measure, don't argue: build a 10k-file test dir, time the loop as written, then the expansion version — name="${f##*/}"; stem="${name%.*}"; clean="${stem//[: ]/_}" — and compare. The ratio settles the review comment with data.

Fix and prevention: expansions inside the loop; if an external tool is genuinely needed (a real regex), restructure so it runs once over the stream (find … | sed …, Module 11's shape) instead of once per file. Codify the review heuristic: $( ) inside a hot loop is a performance smell — justify it or hoist it.

E4. Documentation reference

TopicAuthoritative referenceWhat you'll find there
Every operator in this modulebash(1) — man7.org, "Parameter Expansion" sectionThe complete catalogue: defaults, trims, replace, length, slices, case, anchors
The command-flavored twinsbasename(1) — man7.org · dirname(1) — man7.orgThe external equivalents of ${f##*/} and ${f%/*}, with their edge-case handling
The patterns the trims speakBash manual — Filename Expansion (Pattern Matching)The glob grammar reused by #/% trims and ${var/…} replacements

E5. Self-assessment

Answer from memory, out loud or on paper. Every answer is in this module.

  1. ${var:-x} vs ${var:=x} — what does each expand to, and which one changes the variable?
  2. Write the one-line required-variable guard, explain the role of the leading :, and say what happens on violation.
  3. When does ${count:-5} not fire, even though count "looks empty" to a human? Give two examples.
  4. What does ${var:+word} do, and what is its flag-building idiom?
  5. The four trim operators: which character means which end, what does doubling mean, and what language do the patterns speak?
  6. Spell basename and dirname as pure expansions.
  7. On app.tar.gz, what do ${f%.*} and ${f%%.*} each produce — and why must a script choose knowingly?
  8. ${v/p/r} vs ${v//p/r}; and how do you delete every occurrence of a character?
  9. Two meanings of # inside braces — how does position distinguish length from trim?
  10. Why does ${var:-3} not take the last three characters, and what is the correct spelling?
  11. Why are expansions dramatically faster than $(basename …) in loops? What is the review heuristic?
  12. Name three jobs that are over the line into sed/awk territory, and the two boundaries (pattern power, data location) that put them there.

E6. Sources

Interview questions in this module were captured verbatim from:

Edureka — Top 60 Shell Scripting Interview Questions and Answers — "How to get part of string variable with echo command only?" (page updated Dec 9, 2024)

Zero To Mastery — Bash Interview Prep: 31 Essential Questions and Answers — "How do you perform string manipulation in Bash?" (published June 18, 2026)

PlacementPreparation — Top 50 Shell Scripting Interview Questions for Freshers — "How do you check the length of a string in shell scripting?" (published Sept 23, 2024; last updated Feb 27, 2025)

LinuxTeck — Bash read Command FAQ — "How do I set a default value if the user just presses Enter?" (published May 5, 2026)

A note on the corpus: the defaults family and substring/length operators appear in published lists; the trim operators (#/%) — arguably this module's most-used tools — circulate as tutorial material (Java2Blog, LinuxHint, Sentry all cover extension-stripping) rather than as fixed-wording interview questions, and are labeled accordingly. No questions were invented. All expected-result outputs were produced by running the commands on Ubuntu 24.04 / bash 5.2.

Next: single values are solved; collections are not. Lists of hosts, maps of service→port, safe iteration without quoting accidents — bash arrays, indexed and associative: Module 10 — Arrays and Associative Arrays.
Spotted a mistake or want something added? Send me a note.