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:

  • -i asks before every file. For many files, -I asks 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.txt is missing from *.txt.
  • Names are sorted as text, so report-10.txt comes before report-2.txt. Zero-padded numbers (report-02.txt) sort correctly. The examples ran with LC_ALL=C.UTF-8. In English locales, ls, glob sorting and sort ignore dots, dashes and case at first, so your order may differ; LC_ALL=C gives byte order.
  • When nothing matches, bash passes the pattern on unchanged: *.pdf. A command such as rm *.pdf then reports that a file named *.pdf does 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.

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.

Two names, app.conf and app-hard.conf, point directly to the same inode, which points to the data. A third name, app-link.conf, is a symbolic link: it only stores the path app.conf and points to that name. app.conf app-hard.conf app-link.conf inode link count 2 Port 9090 data on disk hard link: two names, one inode symbolic link: stores the path "app.conf"
A hard link is a second name for the same inode. A symbolic link is a separate small file that holds a path.

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.

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.