Working with Files and Directories
Creating, copying, moving, renaming and deleting files and directories, wildcards, and hard and symbolic links.
Creating files and directories
touch updates a file's modification time to now, and
creates the file (empty) if it does not exist. That second effect is
why it is the usual way to make an empty file. touch -d
sets a specific time instead, which the examples on this page use to
get predictable dates.
mkdir creates a directory. On its own it needs the
parent to exist already; mkdir -p (parents) creates
every missing level along the way, and is silent if the directory
already exists, which makes it safe to use in scripts.
mkdir lab && cd lab
touch notes.txt
mkdir reports
mkdir backup/2026/october # fails: backup does not exist yet
mkdir -p backup/2026/october # creates all three levels
mkdir -p reports # already there: no error
ls -1F
ls -R backup
mkdir: cannot create directory ‘backup/2026/october’: No such file or directory backup/ notes.txt reports/ backup: 2026 backup/2026: october backup/2026/october:
Each example on this page starts in an empty folder and first
creates its own directory, lab, to work in.
ls -F marks directories with a trailing
/ (and executables with *, symbolic links
with @).
Copying with cp
cp source target copies a file. If
the target is an existing directory, the copy goes inside it with
the same name; with several sources, the last argument must be a
directory. The options that matter most:
| Option | Meaning | Example |
|---|---|---|
-r (or -R) |
Recursive: copy a directory with everything in it | cp -r site site-backup |
-i |
Interactive: ask before overwriting an existing file | cp -i new.conf app.conf |
-p |
Preserve modification time and permissions (and ownership, when run as root) | cp -p app.conf app.conf.orig |
-a |
Archive: -r plus preserve everything, and copy
symbolic links as links; the choice for backups
|
cp -a /srv/www /srv/www.bak |
-v |
Verbose: print each file as it is copied | cp -v *.log archive/ |
mkdir lab && cd lab
echo "Port 8080" > app.conf
touch -d '2026-09-01 12:00' app.conf
cp app.conf app.conf.bak # a plain copy gets a new time
cp -p app.conf app.conf.orig # -p keeps the original time
stat -c '%y' app.conf.orig | cut -c1-16
[ app.conf.bak -nt app.conf ] && echo "app.conf.bak got the time of the copy"
2026-09-01 12:00 app.conf.bak got the time of the copy
stat -c '%y' prints the modification time, and
[ a -nt b ] tests whether a is newer than
b. Keeping the original date on a backup copy tells you
later when the configuration last changed, not when you made the
copy.
Without -r, cp refuses to copy a
directory:
mkdir lab && cd lab
mkdir -p site/css
touch site/index.html site/css/style.css
cp site site-copy
cp -r site site-copy
ls -R site-copy
cp: -r not specified; omitting directory 'site' site-copy: css index.html site-copy/css: style.css
Moving and renaming with mv
Linux has no separate rename command for everyday use:
mv does both. Moving a file to a new name in the same
directory is renaming it. Within one file system,
mv only changes the directory entry, so even a huge
file moves instantly; to another file system (another disk), it
copies the data and then deletes the original.
mkdir lab && cd lab
mkdir archive
echo "draft" > report-draft.txt
echo "final" > report.txt
mv report-draft.txt report-v1.txt # rename
mv report-v1.txt archive/ # move into a directory
ls -1 . archive
echo "newer" > notes.txt
echo "older" > archive/notes.txt
mv -n notes.txt archive/ # -n: never overwrite
cat archive/notes.txt
mv -v report.txt archive/report-2026.txt # move and rename, verbosely
.: archive report.txt archive: report-v1.txt older renamed 'report.txt' -> 'archive/report-2026.txt'
By default, mv (like cp) silently
overwrites a target that already exists. -i asks first;
-n (no-clobber) never overwrites, as the example shows:
archive/notes.txt still contains "older", and
notes.txt stayed where it was.
$ mv -i app.conf /etc/myapp/app.conf
mv: overwrite '/etc/myapp/app.conf'? n
Deleting with rm and rmdir
rm removes files. rmdir removes
empty directories only, which makes it a safe choice when
you believe a directory is empty: if it is not, it refuses.
rm -r removes a directory with everything below it.
mkdir lab && cd lab
mkdir -p old/logs empty
touch old/logs/app.log old/readme.txt notes.txt
rm notes.txt
rmdir empty
rmdir old # refuses: not empty
rm -r old # removes old and everything in it
ls -A
rmdir: failed to remove 'old': Directory not empty
ls -A prints nothing: lab is empty again.
Two options change how careful rm is:
-
-iasks before every file. For many files,-Iasks once, before removing more than three files or anything recursively. -
-f(force) does the opposite: it never asks, ignores files that do not exist and prints no errors about them.
$ rm -i *.log
rm: remove regular file 'access.log'? y rm: remove regular file 'error.log'? n
Wildcards and globbing
A wildcard is a character in a file
name pattern that stands for other characters. The shell knows three
kinds, *, ? and brackets, and brackets can
also be negated:
| Pattern | Matches | Example |
|---|---|---|
* |
Any number of characters, including none |
*.log: every name ending in .log
|
? |
Exactly one character |
report-?.txt: report-1.txt, not
report-10.txt
|
[abc], [0-9] |
One character from the set or range |
web0[1-3]: web01 to
web03
|
[!abc] |
One character not in the set |
[!.]*: names that do not start with a dot
|
Globbing is the shell's replacement
of such a pattern by the list of matching names, sorted, before the
command starts. The command never sees the *; it
receives the file names as separate arguments. That is why
echo is the perfect way to preview a pattern: it simply
prints the arguments it received.
mkdir lab && cd lab
touch report-1.txt report-2.txt report-3.txt report-10.txt notes.txt data.csv .hidden.txt
echo *.txt
echo report-?.txt
echo report-[12].txt
echo report-[!1].txt
echo *.pdf
notes.txt report-1.txt report-10.txt report-2.txt report-3.txt report-1.txt report-2.txt report-3.txt report-1.txt report-2.txt report-2.txt report-3.txt *.pdf
Three details from that output:
-
*does not match a leading dot:.hidden.txtis missing from*.txt. -
Names are sorted as text, so
report-10.txtcomes beforereport-2.txt. Zero-padded numbers (report-02.txt) sort correctly. The examples ran withLC_ALL=C.UTF-8. In English locales,ls, glob sorting andsortignore dots, dashes and case at first, so your order may differ;LC_ALL=Cgives byte order. -
When nothing matches, bash passes the pattern on unchanged:
*.pdf. A command such asrm *.pdfthen reports that a file named*.pdfdoes not exist.
Brace expansion
Braces look like a wildcard but are something else. Brace expansion generates text from a list or range, whether or not files with those names exist, which makes it useful for creating things:
mkdir lab && cd lab
echo {jan,feb,mar}.csv
echo report-{1..3}.txt
echo app.conf{,.bak}
mkdir -p 2026/{q1,q2,q3,q4}
ls -1 2026
jan.csv feb.csv mar.csv report-1.txt report-2.txt report-3.txt app.conf app.conf.bak q1 q2 q3 q4
cp app.conf{,.bak} is therefore a common shorthand for
cp app.conf app.conf.bak. Note that there are no spaces
inside the braces; a space would split them into separate words.
Hard links and symbolic links
On Linux a file's data and its name are separate things. The data and the file's details (owner, permissions, size) belong to an inode; a directory entry is only a name that points to an inode. A link adds another way to reach the same file, and there are two kinds.
Hard links
A hard link is an additional
directory entry for an existing inode: a second name for the very
same file. Neither name is the "original". The inode keeps a
link count, and the data is only freed when the last name
is removed (and no program has the file open). ln
creates one:
ln existing new-name.
Hard links have two limits:
- Both names must be on the same file system, because an inode number only means something within one file system.
- You cannot hard-link a directory; that would allow loops in the tree.
Symbolic links
A symbolic link (symlink, soft
link) is a small special file that contains a path to another file
or directory. Opening the link opens whatever is at that path.
ln -s target link-name creates one;
note the order, target first, as with cp. Symbolic
links can point to directories and across file systems, and
ls -l shows them as name -> target. If
the target is moved or deleted, the link stays behind, pointing at
nothing: a broken or dangling link.
mkdir lab && cd lab
echo "Port 8080" > app.conf
ln app.conf app-hard.conf # hard link
ln -s app.conf app-link.conf # symbolic link
stat -c '%h %n' app.conf app-hard.conf
[ app.conf -ef app-hard.conf ] && echo "same inode"
readlink app-link.conf
echo "Port 9090" > app.conf # change the file through one name
cat app-hard.conf app-link.conf
rm app.conf # remove the first name
cat app-hard.conf # the data is still there
stat -c '%h %n' app-hard.conf
ls -1
[ -e app-link.conf ] || echo "app-link.conf is broken"
[ -L app-link.conf ] && echo "but the link itself still exists"
cat app-link.conf
2 app.conf 2 app-hard.conf same inode app.conf Port 9090 Port 9090 Port 9090 1 app-hard.conf app-hard.conf app-link.conf app-link.conf is broken but the link itself still exists cat: app-link.conf: No such file or directory
stat -c '%h' prints the link count;
[ a -ef b ] is true when two names refer to the same
inode; readlink prints the path a symlink holds.
[ -e ] follows the link and asks whether the target
exists, while [ -L ] asks whether the name is a
symbolic link at all. That combination is how you find broken links.
Symbolic links are everywhere on a Linux system. Administrators use
them to switch between versions (current -> releases/2026-10-01), and many services enable a site or a unit with a link from one
directory into another.
Recursive operations
A recursive operation applies a command to a directory and to everything inside it: its files, its subdirectories, their files, and so on down the tree. Many commands have an option for it, but the letter is not consistent:
| Command | Recursive option | Effect |
|---|---|---|
ls |
-R (-r means reverse) |
List every subdirectory |
cp |
-r or -R |
Copy a whole tree |
rm |
-r or -R |
Delete a whole tree |
chmod, chown |
-R only (chmod -r is not an error:
it removes read permission)
|
Change permissions or owner of a whole tree |
grep |
-r or -R |
Search all files below a directory (-R also
follows symbolic links, -r only those named on
the command line)
|
Recursion multiplies the effect of a mistake by the size of the
tree. A recursive chmod on the wrong directory can make
a system unusable, and a recursive change that also touches symbolic
links may reach outside the tree you meant. Aim recursive commands
at the most specific directory you can, and look at the tree first
(ls -R, tree, or find from
Searching with grep, find and Regular Expressions). Setting permissions correctly is covered in
Users, Groups and Permissions in Practice; never "fix" access problems with a recursive
chmod 777.