Sign in
← Terminal: bash scripts
5 min · medium · Lesson 3 of 5

Sum script: numbers from a file

Read numbers from a file, add them up, print the result.

Summing numbers from a file comes up constantly: backup sizes, bytes from logs, daily payment totals. For a one-off, a command in the terminal is enough. If you need it every day or from cron, you wrap the command in a script that takes a file name as an argument and prints one number.

This lesson covers two ways to write that script: with awk and in plain shell with while read. The second way shows up often in other people's scripts, and it has traps that silently ruin the sum.

Where to practice

The examples below work in an ubuntu:24.04 container (awk there is mawk). You can start the same one yourself:

docker run --rm -it ubuntu:24.04 bash

The commands also work in your own terminal on Linux, macOS and in WSL2. We create files with cat and a heredoc, as in the previous lesson. The quotes around 'EOF' are required here: the script contains $1, and without quotes the shell would substitute it while creating the file.

Prepare a file with numbers, one per line:

printf '5\n10\n15\n20\n' > /tmp/input

awk sum script

awk sums a column with one program (covered in detail in the lesson "awk: summing numbers from a file"). The script just needs to pass it the file from the first argument:

cat > /tmp/sum.sh <<'EOF'
#!/bin/bash
awk '{s += $1} END {print s+0}' "$1"
EOF
chmod +x /tmp/sum.sh
/tmp/sum.sh /tmp/input
50

There are two different $1 here, and you shouldn't mix them up. Inside the single quotes, $1 is seen only by awk: it's the first field of the line. Outside, in "$1", the shell substitutes it: it's the script's first argument, the file name. The double quotes around "$1" keep a name with a space from splitting into two arguments.

print s+0 instead of print s matters for an empty file. The variable s never got a value then, and print s would print an empty line. s+0 turns it into the number 0.

Sum script in plain shell

The same result without awk: read the file line by line and add in $(( )) arithmetic.

cat > /tmp/sum-loop.sh <<'EOF'
#!/bin/bash
s=0
while read -r n; do
  s=$((s + n))
done < "$1"
echo "$s"
EOF
chmod +x /tmp/sum-loop.sh
/tmp/sum-loop.sh /tmp/input
50

How this works:

The script also works under sh (in Ubuntu that's dash): sh /tmp/sum-loop.sh /tmp/input prints 50 too.

Last line without a trailing newline

Files exported by programs, or saved by some editors, sometimes don't end with \n. You can see this with od -c:

printf '5\n10\n15\n20' > /tmp/nonl
od -c /tmp/nonl
0000000   5  \n   1   0  \n   1   5  \n   2   0
0000012

There's no \n after 20. Compare the two sum scripts on this file:

/tmp/sum-loop.sh /tmp/nonl
30
/tmp/sum.sh /tmp/nonl
50

The loop lost the last number, with no error at all. It comes down to read's exit code: it reads 20 and puts it into n, but doesn't find a \n and returns 1. while sees 1 and doesn't run the body for that line. awk counts the last line even without a \n.

The fix is a loop condition: keep going if read read a line, or the variable isn't empty.

while read -r n || [ -n "$n" ]; do
  s=$((s + n))
done < "$1"

With this condition the loop gives 50 on /tmp/nonl, and also 50 on the regular /tmp/input: there's no extra pass, because after the last full line n is empty.

Pipeline and subshell

Another common form of this loop feeds the file in through cat and a pipe.

cat > /tmp/sum-pipe.sh <<'EOF'
#!/bin/bash
s=0
cat "$1" | while read -r n; do
  s=$((s + n))
done
echo "$s"
EOF
chmod +x /tmp/sum-pipe.sh
/tmp/sum-pipe.sh /tmp/input
0

Each part of a pipeline joined by | runs in bash as a separate process, a subshell. The loop did add up the numbers correctly, but in its own copy of s. When the pipeline ended, that copy was gone, and echo printed the original s, which is 0. Same thing in dash. That's why you feed the file into the loop with a redirect, done < "$1", not with cat |.

What else breaks the sum

Comparing sum-loop.sh and sum.sh on different inputs.

What's in the filewhile read + $(( )) in bashawk
empty linecounted as 0counted as 0
fraction 1.5error syntax error: invalid arithmetic operator3.5 for 1.5 and 2
Windows newline (\r\n)error syntax error: invalid arithmetic operatorcounts correctly
empty file00 with s+0, empty line with s

Watch what happens to errors inside the loop. bash prints a message to stderr, drops out of the loop, and then echo prints whatever accumulated so far, and the script exits with code 0. If another program reads the script's output, it gets the wrong number and never learns there was an error.

An empty line is only safe with $((s + n)). Write $((s + $n)) instead, and the shell substitutes the empty $n first, giving $((s + )), and bash fails with syntax error: operand expected.

If the file is missing

Scripts with different sum logic behave differently:

/tmp/sum.sh /tmp/nope
awk: cannot open "/tmp/nope" (No such file or directory)

awk writes an error to stderr and exits with code 2. The loop also writes an error, but then echo prints 0, and the script exits with code 0:

/tmp/sum-loop.sh /tmp/nope
/tmp/sum-loop.sh: line 5: /tmp/nope: No such file or directory
0

A zero instead of an error is dangerous: the report will show "0 bytes", and no one will notice the file wasn't found. It's more reliable to check the file at the start of the script, the way you checked the argument count in the previous lesson:

if [ ! -r "$1" ]; then
  echo "sum.sh: can't read file: $1" >&2
  exit 1
fi

-r checks that the file exists and is readable.

Common mistakes

What you'll need for the tasks

Practice in your browser

Linux runs right here. Solve the task and we'll save your progress automatically.

01

Sum script

Create /root/sum.sh, which reads /tmp/input (one number per line) and prints their sum. Running sh /root/sum.sh should print a single number.

{ i }

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson terminal-scripts-03-sum-script