Working with Files · about 40 min

Awkward names and first wildcards

Why this matters

Real directories contain names like Quarterly Report (final).xlsx, it's done.txt and downloaded files beginning with a dash. Every one of them breaks a careless command, and in a script the breakage can be destructive. On the other side, wildcards let you act on hundreds of files with one short pattern. Both topics come down to the same thing: understanding what bash does to your line before any program sees it.

Which characters are awkward, and why

A Unix file name may contain any byte except / (the separator) and the null byte. Spaces, quotes, newlines, stars, dashes: all legal. The kernel does not care. The trouble is the shell: many characters mean something to bash.

Character(s)Meaning to bash
space, tab, newlineseparate words
' " \quoting
* ? [ ]wildcards (glob patterns)
$ ` | expansions (variables, commands)
; & | < > ( )command structure, redirection
# (at the start of a word)comment: rest of the line ignored
~ (at the start of a word)home directory
- (at the start of an argument)not special to bash, but programs read it as an option

Open the shell for this lesson and look at the odd directory:

$ ls odd
-rf  'it'\''s here.txt'  'my report.txt'  normal.txt  'photo [1].jpg'

ls itself quotes names that need it, showing you a form you could paste back into the shell.

Three ways to quote

Single quotes make everything inside literal. Nothing is special except the closing ':

$ cat 'my report.txt'
report
$ echo 'Price: $5 * 2; done'
Price: $5 * 2; done

Double quotes also keep spaces and wildcards literal, but still allow $ expansions (you will need that for variables). Use them for a name that contains a single quote:

$ cat "it's here.txt"
apostrophe

Backslash makes the next one character literal. Tab completion uses this style:

$ cat my\ report.txt
report
$ cat it\'s\ here.txt
apostrophe

You can mix styles within one word; bash glues adjacent pieces together: 'it'"'"'s here.txt' is the word it's here.txt (single-quoted it, double-quoted ', single-quoted s here.txt). Ugly, but sometimes needed. Module 7 treats quoting in depth; for now the rule is simple: if a name has anything other than letters, digits, ., _ and -, quote it.

Names that start with a dash

$ rm -rf
$ ls
-rf ...

Nothing happened: rm read -rf as options and, having no file operands, did nothing (-f also suppresses the complaint). Quoting does not help, because quotes are removed before rm runs and rm still sees -rf. Two fixes:

$ rm -- -rf          # "--" means: end of options, everything after is a name
$ rm ./-rf           # a path that does not start with "-"

The ./ trick works with every program, even ones that do not understand --.

Wildcards (globbing)

A word containing *, ? or [ is a pattern. Before running the command, bash replaces it with the list of matching file names, sorted. This is called globbing (after glob, a program early Unix used for it), and the patterns are globs.

PatternMatches
*any string, including the empty string
?exactly one character
[abc]one character: a, b or c
[a-z]one character in the range a to z
[!0-9] or [^0-9]one character that is not a digit
$ cd camera
$ echo img00?.jpg
img001.jpg img002.jpg img003.jpg img004.jpg img005.jpg img006.jpg img007.jpg img008.jpg img009.jpg
$ echo *.tar
backup-2025-11.tar backup-2025-12.tar backup-2026-01.tar backup-2026-02.tar
$ echo backup-2025-*
backup-2025-11.tar backup-2025-12.tar
$ echo [bn]*
backup-2025-11.tar backup-2025-12.tar backup-2026-01.tar backup-2026-02.tar notes.txt

echo is the perfect way to preview a pattern: it shows exactly the arguments the real command would get. Make it a reflex before rm or mv.

What happens underneath

 you type:      mv img*.jpg photos/
 bash reads the current directory, matches each name against img*.jpg
 bash runs:     mv img001.jpg img002.jpg ... img014.jpg photos/
                └─ execve("/usr/bin/mv", ["mv", "img001.jpg", ..., "photos/"], env)

The program receives a plain list of names. Consequences:

Rules worth knowing now

Brace expansion is not globbing

{a,b,c} looks similar but works differently: bash generates the words whether or not files exist.

$ echo report.{txt,pdf,md}
report.txt report.pdf report.md
$ mkdir -p project/{src,tests,docs}
$ echo img{1..3}.jpg
img1.jpg img2.jpg img3.jpg

Brace expansion is text generation; globbing is matching existing names. cp config{,.bak} is a popular shortcut for cp config config.bak.

Common mistakes

Recap

Going further: run man 7 glob for the full pattern rules. Then try touch $'line\nbreak.txt' (a name with a newline in it) and see how ls shows it. Names like that are why later modules teach null-separated file lists.

Exercises

Exercise 1 · quiz

Who expands the star?

The current directory contains a.txt, b.txt and c.log. You run rm *.txt. What does the rm program receive as its arguments (after its own name)?

Answer
  • Two arguments: a.txt and b.txt.

bash expands the pattern before running rm: it reads the directory, finds the names matching *.txt, and replaces the word with them, sorted. rm never sees the star. This is why every command "supports wildcards" without any code of its own, and also why the same pattern can mean something different depending on what files exist at that moment.

Exercise 2 · quiz

When nothing matches

The current directory contains no file ending in .xyz. What does echo *.xyz print in bash (with default settings)?

Answer
  • *.xyz

By default, a pattern that matches nothing is left unchanged, so echo receives the literal text *.xyz. That is why rm *.xyz reports rm: cannot remove '*.xyz': No such file or directory: rm was asked to delete a file literally named *.xyz. bash can be told to behave differently (shopt -s nullglob makes it expand to nothing, failglob makes it an error); module 14 covers those options.

Exercise 3 · task in a shell

Handle the awkward ones

The directory /work/odd contains files with awkward names. Look at them with ls -l odd (and ls -b odd, which shows escapes). Then, inside odd:

  1. Rename my report.txt to my_report.txt.
  2. Delete the file named -rf (yes, a file whose name is -rf).
  3. Copy it's here.txt to its_here.txt (keep the original).
  4. Rename photo [1].jpg to photo1.jpg.

Do not delete anything else.

Hint 1

Spaces: put the name in quotes. An apostrophe inside: use double quotes around it.

Hint 2

A name starting with a dash looks like options. -- ends the options, or write ./-rf.

Hint 3

mv "my report.txt" my_report.txt, rm -- -rf, cp "it's here.txt" its_here.txt, mv 'photo [1].jpg' photo1.jpg

Reference solution
cd odd
mv "my report.txt" my_report.txt
rm -- -rf
cp "it's here.txt" its_here.txt
mv 'photo [1].jpg' photo1.jpg

Exercise 4 · task in a shell

Sort with wildcards

/work/camera holds a dump from a camera and some leftovers. Using wildcards (not by typing every name), inside camera:

  1. Create a directory photos and move into it every file whose name starts with img and ends with .jpg. Leave the thumb_*.jpg files where they are.
  2. Delete every backup archive from 2025 (backup-2025-*.tar) but keep the 2026 ones.
  3. Create a directory first9 inside photos and move into it the photos numbered 1 to 9, that is img001.jpg to img009.jpg, with one pattern using ? or [...].

Test each pattern with ls PATTERN or echo PATTERN before using it with mv or rm.

Hint 1

echo img*.jpg shows what the pattern matches. * matches any run of characters, ? exactly one.

Hint 2

mkdir photos && mv img*.jpg photos/, then rm backup-2025-*.tar.

Hint 3

The first nine are img001 to img009: img00?.jpg (three digits, the first two are 0).

Reference solution
cd camera
mkdir photos
mv img*.jpg photos/
rm backup-2025-*.tar
mkdir photos/first9
mv photos/img00?.jpg photos/first9/

Exercise 5 · write code

Predict the expansion

The directory contains data1.csv, data2.csv, data10.csv, report.csv and notes.txt (plus your script.sh). Write a script with three echo commands that print, in order:

  1. all names ending in .csv, using the pattern *.csv
  2. the names matching data?.csv
  3. the names matching data[12]*.csv

bash sorts the matches, and in this sandbox's locale that means character by character, so data10.csv comes before data2.csv. Expected output:

data1.csv data10.csv data2.csv report.csv
data1.csv data2.csv
data1.csv data10.csv data2.csv

File: script.sh

Starting point

echo *.csv

Example

expected output

data1.csv data10.csv data2.csv report.csv
data1.csv data2.csv
data1.csv data10.csv data2.csv
Hint 1

? matches exactly one character, so data?.csv cannot match data10.csv.

Hint 2

[12] matches one character that is 1 or 2; the * after it matches the rest (including nothing).

Reference solution
echo *.csv
echo data?.csv
echo data[12]*.csv