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.

Wildcards: what the shell sees vs what the command sees

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) |

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

  1. Preview wildcard targets with ls first. If you intend rm *.bak, run ls *.bak first. Use the up-arrow to recall, then change ls to rm.

  2. Use -i while learning. alias rm='rm -i' in ~/.bashrc makes rm always ask. This will annoy you in a year; until then it will save you.

  3. Prefer relative paths for symbolic links inside a project. ln -s ../shared/config.yml ./config.yml keeps working if the whole project tree is moved.

  4. Use mkdir -p for nested directories. mkdir -p src/lib/utils creates src, then src/lib, then src/lib/utils in 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.

Continue exploring

Tags