The awk program goes in single quotes¶
Level: 101 · for anyone whose awk printed whole lines instead of one field
One line: awk '{print $1}' hands awk the program as typed, awk "{print $1}" hands the shell a $1 first — and bash, zsh and fish all expand it, to nothing, without a word, leaving awk the program {print }, which prints the whole line.
An awk program is a program in another language that has to travel through the shell to get there. $1 means "the first field of this line" to awk and "the first argument of this script" to bash and zsh. Single quotes are the border: inside them, the shell touches nothing.
Measured¶
Verified output of awk_quoting_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ awk '{print $1}' log.txt
03:02:36
03:02:37
03:04:01
03:05:12
$ awk "{print $1}" log.txt
03:02:36 sshd: Disconnected from invalid user admin 10.0.0.7
03:02:37 sshd: Disconnected from invalid user root 10.0.0.9
03:04:01 sshd: Disconnected from invalid user admin 10.0.1.4
03:05:12 sshd: Accepted publickey for ci from 10.0.2.2
$ awk '{print NF, $NF}' log.txt
8 10.0.0.7
8 10.0.0.9
8 10.0.1.4
8 10.0.2.2
$ awk '/invalid/ {print $NF}' log.txt
10.0.0.7
10.0.0.9
10.0.1.4
$ printf 'a:b:c\n' | awk -F: '{print $2}'
b
$ awk 'END {print NR}' log.txt
4
$ printf '3\n4\n' | awk '{total += $1} END {print total}'
7
$ awk '$2 == "sshd:" {n++} END {print n}' log.txt
4
$ cat report.sh
awk "{print $1}" log.txt
$ bash report.sh
03:02:36 sshd: Disconnected from invalid user admin 10.0.0.7
03:02:37 sshd: Disconnected from invalid user root 10.0.0.9
03:04:01 sshd: Disconnected from invalid user admin 10.0.1.4
03:05:12 sshd: Accepted publickey for ci from 10.0.2.2
$ bash report.sh ONE
$ bash strict.sh ; echo "status $?"
strict.sh: line 2: $1: unbound variable
status 1
The first two commands differ by one character and produce completely different answers. Nothing warns: with no arguments set, $1 expands to the empty string, {print } is a valid awk program, and print with nothing after it prints $0 — the whole line. It looks like awk ignored your program, and in a sense it did: your program never arrived.
The rest of the block is the vocabulary that makes awk worth reaching for in the first place:
$1,$2,$NFare fields, split on runs of whitespace;NFis how many there are and$NFis the last one, which is how you get at a trailing column without counting.-F:sets the separator, for/etc/passwd-shaped data.- A pattern before the block —
/invalid/ { … }, or a comparison like$2 == "sshd:"— runs the block only on the lines that match, which isgrepandcutin one process. END { … }runs once after the last line, withNRholding the number of lines read. Accumulating into a variable and printing it inENDis how a column becomes a total.
Then the trap in its natural habitat: report.sh is one line long, and it works — until someone passes it an argument. $1 becomes ONE, the program becomes {print ONE}, ONE is an awk variable nobody assigned, an unassigned awk variable is the empty string, and the report prints one blank line per input line. set -u catches it, which is the last command: with nounset on, the shell refuses to expand $1 at all and the script stops with a message that names the real problem.
On a Mac¶
Nothing changes — this page's key is shared, which is itself the finding. The two machines do not even run the same awk: Ubuntu's /usr/bin/awk is mawk, and macOS ships the one-true-awk that Aho, Weinberger and Kernighan wrote. Every line above prints the same bytes through both. Field splitting, NF, -F, patterns, END, uninitialised variables and string-to-number rules are POSIX, and for the everyday work of this chapter the two implementations agree.
They part on the extensions. gensub, the function most often reached for in a one-liner found online, is GNU awk's and exists in neither of these, and each refuses it in its own words:
macOS $ awk 'BEGIN{print gensub(/a/,"X","g","aa")}'; echo "status $?"
awk: calling undefined function gensub
source line number 1
status 2
Linux $ awk 'BEGIN{print gensub(/a/,"X","g","aa")}'; echo "status $?"
awk: line 2: function gensub never defined
status 2
If a script needs gensub, asort or the other GNU additions, it needs gawk by name — brew install gawk on a Mac, apt install gawk on Ubuntu — and saying gawk in the script is the honest version of that dependency. sub() and gsub(), which cover most of what people want gensub for, are in all three.
In zsh and fish¶
The same, in both — including the silent part.
Verified output of awk_quoting_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ awk '{print $1}' log.txt
03:02:36
03:05:12
$ awk "{print $1}" log.txt
03:02:36 sshd: Disconnected from invalid user admin 10.0.0.7
03:05:12 sshd: Accepted publickey for ci from 10.0.2.2
$ zsh -f report.zsh
03:02:36 sshd: Disconnected from invalid user admin 10.0.0.7
03:05:12 sshd: Accepted publickey for ci from 10.0.2.2
$ zsh -f report.zsh ONE
fish has no positional $1 at all; its arguments live in $argv. That does not save you: fish expands $1 in a double-quoted string to nothing, just as bash and zsh do, and the double-quoted program collapses the same way. Written fish-style, with $argv[1], it collapses for the same reason.
Verified output of awk_quoting_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ awk '{print $1}' log.txt
03:02:36
03:05:12
$ awk "{print $1}" log.txt
03:02:36 sshd: Disconnected from invalid user admin 10.0.0.7
03:05:12 sshd: Accepted publickey for ci from 10.0.2.2
$ cat report.fish
awk "{print $argv[1]}" log.txt
$ fish --no-config report.fish
03:02:36 sshd: Disconnected from invalid user admin 10.0.0.7
03:05:12 sshd: Accepted publickey for ci from 10.0.2.2
$ fish --no-config report.fish ONE
So the rule is one rule in all three shells: single-quote the program. When the program genuinely needs a value from the shell, do not interpolate it — pass it in with -v: awk -v name="$user" '$3 == name' keeps the program a constant and the data a variable, and it is quoted the same way in bash, zsh and fish.
If you are coming from another library¶
- Perl.
perl -lane 'print $F[0]'is the same idea with@Fin place of$1, and the same quoting rule for the same reason. The Perl library ↗ covers the one-liner switches. - Encodings. The tools chapter ↗ measures what
awkandcutdo to fields that are not ASCII, where "one character" and "one byte" stop being the same thing. - Python.
line.split()splits on runs of whitespace and drops leading ones, exactly likeawk's default;line.split(":")does not, exactly likeawk -F:.
See also¶
uniqonly compares neighbours —awk '{print $1}'is how to read its padded countsxargsturns lines into arguments — the other place quoting decides what a program receives- An unquoted
[:lower:]is a glob — the same border, seen from the shell's side - The GNU Awk User's Guide ↗, POSIX
awk↗, and the one-true-awk ↗ that macOS ships