NET-103 · Lesson 3 of 12 · 27 min

Working with files, text and pipes

▶ Video coming soon A video version of this lesson is on the way.

Most of what a Linux administrator does is work with text. Configuration files are text, logs are text, the output of every command is text, and even the list of users is a text file. Linux provides a toolbox of small commands that each do one job with text: show it, search it, sort it, count it, cut it into columns. Their real power appears when they are joined together with pipes, so that the output of one becomes the input of the next. With a few of these tools Jordan can answer questions such as which coffees sold most online this week, which addresses are hammering the shop with requests, or how many orders failed, in a single line typed at the prompt.

This lesson starts with the everyday commands for creating, copying, moving and deleting files, then covers viewing and editing text, wildcards, the three standard streams and redirection, and finally the text-processing tools and pipelines that make the command line so productive.

Creating, copying, moving and deleting

A handful of commands handle files and directories. They are short to type and quick to run, and because they act immediately and silently, they reward care. There is no recycle bin on a Linux server: a file removed with rm is gone, and only a backup can bring it back.

Commands for managing files and directories
CommandWhat it doesExample
touch fileCreates an empty file, or updates the time of an existing onetouch notes.txt
mkdir dirCreates a directory; -p creates parents too and does not complain if it existsmkdir -p /srv/ridgeline/roastlog
cp src destCopies a file; -r copies a directory and everything in it; -a also keeps owners and timescp -a shop shop-backup
mv src destMoves or renames a file or directorymv roast.csv roast_log.csv
rm fileRemoves a file; -r removes a directory tree; -i asks firstrm -i old_notes.txt
rmdir dirRemoves an empty directory onlyrmdir emptydir
Building and removing a small directory tree in a practice area
> mkdir -p ~/practice/reports/2026
> cd ~/practice
> touch todo.txt
> cp todo.txt todo-copy.txt
> mv todo-copy.txt reports/
> ls -R
.:
reports  todo.txt

./reports:
2026  todo-copy.txt

./reports/2026:
> rm -r reports
> ls
todo.txt

The command rm -r removes a directory and everything below it without asking. Combined with -f, which forces removal and suppresses errors, it becomes rm -rf, the most feared command on Linux. A stray space can be disastrous: rm -rf / srv/old, with an accidental space after the slash, tells rm to remove the whole tree starting at /. Modern versions of rm refuse to work on / unless given an extra option, but that safeguard does not protect other directories. Good habits are to use -i when deleting by hand, to run ls with the same arguments first to see what will be removed, and to keep backups.

Sam sets Jordan a simple rule for ridge-web: practice in a directory under your home, never experiment with rm in /etc, /srv or /var, and copy a configuration file before editing it, for example cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak, so that you can always go back.

Viewing text

Several commands show the contents of text files, each suited to a different size of file and a different question.

Commands for viewing and counting text
CommandBest for
cat filePrinting a short file in full; also joins (concatenates) several files
less fileReading a long file page by page, with searching; press q to quit
head -n 20 fileThe first lines, such as the header of a data file
tail -n 20 fileThe last lines, which in a log are the most recent entries
tail -f fileFollowing a log live as new lines are added; press Ctrl+C to stop
wc -l fileCounting lines (or words with -w, characters with -c)

Of these, tail -f is the one administrators reach for in a crisis. When Theo says a customer's order just failed, Jordan can run tail -f /var/log/nginx/access.log in one window, ask Theo to try the order again, and watch the request arrive. In less, typing a slash and a word searches forward, n jumps to the next match, G goes to the end and g back to the start, so even a log of millions of lines is easy to explore.

Editing text: nano and vim

Sooner or later every administrator must edit a configuration file on a server that has no graphical editor. Two text-mode editors are almost always available. GNU nano is simple: you type to insert text, and the commands are listed along the bottom of the screen, with the caret symbol meaning the Ctrl key, so Ctrl+O saves (writes Out) and Ctrl+X exits. Ubuntu opens nano by default when a command such as crontab -e needs an editor.

Vim, an improved version of the classic vi editor from 1976, is on virtually every Unix-like system, including tiny rescue environments where nano is missing. It is a modal editor: in normal mode keys are commands, and only in insert mode does typing insert text. Press i to start inserting, Esc to return to normal mode, then type :w to save, :q to quit, :wq to do both, or :q! to quit without saving. Vim is famously confusing at first, but those few keys are enough to survive, and many administrators come to prefer it for its speed.

To edit a file that belongs to root, put sudo in front: sudo nano /etc/hosts. A safer habit for some files is to use a dedicated command that checks your changes before saving them, such as visudo for the sudo rules, which lesson 4 introduces. A syntax error in some configuration files can lock everyone out.

Wildcards

When a command needs several file names, the shell can generate them from a pattern. These patterns are called wildcards or globs, and the shell expands them before the command runs, so ls *.csv receives the list of matching names, not the pattern itself. That is why the same patterns work with every command.

Shell wildcard patterns
PatternMatchesExample
*Any characters, including none*.csv matches roast_log.csv and orders.csv
?Exactly one characterroast?.csv matches roast1.csv but not roast10.csv
[abc]One character from the setshop[12].log matches shop1.log and shop2.log
[0-9]One character from the rangeroastlog-2026-11-0[1-7].sql.gz matches the first week of November
{a,b}Each alternative in turn (brace expansion)cp file.conf{,.bak} copies file.conf to file.conf.bak

Python's fnmatch module uses the same rules as the shell, so you can test a pattern against a list of names safely before using it with a command like rm.

Testing wildcard patterns against a list of file names
from fnmatch import fnmatch

names = ["roastlog-2026-11-01.sql.gz", "roastlog-2026-11-09.sql.gz", "roastlog-2026-10-31.sql.gz",
         "orders.csv", "roast_log.csv", "README"]
for pattern in ["*.csv", "roastlog-2026-11-0[1-7].sql.gz", "roast?log.csv", "*"]:
    hits = [n for n in names if fnmatch(n, pattern)]
    print(f"{pattern:32} {hits}")
Output
*.csv                            ['orders.csv', 'roast_log.csv']
roastlog-2026-11-0[1-7].sql.gz   ['roastlog-2026-11-01.sql.gz']
roast?log.csv                    ['roast_log.csv']
*                                ['roastlog-2026-11-01.sql.gz', 'roastlog-2026-11-09.sql.gz', 'roastlog-2026-10-31.sql.gz', 'orders.csv', 'roast_log.csv', 'README']

Standard streams and redirection

Every program started from the shell is given three channels, called standard streams. Standard input, number 0, is where it reads input, normally the keyboard. Standard output, number 1, is where it writes its results, normally the screen. Standard error, number 2, is where it writes error messages, also normally the screen. Keeping errors separate from results means that when you save a command's output to a file, error messages still appear where you can see them and do not get mixed into the data.

A diagram of a command in a box with three arrows. Standard input, stream 0, enters from the keyboard on the left. Standard output, stream 1, leaves to the right toward the screen, a file or the next command in a pipe. Standard error, stream 2, leaves downward to the screen. Below, a pipeline of three boxes, grep, sort and uniq, is connected by pipe symbols, each passing its output to the next.
Every command has three standard streams, and a pipe connects one command's output to the next command's input

Redirection changes where these streams go. A greater-than sign after a command sends standard output to a file, replacing whatever the file held. Two greater-than signs append to the file instead. Putting the number 2 in front redirects standard error. A less-than sign feeds a file into standard input. And the special file /dev/null swallows anything written to it, which is how you discard output you do not want.

Redirection operators
SyntaxEffect
command > fileStandard output to file, replacing its contents
command >> fileStandard output appended to the end of file
command 2> fileStandard error to file
command > file 2>&1Both output and errors to the same file
command < fileStandard input read from file
command 2> /dev/nullErrors discarded
Output goes to the file while the error still reaches the screen; appending builds a log; errors are discarded
> ls /srv/ridgeline /nonexistent > listing.txt
ls: cannot access '/nonexistent': No such file or directory
> cat listing.txt
/srv/ridgeline:
roastlog
shop
> date >> checks.log
> df -h /srv >> checks.log
> find /etc -name "*.conf" 2> /dev/null | wc -l
143

The greater-than sign is the most dangerous character in this section. Typing > when you meant >> erases the file's previous contents without any warning, and running a command such as sort roast.csv > roast.csv empties the file before sort even reads it, because the shell opens the output file first. Write to a new file and then rename it instead.

Pipes: joining commands together

A pipe, written with the vertical bar |, connects the standard output of one command to the standard input of the next. The commands run at the same time, with data flowing between them, and nothing is written to disk in between. Pipes let you build a custom tool on the spot out of simple parts. The idea came from Doug McIlroy at Bell Labs and was added to Unix in 1973, and it remains the heart of command-line work.

Counting checkouts, showing recent server errors and finding the busiest client addresses
> grep "POST /checkout" /var/log/nginx/access.log | wc -l
312
> grep " 500 " /var/log/nginx/access.log | tail -n 3
> cut -d' ' -f1 /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -n 5
   1840 203.0.113.88
    411 198.51.100.17
    207 192.0.2.44
    190 192.0.2.61
    122 198.51.100.200
> grep -c "sunrise-blend" /var/log/nginx/access.log

Read the third pipeline from left to right. cut -d' ' -f1 splits each line of the web server log at spaces and keeps the first field, the client's address. sort puts identical addresses next to each other. uniq -c collapses each run of identical lines into one, with a count in front. sort -rn sorts those counts as numbers, largest first. head -n 5 keeps the top five. The answer, that one address made 1,840 requests, is a clue worth following up, and lesson 9 shows what Jordan found.

The text-processing toolbox

The tools used in pipelines are worth knowing individually. Each has many options, which man describes, but a few of each cover most needs.

Text-processing commands used in pipelines
ToolJobCommon options
grep patternKeep lines that match a pattern-i ignore case, -v invert, -c count, -n line numbers, -r search directories, -E extended patterns
sortSort lines-n numeric, -r reverse, -k2 by second field, -t, field separator, -u unique
uniqCollapse adjacent duplicate lines (sort first)-c count, -d only duplicates
cutKeep selected columns-d delimiter, -f field numbers
wcCount lines, words and characters-l lines, -w words, -c bytes
trTranslate or delete characterstr a-z A-Z, tr -d to delete
sedEdit a stream, for example find and replaces/old/new/g, -i to edit a file in place
teeCopy input to a file and to standard output-a to append
xargsTurn input lines into arguments for another command-n to limit arguments per run

The grep command is the one you will use most. Its name comes from a command in an early Unix editor, g/re/p, meaning globally search for a regular expression and print. A regular expression is a pattern language for text: a dot matches any character, a caret anchors the start of a line, square brackets match a set and an asterisk repeats what comes before. You can go a long way with plain words, though, and grep -i error /var/log/syslog is often all you need.

Here is the busiest-address pipeline written in Python, using the Counter class from the collections module. Seeing the same steps in both forms makes the shell version easier to remember, and the Python version is easier to extend when the question gets more complicated.

The same log analysis in Python with Counter
from collections import Counter

log = """203.0.113.88 - - [03/Nov/2026:08:01:12 +0000] "GET /shop HTTP/1.1" 200 5123
198.51.100.17 - - [03/Nov/2026:08:01:15 +0000] "GET /shop/sunrise-blend HTTP/1.1" 200 4210
203.0.113.88 - - [03/Nov/2026:08:01:16 +0000] "GET /wp-login.php HTTP/1.1" 404 162
192.0.2.44 - - [03/Nov/2026:08:02:40 +0000] "POST /checkout HTTP/1.1" 200 811
203.0.113.88 - - [03/Nov/2026:08:02:41 +0000] "GET /.env HTTP/1.1" 404 162
198.51.100.17 - - [03/Nov/2026:08:03:02 +0000] "POST /checkout HTTP/1.1" 500 97"""

lines = log.splitlines()
addresses = Counter(line.split()[0] for line in lines)        # cut | sort | uniq -c
for addr, count in addresses.most_common(2):                  # sort -rn | head
    print(count, addr)
statuses = Counter(line.split()[8] for line in lines)
print("By status:", dict(sorted(statuses.items())))
print("Checkouts:", sum("POST /checkout" in line for line in lines))   # grep | wc -l
Output
3 203.0.113.88
2 198.51.100.17
By status: {'200': 3, '404': 2, '500': 1}
Checkouts: 2

Worked problem

Which coffees sell best online?

Theo wants to know which product pages were viewed most yesterday. Every product page has an address of the form /shop/ followed by the product name. Build a pipeline over the access log that lists the top three product pages.

Show solution
  1. Keep only successful product page requests: grep " 200 " access.log | grep -o "GET /shop/[a-z-]*". The -o option prints only the matching part of each line.
  2. Remove the GET and the /shop/ prefix so that only the name remains: pipe through cut -d/ -f3.
  3. Count each name: sort | uniq -c.
  4. Order by count and keep three: sort -rn | head -n 3.
  5. The result shows sunrise-blend with 412 views, espresso-ridge with 268 and ethiopia-guji with 190, which Theo compares with actual sales.

Answer: grep " 200 " access.log | grep -o "GET /shop/[a-z-]*" | cut -d/ -f3 | sort | uniq -c | sort -rn | head -n 3

Try it yourself

A small grep

Write grep(lines, word, ignore_case=False, invert=False) that returns the list of lines containing word. With ignore_case=True the match ignores capitals, like grep -i; with invert=True it returns the lines that do not match, like grep -v.

def grep(lines, word, ignore_case=False, invert=False):
    return lines


log = ['GET /shop 200', 'POST /checkout 500', 'GET /Checkout 200']
print(grep(log, 'checkout'))

Try it yourself

Top addresses in a web log

Write top_addresses(log_text, n) that returns a list of (count, address) tuples for the n busiest client addresses in a web server log, busiest first, just like cut -d" " -f1 | sort | uniq -c | sort -rn | head -n N. Break ties by address in alphabetical order.

def top_addresses(log_text, n):
    return []


sample = '1.1.1.1 GET /\n2.2.2.2 GET /\n1.1.1.1 GET /shop'
print(top_addresses(sample, 2))

Try it yourself

Count like wc

Write wc(text) that returns a tuple (lines, words, characters) the way wc counts them: lines is the number of newline characters, words are separated by any whitespace, and characters counts every character.

def wc(text):
    return (0, 0, 0)


print(wc('Sunrise Blend\nEthiopia Guji\n'))

Common mistakes

  • MistakeTyping > when you meant >>, wiping a log or configuration file.

    InsteadUse >> to append; copy important files before redirecting into them.

  • MistakeSorting a file into itself, as in sort data.csv > data.csv.

    InsteadWrite to a new file, check it, then rename it over the original.

  • MistakeUsing uniq without sorting first.

    Insteaduniq only collapses adjacent duplicates, so put sort before it.

  • MistakeRunning rm -rf with a variable path or a stray space.

    InsteadCheck the path with ls first, prefer rm -i by hand, and keep backups.

  • MistakeGetting stuck in vim.

    InsteadPress Esc, then type :q! to quit without saving or :wq to save and quit.

  • MistakeForgetting to quote a pattern, so the shell expands it before find or grep sees it.

    InsteadQuote patterns meant for the command, as in find /etc -name "*.conf".

Summary and key terms

  • touch, mkdir -p, cp, mv and rm manage files; there is no recycle bin, so delete with care.
  • cat, less, head, tail and wc show and count text; tail -f follows a log live.
  • nano is simple; in vim, i inserts, Esc leaves insert mode and :wq or :q! leaves the editor.
  • Wildcards such as *, ? and [0-9] are expanded by the shell before the command runs.
  • Every program has standard input, output and error; > and >> redirect output, 2> redirects errors, < feeds input.
  • A pipe | connects one command's output to the next command's input, building tools from small parts.
  • grep, sort, uniq, cut, tr, sed, tee and xargs are the core text-processing tools.
wildcard (glob)
A pattern such as *.csv that the shell expands into matching file names.
standard input
Stream 0, where a program reads its input.
standard output
Stream 1, where a program writes its results.
standard error
Stream 2, where a program writes error messages.
redirection
Sending a stream to or from a file instead of the terminal.
pipe
A connection that feeds one command's output into the next command's input.
pipeline
A chain of commands joined by pipes.
regular expression
A pattern language for matching text, used by grep, sed and many programming languages.
/dev/null
A special file that discards anything written to it.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. Write a command that appends the current date to ~/checks.log.

    Show answer

    date >> ~/checks.log

  2. How would you count the lines in /var/log/auth.log that contain Failed password?

    Show answer

    grep -c "Failed password" /var/log/auth.log

  3. What is wrong with uniq -c names.txt if names.txt is not sorted?

    Show answer

    Duplicates that are not next to each other are counted separately; sort first: sort names.txt | uniq -c.

  4. Which wildcard pattern matches roastlog-2026-11-10.sql.gz to roastlog-2026-11-19.sql.gz?

    Show answer

    roastlog-2026-11-1[0-9].sql.gz

  5. Write a pipeline that shows the ten most common words in notes.txt.

    Show answer

    tr -s " " "\n" < notes.txt | sort | uniq -c | sort -rn | head -n 10, or an equivalent.

  6. How do you save a file and quit in nano?

    Show answer

    Ctrl+O and Enter to save, then Ctrl+X to exit.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz