Bash & the POSIX Shell Module 1 – Start here: the shell, the terminal and your first script
Files and directories from the command line
Move around with pwd, cd and ls, create, copy, move and delete files and folders without surprises, and look inside files with cat, head, tail and wc.
What you will learn
- Navigate with pwd, cd and ls using absolute and relative paths
- Create, copy, move and delete files and directories without surprises
- Inspect files with cat, less, head, tail, wc and file
Before you start
What you’ll learn · Before you start
What you will learn
- Navigate with pwd, cd and ls using absolute and relative paths
- Create, copy, move and delete files and directories without surprises
- Inspect files with cat, less, head, tail, wc and file
Before you start
On this page
rm -rf -- "$build_dir/" deletes a build folder, until the day build_dir is empty and the same line means “delete
everything under /”. The shell has no recycle bin, so the commands that create, copy, move and delete files are worth
learning with their one or two sharp edges in view.
Below you will move around a small tree with relative and absolute paths, build, copy, move and delete files and
folders, write a delete that refuses to run on an empty variable, and look inside files with cat, head, tail,
wc and file, all in recorded sessions you can repeat.
Paths start at the top, at home or at the current folder
Every command runs in a current folder, which pwd prints. A path that starts with / is
absolute path: A path that starts at the top of the file system, /, so it names the same file wherever a command runs, such as /etc/hosts.: it is read from the top of the disk, wherever you are. A path that starts
with ~ starts at your home folder. Any other path is relative path: A path read from the current folder, the one pwd prints; . is the current folder and .. its parent, so docs/a.txt names a different file in each folder. and is read from the
current folder: . is the current folder itself and .. its parent. Step through the moves of the session below in
the tree it builds:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Where you start. pwd prints the current folder, ~/demo. Every relative path, one that does not start with / or ~, is read from here.
- cd project. project is a relative path, so cd looks for project inside ~/demo. mkdir -p created it, with src and docs inside, a moment earlier.
- cd src. Another relative step, one level down. pwd now prints ~/demo/project/src.
- cd ../docs. Two dots mean the parent folder, so ../docs goes up to project and down into docs in one path.
- cd ~/demo. ~ stands for your home folder, so ~/demo is the same folder from wherever you are. A path that starts with / is absolute in the same way, from the top of the disk.
- cd ... From ~/demo, one level up is the home folder itself. Then cd demo/project/src walks down three levels in one relative path.
Here is the same walk as a recorded session. So that no real user name appears, the session shows its home folder as
~; on your own computer pwd prints the full path, such as /home/asha/demo or /Users/asha/demo:
from session import run
# Moving around a small tree with relative and absolute paths. The session starts in ~/demo.
run("""
pwd
mkdir -p project/src project/docs
cd project
pwd
cd src
pwd
cd ../docs
pwd
cd ~/demo
pwd
cd ..
pwd
cd demo/project/src
pwd
cd ~/demo
ls project
""") Output
$ pwd ~/demo $ mkdir -p project/src project/docs $ cd project $ pwd ~/demo/project $ cd src $ pwd ~/demo/project/src $ cd ../docs $ pwd ~/demo/project/docs $ cd ~/demo $ pwd ~/demo $ cd .. $ pwd ~ $ cd demo/project/src $ pwd ~/demo/project/src $ cd ~/demo $ ls project docs src
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 paths_session.py
cd ../docs went up to project and down into docs in one step, and cd ~/demo came back from anywhere. ls project listed the folder without moving into it: most commands take a path, so you rarely need cd just to look.
Two shortcuts save typing. cd on its own goes straight back to your home folder, and cd - returns to the folder
you were in before and prints its name. Both come from POSIX’s specification of cd, so they work in every POSIX
shell, and both are handy when you jump between two folders while you work.
Reading a relative path from the wrong place
Wrong: running cat src/main.sh from your home folder and concluding the file is missing. Right: a relative path
is read from the current folder, so check pwd first, or give the full path ~/demo/project/src/main.sh.
Four commands create, copy, move and delete, and deleting has no undo
Four commands do most of the work. mkdir -p a/b creates a folder and any missing parents, and succeeds if they
exist already, which makes it safe to run twice. cp copies a file, and cp -R a whole tree. mv moves a file, and
renames it when the destination is a new name in the same folder. rm deletes files, and rm -r folders with
everything in them, at once and for good: there is no recycle bin. rmdir removes only an empty folder, which
makes it the safe way to tidy up: it refuses any folder that still holds something.
from session import run
# Creating, copying, moving and deleting, then a sorted listing of what is left.
run("""
mkdir -p project/src project/docs
printf 'echo hi\\n' > project/src/main.sh
cp project/src/main.sh project/src/main.bak
mv project/src/main.bak project/docs/old.sh
cp -R project backup
printf 'secret\\n' > project/.env
ls project
ls -a project
find . -type f | sort
rm -r backup
ls
""") Output
$ mkdir -p project/src project/docs $ printf 'echo hi\n' > project/src/main.sh $ cp project/src/main.sh project/src/main.bak $ mv project/src/main.bak project/docs/old.sh $ cp -R project backup $ printf 'secret\n' > project/.env $ ls project docs src $ ls -a project . .. .env docs src $ find . -type f | sort ./backup/docs/old.sh ./backup/src/main.sh ./project/.env ./project/docs/old.sh ./project/src/main.sh $ rm -r backup $ ls project
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tour_session.py
| Command | What it did in the session |
|---|---|
mkdir -p project/src project/docs |
Created three folders in one call |
cp project/src/main.sh project/src/main.bak |
A second copy beside the first |
mv project/src/main.bak project/docs/old.sh |
Moved and renamed it in one step |
cp -R project backup |
Copied the whole tree |
rm -r backup |
Deleted that tree, with no question asked |
Two of these commands can also destroy data without asking. When the destination of cp or mv is an existing
file, it is overwritten silently, so mv draft.txt final.txt replaces any final.txt that was there. cp -i and
mv -i ask before they overwrite, and mv -n never overwrites at all, which makes them the safer choice when you are
working by hand.
One more detail showed up: ls project did not list .env. Names that begin with a dot are
hidden file: A file whose name begins with a dot, such as .env. ls and the glob * skip such names unless asked, for example with ls -a. from ls and from * unless you ask, and ls -a shows them, together with .
and ... Configuration and secrets often live in such files, so look with ls -a before you copy or delete a folder.
Checkpoint
Check: Absolute and relative paths · Creating, copying, moving, deleting
Look before you delete, and make an empty variable stop the delete
Most deleting accidents come from a word that means something other than you think. A glob may match more than you
expected, so run ls or echo with it first, then rm. A variable may be empty, and then "$build_dir/" is just
/. The parameter expansion ${build_dir:?} stops the command when the variable is empty or unset, which is what
ShellCheck asks for with SC2115:
from session import run
# A delete that refuses to run when the folder variable is empty, and the same line once it is set.
run("""
mkdir -p out/logs
printf 'log\\n' > out/logs/run.log
build_dir=
!rm -rf -- "${build_dir:?}/"
build_dir=out
echo rm -rf -- "${build_dir:?}/"
rm -rf -- "${build_dir:?}/"
ls
""") Output
$ mkdir -p out/logs
$ printf 'log\n' > out/logs/run.log
$ build_dir=
$ rm -rf -- "${build_dir:?}/"
bash: build_dir: parameter null or not set
[exit status 1]
$ build_dir=out
$ echo rm -rf -- "${build_dir:?}/"
rm -rf -- out/
$ rm -rf -- "${build_dir:?}/"
$ ls
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 safe_delete_session.py
With build_dir empty, Bash printed “build_dir: parameter null or not set” and did not run rm at all; the status was
- Once the variable held
out,echoshowed the exact command first, and only then did the realrmremove the folder.
One more habit keeps scripts safe: never read the output of ls in a program. Its listing is formatted for people,
and a file name may contain spaces or even a newline, so splitting it into names goes wrong. ShellCheck reports such
code as SC2045. A glob such as logs/*.txt or the find command gives a script the names exactly as they are.
Key idea
Never let a delete depend on a value you have not seen: print the command with echo first, and write
"${dir:?}" so an empty variable stops it instead of widening it.
cat, head, tail, wc and file show what is inside
You can look inside a file without an editor. cat prints a whole file, head -n N its first N lines and tail -n N
its last N, wc -l counts its lines, and file guesses what kind of file it is from its contents. Given several names, cat
prints the files one after another, which is where its name comes from: it concatenates them. For long files,
less shows one screen at a time: Space moves on, /word searches and q quits. man usually shows its pages
through the same pager, so the same keys work there too.
from session import run
# Looking inside files without opening an editor.
run("""
printf 'one\\ntwo\\nthree\\nfour\\n' > notes.txt
cat notes.txt
head -n 2 notes.txt
tail -n 1 notes.txt
wc -l notes.txt
printf '#!/usr/bin/env bash\\necho hi\\n' > hi.sh
file notes.txt hi.sh
""") Output
$ printf 'one\ntwo\nthree\nfour\n' > notes.txt
$ cat notes.txt
one
two
three
four
$ head -n 2 notes.txt
one
two
$ tail -n 1 notes.txt
four
$ wc -l notes.txt
4 notes.txt
$ printf '#!/usr/bin/env bash\necho hi\n' > hi.sh
$ file notes.txt hi.sh
notes.txt: ASCII text
hi.sh: Bourne-Again shell script text executable, ASCII text
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 inspect_session.py
file recognised hi.sh as a Bash script from its first line alone, whatever its name. Notice that wc -l printed
spaces before the 4: this session ran macOS’s own wc, which pads its numbers, while GNU wc on Linux prints 4 notes.txt without them. A script that reads that number should not depend on the spacing, which is one of the
differences the portability module of this track collects.
Read the end of a growing log
tail -n 20 app.log shows the last 20 lines of a log, and tail -f app.log keeps printing new lines as they are
written, until you press Ctrl-C.
Checkpoint
Check: Deleting safely · Looking inside files
Practise: paths, copies and safe deletes
The questions below put you in folders and in front of commands that are about to run. Pick what happens; each answer explains the rule it rests on:
Practice that adapts to you
Practise this lesson
8 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): how do absolute and relative paths differ?
An absolute path starts at the top of the file system, /, so it names the same file wherever you run the command:
/etc/hosts is always the same file. A relative path is read from the current folder, the one pwd prints, so
docs/notes.txt names a different file in each folder. . means the current folder and .. its parent, and a path
that starts with ~ begins at your home folder. Scripts usually build absolute paths, or cd to a known folder first
and check that the cd worked, because a relative path silently points somewhere else when the script runs from
another folder.
Two harder questions follow, about a cleanup that went too far and about copying a project safely. Decide what you would do before you open each first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to senior
A deploy script contains rm -rf "$TARGET/"* and one day deletes far more than the release folder. What most likely happened, and how would you make the line safe?
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
How does an absolute path differ from a relative one?
An absolute path starts at / and names the same file from anywhere. A relative path is read from the current folder that pwd prints, so docs/notes.txt means a different file in each folder. A path that starts with ~ begins at your home folder.
-
Can you get back what rm -r deleted?
No. The shell has no recycle bin: rm -r removes the folder and everything in it at once and for good, so look at what a delete will touch before you run it.
-
Why can ls and the glob * miss files that a copy or delete should include?
Names that begin with a dot, such as .env, are hidden from both. List the folder with ls -a before copying or deleting it.
-
How do you keep rm -rf "$dir"/ from widening when dir is empty?
Write "${dir:?}". An empty or unset dir then stops the command with an error instead of turning the path into /. Printing the command with echo first shows what it would remove.
-
Which commands create nested folders, copy a whole tree, and rename a file?
mkdir -p creates a folder together with any missing parents, cp -R copies a tree, and mv both moves and renames.
-
Which commands show what a file holds without an editor?
cat prints it, head and tail show its first and last lines, wc -l counts its lines, file names its type, and less pages through a long one.
Key takeaways
- An absolute path starts at
/and a path with~at your home folder, while every other path is read from the current folder thatpwdprints. mkdir -pcreates missing parents safely,cp -Rcopies trees,mvmoves and renames, andrm -rdeletes for good, with no recycle bin.- Names starting with a dot are hidden from
lsand*, so list a folder withls -abefore you copy or delete it. - Print a delete with
echofirst and write"${dir:?}", so an empty variable stops the command instead of widening it. cat,head,tail,wc -landfileshow what a file holds, andlesspages through long ones.
Terms in this lesson
- absolute path
- A path that starts at the top of the file system, /, so it names the same file wherever a command runs, such as /etc/hosts.
- hidden file
- A file whose name begins with a dot, such as .env. ls and the glob * skip such names unless asked, for example with ls -a.
- relative path
- A path read from the current folder, the one pwd prints; . is the current folder and .. its parent, so docs/a.txt names a different file in each folder.
References
- POSIX.1-2024: cd (The Open Group and IEEE)
- mkdir(1) (man7.org (Linux man-pages))
- cp(1) (man7.org (Linux man-pages))
- mv(1) (man7.org (Linux man-pages))
- rm(1) (man7.org (Linux man-pages))
- ls(1) (man7.org (Linux man-pages))
- Bash Reference Manual: Shell Parameter Expansion (Chet Ramey (GNU Bash maintainer))
- SC2115: Use "${var:?}" to ensure this never expands to /* (ShellCheck)
Report a problem with this lesson
Kept only in this browser. Your Learn progress