Chapter 04Lesson 04~55 minutes

Globbing, Brace Expansion, and Safe Bulk Operations

Bulk commands become dangerous when operators mistake a shell pattern for a literal argument. This lesson shows how Bash expands braces, variables, and globs before a command runs—and how to preview and preserve the exact generated argument list.

BeginnerShell expansionSafety patterns

Learning objectives

By the end of this lesson

  • Distinguish shell glob patterns from regular expressions.
  • Use *, ?, and bracket expressions while understanding hidden-file behavior.
  • Explain brace expansion and its position in the shell expansion sequence.
  • Control unmatched patterns with nullglob and failglob.
  • Build safe bulk operations with arrays, quoting, --, dry runs, and find -exec.

1. The shell transforms a command before the program sees it

When a command line contains braces, variables, command substitutions, spaces, or wildcard characters, Bash performs a sequence of expansions. The external program receives the resulting argument vector. It usually never sees the original quotes or wildcard expression.

Simplified Bash argument-generation path
flowchart TD
  C["Typed command line"] --> B["Brace expansion"]
  B --> V["Parameter and command expansion"]
  V --> W["Word splitting when applicable"]
  W --> G["Pathname expansion"]
  G --> Q["Quote removal"]
  Q --> A["Final argv passed to command"]
Preview argv, not prose

A destructive command should be reviewed as the exact list of generated arguments. Use arrays and printf '%q\n' to make spaces, leading dashes, and special characters visible.

2. Pathname expansion uses glob patterns, not regular expressions

PatternMeaning within one path componentExample
*Zero or more characters*.log
?Exactly one characterapp-?.conf
[abc]One listed characternode-[123]
[a-z]One character in a locale-dependent rangerelease-[0-9]
[!0-9]One character not in the setfile-[!0-9]
lab="$HOME/devops-academy/linux/chapter04/lesson04"
rm -rf -- "$lab"
mkdir -p -- "$lab"
printf '%s\n' alpha > "$lab/app-1.log"
printf '%s\n' beta  > "$lab/app-2.log"
printf '%s\n' gamma > "$lab/app-a.log"
printf '%s\n' delta > "$lab/app-10.log"
printf '%s\n' hidden > "$lab/.app-hidden.log"

cd -- "$lab"
printf 'single-character suffix matches:\n'
printf '  %s\n' app-?.log

printf '\nnumeric single-character matches:\n'
printf '  %s\n' app-[0-9].log

A normal * does not match a leading dot at the start of a path component. This protects . and .. from many patterns but also means a command such as cp -a source/* destination/ omits hidden entries. Copying source/. is often the clearer complete-tree form.

3. Quoting decides whether metacharacters remain active

pattern='*.log'

printf 'Unquoted variable may split and glob:\n'
printf '  %s\n' $pattern

printf '\nQuoted variable is one literal argument:\n'
printf '  %s\n' "$pattern"

printf '\nQuoted wildcard is literal:\n'
printf '  %s\n' '*.log'

printf '\nEscaped wildcard is literal:\n'
printf '  %s\n' \*.log

Double quotes permit parameter and command substitution but suppress word splitting and pathname expansion of the resulting value. Single quotes preserve nearly every character literally. A wildcard typed outside quotes remains eligible for pathname expansion.

Why arrays matter

An array preserves argument boundaries. A string containing several filenames cannot safely represent arbitrary names because spaces, newlines, and wildcard characters become ambiguous when re-expanded.

4. Choose what an unmatched pattern means

By default, Bash leaves an unmatched pathname pattern unchanged. A loop over *.log can therefore process the literal text *.log when no files match. Shell options let scripts choose safer behavior.

Default

Pattern remains literal

Useful interactively for visibility, but scripts may mistake it for a real pathname.

nullglob

Unmatched pattern disappears

An array receives zero elements, which is convenient when the script checks its length.

failglob

Unmatched pattern is an error

Useful when absence indicates a failed precondition, though handling requires care because expansion fails before command execution.

dotglob

Wildcards include leading-dot names

Changes the target set broadly; enable only in a controlled scope.

empty="$lab/empty"
mkdir -p -- "$empty"
cd -- "$empty"

shopt -s nullglob
logs=( *.log )
printf 'nullglob match count=%d\n' "${#logs[@]}"
shopt -u nullglob

# Demonstrate failglob in a subshell so the parent shell continues.
if ( shopt -s failglob; printf '%s\n' *.log ) 2>"$lab/failglob.err"; then
  printf 'unexpected match\n'
else
  printf 'failglob rejected unmatched pattern\n'
fi
cat -- "$lab/failglob.err"

5. Brace expansion generates text before pathname matching

Brace expansion is textual. It does not inspect the filesystem. Bash expands alternatives such as {dev,test,prod} and sequences such as {01..05} before parameter expansion and globbing.

root="$lab/environments"
mkdir -p -- "$root"/{dev,test,prod}/{config,logs}

printf 'Generated environment directories:\n'
printf '  %s\n' "$root"/{dev,test,prod}/{config,logs}

printf '\nSequence-generated candidate names:\n'
printf '  node-%02d.conf\n' {1..5}

# Variables are not expanded early enough to define brace syntax.
start=1
end=3
printf '\nThis remains literal brace text:\n'
printf '  %s\n' {$start..$end}

Brace expansion can generate nonexistent names, which is valuable for creation commands but risky if mistaken for a match query. Use globbing to select existing paths and brace expansion to construct predictable text combinations.

6. Use arrays for a reviewable bulk target set

cd -- "$lab"
shopt -s nullglob
candidates=( app-[0-9].log app-10.log )
shopt -u nullglob

printf 'Candidate count: %d\n' "${#candidates[@]}"
printf 'Quoted candidate argv:\n'
printf '  %q\n' "${candidates[@]}"

if ((${#candidates[@]} == 0)); then
  printf 'No matching logs; nothing to do.\n'
else
  mkdir -p -- archive
  cp -vn -- "${candidates[@]}" archive/
fi

printf '\nArchive verification:\n'
find archive -maxdepth 1 -type f -printf '%f\n' | sort

"${array[@]}" expands to one quoted argument per element. By contrast, ${array[*]} and unquoted expansions can collapse or re-split boundaries. Always use the [@] form for command arguments.

7. For recursive selection, prefer find with null-safe execution

Pathnames may contain spaces, tabs, newlines, wildcard characters, and leading dashes. Parsing find output line by line is therefore fragile unless a null delimiter is used. For direct execution, -exec ... {} + passes names as arguments without text parsing.

tree="$lab/tree"
mkdir -p -- "$tree/a" "$tree/b"
printf x > "$tree/a/old build.log"
printf y > "$tree/b/-leading-dash.log"
printf z > "$tree/b/keep.txt"

printf 'Preview selected logs:\n'
find "$tree" -type f -name '*.log' -printf '  %p\n'

mkdir -p -- "$lab/recursive-archive"
find "$tree" -type f -name '*.log' \
  -exec cp -vn -- {} "$lab/recursive-archive/" +

printf '\nCopied basenames:\n'
find "$lab/recursive-archive" -maxdepth 1 -type f -printf '%f\n' | sort
Basename collisions

Copying recursively selected files into one flat directory can overwrite or skip files that share a basename. Preserve relative paths or detect collisions before production use.

8. Hands-on lab: archive logs with preview and exact argv preservation

set -u
root="$HOME/devops-academy/linux/chapter04/lesson04-bulk"
source_dir="$root/incoming"
archive_dir="$root/archive"
rm -rf -- "$root"
mkdir -p -- "$source_dir" "$archive_dir"

printf 'ok\n' > "$source_dir/api 01.log"
printf 'warn\n' > "$source_dir/api 02.log"
printf 'ignore\n' > "$source_dir/api.tmp"
printf 'hidden\n' > "$source_dir/.internal.log"
printf 'dash\n' > "$source_dir/-special.log"

cd -- "$source_dir"
shopt -s nullglob
logs=( *.log )
shopt -u nullglob

printf 'Dry-run target list (%d files):\n' "${#logs[@]}"
printf '  %q\n' "${logs[@]}"

if ((${#logs[@]})); then
  cp -vn -- "${logs[@]}" "$archive_dir/"
fi

printf '\nArchived files:\n'
find "$archive_dir" -maxdepth 1 -type f -printf '%f\n' | sort

printf '\nHidden source file remains excluded by normal glob:\n'
test -f "$source_dir/.internal.log" && printf 'yes\n'

Verification checklist

9. Common expansion mistakes

Calling *.log a regular expression

It is a shell glob. Regex syntax and matching semantics are different.

Looping over $(find ...)

Command substitution and word splitting corrupt pathnames containing whitespace or wildcard characters.

Forgetting unmatched-pattern behavior

The literal pattern may enter a loop or command unless nullglob, failglob, or an explicit existence check is used.

Using unquoted array expansion

Unquoted elements can be split and globbed again. Use "${array[@]}".

10. Knowledge check

Question 1. What is the difference between brace expansion and pathname expansion?

Question 2. Why is "${files[@]}" preferred when passing an array to a command?

Question 3. What problem does nullglob solve?

11. Summary

Bash constructs command arguments before external programs run. Globs select existing pathnames, brace expansion generates text combinations, and quoting controls whether metacharacters remain active. Safe bulk operations materialize targets in arrays, preview them with quoted output, handle zero matches explicitly, terminate option parsing with --, and use null-safe recursive tools.

Next lesson

Temporary Files, Hidden Files, and Directory Layout

You will place transient, configuration, runtime, log, and application data in appropriate locations and create temporary resources safely.

12. Further reading

  • GNU Bash Reference Manual — shell expansions, pattern matching, arrays, glob options, and quoting.
  • GNU Coreutils manuals — argument handling and -- conventions.
  • GNU findutils manual — find expressions, -exec, null delimiters, and filename safety.
  • POSIX Shell Command Language — pathname expansion, field splitting, and quoting.
  • ShellCheck documentation — diagnostics for unsafe expansions and loops over command output.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.