๐Ÿ”— Lesson 11: Pipes, Redirection and Your First Script

Connect small commands into powerful chains, send output into files, and turn what you typed into a script you can run again

Video: Pipes and Redirection (Linux 101 video)
Prefer to watch? This short video covers pipes, redirection and a first script (not the if and exit-code part) on a real Ubuntu 24.04 machine, with on-screen captions and no voiceover. Click to play, or watch it on YouTube.

Small tools, joined together

Every command you have learned so far does one small job: ls lists, grep searches, wc counts, sort sorts. The real power of the command line comes from joining them, so that the output of one becomes the input of the next. This lesson shows you how, and ends with the step that turns you from someone who types commands into someone who writes scripts: saving the commands in a file so the computer can run them again whenever you like.

Everything below was run for real on Ubuntu 24.04. File names, counts and sizes on your machine will differ.

The pipe: |

The | character (the pipe, usually on the key above Enter, with Shift) takes everything the left command prints and hands it to the right command instead of the screen.

ubuntu@practice:~$ ls /usr/bin | head -5
[
addpart
apt
apt-cache
apt-cdrom
ubuntu@practice:~$ ls /usr/bin | wc -l
301
ubuntu@practice:~$ cat /etc/passwd | wc -l
19
ubuntu@practice:~$ du -h --max-depth=1 /usr | sort -h
4.0K	/usr/games
4.0K	/usr/include
4.0K	/usr/lib64
4.0K	/usr/src
36K	/usr/libexec
40K	/usr/local
5.3M	/usr/sbin
6.0M	/usr/share
19M	/usr/bin
49M	/usr/lib
79M	/usr
ubuntu@practice:~$ du -h --max-depth=1 /usr | sort -rh | head -3
79M	/usr
49M	/usr/lib
19M	/usr/bin

You can chain as many as you like, each command reading from the one before. Nothing is changed on disk. A pipe only moves text from one program to the next. (This is also how you find one process among hundreds: ps aux | grep NAME, the pipe from lesson 7's promise; from the documentation, not run here.)

Redirection: > >> 2> <

Pipes join commands to each other. Redirection joins a command to a file.

ubuntu@practice:~$ echo "first line" > out.txt
ubuntu@practice:~$ echo "second line" >> out.txt
ubuntu@practice:~$ cat out.txt
first line
second line
ubuntu@practice:~$ echo "oops" > out.txt
ubuntu@practice:~$ cat out.txt
oops
ubuntu@practice:~$ ls /nonexistent
ls: cannot access '/nonexistent': No such file or directory
ubuntu@practice:~$ ls /nonexistent 2> errors.txt
ubuntu@practice:~$ cat errors.txt
ls: cannot access '/nonexistent': No such file or directory
ubuntu@practice:~$ ls /usr/share/doc /nonexistent > list.txt 2> errors.txt
ubuntu@practice:~$ wc -l list.txt
96 list.txt
ubuntu@practice:~$ cat errors.txt
ls: cannot access '/nonexistent': No such file or directory
ubuntu@practice:~$ ls /nonexistent 2> /dev/null
ubuntu@practice:~$ printf 'pear\napple\nfig\n' > fruit.txt
ubuntu@practice:~$ sort < fruit.txt
apple
fig
pear
ubuntu@practice:~$ sort fruit.txt > sorted.txt
ubuntu@practice:~$ cat sorted.txt
apple
fig
pear
ubuntu@practice:~$ wc -l < fruit.txt
3

A real pipeline: reading a log

Servers write logs, files recording what happened (lesson 8). They are the best practice for pipelines. We made a tiny pretend web log with seven requests, one per line: the visitor's address, the method, the page and the result (200 means found, 404 means not found).

ubuntu@practice:~$ echo "10.0.0.5 GET /index.html 200" > access.log
ubuntu@practice:~$ echo "10.0.0.9 GET /about.html 200" >> access.log
ubuntu@practice:~$ echo "10.0.0.5 GET /missing.html 404" >> access.log
ubuntu@practice:~$ echo "10.0.0.7 GET /index.html 200" >> access.log
ubuntu@practice:~$ echo "10.0.0.5 GET /index.html 200" >> access.log
ubuntu@practice:~$ echo "10.0.0.9 GET /old.html 404" >> access.log
ubuntu@practice:~$ echo "10.0.0.5 GET /contact.html 200" >> access.log
ubuntu@practice:~$ cat access.log
10.0.0.5 GET /index.html 200
10.0.0.9 GET /about.html 200
10.0.0.5 GET /missing.html 404
10.0.0.7 GET /index.html 200
10.0.0.5 GET /index.html 200
10.0.0.9 GET /old.html 404
10.0.0.5 GET /contact.html 200
ubuntu@practice:~$ grep -c 404 access.log
2
ubuntu@practice:~$ grep 404 access.log
10.0.0.5 GET /missing.html 404
10.0.0.9 GET /old.html 404
ubuntu@practice:~$ cut -d' ' -f1 access.log
10.0.0.5
10.0.0.9
10.0.0.5
10.0.0.7
10.0.0.5
10.0.0.9
10.0.0.5
ubuntu@practice:~$ cut -d' ' -f1 access.log | sort | uniq -c
      4 10.0.0.5
      1 10.0.0.7
      2 10.0.0.9
ubuntu@practice:~$ cut -d' ' -f1 access.log | sort | uniq -c | sort -rn
      4 10.0.0.5
      2 10.0.0.9
      1 10.0.0.7
ubuntu@practice:~$ grep 404 access.log | cut -d' ' -f3 | tee missing.txt | wc -l
2
ubuntu@practice:~$ cat missing.txt
/missing.html
/old.html

Now we ask it questions, building up one step at a time:

That one idea, **start with a file, then keep adding | something and look at the result each time**, is how administrators answer almost every question about logs and data. On a real server the log would have thousands of lines, and the very same pipeline would work unchanged.

Your first script

A shell script is just a text file of commands, run from top to bottom. You already know how to create a file with nano (lesson 10). Open nano hello.sh and type:

  GNU nano 7.2                        hello.sh *
#!/bin/bash
# My first script
name="Linux learner"
echo "Hello, $name!"
echo "Today is $(date +%A)."

^G Help      ^O Write Out ^W Where Is  ^K Cut       ^T Execute   ^C Location
^X Exit      ^R Read File ^\ Replace   ^U Paste     ^J Justify   ^/ Go To Line

We saved the file with Ctrl+O, Enter and Ctrl+X. Then:

ubuntu@practice:~$ cat hello.sh
#!/bin/bash
# My first script
name="Linux learner"
echo "Hello, $name!"
echo "Today is $(date +%A)."
ubuntu@practice:~$ bash hello.sh
Hello, Linux learner!
Today is Monday.
ubuntu@practice:~$ ./hello.sh
bash: ./hello.sh: Permission denied
ubuntu@practice:~$ ls -l hello.sh
-rw-r--r-- 1 ubuntu ubuntu 101 Oct  5 16:44 hello.sh
ubuntu@practice:~$ chmod +x hello.sh
ubuntu@practice:~$ ls -l hello.sh
-rwxr-xr-x 1 ubuntu ubuntu 101 Oct  5 16:44 hello.sh
ubuntu@practice:~$ ./hello.sh
Hello, Linux learner!
Today is Monday.
ubuntu@practice:~$

Making decisions: arguments, if, and exit codes

Useful scripts react to their input. This one takes a folder name and counts what is inside it:

  GNU nano 7.2                        count.sh *
#!/bin/bash
# Count the items in a folder
folder="$1"
if [ -z "$folder" ]; then
    echo "Usage: $0 FOLDER"
    exit 1
fi
if [ ! -d "$folder" ]; then
    echo "$folder is not a folder"
    exit 2
fi
count=$(ls "$folder" | wc -l)
echo "$folder contains $count items"

^G Help      ^O Write Out ^W Where Is  ^K Cut       ^T Execute   ^C Location
^X Exit      ^R Read File ^\ Replace   ^U Paste     ^J Justify   ^/ Go To Line

Run it, with echo $? after each run to show the exit code from lesson 4 (0 is success, anything else is a failure):

ubuntu@practice:~$ chmod +x count.sh
ubuntu@practice:~$ ./count.sh
Usage: ./count.sh FOLDER
ubuntu@practice:~$ echo $?
1
ubuntu@practice:~$ ./count.sh /usr/share
/usr/share contains 42 items
ubuntu@practice:~$ echo $?
0
ubuntu@practice:~$ ./count.sh /nonexistent
/nonexistent is not a folder
ubuntu@practice:~$ echo $?
2
ubuntu@practice:~$ ./count.sh /usr/share && echo "That worked"
/usr/share contains 42 items
That worked
ubuntu@practice:~$ ./count.sh /nonexistent || echo "That failed"
/nonexistent is not a folder
That failed
ubuntu@practice:~$ for f in *.txt; do echo "$f has $(wc -l < "$f") lines"; done
errors.txt has 1 lines
fruit.txt has 3 lines
list.txt has 96 lines
missing.txt has 2 lines
out.txt has 1 lines
sorted.txt has 3 lines
ubuntu@practice:~$

With no argument, the script printed its usage message and exited with 1. With a real folder it printed the count and 0. With a missing folder it complained and exited with 2. You choose the codes, and other programs (or other scripts) can read them.

The last lines show the two-way shortcuts: && means "and then, but only if the first command succeeded", and || means "or else, only if it failed". Exactly one of the two messages is printed. Finally, a loop: for f in *.txt; do ... done repeated the echo once for every .txt file in the folder, with $f holding each name in turn. Loops are how one short script handles ten files or ten thousand.

Where to go from here

You have now reached the end of Linux 101. You can find your way around, handle files and permissions, install software, manage processes and services, read logs, check your machine's health, edit files, and chain commands into scripts. The rest of the site is written for someone with exactly these skills:

A programmer's partner says: "Go to the shop and buy a litre of milk. If they have eggs, get a dozen."<br>
The programmer comes back with 12 litres of milk.<br>
"They had eggs!"

๐Ÿงช Try it yourself

In a practice folder of your own:

1. Count the files in /etc with ls and wc. 2. Save the listing of /etc into etc-list.txt, then append the date to the end of the same file. Check with tail. 3. Run ls /nonexistent /etc > good.txt 2> bad.txt and look at both files. 4. Make a small file of five lines with repeated words. Use sort | uniq -c | sort -rn on it. 5. Write a script greet.sh that takes a name as $1 and prints Hello, NAME!, and prints a usage message and exits with 1 if no name is given. Make it executable and run it both ways. 6. After each run, check echo $?.

The cheat sheet

← Lesson 10 · ↑ All lessons · Lesson 12: The Red Hat Family: Meet Rocky Linux →