Home / Learn Linux / Lesson 11
Archives and links: tar, gzip, hard links and symlinks
Pack and compress folders, and the difference between a hard link and a symlink.
- Lesson
- 11 of 30
- Time
- About 40 minutes
On this page
- Set up
- Archive or compress?
- tar: create, list, extract
- Which compression?
- gzip on its own
- Inodes: what a file really is
- Hard links: two names, one file
- Symbolic links: a signpost
- Common mistakes
- Practice
- Cheat sheet
Set up
Make something worth packing: a small project with a config folder, a big text file and a few notes. seq makes a file of a million numbered lines, which is about 6.6 MB of very repetitive text, the kind that compresses well:
$ mkdir -p ~/arc-lab/project/{config,data,notes} $ cd ~/arc-lab $ cp /etc/apt/sources.list.d/ubuntu.sources project/config/ $ seq 1 1000000 > project/data/numbers.txt $ echo "remember to rotate keys" > project/notes/todo.txt $ du -sh project 6.6M project
du -sh shows how much space a directory takes in total (lesson 25 covers it).
Archive or compress?
These are two separate jobs, done by separate tools:
- Archiving packs many files and directories into one file, keeping names, permissions and dates. That's
tar(tape archive, from when backups went to tape). - Compressing makes one file smaller. That's
gzip,xzorzstd.
tar can call a compressor for you, so in practice you do both at once and get a .tar.gz, also written .tgz.
tar: create, list, extract
tar takes a few letters that say what to do. You'll use these three combinations almost every time:
| Command | Does |
|---|---|
tar -czf name.tar.gz dir | Create, gzip it, into this file |
tar -tzf name.tar.gz | List what's inside |
tar -xzf name.tar.gz | Extract it here |
Add v (verbose) to see each file as it goes:
$ tar -czvf project.tar.gz project project/ project/config/ project/config/ubuntu.sources project/notes/ project/notes/todo.txt project/data/ project/data/numbers.txt $ ls -lh project.tar.gz -rw-rw-r-- 1 ubuntu ubuntu 2.1M Oct 21 11:37 project.tar.gz
6.6 MB went down to about 2 MB. Look inside without extracting:
$ tar -tzf project.tar.gz
project/
project/config/
project/config/ubuntu.sources
project/notes/
project/notes/todo.txt
project/data/
project/data/numbers.txtExtract it somewhere else with -C, which tells tar which directory to work in:
$ mkdir restore $ tar -xzf project.tar.gz -C restore $ tree restore restore └── project ├── config │ └── ubuntu.sources ├── data │ └── numbers.txt └── notes └── todo.txt 5 directories, 3 files
You can also pull out a single file by naming it exactly as tar -t lists it:
$ tar -xzf project.tar.gz -C restore project/notes/todo.txt $ cat restore/project/notes/todo.txt remember to rotate keys
And leave things out with --exclude:
$ tar -czf small.tar.gz --exclude="*.txt" project $ tar -tzf small.tar.gz project/ project/config/ project/config/ubuntu.sources project/notes/ project/data/
A note on paths: tar stores whatever path you give it. If you archive /home/ubuntu/arc-lab/project, it strips the leading / and stores home/ubuntu/arc-lab/project/..., so extracting it recreates that whole chain of directories. cd to the parent first, or use -C, and archive the short relative name instead.
Try it
- Archive
/etc/aptintoapt-backup.tar.gzin~/arc-lab, storing the paths asapt/...rather thanetc/apt/.... - List the archive and count how many entries it has.
Answer
$ tar -czf apt-backup.tar.gz -C /etc apt $ tar -tzf apt-backup.tar.gz | wc -l 28
-C /etc makes tar work from /etc, so the stored name is just apt.
Which compression?
tar supports several compressors. Swap the z for another letter:
| Option | Compressor | Extension | Character |
|---|---|---|---|
-z | gzip | .tar.gz | Fast, works everywhere |
-J | xz | .tar.xz | Smallest, slowest |
--zstd | zstd | .tar.zst | Very fast and nearly as small as xz |
$ time tar -czf p.tar.gz project real 0m0.220s user 0m0.219s sys 0m0.006s $ time tar -cJf p.tar.xz project real 0m1.916s user 0m1.895s sys 0m0.026s $ time tar --zstd -cf p.tar.zst project real 0m0.030s user 0m0.027s sys 0m0.006s $ ls -l p.tar.* -rw-rw-r-- 1 ubuntu ubuntu 2131841 Oct 21 11:37 p.tar.gz -rw-rw-r-- 1 ubuntu ubuntu 327568 Oct 21 11:37 p.tar.xz -rw-rw-r-- 1 ubuntu ubuntu 271761 Oct 21 11:37 p.tar.zst
time shows how long each one took: real is the wall-clock time. On this very repetitive file, xz squeezed 6.6 MB down to under 100 KB but took about ten times as long as gzip, and zstd was both the fastest and far smaller than gzip. Real data compresses less than a list of numbers, so your sizes will differ, but the ranking usually holds: xz smallest, zstd fastest, gzip the one every system can open. When extracting, recent versions of GNU tar detect the compression by themselves, so tar -xf file works for all three.
gzip on its own
gzip compresses single files and replaces the original. Add -k to keep it:
$ cp project/data/numbers.txt n.txt $ gzip -k n.txt $ ls -lh n.txt n.txt.gz -rw-rw-r-- 1 ubuntu ubuntu 6.6M Oct 21 11:37 n.txt -rw-rw-r-- 1 ubuntu ubuntu 2.1M Oct 21 11:37 n.txt.gz $ gunzip -f n.txt.gz
gunzip reverses it (-f overwrites the existing n.txt). You'll meet .gz files mostly in /var/log, where old logs are compressed after rotation. You don't have to unpack them to read them: zcat, zless and zgrep work on .gz files directly:
$ ls /var/log/*.gz /var/log/dmesg.1.gz /var/log/dmesg.2.gz $ zcat /var/log/dmesg.1.gz | head -n 3 [ 0.229765] kernel: Booting Linux on physical CPU 0x0000000000 [0x610f0000] [ 0.229780] kernel: Linux version 7.0.0-28-generic (buildd@bos03-arm64-120) (aarch64-linux-gnu-gcc (Ubuntu 15.2.0-16ubuntu1) 15.2.0, GNU ld (GNU Binutils for Ubuntu) 2.46) #28-Ubuntu SMP PREEMPT_DYNAMIC Sun Jun 21 00:50:10 UTC 2026 (Ubuntu 7.0.0-28.28-generic 7.0.12) [ 0.229785] kernel: KASLR enabled
zip, for Windows people
Windows users send .zip files. Ubuntu server doesn't include the tools, so install them, then zip -r packs a directory and unzip unpacks:
$ zip -rq project.zip project $ unzip -l project.zip Archive: project.zip Length Date Time Name --------- ---------- ----- ---- 0 2026-10-21 11:37 project/ 0 2026-10-21 11:37 project/config/ 3000 2026-10-21 11:37 project/config/ubuntu.sources 0 2026-10-21 11:37 project/notes/ 24 2026-10-21 11:37 project/notes/todo.txt 0 2026-10-21 11:37 project/data/ 6888896 2026-10-21 11:37 project/data/numbers.txt --------- ------- 6891920 7 files
sudo apt install -y zip unzip installs them. -q keeps zip quiet, and unzip -l lists without extracting.
Try it
- Make an xz-compressed archive of
project/configand list it. - Search the compressed log
/var/log/dmesg.1.gzfor the wordLinuxwithout unpacking it, showing only the first match.
Answer
$ tar -cJf config.tar.xz project/config $ tar -tf config.tar.xz project/config/ project/config/ubuntu.sources $ zgrep -m 1 Linux /var/log/dmesg.1.gz [ 0.229765] kernel: Booting Linux on physical CPU 0x0000000000 [0x610f0000]
Inodes: what a file really is
To understand links, you need one more idea. A file's data and details (owner, permissions, size, times, where its blocks are on disk) live in an inode, a numbered record. The file's name is just an entry in a directory that points to an inode number. ls -i shows those numbers:
$ ls -i project/notes/todo.txt 534161 project/notes/todo.txt $ stat -c "inode %i, %h link(s)" project/notes/todo.txt inode 534161, 1 link(s)
Names and inodes being separate is why mv is instant inside one disk (it only changes the name), and why there are two kinds of link.
Hard links: two names, one file
ln target newname makes a hard link: a second name pointing to the same inode. Neither name is the original. They're equally real:
$ echo "version 1" > a.txt $ ln a.txt b.txt $ ls -li a.txt b.txt 534180 -rw-rw-r-- 2 ubuntu ubuntu 10 Oct 21 11:37 a.txt 534180 -rw-rw-r-- 2 ubuntu ubuntu 10 Oct 21 11:37 b.txt $ echo "version 2" >> b.txt $ cat a.txt version 1 version 2
Same inode number, and the link count (third column) went up to 2. A change through either name shows up in both, because there's only one file. Deleting a name only removes that name. The data stays until the last name is gone:
$ rm a.txt $ cat b.txt version 1 version 2 $ ls -li b.txt 534180 -rw-rw-r-- 1 ubuntu ubuntu 20 Oct 21 11:37 b.txt
Two limits: a hard link can't point to a directory, and it can't cross to a different filesystem (another disk), because inode numbers only mean something within one filesystem.
Symbolic links: a signpost
ln -s target newname makes a symbolic link (symlink): a tiny file that just contains a path. Open the link and Linux follows the path to the real file. This is the kind you saw in lesson 4 with /bin -> usr/bin:
$ ln -s project/notes/todo.txt todo-link $ ls -l todo-link lrwxrwxrwx 1 ubuntu ubuntu 22 Oct 21 11:37 todo-link -> project/notes/todo.txt $ cat todo-link remember to rotate keys
The l at the start and the -> arrow show it's a link and where it points. Symlinks can point to directories and across disks, which is why they're everywhere. But they point by name, so if the target moves or is deleted, the link breaks:
$ ln -s missing.txt broken-link $ cat broken-link cat: broken-link: No such file or directory $ ls -l broken-link lrwxrwxrwx 1 ubuntu ubuntu 11 Oct 21 11:37 broken-link -> missing.txt
ls still shows a broken link, and find -xtype l finds every one of them under a directory.
A relative target, like project/notes/todo.txt, is relative to where the link is, not where you were when you made it. That's the most common reason a new symlink is broken. When in doubt, use an absolute path, or check with readlink -f, which prints the final real path:
$ readlink -f todo-link /home/ubuntu/arc-lab/project/notes/todo.txt $ readlink -f /bin/ls /usr/lib/cargo/bin/coreutils/ls
The current release trick
Deployment tools keep each version of an app in its own directory and point a current symlink at the live one. Switching versions, or rolling back, is changing one link. -f replaces an existing link and -n treats an existing link to a directory as a link rather than going inside it:
$ mkdir -p releases/v1 releases/v2 $ ln -s releases/v1 current $ ls -l current lrwxrwxrwx 1 ubuntu ubuntu 11 Oct 21 11:37 current -> releases/v1 $ ln -sfn releases/v2 current $ ls -l current lrwxrwxrwx 1 ubuntu ubuntu 11 Oct 21 11:37 current -> releases/v2
Hard or soft?
| Hard link | Symlink | |
|---|---|---|
| Points to | The inode (the data) | A path (a name) |
| Directories | No | Yes |
| Across disks | No | Yes |
| If the original name is deleted | Still works | Breaks |
ls -l shows | A normal file, link count above 1 | l and -> |
In everyday admin work you'll make symlinks far more often. Hard links show up in backups, where tools use them so that unchanged files in each daily snapshot don't take extra space.
Try it
- Make a symlink called
logsin~/arc-labpointing to/var/log, then listlogs/aptthrough it. - Make a hard link to
b.txtcalledc.txt, then check the link count. - Make a symlink to a file that doesn't exist, then find it with
find.
Answer
$ ln -s /var/log logs $ ls logs/apt eipp.log.xz history.log term.log $ ln b.txt c.txt $ stat -c "%h links" b.txt 2 links $ ln -s nowhere.txt dead $ find . -xtype l ./broken-link ./dead
Common mistakes
- Archiving with absolute paths. You extract a whole
home/ubuntu/...tree. Use-Corcdto the parent first. - Extracting into the wrong place.
tar -xwrites into the current directory and overwrites files with the same names. List with-tfirst, and extract into a new directory with-C. - **Mixing up
ln's order.** It'sln -s TARGET LINKNAME, the same order ascp: what exists first, the new thing second. - Relative symlinks made from the wrong directory. The target is resolved from the link's directory. Check with
readlink -f. - Thinking a hard link is a copy. It's the same file, so editing one changes the other. Use
cpfor a copy.
Practice
- Back up the whole
projectdirectory asproject-YYYY-MM-DD.tar.gzusing today's date, then show its size.
Answer
$ tar -czf project-$(date +%F).tar.gz project $ ls -lh project-*.tar.gz -rw-rw-r-- 1 ubuntu ubuntu 2.1M Oct 21 11:37 project-2026-10-21.tar.gz
- Extract only the
configdirectory fromproject.tar.gzinto a new directory calledonly-config.
Answer
$ mkdir only-config $ tar -xzf project.tar.gz -C only-config project/config $ tree only-config only-config └── project └── config └── ubuntu.sources 3 directories, 1 file
- Archive
projectbut leave out thedatadirectory.
Answer
$ tar -czf no-data.tar.gz --exclude=project/data project $ tar -tzf no-data.tar.gz project/ project/config/ project/config/ubuntu.sources project/notes/ project/notes/todo.txt
- Count the lines in
/var/log/dmesg.1.gzwithout unpacking it.
Answer
$ zcat /var/log/dmesg.1.gz | wc -l
613- Create
releases/v3, pointcurrentat it, then roll back tov2, showing the link after each step.
Answer
$ mkdir releases/v3 $ ln -sfn releases/v3 current $ ls -l current lrwxrwxrwx 1 ubuntu ubuntu 11 Oct 21 11:37 current -> releases/v3 $ ln -sfn releases/v2 current $ ls -l current lrwxrwxrwx 1 ubuntu ubuntu 11 Oct 21 11:37 current -> releases/v2
- Show that
b.txtandc.txtare the same file using inode numbers, then deleteb.txtand show thatc.txtstill has the contents.
Answer
$ ls -i b.txt c.txt 534180 b.txt 534180 c.txt $ rm b.txt $ cat c.txt version 1 version 2
Cheat sheet
| Command | What it does |
|---|---|
tar -czf out.tar.gz dir | Create a gzip archive (v to list files as it goes) |
tar -tzf file.tar.gz | List the contents |
tar -xzf file.tar.gz -C dest | Extract into dest |
tar -xzf file.tar.gz path/in/archive | Extract one file |
--exclude="*.log" | Leave things out |
-J, --zstd | xz or zstd instead of gzip |
gzip -k file, gunzip file.gz | Compress (keep original), decompress |
zcat, zless, zgrep | Read .gz files directly |
zip -r out.zip dir, unzip file.zip | Zip files (install zip unzip) |
ls -i, stat -c %i | Show inode numbers |
ln target name | Hard link |
ln -s target name | Symbolic link |
ln -sfn target name | Repoint an existing link |
readlink -f link | Where it really ends up |
find . -xtype l | Find broken symlinks |
← Editing files with VimEditing text with sed and regular expressions →