In short: jj resolves -r (default @), turns the positional filesets into
a matcher, and parses --pattern as kind:pattern, with the kind defaulting
to regex. For each matching entry it materializes the value. A file's bytes are split
on \n, and each matching line is printed as path:line (or
path:N:line with -n, or just the path with --name-only).
Symlinks, submodules and non-file conflicts are skipped. Nothing is written.
The command is cmd_file_search, and the line
printer is write_matches. The help text:
"Search for content in files", followed by "Prints each line that matches the specified pattern,
prefixed by the file path. Use --name-only to print only the file paths"
(source).
1. Command-line surface
| Argument | Type | Default | Meaning |
|---|---|---|---|
-p, --pattern PATTERN (required) | string pattern | kind regex | The pattern each line is tested against. A glob has to match the whole line, so use 'glob:*foo*' to find foo anywhere (source). |
FILESETS (positional, repeatable) | fileset | all files | Only search files these match. Several are unioned (source). |
-r, --revision REVSET | revset | @ | The commit whose tree is searched. It must resolve to exactly one commit (source). |
--name-only | flag | off | Print each matching file's path once instead of its lines. Can't be combined with -n (source). |
-n, --line-number | flag | off | Prefix each match with its 1-based line number (source). |
As with jj file list, the help calls the positional arguments "prefixes", but each is
a full fileset expression, parsed by parse_file_patterns
(cli_util.rs, fileset.rs).
Upstream's fileset docs cover the language.
--pattern is split at its first colon (source).
The part before the colon has to be one of the eight kinds from_str_kind knows
(str_util.rs), so foo:bar is an error rather than
a regex. Colons after the first are part of the pattern, so regex-i:foo:bar searches for
foo:bar. To search for a literal colon in a regex, spell the kind out.
| Kind | A line matches when |
|---|---|
regex (default), regex-i | The regular expression matches somewhere in the line. It runs on bytes, so non-UTF-8 files are searched too. |
substring, substring-i | The line contains the text. |
glob, glob-i | The whole line matches the glob. A glob with no wildcard characters becomes exact. |
exact, exact-i | The whole line equals the text, so exact: finds blank lines. |
StringPattern,
glob,
match_lines.
exact-i and substring-i fold ASCII case only; regexes are Unicode-aware
(source).
2. Configuration
There is no template and no config key of its own: the output format is hard-coded, and a TODO says
so (source). Paths are printed relative to the current
directory with format_file_path, so running in dir/ prints
file3:… rather than dir/file3:…. revset-aliases apply when
resolving -r, fileset-aliases when parsing the filesets, and
ui.pager controls paging. The matched line is written as raw bytes, without colour.
3. End-to-end flow
4. Core architecture touchpoints
| Structure | Read / write | How | Explained on |
|---|---|---|---|
| Commit | read | Resolved from -r; only its tree is used. | commits |
| Trees | read | Walked with entries_matching, exactly as jj file list does (source). | file list, trees |
| File contents | read | Every matched file's blob is read in full through the store (source). | backends |
| Conflicts | read | A conflict of files is simplified and each term's contents read; only the add sides are searched (source). | conflicts |
| Filesets and matchers | read | The positional arguments become a Matcher (source). | upstream docs |
| Revset engine | read | Resolves -r to one commit (source). | revsets |
| Working copy | read, may snapshot | Snapshotted by the shared lifecycle, so unsaved edits on disk are searched in @. | working-copy |
5. What it reads, and snapshotting
- No transaction, no operation of its own. The command only reads the repo it loaded (source).
- Snapshot:
workspace_helpersnapshots the working copy first (source). If files changed on disk, that snapshot records its own operation, sojj file searchsees what is on disk now, not the last snapshot. - Whole blobs, from the store. Even for
@, contents come from the commit's tree objects, not the files on disk. Each file is read to the end into memory (read_all), and binary files are searched like any other. Symlink targets are read too, by the materializer, but never searched. - Concurrent reads, ordered output. Up to the backend's
concurrency()entries are materialized and read at once, withbuffered, which still hands results back in tree order. Matching and printing happen one file at a time on the command's side (source). jj undohas nothing to revert, except the snapshot if one was taken.
6. Algorithm and invariants
Each entry the walk yields is passed to materialize_tree_value
(source), and a resolved file's bytes are read in the same
buffered step (source). The command then branches on the
result:
Line matching. write_matches splits the contents with
split_inclusive('\n'), numbering pieces from 1. It strips the trailing \n
before testing, so patterns can anchor to the line's end, then prints the prefix and the original
line, adding a newline when the last line has none (source).
--name-only uses match_lines, which splits the same way, and stops at the
first match (source). Only \n separates lines, so
a CRLF line is tested with its \r still attached.
Conflicts. try_materialize_file_conflict_value applies only when every
term is a file or absent. It simplifies the merge, dropping pairs that cancel out, and reads each
remaining term; an absent side reads as empty (source,
get_file_contents). The command then searches only the
adds(): the contents each side wants, never the base it diverged from, and never the
marker text written to the working copy. With -n, numbering restarts on each side,
because there is no line numbering shared across sides (source).
Invariants: output follows tree order (depth-first, name order) however the reads finish, and within a file,
line order (sides in merge order for a conflict). Each matching line is printed once per side it
appears on; with --name-only each path at most once. Literal paths that name nothing
get one warning afterwards (explicit_paths). An entry that
matches the filesets but no lines produces no output.
7. Errors and edge cases
| Message | When | Source |
|---|---|---|
Error: Invalid string pattern kind `foo:` (exit 2) | The text before the first colon isn't a known kind. It is a CLI error, like a bad flag. | str_util.rs, search.rs |
| Glob or regex syntax error (exit 2) | The pattern doesn't compile; the globset or regex error is shown as-is. | str_util.rs |
Warning: Skipping '<path>' due to permission error: … | The backend refused to read a file. The search continues. | search.rs, conflicts.rs |
Warning: No matching entries for paths: <paths> | A literal path in the filesets is absent from the tree. The command still succeeds. | cli_util.rs |
Revset `…` resolved to more than one revision (or to none) | -r isn't exactly one commit. | revset_util.rs |
| Fileset parse error | An argument isn't a valid fileset, or names a path outside the workspace. | cli_util.rs |
| Usage error | --name-only together with -n, or no --pattern; clap rejects it before the command runs. | search.rs |
| Backend error | A blob or subtree can't be read for another reason; the command stops after the lines already printed. | search.rs |
A pattern that matches nothing prints nothing and exits 0; there is no "no matches" exit status as in
grep.
8. Example output
From upstream's test_file_search. @- adds
file1 (-foo-) and file2 (-bar-); @
adds dir/file3 (-foobar-):
$ jj file search --pattern 'glob:*foo*'
dir/file3:-foobar-
file1:-foo-
$ jj file search --pattern 'glob:foo' # a glob must match the whole line
$ jj file search --pattern 'glob:*foo*' -r @-
file1:-foo-
$ jj file search --pattern 'glob:*foo*' file9
Warning: No matching entries for paths: file9
$ jj file search --pattern foo:bar
Error: Invalid string pattern kind `foo:`
Every matching line, with line numbers, or only the path:
$ printf 'hit-one\nmiss\nhit-two\nhit-three\n' > multi
$ jj file search -n --pattern hit multi
multi:1:hit-one
multi:3:hit-two
multi:4:hit-three
$ jj file search --name-only --pattern hit multi
multi
From test_file_search_conflicts: file1 conflicts
with add sides -foo- and -baz- and base -bar-. Each side is
searched on its own, so both report line 1. The base and the conflict markers are never matched:
$ jj file search -n --pattern 'regex:-(foo|baz)-'
file1:1:-foo-
file1:1:-baz-
$ jj file search --pattern 'glob:*bar*'
$ jj file search --pattern 'glob:*%%%*'