Skip to content
Shell & TerminalDeep Dive Published Updated 7 min readViews unavailable

GNU grep -z vs -Z: NUL-Delimited Data Records and Filename Boundaries

Distinguish GNU grep's NUL-delimited input records from NUL-terminated filename output, and avoid newline-path corruption and whole-file memory surprises.

GNU grep has two options whose names look almost interchangeable but act on different boundaries. Lowercase -z changes how grep divides input and selected output into records. Uppercase -Z changes the separator after a filename that grep prints. Confusing them can turn a NUL-safe pathname pipeline back into an ambiguous newline stream, or make a search read a much larger record than intended.

This distinction matters when Unix pathnames may contain newlines, when an upstream tool emits NUL-separated names, or when a search deliberately spans line breaks. It is a GNU grep contract, not a general promise about every program named grep; test the installed implementation and its companion utilities as a set.

Two options, two different boundaries

Option Boundary it changes Typical purpose
-z / --null-data Input and output data records use NUL instead of newline Filter a NUL-separated stream or treat multiline text as one record
-Z / --null A printed filename is followed by NUL instead of its ordinary separator Pass matching filenames safely to a NUL-aware consumer

The GNU Grep manual defines -z as treating input and output data as lines terminated by the ASCII NUL byte. Its separate -Z option substitutes NUL for the character that normally follows a filename; for example, grep -lZ prints a NUL after each selected filename. -z does not mean “print filenames safely,” and -Z does not change the record delimiter used to search input.

Do not copy the uppercase option blindly onto macOS. Apple’s published text_cmds grep(1) manual assigns -Z to --decompress, while its source assigns -z to --null-data; FreeBSD 15.1 documents --null for filename output and -z for NUL-data records, without GNU’s -Z spelling. The grep -lZ recipe below is therefore GNU-specific. On macOS, install or otherwise select GNU grep (commonly named ggrep) before using that form, or use the exact filename-delimiter option documented by the target implementation.

The short option’s case is therefore part of the interface. A command review that only notices “null” or “zero” in the option name is not enough. Record both the data framing and filename framing in the pipeline contract.

Filter a NUL-separated pathname stream with -z

find -print0 emits pathnames separated by NUL, which is the one byte a pathname cannot contain. GNU grep -z can treat each such pathname as one record and preserve the NUL record separator for selected output:

find . -type f -print0 |
  LC_ALL=C grep -z -E '(^|/)policy\.toml$' |
  xargs -0 -r -n 1 wc -l

This example selects paths whose final component is policy.toml, then passes each selected name as an argument to wc. A newline, space, tab, quote, or backslash in a directory component remains part of one NUL-delimited record; it is not reinterpreted as a new path by grep or xargs. The LC_ALL=C prefix makes this ASCII-pattern filter byte-oriented, so path bytes that are invalid in a UTF-8 locale are not lost to multibyte decoding or binary-output policy. The -0 on xargs is essential: replacing it with ordinary line-oriented xargs would break the producer-consumer contract. The -r flag asks GNU xargs not to run wc when there are no selected paths. FreeBSD 15.1 accepts -r only for GNU compatibility and documents it as a no-op because its default already avoids running the utility on empty input; do not rely on -r for portable behavior.

wc’s display is for humans, not a safe machine-readable pathname protocol: an embedded newline in a printed name can still make its output visually ambiguous. The important property in this example is that the path reaches wc as one argument. If later stages need to consume names, keep their interfaces NUL-aware instead of parsing this diagnostic output.

These pipelines demonstrate pathname framing, not robust error propagation. GNU grep normally returns 0 when it selects a record, 1 when it selects none, and 2 for an error; with -q, however, a selected record makes its status 0 even if an error also occurred. Ordinary pipeline status usually reflects only the final command. xargs -r can therefore exit successfully after an upstream grep or find error. Production scripts should capture and check each producer’s status, while treating grep’s expected no-match status separately; blindly enabling pipefail also makes an ordinary no-match look like a pipeline failure.

The same principle applies when a command reports matching files directly. In this mode -Z, not -z, is the option that protects the filename list:

grep -lZ -F -e 'deployment=enabled' -- ./*.conf |
  xargs -0 -r -n 1 wc -l

-l asks grep to report selected filenames, and -Z puts a NUL after each name. The consumer’s -0 parses that output using the same separator. This example searches ordinary newline-delimited configuration files; adding -z would change their record model and is not required merely because a filename might contain a newline. Conversely, -Z alone does not make input records NUL-delimited.

The ./*.conf argument also assumes the glob matches at least one file. Bash normally leaves an unmatched pattern literal, causing grep to report a missing file; zsh normally rejects the unmatched glob before starting grep. If an empty match set is a routine case, use explicit file discovery and handle its exit status rather than relying on shell-specific glob behavior.

-z also changes what a “line” means

With -z, newline bytes inside the input are data within a record, not record boundaries. A regular expression is evaluated against the NUL-delimited record, which may contain newline bytes; do not assume every regex mode or operator treats embedded newlines the same way. Test the exact expression and GNU grep version when multiline matching is a requirement. This is useful for bounded inputs where the record grammar is explicitly NUL-terminated or where a deliberate multiline match is needed.

Do not assume -z is a harmless toggle for an ordinary large text file. The GNU manual warns that when an input file contains no NUL byte, grep -z must read the entire file into memory before it can process any of it. A file that is normally streamed as many newline-delimited lines can therefore become one large record. For large logs, generated streams, or unbounded input, this may create high memory use or exhaust available memory. Use it only when the NUL framing or bounded whole-record behavior is part of the design, and test with the largest realistic input.

NUL framing is also not synonymous with binary-file mode. GNU grep documents separate behavior for binary files and for -a (--binary-files=text); -z changes what separates records. If the requirement is to inspect arbitrary binary payloads, choose and test the binary handling options deliberately rather than assuming -z makes grep a general-purpose binary parser.

Test both sides of the pipeline contract

Before deploying a pipeline, record which implementation supplies each option. Check grep --version, and check that find and xargs provide the corresponding NUL options on the target system. Test with pathnames containing newline bytes and bytes invalid under the intended multibyte locale; the first example uses LC_ALL=C for the latter. Do not silently fall back to newline delimiters if the deployment platform lacks one component; either select a compatible toolchain or redesign the interface for the supported platform.

Use a disposable fixture with a directory name containing a newline and a matching basename. Compare the argument handling of the NUL-aware pipeline with an ordinary line-delimited pipeline, and inspect raw output with a byte display such as od -An -tx1 if separator placement is in doubt. Also test a no-match input so the xargs -r behavior is visible, and run the -z form against a realistically large file without NUL bytes before adopting it for broad searches.

The decision rule is compact: use -z when the data records themselves are NUL-delimited or the search intentionally treats newline-separated text as one NUL-terminated record. Use -Z when grep is printing filenames that must survive arbitrary pathname characters. When both data and names have distinct framing requirements, reason about each boundary independently and keep every downstream consumer aligned with the stream it receives.

Related:

Sources:

Comments