Everything you have run so far was harmless: looking, listing, querying. This lesson crosses the line. From here on the commands write to the disk, and some of them delete without asking and with no way of undoing it.
This is not a rhetorical warning. On Linux there is no recycle bin by default: rm unlinks the file and the space becomes available for reuse. Recovering something deleted on a mounted, in-use file system is, in practice, impossible. That is why this lesson devotes a whole section to defensive strategies, and why the VM snapshot matters more today than ever.
In exchange, these are the tools you really work with: creating directory structures, copying while preserving permissions, moving and renaming, synchronising complete trees and packaging software for distribution. By the end you will be able to prepare a package of /opt/tramontana/app ready to send to Luis, confident that it will arrive exactly as it left.
Contents
touch: creating and touching timestampsmkdirandrmdir: directory structurecp: copying, and the trailing-slash trapmv: moving and renamingrm: the serious conversation- Batch renaming with
rename rsync: the right way to copy directoriestar: packaging- Compression:
gzip,bzip2,xzandzip - Practice: preparing the package for Luis
touch: creating and touching timestamps
touch: creating and touching timestampstouch does two different things depending on whether the file exists:
- If it does not exist, it creates it empty.
- If it does exist, it updates its timestamps without touching the contents.
operator@srv-tramontana:~$ touch august-report.txt
operator@srv-tramontana:~$ ls -l august-report.txt
-rw-rw-r-- 1 operator operator 0 Aug 18 10:15 august-report.txtZero bytes: it exists but it is empty. It accepts several arguments:
The second function, updating timestamps, is the one that gives the command its name and the one that surprises anyone who only knows it as "create an empty file". Picking up the three timestamps from the previous lesson:
| Option | What it updates |
|---|---|
| (none) | atime and mtime to the current moment |
-a |
atime only |
-m |
mtime only |
-t YYYYMMDDhhmm.ss |
Sets the timestamp to that specific date |
-d "string" |
Sets the timestamp by interpreting a date in natural language |
-r file |
Copies the timestamps from another file |
-c |
Does not create the file if it does not exist |
operator@srv-tramontana:~$ touch -t 202601011200.00 august-report.txt
operator@srv-tramontana:~$ ls -l --time-style=long-iso august-report.txt
-rw-rw-r-- 1 operator operator 0 2026-01-01 12:00 august-report.txt
operator@srv-tramontana:~$ touch -d "yesterday 09:00" note1.txt
operator@srv-tramontana:~$ ls -l --time-style=long-iso note1.txt
-rw-rw-r-- 1 operator operator 0 2026-08-17 09:00 note1.txtNotice what has not changed:
operator@srv-tramontana:~$ stat -c 'mtime=%y%nctime=%z' august-report.txt
mtime=2026-01-01 12:00:00.000000000 +0200
ctime=2026-08-18 10:18:44.331920103 +0200The mtime says January, but the ctime says today. It is exactly what the exercise in lesson 02-03 anticipated: you can forge the mtime, but the ctime gives away that the inode was touched just now.
Legitimate uses of touch in administration:
- Creating a lock file or a marker (
/var/run/tramontana.lock). - Forcing a tool that compares dates to treat a file as modified.
- Setting the date of a file restored from a backup so that it matches the original.
- Creating an empty file before giving it restrictive permissions and only then writing to it, so that it never exists with open permissions.
-c is the defensive option: update if it exists, and if it does not exist do nothing. It avoids creating empty files because of a typo in the name.
mkdir and rmdir: directory structure
mkdir and rmdir: directory structureoperator@srv-tramontana:~$ mkdir reports
operator@srv-tramontana:~$ mkdir drafts published archive
operator@srv-tramontana:~$ ls -F
archive/ data/ drafts/ published/ reports/ scripts/| Option | What it does |
|---|---|
-p |
Creates any missing parents and does not fail if it already exists |
-m mode |
Creates it with those permissions directly |
-v |
Reports each directory created |
Without -p, mkdir does not create nested paths:
operator@srv-tramontana:~$ mkdir reports/2026/august
mkdir: cannot create directory 'reports/2026/august': No such file or directory
operator@srv-tramontana:~$ mkdir -p reports/2026/august
operator@srv-tramontana:~$ tree reports
reports
└── 2026
└── august-p has a second property, as important as the first: it does not fail if the directory already exists.
operator@srv-tramontana:~$ mkdir reports
mkdir: cannot create directory 'reports': File exists
operator@srv-tramontana:~$ echo $?
1
operator@srv-tramontana:~$ mkdir -p reports
operator@srv-tramontana:~$ echo $?
0That makes mkdir -p idempotent: running it ten times leaves the system in the same state as running it once. It is the property you look for in scripts, where a mkdir failing because the directory already existed would abort execution for no reason. You will pick it up again in Module 4 and with Ansible in Module 7.
-m creates with specific permissions in a single operation:
operator@srv-tramontana:~$ mkdir -m 700 private
operator@srv-tramontana:~$ ls -ld private
drwx------ 2 operator operator 4096 Aug 18 10:24 privateIt is better than creating and then running chmod, because between the two operations there would be a window — brief, but real — in which the directory has more open permissions than it should. On a multi-user system, that window is a security problem. The meaning of the 700 is lesson 02-07.
With brace expansion you can create complete structures in one go:
operator@srv-tramontana:~$ mkdir -p reports/2026/{01..03}
operator@srv-tramontana:~$ tree reports/2026
reports/2026
├── 01
├── 02
├── 03
└── augustThose braces are brace expansion, a shell feature explained in lesson 03-02. Use it now; you will understand its mechanics later.
rmdir deletes a directory, but only if it is empty:
operator@srv-tramontana:~$ rmdir reports
rmdir: failed to remove 'reports': Directory not empty
operator@srv-tramontana:~$ rmdir drafts
operator@srv-tramontana:~$ ls -d drafts
ls: cannot access 'drafts': No such file or directoryThat limitation is not a defect: it is a safety net. rmdir cannot delete anything by accident because it only acts on empty directories. When you want to remove a directory with contents you will have to use rm -r, and at that moment you are consciously accepting the risk.
rmdir -p removes the whole branch as long as it keeps being left empty:
operator@srv-tramontana:~$ rmdir -p reports/2026/august
operator@srv-tramontana:~$ ls -d reports
ls: cannot access 'reports': No such file or directoryIt deleted august, then 2026 because it was left empty, and then reports.
cp: copying, and the trailing-slash trap
cp: copying, and the trailing-slash trapoperator@srv-tramontana:~$ cp data/bookings.csv data/bookings-copy.csv
operator@srv-tramontana:~$ ls data/
bookings-copy.csv bookings.csv houses.txtThe options that matter:
| Option | What it does |
|---|---|
-r / -R |
Recursive: mandatory for copying directories |
-i |
Asks before overwriting |
-n |
Never overwrites, without asking |
-u |
Copies only if the source is newer or the destination does not exist |
-p |
Preserves mode, owner and timestamps |
-a |
Archive: equivalent to -dR --preserve=all. The option for faithful copies |
-v |
Shows what it copies |
-L / -P |
Follows / does not follow symbolic links (lesson 02-06) |
--backup=numbered |
Keeps a numbered copy of the destination before overwriting it |
cp overwrites without warning. It is the default behaviour and it has destroyed a lot of work:
operator@srv-tramontana:~$ cp data/houses.txt data/bookings.csv
operator@srv-tramontana:~$ head -n 1 data/bookings.csv
mas-figueres;Mas Figueres;Girona;6bookings.csv no longer contains bookings: it contains houses. There was not a single warning.
Compare with the defensive version:
operator@srv-tramontana:~$ cp -i data/houses.txt data/bookings.csv
cp: overwrite 'data/bookings.csv'? nThe difference between -p and -a:
operator@srv-tramontana:~$ ls -l --time-style=long-iso /etc/hostname
-rw-r--r-- 1 root root 16 2025-06-12 10:03 /etc/hostname
operator@srv-tramontana:~$ cp /etc/hostname /tmp/h1
operator@srv-tramontana:~$ cp -p /etc/hostname /tmp/h2
operator@srv-tramontana:~$ ls -l --time-style=long-iso /tmp/h1 /tmp/h2
-rw-r--r-- 1 operator operator 16 2026-08-18 10:31 /tmp/h1
-rw-r--r-- 1 operator operator 16 2025-06-12 10:03 /tmp/h2The normal copy has today's date; the copy made with -p keeps the original date. The owner changed in both because operator cannot create files belonging to root (that requires sudo; the details are in lesson 02-07).
| What is preserved | cp |
cp -p |
cp -a |
|---|---|---|---|
| Contents | Yes | Yes | Yes |
| Permissions | No (the umask applies) | Yes | Yes |
| Timestamps | No | Yes | Yes |
| Owner and group | No | Yes (if you are root) | Yes (if you are root) |
| Symbolic links | Follows them and copies the contents | Follows them | Copies them as links |
| Recursive | No | No | Yes |
| Extended attributes and ACLs | No | Partially | Yes |
Practical rule: to copy a directory tree exactly as it is, cp -a. For everything else, cp -r is enough.
The trailing-slash trap
This is the behaviour that generates the most confusion in the whole command. The key: cp behaves differently depending on whether the destination exists and is a directory or not.
# CASE 1: the destination does NOT exist -> it creates a file with that name
operator@srv-tramontana:~$ cp data/houses.txt copy.txt
operator@srv-tramontana:~$ ls -F copy.txt
copy.txt
# CASE 2: the destination exists and IS a directory -> it copies INSIDE it
operator@srv-tramontana:~$ mkdir backup
operator@srv-tramontana:~$ cp data/houses.txt backup
operator@srv-tramontana:~$ ls backup/
houses.txtIn case 2, the file is called backup/houses.txt. So far it is intuitive. The surprise comes with directories:
operator@srv-tramontana:~$ tree backup
backup
└── houses.txt
# WITHOUT a trailing slash and with a destination that ALREADY EXISTS: it copies the directory INSIDE
operator@srv-tramontana:~$ cp -r data backup
operator@srv-tramontana:~$ tree backup
backup
├── data
│ ├── bookings.csv
│ └── houses.txt
└── houses.txt
# If the destination does NOT exist: it creates the directory with that name
operator@srv-tramontana:~$ cp -r data backup2
operator@srv-tramontana:~$ tree backup2
backup2
├── bookings.csv
└── houses.txtThe same command produces two different results depending on whether backup existed. That is the classic failure: you run a copy script twice and the second time it creates a nested backup/data/.
And now the trailing slash on the source, which changes nothing in cp but changes everything in rsync:
operator@srv-tramontana:~$ cp -r data/ backup3
operator@srv-tramontana:~$ tree backup3
backup3
├── bookings.csv
└── houses.txtWith cp, data and data/ give the same result. In rsync they do not, as you will see in section 7. It is one of the historical inconsistencies of Unix that you have to live with.
A practical defence: when copying directories, run ls -d destination first to find out whether it exists. And in scripts, always use explicit, checked destinations.
mv: moving and renaming
mv: moving and renamingmv does both things with the same command, because deep down they are the same operation: changing the directory entry that points to a file.
# Rename
operator@srv-tramontana:~$ mv august-report.txt report-2026-08.txt
# Move
operator@srv-tramontana:~$ mv report-2026-08.txt backup/
# Move and rename at the same time
operator@srv-tramontana:~$ mv backup/report-2026-08.txt data/monthly-report.txt| Option | What it does |
|---|---|
-i |
Asks before overwriting |
-n |
Never overwrites |
-u |
Moves only if the source is newer |
-v |
Reports each move |
-b |
Makes a backup of the destination before overwriting it |
-t dir |
Gives the destination first, useful in scripts |
Like cp, mv overwrites without asking, and with one crucial difference: with cp you lose only the destination; with mv you lose the destination and the source is no longer where it was.
Inside and across file systems
This detail explains a behaviour that is disconcerting when you move large files.
operator@srv-tramontana:~$ ls -i data/houses.txt
262149 data/houses.txt
operator@srv-tramontana:~$ mv data/houses.txt backup/houses.txt
operator@srv-tramontana:~$ ls -i backup/houses.txt
262149 backup/houses.txtThe same inode. Not a single byte has been copied: one directory entry has simply been deleted and another created pointing to the same inode. That is why moving a 40 GB file within the same disk is instantaneous.
Now, between different file systems:
flowchart TD
A["mv source destination"] --> B{"Same file<br/>system?"}
B -->|Yes| C["rename(): changes the<br/>directory entry"]
C --> D["INSTANTANEOUS<br/>Same inode<br/>Preserves everything"]
B -->|No| E["Copy the bytes to the destination"]
E --> F["Delete the source"]
F --> G["SLOW: depends on the size<br/>New inode<br/>Can be interrupted halfway"]
The practical consequences of the right-hand branch:
- It takes time: moving 40 GB from
/to a USB disk means reading and writing 40 GB. - It needs free space at the destination throughout the whole operation.
- If it is interrupted halfway (a power cut,
Ctrl+C), the destination is left with a partial file and the source may have been partially deleted if there were several files.mvis not atomic across file systems. - The ctime changes and the owner may change, because a new file has in fact been created.
How to find out whether two paths are on the same file system:
operator@srv-tramontana:~$ df /home/operator /opt/tramontana
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/sda2 23791616 6984924 15574056 31% /
/dev/sda2 23791616 6984924 15574056 31% /The same device, /dev/sda2: the move will be instantaneous. If different devices appeared, it would be copy-and-delete.
A rule for moving big things between disks: use rsync and delete afterwards, once you have verified. That way, if something fails, the source is still intact.
rm: the serious conversation
rm: the serious conversationrm removes files. On a mounted, in-use file system, it does so irreversibly.
| Option | What it does |
|---|---|
-r / -R |
Recursive: mandatory for directories |
-f |
Force: does not ask, ignores files that do not exist |
-i |
Asks for every file |
-I |
Asks only once if there are more than 3 files or it is recursive |
-v |
Shows what it deletes |
-d |
Deletes empty directories, like rmdir |
--one-file-system |
Does not cross into other file systems while walking |
--preserve-root |
Refuses to operate on / (on by default) |
-I is the sweet spot: it stops you to think once, without the fatigue of -i asking four hundred times (which ends up making you answer "yes" automatically, which is worse than not asking).
Why rm -rf deserves a section of its own
rm -rf is the combination that deletes a whole tree without a single question and without complaining about anything. It is necessary in scripts and it is responsible for most self-inflicted data disasters.
The paradigmatic case is the empty variable. A script contains:
If for whatever reason DEST and SUBDIR are empty — because a previous cd failed, because a configuration file was not read, because there was a typo in the variable name — the line that gets executed is:
This mistake has happened in real companies and has gone as far as wiping out entire production systems. The best-known public case is that of an installer for a commercial game which, for this very reason, deleted the complete home directory of its users.
Modern versions of rm include --preserve-root, which refuses rm -rf /:
operator@srv-tramontana:~$ sudo rm -rf /
rm: it is dangerous to operate recursively on '/'
rm: use --no-preserve-root to override this failsafeBut that protection only covers the exact root. rm -rf /home or rm -rf /opt/tramontana run without objection. And with the slash: rm -rf "$DEST"/ when DEST is empty gives rm -rf /, which is protected; but rm -rf "$DEST"/* with an empty DEST gives rm -rf /*, which is not protected and deletes the entire contents of the root.
Defensive strategies
1. Look before deleting. The habit you already know, now mandatory:
operator@srv-tramontana:~$ ls -la /srv/tramontana/backups/temp/
operator@srv-tramontana:~$ rm -rI /srv/tramontana/backups/temp/And with patterns, even more so: run ls with the same pattern, check the list and only then change ls for rm with the up arrow.
2. Use -I out of habit in interactive work.
3. A wastebasket with trash-cli. There is an equivalent of the desktop wastebasket for the terminal:
operator@srv-tramontana:~$ sudo apt install -y trash-cli
operator@srv-tramontana:~$ trash-put data/monthly-report.txt
operator@srv-tramontana:~$ trash-list
2026-08-18 10:52:31 /home/operator/data/monthly-report.txt
operator@srv-tramontana:~$ trash-restoreIt is useful on your laptop. On a server it has a serious drawback: the files still take up space in ~/.local/share/Trash, and if you were deleting to free up disk, you have freed nothing. Use it with judgement.
4. Defensive aliases. You can make rm mean rm -I by default through an alias. Aliases are covered in lesson 03-01, and there we will discuss why this particular practice is debatable: you get used to a protection that does not exist on any other server, and the day you work on somebody else's machine you will have a false sense of security.
5. Quotes and --. Always quote your variables and use -- before names that come from outside.
6. What really saves you: backups. None of the measures above protects against a deliberate, well-written deletion. The only thing that protects you is having copies somewhere else, and that is lesson 05-08. And in your lab, the VM snapshot.
One last technical note: rm does not delete the contents, it only unlinks the name. The data stays in the blocks until it is overwritten. That means two opposite things: that a forensic analyst could recover it, and that you, in practice, will not be able to, because the system will keep writing over it while you try. For genuinely secure deletion there is shred, with its own limitations on SSDs and journalled file systems.
- Batch renaming with
rename
renameRenaming fifty files by hand is not an option. rename (the Perl version, which is the one on Debian and Ubuntu) applies a transformation to many names:
operator@srv-tramontana:~$ ls reports/
report_january.TXT report_february.TXT report_march.TXT
operator@srv-tramontana:~$ rename -n 's/\.TXT$/.txt/' reports/*.TXT
rename(reports/report_january.TXT, reports/report_january.txt)
rename(reports/report_february.TXT, reports/report_february.txt)
rename(reports/report_march.TXT, reports/report_march.txt)
operator@srv-tramontana:~$ rename 's/\.TXT$/.txt/' reports/*.TXT
operator@srv-tramontana:~$ ls reports/
report_january.txt report_february.txt report_march.txt-n (or --nono) is mandatory on the first run: it shows what it would do without doing it. It is the equivalent of "look before acting" for mass renames.
More examples:
# Replace underscores with hyphens
operator@srv-tramontana:~$ rename -n 's/_/-/g' reports/*
rename(reports/report_january.txt, reports/report-january.txt)
# Add a prefix
operator@srv-tramontana:~$ rename -n 's/^/2026-/' reports/*
rename(reports/report-january.txt, reports/2026-report-january.txt)
# Convert to lower case
operator@srv-tramontana:~$ rename -n 'y/A-Z/a-z/' reports/*The s/pattern/replacement/ syntax is Perl regular expressions, the subject of lesson 03-02. For now the literal pattern and -n to verify are enough.
Watch out for two things: on some distributions (Fedora, RHEL) rename is a completely different program with a different syntax, and rename overwrites without warning if the destination name already exists. Add -v to see what it is really doing.
rsync: the right way to copy directories
rsync: the right way to copy directoriesrsync is designed for synchronising directory trees. Compared with cp -a it has three advantages that make it the default tool as soon as the copy is large or important:
cp -a |
rsync -av |
|
|---|---|---|
| Incremental copy | No: it copies everything every time | Yes: only what changed |
| Resumable | No: you have to start from scratch | Yes (with --partial) |
| Progress | Silent | --progress |
| Verification | None | Checksum of the contents |
| Deleting extras at the destination | No | --delete |
| Exclusions | No | --exclude |
| Remote copy over SSH | No | Yes, natively |
operator@srv-tramontana:~$ rsync -av /opt/tramontana/app/ /srv/tramontana/backups/app-copy/
sending incremental file list
created directory /srv/tramontana/backups/app-copy
./
executable
version.txt
templates/
templates/confirmation.html
templates/invoice.html
sent 50,331,208 bytes received 115 bytes 100,662,646.00 bytes/sec
total size is 50,329,412 speedup is 1.00A second run, with no changes:
operator@srv-tramontana:~$ rsync -av /opt/tramontana/app/ /srv/tramontana/backups/app-copy/
sending incremental file list
sent 143 bytes received 19 bytes 324.00 bytes/sec
total size is 50,329,412 speedup is 310,675.38162 bytes instead of 50 MB. It has compared and found nothing to copy. That is the saving that justifies using rsync for anything that gets repeated.
Essential options:
| Option | What it does |
|---|---|
-a |
Archive mode: recursive + preserves permissions, dates, owners and links |
-v |
Verbose |
-h |
Readable figures |
--progress |
A progress bar per file |
-n / --dry-run |
Simulates without copying anything |
--delete |
Deletes at the destination anything no longer in the source |
--exclude 'pattern' |
Excludes paths |
-c |
Compares by checksum instead of by size and date |
-z |
Compresses during the transfer (only useful over the network) |
In rsync the trailing slash really does matter
Here it does, and it is mistake number one with this tool:
# WITH a slash on the source: it copies the CONTENTS of app
operator@srv-tramontana:~$ rsync -av /opt/tramontana/app/ /tmp/dest/
# Result: /tmp/dest/executable, /tmp/dest/version.txt...
# WITHOUT a slash: it copies the DIRECTORY app inside the destination
operator@srv-tramontana:~$ rsync -av /opt/tramontana/app /tmp/dest/
# Result: /tmp/dest/app/executable, /tmp/dest/app/version.txt...A mnemonic: the trailing slash means "the contents of". Without a slash, "the folder itself".
--delete combined with this is dangerous, because it deletes everything at the destination that is not in the source. If you get the level wrong, you empty the destination. Always -n first:
operator@srv-tramontana:~$ rsync -avn --delete /opt/tramontana/app/ /srv/tramontana/backups/app-copy/
sending incremental file list
deleting obsolete.txt
sent 156 bytes received 22 bytes 356.00 bytes/sec-n has shown you that it was going to delete obsolete.txt. Now you decide with information.
Using rsync for real backups — with rotation, linked incremental copies and remote destinations — is lesson 05-08. Here you use it as the good cp for directories.
tar: packaging
tar: packagingtar (tape archive) joins many files into one preserving the structure, the permissions and the dates. On its own it does not compress: that is done by an external compressor it calls.
The three operation modes are mutually exclusive, as you worked out by reading the manual in lesson 02-02:
| Option | Mode |
|---|---|
-c |
Create |
-x |
eXtract |
-t |
lisT the contents |
And the options that accompany them:
| Option | What it does |
|---|---|
-f file |
The archive name. It must come last if you group options |
-v |
Verbose |
-z |
Compress with gzip (.tar.gz) |
-j |
Compress with bzip2 (.tar.bz2) |
-J |
Compress with xz (.tar.xz) |
-C dir |
Changes to that directory before operating |
--exclude='pattern' |
Excludes paths |
-p |
Preserves permissions when extracting |
Creating:
operator@srv-tramontana:~$ tar -czvf /tmp/app-3.2.1.tar.gz -C /opt/tramontana app
app/
app/executable
app/version.txt
app/templates/
app/templates/confirmation.html
app/templates/invoice.htmlA breakdown of the command, which is the important part:
-ccreate,-zcompress with gzip,-vshow,-ffollowed by the name of the resulting file.-C /opt/tramontanamakestarposition itself there before packaging. Without this, it would packageopt/tramontana/app/...with the whole hierarchy inside.appis what gets packaged, already relative to-C.
Listing before extracting is mandatory:
operator@srv-tramontana:~$ tar -tzvf /tmp/app-3.2.1.tar.gz
drwxr-xr-x root/root 0 2026-08-18 08:30 app/
-rwxr-xr-x root/root 50319872 2026-08-18 08:30 app/executable
-rw-r--r-- root/root 26 2026-08-18 08:30 app/version.txt
drwxr-xr-x root/root 0 2026-08-18 08:30 app/templates/
-rw-r--r-- root/root 4218 2026-08-18 08:30 app/templates/confirmation.html
-rw-r--r-- root/root 2104 2026-08-18 08:30 app/templates/invoice.htmlWhy it is mandatory: to check that the archive has a single root directory. If instead of app/... you saw the files loose at the root of the package, extracting it would scatter dozens of files around your current directory. That is what is known as a tarbomb, and cleaning it up by hand is tedious.
Extracting:
operator@srv-tramontana:~$ mkdir -p /tmp/test && tar -xzvf /tmp/app-3.2.1.tar.gz -C /tmp/test
operator@srv-tramontana:~$ tree /tmp/test
/tmp/test
└── app
├── executable
├── templates
│ ├── confirmation.html
│ └── invoice.html
└── version.txtAlways extracting with -C into an empty directory avoids the tarbomb problem entirely.
Modern versions of tar detect the compression automatically when extracting, so -z, -j or -J are not needed to decompress. They are needed when creating, because there tar cannot guess what you want.
Excluding what should not travel:
operator@srv-tramontana:~$ tar -czf /tmp/app-clean.tar.gz \
--exclude='*.log' --exclude='*.tmp' --exclude='cache' \
-C /opt/tramontana appAnd a note that is worth gold: the historical syntax of tar accepts options without a hyphen (tar czf) because it predates the POSIX hyphen convention. Both work. Use the hyphenated one, which is the one people understand.
- Compression:
gzip, bzip2, xz and zip
gzip, bzip2, xz and zipThe Unix compressors work on a single file and by default replace the original:
operator@srv-tramontana:~$ ls -lh /tmp/data.csv
-rw-rw-r-- 1 operator operator 12M Aug 18 11:02 /tmp/data.csv
operator@srv-tramontana:~$ gzip /tmp/data.csv
operator@srv-tramontana:~$ ls -lh /tmp/data.csv*
-rw-rw-r-- 1 operator operator 2.1M Aug 18 11:02 /tmp/data.csv.gzdata.csv no longer exists: it has become data.csv.gz. To keep the original you have to ask for it with -k:
operator@srv-tramontana:~$ gzip -k /tmp/data.csv
operator@srv-tramontana:~$ gunzip /tmp/data.csv.gz # or gzip -dA comparison over a 100 MB text file, which is where these differences show:
| Compressor | Extension | Typical ratio | Compression speed | Decompression | RAM use | When to use it |
|---|---|---|---|---|---|---|
| gzip | .gz |
~30 % | Very fast | Very fast | Low | By default. Logs, transfers, everyday work |
| bzip2 | .bz2 |
~25 % | Slow | Slow | Medium | Hardly recommendable today: xz beats it |
| xz | .xz |
~20 % | Very slow | Fast | High | Distributing software, long-term archiving |
| zstd | .zst |
~28 % | Very fast | Very fast | Medium | The modern alternative to gzip |
| zip | .zip |
~32 % | Fast | Fast | Low | Compatibility with Windows |
The decision rule:
- You compress many times and decompress few (logs rotated daily):
gzip. Speed matters more than size. - You compress once and decompress many times (a release that will be downloaded a thousand times):
xz. The compression time is paid once and you save bandwidth on every download. - You are passing it to somebody on Windows:
zip.
zip is the only one that archives and compresses at the same time, as on Windows:
operator@srv-tramontana:~$ zip -r /tmp/app.zip /opt/tramontana/app
adding: opt/tramontana/app/ (stored 0%)
adding: opt/tramontana/app/executable (deflated 62%)
...
operator@srv-tramontana:~$ unzip -l /tmp/app.zip
operator@srv-tramontana:~$ unzip /tmp/app.zip -d /tmp/from-zipIts big limitation on Linux: it does not preserve Unix permissions or ownership properly. If you package with zip and unpack, the execution bits can be lost and the script stops working. That is why on Linux you use tar, which does preserve them, and zip only when the recipient uses Windows.
Tools for working with compressed files without decompressing them, very useful with logs:
operator@srv-tramontana:~$ zcat /var/log/tramontana/access.log.1.gz
operator@srv-tramontana:~$ zless /var/log/tramontana/access.log.1.gz
operator@srv-tramontana:~$ zgrep "ERROR" /var/log/tramontana/errors.log.2.gz
- Practice: preparing the package for Luis
Luis Ferrer needs an exact copy of version 3.2.1 deployed in production in order to reproduce a bug on his laptop. Marta has approved sending it to him. The package must not include logs or temporary files, and it has to arrive intact.
Step 1: verify what you are going to package.
operator@srv-tramontana:~$ cat /opt/tramontana/app/version.txt
Tramontana Bookings 3.2.1
operator@srv-tramontana:~$ du -sh /opt/tramontana/app
48M /opt/tramontana/app
operator@srv-tramontana:~$ tree -L 2 /opt/tramontana/app
/opt/tramontana/app
├── executable
├── templates
│ ├── confirmation.html
│ └── invoice.html
└── version.txtStep 2: create the working directory. You do not package in plain /tmp: you use somewhere with space and where the file will not vanish on the next reboot.
Step 3: package it, excluding what must not leave.
operator@srv-tramontana:~$ sudo tar -czvf /srv/tramontana/backups/outgoing/tramontana-app-3.2.1.tar.gz \
--exclude='*.log' --exclude='*.tmp' --exclude='__pycache__' \
-C /opt/tramontana app
app/
app/executable
app/version.txt
app/templates/
app/templates/confirmation.html
app/templates/invoice.htmlStep 4: verify the result. Never send a package you have not listed.
operator@srv-tramontana:~$ ls -lh /srv/tramontana/backups/outgoing/
total 19M
-rw-r--r-- 1 root root 19M Aug 18 11:20 tramontana-app-3.2.1.tar.gz
operator@srv-tramontana:~$ tar -tzvf /srv/tramontana/backups/outgoing/tramontana-app-3.2.1.tar.gz
drwxr-xr-x root/root 0 2026-08-18 08:30 app/
-rwxr-xr-x root/root 50319872 2026-08-18 08:30 app/executable
-rw-r--r-- root/root 26 2026-08-18 08:30 app/version.txt
drwxr-xr-x root/root 0 2026-08-18 08:30 app/templates/
-rw-r--r-- root/root 4218 2026-08-18 08:30 app/templates/confirmation.html
-rw-r--r-- root/root 2104 2026-08-18 08:30 app/templates/invoice.html48 MB compressed to 19 MB, a single root directory app/, permissions preserved (notice the -rwxr-xr-x on the executable) and not a single .log inside.
Step 5: generate a checksum so that Luis can verify that the file has arrived intact. It is the same mechanism you used when verifying the Ubuntu ISO in lesson 01-04.
operator@srv-tramontana:~$ cd /srv/tramontana/backups/outgoing
operator@srv-tramontana:/srv/tramontana/backups/outgoing$ sha256sum tramontana-app-3.2.1.tar.gz | sudo tee tramontana-app-3.2.1.tar.gz.sha256
9f2c4a1e8b3d7f60a5c2e1b4d8f3a70c9e2b5d1f8a4c7e0b3d6f9a2c5e8b1d4f tramontana-app-3.2.1.tar.gzWhen Luis receives it, he runs:
Step 6: a local extraction test. Check that the package opens properly before sending it, not after Luis says it does not work.
operator@srv-tramontana:~$ mkdir -p /tmp/verification
operator@srv-tramontana:~$ tar -xzf /srv/tramontana/backups/outgoing/tramontana-app-3.2.1.tar.gz -C /tmp/verification
operator@srv-tramontana:~$ diff -rq /opt/tramontana/app /tmp/verification/app
operator@srv-tramontana:~$ echo $?
0diff -rq compares two trees recursively and reports only the differences. No output and exit code 0: they are identical. You will see diff in detail in the next lesson.
Step 7: clean up.
A complete, reproducible, verified procedure. Transferring the file to Luis will be done with scp or rsync over SSH, which is lesson 06-02.
Common Mistakes and Tips
Forgetting -r when copying or deleting directories. cp: -r not specified; omitting directory and rm: cannot remove: Is a directory are the same warning.
The trailing slash. In cp it hardly matters; in rsync it decides whether you copy the folder or its contents. And in cp -r, the result depends on whether the destination already existed.
cp and mv overwrite silently. There is no confirmation by default. -i or -n when the destination may exist.
Copying configuration with cp instead of cp -p. You lose the dates, which are often the only clue as to when something was changed.
Packaging with absolute paths. tar -czf x.tar.gz /opt/tramontana/app stores the hierarchy opt/tramontana/app/ inside. Use -C and relative paths.
Extracting without listing first. tar -tzvf before tar -xzf, always.
zip for Linux files. It loses execution bits and ownership. tar for Linux, zip only for Windows.
Tip: rsync -n and rename -n before the real thing. Simulation mode is free and it shows you exactly what is going to happen.
Tip: name your packages with a version and a date. tramontana-app-3.2.1.tar.gz is informative; backup.tar.gz says nothing in three months' time.
Tip: always send a package together with its sha256sum. It costs one command and it eliminates a whole category of incidents.
Tip: before an rm -r, press Ctrl+A and type ls -la in front. You look, then press the up arrow and change it back to the rm. Ten seconds that have saved a lot of data.
Exercises
Exercise 1: setting up a working structure
In your home directory on srv-tramontana, with the minimum number of commands:
- Create the structure
work/2026/{07,08,09}/reportsandwork/2026/{07,08,09}/data. - Create a file
work/2026/08/reports/summary.txtwith a modification date of 1 August 2026 at 09:00. - Copy
~/data/bookings.csvtowork/2026/08/data/preserving its timestamps, and verify it. - Rename every
.txtinwork/2026/08/reports/to.mdwithout making a mistake (use simulation mode).
Exercise 2: reviewing Luis's command
Luis proposes this procedure for making a daily copy of the application before each deployment:
cp -r /opt/tramontana/app /srv/tramontana/backups/app-backup
tar czf /srv/tramontana/backups/app.tar.gz /opt/tramontana/appFind at least five problems and rewrite the procedure correctly.
Exercise 3: a data package for Marta
Marta needs to take the booking data and the list of houses to a meeting. She works on Windows and wants to open the files with Excel. She also asks that the package include a short report of what it contains.
Prepare the complete delivery: write the report, package it in the appropriate format, verify the contents of the package and generate the checksum. Justify your choice of compression format.
Solutions
Solution 1
operator@srv-tramontana:~$ mkdir -p work/2026/{07,08,09}/{reports,data}
operator@srv-tramontana:~$ tree work
work
└── 2026
├── 07
│ ├── data
│ └── reports
├── 08
│ ├── data
│ └── reports
└── 09
├── data
└── reportsA single command. Nested brace expansion generates the six combinations and -p creates all the parents. Without -p it would fail because work and work/2026 do not exist.
operator@srv-tramontana:~$ touch -t 202608010900.00 work/2026/08/reports/summary.txt
operator@srv-tramontana:~$ ls -l --time-style=long-iso work/2026/08/reports/
total 0
-rw-rw-r-- 1 operator operator 0 2026-08-01 09:00 summary.txttouch with -t creates the file and sets the timestamp in a single operation. The format is YYYYMMDDhhmm.ss.
operator@srv-tramontana:~$ stat -c '%n %y' data/bookings.csv
data/bookings.csv 2026-08-18 07:55:02.882910233 +0200
operator@srv-tramontana:~$ cp -p data/bookings.csv work/2026/08/data/
operator@srv-tramontana:~$ stat -c '%n %y' work/2026/08/data/bookings.csv
work/2026/08/data/bookings.csv 2026-08-18 07:55:02.882910233 +0200The timestamps match to the nanosecond. Without -p, the copy would have the current date. Notice that the destination ends in /: since the directory exists, the copy is made inside it keeping the name.
operator@srv-tramontana:~$ rename -n 's/\.txt$/.md/' work/2026/08/reports/*.txt
rename(work/2026/08/reports/summary.txt, work/2026/08/reports/summary.md)
operator@srv-tramontana:~$ rename 's/\.txt$/.md/' work/2026/08/reports/*.txt
operator@srv-tramontana:~$ ls work/2026/08/reports/
summary.mdThe \. escapes the dot so that it means a literal dot and not "any character", and the $ anchors the pattern to the end of the name, so that a file called notes.txt.old would not be affected. Both are regular expressions (lesson 03-02).
Solution 2
The problems with Luis's procedure:
1. Both commands overwrite the previous backup without warning. The names app-backup and app.tar.gz are fixed. Every day the previous day's copy is overwritten, so in reality only one copy exists: yesterday's. If the problem is detected two days later, there is nothing to go back to.
2. Worse still: it is overwritten before knowing whether the deployment goes well. If today's deployment fails and you have already crushed the good copy with a copy of the broken version, you have lost your point of return.
3. cp -r preserves neither permissions nor owners. The executable may lose its execution bit and everything ends up belonging to the user running the command. Restoring that copy would leave the application unable to start. It should be cp -a or, better, rsync -a.
4. tar czf with an absolute path. It stores the hierarchy opt/tramontana/app/ inside the package. On extracting you do not get app/ but three levels of directories, and tar warns with "Removing leading / from member names". -C is missing.
5. Two redundant copies of the same thing are made, one uncompressed (48 MB) and one compressed (19 MB), taking up 67 MB per deployment without either of them contributing anything the other does not have.
6. There is no verification. Nobody checks that the .tar.gz can be opened or that it contains what it should. A copy that has not been verified is not a copy: it is an assumption.
7. There is no space check at all. If /srv fills up halfway through the tar, you are left with a truncated file that looks like a valid copy.
The rewritten procedure:
#!/bin/bash
# Pre-deployment backup of Tramontana Bookings
# Usage: run BEFORE deploying
DATE=$(date +%F-%H%M)
VERSION=$(cat /opt/tramontana/app/version.txt | tr ' ' '-')
DEST=/srv/tramontana/backups/pre-deploy
PACKAGE="$DEST/app-${DATE}.tar.gz"
# 1. Make sure the destination exists (idempotent)
mkdir -p "$DEST" || exit 1
# 2. Check that there is enough space
df -h "$DEST"
# 3. Package with a unique name per date, preserving permissions
tar -czf "$PACKAGE" -C /opt/tramontana app || exit 1
# 4. Verify that the package can be read and what it contains
tar -tzf "$PACKAGE" > /dev/null || exit 1
echo "Contents:"
tar -tzvf "$PACKAGE"
# 5. Checksum
sha256sum "$PACKAGE" > "$PACKAGE.sha256"
echo "Backup created: $PACKAGE"Corrections applied:
- A name with the date and time: each run creates a different file. Nothing is ever overwritten.
- A single compressed package instead of two copies.
-C /opt/tramontana app: inside the package there is a single, cleanapp/.tarpreserves permissions and owners by design, so-ais not needed.|| exit 1at every critical step: if something fails, the script stops instead of carrying on as if nothing had happened. It is the&&/||of lesson 02-01 applied.- Verification with
tar -tzfbefore treating the copy as good. - A checksum to detect later corruption.
What it still lacks and will come in due course: rotation (deleting copies older than N days so that /srv does not grow forever), which is done with find -mtime in lesson 03-03; automation with cron (03-07); and a remote destination, because a copy on the same disk as the original does not protect against a hardware failure (05-08).
How to put it to Luis: his procedure goes through the motions of copying but meets none of the three properties of a useful backup — unique, verifiable and restorable. And the most serious one is the first: by using a fixed name, the good copy is destroyed just before you need it.
Solution 3
Choice of format: zip. Marta works on Windows and is going to open the files with Excel. Although on Linux tar.gz preserves metadata better, here that contributes nothing — these are data files, not executables — and it would mean Marta having to install a tool to open it. Windows opens .zip natively with a double click. Compatibility with the recipient outweighs technical purity.
# 1. Prepare the delivery directory
operator@srv-tramontana:~$ mkdir -p /tmp/for-marta
# 2. Copy the data preserving the dates
operator@srv-tramontana:~$ cp -p ~/data/bookings.csv ~/data/houses.txt /tmp/for-marta/
# 3. Write the contents report
operator@srv-tramontana:~$ cat > /tmp/for-marta/README.txt <<'END'
DATA PACKAGE - TRAMONTANA BOOKINGS
Generated on 2026-08-18 by the systems team
CONTENTS
--------
bookings.csv Registered bookings. Separator: semicolon (;)
Columns: id;date;house;guest;nights;amount
houses.txt List of available rural cottages
HOW TO OPEN bookings.csv IN EXCEL
---------------------------------
The file uses the semicolon as its separator. If everything
appears in a single column when you open it, use Data > Text to
Columns and select "semicolon" as the delimiter.
VERIFICATION
------------
The package includes a .sha256 file with the digital fingerprint.
END(That cat > file <<'END' is a here document: it writes the whole block into the file. It is explained in Module 3; here you use it as a tool.)
# 4. Package it as a zip, from inside the directory so as not to drag paths along
operator@srv-tramontana:~$ cd /tmp/for-marta
operator@srv-tramontana:/tmp/for-marta$ zip /tmp/tramontana-data-2026-08-18.zip *
adding: README.txt (deflated 48%)
adding: bookings.csv (deflated 67%)
adding: houses.txt (deflated 41%)
# 5. Verify the contents of the package
operator@srv-tramontana:/tmp/for-marta$ unzip -l /tmp/tramontana-data-2026-08-18.zip
Archive: /tmp/tramontana-data-2026-08-18.zip
Length Date Time Name
--------- ---------- ----- ----
782 2026-08-18 11:44 README.txt
1204 2026-08-18 07:55 bookings.csv
418 2026-08-17 19:40 houses.txt
--------- -------
2404 3 files
# 6. Checksum
operator@srv-tramontana:/tmp/for-marta$ cd /tmp
operator@srv-tramontana:/tmp$ sha256sum tramontana-data-2026-08-18.zip > tramontana-data-2026-08-18.zip.sha256
operator@srv-tramontana:/tmp$ cat tramontana-data-2026-08-18.zip.sha256
3a7f1c9e2b8d4056f1a3c7e9b2d5f804a6c1e3b7d9f2a5c8e0b4d7f1a3c6e9b2 tramontana-data-2026-08-18.zipDetails of the procedure that deserve comment:
cdinto the directory andzip *instead ofzip -r /tmp/x.zip /tmp/for-marta: this way the package contains the three files at its root, without thetmp/for-marta/hierarchy inside. It is the equivalent oftar's-C.cp -pkeeps the original dates of the data, visible in the zip listing. Marta can see thathouses.txtis from yesterday andbookings.csvfrom this morning.- The README answers in advance the question Marta would have asked over the phone: why the CSV looks wrong in Excel. The semicolon separator is the one
bookings.csvuses, and in an Excel with Spanish regional settings it usually works straight away, but it is worth documenting. - The checksum goes in a separate file, not inside the zip: it has to be verifiable without opening the package.
An important note for the real world: bookings.csv contains guest names, that is, personal data. Sending that file off the server by email or over an unencrypted channel has data protection implications. In a real environment, this delivery should be encrypted and should have the approval of the data protection officer. We will come back to file encryption in lesson 06-05.
Conclusion
You are writing to the disk now, and you are doing it with a safety net.
touchcreates empty files and manipulates timestamps, and itsctimealways gives the manipulation away.mkdir -pis idempotent and creates complete trees;rmdiris safe by design because it only deletes what is empty.cpoverwrites silently;-ais the option for faithful copies; and the trap of the trailing slash and the pre-existing destination produces different results from the same command.mvis instantaneous within the same file system because it only changes a directory entry, and it is copy-and-delete, slow and non-atomic, when it crosses into another.rmis irreversible,rm -rfwith an empty variable has wiped out entire systems, and the defences are looking first,-I, quotes and, above all, backups.rename -nrenames in batches with a prior simulation.rsync -avis the rightcpfor directories: incremental, verifiable, resumable, with--dry-runand with the trailing slash that really does matter.tarpackages while preserving permissions; you list with-tbefore extracting and use-Cso as not to drag absolute paths along.gzipby default,xzto distribute,zipfor Windows, and you can justify the choice.- And you have prepared a complete production package: verified, with a checksum and tested before sending it.
You now know how to move files around. What is missing is what is inside them. In the next lesson, Viewing and Editing Files, you will learn to look at the contents without opening an editor using cat, less, head and tail — including tail -F to follow errors.log live while an incident is happening — and to edit them with nano in depth and with the survival vim you will need the day you enter a server where there is nothing else. You will also establish a convention that the course will use from then on: back up the configuration file before touching it. And you will see how to compare two versions of app.conf with diff to find out exactly what changed, which is the question everybody asks after something stops working.
Linux Course: From Beginner to System Administrator
Module 1: Introduction to Linux
- What Is Linux?
- History of Linux
- Linux Distributions
- Installing Linux
- First Contact with the System
- The Linux File System Structure
Module 2: Basic Linux Commands
- Introduction to the Command Line
- Getting Help and System Documentation
- Navigating the File System
- File and Directory Operations
- Viewing and Editing Files
- Hard and Symbolic Links
- File Permissions and Ownership
Module 3: Advanced Command-Line Skills
- The Shell Environment: Variables, Aliases and History
- Using Wildcards and Regular Expressions
- Searching Files and Content: find, locate and grep
- Pipes and Redirection
- Text Processing: cut, sort, uniq, sed and awk
- Process Management
- Scheduling Tasks with Cron
- Networking Commands
Module 4: Shell Scripting
- Introduction to Shell Scripting
- Variables and Data Types
- Script Input, Output and Arguments
- Control Structures
- Functions and Libraries
- Debugging and Error Handling
- Production Scripts: Best Practices
Module 5: System Administration
- User and Group Management
- sudo and Special Permissions
- Package Management
- Disk Management
- systemd and Service Management
- System Logs: journald and syslog
- System Monitoring and Performance Tuning
- Backup and Restore
Module 6: Networking and Security
- Network Configuration
- SSH and Remote Access
- Firewalls and Perimeter Security
- Intrusion Detection Systems
- Secrets Management and TLS Certificates
- Securing Linux Systems
Module 7: Advanced Topics
- The Boot Process and System Recovery
- Advanced Diagnostics: strace, perf and eBPF
- Linux Kernel Tuning
- Virtualization with Linux
- Linux Containers and Docker
- Automation with Ansible
- High Availability and Load Balancing
