Working with Files · about 35 min

Creating and viewing files

Why this matters

Most of what you do in a shell is make files, look inside them, and pull out the part you care about: the last lines of a log after an error, the header of a CSV, the size of a dump. Graphical editors choke on a 2 GB log file; tail reads its last ten lines instantly because it jumps straight to the end. This lesson gives you the everyday toolkit.

Making directories: mkdir

$ mkdir reports
$ mkdir reports/2026/october
mkdir: cannot create directory 'reports/2026/october': No such file or directory
$ mkdir -p reports/2026/october
$ mkdir -p reports/2026/october
$

Plain mkdir creates one level and fails if the parent is missing or the directory already exists. -p (parents) creates every missing directory along the path and does not complain if the target exists, which makes it safe to run twice: ideal in scripts. mkdir accepts several paths at once: mkdir -p src tests docs.

Making files: touch, echo and redirection

touch creates an empty file if the name does not exist, and otherwise just updates its timestamps:

$ touch a.txt b.txt
$ ls -l a.txt
-rw-r--r--. 1 learner learner 0 Nov  1 14:30 a.txt
$ touch -d '2020-01-01 00:00' a.txt
$ ls -l a.txt
-rw-r--r--. 1 learner learner 0 Jan  1  2020 a.txt

To create a file with content, redirect output into it:

$ echo "first line" > notes.txt
$ echo "second line" >> notes.txt
$ cat notes.txt
first line
second line

> creates or empties the file first; >> appends. Mixing them up is the most common way to lose a file's content, so read your line before pressing Enter.

Editing files: nano

For anything longer, use an editor. nano is the friendliest one installed here:

$ nano notes.txt

The bottom two lines of nano's screen list its commands; ^ means Ctrl. The ones you need: Ctrl-O then Enter to save ("write Out"), Ctrl-X to exit, Ctrl-K to cut a line, Ctrl-U to paste, Ctrl-W to search. (vi is also installed; if you open it by accident, type :q! and Enter to escape without saving.)

Viewing: cat, less

cat (concatenate) prints whole files, one after the other:

$ cat notes.txt poem.txt

That is fine for short files. For long ones, use the pager less, the same viewer man uses: Space and b to page, /text to search, G to go to the end, q to quit. less reads only what it needs to show, so it opens huge files instantly. less -N shows line numbers, and cat -n prints them too.

The beginning and the end: head, tail

$ head server.log
2026-10-31 12:00:01 INFO request 1 served in 40ms
...                                          (10 lines by default)
$ head -n 3 server.log
$ tail -n 3 server.log
2026-10-31 12:03:18 INFO request 198 served in 129ms
2026-10-31 12:03:19 INFO request 199 served in 166ms
2026-10-31 12:03:20 INFO request 200 served in 203ms

To get line 100 alone, combine them: take the first 100, then the last of those.

$ head -n 100 server.log | tail -n 1
2026-10-31 12:01:40 INFO request 100 served in 103ms

Counting: wc

wc (word count) counts lines, words and bytes:

$ wc server.log
  200  1600 10467 server.log
$ wc -l server.log
200 server.log
$ wc -l < server.log
200

With a file name argument, wc prints the name too; when it reads from its input (< file), it does not know any name, so it prints just the number. That trick is useful in scripts.

What happens underneath: how tail finds the end

A file is a sequence of bytes, and "lines" are just stretches of bytes ending in a newline character (byte 10, written \n). There is no index of lines. So how does tail find the last three lines of a 2 GB file without reading 2 GB?

 file:  [ .......... 2 GB of bytes .......... | line198\n line199\n line200\n ]
                                             ▲
             lseek(fd, -8192, SEEK_END)  ────┘  jump near the end, read backwards,
                                                count newlines until it has 3 lines

tail asks the kernel to move its read position (lseek) to near the end and reads backwards in blocks, counting newlines. head simply reads from the start and stops. cat and wc, by contrast, must read every byte. Thinking about how much data a tool has to touch is a habit that pays off with big files.

Because a line is "bytes up to a \n", a file whose last line has no trailing newline confuses some tools: wc -l counts newline characters, so it reports one line fewer than you see. Editors like nano add the final newline for you; printf 'no newline' > f does not.

Common mistakes

Recap

Going further: try tail -f in action. In the shell, run (for i in 1 2 3 4 5; do echo tick $i >> clock.log; sleep 1; done) & and then immediately tail -f clock.log. Press Ctrl-C when it stops ticking.

Exercises

Exercise 1 · quiz

What does touch do?

notes.txt already exists and contains 3 lines. What does touch notes.txt do?

Answer
  • Leaves the content alone and sets the file's modification and access times to now.

touch updates the timestamps of an existing file and creates an empty file only if the name does not exist yet. That is its original purpose: tools like make decide what to rebuild by comparing modification times, and touching a file marks it as changed. Creating empty files is a convenient side effect. To empty a file, use > notes.txt or truncate -s 0 notes.txt.

Exercise 2 · task in a shell

Build a project skeleton

In /work, create this structure (all directories, plus three empty files):

webapp/
├── src/
│   └── components/
├── tests/
│   └── unit/
├── docs/
│   └── README.md
├── .gitignore
└── src/main.js

That is: directories webapp/src/components, webapp/tests/unit and webapp/docs, and empty files webapp/docs/README.md, webapp/.gitignore and webapp/src/main.js. Try to do all the directories with a single mkdir -p command and all the files with a single touch command.

Hint 1

mkdir -p a/b/c creates a, a/b and a/b/c as needed. mkdir accepts several paths at once.

Hint 2

mkdir -p webapp/src/components webapp/tests/unit webapp/docs then touch webapp/docs/README.md webapp/.gitignore webapp/src/main.js

Reference solution
mkdir -p webapp/src/components webapp/tests/unit webapp/docs
touch webapp/docs/README.md webapp/.gitignore webapp/src/main.js

Exercise 3 · task in a shell

Slice a log file

/work/server.log has 200 lines. Using head and tail, create:

  1. /work/first5.txt: the first 5 lines of the log,
  2. /work/last3.txt: the last 3 lines,
  3. /work/line100.txt: only line number 100.

For the third one, think about combining two commands with |: take the first 100 lines, then the last 1 of those. (A pipe a | b feeds a's output into b; module 5 explains it fully.)

Hint 1

head -n 5 server.log > first5.txt; tail -n 3 works the same way from the end.

Hint 2

head -n 100 server.log | tail -n 1 > line100.txt

Reference solution
head -n 5 server.log > first5.txt
tail -n 3 server.log > last3.txt
head -n 100 server.log | tail -n 1 > line100.txt

Exercise 4 · write code

A tiny file viewer

The directory contains a file poem.txt (8 lines). Write a script that prints:

  1. the line --- top ---
  2. the first 2 lines of poem.txt
  3. the line --- bottom ---
  4. the last 2 lines of poem.txt
  5. a final line with the number of lines in the file, printed by wc -l < poem.txt

Expected output:

--- top ---
Shall I compare thee to a summer's day?
Thou art more lovely and more temperate:
--- bottom ---
So long as men can breathe or eyes can see,
So long lives this, and this gives life to thee.
8

File: script.sh

Starting point

echo "--- top ---"

Example

expected output

--- top ---
Shall I compare thee to a summer's day?
Thou art more lovely and more temperate:
--- bottom ---
So long as men can breathe or eyes can see,
So long lives this, and this gives life to thee.
8
Hint 1

head -n 2 poem.txt and tail -n 2 poem.txt.

Hint 2

wc -l poem.txt prints 8 poem.txt; with < poem.txt wc reads from its input and prints just 8.

Reference solution
echo "--- top ---"
head -n 2 poem.txt
echo "--- bottom ---"
tail -n 2 poem.txt
wc -l < poem.txt