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

  1. touch: creating and touching timestamps
  2. mkdir and rmdir: directory structure
  3. cp: copying, and the trailing-slash trap
  4. mv: moving and renaming
  5. rm: the serious conversation
  6. Batch renaming with rename
  7. rsync: the right way to copy directories
  8. tar: packaging
  9. Compression: gzip, bzip2, xz and zip
  10. Practice: preparing the package for Luis

  1. touch: creating and touching timestamps

touch 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.txt

Zero bytes: it exists but it is empty. It accepts several arguments:

operator@srv-tramontana:~$ touch note1.txt note2.txt note3.txt

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.txt

Notice 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 +0200

The 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.

  1. mkdir and rmdir: directory structure

operator@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 $?
0

That 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 private

It 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
└── august

Those 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 directory

That 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 directory

It deleted august, then 2026 because it was left empty, and then reports.

  1. cp: copying, and the trailing-slash trap

operator@srv-tramontana:~$ cp data/bookings.csv data/bookings-copy.csv
operator@srv-tramontana:~$ ls data/
bookings-copy.csv  bookings.csv  houses.txt

The 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;6

bookings.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'? n

The 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/h2

The 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.txt

In 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.txt

The 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.txt

With 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.

  1. mv: moving and renaming

mv 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.txt

The 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. mv is 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.

  1. rm: the serious conversation

rm removes files. On a mounted, in-use file system, it does so irreversibly.

operator@srv-tramontana:~$ rm copy.txt
operator@srv-tramontana:~$ rm note1.txt note2.txt note3.txt
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)
operator@srv-tramontana:~$ rm -rI backup3
rm: remove 1 argument recursively? y

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

rm -rf "$DEST/$SUBDIR"

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:

rm -rf "/"

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 failsafe

But 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-restore

It 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.

  1. Batch renaming with rename

Renaming 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.

  1. rsync: the right way to copy directories

rsync 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.00

A 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.38

162 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.

  1. tar: packaging

tar (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.html

A breakdown of the command, which is the important part:

  • -c create, -z compress with gzip, -v show, -f followed by the name of the resulting file.
  • -C /opt/tramontana makes tar position itself there before packaging. Without this, it would package opt/tramontana/app/... with the whole hierarchy inside.
  • app is 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.html

Why 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.txt

Always 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 app

And 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.

  1. Compression: gzip, bzip2, xz and zip

The 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.gz

data.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 -d

A 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-zip

Its 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

  1. 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.txt

Step 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.

operator@srv-tramontana:~$ mkdir -p /srv/tramontana/backups/outgoing

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.html

Step 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.html

48 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.gz

When Luis receives it, he runs:

luis@laptop-luis:~$ sha256sum -c tramontana-app-3.2.1.tar.gz.sha256
tramontana-app-3.2.1.tar.gz: OK

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 $?
0

diff -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.

operator@srv-tramontana:~$ rm -rI /tmp/verification
rm: remove 1 argument recursively? y

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:

  1. Create the structure work/2026/{07,08,09}/reports and work/2026/{07,08,09}/data.
  2. Create a file work/2026/08/reports/summary.txt with a modification date of 1 August 2026 at 09:00.
  3. Copy ~/data/bookings.csv to work/2026/08/data/ preserving its timestamps, and verify it.
  4. Rename every .txt in work/2026/08/reports/ to .md without 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/app

Find 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
        └── reports

A 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.txt

touch 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 +0200

The 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.md

The \. 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, clean app/.
  • tar preserves permissions and owners by design, so -a is not needed.
  • || exit 1 at 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 -tzf before 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.zip

Details of the procedure that deserve comment:

  • cd into the directory and zip * instead of zip -r /tmp/x.zip /tmp/for-marta: this way the package contains the three files at its root, without the tmp/for-marta/ hierarchy inside. It is the equivalent of tar's -C.
  • cp -p keeps the original dates of the data, visible in the zip listing. Marta can see that houses.txt is from yesterday and bookings.csv from 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.csv uses, 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.

  • touch creates empty files and manipulates timestamps, and its ctime always gives the manipulation away.
  • mkdir -p is idempotent and creates complete trees; rmdir is safe by design because it only deletes what is empty.
  • cp overwrites silently; -a is the option for faithful copies; and the trap of the trailing slash and the pre-existing destination produces different results from the same command.
  • mv is 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.
  • rm is irreversible, rm -rf with an empty variable has wiped out entire systems, and the defences are looking first, -I, quotes and, above all, backups.
  • rename -n renames in batches with a prior simulation.
  • rsync -av is the right cp for directories: incremental, verifiable, resumable, with --dry-run and with the trailing slash that really does matter.
  • tar packages while preserving permissions; you list with -t before extracting and use -C so as not to drag absolute paths along.
  • gzip by default, xz to distribute, zip for 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

Module 2: Basic Linux Commands

Module 3: Advanced Command-Line Skills

Module 4: Shell Scripting

Module 5: System Administration

Module 6: Networking and Security

Module 7: Advanced Topics

Module 8: Practical Projects

© Copyright 2026. All rights reserved