Create, Copy, Move and Delete
Problem statement
Build a small release folder from the command line: create folders and files, copy them, rename one, and delete what you no longer need. Engineers do this every day when they prepare a deploy, back up a config before editing it, or clean up a server.
The script starts in an empty folder with two files:
app.conf
port=8080-old.txt (a file whose name starts with a dash)
old notesDo these steps in order:
- Make the folder
release/v1/config, with its parents, in one command. - Copy
app.confintorelease/v1/config/. - Rename the copy to
app.ini. - Create an empty file
release/v1/notes.txt. - Copy the whole
release/v1folder torelease/v2. - Make a
scratchfolder with two files in it, then delete the folder and its files. - Make an empty folder called
empty, then remove it. - Delete the file
-old.txt. - Print every path that is left, sorted.
Expected output:
Final tree:../app.conf./release./release/v1./release/v1/config./release/v1/config/app.ini./release/v1/notes.txt./release/v2./release/v2/config./release/v2/config/app.ini./release/v2/notes.txtHints
mkdir can make a whole chain of folders at once with one flag, and cp needs a flag before it will copy a folder.Approach
Optimal: mkdir, cp, mv, rm
Covers: mkdir -p, touch, cp, cp -r, mv, rm, rm -r, rmdir, --, find.
Five commands do almost all file work. Here is what each one does, with the flags you need most:
| Command | What it does | Useful flags |
|---|---|---|
mkdir name |
make a folder | -p makes missing parents, and no error if it already exists |
touch file |
make an empty file (or update the time of an existing one) | |
cp from to |
copy | -r copies a folder and everything in it, -a also keeps owners, times and permissions |
mv from to |
move or rename | -i asks before overwriting |
rm file |
delete | -r deletes a folder and everything in it, -i asks first |
rmdir name |
remove a folder | only works if the folder is empty |
mv moves and renames. There is no separate rename command. If the new path is in the same folder, mv renames. If it is in another folder, mv moves. Moving inside the same disk is instant, because only the name changes, not the data.
rm has no undo. There is no recycle bin on the command line. Once rm runs, the file is gone. That is why careful engineers:
- run
lson the same path first, to see what would be deleted; - use
rmdirfor folders they expect to be empty, because it refuses to delete anything else; - never run
rm -rfon a path built from a variable they have not checked.
What -- does. Most commands read any word that starts with - as an option. So rm -old.txt fails: rm reads -old.txt as a bundle of options and stops with "invalid option". Writing rm -- -old.txt says "no more options after this point", so -old.txt is read as a name. Writing rm ./-old.txt also works, because the name now starts with ..
Walking through the code. The # Setup: lines only create the sample files, so skip past them. They work in a new temporary folder, so your real files are safe.
mkdir -p release/v1/configmakesrelease, thenrelease/v1, thenrelease/v1/config. Without-p,mkdirstops with an error becausereleasedoes not exist yet.cp app.conf release/v1/config/copies the file into the folder. The trailing/makes it clear the target is a folder.mv .../app.conf .../app.inistays in the same folder, so it renames.touch release/v1/notes.txtcreates an empty file.cp -r release/v1 release/v2copies the folder.release/v2does not exist yet, socpcreates it as the copy.rm -r scratchdeletes the folder and both files inside it.rmdir emptyworks because the folder is empty.rm -- -old.txtdeletes the file with the awkward name.find . | sortprints every path under the current folder.findlists files in whatever order the disk gives them, sosortmakes the order stable.
Edge cases. mkdir -p on a folder that already exists does nothing and does not fail, which is why scripts use it. rmdir on a folder that still has files fails with "Directory not empty", which protects you. cp onto an existing file overwrites it without asking, unless you add -i.
# Setup: start in a fresh, empty temporary folder with one config file
cd "$(mktemp -d)"
printf 'port=8080\n' > app.conf
printf 'old notes\n' > -old.txt
mkdir -p release/v1/config # -p: make every missing parent folder too
cp app.conf release/v1/config/ # copy a file into a folder
mv release/v1/config/app.conf release/v1/config/app.ini # mv renames when it stays in the same folder
touch release/v1/notes.txt # touch: create an empty file
cp -r release/v1 release/v2 # -r: copy a folder and everything inside it
mkdir scratch
touch scratch/a.tmp scratch/b.tmp
rm -r scratch # -r: delete a folder and everything inside it
mkdir empty
rmdir empty # rmdir only removes a folder that is already empty
rm -- -old.txt # --: "no more options", so -old.txt is read as a name
echo "Final tree:"
find . | sort # every path under here, in a stable orderInterview follow-ups
How do you make the delete step safe inside a script?
Start the script with
set -u, so an unset variable stops it instead of becoming empty. Write paths as"${DIR:?}"so an empty value stops the script too. Before a big delete, print what would go, for example withfind "$DIR" -name '*.tmp' -print, and only then run the delete. Keep deletes inside a folder the script owns, such as one made withmktemp -d, so a mistake cannot reach anything else.
Frequently asked questions
If DIR is empty or unset, the shell turns rm -rf "$DIR/" into rm -rf "/", which tries to delete the whole system. This has happened in real install scripts. Protect yourself by writing rm -rf "${DIR:?}/": the :? makes the shell stop with an error if DIR is empty. Adding set -u at the top of a script also stops it when it uses a variable that was never set. Printing the path with echo before deleting is a good habit when you are unsure.
cp -r copies a folder and everything inside it, but the copies get you as the owner and the current time. cp -a (archive) also keeps the original owner, permissions, times and symlinks as they were. Use cp -a for backups and for copying app folders, where permissions matter. Use cp -r when you just want the contents somewhere else.
mv replaces the existing file silently, and the old one is gone. The same is true for cp. Add -i to make them ask first, or -n to never overwrite. In scripts, check first with a test like [[ -e target ]] when overwriting would be a problem.