Manipulating Files and Directories
3 min read
Core idea
Five commands cover the vast majority of file management on a Linux system: mkdir (create directories), cp (copy), mv (move and rename), rm (remove), and ln (create links). They are blunt instruments. There is no undo, no recycle bin, no warning by default. The compensating gift is that they all accept wildcards (also called globbing patterns) — special characters the shell expands into lists of matching filenames before the command runs. With wildcards, a single line can rename hundreds of files, copy only the HTML in a folder, or delete every backup older than today. With wildcards and no thought, a single line can also delete every file you own.
The right discipline is to preview with ls, then act with the verb. Almost every painful mistake on the command line traces back to skipping the preview step.
Why it matters
Mental model
Wildcards: what the shell sees vs what the command sees
When you type cp *.txt backup/, the shell does the wildcard expansion before the cp program runs. So cp actually receives cp report.txt notes.txt todo.txt backup/. This means the wildcards work with any command that accepts filenames, not just cp. It also means a wildcard that matches nothing produces an error from the command, not a "no matches" message from the shell.
Wildcard cheat sheet
| Pattern | Matches |
|---|---|
| * | Anything (including the empty string) |
| ? | Exactly one character |
| [abc] | Any one of a, b, or c |
| [!abc] | Anything except a, b, or c |
| [[:alpha:]] | Any letter |
| [[:digit:]] | Any digit |
| [[:upper:]] | Any uppercase letter |
| *.txt | Any file ending in .txt |
| data??? | data followed by exactly three characters |
| [a-c]* | Starts with a, b, or c — use POSIX classes when locale-safe matters |
Dotfiles are excluded from * by default. To match them use .* (which on some shells also matches . and .. — prefer .[!.]* to skip those).
The five verbs
| Verb | What it does | Most-used flag |
|---|---|---|
| mkdir DIR | Create directory(ies) | -p create intermediate directories |
| cp SRC DST | Copy file or directory | -r recursive (required for directories), -i ask, -v show progress |
| mv SRC DST | Move OR rename | -i ask before overwriting |
| rm FILE | Delete file(s) | -r recursive (for directories), -i ask, -f force (no questions, no errors) |
| ln SRC LINK | Create a link | -s symbolic link (almost always what you want) |
Hard links vs symbolic links
A file on disk has two parts: a data block holding the contents and a directory entry holding the name. A hard link is a second directory entry pointing at the same data block — both names refer to one file, the inode count goes up by one, and the data only disappears when the last name is removed. A symbolic link is a tiny file whose contents are a path string; following the link means reading that path and going there. Hard links cannot cross filesystems or refer to directories. Symbolic links can do both — they have replaced hard links in nearly every modern usage.
hard link: name1 ──┐
├──> [inode] ── data on disk
name2 ──┘
symbolic link: name1 ──> "name2" (a path string)
name2 ──> [inode] ── data on disk
If name2 is deleted, name1 (symbolic) becomes a broken link pointing at nothing.
Practical application
Safe defaults
-
Preview wildcard targets with
lsfirst. If you intendrm *.bak, runls *.bakfirst. Use the up-arrow to recall, then changelstorm. -
Use
-iwhile learning.alias rm='rm -i'in~/.bashrcmakesrmalways ask. This will annoy you in a year; until then it will save you. -
Prefer relative paths for symbolic links inside a project.
ln -s ../shared/config.yml ./config.ymlkeeps working if the whole project tree is moved. -
Use
mkdir -pfor nested directories.mkdir -p src/lib/utilscreatessrc, thensrc/lib, thensrc/lib/utilsin one call without erroring on parents that already exist.
Common recipes
# Create a project skeleton in one go
mkdir -p myproject/{src,tests,docs}
# Copy a directory tree, preserving permissions and timestamps
cp -a src/ backup/
# Move a file and keep a backup of the old destination if it exists
mv -i report.txt archive/
# Delete every .tmp file in this directory only
ls *.tmp # preview first
rm *.tmp # then commit
# Recursively delete a directory tree (extreme caution required)
rm -rf old-project/
# Make a symbolic link to a long path
ln -s /var/log/nginx/access.log access.log
Example
You are tidying a folder full of camera dumps named IMG_0001.JPG, IMG_0002.jpg (mixed case), some screenshots called Screenshot 2026-05-26 09-12.png, and a few junk files ending .tmp. You want all photos in a single photos/ directory and the screenshots dropped:
$ cd ~/Downloads
$ ls
IMG_0001.JPG IMG_0002.jpg IMG_0003.JPG
'Screenshot 2026-05-26 09-12.png' 'Screenshot 2026-05-26 09-13.png'
notes.tmp thumb.tmp
# 1. Build the destination
$ mkdir -p photos
# 2. Preview the move
$ ls IMG_*
IMG_0001.JPG IMG_0002.jpg IMG_0003.JPG
# 3. Move the photos. The shell expands IMG_* into a list, the last argument is the destination.
$ mv IMG_* photos/
# 4. Preview the deletion of .tmp files
$ ls *.tmp
notes.tmp thumb.tmp
# 5. Delete them
$ rm *.tmp
# 6. Get rid of the screenshots — note the quoted glob, since the filenames contain spaces
$ rm Screenshot*.png
$ ls
photos/
$ ls photos/
IMG_0001.JPG IMG_0002.jpg IMG_0003.JPG
Six commands, four wildcards, two destructive operations — each one preceded by a non-destructive ls to preview. The whole cleanup took ten seconds and is scriptable: paste it into a file, save as tidy.sh, and you have a tool you can re-run.
Related lessons
Related concepts
- Globbinglinked concept
- Shell Expansionlinked concept
- Filesystemlinked concept
- Command Linelinked concept