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

Zsh Glob Qualifiers: Filter Files Safely Before Commands Run

Use Zsh glob qualifiers to select files by type and metadata, control no-match behavior, and preview expansions before destructive commands.

Zsh glob qualifiers attach file-selection rules to a filename pattern. They let a command select regular files, directories, symbolic links, or files with particular metadata without piping a broad listing through another program. A qualifier is evaluated as part of filename generation, before the command receives its arguments. That timing matters: an unmatched pattern can stop command execution, a selected set can be much larger than expected, and a qualifier can even execute shell code if explicitly requested.

Qualifiers are Zsh-specific syntax. They are not portable POSIX shell globs, and they are not ordinary regular expressions. A script that uses them should declare Zsh as its interpreter and test with the target Zsh version. Keep the pattern readable, preview its expansion, and do not combine an ambiguous bare-qualifier syntax with ordinary pattern parentheses unless the expression is carefully escaped.

Filter by file type

The familiar glob *.log matches names; a qualifier adds a filesystem test. (.) selects plain files, (/) selects directories, and (@) selects symbolic links. Other qualifiers can select executable plain files, sockets, named pipes, and device files. Use the explicit qualifier that reflects what the command expects rather than assuming a filename suffix identifies the object’s type.

print -rl -- *.log(N.)
print -rl -- build(/N)
print -rl -- *.sh(*N)

N makes an unmatched pattern expand to an empty list rather than raising Zsh’s usual NOMATCH error. In the examples, the first command lists matching regular .log files, the second lists a directory named build if it exists, and the third lists executable plain files ending in .sh. print -rl -- prints each result on its own line and treats a leading hyphen as data instead of an option.

Null-on-no-match behavior is a policy choice. N is convenient for optional inputs, but it can also hide a typo or unexpectedly empty directory. For an operation that must find at least one file, omit N or check the resulting array length and fail with a useful diagnostic. Do not let an empty expansion silently become an invocation with no operands when the tool’s default behavior is broad or destructive.

Use explicit (#q...) syntax when clarity matters

Zsh supports a (#q...) qualifier form when EXTENDED_GLOB is enabled. It is unambiguous and can combine multiple qualifier groups as a logical AND. For example, a pattern can use (#qN.) to select regular files while allowing an empty result. The qualifiers must appear at the end of the pattern, just like the bare-parentheses form.

setopt extended_glob
print -rl -- **/*.log(#qN.)

This form is useful when a pattern already contains parentheses or when a reader might mistake trailing parentheses for part of the filename pattern. Enable only the pattern option the script needs, and consider setting options locally in a function so they do not change the caller’s interactive shell unexpectedly. In a reusable script, document whether EXTENDED_GLOB is required.

Recursive patterns such as **/*.log can traverse a large tree. Bound the search root, exclude generated or mounted directories where appropriate, and measure the expansion cost on the actual filesystem. A filename match across a repository or home directory can include build outputs, caches, symlinked content, or unexpected permission boundaries; verify the set before passing it to an operation.

Select by metadata conservatively

Glob qualifiers can test properties such as ownership, permissions, link count, size, access time, and modification time. These are useful for reports and maintenance candidates, but the metadata has operational limits. Modification time may be deliberately preserved by a copy, access time may be disabled or delayed by mount options, and ownership can be translated by a network or container filesystem.

Treat metadata selection as a snapshot. Between filename generation and the command opening a path, another process can replace the file, change a symlink, or alter permissions. A glob qualifier is not an atomic security check or a transaction. For a security-sensitive or race-sensitive workflow, use a program that opens entries relative to a directory file descriptor, verifies the opened object, and acts on that handle rather than trusting a previously expanded pathname.

Qualifiers that inspect timestamps or permissions can behave differently across filesystems. Test on the target volume and inspect representative edge cases such as broken symbolic links, hard links, files with no read permission, and directories mounted from another filesystem. Do not use a human-readable ls format as the authoritative input to a script; the pattern performs the selection, and the command should still handle failures at use time.

Keep matching and sorting separate

Glob qualifiers may also order and limit matches, but sorting a pathname list does not make an operation safe. If a script selects the “newest” logs, define which timestamp is authoritative and what happens when several files have the same time. A locale-dependent lexical order may differ from the order expected by a downstream tool. If reproducibility matters, set the locale deliberately and record the version and filesystem context.

When a task needs a stable sort, consider collecting candidates into an array and passing them through a dedicated sort step with an explicit key and locale. That can be easier to audit than a dense qualifier chain. If you use an order qualifier, test the exact Zsh syntax and whether the desired list is oldest-first or newest-first; reversal can change which files a range or head-like command sees.

Avoid relying on argument ordering for correctness. A command should not delete “all but the first” based on an implicit sort unless the selection policy is reviewed and tested. Large glob expansions can exceed the operating system’s argument-size limit; for large inventories, use a streaming tool such as find with a carefully designed predicate rather than expanding every path into one argument vector.

Preview before destructive actions

Before using a qualifier with rm, chmod, chown, or a custom maintenance command, print the exact selected paths. Use print -rl -- or an array, inspect the list, and check the count. Do not pass the expansion through eval; that converts path data back into shell syntax and can create command injection or quoting errors.

files=(**/*.tmp(N.))
printf 'candidates: %d\n' ${#files}
print -rl -- "${files[@]}"

The array preserves the list as separate shell words, but it does not eliminate time-of-check/time-of-use races or validate a downstream command’s semantics. Quote individual expansions where appropriate and use the command’s -- end-of-options marker when supported. If the action requires confirmation, place the review and confirmation in the same tool or program so the selected set cannot silently change between steps.

Do not use the e qualifier casually. That qualifier runs shell code for candidate filenames and can include or transform a name based on the code’s status. A +function qualifier can call a shell function as part of matching. These features are powerful for specialized workflows, but they make glob expansion executable logic. Review them like code, avoid them for untrusted directories, and never construct the qualifier body from an untrusted filename or user-provided string.

Diagnose no-match and unexpected-match behavior

If a pattern reports no matches found, determine whether that is the intended failure policy. Check the current directory, quoting, case sensitivity, hidden-file behavior, GLOB_DOTS, and whether the qualifier is in the final position. A pattern passed through a variable may need different quoting so that the pattern is expanded at the intended stage. Do not add N as a reflex: first decide whether “no candidate” should be harmless or an error.

If too many paths match, split the pattern into filename and metadata conditions and print intermediate results. Verify whether a symlink is being followed or selected as a link, whether recursive traversal crossed a mount, and whether the shell’s current options change behavior. Use a temporary directory with representative file types to test a new qualifier before adding it to a cleanup or deployment script.

If a command receives one literal wildcard instead of the expanded paths, check shell quoting and whether another shell is running the script. POSIX sh does not implement Zsh glob qualifiers. A script with a #!/bin/sh shebang must not contain (.N) or (#q...) syntax even if the developer’s interactive shell is Zsh.

Zsh glob qualifiers make filename selection concise, but they run during shell expansion and inherit the risks of any path-based operation. Keep syntax explicit, use no-match behavior intentionally, preview destructive matches, and switch to a purpose-built file walker when the job needs streaming, stable identity, or race-resistant handling.

Related:

Sources:

Comments