ls | grep alma | wc -l counts lines, not files¶
Level: 101 · you counted files with ls -l | grep alma | wc -l and believed the number
One line: wc -l counts the lines grep let through, so the answer depends on what ls printed: ls -l writes a symlink's target on the same line as its name, and a link to an alma file becomes a fourth match. grep -c alma gives the same count as grep alma | wc -l in one process, without the spaces a Mac's wc puts in front of the number. An unquoted alma* is not a pattern but a glob: where it matches file names, bash, zsh and fish all hand grep those names, and grep searches the files instead of the pipe. Quote the pattern.
Measured¶
The book's example counts three alma files with ls -l | grep alma | wc -l. The script below makes a directory with five empty files and one symlink, latest, which points at one of them, and counts them several ways:
Verified output of lsgrep_count_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ ls
alma9_install.txt
alma9_updates.txt
alma_mirror.txt
almond.txt
darkfi_notes.txt
latest
$ ls | grep alma
alma9_install.txt
alma9_updates.txt
alma_mirror.txt
$ ls | grep alma | wc -l | tr -d " "
3
$ ls | grep -c alma
3
$ ls -l | grep -c alma
4
$ ls -l | grep alma | sed "s/.* 2024 //"
alma9_install.txt
alma9_updates.txt
alma_mirror.txt
latest -> alma9_updates.txt
$ ls -l | wc -l | tr -d " "
7
$ ls -l | head -n 1 | cut -c 1-5
total
$ echo grep alma*
grep alma9_install.txt alma9_updates.txt alma_mirror.txt
$ ls | grep alma*; echo "status $?"
status 1
$ cd ../elsewhere
$ echo grep alma*
grep alma*
$ ls ../files | grep alma*
alma9_install.txt
alma9_updates.txt
alma_mirror.txt
almond.txt
$ ls ../files | grep "^alma"
alma9_install.txt
alma9_updates.txt
alma_mirror.txt
- Into a pipe,
lswrote one name per line, sogrepfiltered names andwc -lcounted them: 3.tr -d " "is there for the Mac, below. grep -c almaprinted the same 3.-ccounts the lines that matched, which is whatgrep | wc -lcounts. Lines, not matches: the encodings library measures the difference, below.ls -l | grep -c almaprinted 4. The long line for the symlink endslatest -> alma9_updates.txt, andgrepsearches the whole line, not only the name. The owner, group and date are on that line too;sedcuts each line after the year so the page shows only the part that matched.ls -l | wc -lprinted 7 for six entries, because the long listing starts with atotalline.- An unquoted
alma*is a glob, and the shell expands it before grep starts.echo grep alma*shows what grep received: three file names. The first became the pattern and the other two became files to search, so grep never read the pipe. Those files are empty, so it printed nothing and exited 1. No error, and a count of zero ifwc -lhad been next. - In a directory where nothing matches, bash leaves the word alone:
echoprintedgrep alma*, and grep got the regular expressionalma*. In a regular expression*repeats the character before it, soalma*isalmfollowed by any number ofa, and it also matchedalmond.txt. What the glob meant, a name that starts withalma, is^almato grep.
On a Mac¶
The key above is shared: /bin/bash 3.2, BSD grep, ls and sed printed the same bytes as their Linux counterparts, because the script strips wc's padding. Without tr -d " ", the two machines disagree:
Verified output of lsgrep_wc_padding_sh.sh on Linux — regenerated by tools/run_examples.py, never hand-typed.
$ ls | grep alma | wc -l
3
$ printf "[%s]\n" "$(ls | grep alma | wc -l)"
[3]
$ [ "$(ls | grep alma | wc -l)" -eq 3 ] && echo "equal to 3"
equal to 3
$ ls | grep alma | wc -l | tr -d " "
3
$ ls | grep -c alma
3
Verified output of lsgrep_wc_padding_sh.sh on macOS — regenerated by tools/run_examples.py, never hand-typed.
$ ls | grep alma | wc -l
3
$ printf "[%s]\n" "$(ls | grep alma | wc -l)"
[ 3]
$ [ "$(ls | grep alma | wc -l)" -eq 3 ] && echo "equal to 3"
equal to 3
$ ls | grep alma | wc -l | tr -d " "
3
$ ls | grep -c alma
3
macOS's wc put seven spaces in front of the 3, and they are part of the value $(...) captures, as the brackets show. The numeric test [ ... -eq 3 ] still said "equal to 3" on both machines, so arithmetic is safe; printing the count, or comparing it as text, is where the spaces show. grep -c printed a bare 3 on both, which makes it the portable way to count matching lines.
In zsh and fish¶
zsh counts the same, and its [[ ... -eq 3 ]] accepted the Mac's padded number too. Where zsh differs is the unquoted alma* in a directory with nothing to match:
Verified output of lsgrep_count_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ ls | grep alma | wc -l | tr -d " "
3
$ [[ $(ls | grep alma | wc -l) -eq 3 ]] && echo "equal to 3"
equal to 3
$ echo grep alma*
grep alma9_install.txt alma9_updates.txt alma_mirror.txt
$ ls | grep alma*; echo "status $?"
status 1
$ cd ../elsewhere
$ ls ../files | grep alma*; echo "status $?"
(eval):1: no matches found: alma*
status 1
$ ls ../files | grep "alma*"
alma9_install.txt
alma9_updates.txt
alma_mirror.txt
almond.txt
$ ls ../files | grep "^alma"
alma9_install.txt
alma9_updates.txt
alma_mirror.txt
no matches found: alma* and status 1: zsh did not run grep at all. Where files matched, zsh expanded alma* to their names exactly as bash did. An unquoted [:lower:] is a glob is the same rule with square brackets, and it covers setopt no_nomatch, the option that gives zsh bash's pass-through.
fish also expands * where files match and stops where none do:
Verified output of lsgrep_count_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ ls | grep -c alma
3
$ test (ls | grep alma | wc -l) -eq 3; and echo "equal to 3"
equal to 3
$ string trim -- (ls | grep alma | wc -l)
3
$ echo grep alma*
grep alma9_install.txt alma9_updates.txt alma_mirror.txt
$ ls | grep alma*; echo "status $status"
status 1
$ cd ../elsewhere
$ ls ../files | grep alma*; echo "status $status"
fish: No matches for wildcard 'alma*'. See `help language#wildcards-globbing`.
ls ../files | grep alma*; echo "status $status"
^~~~^
status 124
$ ls ../files | grep "alma*"
alma9_install.txt
alma9_updates.txt
alma_mirror.txt
almond.txt
$ ls ../files | grep "^alma"
alma9_install.txt
alma9_updates.txt
alma_mirror.txt
test (ls | grep alma | wc -l) -eq 3 said "equal to 3" on both machines, so fish's test accepted the padded number as well, and string trim removes the spaces when you want to print the count. The unquoted alma* in the empty directory is an error with a caret under the word, and status 124. The error stopped the pipeline, but not the line: the echo after the ; still ran.
| bash | zsh | fish | |
|---|---|---|---|
alma* matches files here |
grep gets the file names | grep gets the file names | grep gets the file names |
alma* matches nothing |
grep gets alma* |
error, status 1; grep does not run | error, status 124; grep does not run |
| always right | grep '^alma' |
grep '^alma' |
grep '^alma' |
If you are coming from another library¶
- Encodings.
grepon text that is not ASCII ↗ measuresgrep -cagainstgrep -o | wc -lon one file: three matching lines, four matches. It is the count this page's-cdoes not give.
See also¶
- A pipeline reports its last command — the status of
ls | grep alma | wc -liswc's, even when grep matched nothing cat file | grepis one process too many — what grep can tell you when it gets names instead of a stream- An unquoted
[:lower:]is a glob - GNU grep manual ↗ and FreeBSD
grep(1)↗ —-c - fish language, wildcards ↗