Module 2: Working with Files and Text

3. Copying, moving, and deleting without destroying anything

Description

By the end of this lesson you will be able to copy files and entire folders with cp, move and rename with mv while understanding why in Unix they are literally the same command, and delete with rm, rm -r, and rm -rf applying explicit safety judgment instead of typing the command and praying. You will also know what to do at the worst possible moment: right after you pressed Enter and the file you just deleted was the one you did not have backed up.

This is the lesson where the terminal stops being harmless. Up to now you created structure (mkdir, touch) and there is not much to break by creating empty folders. Reorganizing a project, cleaning a build folder before a deployment, or preparing a delivery directory for a client are everyday tasks in any job that uses the terminal — and all three involve moving or deleting things that actually matter. The cost of a slip here is not an error message: it is a file that no longer exists anywhere.

Connection to the module: the previous lesson gave you the tools to create structure (mkdir -p, touch, brace expansion). This lesson gives you the tools to reorganize and clean it up — exactly what you are going to need in the module's final project, where you are going to audit someone else's directory and leave it in order.

A path is a label, not the file

In a big warehouse, every box has a label stuck to it with its location: "shelf 4, row B." If an employee moves the box from shelf 4 to shelf 7, they do not destroy the box or manufacture a new one — they peel off the old label and stick on a new one. The content never got touched. And if someone tears off the label without moving the box, the box is still physically there, but nobody with only the label list is going to find it again.

That is almost exactly what happens with your files. A file name is not the file: it is an entry in a directory that points to where the real content lives on disk. With that idea, this lesson's three commands stop being three loose things to memorize and become a single idea applied three times:

  • cp (copy) creates new content with a new label. Now there are two boxes.
  • mv (move) changes an existing box's label — or moves it to another shelf. The content never gets duplicated.
  • rm (remove) tears off the label. If no other label points to that box, the space becomes available for the system to reuse — but nobody is going to hand you the content back by calling it by its old name.

That last sentence is what shapes this entire lesson: the terminal has no recycle bin by default. When you delete, you delete.

Worked example: preparing a backup, renaming a delivery, and cleaning up afterward

Suppose you have this project for a client, built with what you learned in the previous lesson:

mkdir -p client-a/src client-a/exports
touch client-a/notes.txt client-a/src/app.py
touch client-a/exports/report-draft.pdf client-a/exports/report-final.pdf
ls -R client-a

What to expect:

client-a:
exports		notes.txt	src

client-a/exports:
report-draft.pdf	report-final.pdf

client-a/src:
app.py

Before making a risky change (for example, reorganizing exports/), you make a full backup copy with cp -r (-r for recursive: without it, cp refuses to copy a directory):

cp -r client-a client-a-backup
ls -R client-a-backup

What to expect: the same full structure, now under client-a-backup/exports/, notes.txt, and src/app.py copied byte for byte. Since client-a-backup did not exist yet, cp simply creates it as a full copy; on this, macOS and Linux behave the same.

cp does not blindly overwrite by default in most interactive setups, but the explicit way to be sure is -i (interactive):

cp -i client-a/notes.txt client-a-backup/notes.txt

What to expect:

overwrite client-a-backup/notes.txt? (y/n [n])

If you answer n (or just press Enter, which defaults to n), the old copy stays intact. It is the same logic you are going to use with mv and rm via -i: before destroying something, the command shows you exactly what it is about to overwrite and waits for your confirmation.

With the backup already made, you rename the export folder once the client approved the final version:

mv client-a/exports client-a/deliverables
ls client-a

What to expect:

deliverables	notes.txt	src

exports no longer exists as a name; deliverables contains exactly the same two files, with the same modification dates. Not a single byte got copied — mv only rewrote the directory entry. That is why it is instant no matter whether that folder weighs one kilobyte or a hundred gigabytes: as long as source and destination are on the same filesystem (the same disk), moving is pure label-swapping. Only when you move something between different disks — your internal disk and a USB drive, for example — does mv have to actually copy the content and then delete the original, and only then does the time depend on the size.

Finally, with the backup verified and no longer needed, you delete it. Before deleting, you list exactly what you are about to delete — the first of the three safety rules you see in detail below:

ls client-a-backup
rm -r client-a-backup
ls client-a-backup

What to expect: the first ls shows the backup's full content — your last chance to confirm it is what you think it is. rm -r does not print anything if it succeeds (silence means success in the terminal). The second ls confirms it no longer exists:

ls: client-a-backup: No such file or directory

No confirmation, no recycle bin, no undo. rm -r did exactly what you asked it to.

The trailing slash in cp: the same command, two different results

There is a detail about cp -r that surprises almost everyone the first time, and it depends on your operating system. You already saw that if the destination does not exist yet, cp -r source destination simply creates destination as a full copy — there is no ambiguity there, and it is the same on macOS or Linux.

The problem shows up when the destination already exists as a folder. There, it matters whether the source ends in / or not:

Command (destination already exists)macOS / BSDLinux / GNU
cp -r deliverables client-a-backup (no slash)Nests: client-a-backup/deliverables/...Nests: client-a-backup/deliverables/...
cp -r deliverables/ client-a-backup (with slash)Dumps the content directly: client-a-backup/report-draft.pdfSame as without the slash: client-a-backup/deliverables/...

macOS's cp manual states it literally: "If the source_file ends in a /, the contents of the directory are copied rather than the directory itself." GNU cp (the one on any Linux, including WSL2 with Ubuntu) ignores that slash entirely: with or without it, the result is identical.

In practice, this means a command you tested on your Mac and that worked can behave differently on the Linux server you SSH into — with the exact same line, letter for letter. The way to not depend on memory is to always run an ls after copying and verify the tree came out as expected, instead of assuming. If you really need the behavior to be identical on both systems, the portable way to say "I want the content, not the folder" is to add a dot: cp -r deliverables/. client-a-backup/.

The three rules before typing "rm -rf"

rm does not ask, has no recycle bin, and has no undo — unlike dragging a file to the trash in a graphical interface, here there is no built-in second chance. That does not mean it is unpredictable: it means safety depends on a habit of yours, not on the program. There are three rules, and all three take three seconds to apply.

Rule 1 — List with ls exactly what you are about to delete, before deleting it. If you are going to run rm -r client-a-backup, run ls client-a-backup first (or ls -R if you want to see the whole tree) and read the result. If you are going to delete with a pattern, run ls with that same pattern first: ls *.tmp before rm *.tmp. The goal is not to memorize the rule — it is to make the pattern match, in your head, what is really on disk before it becomes irreversible.

Rule 2 — Do not use wildcards blindly. A wildcard (*, as in rm -r build/*) gets expanded by the shell before rm ever sees anything — by the time rm receives the command, there is no * left, there is a literal list of file names. If that expansion is not what you imagined (because you were in the wrong directory, because the pattern matched more than you thought), rm is going to delete exactly that list without complaining. GNU rm on Linux ships a protection for the extreme case: by default it refuses to delete recursively if the literal argument is /, with the message it is dangerous to operate recursively on '/'. But that protection does not save you from rm -rf /* — there, the shell already expanded /* into a list of folders (/bin /etc /home /usr ...) before rm ever saw it, so it never receives a literal / and the protection never kicks in. The real defense is not the software: it is running ls with that same pattern first.

Rule 3 — Never leave a variable unquoted inside an rm, and check that it is not empty before using it. These are two separate protections, not one:

  • Quotes, so a value with spaces does not split into several arguments. Without quotes, rm -rf $file with file="final report.txt" tries to delete two things: a file or folder named final and another named report.txt — almost never what you wanted.
  • Checking it is not empty, because quotes alone do not save you from an undefined variable. In January 2015, Steam's Linux installer had a line similar to rm -rf "$STEAMROOT/"*. When an earlier bug left $STEAMROOT empty, that line — quotes and all — turned into rm -rf "/"*, and it deleted everything the user had permission to delete on their account, starting from the system root. The quotes prevented the spaces problem; they did not prevent the empty-variable problem.
# BAD: if $BUILD_DIR was never defined, this tries to delete "/*"
rm -rf $BUILD_DIR/*

# GOOD: quotes against spaces, and an explicit check against empty
if [ -z "$BUILD_DIR" ]; then
  echo "Error: BUILD_DIR is not defined. Aborting." >&2
  exit 1
fi
rm -rf "$BUILD_DIR"/*

(The shorter, more elegant form of that same check, with set -euo pipefail and trap, you are going to see once you write your own scripts later in the guide — for now, the explicit if is enough and reads with no tricks.)

A recycle bin from the terminal: trash-cli (and its real limits)

If rm's risk makes you uncomfortable, there is a real alternative: sending files to a trash can instead of deleting them permanently. On Linux, trash-cli (installed with pip install trash-cli) adds the commands trash-put (sends to trash instead of deleting), trash-list (what is inside), and trash-restore (recover something). On macOS, the equivalent alternative is called trash (brew install trash) and uses Finder's native trash, the same one you see if you open a graphical window.

trash-put old-report.txt      # Linux, with trash-cli installed
trash old-report.txt          # macOS, with trash installed

That said, it is worth being honest about how much these actually get used in practice: almost nobody has them installed on a remote server, and none of them come preinstalled by default. On a machine you administer yourself and use every day, it can be worth it. On a server you SSH into for a specific task, you are going to run into the usual rm, with no safety net — which is exactly why the three rules from the previous section matter more than any tool you install.

If you already deleted something important

If the file is gone and you have no copy, there is a sequence that maximizes your chances, from most to least likely to work:

  1. Stop writing to that disk. Do not create new files, do not install anything, do not keep working in that folder. When rm deletes a file, the space it occupied gets marked as available but the data is not overwritten instantly — any new write can reuse exactly those blocks and destroy the only real chance of recovery.
  2. Look for a copy you already had. A Time Machine backup (macOS), a filesystem snapshot, a cloud sync, or simply a git status if the file was version-controlled. This solves the problem in 90% of real cases and is infinitely more reliable than any recovery tool.
  3. If a program still has the file open, on Linux you can copy its content from /proc/<pid>/fd/ before that process closes it — as long as some process keeps it open, the data is still alive even though the name has already vanished.
  4. As a last resort, there are disk-level recovery tools (extundelete, testdisk, photorec), but they guarantee nothing: they depend on the filesystem, on whether the space has already been reused, and on modern SSD drives the drive itself can physically erase blocks marked as free (TRIM) long before you notice.

The honest conclusion is the same as in the previous section: real protection does not arrive after the deletion. It arrives before, with the three rules and with a backup that already existed by the time you needed it.

Common mistakes

Thinking mv copies and then deletes, always. It is this lesson's most common conceptual misunderstanding, and it is reasonable to land on it because mv really does that — but only when source and destination are on different disks. Within the same filesystem, mv never copies a single byte: it only rewrites a directory entry, which is why it is instant no matter the size. How to spot the misunderstanding: if you expect moving a 20 GB folder to take a while and it finishes instantly, your mental model was wrong, not the command. How to fix it: remember the label and the box — mv changes the label; it only copies content for real when the box has to cross to a shelf in another building (another disk).

Forgetting -r when copying or deleting a folder. cp folder destination and rm folder fail with messages like cp: folder is a directory (not copied) or rm: folder: is a directory. It is not a bug: cp and rm deliberately require the recursive flag, as a built-in brake before an operation that can touch hundreds of files at once. How to spot it: the error message says it explicitly ("is a directory"). How to fix it: add -r (or -R) — but before adding it without thinking, that is the exact moment to apply Rule 1 and confirm with ls that this folder really is the one you want to touch.

Trusting a wildcard without confirming which folder you are standing in. The classic disaster is running rm -rf * thinking you are inside build/ when actually an earlier cd failed silently (say, you misspelled the name) and you are still in the full project folder. rm has no way of knowing you got the location wrong — it only sees the expanded pattern and acts. How to spot it: by checking pwd and ls before the rm, not after. How to fix it: the habit of chaining pwd && ls (or directly ls with the same pattern as the rm) immediately before any wildcard deletion, no exceptions — it is the same Rule 1, applied to the location and not just the content.

Exercises

Exercise 1. You have this folder:

reports/
├── summary.txt
└── charts/
    └── q1.png

You run this command twice in a row, without deleting anything in between:

cp -r reports archive
cp -r reports archive

What does archive contain after the second time? Write the resulting tree.

See solution

After the first command, archive did not exist, so it gets created as a full copy: archive/summary.txt and archive/charts/q1.png.

After the second command, archive already exists as a folder — and the source (reports, no trailing slash) does not have one. That means it nests: cp copies the entire reports inside archive. The final tree is:

archive/
├── summary.txt
├── charts/
│   └── q1.png
└── reports/
    ├── summary.txt
    └── charts/
        └── q1.png

Why it works this way: when the destination already exists as a directory and the source does not end in /, both macOS and Linux nest the entire source inside the destination — it is the only one of the lesson table's two cases where both systems agree unconditionally.

Exercise 2. You find this line in a cleanup script someone else wrote:

rm -rf $TEMP_DIR/*

a) What happens if the variable $TEMP_DIR was never defined (for example, because of a typo earlier in the script)?

b) Rewrite the line applying the full Rule 3.

See solution

a) If $TEMP_DIR is empty, the shell substitutes it with nothing before running the command. $TEMP_DIR/* becomes /*, so the actual command that runs is rm -rf /* — which recursively deletes everything the process has permission to delete starting from the root. It is exactly the same mechanism as the 2015 Steam incident.

b)

if [ -z "$TEMP_DIR" ]; then
  echo "Error: TEMP_DIR is not defined. Aborting." >&2
  exit 1
fi
rm -rf "$TEMP_DIR"/*

Why it works: the -z check stops the script before rm sees anything if the variable is empty, and the quotes around "$TEMP_DIR" keep a value with spaces from splitting into several arguments. They are two separate protections and both are needed.

Exercise 3. You have client-a-draft/ (several files, already approved by the client) and client-a-old-exports/ (an old folder you no longer need). Write, in order, the commands to: (1) rename client-a-draft to client-a-final, and (2) delete client-a-old-exports applying Rule 1. Then explain in one sentence why step 1 finishes instantly no matter how many gigabytes the folder weighs.

See solution
mv client-a-draft client-a-final

ls client-a-old-exports
rm -r client-a-old-exports

Why the rename is instant: client-a-draft and client-a-final are on the same filesystem (the same disk), so mv does not copy a single byte of content — it only changes the directory entry that points to the same content it always did. The folder's size never comes into play because that content is never read or rewritten.

Summary and next step

Before moving on, you should be able to copy an entire folder with cp -r, predict whether it is going to nest or dump based on whether the destination already exists and your operating system, rename and move with mv explaining why it is instant within the same disk, and delete with rm -r or rm -rf having listed beforehand exactly what you were about to delete.

You now know how to create structure and how to reorganize and destroy it with judgment. What you still do not have is a way to read what is inside a file without opening an editor — and that matters because in the next lesson you are going to run into files that do not fit on a single screen, something no command in this lesson solves. cat, less, head, and tail are exactly that missing piece.

Resources