Linux Basics: Control Bash Filename Matching
Filename patterns can miss hidden files, leave an unmatched pattern unchanged, or stop at the first directory level. Bash has options for each situation. Practice including dotfiles, refusing an empty match, and searching a small directory tree while keeping filenames as separate values.
These examples were tested on Linux with GNU Bash 5.2.37. Use Bash rather than sh. They create only named practice files in a fresh scratch directory. Run each script once before cleanup; an existing practice directory causes setup to stop.
Prepare a scratch directory
You need Bash, a text editor, and the mkdir, mktemp, rm, and rmdir utilities. Keep using the same terminal. Save each named source file in this directory before running it. The guards stop the shell if setup fails; invoking bash explicitly avoids changing executable permissions. No administrator access is needed.
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 11. Include hidden names deliberately
Save this as glob-hidden.sh:
#!/usr/bin/env bash
export LC_ALL=C
unset GLOBIGNORE
shopt -u dotglob failglob nullglob
mkdir -- glob-hidden || exit 1
: > glob-hidden/visible.txt || exit 1
: > glob-hidden/.hidden.txt || exit 1
files=(glob-hidden/*)
printf 'Default: %s\n' "${files[@]}"
shopt -s dotglob
files=(glob-hidden/*)
printf 'With dotglob: %s\n' "${files[@]}"Run it:
bash glob-hidden.shOutput:
Default: glob-hidden/visible.txt
With dotglob: glob-hidden/.hidden.txt
With dotglob: glob-hidden/visible.txtdotglob allows * to include a leading dot. The script resets relevant options and unsets GLOBIGNORE, whose filtering behavior can otherwise change results. The C locale makes this small listing’s order predictable.
The array receives one element per matched name. Quoted "${files[@]}" preserves that separation, including any spaces in a filename. This example contains only two known files. Enabling dotglob in a real directory may also select hidden configuration or credential files, so preview the list before copying or modifying anything.
2. Refuse to run when a pattern matches nothing
Save this as glob-required.sh:
#!/usr/bin/env bash
if (
shopt -s failglob
printf 'Found: %s\n' ./missing-*.csv
) 2>/dev/null; then
printf 'Pattern matched\n'
else
printf 'No matching CSV files; command was not run\n'
fiRun it:
bash glob-required.shOutput:
No matching CSV files; command was not runThere are no matching CSV files in the fresh directory. failglob raises an expansion error before printf runs, so no Found: line appears. The subshell keeps that expansion error contained, and the outer conditional prints a controlled explanation.
This demonstration suppresses Bash’s own diagnostic to keep the transcript stable; retain standard error when investigating an unexpected failure. nullglob is different: it removes an unmatched pattern, potentially leaving a command with no filename arguments. Choose that only when an empty list is valid. Neither option checks whether a matched file is readable or has the expected contents.
3. Match text files at several depths
Save this as glob-recursive.sh:
#!/usr/bin/env bash
export LC_ALL=C
unset GLOBIGNORE
shopt -u dotglob failglob
shopt -s globstar nullglob
mkdir -- glob-tree || exit 1
mkdir -- glob-tree/notes || exit 1
: > glob-tree/top.txt || exit 1
: > glob-tree/notes/deep.txt || exit 1
: > glob-tree/notes/skip.log || exit 1
for path in glob-tree/**/*.txt; do
[[ -f $path ]] || continue
printf 'Text file: %s\n' "$path"
doneRun it:
bash glob-recursive.shOutput:
Text file: glob-tree/notes/deep.txt
Text file: glob-tree/top.txtWith globstar enabled, **/ spans zero or more directory levels. The pattern therefore includes both the top-level file and the nested text file. The log file does not match the suffix. The -f test skips directory matches; it also accepts symbolic links that resolve to regular files.
Here dotglob remains off, so hidden names and hidden directories are not included. nullglob makes an empty result produce zero loop iterations rather than one iteration over a literal pattern. These option changes belong to the script’s Bash process, not the terminal that launched it.
Keep in mind
Globs expand before the command or loop uses them. A large tree can create a large in-memory list, and files can change after matching. For complex traversal rules or very large trees, use a tool designed for traversal rather than assuming a pattern is a complete filesystem query.
Clean up
In the same terminal, remove only the named practice files, return to your original directory, and remove the empty scratch directory:
rm -- glob-hidden/visible.txt glob-hidden/.hidden.txt
rm -- glob-tree/top.txt glob-tree/notes/deep.txt glob-tree/notes/skip.log
rmdir -- glob-hidden glob-tree/notes glob-tree
rm -- glob-hidden.sh glob-required.sh glob-recursive.sh
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirReferences
Acknowledgements
This post was created with assistance from AI.