Skip to content

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, ls wrote one name per line, so grep filtered names and wc -l counted them: 3. tr -d " " is there for the Mac, below.
  • grep -c alma printed the same 3. -c counts the lines that matched, which is what grep | wc -l counts. Lines, not matches: the encodings library measures the difference, below.
  • ls -l | grep -c alma printed 4. The long line for the symlink ends latest -> alma9_updates.txt, and grep searches the whole line, not only the name. The owner, group and date are on that line too; sed cuts each line after the year so the page shows only the part that matched.
  • ls -l | wc -l printed 7 for six entries, because the long listing starts with a total line.
  • 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 if wc -l had been next.
  • In a directory where nothing matches, bash leaves the word alone: echo printed grep alma*, and grep got the regular expression alma*. In a regular expression * repeats the character before it, so alma* is alm followed by any number of a, and it also matched almond.txt. What the glob meant, a name that starts with alma, is ^alma to 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. grep on text that is not ASCII ↗ measures grep -c against grep -o | wc -l on one file: three matching lines, four matches. It is the count this page's -c does not give.

See also