{"thread":{"id":"65748","subject":"[PATCH 0/6] t: add lint-style.pl and convert grep to test_grep","startedAt":"2026-06-04T07:46:01Z","lastAt":"2026-07-06T05:02:14Z","messageCount":40,"participants":["Michael Montalbo via GitGitGadget","D. Ben Knoble","Michael Montalbo","Junio C Hamano","SZEDER Gábor"],"isPatch":true,"patchVersion":1,"patchTotal":6},"messages":[{"id":"544679","messageId":"pull.2135.git.1780559158.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":null,"subject":"[PATCH 0/6] t: add lint-style.pl and convert grep to test_grep","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-04T07:45:52Z","receivedAt":"2026-06-04T07:46:01Z","isPatch":true,"body":"The test suite has a test_grep wrapper that prints file contents on\nassertion failure, making debugging easier. Many tests still use bare 'grep'\nfor assertions, which silently swallows context on failure.\n\nThis series adds a lint tool (lint-style.pl) to mechanically detect and\nconvert these, then applies it across the test suite.\n\nThe tool reuses the shared shell parser (t/lib-shell-parser.pl) rather than\nreimplementing shell parsing. This gives us proper handling of heredocs,\n$(...), pipes, quoting, case patterns, and control flow structures for free.\nThe only new parsing logic is the grep assertion classifier, which works on\nthe already-parsed token stream.\n\nThe classifier distinguishes grep used as an assertion (checking exit status\nagainst PATTERN + FILE) from grep used as a filter (pipes, redirects, $(),\ncontrol flow conditions). Only assertions are converted. Greps at assertion\nlevel with no file argument are flagged as likely bugs; three pre-existing\ninstances were found and fixed in patch 5.\n\nThe --fix mode converts mechanically: 'grep' becomes 'test_grep', '! grep'\nbecomes 'test_grep !'.\n\nStructure:\n\n1/6 t/README: document test_grep helper 2/6 t: extract chainlint's parser\ninto shared module 3/6 t: fix Lexer line count for $() inside double-quoted\nstrings 4/6 t: add lint-style.pl with test_grep negation rule 5/6 t: fix\ngrep assertions missing file arguments 6/6 t: lint and convert grep\nassertions to test_grep\n\nPatch 2 extracts chainlint.pl's Lexer, ShellParser, and ScriptParser into\nt/lib-shell-parser.pl. ScriptParser's check_test() becomes a no-op in the\nmodule; chainlint.pl defines ChainlintParser (a ScriptParser subclass that\nruns TestParser for &&-chain detection), and lint-style.pl defines\nLintParser (a ScriptParser subclass that runs grep lint rules).\n\nPatch 3 fixes a pre-existing bug in scan_dqstring where the post-loop\nnewline counter re-counted newlines that were already counted during\nrecursive $() parsing. The fix counts newlines inline as non-special\ncharacters are consumed, removing the catch-all counter entirely. chainlint\nis unaffected (it uses byte offsets), but lint-style.pl needs accurate token\nline numbers to locate and fix specific lines in the original file.\n\nPatch 4 introduces the lint-style.pl framework (LintParser subclass,\nMakefile targets, fixture infrastructure) with a small, complete rule (!\ntest_grep -> test_grep !) so reviewers can see the machinery in action\nbefore the bulk conversion.\n\nPatch 5 fixes three test bugs where grep assertions were missing their file\narguments, causing them to pass vacuously (all three pass with the corrected\narguments). These were independently discovered by the missing-file\ndetection rule introduced in patch 6.\n\nPatch 6 adds the main rule, its fixtures, and the mechanical conversion of\n~2800 assertions across ~340 files, including sourced test fragments in\nt/t5411/ and heredoc test bodies in t/t5564/.\n\nTo verify the conversion (patch 6 adds both the rule and the mechanical\nconversion in the same commit, so apply the full series and re-run --fix to\nconfirm it produces no further changes):\n\ngit checkout && perl t/lint-style.pl --fix t/t*.sh t/test-lib*.sh t/lib-.sh\nt/-tests.sh t/perf/.sh t/t5411/.sh\n\nAs an independent completeness check:\n\ngrep -rn '^\\s*!\\sgrep\\s' t/t*.sh t/lib-.sh t/-tests.sh\nt/test-lib*.sh t/perf/*.sh | grep -v test_grep | grep -v lint-ok | grep -v\n'#.*grep'\n\nFuture rules:\n\nThe framework is designed to make adding new rules cheap. Each rule is a\nfunction that receives parsed commands and the token stream. The harness\nhandles tokenization, line mapping, --fix, and fixture testing. Three\nnatural follow-ups:\n\n * 'test_must_fail grep': test_must_fail distinguishes expected failures\n   from crashes (signals), which only makes sense for git commands. Using it\n   with grep or test_grep should be '! grep' or 'test_grep !' instead.\n\n * '! git cmd': should use test_must_fail, which distinguishes controlled\n   failures from crashes. The README explicitly documents this as a \"don't\".\n\n * 'test -f' / 'test -d' / 'test -e': should use test_path_is_file,\n   test_path_is_dir, test_path_exists. The helpers print the actual\n   directory listing on failure; bare 'test' just says \"failed\".\n\nKnown limitations:\n\n * One grep in t/t1400 asserts against .git/packed-refs which does not exist\n   on the reftable backend; suppressed with '# lint-ok'.\n\n * One grep in t/t7450 checks a path inside a clone that may have failed\n   (MINGW-only test); the file may not exist, so test_grep's existence check\n   would trip; suppressed with '# lint-ok'.\n\n * Two greps in t/t3901 inside case branches that inherit piped stdin from\n   two lines above are suppressed with '# lint-ok'.\n\n * One grep in t/t6437 uses glob expansion (grep -q content *) which breaks\n   test_grep's file check; suppressed with '# lint-ok'.\n\n * One grep in t/t7527 captures $? for later use rather than asserting\n   inline; suppressed with '# lint-ok'.\n\nMichael Montalbo (6):\n  t/README: document test_grep helper\n  t: extract chainlint's parser into shared module\n  t: fix Lexer line count for $() inside double-quoted strings\n  t: add lint-style.pl with test_grep negation rule\n  t: fix grep assertions missing file arguments\n  t: lint and convert grep assertions to test_grep\n\n t/.gitattributes                             |   2 +\n t/Makefile                                   |  37 +-\n t/README                                     |  21 +\n t/chainlint.pl                               | 521 +-----------------\n t/check-shell-parser.pl                      |  58 +++\n t/for-each-ref-tests.sh                      |  12 +-\n t/lib-bitmap.sh                              |  12 +-\n t/lib-bundle-uri-protocol.sh                 |  26 +-\n t/lib-httpd.sh                               |   2 +-\n t/lib-shell-parser.pl                        | 522 +++++++++++++++++++\n t/lint-style.pl                              | 425 +++++++++++++++\n t/lint-style/grep-assert.expect              |  13 +\n t/lint-style/grep-assert.test                |  24 +\n t/lint-style/grep-fix.expect                 |  16 +\n t/lint-style/grep-fix.test                   |  16 +\n t/lint-style/grep-missing-file.expect        |   4 +\n t/lint-style/grep-missing-file.test          |   6 +\n t/lint-style/grep-negated.expect             |   5 +\n t/lint-style/grep-negated.test               |   9 +\n t/lint-style/grep-not-assert.expect          |   0\n t/lint-style/grep-not-assert.test            |  43 ++\n t/lint-style/heredoc.expect                  |   3 +\n t/lint-style/heredoc.test                    |  14 +\n t/lint-style/test-grep-negation-fix.expect   |   4 +\n t/lint-style/test-grep-negation-fix.test     |   4 +\n t/lint-style/test-grep-negation.expect       |   3 +\n t/lint-style/test-grep-negation.test         |   4 +\n t/pack-refs-tests.sh                         |   2 +-\n t/show-ref-exists-tests.sh                   |   2 +-\n t/t0000-basic.sh                             |  16 +-\n t/t0001-init.sh                              |  18 +-\n t/t0008-ignores.sh                           |   8 +-\n t/t0009-git-dir-validation.sh                |   6 +-\n t/t0012-help.sh                              |   4 +-\n t/t0013-sha1dc.sh                            |   2 +-\n t/t0017-env-helper.sh                        |   4 +-\n t/t0021-conversion.sh                        |  18 +-\n t/t0029-core-unsetenvvars.sh                 |   4 +-\n t/t0030-stripspace.sh                        |   4 +-\n t/t0031-lockfile-pid.sh                      |   2 +-\n t/t0040-parse-options.sh                     |  52 +-\n t/t0041-usage.sh                             |   2 +-\n t/t0052-simple-ipc.sh                        |  10 +-\n t/t0061-run-command.sh                       |   2 +-\n t/t0066-dir-iterator.sh                      |   2 +-\n t/t0068-for-each-repo.sh                     |  16 +-\n t/t0070-fundamental.sh                       |   6 +-\n t/t0081-find-pack.sh                         |  12 +-\n t/t0091-bugreport.sh                         |  18 +-\n t/t0092-diagnose.sh                          |  12 +-\n t/t0100-previous.sh                          |   2 +-\n t/t0200-gettext-basic.sh                     |  14 +-\n t/t0203-gettext-setlocale-sanity.sh          |   4 +-\n t/t0204-gettext-reencode-sanity.sh           |   8 +-\n t/t0210-trace2-normal.sh                     |   6 +-\n t/t0211-trace2-perf.sh                       |  80 +--\n t/t0212-trace2-event.sh                      |   8 +-\n t/t0300-credentials.sh                       |   4 +-\n t/t0410-partial-clone.sh                     |  82 +--\n t/t0450-txt-doc-vs-help.sh                   |   2 +-\n t/t0500-progress-display.sh                  |  18 +-\n t/t0610-reftable-basics.sh                   |   8 +-\n t/t1004-read-tree-m-u-wf.sh                  |   8 +-\n t/t1006-cat-file.sh                          |  18 +-\n t/t1007-hash-object.sh                       |   8 +-\n t/t1011-read-tree-sparse-checkout.sh         |  10 +-\n t/t1050-large.sh                             |   6 +-\n t/t1091-sparse-checkout-builtin.sh           |  24 +-\n t/t1092-sparse-checkout-compatibility.sh     |  44 +-\n t/t1300-config.sh                            |  16 +-\n t/t1305-config-include.sh                    |   2 +-\n t/t1308-config-set.sh                        |   6 +-\n t/t1400-update-ref.sh                        | 170 +++---\n t/t1403-show-ref.sh                          |  18 +-\n t/t1410-reflog.sh                            |   4 +-\n t/t1415-worktree-refs.sh                     |   4 +-\n t/t1430-bad-ref-name.sh                      |  56 +-\n t/t1450-fsck.sh                              |  12 +-\n t/t1451-fsck-buffer.sh                       |   6 +-\n t/t1460-refs-migrate.sh                      |   2 +-\n t/t1500-rev-parse.sh                         |   6 +-\n t/t1502-rev-parse-parseopt.sh                |   2 +-\n t/t1503-rev-parse-verify.sh                  |  10 +-\n t/t1510-repo-setup.sh                        |  10 +-\n t/t1512-rev-parse-disambiguation.sh          |   4 +-\n t/t1515-rev-parse-outside-repo.sh            |   2 +-\n t/t1800-hook.sh                              |  18 +-\n t/t2004-checkout-cache-temp.sh               |   4 +-\n t/t2019-checkout-ambiguous-ref.sh            |   4 +-\n t/t2024-checkout-dwim.sh                     |   8 +-\n t/t2030-unresolve-info.sh                    |   6 +-\n t/t2060-switch.sh                            |   6 +-\n t/t2070-restore.sh                           |   2 +-\n t/t2080-parallel-checkout-basics.sh          |  14 +-\n t/t2081-parallel-checkout-collisions.sh      |  24 +-\n t/t2082-parallel-checkout-attributes.sh      |  12 +-\n t/t2103-update-index-ignore-missing.sh       |   6 +-\n t/t2200-add-update.sh                        |   2 +-\n t/t2203-add-intent.sh                        |   6 +-\n t/t2400-worktree-add.sh                      |  24 +-\n t/t2402-worktree-list.sh                     |  16 +-\n t/t2403-worktree-move.sh                     |   6 +-\n t/t2405-worktree-submodule.sh                |   6 +-\n t/t2407-worktree-heads.sh                    |  26 +-\n t/t2500-untracked-overwriting.sh             |   8 +-\n t/t2501-cwd-empty.sh                         |   4 +-\n t/t3001-ls-files-others-exclude.sh           |   6 +-\n t/t3007-ls-files-recurse-submodules.sh       |   6 +-\n t/t3200-branch.sh                            |  12 +-\n t/t3202-show-branch.sh                       |  10 +-\n t/t3203-branch-output.sh                     |   4 +-\n t/t3206-range-diff.sh                        |  78 +--\n t/t3207-branch-submodule.sh                  |   4 +-\n t/t3301-notes.sh                             |  32 +-\n t/t3310-notes-merge-manual-resolve.sh        |  16 +-\n t/t3320-notes-merge-worktrees.sh             |   2 +-\n t/t3400-rebase.sh                            |  16 +-\n t/t3402-rebase-merge.sh                      |  16 +-\n t/t3404-rebase-interactive.sh                |  72 +--\n t/t3406-rebase-message.sh                    |   6 +-\n t/t3415-rebase-autosquash.sh                 |  10 +-\n t/t3416-rebase-onto-threedots.sh             |   4 +-\n t/t3418-rebase-continue.sh                   |  10 +-\n t/t3420-rebase-autostash.sh                  |  25 +-\n t/t3422-rebase-incompatible-options.sh       |   4 +-\n t/t3429-rebase-edit-todo.sh                  |   2 +-\n t/t3430-rebase-merges.sh                     |  32 +-\n t/t3500-cherry.sh                            |   4 +-\n t/t3501-revert-cherry-pick.sh                |   6 +-\n t/t3504-cherry-pick-rerere.sh                |   6 +-\n t/t3510-cherry-pick-sequence.sh              |  24 +-\n t/t3602-rm-sparse-checkout.sh                |   4 +-\n t/t3705-add-sparse-checkout.sh               |  10 +-\n t/t3800-mktag.sh                             |   4 +-\n t/t3901-i18n-patch.sh                        |  20 +-\n t/t3903-stash.sh                             |  28 +-\n t/t3904-stash-patch.sh                       |   4 +-\n t/t3908-stash-in-worktree.sh                 |   2 +-\n t/t4000-diff-format.sh                       |   2 +-\n t/t4001-diff-rename.sh                       |   4 +-\n t/t4011-diff-symlink.sh                      |   2 +-\n t/t4013-diff-various.sh                      |   2 +-\n t/t4014-format-patch.sh                      | 344 ++++++------\n t/t4015-diff-whitespace.sh                   |  16 +-\n t/t4017-diff-retval.sh                       |   2 +-\n t/t4018-diff-funcname.sh                     |   2 +-\n t/t4019-diff-wserror.sh                      |   8 +-\n t/t4020-diff-external.sh                     |  18 +-\n t/t4021-format-patch-numbered.sh             |   4 +-\n t/t4022-diff-rewrite.sh                      |  14 +-\n t/t4028-format-patch-mime-headers.sh         |   6 +-\n t/t4031-diff-rewrite-binary.sh               |  18 +-\n t/t4033-diff-patience.sh                     |   2 +-\n t/t4036-format-patch-signer-mime.sh          |   6 +-\n t/t4038-diff-combined.sh                     |   6 +-\n t/t4051-diff-function-context.sh             |  38 +-\n t/t4053-diff-no-index.sh                     |   4 +-\n t/t4063-diff-blobs.sh                        |   2 +-\n t/t4065-diff-anchored.sh                     |  26 +-\n t/t4067-diff-partial-clone.sh                |  12 +-\n t/t4073-diff-stat-name-width.sh              |  24 +-\n t/t4103-apply-binary.sh                      |   2 +-\n t/t4120-apply-popt.sh                        |   2 +-\n t/t4124-apply-ws-rule.sh                     |  10 +-\n t/t4128-apply-root.sh                        |   2 +-\n t/t4140-apply-ita.sh                         |   4 +-\n t/t4141-apply-too-large.sh                   |   2 +-\n t/t4150-am.sh                                |  48 +-\n t/t4200-rerere.sh                            |   6 +-\n t/t4201-shortlog.sh                          |   2 +-\n t/t4202-log.sh                               |  84 +--\n t/t4204-patch-id.sh                          |   2 +-\n t/t4205-log-pretty-formats.sh                |   2 +-\n t/t4209-log-pickaxe.sh                       |  10 +-\n t/t4211-line-log.sh                          |  72 +--\n t/t4216-log-bloom.sh                         |  18 +-\n t/t4252-am-options.sh                        |  22 +-\n t/t4254-am-corrupt.sh                        |   6 +-\n t/t4258-am-quoted-cr.sh                      |   2 +-\n t/t4301-merge-tree-write-tree.sh             |  18 +-\n t/t5000-tar-tree.sh                          |  10 +-\n t/t5004-archive-corner-cases.sh              |   2 +-\n t/t5100-mailinfo.sh                          |   2 +-\n t/t5150-request-pull.sh                      |  18 +-\n t/t5300-pack-object.sh                       |  22 +-\n t/t5302-pack-index.sh                        |   6 +-\n t/t5304-prune.sh                             |   8 +-\n t/t5310-pack-bitmaps.sh                      |  14 +-\n t/t5317-pack-objects-filter-objects.sh       |  12 +-\n t/t5318-commit-graph.sh                      |   8 +-\n t/t5319-multi-pack-index.sh                  |  16 +-\n t/t5324-split-commit-graph.sh                |  10 +-\n t/t5325-reverse-index.sh                     |   2 +-\n t/t5326-multi-pack-bitmaps.sh                |  28 +-\n t/t5328-commit-graph-64bit-time.sh           |   2 +-\n t/t5329-pack-objects-cruft.sh                |   8 +-\n t/t5334-incremental-multi-pack-index.sh      |   2 +-\n t/t5335-compact-multi-pack-index.sh          |   4 +-\n t/t5351-unpack-large-objects.sh              |   2 +-\n t/t5402-post-merge-hook.sh                   |   4 +-\n t/t5403-post-checkout-hook.sh                |   2 +-\n t/t5404-tracking-branches.sh                 |   2 +-\n t/t5406-remote-rejects.sh                    |   2 +-\n t/t5407-post-rewrite-hook.sh                 |   8 +-\n t/t5409-colorize-remote-messages.sh          |  36 +-\n t/t5411/test-0013-bad-protocol.sh            |  14 +-\n t/t5411/test-0014-bad-protocol--porcelain.sh |  14 +-\n t/t5500-fetch-pack.sh                        |  38 +-\n t/t5504-fetch-receive-strict.sh              |  14 +-\n t/t5505-remote.sh                            |  20 +-\n t/t5510-fetch.sh                             |  10 +-\n t/t5512-ls-remote.sh                         |   8 +-\n t/t5514-fetch-multiple.sh                    |   2 +-\n t/t5516-fetch-push.sh                        |  20 +-\n t/t5520-pull.sh                              |   4 +-\n t/t5524-pull-msg.sh                          |   6 +-\n t/t5526-fetch-submodules.sh                  |  16 +-\n t/t5529-push-errors.sh                       |   4 +-\n t/t5530-upload-pack-error.sh                 |  18 +-\n t/t5531-deep-submodule-push.sh               |   2 +-\n t/t5532-fetch-proxy.sh                       |   2 +-\n t/t5533-push-cas.sh                          |  12 +-\n t/t5534-push-signed.sh                       |  22 +-\n t/t5537-fetch-shallow.sh                     |   2 +-\n t/t5538-push-shallow.sh                      |   2 +-\n t/t5539-fetch-http-shallow.sh                |   4 +-\n t/t5541-http-push-smart.sh                   |  32 +-\n t/t5544-pack-objects-hook.sh                 |  12 +-\n t/t5550-http-fetch-dumb.sh                   |   4 +-\n t/t5551-http-fetch-smart.sh                  |  46 +-\n t/t5552-skipping-fetch-negotiator.sh         |   6 +-\n t/t5554-noop-fetch-negotiator.sh             |   4 +-\n t/t5557-http-get.sh                          |   2 +-\n t/t5558-clone-bundle-uri.sh                  |  38 +-\n t/t5562-http-backend-content-length.sh       |   2 +-\n t/t5564-http-proxy.sh                        |  10 +-\n t/t5581-http-curl-verbose.sh                 |   2 +-\n t/t5583-push-branches.sh                     |   8 +-\n t/t5601-clone.sh                             |  28 +-\n t/t5604-clone-reference.sh                   |   8 +-\n t/t5605-clone-local.sh                       |   2 +-\n t/t5606-clone-options.sh                     |   6 +-\n t/t5612-clone-refspec.sh                     |   2 +-\n t/t5616-partial-clone.sh                     |  60 +--\n t/t5619-clone-local-ambiguous-transport.sh   |   2 +-\n t/t5620-backfill.sh                          |  12 +-\n t/t5700-protocol-v1.sh                       |  46 +-\n t/t5701-git-serve.sh                         |  14 +-\n t/t5702-protocol-v2.sh                       | 152 +++---\n t/t5703-upload-pack-ref-in-want.sh           |  22 +-\n t/t5705-session-id-in-capabilities.sh        |  12 +-\n t/t5750-bundle-uri-parse.sh                  |   8 +-\n t/t5801-remote-helpers.sh                    |   4 +-\n t/t5810-proto-disable-local.sh               |   2 +-\n t/t5813-proto-disable-ssh.sh                 |   4 +-\n t/t6000-rev-list-misc.sh                     |  26 +-\n t/t6005-rev-list-count.sh                    |   8 +-\n t/t6006-rev-list-format.sh                   |   4 +-\n t/t6009-rev-list-parent.sh                   |   4 +-\n t/t6020-bundle-misc.sh                       |  12 +-\n t/t6022-rev-list-missing.sh                  |   4 +-\n t/t6030-bisect-porcelain.sh                  | 150 +++---\n t/t6040-tracking-info.sh                     |   2 +-\n t/t6112-rev-list-filters-objects.sh          |  24 +-\n t/t6115-rev-list-du.sh                       |   4 +-\n t/t6120-describe.sh                          |  14 +-\n t/t6200-fmt-merge-msg.sh                     |  82 +--\n t/t6402-merge-rename.sh                      |   4 +-\n t/t6403-merge-file.sh                        |   6 +-\n t/t6404-recursive-merge.sh                   |   2 +-\n t/t6406-merge-attr.sh                        |  20 +-\n t/t6417-merge-ours-theirs.sh                 |  30 +-\n t/t6418-merge-text-auto.sh                   |   2 +-\n t/t6422-merge-rename-corner-cases.sh         |   8 +-\n t/t6423-merge-rename-directories.sh          |  72 +--\n t/t6424-merge-unrelated-index-changes.sh     |   6 +-\n t/t6427-diff3-conflict-markers.sh            |  10 +-\n t/t6432-merge-recursive-space-options.sh     |   4 +-\n t/t6436-merge-overwrite.sh                   |   6 +-\n t/t6437-submodule-merge.sh                   |  12 +-\n t/t6500-gc.sh                                |   8 +-\n t/t6600-test-reach.sh                        |   4 +-\n t/t7001-mv.sh                                |  16 +-\n t/t7002-mv-sparse-checkout.sh                |  38 +-\n t/t7003-filter-branch.sh                     |  16 +-\n t/t7004-tag.sh                               |   2 +-\n t/t7006-pager.sh                             |  16 +-\n t/t7012-skip-worktree-writing.sh             |   6 +-\n t/t7030-verify-tag.sh                        |  52 +-\n t/t7031-verify-tag-signed-ssh.sh             |  46 +-\n t/t7102-reset.sh                             |   2 +-\n t/t7110-reset-merge.sh                       |  40 +-\n t/t7201-co.sh                                |   6 +-\n t/t7300-clean.sh                             |   2 +-\n t/t7301-clean-interactive.sh                 |   2 +-\n t/t7400-submodule-basic.sh                   |  32 +-\n t/t7402-submodule-rebase.sh                  |   2 +-\n t/t7406-submodule-update.sh                  |  26 +-\n t/t7416-submodule-dash-url.sh                |  20 +-\n t/t7417-submodule-path-url.sh                |   2 +-\n t/t7450-bad-git-dotfiles.sh                  |  14 +-\n t/t7501-commit-basic-functionality.sh        |  14 +-\n t/t7502-commit-porcelain.sh                  |   2 +-\n t/t7507-commit-verbose.sh                    |   6 +-\n t/t7508-status.sh                            |   6 +-\n t/t7510-signed-commit.sh                     |  68 +--\n t/t7516-commit-races.sh                      |   4 +-\n t/t7519-status-fsmonitor.sh                  |  14 +-\n t/t7527-builtin-fsmonitor.sh                 |  84 +--\n t/t7528-signed-commit-ssh.sh                 |  68 +--\n t/t7600-merge.sh                             |  10 +-\n t/t7603-merge-reduce-heads.sh                |  20 +-\n t/t7606-merge-custom.sh                      |   2 +-\n t/t7607-merge-state.sh                       |   4 +-\n t/t7610-mergetool.sh                         |  18 +-\n t/t7700-repack.sh                            |  14 +-\n t/t7703-repack-geometric.sh                  |   4 +-\n t/t7704-repack-cruft.sh                      |  12 +-\n t/t7800-difftool.sh                          |  26 +-\n t/t7810-grep.sh                              |  22 +-\n t/t7814-grep-recurse-submodules.sh           |   2 +-\n t/t7900-maintenance.sh                       |  36 +-\n t/t8008-blame-formats.sh                     |   2 +-\n t/t8010-cat-file-filters.sh                  |   2 +-\n t/t8012-blame-colors.sh                      |   2 +-\n t/t9001-send-email.sh                        | 190 +++----\n t/t9003-help-autocorrect.sh                  |   6 +-\n t/t9106-git-svn-commit-diff-clobber.sh       |   2 +-\n t/t9107-git-svn-migrate.sh                   |  30 +-\n t/t9110-git-svn-use-svm-props.sh             |  20 +-\n t/t9111-git-svn-use-svnsync-props.sh         |  18 +-\n t/t9114-git-svn-dcommit-merge.sh             |   6 +-\n t/t9116-git-svn-log.sh                       |   8 +-\n t/t9117-git-svn-init-clone.sh                |  12 +-\n t/t9119-git-svn-info.sh                      |  16 +-\n t/t9122-git-svn-author.sh                    |   8 +-\n t/t9130-git-svn-authors-file.sh              |   8 +-\n t/t9138-git-svn-authors-prog.sh              |  14 +-\n t/t9140-git-svn-reset.sh                     |   4 +-\n t/t9153-git-svn-rewrite-uuid.sh              |   4 +-\n t/t9200-git-cvsexportcommit.sh               |   2 +-\n t/t9210-scalar.sh                            |  34 +-\n t/t9211-scalar-clone.sh                      |  16 +-\n t/t9300-fast-import.sh                       |  10 +-\n t/t9350-fast-export.sh                       |  54 +-\n t/t9351-fast-export-anonymize.sh             |  36 +-\n t/t9400-git-cvsserver-server.sh              |   4 +-\n t/t9501-gitweb-standalone-http-status.sh     |  58 +--\n t/t9502-gitweb-standalone-parse-output.sh    |  38 +-\n t/t9800-git-p4-basic.sh                      |  10 +-\n t/t9801-git-p4-branch.sh                     |  48 +-\n t/t9806-git-p4-options.sh                    |  10 +-\n t/t9807-git-p4-submit.sh                     |   2 +-\n t/t9810-git-p4-rcs.sh                        |   8 +-\n t/t9813-git-p4-preserve-users.sh             |   8 +-\n t/t9814-git-p4-rename.sh                     |   8 +-\n t/t9827-git-p4-change-filetype.sh            |   4 +-\n t/t9832-unshelve.sh                          |   6 +-\n t/t9833-errors.sh                            |   4 +-\n t/t9835-git-p4-metadata-encoding-python2.sh  |  36 +-\n t/t9836-git-p4-metadata-encoding-python3.sh  |  38 +-\n t/t9850-shell.sh                             |   2 +-\n t/t9902-completion.sh                        |  26 +-\n 363 files changed, 4067 insertions(+), 3334 deletions(-)\n create mode 100644 t/check-shell-parser.pl\n create mode 100644 t/lib-shell-parser.pl\n create mode 100755 t/lint-style.pl\n create mode 100644 t/lint-style/grep-assert.expect\n create mode 100644 t/lint-style/grep-assert.test\n create mode 100644 t/lint-style/grep-fix.expect\n create mode 100644 t/lint-style/grep-fix.test\n create mode 100644 t/lint-style/grep-missing-file.expect\n create mode 100644 t/lint-style/grep-missing-file.test\n create mode 100644 t/lint-style/grep-negated.expect\n create mode 100644 t/lint-style/grep-negated.test\n create mode 100644 t/lint-style/grep-not-assert.expect\n create mode 100644 t/lint-style/grep-not-assert.test\n create mode 100644 t/lint-style/heredoc.expect\n create mode 100644 t/lint-style/heredoc.test\n create mode 100644 t/lint-style/test-grep-negation-fix.expect\n create mode 100644 t/lint-style/test-grep-negation-fix.test\n create mode 100644 t/lint-style/test-grep-negation.expect\n create mode 100644 t/lint-style/test-grep-negation.test\n\n\nbase-commit: 9ac3f193c05c2237e2b14ebaa1149e9fc8a1abe0\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2135%2Fmmontalbo%2Fmm%2Ftest-grep-docs-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2135/mmontalbo/mm/test-grep-docs-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/2135\n-- \ngitgitgadget\n"},{"id":"544680","messageId":"43402040bf711e6266cfe664745099b8d3574255.1780559158.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.git.1780559158.gitgitgadget@gmail.com","subject":"[PATCH 1/6] t/README: document test_grep helper","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-04T07:45:53Z","receivedAt":"2026-06-04T07:46:02Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\ntest_grep is a wrapper around grep for test assertions that prints\nthe file contents on failure for easier debugging.  It also accepts\n'!' as its first argument for negation, which preserves the\ndiagnostic output that '! test_grep' would suppress.\n\nDespite being widely used (and the preferred replacement for bare\ngrep in assertions), test_grep has no entry in t/README alongside\nthe other documented helpers like test_cmp and test_line_count.\nAdd one.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/README | 21 +++++++++++++++++++++\n 1 file changed, 21 insertions(+)\n\ndiff --git a/t/README b/t/README\nindex adbbd9acf4..c12a1c317a 100644\n--- a/t/README\n+++ b/t/README\n@@ -1039,6 +1039,27 @@ see test-lib-functions.sh for the full list and their options.\n \n    Check whether a file has the length it is expected to.\n \n+ - test_grep [!] [<grep-options>] <pattern> <file>\n+\n+   Check whether <file> contains a line matching <pattern>, or\n+   with '!' that no line matches.  Use this instead of bare\n+   'grep <pattern> <file>' in test assertions.  On failure,\n+   test_grep prints the contents of <file> for easier debugging,\n+   whereas a bare 'grep' would fail silently.\n+\n+   For negation, pass '!' as the first argument:\n+\n+\ttest_grep ! \"^diff --git\" actual\n+\n+   Do not negate by writing '! test_grep', as that suppresses the\n+   diagnostic output.\n+\n+   test_grep should only be used as a test assertion.  When grep\n+   is used as a data filter (e.g. 'grep -v \"^index\" actual >filtered')\n+   or inside a command substitution (e.g. '$(grep -c ...)'), plain\n+   'grep' is the right choice because the exit code is not the\n+   assertion itself.\n+\n  - test_path_is_file <path>\n    test_path_is_dir <path>\n    test_path_is_missing <path>\n-- \ngitgitgadget\n\n"},{"id":"544681","messageId":"93c2b29683ff158920013af37cd28e1c2f4e2617.1780559158.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.git.1780559158.gitgitgadget@gmail.com","subject":"[PATCH 3/6] t: fix Lexer line count for $() inside double-quoted strings","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-04T07:45:55Z","receivedAt":"2026-06-04T07:46:05Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nscan_dqstring's post-loop newline counter re-counts newlines that\nwere already counted during recursive parsing of $() bodies.  This\nhappens because scan_dollar's returned text can contain newlines\n(from token text of multi-line strings and from \\n command separator\ntokens), and the catch-all counter at the end of scan_dqstring\ncounts all of them again.\n\nFix this by counting newlines inline as non-special characters are\nconsumed, and removing the post-loop catch-all.  Each newline is\nnow counted exactly once: literal newlines at the inline match,\nline splices at the \\<newline> handler, and $() newlines by\nscan_token during the recursive parse.\n\nThis does not affect chainlint's output because chainlint annotates\nthe original body text using byte offsets, not token line numbers.\nIt does matter for tools like lint-style.pl (introduced in a\nsubsequent commit) that use token line numbers to locate and fix\nspecific lines in the original file.\n\nAdd check-shell-parser.pl to verify that the Lexer reports correct\nline numbers after multi-line $() in double-quoted strings.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/Makefile              |  7 +++--\n t/check-shell-parser.pl | 58 +++++++++++++++++++++++++++++++++++++++++\n t/lib-shell-parser.pl   | 11 +++++---\n 3 files changed, 71 insertions(+), 5 deletions(-)\n create mode 100644 t/check-shell-parser.pl\n\ndiff --git a/t/Makefile b/t/Makefile\nindex ab8a5b54aa..25f923fed9 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -139,7 +139,7 @@ check-meson:\n test-lint: test-lint-duplicates test-lint-executable \\\n \ttest-lint-filenames\n ifneq ($(PERL_PATH),)\n-test-lint: test-lint-shell-syntax\n+test-lint: test-lint-shell-syntax check-shell-parser\n else\n GIT_TEST_CHAIN_LINT = 0\n endif\n@@ -160,6 +160,8 @@ test-lint-executable:\n test-lint-shell-syntax:\n \t@'$(PERL_PATH_SQ)' check-non-portable-shell.pl $(T) $(THELPERS) $(TPERF)\n \n+check-shell-parser:\n+\t@'$(PERL_PATH_SQ)' check-shell-parser.pl\n test-lint-filenames:\n \t@# We do *not* pass a glob to ls-files but use grep instead, to catch\n \t@# non-ASCII characters (which are quoted within double-quotes)\n@@ -185,7 +187,8 @@ perf:\n \t$(MAKE) -C perf/ all\n \n .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \\\n-\tcheck-chainlint clean-chainlint test-chainlint $(UNIT_TESTS)\n+\tcheck-chainlint clean-chainlint test-chainlint \\\n+\tcheck-shell-parser $(UNIT_TESTS)\n \n .PHONY: libgit-sys-test libgit-rs-test\n libgit-sys-test:\ndiff --git a/t/check-shell-parser.pl b/t/check-shell-parser.pl\nnew file mode 100644\nindex 0000000000..7d4ba6da7f\n--- /dev/null\n+++ b/t/check-shell-parser.pl\n@@ -0,0 +1,58 @@\n+#!/usr/bin/perl\n+\n+# Tests for the shared shell parser (lib-shell-parser.pl).\n+\n+use strict;\n+use warnings;\n+use File::Basename;\n+\n+my $_lib = dirname($0) . \"/lib-shell-parser.pl\";\n+$_lib = \"./$_lib\" unless $_lib =~ m{^/};\n+do $_lib or die \"$0: failed to load $_lib: $@$!\\n\";\n+\n+my $rc = 0;\n+\n+sub check {\n+\tmy ($desc, $body, $want_token, $want_line) = @_;\n+\tmy $parser = ShellParser->new(\\$body);\n+\tmy @tokens = $parser->parse();\n+\tfor my $t (reverse @tokens) {\n+\t\tnext unless $t->[0] eq $want_token && defined $t->[3];\n+\t\tif ($t->[3] != $want_line) {\n+\t\t\tprint STDERR \"FAIL: $desc: \" .\n+\t\t\t\t\"'$want_token' at line $t->[3], \" .\n+\t\t\t\t\"expected line $want_line\\n\";\n+\t\t\t$rc = 1;\n+\t\t}\n+\t\treturn;\n+\t}\n+\tprint STDERR \"FAIL: $desc: token '$want_token' not found\\n\";\n+\t$rc = 1;\n+}\n+\n+# Multi-line $() inside a dq-string: MARKER should be at line 3.\n+check('dq-string with multi-line $()', <<'BODY', 'MARKER', 3);\n+\tx=\"$(echo one\n+\techo two)\" &&\n+\tMARKER here\n+BODY\n+\n+# Two multi-line $() substitutions: verifies drift does not accumulate.\n+# MARKER should be at line 5.\n+check('two dq-string $()', <<'BODY', 'MARKER', 5);\n+\tx=\"$(echo a\n+\tb)\" &&\n+\ty=\"$(echo c\n+\td)\" &&\n+\tMARKER here\n+BODY\n+\n+# $() outside a dq-string: no double-counting either way.\n+# MARKER should be at line 3.\n+check('bare $() spanning lines', <<'BODY', 'MARKER', 3);\n+\tx=$(echo one\n+\techo two) &&\n+\tMARKER here\n+BODY\n+\n+exit $rc;\ndiff --git a/t/lib-shell-parser.pl b/t/lib-shell-parser.pl\nindex 1e521a94f8..fa9b44d6ec 100644\n--- a/t/lib-shell-parser.pl\n+++ b/t/lib-shell-parser.pl\n@@ -89,8 +89,14 @@ sub scan_dqstring {\n \tmy $b = $self->{buff};\n \tmy $s = '\"';\n \twhile (1) {\n-\t\t# slurp up non-special characters\n-\t\t$s .= $1 if $$b =~ /\\G([^\"\\$\\\\]+)/gc;\n+\t\t# slurp up non-special characters; count newlines\n+\t\t# inline so we don't need a catch-all counter that\n+\t\t# would miscount newlines from recursive $() parsing\n+\t\tif ($$b =~ /\\G([^\"\\$\\\\]+)/gc) {\n+\t\t\tmy $chunk = $1;\n+\t\t\t$self->{lineno} += () = $chunk =~ /\\n/sg;\n+\t\t\t$s .= $chunk;\n+\t\t}\n \t\t# handle special characters\n \t\tlast unless $$b =~ /\\G(.)/sgc;\n \t\tmy $c = $1;\n@@ -107,7 +113,6 @@ sub scan_dqstring {\n \t\t}\n \t\tdie(\"internal error scanning dq-string '$c'\\n\");\n \t}\n-\t$self->{lineno} += () = $s =~ /\\n/sg;\n \treturn $s;\n }\n \n-- \ngitgitgadget\n\n"},{"id":"544682","messageId":"a1069efa8fafe17b88f701168e7fe7c4ac663304.1780559158.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.git.1780559158.gitgitgadget@gmail.com","subject":"[PATCH 2/6] t: extract chainlint's parser into shared module","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-04T07:45:54Z","receivedAt":"2026-06-04T07:46:05Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nMove the Lexer, ShellParser, and ScriptParser packages from\nchainlint.pl into t/lib-shell-parser.pl so they can be reused by\nother tools.  ScriptParser's check_test() is a no-op in the shared\nmodule; callers subclass ScriptParser and override it.\n\nchainlint.pl defines TestParser (&&-chain detection) and\nChainlintParser (a ScriptParser subclass whose check_test runs\nTestParser and formats the results).  The shared module is loaded\nvia do() for portability with minimal Perl installations.\n\nA subsequent commit introduces lint-style.pl which needs the same\nshell parser to properly tokenize test scripts.  Sharing the parser\navoids reimplementing heredoc handling, $(...) nesting, pipe\ntracking, quoting, and test body extraction.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/chainlint.pl        | 521 ++----------------------------------------\n t/lib-shell-parser.pl | 517 +++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 530 insertions(+), 508 deletions(-)\n create mode 100644 t/lib-shell-parser.pl\n\ndiff --git a/t/chainlint.pl b/t/chainlint.pl\nindex f0598e3934..49b7cc6cb8 100755\n--- a/t/chainlint.pl\n+++ b/t/chainlint.pl\n@@ -23,458 +23,10 @@ my $jobs = -1;\n my $show_stats;\n my $emit_all;\n \n-# Lexer tokenizes POSIX shell scripts. It is roughly modeled after section 2.3\n-# \"Token Recognition\" of POSIX chapter 2 \"Shell Command Language\". Although\n-# similar to lexical analyzers for other languages, this one differs in a few\n-# substantial ways due to quirks of the shell command language.\n-#\n-# For instance, in many languages, newline is just whitespace like space or\n-# TAB, but in shell a newline is a command separator, thus a distinct lexical\n-# token. A newline is significant and returned as a distinct token even at the\n-# end of a shell comment.\n-#\n-# In other languages, `1+2` would typically be scanned as three tokens\n-# (`1`, `+`, and `2`), but in shell it is a single token. However, the similar\n-# `1 + 2`, which embeds whitepace, is scanned as three token in shell, as well.\n-# In shell, several characters with special meaning lose that meaning when not\n-# surrounded by whitespace. For instance, the negation operator `!` is special\n-# when standing alone surrounded by whitespace; whereas in `foo!uucp` it is\n-# just a plain character in the longer token \"foo!uucp\". In many other\n-# languages, `\"string\"/foo:'string'` might be scanned as five tokens (\"string\",\n-# `/`, `foo`, `:`, and 'string'), but in shell, it is just a single token.\n-#\n-# The lexical analyzer for the shell command language is also somewhat unusual\n-# in that it recursively invokes the parser to handle the body of `$(...)`\n-# expressions which can contain arbitrary shell code. Such expressions may be\n-# encountered both inside and outside of double-quoted strings.\n-#\n-# The lexical analyzer is responsible for consuming shell here-doc bodies which\n-# extend from the line following a `<<TAG` operator until a line consisting\n-# solely of `TAG`. Here-doc consumption begins when a newline is encountered.\n-# It is legal for multiple here-doc `<<TAG` operators to be present on a single\n-# line, in which case their bodies must be present one following the next, and\n-# are consumed in the (left-to-right) order the `<<TAG` operators appear on the\n-# line. A special complication is that the bodies of all here-docs must be\n-# consumed when the newline is encountered even if the parse context depth has\n-# changed. For instance, in `cat <<A && x=$(cat <<B &&\\n`, bodies of here-docs\n-# \"A\" and \"B\" must be consumed even though \"A\" was introduced outside the\n-# recursive parse context in which \"B\" was introduced and in which the newline\n-# is encountered.\n-package Lexer;\n-\n-sub new {\n-\tmy ($class, $parser, $s) = @_;\n-\tbless {\n-\t\tparser => $parser,\n-\t\tbuff => $s,\n-\t\tlineno => 1,\n-\t\theretags => []\n-\t} => $class;\n-}\n-\n-sub scan_heredoc_tag {\n-\tmy $self = shift @_;\n-\t${$self->{buff}} =~ /\\G(-?)/gc;\n-\tmy $indented = $1;\n-\tmy $token = $self->scan_token();\n-\treturn \"<<$indented\" unless $token;\n-\tmy $tag = $token->[0];\n-\t$tag =~ s/['\"\\\\]//g;\n-\t$$token[0] = $indented ? \"\\t$tag\" : \"$tag\";\n-\tpush(@{$self->{heretags}}, $token);\n-\treturn \"<<$indented$tag\";\n-}\n-\n-sub scan_op {\n-\tmy ($self, $c) = @_;\n-\tmy $b = $self->{buff};\n-\treturn $c unless $$b =~ /\\G(.)/sgc;\n-\tmy $cc = $c . $1;\n-\treturn scan_heredoc_tag($self) if $cc eq '<<';\n-\treturn $cc if $cc =~ /^(?:&&|\\|\\||>>|;;|<&|>&|<>|>\\|)$/;\n-\tpos($$b)--;\n-\treturn $c;\n-}\n-\n-sub scan_sqstring {\n-\tmy $self = shift @_;\n-\t${$self->{buff}} =~ /\\G([^']*'|.*\\z)/sgc;\n-\tmy $s = $1;\n-\t$self->{lineno} += () = $s =~ /\\n/sg;\n-\treturn \"'\" . $s;\n-}\n-\n-sub scan_dqstring {\n-\tmy $self = shift @_;\n-\tmy $b = $self->{buff};\n-\tmy $s = '\"';\n-\twhile (1) {\n-\t\t# slurp up non-special characters\n-\t\t$s .= $1 if $$b =~ /\\G([^\"\\$\\\\]+)/gc;\n-\t\t# handle special characters\n-\t\tlast unless $$b =~ /\\G(.)/sgc;\n-\t\tmy $c = $1;\n-\t\t$s .= '\"', last if $c eq '\"';\n-\t\t$s .= '$' . $self->scan_dollar(), next if $c eq '$';\n-\t\tif ($c eq '\\\\') {\n-\t\t\t$s .= '\\\\', last unless $$b =~ /\\G(.)/sgc;\n-\t\t\t$c = $1;\n-\t\t\t$self->{lineno}++, next if $c eq \"\\n\"; # line splice\n-\t\t\t# backslash escapes only $, `, \", \\ in dq-string\n-\t\t\t$s .= '\\\\' unless $c =~ /^[\\$`\"\\\\]$/;\n-\t\t\t$s .= $c;\n-\t\t\tnext;\n-\t\t}\n-\t\tdie(\"internal error scanning dq-string '$c'\\n\");\n-\t}\n-\t$self->{lineno} += () = $s =~ /\\n/sg;\n-\treturn $s;\n-}\n-\n-sub scan_balanced {\n-\tmy ($self, $c1, $c2) = @_;\n-\tmy $b = $self->{buff};\n-\tmy $depth = 1;\n-\tmy $s = $c1;\n-\twhile ($$b =~ /\\G([^\\Q$c1$c2\\E]*(?:[\\Q$c1$c2\\E]|\\z))/gc) {\n-\t\t$s .= $1;\n-\t\t$depth++, next if $s =~ /\\Q$c1\\E$/;\n-\t\t$depth--;\n-\t\tlast if $depth == 0;\n-\t}\n-\t$self->{lineno} += () = $s =~ /\\n/sg;\n-\treturn $s;\n-}\n-\n-sub scan_subst {\n-\tmy $self = shift @_;\n-\tmy @tokens = $self->{parser}->parse(qr/^\\)$/);\n-\t$self->{parser}->next_token(); # closing \")\"\n-\treturn @tokens;\n-}\n-\n-sub scan_dollar {\n-\tmy $self = shift @_;\n-\tmy $b = $self->{buff};\n-\treturn $self->scan_balanced('(', ')') if $$b =~ /\\G\\((?=\\()/gc; # $((...))\n-\treturn '(' . join(' ', map {$_->[0]} $self->scan_subst()) . ')' if $$b =~ /\\G\\(/gc; # $(...)\n-\treturn $self->scan_balanced('{', '}') if $$b =~ /\\G\\{/gc; # ${...}\n-\treturn $1 if $$b =~ /\\G(\\w+)/gc; # $var\n-\treturn $1 if $$b =~ /\\G([@*#?$!0-9-])/gc; # $*, $1, $$, etc.\n-\treturn '';\n-}\n-\n-sub swallow_heredocs {\n-\tmy $self = shift @_;\n-\tmy $b = $self->{buff};\n-\tmy $tags = $self->{heretags};\n-\twhile (my $tag = shift @$tags) {\n-\t\tmy $start = pos($$b);\n-\t\tmy $indent = $$tag[0] =~ s/^\\t// ? '\\\\s*' : '';\n-\t\t$$b =~ /(?:\\G|\\n)$indent\\Q$$tag[0]\\E(?:\\n|\\z)/gc;\n-\t\tif (pos($$b) > $start) {\n-\t\t\tmy $body = substr($$b, $start, pos($$b) - $start);\n-\t\t\t$self->{parser}->{heredocs}->{$$tag[0]} = {\n-\t\t\t\tcontent => substr($body, 0, length($body) - length($&)),\n-\t\t\t\tstart_line => $self->{lineno},\n-\t\t        };\n-\t\t\t$self->{lineno} += () = $body =~ /\\n/sg;\n-\t\t\tnext;\n-\t\t}\n-\t\tpush(@{$self->{parser}->{problems}}, ['HEREDOC', $tag]);\n-\t\t$$b =~ /(?:\\G|\\n).*\\z/gc; # consume rest of input\n-\t\tmy $body = substr($$b, $start, pos($$b) - $start);\n-\t\t$self->{lineno} += () = $body =~ /\\n/sg;\n-\t\tlast;\n-\t}\n-}\n-\n-sub scan_token {\n-\tmy $self = shift @_;\n-\tmy $b = $self->{buff};\n-\tmy $token = '';\n-\tmy ($start, $startln);\n-RESTART:\n-\t$startln = $self->{lineno};\n-\t$$b =~ /\\G[ \\t]+/gc; # skip whitespace (but not newline)\n-\t$start = pos($$b) || 0;\n-\t$self->{lineno}++, return [\"\\n\", $start, pos($$b), $startln, $startln] if $$b =~ /\\G#[^\\n]*(?:\\n|\\z)/gc; # comment\n-\twhile (1) {\n-\t\t# slurp up non-special characters\n-\t\t$token .= $1 if $$b =~ /\\G([^\\\\;&|<>(){}'\"\\$\\s]+)/gc;\n-\t\t# handle special characters\n-\t\tlast unless $$b =~ /\\G(.)/sgc;\n-\t\tmy $c = $1;\n-\t\tpos($$b)--, last if $c =~ /^[ \\t]$/; # whitespace ends token\n-\t\tpos($$b)--, last if length($token) && $c =~ /^[;&|<>(){}\\n]$/;\n-\t\t$token .= $self->scan_sqstring(), next if $c eq \"'\";\n-\t\t$token .= $self->scan_dqstring(), next if $c eq '\"';\n-\t\t$token .= $c . $self->scan_dollar(), next if $c eq '$';\n-\t\t$self->{lineno}++, $self->swallow_heredocs(), $token = $c, last if $c eq \"\\n\";\n-\t\t$token = $self->scan_op($c), last if $c =~ /^[;&|<>]$/;\n-\t\t$token = $c, last if $c =~ /^[(){}]$/;\n-\t\tif ($c eq '\\\\') {\n-\t\t\t$token .= '\\\\', last unless $$b =~ /\\G(.)/sgc;\n-\t\t\t$c = $1;\n-\t\t\t$self->{lineno}++, next if $c eq \"\\n\" && length($token); # line splice\n-\t\t\t$self->{lineno}++, goto RESTART if $c eq \"\\n\"; # line splice\n-\t\t\t$token .= '\\\\' . $c;\n-\t\t\tnext;\n-\t\t}\n-\t\tdie(\"internal error scanning character '$c'\\n\");\n-\t}\n-\treturn length($token) ? [$token, $start, pos($$b), $startln, $self->{lineno}] : undef;\n-}\n-\n-# ShellParser parses POSIX shell scripts (with minor extensions for Bash). It\n-# is a recursive descent parser very roughly modeled after section 2.10 \"Shell\n-# Grammar\" of POSIX chapter 2 \"Shell Command Language\".\n-package ShellParser;\n-\n-sub new {\n-\tmy ($class, $s) = @_;\n-\tmy $self = bless {\n-\t\tbuff => [],\n-\t\tstop => [],\n-\t\toutput => [],\n-\t\theredocs => {},\n-\t\tinsubshell => 0,\n-\t} => $class;\n-\t$self->{lexer} = Lexer->new($self, $s);\n-\treturn $self;\n-}\n-\n-sub next_token {\n-\tmy $self = shift @_;\n-\treturn pop(@{$self->{buff}}) if @{$self->{buff}};\n-\treturn $self->{lexer}->scan_token();\n-}\n-\n-sub untoken {\n-\tmy $self = shift @_;\n-\tpush(@{$self->{buff}}, @_);\n-}\n-\n-sub peek {\n-\tmy $self = shift @_;\n-\tmy $token = $self->next_token();\n-\treturn undef unless defined($token);\n-\t$self->untoken($token);\n-\treturn $token;\n-}\n-\n-sub stop_at {\n-\tmy ($self, $token) = @_;\n-\treturn 1 unless defined($token);\n-\tmy $stop = ${$self->{stop}}[-1] if @{$self->{stop}};\n-\treturn defined($stop) && $token->[0] =~ $stop;\n-}\n-\n-sub expect {\n-\tmy ($self, $expect) = @_;\n-\tmy $token = $self->next_token();\n-\treturn $token if defined($token) && $token->[0] eq $expect;\n-\tpush(@{$self->{output}}, \"?!ERR?! expected '$expect' but found '\" . (defined($token) ? $token->[0] : \"<end-of-input>\") . \"'\\n\");\n-\t$self->untoken($token) if defined($token);\n-\treturn ();\n-}\n-\n-sub optional_newlines {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\twhile (my $token = $self->peek()) {\n-\t\tlast unless $token->[0] eq \"\\n\";\n-\t\tpush(@tokens, $self->next_token());\n-\t}\n-\treturn @tokens;\n-}\n-\n-sub parse_group {\n-\tmy $self = shift @_;\n-\treturn ($self->parse(qr/^}$/),\n-\t\t$self->expect('}'));\n-}\n-\n-sub parse_subshell {\n-\tmy $self = shift @_;\n-\t$self->{insubshell}++;\n-\tmy @tokens = ($self->parse(qr/^\\)$/),\n-\t\t      $self->expect(')'));\n-\t$self->{insubshell}--;\n-\treturn @tokens;\n-}\n-\n-sub parse_case_pattern {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\twhile (defined(my $token = $self->next_token())) {\n-\t\tpush(@tokens, $token);\n-\t\tlast if $token->[0] eq ')';\n-\t}\n-\treturn @tokens;\n-}\n-\n-sub parse_case {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\tpush(@tokens,\n-\t     $self->next_token(), # subject\n-\t     $self->optional_newlines(),\n-\t     $self->expect('in'),\n-\t     $self->optional_newlines());\n-\twhile (1) {\n-\t\tmy $token = $self->peek();\n-\t\tlast unless defined($token) && $token->[0] ne 'esac';\n-\t\tpush(@tokens,\n-\t\t     $self->parse_case_pattern(),\n-\t\t     $self->optional_newlines(),\n-\t\t     $self->parse(qr/^(?:;;|esac)$/)); # item body\n-\t\t$token = $self->peek();\n-\t\tlast unless defined($token) && $token->[0] ne 'esac';\n-\t\tpush(@tokens,\n-\t\t     $self->expect(';;'),\n-\t\t     $self->optional_newlines());\n-\t}\n-\tpush(@tokens, $self->expect('esac'));\n-\treturn @tokens;\n-}\n-\n-sub parse_for {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\tpush(@tokens,\n-\t     $self->next_token(), # variable\n-\t     $self->optional_newlines());\n-\tmy $token = $self->peek();\n-\tif (defined($token) && $token->[0] eq 'in') {\n-\t\tpush(@tokens,\n-\t\t     $self->expect('in'),\n-\t\t     $self->optional_newlines());\n-\t}\n-\tpush(@tokens,\n-\t     $self->parse(qr/^do$/), # items\n-\t     $self->expect('do'),\n-\t     $self->optional_newlines(),\n-\t     $self->parse_loop_body(),\n-\t     $self->expect('done'));\n-\treturn @tokens;\n-}\n-\n-sub parse_if {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\twhile (1) {\n-\t\tpush(@tokens,\n-\t\t     $self->parse(qr/^then$/), # if/elif condition\n-\t\t     $self->expect('then'),\n-\t\t     $self->optional_newlines(),\n-\t\t     $self->parse(qr/^(?:elif|else|fi)$/)); # if/elif body\n-\t\tmy $token = $self->peek();\n-\t\tlast unless defined($token) && $token->[0] eq 'elif';\n-\t\tpush(@tokens, $self->expect('elif'));\n-\t}\n-\tmy $token = $self->peek();\n-\tif (defined($token) && $token->[0] eq 'else') {\n-\t\tpush(@tokens,\n-\t\t     $self->expect('else'),\n-\t\t     $self->optional_newlines(),\n-\t\t     $self->parse(qr/^fi$/)); # else body\n-\t}\n-\tpush(@tokens, $self->expect('fi'));\n-\treturn @tokens;\n-}\n-\n-sub parse_loop_body {\n-\tmy $self = shift @_;\n-\treturn $self->parse(qr/^done$/);\n-}\n-\n-sub parse_loop {\n-\tmy $self = shift @_;\n-\treturn ($self->parse(qr/^do$/), # condition\n-\t\t$self->expect('do'),\n-\t\t$self->optional_newlines(),\n-\t\t$self->parse_loop_body(),\n-\t\t$self->expect('done'));\n-}\n-\n-sub parse_func {\n-\tmy $self = shift @_;\n-\treturn ($self->expect('('),\n-\t\t$self->expect(')'),\n-\t\t$self->optional_newlines(),\n-\t\t$self->parse_cmd()); # body\n-}\n-\n-sub parse_bash_array_assignment {\n-\tmy $self = shift @_;\n-\tmy @tokens = $self->expect('(');\n-\twhile (defined(my $token = $self->next_token())) {\n-\t\tpush(@tokens, $token);\n-\t\tlast if $token->[0] eq ')';\n-\t}\n-\treturn @tokens;\n-}\n-\n-my %compound = (\n-\t'{' => \\&parse_group,\n-\t'(' => \\&parse_subshell,\n-\t'case' => \\&parse_case,\n-\t'for' => \\&parse_for,\n-\t'if' => \\&parse_if,\n-\t'until' => \\&parse_loop,\n-\t'while' => \\&parse_loop);\n-\n-sub parse_cmd {\n-\tmy $self = shift @_;\n-\tmy $cmd = $self->next_token();\n-\treturn () unless defined($cmd);\n-\treturn $cmd if $cmd->[0] eq \"\\n\";\n-\n-\tmy $token;\n-\tmy @tokens = $cmd;\n-\tif ($cmd->[0] eq '!') {\n-\t\tpush(@tokens, $self->parse_cmd());\n-\t\treturn @tokens;\n-\t} elsif (my $f = $compound{$cmd->[0]}) {\n-\t\tpush(@tokens, $self->$f());\n-\t} elsif (defined($token = $self->peek()) && $token->[0] eq '(') {\n-\t\tif ($cmd->[0] !~ /\\w=$/) {\n-\t\t\tpush(@tokens, $self->parse_func());\n-\t\t\treturn @tokens;\n-\t\t}\n-\t\tmy @array = $self->parse_bash_array_assignment();\n-\t\t$tokens[-1]->[0] .= join(' ', map {$_->[0]} @array);\n-\t\t$tokens[-1]->[2] = $array[$#array][2] if @array;\n-\t}\n-\n-\twhile (defined(my $token = $self->next_token())) {\n-\t\t$self->untoken($token), last if $self->stop_at($token);\n-\t\tpush(@tokens, $token);\n-\t\tlast if $token->[0] =~ /^(?:[;&\\n|]|&&|\\|\\|)$/;\n-\t}\n-\tpush(@tokens, $self->next_token()) if $tokens[-1]->[0] ne \"\\n\" && defined($token = $self->peek()) && $token->[0] eq \"\\n\";\n-\treturn @tokens;\n-}\n-\n-sub accumulate {\n-\tmy ($self, $tokens, $cmd) = @_;\n-\tpush(@$tokens, @$cmd);\n-}\n-\n-sub parse {\n-\tmy ($self, $stop) = @_;\n-\tpush(@{$self->{stop}}, $stop);\n-\tgoto DONE if $self->stop_at($self->peek());\n-\tmy @tokens;\n-\twhile (my @cmd = $self->parse_cmd()) {\n-\t\t$self->accumulate(\\@tokens, \\@cmd);\n-\t\tlast if $self->stop_at($self->peek());\n-\t}\n-DONE:\n-\tpop(@{$self->{stop}});\n-\treturn @tokens;\n-}\n+use File::Basename;\n+my $_lib = dirname($0) . \"/lib-shell-parser.pl\";\n+$_lib = \"./$_lib\" unless $_lib =~ m{^/};\n+do $_lib or die \"failed to load $_lib: $@$!\\n\";\n \n # TestParser is a subclass of ShellParser which, beyond parsing shell script\n # code, is also imbued with semantic knowledge of test construction, and checks\n@@ -484,7 +36,7 @@ DONE:\n # scripts in which the tests are defined.\n package TestParser;\n \n-use base 'ShellParser';\n+our @ISA = ('ShellParser');\n \n sub new {\n \tmy $class = shift @_;\n@@ -578,14 +130,12 @@ DONE:\n \t$self->SUPER::accumulate($tokens, $cmd);\n }\n \n-# ScriptParser is a subclass of ShellParser which identifies individual test\n-# definitions within test scripts, and passes each test body through TestParser\n-# to identify possible problems. ShellParser detects test definitions not only\n-# at the top-level of test scripts but also within compound commands such as\n-# loops and function definitions.\n-package ScriptParser;\n+# ChainlintParser is a subclass of ScriptParser which checks each test\n+# body for broken &&-chains via TestParser, then formats and collects\n+# the results.\n+package ChainlintParser;\n \n-use base 'ShellParser';\n+our @ISA = ('ScriptParser');\n \n sub new {\n \tmy $class = shift @_;\n@@ -595,35 +145,6 @@ sub new {\n \treturn $self;\n }\n \n-# extract the raw content of a token, which may be a single string or a\n-# composition of multiple strings and non-string character runs; for instance,\n-# `\"test body\"` unwraps to `test body`; `word\"a b\"42'c d'` to `worda b42c d`\n-sub unwrap {\n-\tmy $token = (@_ ? shift @_ : $_)->[0];\n-\t# simple case: 'sqstring' or \"dqstring\"\n-\treturn $token if $token =~ s/^'([^']*)'$/$1/;\n-\treturn $token if $token =~ s/^\"([^\"]*)\"$/$1/;\n-\n-\t# composite case\n-\tmy ($s, $q, $escaped);\n-\twhile (1) {\n-\t\t# slurp up non-special characters\n-\t\t$s .= $1 if $token =~ /\\G([^\\\\'\"]*)/gc;\n-\t\t# handle special characters\n-\t\tlast unless $token =~ /\\G(.)/sgc;\n-\t\tmy $c = $1;\n-\t\t$q = undef, next if defined($q) && $c eq $q;\n-\t\t$q = $c, next if !defined($q) && $c =~ /^['\"]$/;\n-\t\tif ($c eq '\\\\') {\n-\t\t\tlast unless $token =~ /\\G(.)/sgc;\n-\t\t\t$c = $1;\n-\t\t\t$s .= '\\\\' if $c eq \"\\n\"; # preserve line splice\n-\t\t}\n-\t\t$s .= $c;\n-\t}\n-\treturn $s\n-}\n-\n sub format_problem {\n \tlocal $_ = shift;\n \t/^AMP$/ && return \"missing '&&'\";\n@@ -635,10 +156,10 @@ sub format_problem {\n \n sub check_test {\n \tmy $self = shift @_;\n-\tmy $title = unwrap(shift @_);\n+\tmy $title = ScriptParser::unwrap(shift @_);\n \tmy $body = shift @_;\n \tmy $lineno = $body->[3];\n-\t$body = unwrap($body);\n+\t$body = ScriptParser::unwrap($body);\n \tif ($body eq '-') {\n \t\tmy $herebody = shift @_;\n \t\t$body = $herebody->{content};\n@@ -673,22 +194,6 @@ sub check_test {\n \tpush(@{$self->{output}}, \"$c->{blue}# chainlint: $title$c->{reset}\\n$checked\");\n }\n \n-sub parse_cmd {\n-\tmy $self = shift @_;\n-\tmy @tokens = $self->SUPER::parse_cmd();\n-\treturn @tokens unless @tokens && $tokens[0]->[0] =~ /^test_expect_(?:success|failure)$/;\n-\tmy $n = $#tokens;\n-\t$n-- while $n >= 0 && $tokens[$n]->[0] =~ /^(?:[;&\\n|]|&&|\\|\\|)$/;\n-\tmy $herebody;\n-\tif ($n >= 2 && $tokens[$n-1]->[0] eq '-' && $tokens[$n]->[0] =~ /^<<-?(.+)$/) {\n-\t\t$herebody = $self->{heredocs}->{$1};\n-\t\t$n--;\n-\t}\n-\t$self->check_test($tokens[1], $tokens[2], $herebody) if $n == 2; # title body\n-\t$self->check_test($tokens[2], $tokens[3], $herebody) if $n > 2;  # prereq title body\n-\treturn @tokens;\n-}\n-\n # main contains high-level functionality for processing command-line switches,\n # feeding input test scripts to ScriptParser, and reporting results.\n package main;\n@@ -803,7 +308,7 @@ sub check_script {\n \t\t}\n \t\tmy $s = do { local $/; <$fh> };\n \t\tclose($fh);\n-\t\tmy $parser = ScriptParser->new(\\$s);\n+\t\tmy $parser = ChainlintParser->new(\\$s);\n \t\t1 while $parser->parse_cmd();\n \t\tif (@{$parser->{output}}) {\n \t\t\tmy $c = fd_colors(1);\ndiff --git a/t/lib-shell-parser.pl b/t/lib-shell-parser.pl\nnew file mode 100644\nindex 0000000000..1e521a94f8\n--- /dev/null\n+++ b/t/lib-shell-parser.pl\n@@ -0,0 +1,517 @@\n+use strict;\n+use warnings;\n+\n+# Copyright (c) 2021-2022 Eric Sunshine <sunshine@sunshineco.com>\n+#\n+# Lexer tokenizes POSIX shell scripts. It is roughly modeled after section 2.3\n+# \"Token Recognition\" of POSIX chapter 2 \"Shell Command Language\". Although\n+# similar to lexical analyzers for other languages, this one differs in a few\n+# substantial ways due to quirks of the shell command language.\n+#\n+# For instance, in many languages, newline is just whitespace like space or\n+# TAB, but in shell a newline is a command separator, thus a distinct lexical\n+# token. A newline is significant and returned as a distinct token even at the\n+# end of a shell comment.\n+#\n+# In other languages, `1+2` would typically be scanned as three tokens\n+# (`1`, `+`, and `2`), but in shell it is a single token. However, the similar\n+# `1 + 2`, which embeds whitepace, is scanned as three token in shell, as well.\n+# In shell, several characters with special meaning lose that meaning when not\n+# surrounded by whitespace. For instance, the negation operator `!` is special\n+# when standing alone surrounded by whitespace; whereas in `foo!uucp` it is\n+# just a plain character in the longer token \"foo!uucp\". In many other\n+# languages, `\"string\"/foo:'string'` might be scanned as five tokens (\"string\",\n+# `/`, `foo`, `:`, and 'string'), but in shell, it is just a single token.\n+#\n+# The lexical analyzer for the shell command language is also somewhat unusual\n+# in that it recursively invokes the parser to handle the body of `$(...)`\n+# expressions which can contain arbitrary shell code. Such expressions may be\n+# encountered both inside and outside of double-quoted strings.\n+#\n+# The lexical analyzer is responsible for consuming shell here-doc bodies which\n+# extend from the line following a `<<TAG` operator until a line consisting\n+# solely of `TAG`. Here-doc consumption begins when a newline is encountered.\n+# It is legal for multiple here-doc `<<TAG` operators to be present on a single\n+# line, in which case their bodies must be present one following the next, and\n+# are consumed in the (left-to-right) order the `<<TAG` operators appear on the\n+# line. A special complication is that the bodies of all here-docs must be\n+# consumed when the newline is encountered even if the parse context depth has\n+# changed. For instance, in `cat <<A && x=$(cat <<B &&\\n`, bodies of here-docs\n+# \"A\" and \"B\" must be consumed even though \"A\" was introduced outside the\n+# recursive parse context in which \"B\" was introduced and in which the newline\n+# is encountered.\n+package Lexer;\n+\n+sub new {\n+\tmy ($class, $parser, $s) = @_;\n+\tbless {\n+\t\tparser => $parser,\n+\t\tbuff => $s,\n+\t\tlineno => 1,\n+\t\theretags => []\n+\t} => $class;\n+}\n+\n+sub scan_heredoc_tag {\n+\tmy $self = shift @_;\n+\t${$self->{buff}} =~ /\\G(-?)/gc;\n+\tmy $indented = $1;\n+\tmy $token = $self->scan_token();\n+\treturn \"<<$indented\" unless $token;\n+\tmy $tag = $token->[0];\n+\t$tag =~ s/['\"\\\\]//g;\n+\t$$token[0] = $indented ? \"\\t$tag\" : \"$tag\";\n+\tpush(@{$self->{heretags}}, $token);\n+\treturn \"<<$indented$tag\";\n+}\n+\n+sub scan_op {\n+\tmy ($self, $c) = @_;\n+\tmy $b = $self->{buff};\n+\treturn $c unless $$b =~ /\\G(.)/sgc;\n+\tmy $cc = $c . $1;\n+\treturn scan_heredoc_tag($self) if $cc eq '<<';\n+\treturn $cc if $cc =~ /^(?:&&|\\|\\||>>|;;|<&|>&|<>|>\\|)$/;\n+\tpos($$b)--;\n+\treturn $c;\n+}\n+\n+sub scan_sqstring {\n+\tmy $self = shift @_;\n+\t${$self->{buff}} =~ /\\G([^']*'|.*\\z)/sgc;\n+\tmy $s = $1;\n+\t$self->{lineno} += () = $s =~ /\\n/sg;\n+\treturn \"'\" . $s;\n+}\n+\n+sub scan_dqstring {\n+\tmy $self = shift @_;\n+\tmy $b = $self->{buff};\n+\tmy $s = '\"';\n+\twhile (1) {\n+\t\t# slurp up non-special characters\n+\t\t$s .= $1 if $$b =~ /\\G([^\"\\$\\\\]+)/gc;\n+\t\t# handle special characters\n+\t\tlast unless $$b =~ /\\G(.)/sgc;\n+\t\tmy $c = $1;\n+\t\t$s .= '\"', last if $c eq '\"';\n+\t\t$s .= '$' . $self->scan_dollar(), next if $c eq '$';\n+\t\tif ($c eq '\\\\') {\n+\t\t\t$s .= '\\\\', last unless $$b =~ /\\G(.)/sgc;\n+\t\t\t$c = $1;\n+\t\t\t$self->{lineno}++, next if $c eq \"\\n\"; # line splice\n+\t\t\t# backslash escapes only $, `, \", \\ in dq-string\n+\t\t\t$s .= '\\\\' unless $c =~ /^[\\$`\"\\\\]$/;\n+\t\t\t$s .= $c;\n+\t\t\tnext;\n+\t\t}\n+\t\tdie(\"internal error scanning dq-string '$c'\\n\");\n+\t}\n+\t$self->{lineno} += () = $s =~ /\\n/sg;\n+\treturn $s;\n+}\n+\n+sub scan_balanced {\n+\tmy ($self, $c1, $c2) = @_;\n+\tmy $b = $self->{buff};\n+\tmy $depth = 1;\n+\tmy $s = $c1;\n+\twhile ($$b =~ /\\G([^\\Q$c1$c2\\E]*(?:[\\Q$c1$c2\\E]|\\z))/gc) {\n+\t\t$s .= $1;\n+\t\t$depth++, next if $s =~ /\\Q$c1\\E$/;\n+\t\t$depth--;\n+\t\tlast if $depth == 0;\n+\t}\n+\t$self->{lineno} += () = $s =~ /\\n/sg;\n+\treturn $s;\n+}\n+\n+sub scan_subst {\n+\tmy $self = shift @_;\n+\tmy @tokens = $self->{parser}->parse(qr/^\\)$/);\n+\t$self->{parser}->next_token(); # closing \")\"\n+\treturn @tokens;\n+}\n+\n+sub scan_dollar {\n+\tmy $self = shift @_;\n+\tmy $b = $self->{buff};\n+\treturn $self->scan_balanced('(', ')') if $$b =~ /\\G\\((?=\\()/gc; # $((...))\n+\treturn '(' . join(' ', map {$_->[0]} $self->scan_subst()) . ')' if $$b =~ /\\G\\(/gc; # $(...)\n+\treturn $self->scan_balanced('{', '}') if $$b =~ /\\G\\{/gc; # ${...}\n+\treturn $1 if $$b =~ /\\G(\\w+)/gc; # $var\n+\treturn $1 if $$b =~ /\\G([@*#?$!0-9-])/gc; # $*, $1, $$, etc.\n+\treturn '';\n+}\n+\n+sub swallow_heredocs {\n+\tmy $self = shift @_;\n+\tmy $b = $self->{buff};\n+\tmy $tags = $self->{heretags};\n+\twhile (my $tag = shift @$tags) {\n+\t\tmy $start = pos($$b);\n+\t\tmy $indent = $$tag[0] =~ s/^\\t// ? '\\\\s*' : '';\n+\t\t$$b =~ /(?:\\G|\\n)$indent\\Q$$tag[0]\\E(?:\\n|\\z)/gc;\n+\t\tif (pos($$b) > $start) {\n+\t\t\tmy $body = substr($$b, $start, pos($$b) - $start);\n+\t\t\t$self->{parser}->{heredocs}->{$$tag[0]} = {\n+\t\t\t\tcontent => substr($body, 0, length($body) - length($&)),\n+\t\t\t\tstart_line => $self->{lineno},\n+\t\t        };\n+\t\t\t$self->{lineno} += () = $body =~ /\\n/sg;\n+\t\t\tnext;\n+\t\t}\n+\t\tpush(@{$self->{parser}->{problems}}, ['HEREDOC', $tag]);\n+\t\t$$b =~ /(?:\\G|\\n).*\\z/gc; # consume rest of input\n+\t\tmy $body = substr($$b, $start, pos($$b) - $start);\n+\t\t$self->{lineno} += () = $body =~ /\\n/sg;\n+\t\tlast;\n+\t}\n+}\n+\n+sub scan_token {\n+\tmy $self = shift @_;\n+\tmy $b = $self->{buff};\n+\tmy $token = '';\n+\tmy ($start, $startln);\n+RESTART:\n+\t$startln = $self->{lineno};\n+\t$$b =~ /\\G[ \\t]+/gc; # skip whitespace (but not newline)\n+\t$start = pos($$b) || 0;\n+\t$self->{lineno}++, return [\"\\n\", $start, pos($$b), $startln, $startln] if $$b =~ /\\G#[^\\n]*(?:\\n|\\z)/gc; # comment\n+\twhile (1) {\n+\t\t# slurp up non-special characters\n+\t\t$token .= $1 if $$b =~ /\\G([^\\\\;&|<>(){}'\"\\$\\s]+)/gc;\n+\t\t# handle special characters\n+\t\tlast unless $$b =~ /\\G(.)/sgc;\n+\t\tmy $c = $1;\n+\t\tpos($$b)--, last if $c =~ /^[ \\t]$/; # whitespace ends token\n+\t\tpos($$b)--, last if length($token) && $c =~ /^[;&|<>(){}\\n]$/;\n+\t\t$token .= $self->scan_sqstring(), next if $c eq \"'\";\n+\t\t$token .= $self->scan_dqstring(), next if $c eq '\"';\n+\t\t$token .= $c . $self->scan_dollar(), next if $c eq '$';\n+\t\t$self->{lineno}++, $self->swallow_heredocs(), $token = $c, last if $c eq \"\\n\";\n+\t\t$token = $self->scan_op($c), last if $c =~ /^[;&|<>]$/;\n+\t\t$token = $c, last if $c =~ /^[(){}]$/;\n+\t\tif ($c eq '\\\\') {\n+\t\t\t$token .= '\\\\', last unless $$b =~ /\\G(.)/sgc;\n+\t\t\t$c = $1;\n+\t\t\t$self->{lineno}++, next if $c eq \"\\n\" && length($token); # line splice\n+\t\t\t$self->{lineno}++, goto RESTART if $c eq \"\\n\"; # line splice\n+\t\t\t$token .= '\\\\' . $c;\n+\t\t\tnext;\n+\t\t}\n+\t\tdie(\"internal error scanning character '$c'\\n\");\n+\t}\n+\treturn length($token) ? [$token, $start, pos($$b), $startln, $self->{lineno}] : undef;\n+}\n+\n+# ShellParser parses POSIX shell scripts (with minor extensions for Bash). It\n+# is a recursive descent parser very roughly modeled after section 2.10 \"Shell\n+# Grammar\" of POSIX chapter 2 \"Shell Command Language\".\n+package ShellParser;\n+\n+sub new {\n+\tmy ($class, $s) = @_;\n+\tmy $self = bless {\n+\t\tbuff => [],\n+\t\tstop => [],\n+\t\toutput => [],\n+\t\theredocs => {},\n+\t\tinsubshell => 0,\n+\t} => $class;\n+\t$self->{lexer} = Lexer->new($self, $s);\n+\treturn $self;\n+}\n+\n+sub next_token {\n+\tmy $self = shift @_;\n+\treturn pop(@{$self->{buff}}) if @{$self->{buff}};\n+\treturn $self->{lexer}->scan_token();\n+}\n+\n+sub untoken {\n+\tmy $self = shift @_;\n+\tpush(@{$self->{buff}}, @_);\n+}\n+\n+sub peek {\n+\tmy $self = shift @_;\n+\tmy $token = $self->next_token();\n+\treturn undef unless defined($token);\n+\t$self->untoken($token);\n+\treturn $token;\n+}\n+\n+sub stop_at {\n+\tmy ($self, $token) = @_;\n+\treturn 1 unless defined($token);\n+\tmy $stop = ${$self->{stop}}[-1] if @{$self->{stop}};\n+\treturn defined($stop) && $token->[0] =~ $stop;\n+}\n+\n+sub expect {\n+\tmy ($self, $expect) = @_;\n+\tmy $token = $self->next_token();\n+\treturn $token if defined($token) && $token->[0] eq $expect;\n+\tpush(@{$self->{output}}, \"?!ERR?! expected '$expect' but found '\" . (defined($token) ? $token->[0] : \"<end-of-input>\") . \"'\\n\");\n+\t$self->untoken($token) if defined($token);\n+\treturn ();\n+}\n+\n+sub optional_newlines {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\twhile (my $token = $self->peek()) {\n+\t\tlast unless $token->[0] eq \"\\n\";\n+\t\tpush(@tokens, $self->next_token());\n+\t}\n+\treturn @tokens;\n+}\n+\n+sub parse_group {\n+\tmy $self = shift @_;\n+\treturn ($self->parse(qr/^}$/),\n+\t\t$self->expect('}'));\n+}\n+\n+sub parse_subshell {\n+\tmy $self = shift @_;\n+\t$self->{insubshell}++;\n+\tmy @tokens = ($self->parse(qr/^\\)$/),\n+\t\t      $self->expect(')'));\n+\t$self->{insubshell}--;\n+\treturn @tokens;\n+}\n+\n+sub parse_case_pattern {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\twhile (defined(my $token = $self->next_token())) {\n+\t\tpush(@tokens, $token);\n+\t\tlast if $token->[0] eq ')';\n+\t}\n+\treturn @tokens;\n+}\n+\n+sub parse_case {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\tpush(@tokens,\n+\t     $self->next_token(), # subject\n+\t     $self->optional_newlines(),\n+\t     $self->expect('in'),\n+\t     $self->optional_newlines());\n+\twhile (1) {\n+\t\tmy $token = $self->peek();\n+\t\tlast unless defined($token) && $token->[0] ne 'esac';\n+\t\tpush(@tokens,\n+\t\t     $self->parse_case_pattern(),\n+\t\t     $self->optional_newlines(),\n+\t\t     $self->parse(qr/^(?:;;|esac)$/)); # item body\n+\t\t$token = $self->peek();\n+\t\tlast unless defined($token) && $token->[0] ne 'esac';\n+\t\tpush(@tokens,\n+\t\t     $self->expect(';;'),\n+\t\t     $self->optional_newlines());\n+\t}\n+\tpush(@tokens, $self->expect('esac'));\n+\treturn @tokens;\n+}\n+\n+sub parse_for {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\tpush(@tokens,\n+\t     $self->next_token(), # variable\n+\t     $self->optional_newlines());\n+\tmy $token = $self->peek();\n+\tif (defined($token) && $token->[0] eq 'in') {\n+\t\tpush(@tokens,\n+\t\t     $self->expect('in'),\n+\t\t     $self->optional_newlines());\n+\t}\n+\tpush(@tokens,\n+\t     $self->parse(qr/^do$/), # items\n+\t     $self->expect('do'),\n+\t     $self->optional_newlines(),\n+\t     $self->parse_loop_body(),\n+\t     $self->expect('done'));\n+\treturn @tokens;\n+}\n+\n+sub parse_if {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\twhile (1) {\n+\t\tpush(@tokens,\n+\t\t     $self->parse(qr/^then$/), # if/elif condition\n+\t\t     $self->expect('then'),\n+\t\t     $self->optional_newlines(),\n+\t\t     $self->parse(qr/^(?:elif|else|fi)$/)); # if/elif body\n+\t\tmy $token = $self->peek();\n+\t\tlast unless defined($token) && $token->[0] eq 'elif';\n+\t\tpush(@tokens, $self->expect('elif'));\n+\t}\n+\tmy $token = $self->peek();\n+\tif (defined($token) && $token->[0] eq 'else') {\n+\t\tpush(@tokens,\n+\t\t     $self->expect('else'),\n+\t\t     $self->optional_newlines(),\n+\t\t     $self->parse(qr/^fi$/)); # else body\n+\t}\n+\tpush(@tokens, $self->expect('fi'));\n+\treturn @tokens;\n+}\n+\n+sub parse_loop_body {\n+\tmy $self = shift @_;\n+\treturn $self->parse(qr/^done$/);\n+}\n+\n+sub parse_loop {\n+\tmy $self = shift @_;\n+\treturn ($self->parse(qr/^do$/), # condition\n+\t\t$self->expect('do'),\n+\t\t$self->optional_newlines(),\n+\t\t$self->parse_loop_body(),\n+\t\t$self->expect('done'));\n+}\n+\n+sub parse_func {\n+\tmy $self = shift @_;\n+\treturn ($self->expect('('),\n+\t\t$self->expect(')'),\n+\t\t$self->optional_newlines(),\n+\t\t$self->parse_cmd()); # body\n+}\n+\n+sub parse_bash_array_assignment {\n+\tmy $self = shift @_;\n+\tmy @tokens = $self->expect('(');\n+\twhile (defined(my $token = $self->next_token())) {\n+\t\tpush(@tokens, $token);\n+\t\tlast if $token->[0] eq ')';\n+\t}\n+\treturn @tokens;\n+}\n+\n+my %compound = (\n+\t'{' => \\&parse_group,\n+\t'(' => \\&parse_subshell,\n+\t'case' => \\&parse_case,\n+\t'for' => \\&parse_for,\n+\t'if' => \\&parse_if,\n+\t'until' => \\&parse_loop,\n+\t'while' => \\&parse_loop);\n+\n+sub parse_cmd {\n+\tmy $self = shift @_;\n+\tmy $cmd = $self->next_token();\n+\treturn () unless defined($cmd);\n+\treturn $cmd if $cmd->[0] eq \"\\n\";\n+\n+\tmy $token;\n+\tmy @tokens = $cmd;\n+\tif ($cmd->[0] eq '!') {\n+\t\tpush(@tokens, $self->parse_cmd());\n+\t\treturn @tokens;\n+\t} elsif (my $f = $compound{$cmd->[0]}) {\n+\t\tpush(@tokens, $self->$f());\n+\t} elsif (defined($token = $self->peek()) && $token->[0] eq '(') {\n+\t\tif ($cmd->[0] !~ /\\w=$/) {\n+\t\t\tpush(@tokens, $self->parse_func());\n+\t\t\treturn @tokens;\n+\t\t}\n+\t\tmy @array = $self->parse_bash_array_assignment();\n+\t\t$tokens[-1]->[0] .= join(' ', map {$_->[0]} @array);\n+\t\t$tokens[-1]->[2] = $array[$#array][2] if @array;\n+\t}\n+\n+\twhile (defined(my $token = $self->next_token())) {\n+\t\t$self->untoken($token), last if $self->stop_at($token);\n+\t\tpush(@tokens, $token);\n+\t\tlast if $token->[0] =~ /^(?:[;&\\n|]|&&|\\|\\|)$/;\n+\t}\n+\tpush(@tokens, $self->next_token()) if $tokens[-1]->[0] ne \"\\n\" && defined($token = $self->peek()) && $token->[0] eq \"\\n\";\n+\treturn @tokens;\n+}\n+\n+sub accumulate {\n+\tmy ($self, $tokens, $cmd) = @_;\n+\tpush(@$tokens, @$cmd);\n+}\n+\n+sub parse {\n+\tmy ($self, $stop) = @_;\n+\tpush(@{$self->{stop}}, $stop);\n+\tgoto DONE if $self->stop_at($self->peek());\n+\tmy @tokens;\n+\twhile (my @cmd = $self->parse_cmd()) {\n+\t\t$self->accumulate(\\@tokens, \\@cmd);\n+\t\tlast if $self->stop_at($self->peek());\n+\t}\n+DONE:\n+\tpop(@{$self->{stop}});\n+\treturn @tokens;\n+}\n+\n+# ScriptParser is a subclass of ShellParser which identifies individual test\n+# definitions within test scripts and calls check_test() for each test body\n+# found.  Callers subclass ScriptParser and override check_test() to\n+# implement specific checks (e.g. chainlint checks &&-chains, lint-style\n+# checks grep usage).\n+package ScriptParser;\n+\n+our @ISA = ('ShellParser');\n+\n+# extract the raw content of a token, which may be a single string or a\n+# composition of multiple strings and non-string character runs; for instance,\n+# `\"test body\"` unwraps to `test body`; `word\"a b\"42'c d'` to `worda b42c d`\n+sub unwrap {\n+\tmy $token = (@_ ? shift @_ : $_)->[0];\n+\t# simple case: 'sqstring' or \"dqstring\"\n+\treturn $token if $token =~ s/^'([^']*)'$/$1/;\n+\treturn $token if $token =~ s/^\"([^\"]*)\"$/$1/;\n+\n+\t# composite case\n+\tmy ($s, $q, $escaped);\n+\twhile (1) {\n+\t\t# slurp up non-special characters\n+\t\t$s .= $1 if $token =~ /\\G([^\\\\'\"]*)/gc;\n+\t\t# handle special characters\n+\t\tlast unless $token =~ /\\G(.)/sgc;\n+\t\tmy $c = $1;\n+\t\t$q = undef, next if defined($q) && $c eq $q;\n+\t\t$q = $c, next if !defined($q) && $c =~ /^['\"]$/;\n+\t\tif ($c eq '\\\\') {\n+\t\t\tlast unless $token =~ /\\G(.)/sgc;\n+\t\t\t$c = $1;\n+\t\t\t$s .= '\\\\' if $c eq \"\\n\"; # preserve line splice\n+\t\t}\n+\t\t$s .= $c;\n+\t}\n+\treturn $s\n+}\n+\n+sub check_test {\n+\t# no-op; subclasses override to implement specific checks\n+}\n+\n+sub parse_cmd {\n+\tmy $self = shift @_;\n+\tmy @tokens = $self->SUPER::parse_cmd();\n+\treturn @tokens unless @tokens && $tokens[0]->[0] =~ /^test_expect_(?:success|failure)$/;\n+\tmy $n = $#tokens;\n+\t$n-- while $n >= 0 && $tokens[$n]->[0] =~ /^(?:[;&\\n|]|&&|\\|\\|)$/;\n+\tmy $herebody;\n+\tif ($n >= 2 && $tokens[$n-1]->[0] eq '-' && $tokens[$n]->[0] =~ /^<<-?(.+)$/) {\n+\t\t$herebody = $self->{heredocs}->{$1};\n+\t\t$n--;\n+\t}\n+\t$self->check_test($tokens[1], $tokens[2], $herebody) if $n == 2; # title body\n+\t$self->check_test($tokens[2], $tokens[3], $herebody) if $n > 2;  # prereq title body\n+\treturn @tokens;\n+}\n+\n+1;\n-- \ngitgitgadget\n\n"},{"id":"544683","messageId":"c1b90101ef5c38a21fc901bd7387acf83eb96806.1780559158.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.git.1780559158.gitgitgadget@gmail.com","subject":"[PATCH 4/6] t: add lint-style.pl with test_grep negation rule","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-04T07:45:56Z","receivedAt":"2026-06-04T07:46:07Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nAdd a mechanical lint checker for test scripts, similar in spirit to\ncheck-non-portable-shell.pl but focused on test conventions rather\nthan portability.\n\nThe tool defines LintParser, a subclass of ScriptParser (from the\nshared lib-shell-parser.pl module).  ScriptParser's\nparse_cmd() finds test_expect_success blocks and calls check_test()\nfor each body; LintParser overrides check_test() to run lint rules\non the parsed commands.  A \"# lint-ok\" comment suppresses all\nchecks for intentional style violations.\n\nThe first rule detects '! test_grep' and replaces it with\n'test_grep !'.  Shell-level negation suppresses the diagnostic\noutput that test_grep prints on failure; the built-in negation\npreserves it.\n\nThree violations inside test bodies are converted via --fix.  One\nadditional violation in a helper function outside test_expect_success\n(t7900's test_geometric_repack_needed) is converted manually, since\nthe parser only processes test bodies.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/.gitattributes                           |   2 +\n t/Makefile                                 |  32 +++-\n t/lint-style.pl                            | 200 +++++++++++++++++++++\n t/lint-style/heredoc.expect                |   3 +\n t/lint-style/heredoc.test                  |  14 ++\n t/lint-style/test-grep-negation-fix.expect |   4 +\n t/lint-style/test-grep-negation-fix.test   |   4 +\n t/lint-style/test-grep-negation.expect     |   3 +\n t/lint-style/test-grep-negation.test       |   4 +\n t/t0031-lockfile-pid.sh                    |   2 +-\n t/t5300-pack-object.sh                     |   2 +-\n t/t5319-multi-pack-index.sh                |   2 +-\n t/t7900-maintenance.sh                     |   2 +-\n 13 files changed, 268 insertions(+), 6 deletions(-)\n create mode 100755 t/lint-style.pl\n create mode 100644 t/lint-style/heredoc.expect\n create mode 100644 t/lint-style/heredoc.test\n create mode 100644 t/lint-style/test-grep-negation-fix.expect\n create mode 100644 t/lint-style/test-grep-negation-fix.test\n create mode 100644 t/lint-style/test-grep-negation.expect\n create mode 100644 t/lint-style/test-grep-negation.test\n\ndiff --git a/t/.gitattributes b/t/.gitattributes\nindex 7664c6e027..aea6889d03 100644\n--- a/t/.gitattributes\n+++ b/t/.gitattributes\n@@ -1,5 +1,7 @@\n t[0-9][0-9][0-9][0-9]/* -whitespace\n /chainlint/*.expect eol=lf -whitespace\n+/lint-style/*.expect eol=lf -whitespace\n+/lint-style/*.test eol=lf -whitespace\n /t0110/url-* binary\n /t3206/* eol=lf\n /t3900/*.txt eol=lf\ndiff --git a/t/Makefile b/t/Makefile\nindex 25f923fed9..3a5fa4ce37 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -46,6 +46,7 @@ TPERF = $(sort $(wildcard perf/p[0-9][0-9][0-9][0-9]-*.sh))\n TINTEROP = $(sort $(wildcard interop/i[0-9][0-9][0-9][0-9]-*.sh))\n CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.test)))\n CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl\n+LINT_STYLE_TESTS = $(sort $(wildcard lint-style/*.test))\n UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)\n UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))\n UNIT_TEST_PROGRAMS += unit-tests/bin/unit-tests$(X)\n@@ -139,7 +140,7 @@ check-meson:\n test-lint: test-lint-duplicates test-lint-executable \\\n \ttest-lint-filenames\n ifneq ($(PERL_PATH),)\n-test-lint: test-lint-shell-syntax check-shell-parser\n+test-lint: test-lint-shell-syntax test-lint-style check-lint-style check-shell-parser\n else\n GIT_TEST_CHAIN_LINT = 0\n endif\n@@ -162,6 +163,32 @@ test-lint-shell-syntax:\n \n check-shell-parser:\n \t@'$(PERL_PATH_SQ)' check-shell-parser.pl\n+\n+test-lint-style:\n+\t@'$(PERL_PATH_SQ)' lint-style.pl $(T) $(THELPERS) $(TPERF)\n+\n+check-lint-style:\n+\t@rc=0; for t in $(LINT_STYLE_TESTS); do \\\n+\t\tbase=$${t%.test}; \\\n+\t\tcase $$base in \\\n+\t\t*-fix) \\\n+\t\t\tcp \"$$t\" \"$$t.tmp\" && \\\n+\t\t\t'$(PERL_PATH_SQ)' lint-style.pl --fix \"$$t.tmp\" >/dev/null 2>&1; \\\n+\t\t\tfix_rc=$$?; \\\n+\t\t\tif test $$fix_rc != 0; then \\\n+\t\t\t\techo \"FAIL: $$t (--fix exit code $$fix_rc)\"; rc=1; \\\n+\t\t\telif ! diff -u \"$$base.expect\" \"$$t.tmp\"; then \\\n+\t\t\t\techo \"FAIL: $$t (--fix output)\"; rc=1; \\\n+\t\t\tfi; \\\n+\t\t\trm -f \"$$t.tmp\" ;; \\\n+\t\t*) \\\n+\t\t\tif ! '$(PERL_PATH_SQ)' lint-style.pl \"$$t\" 2>&1 | \\\n+\t\t\t\tdiff -u \"$$base.expect\" -; then \\\n+\t\t\t\techo \"FAIL: $$t\"; rc=1; \\\n+\t\t\tfi ;; \\\n+\t\tesac; \\\n+\tdone; test $$rc = 0\n+\n test-lint-filenames:\n \t@# We do *not* pass a glob to ls-files but use grep instead, to catch\n \t@# non-ASCII characters (which are quoted within double-quotes)\n@@ -188,7 +215,8 @@ perf:\n \n .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \\\n \tcheck-chainlint clean-chainlint test-chainlint \\\n-\tcheck-shell-parser $(UNIT_TESTS)\n+\tcheck-shell-parser \\\n+\tcheck-lint-style test-lint-style $(UNIT_TESTS)\n \n .PHONY: libgit-sys-test libgit-rs-test\n libgit-sys-test:\ndiff --git a/t/lint-style.pl b/t/lint-style.pl\nnew file mode 100755\nindex 0000000000..9268577f9b\n--- /dev/null\n+++ b/t/lint-style.pl\n@@ -0,0 +1,200 @@\n+#!/usr/bin/perl\n+\n+# Check test scripts for style violations that can be detected\n+# mechanically, such as using bare 'grep' where test_grep should\n+# be used.  Use --fix to automatically apply suggested replacements.\n+#\n+# Detection uses parsed tokens from the shared shell parser for\n+# correct handling of heredocs, $(...), pipes, and quoting.\n+# Fixes modify the original file text to preserve formatting.\n+\n+use strict;\n+use warnings;\n+use File::Basename;\n+# Force LF output so check-lint-style's diff against the\n+# pre-committed .expect files works on Windows.\n+binmode(STDOUT, ':unix');\n+binmode(STDERR, ':unix');\n+\n+my $fix_mode = 0;\n+if (@ARGV && $ARGV[0] eq '--fix') {\n+\t$fix_mode = 1;\n+\tshift @ARGV;\n+}\n+\n+# Load the shared shell parser (Lexer, ShellParser, ScriptParser).\n+my $_lib = dirname($0) . \"/lib-shell-parser.pl\";\n+$_lib = \"./$_lib\" unless $_lib =~ m{^/};\n+do $_lib or die \"$0: failed to load $_lib: $@$!\\n\";\n+\n+# LintParser is a subclass of ScriptParser which runs lint rules\n+# on each test body.  Per-file state (file name, raw lines, dirty\n+# flag) is stored on the instance before calling parse().\n+#\n+# Subroutines defined below (parse_commands, check_test_grep_negation,\n+# etc.) are in package main and called with the main:: prefix.\n+# File-scoped lexicals ($fix_mode, $has_fixable, etc.) are visible\n+# across packages since 'package' does not introduce a new scope.\n+package LintParser;\n+our @ISA = ('ScriptParser');\n+\n+package main;\n+\n+my $exit_code = 0;\n+my $has_fixable = 0;\n+\n+sub err {\n+\tmy ($file, $lineno, $line, $msg, %opts) = @_;\n+\t$line =~ s/^\\s+//;\n+\t$line =~ s/\\s+$//;\n+\t$line =~ s/\\s+/ /g;\n+\tmy $prefix = ($fix_mode && $opts{fixable}) ? 'fixed' : 'error';\n+\tprint \"$file:$lineno: $prefix: $msg: $line\\n\";\n+\t$exit_code = 1 unless $fix_mode && $opts{fixable};\n+}\n+\n+# Report a lint violation found by a rule.  In --fix mode, apply\n+# the regex substitution on the raw line and report success.\n+# Otherwise just report.  Returns 1 if the line was modified.\n+sub report_violation {\n+\tmy ($file, $cmd, $line_ref, $match, $fix, $from) = @_;\n+\tmy $lineno = $cmd->{lineno};\n+\tmy $display = join(' ', @{$cmd->{tokens}});\n+\t$has_fixable++;  # count for the \"--fix\" hint\n+\tif ($fix_mode) {\n+\t\tif ($$line_ref =~ s/$match/$fix/) {\n+\t\t\terr $file, $lineno, $display,\n+\t\t\t\t\"replace '$from' with '$fix'\",\n+\t\t\t\tfixable => 1;\n+\t\t\treturn 1;\n+\t\t}\n+\t\terr $file, $lineno, $display,\n+\t\t\t\"replace '$from' with '$fix' (could not auto-fix)\";\n+\t} else {\n+\t\terr $file, $lineno, $display,\n+\t\t\t\"replace '$from' with '$fix'\";\n+\t}\n+\treturn 0;\n+}\n+\n+# Split a token stream into commands at &&, ||, ;;, and \\n.\n+sub parse_commands {\n+\tmy ($content) = @_;\n+\tmy $parser = ShellParser->new(\\$content);\n+\tmy @all_tokens = $parser->parse();\n+\n+\tmy @commands;\n+\tmy @current;\n+\tmy $lineno = 1;\n+\n+\tfor (my $ti = 0; $ti < @all_tokens; $ti++) {\n+\t\tmy $text = $all_tokens[$ti]->[0];\n+\t\tif ($text =~ /^(?:&&|\\|\\||;;|\\n)$/) {\n+\t\t\tif (@current) {\n+\t\t\t\tpush @commands, {\n+\t\t\t\t\ttokens => [@current],\n+\t\t\t\t\tlineno => $lineno,\n+\t\t\t\t};\n+\t\t\t\t@current = ();\n+\t\t\t}\n+\t\t} else {\n+\t\t\t$lineno = $all_tokens[$ti]->[3]\n+\t\t\t\tif !@current && defined $all_tokens[$ti]->[3];\n+\t\t\tpush @current, $text;\n+\t\t}\n+\t}\n+\tif (@current) {\n+\t\tpush @commands, {\n+\t\t\ttokens => [@current],\n+\t\t\tlineno => $lineno,\n+\t\t};\n+\t}\n+\treturn @commands;\n+}\n+\n+# --- Rule: '! test_grep' should be 'test_grep !' ---\n+# Shell-level negation suppresses test_grep's diagnostic output\n+# on failure.  Built-in negation preserves it.\n+sub check_test_grep_negation {\n+\tmy ($cmd, $file, $line_ref) = @_;\n+\tmy @tokens = @{$cmd->{tokens}};\n+\treturn unless @tokens >= 2 && $tokens[0] eq '!' && $tokens[1] eq 'test_grep';\n+\n+\treturn report_violation($file, $cmd, $line_ref,\n+\t\tqr/!\\s*test_grep/, 'test_grep !', '! test_grep');\n+}\n+\n+# Map parsed commands back to raw file lines for --fix.\n+# Detection uses parsed tokens (correct handling of quoting,\n+# heredocs, pipes) but fixes must modify the original text\n+# to preserve formatting.\n+package LintParser;\n+\n+sub check_test {\n+\t# Called by ScriptParser::parse_cmd for each test_expect_success\n+\t# or test_expect_failure block.\n+\tmy $self = shift @_;\n+\tmy $title = ScriptParser::unwrap(shift @_);\n+\n+\t# Two test body formats:\n+\t#   Quoted:  test_expect_success 'title' '..body..'\n+\t#   Heredoc: test_expect_success 'title' - <<\\EOF\n+\t#              ..body..\n+\t#            EOF\n+\t# For quoted, the body token is the quoted string.\n+\t# For heredoc, the body token is '-' and the actual\n+\t# code arrives as the next argument from the Lexer.\n+\tmy $body_token = shift @_;\n+\tmy $lineno_base = $body_token->[3] || 1;\n+\tmy $body = ScriptParser::unwrap($body_token);\n+\n+\tif ($body eq '-') {\n+\t\tmy $herebody = shift @_;\n+\t\tif ($herebody) {\n+\t\t\t$body = $herebody->{content};\n+\t\t\t$lineno_base = $herebody->{start_line} || 1;\n+\t\t}\n+\t}\n+\treturn unless $body;\n+\n+\t# Map each command back to its file line number.\n+\t# $lineno_base is where the body starts in the file;\n+\t# $cmd->{lineno} is relative to the body (starting at 1).\n+\tmy $raw_lines = $self->{raw_lines};\n+\tfor my $cmd (main::parse_commands($body)) {\n+\t\tmy $ln = ($cmd->{lineno} || 0) + $lineno_base - 1;\n+\t\t$cmd->{lineno} = $ln;\n+\t\tnext unless $ln >= 1 && $ln <= @$raw_lines;\n+\t\tnext if $raw_lines->[$ln - 1] =~ /#.*lint-ok/;\n+\n+\t\tif (main::check_test_grep_negation($cmd, $self->{file}, \\$raw_lines->[$ln - 1])) {\n+\t\t\t$self->{dirty} = 1;\n+\t\t}\n+\t}\n+}\n+\n+package main;\n+\n+for my $file (@ARGV) {\n+\t# :unix:crlf strips \\r on Windows (same as chainlint.pl)\n+\topen(my $fh, '<:unix:crlf', $file) or die \"$0: $file: $!\\n\";\n+\tmy @raw_lines = <$fh>;\n+\tclose $fh;\n+\n+\tmy $parser = LintParser->new(\\join('', @raw_lines));\n+\t$parser->{file} = $file;\n+\t$parser->{raw_lines} = \\@raw_lines;\n+\t$parser->{dirty} = 0;\n+\t$parser->parse();\n+\n+\tif ($fix_mode && $parser->{dirty}) {\n+\t\topen(my $out, '>', $file) or die \"$0: $file: $!\\n\";\n+\t\tprint $out @{$parser->{raw_lines}};\n+\t\tclose $out;\n+\t}\n+}\n+\n+if ($has_fixable && !$fix_mode) {\n+\tprint \"hint: run with --fix to apply the suggested replacements.\\n\";\n+}\n+exit $exit_code;\ndiff --git a/t/lint-style/heredoc.expect b/t/lint-style/heredoc.expect\nnew file mode 100644\nindex 0000000000..7ff6d4a52d\n--- /dev/null\n+++ b/t/lint-style/heredoc.expect\n@@ -0,0 +1,3 @@\n+lint-style/heredoc.test:8: error: replace '! test_grep' with 'test_grep !': ! test_grep \"after-heredoc-is-caught\" actual\n+lint-style/heredoc.test:13: error: replace '! test_grep' with 'test_grep !': ! test_grep \"not-inside-sed-heredoc\" actual\n+hint: run with --fix to apply the suggested replacements.\ndiff --git a/t/lint-style/heredoc.test b/t/lint-style/heredoc.test\nnew file mode 100644\nindex 0000000000..4c05831cfb\n--- /dev/null\n+++ b/t/lint-style/heredoc.test\n@@ -0,0 +1,14 @@\n+test_expect_success 'greps inside heredocs are skipped' '\n+\tcat <<-EOF &&\n+\tgrep \"inside-strip-tabs\" file\n+\tEOF\n+\tcat <<-\\EOF &&\n+\tgrep \"inside-no-expand\" file\n+\tEOF\n+\t! test_grep \"after-heredoc-is-caught\" actual\n+'\n+\n+test_expect_success 'sed with << does not start a heredoc' '\n+\tsed \"s/<< foo/bar/\" file &&\n+\t! test_grep \"not-inside-sed-heredoc\" actual\n+'\ndiff --git a/t/lint-style/test-grep-negation-fix.expect b/t/lint-style/test-grep-negation-fix.expect\nnew file mode 100644\nindex 0000000000..28ecde1073\n--- /dev/null\n+++ b/t/lint-style/test-grep-negation-fix.expect\n@@ -0,0 +1,4 @@\n+test_expect_success 'negated test_grep' '\n+\ttest_grep ! \"pattern\" actual &&\n+\ttest_grep ! -i \"insensitive\" actual\n+'\ndiff --git a/t/lint-style/test-grep-negation-fix.test b/t/lint-style/test-grep-negation-fix.test\nnew file mode 100644\nindex 0000000000..571c150031\n--- /dev/null\n+++ b/t/lint-style/test-grep-negation-fix.test\n@@ -0,0 +1,4 @@\n+test_expect_success 'negated test_grep' '\n+\t! test_grep \"pattern\" actual &&\n+\t! test_grep -i \"insensitive\" actual\n+'\ndiff --git a/t/lint-style/test-grep-negation.expect b/t/lint-style/test-grep-negation.expect\nnew file mode 100644\nindex 0000000000..1fa9e124aa\n--- /dev/null\n+++ b/t/lint-style/test-grep-negation.expect\n@@ -0,0 +1,3 @@\n+lint-style/test-grep-negation.test:2: error: replace '! test_grep' with 'test_grep !': ! test_grep \"pattern\" actual\n+lint-style/test-grep-negation.test:3: error: replace '! test_grep' with 'test_grep !': ! test_grep -i \"insensitive\" actual\n+hint: run with --fix to apply the suggested replacements.\ndiff --git a/t/lint-style/test-grep-negation.test b/t/lint-style/test-grep-negation.test\nnew file mode 100644\nindex 0000000000..571c150031\n--- /dev/null\n+++ b/t/lint-style/test-grep-negation.test\n@@ -0,0 +1,4 @@\n+test_expect_success 'negated test_grep' '\n+\t! test_grep \"pattern\" actual &&\n+\t! test_grep -i \"insensitive\" actual\n+'\ndiff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh\nindex 8ef87addf5..e9e2f04049 100755\n--- a/t/t0031-lockfile-pid.sh\n+++ b/t/t0031-lockfile-pid.sh\n@@ -29,7 +29,7 @@ test_expect_success 'PID info not shown by default' '\n \t\ttest_must_fail git add . 2>err &&\n \t\t# Should not crash, just show normal error without PID\n \t\ttest_grep \"Unable to create\" err &&\n-\t\t! test_grep \"is held by process\" err\n+\t\ttest_grep ! \"is held by process\" err\n \t)\n '\n \ndiff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh\nindex 73445782e7..3179b4963e 100755\n--- a/t/t5300-pack-object.sh\n+++ b/t/t5300-pack-object.sh\n@@ -720,7 +720,7 @@ test_expect_success '--name-hash-version=2 and --write-bitmap-index are incompat\n \n \t# --stdout option silently removes --write-bitmap-index\n \tgit pack-objects --stdout --all --name-hash-version=2 --write-bitmap-index >out 2>err &&\n-\t! test_grep \"currently, --write-bitmap-index requires --name-hash-version=1\" err\n+\ttest_grep ! \"currently, --write-bitmap-index requires --name-hash-version=1\" err\n '\n \n test_expect_success '--path-walk pack everything' '\ndiff --git a/t/t5319-multi-pack-index.sh b/t/t5319-multi-pack-index.sh\nindex fa0d4046f7..9154d9795f 100755\n--- a/t/t5319-multi-pack-index.sh\n+++ b/t/t5319-multi-pack-index.sh\n@@ -1175,7 +1175,7 @@ test_expect_success 'load reverse index when missing .idx, .pack' '\n \n test_expect_success 'usage shown without sub-command' '\n \ttest_expect_code 129 git multi-pack-index 2>err &&\n-\t! test_grep \"unrecognized subcommand\" err\n+\ttest_grep ! \"unrecognized subcommand\" err\n '\n \n test_expect_success 'complains when run outside of a repository' '\ndiff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\nindex d7f82e1bec..9db4a76f67 100755\n--- a/t/t7900-maintenance.sh\n+++ b/t/t7900-maintenance.sh\n@@ -664,7 +664,7 @@ test_geometric_repack_needed () {\n \ttrue)\n \t\ttest_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n \tfalse)\n-\t\t! test_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n+\t\ttest_grep ! \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n \t*)\n \t\tBUG \"invalid parameter: $NEEDED\";;\n \tesac\n-- \ngitgitgadget\n\n"},{"id":"544684","messageId":"c0db9fdb5bf9a9b8218e32de043b8a9ca1aaf023.1780559158.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.git.1780559158.gitgitgadget@gmail.com","subject":"[PATCH 5/6] t: fix grep assertions missing file arguments","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-04T07:45:57Z","receivedAt":"2026-06-04T07:46:09Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nThree grep assertions were missing their file arguments, causing\nthem to read from empty stdin instead of the intended file:\n\n- t2402: '! grep ...' should read from 'out', matching the\n  grep on the preceding line.\n- t7507: the closing quote is in the wrong place, making the\n  entire 'diff --git actual' a single pattern with no file\n  argument instead of pattern 'diff --git' and file 'actual'.\n- t7700: '! grep ...' should read from 'packlist', matching\n  the redirect on the preceding line.\n\nWithout file arguments these greps always succeed (empty stdin\nmatches nothing), so the assertions were not actually checking\nanything.  All three tests pass with the corrected file arguments,\nconfirming the intended behavior is sound.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/t2402-worktree-list.sh  | 2 +-\n t/t7507-commit-verbose.sh | 2 +-\n t/t7700-repack.sh         | 2 +-\n 3 files changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh\nindex e0c6abd2f5..93f92e854a 100755\n--- a/t/t2402-worktree-list.sh\n+++ b/t/t2402-worktree-list.sh\n@@ -144,7 +144,7 @@ test_expect_success '\"list\" all worktrees with prunable annotation' '\n \trm -rf prunable &&\n \tgit worktree list >out &&\n \tgrep \"/prunable  *[0-9a-f].* prunable$\" out &&\n-\t! grep \"/unprunable  *[0-9a-f].* prunable$\"\n+\t! grep \"/unprunable  *[0-9a-f].* prunable$\" out\n '\n \n test_expect_success '\"list\" all worktrees --porcelain with prunable' '\ndiff --git a/t/t7507-commit-verbose.sh b/t/t7507-commit-verbose.sh\nindex b53d71c086..acdb6b1455 100755\n--- a/t/t7507-commit-verbose.sh\n+++ b/t/t7507-commit-verbose.sh\n@@ -163,7 +163,7 @@ done\n \n test_expect_success \"status ignores commit.verbose=true\" '\n \tgit -c commit.verbose=true status >actual &&\n-\t! grep \"^diff --git actual\"\n+\t! grep \"^diff --git\" actual\n '\n \n test_done\ndiff --git a/t/t7700-repack.sh b/t/t7700-repack.sh\nindex 63ef63fc50..c6ff3aed30 100755\n--- a/t/t7700-repack.sh\n+++ b/t/t7700-repack.sh\n@@ -194,7 +194,7 @@ test_expect_success 'local packed unreachable obs that exist in alternate ODB ar\n \trm -f .git/objects/pack/* &&\n \tmv pack-* .git/objects/pack/ &&\n \tgit verify-pack -v -- .git/objects/pack/*.idx >packlist &&\n-\t! grep \"^$coid \" &&\n+\t! grep \"^$coid \" packlist &&\n \techo >.git/objects/info/alternates &&\n \ttest_must_fail git show $coid\n '\n-- \ngitgitgadget\n\n"},{"id":"544690","messageId":"1527293f1cb9e7ba2e0b0edeb0761b53d9de368a.1780559158.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.git.1780559158.gitgitgadget@gmail.com","subject":"[PATCH 6/6] t: lint and convert grep assertions to test_grep","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-04T07:45:58Z","receivedAt":"2026-06-04T07:46:15Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nExtend lint-style.pl with a rule that detects bare 'grep' used as a\ntest assertion and converts it to test_grep.  test_grep prints the\nfile contents on failure, making test debugging significantly easier.\n\nparse_commands() is extended to split at shell structural tokens\n({, }, (, ), |) and keywords (if, then, for, etc.), and each\ncommand gains a token_pos index so that rules can scan backward and\nforward in the token stream for context.\n\nThree new functions implement the grep-assertion rule:\n\n - is_filter_context() scans the surrounding tokens for pipes,\n   control-flow keywords (if/elif/while/until), for-in value\n   lists, and brace groups with output redirects.\n\n - is_grep_assertion() classifies a grep command: convertible\n   assertion (pattern and file present), filter (not an assertion),\n   or missing file argument (flagged as a likely bug).\n\n - check_bare_grep() ties them together and calls\n   report_violation() with the appropriate fix.\n\nThe --fix mode handles:\n - Replacing 'grep' with 'test_grep'\n - Moving negation from '! grep' to 'test_grep !'\n - Stripping the -q flag (test_grep inherently checks match status)\n\nFive files require '# lint-ok' annotations for intentional grep\nusage that cannot be mechanically converted: t1400 (packed-refs\nmay not exist on reftable), t3901 (piped stdin via case block),\nt6437 (glob argument breaks test_grep's test -f check), t7450\n(file may not exist after failed MINGW clone), and t7527 ($?\ncapture on the next line).\n\nThe test-lint-style scope is extended to include sourced test\nfragments in subdirectories (t5411/*.sh and similar) via a new\nTSOURCED variable.\n\nRun '--fix' to convert all ~2800 grep assertions across ~340 files\nin the test suite.  test-lib-functions.sh and lib-rebase.sh are\nexcluded from linting since they implement test infrastructure\nrather than test assertions.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/Makefile                                   |   4 +-\n t/for-each-ref-tests.sh                      |  12 +-\n t/lib-bitmap.sh                              |  12 +-\n t/lib-bundle-uri-protocol.sh                 |  26 +-\n t/lib-httpd.sh                               |   2 +-\n t/lint-style.pl                              | 289 ++++++++++++++--\n t/lint-style/grep-assert.expect              |  13 +\n t/lint-style/grep-assert.test                |  24 ++\n t/lint-style/grep-fix.expect                 |  16 +\n t/lint-style/grep-fix.test                   |  16 +\n t/lint-style/grep-missing-file.expect        |   4 +\n t/lint-style/grep-missing-file.test          |   6 +\n t/lint-style/grep-negated.expect             |   5 +\n t/lint-style/grep-negated.test               |   9 +\n t/lint-style/grep-not-assert.expect          |   0\n t/lint-style/grep-not-assert.test            |  43 +++\n t/pack-refs-tests.sh                         |   2 +-\n t/show-ref-exists-tests.sh                   |   2 +-\n t/t0000-basic.sh                             |  16 +-\n t/t0001-init.sh                              |  18 +-\n t/t0008-ignores.sh                           |   8 +-\n t/t0009-git-dir-validation.sh                |   6 +-\n t/t0012-help.sh                              |   4 +-\n t/t0013-sha1dc.sh                            |   2 +-\n t/t0017-env-helper.sh                        |   4 +-\n t/t0021-conversion.sh                        |  18 +-\n t/t0029-core-unsetenvvars.sh                 |   4 +-\n t/t0030-stripspace.sh                        |   4 +-\n t/t0040-parse-options.sh                     |  52 +--\n t/t0041-usage.sh                             |   2 +-\n t/t0052-simple-ipc.sh                        |  10 +-\n t/t0061-run-command.sh                       |   2 +-\n t/t0066-dir-iterator.sh                      |   2 +-\n t/t0068-for-each-repo.sh                     |  16 +-\n t/t0070-fundamental.sh                       |   6 +-\n t/t0081-find-pack.sh                         |  12 +-\n t/t0091-bugreport.sh                         |  18 +-\n t/t0092-diagnose.sh                          |  12 +-\n t/t0100-previous.sh                          |   2 +-\n t/t0200-gettext-basic.sh                     |  14 +-\n t/t0203-gettext-setlocale-sanity.sh          |   4 +-\n t/t0204-gettext-reencode-sanity.sh           |   8 +-\n t/t0210-trace2-normal.sh                     |   6 +-\n t/t0211-trace2-perf.sh                       |  80 ++---\n t/t0212-trace2-event.sh                      |   8 +-\n t/t0300-credentials.sh                       |   4 +-\n t/t0410-partial-clone.sh                     |  82 ++---\n t/t0450-txt-doc-vs-help.sh                   |   2 +-\n t/t0500-progress-display.sh                  |  18 +-\n t/t0610-reftable-basics.sh                   |   8 +-\n t/t1004-read-tree-m-u-wf.sh                  |   8 +-\n t/t1006-cat-file.sh                          |  18 +-\n t/t1007-hash-object.sh                       |   8 +-\n t/t1011-read-tree-sparse-checkout.sh         |  10 +-\n t/t1050-large.sh                             |   6 +-\n t/t1091-sparse-checkout-builtin.sh           |  24 +-\n t/t1092-sparse-checkout-compatibility.sh     |  44 +--\n t/t1300-config.sh                            |  16 +-\n t/t1305-config-include.sh                    |   2 +-\n t/t1308-config-set.sh                        |   6 +-\n t/t1400-update-ref.sh                        | 170 ++++-----\n t/t1403-show-ref.sh                          |  18 +-\n t/t1410-reflog.sh                            |   4 +-\n t/t1415-worktree-refs.sh                     |   4 +-\n t/t1430-bad-ref-name.sh                      |  56 +--\n t/t1450-fsck.sh                              |  12 +-\n t/t1451-fsck-buffer.sh                       |   6 +-\n t/t1460-refs-migrate.sh                      |   2 +-\n t/t1500-rev-parse.sh                         |   6 +-\n t/t1502-rev-parse-parseopt.sh                |   2 +-\n t/t1503-rev-parse-verify.sh                  |  10 +-\n t/t1510-repo-setup.sh                        |  10 +-\n t/t1512-rev-parse-disambiguation.sh          |   4 +-\n t/t1515-rev-parse-outside-repo.sh            |   2 +-\n t/t1800-hook.sh                              |  18 +-\n t/t2004-checkout-cache-temp.sh               |   4 +-\n t/t2019-checkout-ambiguous-ref.sh            |   4 +-\n t/t2024-checkout-dwim.sh                     |   8 +-\n t/t2030-unresolve-info.sh                    |   6 +-\n t/t2060-switch.sh                            |   6 +-\n t/t2070-restore.sh                           |   2 +-\n t/t2080-parallel-checkout-basics.sh          |  14 +-\n t/t2081-parallel-checkout-collisions.sh      |  24 +-\n t/t2082-parallel-checkout-attributes.sh      |  12 +-\n t/t2103-update-index-ignore-missing.sh       |   6 +-\n t/t2200-add-update.sh                        |   2 +-\n t/t2203-add-intent.sh                        |   6 +-\n t/t2400-worktree-add.sh                      |  24 +-\n t/t2402-worktree-list.sh                     |  16 +-\n t/t2403-worktree-move.sh                     |   6 +-\n t/t2405-worktree-submodule.sh                |   6 +-\n t/t2407-worktree-heads.sh                    |  26 +-\n t/t2500-untracked-overwriting.sh             |   8 +-\n t/t2501-cwd-empty.sh                         |   4 +-\n t/t3001-ls-files-others-exclude.sh           |   6 +-\n t/t3007-ls-files-recurse-submodules.sh       |   6 +-\n t/t3200-branch.sh                            |  12 +-\n t/t3202-show-branch.sh                       |  10 +-\n t/t3203-branch-output.sh                     |   4 +-\n t/t3206-range-diff.sh                        |  78 ++---\n t/t3207-branch-submodule.sh                  |   4 +-\n t/t3301-notes.sh                             |  32 +-\n t/t3310-notes-merge-manual-resolve.sh        |  16 +-\n t/t3320-notes-merge-worktrees.sh             |   2 +-\n t/t3400-rebase.sh                            |  16 +-\n t/t3402-rebase-merge.sh                      |  16 +-\n t/t3404-rebase-interactive.sh                |  72 ++--\n t/t3406-rebase-message.sh                    |   6 +-\n t/t3415-rebase-autosquash.sh                 |  10 +-\n t/t3416-rebase-onto-threedots.sh             |   4 +-\n t/t3418-rebase-continue.sh                   |  10 +-\n t/t3420-rebase-autostash.sh                  |  25 +-\n t/t3422-rebase-incompatible-options.sh       |   4 +-\n t/t3429-rebase-edit-todo.sh                  |   2 +-\n t/t3430-rebase-merges.sh                     |  32 +-\n t/t3500-cherry.sh                            |   4 +-\n t/t3501-revert-cherry-pick.sh                |   6 +-\n t/t3504-cherry-pick-rerere.sh                |   6 +-\n t/t3510-cherry-pick-sequence.sh              |  24 +-\n t/t3602-rm-sparse-checkout.sh                |   4 +-\n t/t3705-add-sparse-checkout.sh               |  10 +-\n t/t3800-mktag.sh                             |   4 +-\n t/t3901-i18n-patch.sh                        |  20 +-\n t/t3903-stash.sh                             |  28 +-\n t/t3904-stash-patch.sh                       |   4 +-\n t/t3908-stash-in-worktree.sh                 |   2 +-\n t/t4000-diff-format.sh                       |   2 +-\n t/t4001-diff-rename.sh                       |   4 +-\n t/t4011-diff-symlink.sh                      |   2 +-\n t/t4013-diff-various.sh                      |   2 +-\n t/t4014-format-patch.sh                      | 344 +++++++++----------\n t/t4015-diff-whitespace.sh                   |  16 +-\n t/t4017-diff-retval.sh                       |   2 +-\n t/t4018-diff-funcname.sh                     |   2 +-\n t/t4019-diff-wserror.sh                      |   8 +-\n t/t4020-diff-external.sh                     |  18 +-\n t/t4021-format-patch-numbered.sh             |   4 +-\n t/t4022-diff-rewrite.sh                      |  14 +-\n t/t4028-format-patch-mime-headers.sh         |   6 +-\n t/t4031-diff-rewrite-binary.sh               |  18 +-\n t/t4033-diff-patience.sh                     |   2 +-\n t/t4036-format-patch-signer-mime.sh          |   6 +-\n t/t4038-diff-combined.sh                     |   6 +-\n t/t4051-diff-function-context.sh             |  38 +-\n t/t4053-diff-no-index.sh                     |   4 +-\n t/t4063-diff-blobs.sh                        |   2 +-\n t/t4065-diff-anchored.sh                     |  26 +-\n t/t4067-diff-partial-clone.sh                |  12 +-\n t/t4073-diff-stat-name-width.sh              |  24 +-\n t/t4103-apply-binary.sh                      |   2 +-\n t/t4120-apply-popt.sh                        |   2 +-\n t/t4124-apply-ws-rule.sh                     |  10 +-\n t/t4128-apply-root.sh                        |   2 +-\n t/t4140-apply-ita.sh                         |   4 +-\n t/t4141-apply-too-large.sh                   |   2 +-\n t/t4150-am.sh                                |  48 +--\n t/t4200-rerere.sh                            |   6 +-\n t/t4201-shortlog.sh                          |   2 +-\n t/t4202-log.sh                               |  84 ++---\n t/t4204-patch-id.sh                          |   2 +-\n t/t4205-log-pretty-formats.sh                |   2 +-\n t/t4209-log-pickaxe.sh                       |  10 +-\n t/t4211-line-log.sh                          |  72 ++--\n t/t4216-log-bloom.sh                         |  18 +-\n t/t4252-am-options.sh                        |  22 +-\n t/t4254-am-corrupt.sh                        |   6 +-\n t/t4258-am-quoted-cr.sh                      |   2 +-\n t/t4301-merge-tree-write-tree.sh             |  18 +-\n t/t5000-tar-tree.sh                          |  10 +-\n t/t5004-archive-corner-cases.sh              |   2 +-\n t/t5100-mailinfo.sh                          |   2 +-\n t/t5150-request-pull.sh                      |  18 +-\n t/t5300-pack-object.sh                       |  20 +-\n t/t5302-pack-index.sh                        |   6 +-\n t/t5304-prune.sh                             |   8 +-\n t/t5310-pack-bitmaps.sh                      |  14 +-\n t/t5317-pack-objects-filter-objects.sh       |  12 +-\n t/t5318-commit-graph.sh                      |   8 +-\n t/t5319-multi-pack-index.sh                  |  14 +-\n t/t5324-split-commit-graph.sh                |  10 +-\n t/t5325-reverse-index.sh                     |   2 +-\n t/t5326-multi-pack-bitmaps.sh                |  28 +-\n t/t5328-commit-graph-64bit-time.sh           |   2 +-\n t/t5329-pack-objects-cruft.sh                |   8 +-\n t/t5334-incremental-multi-pack-index.sh      |   2 +-\n t/t5335-compact-multi-pack-index.sh          |   4 +-\n t/t5351-unpack-large-objects.sh              |   2 +-\n t/t5402-post-merge-hook.sh                   |   4 +-\n t/t5403-post-checkout-hook.sh                |   2 +-\n t/t5404-tracking-branches.sh                 |   2 +-\n t/t5406-remote-rejects.sh                    |   2 +-\n t/t5407-post-rewrite-hook.sh                 |   8 +-\n t/t5409-colorize-remote-messages.sh          |  36 +-\n t/t5411/test-0013-bad-protocol.sh            |  14 +-\n t/t5411/test-0014-bad-protocol--porcelain.sh |  14 +-\n t/t5500-fetch-pack.sh                        |  38 +-\n t/t5504-fetch-receive-strict.sh              |  14 +-\n t/t5505-remote.sh                            |  20 +-\n t/t5510-fetch.sh                             |  10 +-\n t/t5512-ls-remote.sh                         |   8 +-\n t/t5514-fetch-multiple.sh                    |   2 +-\n t/t5516-fetch-push.sh                        |  20 +-\n t/t5520-pull.sh                              |   4 +-\n t/t5524-pull-msg.sh                          |   6 +-\n t/t5526-fetch-submodules.sh                  |  16 +-\n t/t5529-push-errors.sh                       |   4 +-\n t/t5530-upload-pack-error.sh                 |  18 +-\n t/t5531-deep-submodule-push.sh               |   2 +-\n t/t5532-fetch-proxy.sh                       |   2 +-\n t/t5533-push-cas.sh                          |  12 +-\n t/t5534-push-signed.sh                       |  22 +-\n t/t5537-fetch-shallow.sh                     |   2 +-\n t/t5538-push-shallow.sh                      |   2 +-\n t/t5539-fetch-http-shallow.sh                |   4 +-\n t/t5541-http-push-smart.sh                   |  32 +-\n t/t5544-pack-objects-hook.sh                 |  12 +-\n t/t5550-http-fetch-dumb.sh                   |   4 +-\n t/t5551-http-fetch-smart.sh                  |  46 +--\n t/t5552-skipping-fetch-negotiator.sh         |   6 +-\n t/t5554-noop-fetch-negotiator.sh             |   4 +-\n t/t5557-http-get.sh                          |   2 +-\n t/t5558-clone-bundle-uri.sh                  |  38 +-\n t/t5562-http-backend-content-length.sh       |   2 +-\n t/t5564-http-proxy.sh                        |  10 +-\n t/t5581-http-curl-verbose.sh                 |   2 +-\n t/t5583-push-branches.sh                     |   8 +-\n t/t5601-clone.sh                             |  28 +-\n t/t5604-clone-reference.sh                   |   8 +-\n t/t5605-clone-local.sh                       |   2 +-\n t/t5606-clone-options.sh                     |   6 +-\n t/t5612-clone-refspec.sh                     |   2 +-\n t/t5616-partial-clone.sh                     |  60 ++--\n t/t5619-clone-local-ambiguous-transport.sh   |   2 +-\n t/t5620-backfill.sh                          |  12 +-\n t/t5700-protocol-v1.sh                       |  46 +--\n t/t5701-git-serve.sh                         |  14 +-\n t/t5702-protocol-v2.sh                       | 152 ++++----\n t/t5703-upload-pack-ref-in-want.sh           |  22 +-\n t/t5705-session-id-in-capabilities.sh        |  12 +-\n t/t5750-bundle-uri-parse.sh                  |   8 +-\n t/t5801-remote-helpers.sh                    |   4 +-\n t/t5810-proto-disable-local.sh               |   2 +-\n t/t5813-proto-disable-ssh.sh                 |   4 +-\n t/t6000-rev-list-misc.sh                     |  26 +-\n t/t6005-rev-list-count.sh                    |   8 +-\n t/t6006-rev-list-format.sh                   |   4 +-\n t/t6009-rev-list-parent.sh                   |   4 +-\n t/t6020-bundle-misc.sh                       |  12 +-\n t/t6022-rev-list-missing.sh                  |   4 +-\n t/t6030-bisect-porcelain.sh                  | 150 ++++----\n t/t6040-tracking-info.sh                     |   2 +-\n t/t6112-rev-list-filters-objects.sh          |  24 +-\n t/t6115-rev-list-du.sh                       |   4 +-\n t/t6120-describe.sh                          |  14 +-\n t/t6200-fmt-merge-msg.sh                     |  82 ++---\n t/t6402-merge-rename.sh                      |   4 +-\n t/t6403-merge-file.sh                        |   6 +-\n t/t6404-recursive-merge.sh                   |   2 +-\n t/t6406-merge-attr.sh                        |  20 +-\n t/t6417-merge-ours-theirs.sh                 |  30 +-\n t/t6418-merge-text-auto.sh                   |   2 +-\n t/t6422-merge-rename-corner-cases.sh         |   8 +-\n t/t6423-merge-rename-directories.sh          |  72 ++--\n t/t6424-merge-unrelated-index-changes.sh     |   6 +-\n t/t6427-diff3-conflict-markers.sh            |  10 +-\n t/t6432-merge-recursive-space-options.sh     |   4 +-\n t/t6436-merge-overwrite.sh                   |   6 +-\n t/t6437-submodule-merge.sh                   |  12 +-\n t/t6500-gc.sh                                |   8 +-\n t/t6600-test-reach.sh                        |   4 +-\n t/t7001-mv.sh                                |  16 +-\n t/t7002-mv-sparse-checkout.sh                |  38 +-\n t/t7003-filter-branch.sh                     |  16 +-\n t/t7004-tag.sh                               |   2 +-\n t/t7006-pager.sh                             |  16 +-\n t/t7012-skip-worktree-writing.sh             |   6 +-\n t/t7030-verify-tag.sh                        |  52 +--\n t/t7031-verify-tag-signed-ssh.sh             |  46 +--\n t/t7102-reset.sh                             |   2 +-\n t/t7110-reset-merge.sh                       |  40 +--\n t/t7201-co.sh                                |   6 +-\n t/t7300-clean.sh                             |   2 +-\n t/t7301-clean-interactive.sh                 |   2 +-\n t/t7400-submodule-basic.sh                   |  32 +-\n t/t7402-submodule-rebase.sh                  |   2 +-\n t/t7406-submodule-update.sh                  |  26 +-\n t/t7416-submodule-dash-url.sh                |  20 +-\n t/t7417-submodule-path-url.sh                |   2 +-\n t/t7450-bad-git-dotfiles.sh                  |  14 +-\n t/t7501-commit-basic-functionality.sh        |  14 +-\n t/t7502-commit-porcelain.sh                  |   2 +-\n t/t7507-commit-verbose.sh                    |   6 +-\n t/t7508-status.sh                            |   6 +-\n t/t7510-signed-commit.sh                     |  68 ++--\n t/t7516-commit-races.sh                      |   4 +-\n t/t7519-status-fsmonitor.sh                  |  14 +-\n t/t7527-builtin-fsmonitor.sh                 |  84 ++---\n t/t7528-signed-commit-ssh.sh                 |  68 ++--\n t/t7600-merge.sh                             |  10 +-\n t/t7603-merge-reduce-heads.sh                |  20 +-\n t/t7606-merge-custom.sh                      |   2 +-\n t/t7607-merge-state.sh                       |   4 +-\n t/t7610-mergetool.sh                         |  18 +-\n t/t7700-repack.sh                            |  14 +-\n t/t7703-repack-geometric.sh                  |   4 +-\n t/t7704-repack-cruft.sh                      |  12 +-\n t/t7800-difftool.sh                          |  26 +-\n t/t7810-grep.sh                              |  22 +-\n t/t7814-grep-recurse-submodules.sh           |   2 +-\n t/t7900-maintenance.sh                       |  34 +-\n t/t8008-blame-formats.sh                     |   2 +-\n t/t8010-cat-file-filters.sh                  |   2 +-\n t/t8012-blame-colors.sh                      |   2 +-\n t/t9001-send-email.sh                        | 190 +++++-----\n t/t9003-help-autocorrect.sh                  |   6 +-\n t/t9106-git-svn-commit-diff-clobber.sh       |   2 +-\n t/t9107-git-svn-migrate.sh                   |  30 +-\n t/t9110-git-svn-use-svm-props.sh             |  20 +-\n t/t9111-git-svn-use-svnsync-props.sh         |  18 +-\n t/t9114-git-svn-dcommit-merge.sh             |   6 +-\n t/t9116-git-svn-log.sh                       |   8 +-\n t/t9117-git-svn-init-clone.sh                |  12 +-\n t/t9119-git-svn-info.sh                      |  16 +-\n t/t9122-git-svn-author.sh                    |   8 +-\n t/t9130-git-svn-authors-file.sh              |   8 +-\n t/t9138-git-svn-authors-prog.sh              |  14 +-\n t/t9140-git-svn-reset.sh                     |   4 +-\n t/t9153-git-svn-rewrite-uuid.sh              |   4 +-\n t/t9200-git-cvsexportcommit.sh               |   2 +-\n t/t9210-scalar.sh                            |  34 +-\n t/t9211-scalar-clone.sh                      |  16 +-\n t/t9300-fast-import.sh                       |  10 +-\n t/t9350-fast-export.sh                       |  54 +--\n t/t9351-fast-export-anonymize.sh             |  36 +-\n t/t9400-git-cvsserver-server.sh              |   4 +-\n t/t9501-gitweb-standalone-http-status.sh     |  58 ++--\n t/t9502-gitweb-standalone-parse-output.sh    |  38 +-\n t/t9800-git-p4-basic.sh                      |  10 +-\n t/t9801-git-p4-branch.sh                     |  48 +--\n t/t9806-git-p4-options.sh                    |  10 +-\n t/t9807-git-p4-submit.sh                     |   2 +-\n t/t9810-git-p4-rcs.sh                        |   8 +-\n t/t9813-git-p4-preserve-users.sh             |   8 +-\n t/t9814-git-p4-rename.sh                     |   8 +-\n t/t9827-git-p4-change-filetype.sh            |   4 +-\n t/t9832-unshelve.sh                          |   6 +-\n t/t9833-errors.sh                            |   4 +-\n t/t9835-git-p4-metadata-encoding-python2.sh  |  36 +-\n t/t9836-git-p4-metadata-encoding-python3.sh  |  38 +-\n t/t9850-shell.sh                             |   2 +-\n t/t9902-completion.sh                        |  26 +-\n 351 files changed, 3215 insertions(+), 2853 deletions(-)\n create mode 100644 t/lint-style/grep-assert.expect\n create mode 100644 t/lint-style/grep-assert.test\n create mode 100644 t/lint-style/grep-fix.expect\n create mode 100644 t/lint-style/grep-fix.test\n create mode 100644 t/lint-style/grep-missing-file.expect\n create mode 100644 t/lint-style/grep-missing-file.test\n create mode 100644 t/lint-style/grep-negated.expect\n create mode 100644 t/lint-style/grep-negated.test\n create mode 100644 t/lint-style/grep-not-assert.expect\n create mode 100644 t/lint-style/grep-not-assert.test\n\ndiff --git a/t/Makefile b/t/Makefile\nindex 3a5fa4ce37..9492195511 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -164,8 +164,10 @@ test-lint-shell-syntax:\n check-shell-parser:\n \t@'$(PERL_PATH_SQ)' check-shell-parser.pl\n \n+TSOURCED = $(sort $(wildcard t[0-9]*/*.sh))\n+\n test-lint-style:\n-\t@'$(PERL_PATH_SQ)' lint-style.pl $(T) $(THELPERS) $(TPERF)\n+\t@'$(PERL_PATH_SQ)' lint-style.pl $(T) $(THELPERS) $(TPERF) $(TSOURCED)\n \n check-lint-style:\n \t@rc=0; for t in $(LINT_STYLE_TESTS); do \\\ndiff --git a/t/for-each-ref-tests.sh b/t/for-each-ref-tests.sh\nindex bd2d45c971..6b359d940e 100644\n--- a/t/for-each-ref-tests.sh\n+++ b/t/for-each-ref-tests.sh\n@@ -523,7 +523,7 @@ test_expect_success 'Verify descending sort' '\n \n test_expect_success 'Give help even with invalid sort atoms' '\n \ttest_expect_code 129 ${git_for_each_ref} --sort=bogus -h >actual 2>&1 &&\n-\tgrep \"^usage: ${git_for_each_ref}\" actual\n+\ttest_grep \"^usage: ${git_for_each_ref}\" actual\n '\n \n cat >expected <<\\EOF\n@@ -622,7 +622,7 @@ test_expect_success 'Quoting style: tcl' '\n for i in \"--perl --shell\" \"-s --python\" \"--python --tcl\" \"--tcl --perl\"; do\n \ttest_expect_success \"more than one quoting style: $i\" \"\n \t\ttest_must_fail ${git_for_each_ref} $i 2>err &&\n-\t\tgrep '^error: more than one quoting style' err\n+\t\ttest_grep '^error: more than one quoting style' err\n \t\"\n done\n \n@@ -1892,7 +1892,7 @@ test_expect_success \"${git_for_each_ref} --stdin: fails if extra args\" '\n \t>in &&\n \ttest_must_fail ${git_for_each_ref} --format=\"%(refname)\" \\\n \t\t--stdin refs/heads/extra <in 2>err &&\n-\tgrep \"unknown arguments supplied with --stdin\" err\n+\ttest_grep \"unknown arguments supplied with --stdin\" err\n '\n \n test_expect_success \"${git_for_each_ref} --stdin: matches\" '\n@@ -1955,11 +1955,11 @@ test_expect_success \"${git_for_each_ref} with nested tags\" '\n \n test_expect_success 'is-base atom with non-commits' '\n \t${git_for_each_ref} --format=\"%(is-base:HEAD) %(refname)\" >out 2>err &&\n-\tgrep \"(HEAD) refs/heads/main\" out &&\n+\ttest_grep \"(HEAD) refs/heads/main\" out &&\n \n \ttest_line_count = 2 err &&\n-\tgrep \"error: object .* is a commit, not a blob\" err &&\n-\tgrep \"error: bad tag pointer to\" err\n+\ttest_grep \"error: object .* is a commit, not a blob\" err &&\n+\ttest_grep \"error: bad tag pointer to\" err\n '\n \n GRADE_FORMAT=\"%(signature:grade)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)\"\ndiff --git a/t/lib-bitmap.sh b/t/lib-bitmap.sh\nindex 62aa6744a6..2de6646128 100644\n--- a/t/lib-bitmap.sh\n+++ b/t/lib-bitmap.sh\n@@ -173,7 +173,7 @@ rev_list_tests_head () {\n \n \ttest_expect_success \"bitmap --objects handles non-commit objects ($state, $branch)\" '\n \t\tgit rev-list --objects --use-bitmap-index $branch tagged-blob >actual &&\n-\t\tgrep $blob actual\n+\t\ttest_grep $blob actual\n \t'\n }\n \n@@ -242,16 +242,16 @@ basic_bitmap_tests () {\n \t\tGIT_PROGRESS_DELAY=0 \\\n \t\t\tgit pack-objects --all --stdout --progress \\\n \t\t\t</dev/null >/dev/null 2>stderr &&\n-\t\tgrep \"Enumerating objects: $count, done\" stderr &&\n-\t\tgrep \"pack-reused $count\" stderr &&\n+\t\ttest_grep \"Enumerating objects: $count, done\" stderr &&\n+\t\ttest_grep \"pack-reused $count\" stderr &&\n \n \t\t# now the same but with one non-reused object\n \t\tgit commit --allow-empty -m \"an extra commit object\" &&\n \t\tGIT_PROGRESS_DELAY=0 \\\n \t\t\tgit pack-objects --all --stdout --progress \\\n \t\t\t</dev/null >/dev/null 2>stderr &&\n-\t\tgrep \"Enumerating objects: $((count+1)), done\" stderr &&\n-\t\tgrep \"pack-reused $count\" stderr\n+\t\ttest_grep \"Enumerating objects: $((count+1)), done\" stderr &&\n+\t\ttest_grep \"pack-reused $count\" stderr\n \t'\n }\n \n@@ -283,7 +283,7 @@ test_rev_exists () {\n \t\tthen\n \t\t\ttest_path_is_file $midx-$(midx_checksum $objdir).rev\n \t\tfi &&\n-\t\tgrep \"\\\"category\\\":\\\"load_midx_revindex\\\",\\\"key\\\":\\\"source\\\",\\\"value\\\":\\\"$kind\\\"\" event.trace\n+\t\ttest_grep \"\\\"category\\\":\\\"load_midx_revindex\\\",\\\"key\\\":\\\"source\\\",\\\"value\\\":\\\"$kind\\\"\" event.trace\n \t'\n }\n \ndiff --git a/t/lib-bundle-uri-protocol.sh b/t/lib-bundle-uri-protocol.sh\nindex de09b6b02e..ecf3284297 100644\n--- a/t/lib-bundle-uri-protocol.sh\n+++ b/t/lib-bundle-uri-protocol.sh\n@@ -63,9 +63,9 @@ test_expect_success \"connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: no\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log &&\n+\ttest_grep \"< version 2\" log &&\n \n-\t! grep bundle-uri log\n+\ttest_grep ! bundle-uri log\n '\n \n test_expect_success \"connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: have bundle-uri\" '\n@@ -78,10 +78,10 @@ test_expect_success \"connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: hav\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log &&\n+\ttest_grep \"< version 2\" log &&\n \n \t# Server advertised bundle-uri capability\n-\tgrep \"< bundle-uri\" log\n+\ttest_grep \"< bundle-uri\" log\n '\n \n test_expect_success \"clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: request bundle-uris\" '\n@@ -95,13 +95,13 @@ test_expect_success \"clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log &&\n+\ttest_grep \"< version 2\" log &&\n \n \t# Server advertised bundle-uri capability\n-\tgrep \"< bundle-uri\" log &&\n+\ttest_grep \"< bundle-uri\" log &&\n \n \t# Client did not issue bundle-uri command\n-\t! grep \"> command=bundle-uri\" log &&\n+\ttest_grep ! \"> command=bundle-uri\" log &&\n \n \tGIT_TRACE_PACKET=\"$PWD/log\" \\\n \tgit \\\n@@ -111,13 +111,13 @@ test_expect_success \"clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log &&\n+\ttest_grep \"< version 2\" log &&\n \n \t# Server advertised bundle-uri capability\n-\tgrep \"< bundle-uri\" log &&\n+\ttest_grep \"< bundle-uri\" log &&\n \n \t# Client issued bundle-uri command\n-\tgrep \"> command=bundle-uri\" log &&\n+\ttest_grep \"> command=bundle-uri\" log &&\n \n \tGIT_TRACE_PACKET=\"$PWD/log3\" \\\n \tgit \\\n@@ -128,13 +128,13 @@ test_expect_success \"clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log3 &&\n+\ttest_grep \"< version 2\" log3 &&\n \n \t# Server advertised bundle-uri capability\n-\tgrep \"< bundle-uri\" log3 &&\n+\ttest_grep \"< bundle-uri\" log3 &&\n \n \t# Client did not issue bundle-uri command (--bundle-uri override)\n-\t! grep \"> command=bundle-uri\" log3\n+\ttest_grep ! \"> command=bundle-uri\" log3\n '\n \n # The remaining tests will all assume transfer.bundleURI=true\ndiff --git a/t/lib-httpd.sh b/t/lib-httpd.sh\nindex fc646447d5..a216e5376f 100644\n--- a/t/lib-httpd.sh\n+++ b/t/lib-httpd.sh\n@@ -275,7 +275,7 @@ test_http_push_nonff () {\n \t'\n \n \ttest_expect_success 'non-fast-forward push show ref status' '\n-\t\tgrep \"^ ! \\[rejected\\][ ]*$BRANCH -> $BRANCH (non-fast-forward)$\" output\n+\t\ttest_grep \"^ ! \\[rejected\\][ ]*$BRANCH -> $BRANCH (non-fast-forward)$\" output\n \t'\n \n \ttest_expect_success 'non-fast-forward push shows help message' '\ndiff --git a/t/lint-style.pl b/t/lint-style.pl\nindex 9268577f9b..8b0b44c146 100755\n--- a/t/lint-style.pl\n+++ b/t/lint-style.pl\n@@ -7,6 +7,11 @@\n # Detection uses parsed tokens from the shared shell parser for\n # correct handling of heredocs, $(...), pipes, and quoting.\n # Fixes modify the original file text to preserve formatting.\n+#\n+# Architecture: the harness (LintParser, parse_commands) tokenizes\n+# test bodies and splits them into commands.  Rules are independent\n+# functions that examine each command and its surrounding token\n+# context to decide if there is a violation.\n \n use strict;\n use warnings;\n@@ -31,10 +36,10 @@ do $_lib or die \"$0: failed to load $_lib: $@$!\\n\";\n # on each test body.  Per-file state (file name, raw lines, dirty\n # flag) is stored on the instance before calling parse().\n #\n-# Subroutines defined below (parse_commands, check_test_grep_negation,\n-# etc.) are in package main and called with the main:: prefix.\n-# File-scoped lexicals ($fix_mode, $has_fixable, etc.) are visible\n-# across packages since 'package' does not introduce a new scope.\n+# Subroutines defined below are in package main and called with\n+# the main:: prefix.  File-scoped lexicals ($fix_mode, etc.) are\n+# visible across packages since 'package' does not introduce a\n+# new scope.\n package LintParser;\n our @ISA = ('ScriptParser');\n \n@@ -43,6 +48,9 @@ package main;\n my $exit_code = 0;\n my $has_fixable = 0;\n \n+my %skip_file = map { $_ => 1 }\n+\tgrep { m{(?:test-lib-functions|lib-rebase)\\.sh$} } @ARGV;\n+\n sub err {\n \tmy ($file, $lineno, $line, $msg, %opts) = @_;\n \t$line =~ s/^\\s+//;\n@@ -53,9 +61,8 @@ sub err {\n \t$exit_code = 1 unless $fix_mode && $opts{fixable};\n }\n \n-# Report a lint violation found by a rule.  In --fix mode, apply\n-# the regex substitution on the raw line and report success.\n-# Otherwise just report.  Returns 1 if the line was modified.\n+# Report a lint violation.  In --fix mode, apply the regex\n+# substitution on the raw line.  Returns 1 if modified.\n sub report_violation {\n \tmy ($file, $cmd, $line_ref, $match, $fix, $from) = @_;\n \tmy $lineno = $cmd->{lineno};\n@@ -77,46 +84,70 @@ sub report_violation {\n \treturn 0;\n }\n \n+# --- Harness: tokenize and split into commands ---\n+#\n # Split a token stream into commands at &&, ||, ;;, and \\n.\n+# Each command is {tokens => [...], lineno => N, token_pos => I}\n+# where token_pos is the index in @all_tokens where the command's\n+# first token appeared (so rules can look backward for context).\n sub parse_commands {\n-\tmy ($content) = @_;\n-\tmy $parser = ShellParser->new(\\$content);\n-\tmy @all_tokens = $parser->parse();\n-\n+\tmy ($all_tokens) = @_;\n \tmy @commands;\n \tmy @current;\n \tmy $lineno = 1;\n+\tmy $first_pos = 0;\n+\n+\tmy %shell_keyword;\n+\t@shell_keyword{qw(if then else elif fi for do done\n+\t\t\t   while until case in esac)} = ();\n \n-\tfor (my $ti = 0; $ti < @all_tokens; $ti++) {\n-\t\tmy $text = $all_tokens[$ti]->[0];\n+\tfor (my $ti = 0; $ti < @$all_tokens; $ti++) {\n+\t\tmy $text = $all_tokens->[$ti]->[0];\n \t\tif ($text =~ /^(?:&&|\\|\\||;;|\\n)$/) {\n+\t\t\t# Command separators: flush current command\n+\t\t\tif (@current) {\n+\t\t\t\tpush @commands, {\n+\t\t\t\t\ttokens    => [@current],\n+\t\t\t\t\tlineno    => $lineno,\n+\t\t\t\t\ttoken_pos => $first_pos,\n+\t\t\t\t};\n+\t\t\t\t@current = ();\n+\t\t\t}\n+\t\t} elsif ($text =~ /^[{}()|]$/ || exists $shell_keyword{$text}) {\n+\t\t\t# Shell structural tokens and keywords:\n+\t\t\t# flush current command (these are boundaries,\n+\t\t\t# not part of the command's arguments)\n \t\t\tif (@current) {\n \t\t\t\tpush @commands, {\n-\t\t\t\t\ttokens => [@current],\n-\t\t\t\t\tlineno => $lineno,\n+\t\t\t\t\ttokens    => [@current],\n+\t\t\t\t\tlineno    => $lineno,\n+\t\t\t\t\ttoken_pos => $first_pos,\n \t\t\t\t};\n \t\t\t\t@current = ();\n \t\t\t}\n \t\t} else {\n-\t\t\t$lineno = $all_tokens[$ti]->[3]\n-\t\t\t\tif !@current && defined $all_tokens[$ti]->[3];\n+\t\t\tif (!@current) {\n+\t\t\t\t# Record line number of the first token\n+\t\t\t\t$lineno = $all_tokens->[$ti]->[3]\n+\t\t\t\t\tif defined $all_tokens->[$ti]->[3];\n+\t\t\t\t$first_pos = $ti;\n+\t\t\t}\n \t\t\tpush @current, $text;\n \t\t}\n \t}\n \tif (@current) {\n \t\tpush @commands, {\n-\t\t\ttokens => [@current],\n-\t\t\tlineno => $lineno,\n+\t\t\ttokens    => [@current],\n+\t\t\tlineno    => $lineno,\n+\t\t\ttoken_pos => $first_pos,\n \t\t};\n \t}\n \treturn @commands;\n }\n \n # --- Rule: '! test_grep' should be 'test_grep !' ---\n-# Shell-level negation suppresses test_grep's diagnostic output\n-# on failure.  Built-in negation preserves it.\n sub check_test_grep_negation {\n-\tmy ($cmd, $file, $line_ref) = @_;\n+\tmy ($cmd, $file, $line_ref, $all_tokens) = @_;\n \tmy @tokens = @{$cmd->{tokens}};\n \treturn unless @tokens >= 2 && $tokens[0] eq '!' && $tokens[1] eq 'test_grep';\n \n@@ -124,15 +155,197 @@ sub check_test_grep_negation {\n \t\tqr/!\\s*test_grep/, 'test_grep !', '! test_grep');\n }\n \n-# Map parsed commands back to raw file lines for --fix.\n-# Detection uses parsed tokens (correct handling of quoting,\n-# heredocs, pipes) but fixes must modify the original text\n-# to preserve formatting.\n+# --- Rule: bare 'grep' should be 'test_grep' ---\n+\n+# Check if this command is in a filter context by looking at\n+# the surrounding tokens in the stream.  This is grep-rule\n+# specific: it knows what contexts make a grep not an assertion.\n+sub is_filter_context {\n+\tmy ($all_tokens, $cmd) = @_;\n+\tmy $pos = $cmd->{token_pos};\n+\n+\t# Scan backward to the previous command separator.\n+\t# If we find '|', this command is part of a pipeline.\n+\t# If we find if/elif/while/until, it's a condition.\n+\tfor (my $j = $pos - 1; $j >= 0; $j--) {\n+\t\tmy $t = $all_tokens->[$j]->[0];\n+\t\t# Stop at command separators (but not \\n after |)\n+\t\tlast if $t =~ /^(?:&&|\\|\\||;;)$/;\n+\t\tif ($t eq \"\\n\") {\n+\t\t\t# \\n after | is a line continuation, keep scanning\n+\t\t\tnext if $j > 0 && $all_tokens->[$j - 1]->[0] eq '|';\n+\t\t\tlast;\n+\t\t}\n+\t\treturn 1 if $t eq '|';\n+\t\treturn 1 if $t =~ /^(?:if|elif|while|until)$/;\n+\t\t# for ... in ITEMS ... do: if we're between 'in' and 'do',\n+\t\t# we're in a value list, not a command\n+\t\treturn 1 if $t eq 'in';\n+\t}\n+\n+\t# Forward: pipe after command\n+\tfor (my $j = $pos + @{$cmd->{tokens}}; $j < @$all_tokens; $j++) {\n+\t\tmy $t = $all_tokens->[$j]->[0];\n+\t\tlast if $t =~ /^(?:&&|\\|\\||;;|\\n)$/;\n+\t\treturn 1 if $t eq '|';\n+\t}\n+\n+\t# { cmd; } >output\n+\treturn 1 if is_in_redirected_brace($all_tokens, $pos);\n+\n+\treturn 0;\n+}\n+\n+# Check if position $pos is inside a brace group whose output is\n+# redirected: { grep ...; } >file.  Scan backward for the enclosing\n+# '{', then forward for the matching '}', and check what follows it.\n+sub is_in_redirected_brace {\n+\tmy ($all_tokens, $pos) = @_;\n+\tmy $brace_depth = 0;\n+\tfor (my $j = $pos - 1; $j >= 0; $j--) {\n+\t\tmy $t = $all_tokens->[$j]->[0];\n+\t\t$brace_depth++ if $t eq '}';\n+\t\tif ($t eq '{') {\n+\t\t\treturn 0 if $brace_depth > 0;\n+\t\t\t$brace_depth--;\n+\t\t\t# Found our enclosing '{'. Find matching '}'\n+\t\t\tmy $depth = 1;\n+\t\t\tfor (my $k = $j + 1; $k < @$all_tokens; $k++) {\n+\t\t\t\t$depth++ if $all_tokens->[$k]->[0] eq '{';\n+\t\t\t\t$depth-- if $all_tokens->[$k]->[0] eq '}';\n+\t\t\t\tif ($depth == 0) {\n+\t\t\t\t\tmy $after = $k + 1 < @$all_tokens ?\n+\t\t\t\t\t\t$all_tokens->[$k + 1]->[0] : '';\n+\t\t\t\t\treturn $after =~ /^>{1,2}/;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\treturn 0;\n+}\n+\n+# Classify a grep command: assertion, filter, or bug.\n+#\n+# Returns:\n+#   1  assertion (PATTERN + FILE), can be converted to test_grep\n+#   0  not a grep, or grep used as a filter\n+#  -1  likely bug (e.g., missing file argument)\n+sub is_grep_assertion {\n+\tmy ($cmd, $all_tokens) = @_;\n+\tmy @tokens = @{$cmd->{tokens}};\n+\n+\t# Find grep, possibly after \"!\"\n+\tmy $i = 0;\n+\t$i++ if $tokens[0] eq '!';\n+\treturn 0 unless defined $tokens[$i] && $tokens[$i] eq 'grep';\n+\treturn 0 if grep { $_ eq 'test_grep' } @tokens;\n+\n+\t# Check surrounding context (pipes, control flow, brace redirects)\n+\treturn 0 if is_filter_context($all_tokens, $cmd);\n+\n+\t$i++;  # skip 'grep'\n+\n+\t# Check grep's own flags and arguments\n+\tmy @positional;\n+\tmy $has_pattern_flag = 0;\n+\tmy $end_of_flags = 0;\n+\twhile ($i < @tokens) {\n+\t\tmy $tok = $tokens[$i];\n+\t\tif ($tok eq '|' || $tok eq '<') {\n+\t\t\treturn 0;\n+\t\t}\n+\t\tif ($tok =~ /^>{1,2}$/) {\n+\t\t\t# Stdout redirect means filter (grep ... >out).\n+\t\t\t# Stderr redirect (2>err) is fine: skip the fd\n+\t\t\t# and the target, and keep classifying.\n+\t\t\tmy $prev = $i > 0 ? $tokens[$i - 1] : '';\n+\t\t\treturn 0 unless $prev =~ /^\\d+$/ && $prev >= 2;\n+\t\t\tpop @positional if @positional && $positional[-1] eq $prev;\n+\t\t\t$i += 2;\n+\t\t\tnext;\n+\t\t}\n+\t\tif (!$end_of_flags && $tok =~ /^-\\w*[clLrR]/) {\n+\t\t\treturn 0;\n+\t\t}\n+\t\tif (!$end_of_flags && $tok eq '--') {\n+\t\t\t$end_of_flags = 1;\n+\t\t} elsif (!$end_of_flags && $tok =~ /^-\\w*[ef]$/) {\n+\t\t\t$has_pattern_flag = 1;\n+\t\t\t$i++;\n+\t\t} elsif (!$end_of_flags && $tok =~ /^-/) {\n+\t\t\t# skip other flags\n+\t\t} else {\n+\t\t\tpush @positional, $tok;\n+\t\t}\n+\t\t$i++;\n+\t}\n+\n+\tmy $need = $has_pattern_flag ? 1 : 2;\n+\treturn 0 if !@positional && !$has_pattern_flag;\n+\treturn -1 if @positional < $need;\n+\treturn 0 if $positional[-1] =~ /^-/;\n+\treturn 1;\n+}\n+\n+sub check_bare_grep {\n+\tmy ($cmd, $file, $line_ref, $all_tokens) = @_;\n+\tmy @tokens = @{$cmd->{tokens}};\n+\n+\tmy $result = is_grep_assertion($cmd, $all_tokens);\n+\treturn unless $result;\n+\n+\tif ($result == -1) {\n+\t\terr $file, $cmd->{lineno}, join(' ', @tokens),\n+\t\t\t\"grep assertion appears to be missing a file argument\";\n+\t\treturn 0;\n+\t}\n+\n+\t# Determine negation and -q flag\n+\tmy $negated = $tokens[0] eq '!';\n+\tmy $has_q = 0;\n+\tmy ($pre_q, $post_q) = ('', '');\n+\tfor my $tok (@tokens) {\n+\t\tif ($tok =~ /^-(\\w*)q(\\w*)$/) {\n+\t\t\t$has_q = 1;\n+\t\t\t($pre_q, $post_q) = ($1, $2);\n+\t\t\tlast;\n+\t\t}\n+\t\tlast if $tok !~ /^-/ && $tok ne '!' && $tok ne 'grep';\n+\t}\n+\n+\t# Build the replacement\n+\tmy $fix = \"test_grep\";\n+\t$fix .= \" !\" if $negated;\n+\tif ($has_q) {\n+\t\tmy $rest = \"$pre_q$post_q\";\n+\t\t$fix .= \" -$rest\" if $rest;\n+\t}\n+\n+\t# Build the match pattern\n+\tmy $neg_match = $negated ? '!\\s*' : '\\b';\n+\tmy $neg_from  = $negated ? '! '   : '';\n+\tmy ($match, $from);\n+\tif ($has_q) {\n+\t\t$match = qr/${neg_match}grep\\s+-\\w*q\\w*/;\n+\t\t$from  = \"${neg_from}grep -${pre_q}q${post_q}\";\n+\t} else {\n+\t\t$match = qr/${neg_match}grep\\b/;\n+\t\t$from  = \"${neg_from}grep\";\n+\t}\n+\n+\treturn report_violation($file, $cmd, $line_ref,\n+\t\t$match, $fix, $from);\n+}\n+\n+# --- Harness: LintParser.check_test ---\n+#\n+# Called by ScriptParser::parse_cmd for each test_expect_success\n+# or test_expect_failure block.  Extracts the body, tokenizes it,\n+# splits into commands, and runs each rule.\n package LintParser;\n \n sub check_test {\n-\t# Called by ScriptParser::parse_cmd for each test_expect_success\n-\t# or test_expect_failure block.\n \tmy $self = shift @_;\n \tmy $title = ScriptParser::unwrap(shift @_);\n \n@@ -157,25 +370,37 @@ sub check_test {\n \t}\n \treturn unless $body;\n \n+\t# Tokenize the body once; commands and rules share the stream\n+\tmy $parser = ShellParser->new(\\$body);\n+\tmy @all_tokens = $parser->parse();\n+\tmy @commands = main::parse_commands(\\@all_tokens);\n+\n \t# Map each command back to its file line number.\n \t# $lineno_base is where the body starts in the file;\n \t# $cmd->{lineno} is relative to the body (starting at 1).\n \tmy $raw_lines = $self->{raw_lines};\n-\tfor my $cmd (main::parse_commands($body)) {\n+\tfor my $cmd (@commands) {\n \t\tmy $ln = ($cmd->{lineno} || 0) + $lineno_base - 1;\n \t\t$cmd->{lineno} = $ln;\n \t\tnext unless $ln >= 1 && $ln <= @$raw_lines;\n \t\tnext if $raw_lines->[$ln - 1] =~ /#.*lint-ok/;\n \n-\t\tif (main::check_test_grep_negation($cmd, $self->{file}, \\$raw_lines->[$ln - 1])) {\n-\t\t\t$self->{dirty} = 1;\n-\t\t}\n+\t\tmy $line_ref = \\$raw_lines->[$ln - 1];\n+\t\t# Stop after the first fix: later rules should not\n+\t\t# re-match against already-modified text.\n+\t\tmy $modified = 0;\n+\t\t$modified ||= main::check_test_grep_negation(\n+\t\t\t$cmd, $self->{file}, $line_ref, \\@all_tokens);\n+\t\t$modified ||= main::check_bare_grep(\n+\t\t\t$cmd, $self->{file}, $line_ref, \\@all_tokens);\n+\t\t$self->{dirty} = 1 if $modified;\n \t}\n }\n \n package main;\n \n for my $file (@ARGV) {\n+\tnext if $skip_file{$file};\n \t# :unix:crlf strips \\r on Windows (same as chainlint.pl)\n \topen(my $fh, '<:unix:crlf', $file) or die \"$0: $file: $!\\n\";\n \tmy @raw_lines = <$fh>;\ndiff --git a/t/lint-style/grep-assert.expect b/t/lint-style/grep-assert.expect\nnew file mode 100644\nindex 0000000000..93c518ed43\n--- /dev/null\n+++ b/t/lint-style/grep-assert.expect\n@@ -0,0 +1,13 @@\n+lint-style/grep-assert.test:2: error: replace 'grep' with 'test_grep': grep \"pattern\" actual\n+lint-style/grep-assert.test:3: error: replace 'grep' with 'test_grep': grep -E \"extended\" actual\n+lint-style/grep-assert.test:4: error: replace 'grep' with 'test_grep': grep -e \"explicit\" actual\n+lint-style/grep-assert.test:5: error: replace 'grep' with 'test_grep': grep -f patterns.txt actual\n+lint-style/grep-assert.test:6: error: replace 'grep' with 'test_grep': grep -Fe \"fixed-explicit\" actual\n+lint-style/grep-assert.test:7: error: replace 'grep' with 'test_grep': grep \"^-looks-like-flag\" actual\n+lint-style/grep-assert.test:8: error: replace 'grep' with 'test_grep': grep -v \"inverted\" actual\n+lint-style/grep-assert.test:9: error: replace 'grep' with 'test_grep': grep -- \"-e\" actual\n+lint-style/grep-assert.test:10: error: replace 'grep' with 'test_grep': grep \"with-stderr-redirect\" actual 2 > err\n+lint-style/grep-assert.test:12: error: replace 'grep' with 'test_grep': grep \"after-or\" actual\n+lint-style/grep-assert.test:17: error: replace 'grep' with 'test_grep': grep \"after-pipe\" actual\n+lint-style/grep-assert.test:22: error: replace 'grep' with 'test_grep': grep \"inside-case\" actual\n+hint: run with --fix to apply the suggested replacements.\ndiff --git a/t/lint-style/grep-assert.test b/t/lint-style/grep-assert.test\nnew file mode 100644\nindex 0000000000..1567285754\n--- /dev/null\n+++ b/t/lint-style/grep-assert.test\n@@ -0,0 +1,24 @@\n+test_expect_success 'grep assertions' '\n+\tgrep \"pattern\" actual &&\n+\tgrep -E \"extended\" actual &&\n+\tgrep -e \"explicit\" actual &&\n+\tgrep -f patterns.txt actual &&\n+\tgrep -Fe \"fixed-explicit\" actual &&\n+\tgrep \"^-looks-like-flag\" actual &&\n+\tgrep -v \"inverted\" actual &&\n+\tgrep -- \"-e\" actual &&\n+\tgrep \"with-stderr-redirect\" actual 2>err &&\n+\tcmd ||\n+\tgrep \"after-or\" actual\n+'\n+\n+test_expect_success 'pipe only suppresses first command' '\n+\tcmd |\n+\tgrep \"piped\" && grep \"after-pipe\" actual\n+'\n+\n+test_expect_success 'case pattern does not hide assertion' '\n+\tcase foo in\n+\t*) grep \"inside-case\" actual ;;\n+\tesac\n+'\ndiff --git a/t/lint-style/grep-fix.expect b/t/lint-style/grep-fix.expect\nnew file mode 100644\nindex 0000000000..04849634c6\n--- /dev/null\n+++ b/t/lint-style/grep-fix.expect\n@@ -0,0 +1,16 @@\n+test_expect_success 'all fixable forms' '\n+\ttest_grep \"pattern\" actual &&\n+\ttest_grep -E \"extended\" actual &&\n+\ttest_grep \"quiet\" actual &&\n+\ttest_grep -F \"combined\" actual &&\n+\ttest_grep -i \"quiet-insensitive\" actual &&\n+\ttest_grep ! \"negated\" actual &&\n+\ttest_grep ! \"neg-quiet\" actual &&\n+\ttest_grep ! \"shell-neg\" actual &&\n+\ttest_grep -e \"explicit\" actual &&\n+\ttest_grep -Fe \"fixed-explicit\" actual &&\n+\ttest_grep -f patterns.txt actual &&\n+\ttest_grep -- \"-e\" actual &&\n+\ttest_grep \"continuation\" \\\n+\t\tactual\n+'\ndiff --git a/t/lint-style/grep-fix.test b/t/lint-style/grep-fix.test\nnew file mode 100644\nindex 0000000000..a9202fc177\n--- /dev/null\n+++ b/t/lint-style/grep-fix.test\n@@ -0,0 +1,16 @@\n+test_expect_success 'all fixable forms' '\n+\tgrep \"pattern\" actual &&\n+\tgrep -E \"extended\" actual &&\n+\tgrep -q \"quiet\" actual &&\n+\tgrep -qF \"combined\" actual &&\n+\tgrep -qi \"quiet-insensitive\" actual &&\n+\t! grep \"negated\" actual &&\n+\t! grep -q \"neg-quiet\" actual &&\n+\t! test_grep \"shell-neg\" actual &&\n+\tgrep -e \"explicit\" actual &&\n+\tgrep -Fe \"fixed-explicit\" actual &&\n+\tgrep -f patterns.txt actual &&\n+\tgrep -- \"-e\" actual &&\n+\tgrep \"continuation\" \\\n+\t\tactual\n+'\ndiff --git a/t/lint-style/grep-missing-file.expect b/t/lint-style/grep-missing-file.expect\nnew file mode 100644\nindex 0000000000..8d4dc585b5\n--- /dev/null\n+++ b/t/lint-style/grep-missing-file.expect\n@@ -0,0 +1,4 @@\n+lint-style/grep-missing-file.test:2: error: grep assertion appears to be missing a file argument: grep \"pattern\"\n+lint-style/grep-missing-file.test:3: error: grep assertion appears to be missing a file argument: ! grep \"negated\"\n+lint-style/grep-missing-file.test:4: error: grep assertion appears to be missing a file argument: grep -e \"pattern-flag-no-file\"\n+lint-style/grep-missing-file.test:5: error: grep assertion appears to be missing a file argument: grep -f patterns.txt\ndiff --git a/t/lint-style/grep-missing-file.test b/t/lint-style/grep-missing-file.test\nnew file mode 100644\nindex 0000000000..aac0528dfc\n--- /dev/null\n+++ b/t/lint-style/grep-missing-file.test\n@@ -0,0 +1,6 @@\n+test_expect_success 'grep missing file argument' '\n+\tgrep \"pattern\" &&\n+\t! grep \"negated\" &&\n+\tgrep -e \"pattern-flag-no-file\" &&\n+\tgrep -f patterns.txt\n+'\ndiff --git a/t/lint-style/grep-negated.expect b/t/lint-style/grep-negated.expect\nnew file mode 100644\nindex 0000000000..7029b5544a\n--- /dev/null\n+++ b/t/lint-style/grep-negated.expect\n@@ -0,0 +1,5 @@\n+lint-style/grep-negated.test:2: error: replace '! grep' with 'test_grep !': ! grep \"pattern\" actual\n+lint-style/grep-negated.test:3: error: replace '! grep' with 'test_grep !': ! grep -i \"insensitive\" actual\n+lint-style/grep-negated.test:7: error: replace '! grep -q' with 'test_grep !': ! grep -q \"pattern\" actual\n+lint-style/grep-negated.test:8: error: replace '! grep -qF' with 'test_grep ! -F': ! grep -qF \"combined\" actual\n+hint: run with --fix to apply the suggested replacements.\ndiff --git a/t/lint-style/grep-negated.test b/t/lint-style/grep-negated.test\nnew file mode 100644\nindex 0000000000..862a93b93b\n--- /dev/null\n+++ b/t/lint-style/grep-negated.test\n@@ -0,0 +1,9 @@\n+test_expect_success 'negated grep' '\n+\t! grep \"pattern\" actual &&\n+\t! grep -i \"insensitive\" actual\n+'\n+\n+test_expect_success 'negated grep -q' '\n+\t! grep -q \"pattern\" actual &&\n+\t! grep -qF \"combined\" actual\n+'\ndiff --git a/t/lint-style/grep-not-assert.expect b/t/lint-style/grep-not-assert.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/lint-style/grep-not-assert.test b/t/lint-style/grep-not-assert.test\nnew file mode 100644\nindex 0000000000..a2c38ae4b1\n--- /dev/null\n+++ b/t/lint-style/grep-not-assert.test\n@@ -0,0 +1,43 @@\n+test_expect_success 'grep used as filter (not assertion)' '\n+\tgrep \"pattern\" file | wc -l &&\n+\tgrep \"pattern\" file >output &&\n+\tgrep \"pattern\" file 1>output &&\n+\tgrep -c \"count\" file &&\n+\tgrep -ci \"count-insensitive\" file &&\n+\tgrep -l \"list\" file &&\n+\tgrep -rl \"recursive-list\" dir &&\n+\tgrep -L \"list-without\" file1 file2 &&\n+\tresult=$(grep \"pattern\" file) &&\n+\tresult=$(echo $(grep \"nested-subshell\" file)) &&\n+\tgrep \"pattern\" <stdin &&\n+\tgrep \"pattern\" file && # lint-ok\n+\tcmd | grep \"pattern-only\" &&\n+\tcmd |\n+\tgrep \"cross-line-pipe\" &&\n+\tgrep -r \"recursive\" dir\n+'\n+\n+test_expect_success 'grep in control flow (not assertion)' '\n+\tif grep \"condition\" file\n+\tthen\n+\t\techo yes\n+\telif grep \"other-condition\" file\n+\tthen\n+\t\techo no\n+\tfi\n+'\n+\n+test_expect_success 'grep in brace group with redirect' '\n+\t{ grep \"captured\" out; } >result\n+'\n+\n+test_expect_success 'grep in for-in value list' '\n+\tfor cmd in grep sed awk; do\n+\t\techo \"$cmd\"\n+\tdone\n+'\n+\n+test_expect_success 'grep in subshell' '\n+\t(cd sub && grep \"pattern\" file >output) &&\n+\t(cmd | grep \"piped-in-subshell\")\n+'\ndiff --git a/t/pack-refs-tests.sh b/t/pack-refs-tests.sh\nindex d76b087b09..a800177446 100644\n--- a/t/pack-refs-tests.sh\n+++ b/t/pack-refs-tests.sh\n@@ -195,7 +195,7 @@ test_expect_success 'delete ref while another dangling packed ref' '\n test_expect_success 'pack ref directly below refs/' '\n \tgit update-ref refs/top HEAD &&\n \tgit ${pack_refs} --all --prune &&\n-\tgrep refs/top .git/packed-refs &&\n+\ttest_grep refs/top .git/packed-refs &&\n \ttest_path_is_missing .git/refs/top\n '\n \ndiff --git a/t/show-ref-exists-tests.sh b/t/show-ref-exists-tests.sh\nindex 36e8e9df33..1351f690eb 100644\n--- a/t/show-ref-exists-tests.sh\n+++ b/t/show-ref-exists-tests.sh\n@@ -19,7 +19,7 @@ test_expect_success '--exists with missing reference' '\n \n test_expect_success '--exists does not use DWIM' '\n \ttest_expect_code 2 ${git_show_ref_exists} $GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME 2>err &&\n-\tgrep \"reference does not exist\" err\n+\ttest_grep \"reference does not exist\" err\n '\n \n test_expect_success '--exists with HEAD' '\ndiff --git a/t/t0000-basic.sh b/t/t0000-basic.sh\nindex 2b63e1c86c..7bd1f4e7b7 100755\n--- a/t/t0000-basic.sh\n+++ b/t/t0000-basic.sh\n@@ -743,7 +743,7 @@ test_expect_success 'subtest: lazy prereqs do not turn off tracing' '\n \ttest_done\n \tEOF\n \n-\tgrep \"echo trace\" lazy-prereq-and-tracing/err\n+\ttest_grep \"echo trace\" lazy-prereq-and-tracing/err\n '\n \n test_expect_success 'subtest: tests clean up after themselves' '\n@@ -815,7 +815,7 @@ test_expect_success 'subtest: test_atexit is run' '\n \n test_expect_success 'test_oid provides sane info by default' '\n \ttest_oid zero >actual &&\n-\tgrep \"^00*\\$\" actual &&\n+\ttest_grep \"^00*\\$\" actual &&\n \trawsz=\"$(test_oid rawsz)\" &&\n \thexsz=\"$(test_oid hexsz)\" &&\n \t# +1 accounts for the trailing newline\n@@ -827,7 +827,7 @@ test_expect_success 'test_oid can look up data for SHA-1' '\n \ttest_when_finished \"test_detect_hash\" &&\n \ttest_set_hash sha1 &&\n \ttest_oid zero >actual &&\n-\tgrep \"^00*\\$\" actual &&\n+\ttest_grep \"^00*\\$\" actual &&\n \trawsz=\"$(test_oid rawsz)\" &&\n \thexsz=\"$(test_oid hexsz)\" &&\n \ttest $(wc -c <actual) -eq 41 &&\n@@ -839,7 +839,7 @@ test_expect_success 'test_oid can look up data for SHA-256' '\n \ttest_when_finished \"test_detect_hash\" &&\n \ttest_set_hash sha256 &&\n \ttest_oid zero >actual &&\n-\tgrep \"^00*\\$\" actual &&\n+\ttest_grep \"^00*\\$\" actual &&\n \trawsz=\"$(test_oid rawsz)\" &&\n \thexsz=\"$(test_oid hexsz)\" &&\n \ttest $(wc -c <actual) -eq 65 &&\n@@ -884,11 +884,11 @@ test_expect_success 'test_bool_env' '\n \t\t# test script, hence the redirection of fd 7, and aborts\n \t\t# with \"exit 1\", hence the subshell.\n \t\t! ( test_bool_env envvar true ) 7>err &&\n-\t\tgrep \"error: test_bool_env requires bool values\" err &&\n+\t\ttest_grep \"error: test_bool_env requires bool values\" err &&\n \n \t\tenvvar=true &&\n \t\t! ( test_bool_env envvar invalid ) 7>err &&\n-\t\tgrep \"error: test_bool_env requires bool values\" err\n+\t\ttest_grep \"error: test_bool_env requires bool values\" err\n \t)\n '\n \n@@ -1242,12 +1242,12 @@ test_expect_success 'test_must_fail on a failing git command with env' '\n \n test_expect_success 'test_must_fail rejects a non-git command' '\n \t! test_must_fail grep ^$ notafile 2>err &&\n-\tgrep -F \"test_must_fail: only '\"'\"'git'\"'\"' is allowed\" err\n+\ttest_grep -F \"test_must_fail: only '\"'\"'git'\"'\"' is allowed\" err\n '\n \n test_expect_success 'test_must_fail rejects a non-git command with env' '\n \t! test_must_fail env var1=a var2=b grep ^$ notafile 2>err &&\n-\tgrep -F \"test_must_fail: only '\"'\"'git'\"'\"' is allowed\" err\n+\ttest_grep -F \"test_must_fail: only '\"'\"'git'\"'\"' is allowed\" err\n '\n \n test_done\ndiff --git a/t/t0001-init.sh b/t/t0001-init.sh\nindex e4d32bb4d2..7dcd1e9d48 100755\n--- a/t/t0001-init.sh\n+++ b/t/t0001-init.sh\n@@ -278,9 +278,9 @@ test_expect_success POSIXPERM 'init creates a new deep directory (umask vs. shar\n \t\tgit init --bare --shared=0660 newdir/a/b/c &&\n \t\ttest_path_is_dir newdir/a/b/c/refs &&\n \t\tls -ld newdir/a newdir/a/b > lsab.out &&\n-\t\t! grep -v \"^drwxrw[sx]r-x\" lsab.out &&\n+\t\ttest_grep ! -v \"^drwxrw[sx]r-x\" lsab.out &&\n \t\tls -ld newdir/a/b/c > lsc.out &&\n-\t\t! grep -v \"^drwxrw[sx]---\" lsc.out\n+\t\ttest_grep ! -v \"^drwxrw[sx]---\" lsc.out\n \t)\n '\n \n@@ -619,7 +619,7 @@ test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage is not allowed wi\n \tgit init refstorage &&\n \tgit -C refstorage config extensions.refStorage files &&\n \ttest_must_fail git -C refstorage rev-parse 2>err &&\n-\tgrep \"repo version is 0, but v1-only extension found\" err\n+\ttest_grep \"repo version is 0, but v1-only extension found\" err\n '\n \n test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage with files backend' '\n@@ -637,7 +637,7 @@ test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage with unknown back\n \tgit -C refstorage config core.repositoryformatversion 1 &&\n \tgit -C refstorage config extensions.refStorage garbage &&\n \ttest_must_fail git -C refstorage rev-parse 2>err &&\n-\tgrep \"invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}garbage${SQ}\" err\n+\ttest_grep \"invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}garbage${SQ}\" err\n '\n \n test_expect_success 'init with GIT_DEFAULT_REF_FORMAT=garbage' '\n@@ -848,8 +848,8 @@ test_expect_success MINGW 'redirect std handles' '\n \t\tGIT_REDIRECT_STDOUT=output.txt \\\n \t\tGIT_REDIRECT_STDERR=\"2>&1\" \\\n \t\tgit rev-parse --git-dir --verify refs/invalid &&\n-\tgrep \"^\\\\.git\\$\" output.txt &&\n-\tgrep \"Needed a single revision\" output.txt\n+\ttest_grep \"^\\\\.git\\$\" output.txt &&\n+\ttest_grep \"Needed a single revision\" output.txt\n '\n \n test_expect_success '--initial-branch' '\n@@ -862,14 +862,14 @@ test_expect_success '--initial-branch' '\n \tgit init --initial-branch=ignore initial-branch-option 2>err &&\n \ttest_grep \"ignored --initial-branch\" err &&\n \tgit -C initial-branch-option symbolic-ref HEAD >actual &&\n-\tgrep hello actual\n+\ttest_grep hello actual\n '\n \n test_expect_success 'overridden default initial branch name (config)' '\n \ttest_config_global init.defaultBranch nmb &&\n \tGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME= git init initial-branch-config &&\n \tgit -C initial-branch-config symbolic-ref HEAD >actual &&\n-\tgrep nmb actual\n+\ttest_grep nmb actual\n '\n \n test_expect_success 'advice on unconfigured init.defaultBranch' '\n@@ -907,7 +907,7 @@ test_expect_success 'overridden default main branch name (env)' '\n \ttest_config_global init.defaultBranch nmb &&\n \tGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=env git init main-branch-env &&\n \tgit -C main-branch-env symbolic-ref HEAD >actual &&\n-\tgrep env actual\n+\ttest_grep env actual\n '\n \n test_expect_success 'invalid default branch name' '\ndiff --git a/t/t0008-ignores.sh b/t/t0008-ignores.sh\nindex d77a179bdd..ed95faf327 100755\n--- a/t/t0008-ignores.sh\n+++ b/t/t0008-ignores.sh\n@@ -790,8 +790,8 @@ test_expect_success 'existing file and directory' '\n \t>one &&\n \tmkdir top-level-dir &&\n \tgit check-ignore one top-level-dir >actual &&\n-\tgrep one actual &&\n-\tgrep top-level-dir actual\n+\ttest_grep one actual &&\n+\ttest_grep top-level-dir actual\n '\n \n test_expect_success 'existing directory and file' '\n@@ -800,8 +800,8 @@ test_expect_success 'existing directory and file' '\n \t>one &&\n \tmkdir top-level-dir &&\n \tgit check-ignore top-level-dir one >actual &&\n-\tgrep one actual &&\n-\tgrep top-level-dir actual\n+\ttest_grep one actual &&\n+\ttest_grep top-level-dir actual\n '\n \n test_expect_success 'exact prefix matching (with root)' '\ndiff --git a/t/t0009-git-dir-validation.sh b/t/t0009-git-dir-validation.sh\nindex 33d21ed9ea..4cba478e50 100755\n--- a/t/t0009-git-dir-validation.sh\n+++ b/t/t0009-git-dir-validation.sh\n@@ -35,7 +35,7 @@ test_expect_success PIPE 'setup: .git as a FIFO (named pipe) is rejected' '\n \t\tcd parent/fifo-trap &&\n \t\tmkfifo .git &&\n \t\ttest_must_fail git rev-parse --git-dir 2>stderr &&\n-\t\tgrep \"not a regular file\" stderr\n+\t\ttest_grep \"not a regular file\" stderr\n \t)\n '\n \n@@ -47,7 +47,7 @@ test_expect_success SYMLINKS,PIPE 'setup: .git as a symlink to a FIFO is rejecte\n \t\tmkfifo target-fifo &&\n \t\tln -s target-fifo .git &&\n \t\ttest_must_fail git rev-parse --git-dir 2>stderr &&\n-\t\tgrep \"not a regular file\" stderr\n+\t\ttest_grep \"not a regular file\" stderr\n \t)\n '\n \n@@ -58,7 +58,7 @@ test_expect_success 'setup: .git with garbage content is rejected' '\n \t\tcd parent/garbage-trap &&\n \t\techo \"garbage\" >.git &&\n \t\ttest_must_fail git rev-parse --git-dir 2>stderr &&\n-\t\tgrep \"invalid gitfile format\" stderr\n+\t\ttest_grep \"invalid gitfile format\" stderr\n \t)\n '\n \ndiff --git a/t/t0012-help.sh b/t/t0012-help.sh\nindex c33501bdcd..48d128ad04 100755\n--- a/t/t0012-help.sh\n+++ b/t/t0012-help.sh\n@@ -131,8 +131,8 @@ test_expect_success 'git help succeeds without git.html' '\n \n test_expect_success 'git help --user-interfaces' '\n \tgit help --user-interfaces >help.output &&\n-\tgrep \"^   attributes   \" help.output &&\n-\tgrep \"^   mailmap   \" help.output\n+\ttest_grep \"^   attributes   \" help.output &&\n+\ttest_grep \"^   mailmap   \" help.output\n '\n \n test_expect_success 'git help -c' '\ndiff --git a/t/t0013-sha1dc.sh b/t/t0013-sha1dc.sh\nindex ce3d81227a..3ea3169d92 100755\n--- a/t/t0013-sha1dc.sh\n+++ b/t/t0013-sha1dc.sh\n@@ -16,7 +16,7 @@ fi\n test_expect_success 'test-sha1 detects shattered pdf' '\n \ttest_must_fail test-tool sha1 <\"$TEST_DATA/shattered-1.pdf\" 2>err &&\n \ttest_grep collision err &&\n-\tgrep 38762cf7f55934b34d179ae6a4c80cadccbb7f0a err\n+\ttest_grep 38762cf7f55934b34d179ae6a4c80cadccbb7f0a err\n '\n \n test_done\ndiff --git a/t/t0017-env-helper.sh b/t/t0017-env-helper.sh\nindex 32fe848179..da02bce0ef 100755\n--- a/t/t0017-env-helper.sh\n+++ b/t/t0017-env-helper.sh\n@@ -88,7 +88,7 @@ test_expect_success 'test-tool env-helper reads config thanks to trace2' '\n \ttest_must_fail \\\n \t\tenv HOME=\"$(pwd)/home\" \\\n \t\tgit config -l 2>err &&\n-\tgrep \"exceeded maximum include depth\" err &&\n+\ttest_grep \"exceeded maximum include depth\" err &&\n \n \t# This validates that the assumption that we attempt to\n \t# read the configuration and fail very early in the start-up\n@@ -100,7 +100,7 @@ test_expect_success 'test-tool env-helper reads config thanks to trace2' '\n \t\ttest-tool -C no-such-directory \\\n \t\tenv-helper --type=bool --default=0 \\\n \t\t--exit-code GIT_TEST_ENV_HELPER 2>err &&\n-\tgrep \"exceeded maximum include depth\" err\n+\ttest_grep \"exceeded maximum include depth\" err\n '\n \n test_done\ndiff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh\nindex f0d50d769e..50994a6ada 100755\n--- a/t/t0021-conversion.sh\n+++ b/t/t0021-conversion.sh\n@@ -731,7 +731,7 @@ test_expect_success 'process filter should restart after unexpected write failur\n \t\trm -f debug.log &&\n \t\tgit checkout --quiet --no-progress . 2>git-stderr.log &&\n \n-\t\tgrep \"smudge write error\" git-stderr.log &&\n+\t\ttest_grep \"smudge write error\" git-stderr.log &&\n \t\ttest_grep \"error: external filter\" git-stderr.log &&\n \n \t\tcat >expected.log <<-EOF &&\n@@ -853,7 +853,7 @@ test_expect_success 'invalid process filter must fail (and not hang!)' '\n \n \t\tcp \"$TEST_ROOT/test.o\" test.r &&\n \t\ttest_must_fail git add . 2>git-stderr.log &&\n-\t\tgrep \"expected git-filter-server\" git-stderr.log\n+\t\ttest_grep \"expected git-filter-server\" git-stderr.log\n \t)\n '\n \n@@ -953,7 +953,7 @@ test_expect_success 'missing file in delayed checkout' '\n \n \trm -rf repo-cloned &&\n \ttest_must_fail git clone repo repo-cloned 2>git-stderr.log &&\n-\tgrep \"error: .missing-delay\\.a. was not filtered properly\" git-stderr.log\n+\ttest_grep \"error: .missing-delay\\.a. was not filtered properly\" git-stderr.log\n '\n \n test_expect_success 'invalid file in delayed checkout' '\n@@ -974,7 +974,7 @@ test_expect_success 'invalid file in delayed checkout' '\n \n \trm -rf repo-cloned &&\n \ttest_must_fail git clone repo repo-cloned 2>git-stderr.log &&\n-\tgrep \"error: external filter .* signaled that .unfiltered. is now available although it has not been delayed earlier\" git-stderr.log\n+\ttest_grep \"error: external filter .* signaled that .unfiltered. is now available although it has not been delayed earlier\" git-stderr.log\n '\n \n for mode in 'case' 'utf-8'\n@@ -1015,7 +1015,7 @@ do\n \n \t\tgit clone $mode-collision $mode-collision-cloned &&\n \t\t# Make sure z was really delayed\n-\t\tgrep \"IN: smudge $dir/z .* \\\\[DELAYED\\\\]\" $mode-collision-cloned/delayed.log &&\n+\t\ttest_grep \"IN: smudge $dir/z .* \\\\[DELAYED\\\\]\" $mode-collision-cloned/delayed.log &&\n \n \t\t# Should not create $dir/z at $symlink/z\n \t\ttest_path_is_missing $mode-collision/target-dir/z\n@@ -1053,7 +1053,7 @@ test_expect_success SYMLINKS,CASE_INSENSITIVE_FS \\\n \t\tgit commit -m super &&\n \n \t\tgit checkout --recurse-submodules . &&\n-\t\tgrep \"IN: smudge A/B/y .* \\\\[DELAYED\\\\]\" delayed.log &&\n+\t\ttest_grep \"IN: smudge A/B/y .* \\\\[DELAYED\\\\]\" delayed.log &&\n \t\ttest_path_is_missing target-dir/y\n \t)\n '\n@@ -1144,9 +1144,9 @@ test_expect_success 'delayed checkout correctly reports the number of updated en\n \n \t\trm *.a &&\n \t\tgit checkout . 2>err &&\n-\t\tgrep \"IN: smudge test-delay10.a .* \\\\[DELAYED\\\\]\" delayed.log &&\n-\t\tgrep \"IN: smudge test-delay11.a .* \\\\[DELAYED\\\\]\" delayed.log &&\n-\t\tgrep \"Updated 2 paths from the index\" err\n+\t\ttest_grep \"IN: smudge test-delay10.a .* \\\\[DELAYED\\\\]\" delayed.log &&\n+\t\ttest_grep \"IN: smudge test-delay11.a .* \\\\[DELAYED\\\\]\" delayed.log &&\n+\t\ttest_grep \"Updated 2 paths from the index\" err\n \t)\n '\n \ndiff --git a/t/t0029-core-unsetenvvars.sh b/t/t0029-core-unsetenvvars.sh\nindex baa1b7e85b..975620b389 100755\n--- a/t/t0029-core-unsetenvvars.sh\n+++ b/t/t0029-core-unsetenvvars.sh\n@@ -20,10 +20,10 @@ test_expect_success 'core.unsetenvvars works' '\n \tHOBBES=Calvin &&\n \texport HOBBES &&\n \tgit commit --allow-empty -m with 2>err &&\n-\tgrep Calvin err &&\n+\ttest_grep Calvin err &&\n \tgit -c core.unsetenvvars=FINDUS,HOBBES,CALVIN \\\n \t\tcommit --allow-empty -m without 2>err &&\n-\t! grep Calvin err\n+\ttest_grep ! Calvin err\n '\n \n test_done\ndiff --git a/t/t0030-stripspace.sh b/t/t0030-stripspace.sh\nindex 43155f6bd8..d6e2f63c59 100755\n--- a/t/t0030-stripspace.sh\n+++ b/t/t0030-stripspace.sh\n@@ -407,12 +407,12 @@ test_expect_success 'strip comments with changed comment string' '\n \n test_expect_success 'newline as commentchar is forbidden' '\n \ttest_must_fail git -c core.commentChar=\"$LF\" stripspace -s 2>err &&\n-\tgrep \"core.commentchar cannot contain newline\" err\n+\ttest_grep \"core.commentchar cannot contain newline\" err\n '\n \n test_expect_success 'empty commentchar is forbidden' '\n \ttest_must_fail git -c core.commentchar= stripspace -s 2>err &&\n-\tgrep \"core.commentchar must have at least one character\" err\n+\ttest_grep \"core.commentchar must have at least one character\" err\n '\n \n test_expect_success '-c with single line' '\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex ca55ea8228..a8348a851c 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -324,13 +324,13 @@ test_expect_success 'non ambiguous option (after two options it abbreviates)' '\n \n test_expect_success 'Alias options do not contribute to abbreviation' '\n \ttest-tool parse-options --alias-source 123 >output &&\n-\tgrep \"^string: 123\" output &&\n+\ttest_grep \"^string: 123\" output &&\n \ttest-tool parse-options --alias-target 123 >output &&\n-\tgrep \"^string: 123\" output &&\n+\ttest_grep \"^string: 123\" output &&\n \ttest_must_fail test-tool parse-options --alias &&\n \tGIT_TEST_DISALLOW_ABBREVIATED_OPTIONS=false \\\n \ttest-tool parse-options --alias 123 >output &&\n-\tgrep \"^string: 123\" output\n+\ttest_grep \"^string: 123\" output\n '\n \n cat >typo.err <<\\EOF\n@@ -582,16 +582,16 @@ test_expect_success 'KEEP_UNKNOWN_OPT works' '\n \n test_expect_success 'NO_INTERNAL_HELP works for -h' '\n \ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&\n-\tgrep \"^error: unknown switch \\`h$SQ\" err &&\n-\tgrep \"^usage: \" err\n+\ttest_grep \"^error: unknown switch \\`h$SQ\" err &&\n+\ttest_grep \"^usage: \" err\n '\n \n for help_opt in help help-all\n do\n \ttest_expect_success \"NO_INTERNAL_HELP works for --$help_opt\" \"\n \t\ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd --$help_opt 2>err &&\n-\t\tgrep '^error: unknown option \\`'$help_opt\\' err &&\n-\t\tgrep '^usage: ' err\n+\t\ttest_grep '^error: unknown option \\`'$help_opt\\' err &&\n+\t\ttest_grep '^usage: ' err\n \t\"\n done\n \n@@ -608,38 +608,38 @@ test_expect_success 'KEEP_UNKNOWN_OPT | NO_INTERNAL_HELP works' '\n \n test_expect_success 'subcommand - no subcommand shows error and usage' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd 2>err &&\n-\tgrep \"^error: need a subcommand\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: need a subcommand\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - subcommand after -- shows error and usage' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd -- subcmd-one 2>err &&\n-\tgrep \"^error: need a subcommand\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: need a subcommand\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - subcommand after --end-of-options shows error and usage' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd --end-of-options subcmd-one 2>err &&\n-\tgrep \"^error: need a subcommand\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: need a subcommand\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - unknown subcommand shows error and usage' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd nope 2>err &&\n-\tgrep \"^error: unknown subcommand: \\`nope$SQ\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: unknown subcommand: \\`nope$SQ\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - subcommands cannot be abbreviated' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd subcmd-o 2>err &&\n-\tgrep \"^error: unknown subcommand: \\`subcmd-o$SQ$\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: unknown subcommand: \\`subcmd-o$SQ$\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - no negated subcommands' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd no-subcmd-one 2>err &&\n-\tgrep \"^error: unknown subcommand: \\`no-subcmd-one$SQ\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: unknown subcommand: \\`no-subcmd-one$SQ\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - simple' '\n@@ -709,8 +709,8 @@ test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given + u\n \n test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given + unknown option' '\n \ttest_expect_code 129 test-tool parse-subcommand --subcommand-optional cmd --subcommand-opt 2>err &&\n-\tgrep \"^error: unknown option\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: unknown option\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT + subcommand not given + unknown option' '\n@@ -778,28 +778,28 @@ test_expect_success 'subcommand - completion helper' '\n \n test_expect_success 'subcommands are incompatible with STOP_AT_NON_OPTION' '\n \ttest_must_fail test-tool parse-subcommand --stop-at-non-option cmd subcmd-one 2>err &&\n-\tgrep ^BUG err\n+\ttest_grep ^BUG err\n '\n \n test_expect_success 'subcommands are incompatible with KEEP_UNKNOWN_OPT unless in combination with SUBCOMMAND_OPTIONAL' '\n \ttest_must_fail test-tool parse-subcommand --keep-unknown-opt cmd subcmd-two 2>err &&\n-\tgrep ^BUG err\n+\ttest_grep ^BUG err\n '\n \n test_expect_success 'subcommands are incompatible with KEEP_DASHDASH unless in combination with SUBCOMMAND_OPTIONAL' '\n \ttest_must_fail test-tool parse-subcommand --keep-dashdash cmd subcmd-two 2>err &&\n-\tgrep ^BUG err\n+\ttest_grep ^BUG err\n '\n \n test_expect_success 'negative unsigned' '\n \ttest_must_fail test-tool parse-options --unsigned -1 >out 2>err &&\n-\tgrep \"non-negative integer\" err &&\n+\ttest_grep \"non-negative integer\" err &&\n \ttest_must_be_empty out\n '\n \n test_expect_success 'unsigned with units but no numbers' '\n \ttest_must_fail test-tool parse-options --unsigned m >out 2>err &&\n-\tgrep \"non-negative integer\" err &&\n+\ttest_grep \"non-negative integer\" err &&\n \ttest_must_be_empty out\n '\n \ndiff --git a/t/t0041-usage.sh b/t/t0041-usage.sh\nindex a0f6f134c7..51af7cc030 100755\n--- a/t/t0041-usage.sh\n+++ b/t/t0041-usage.sh\n@@ -13,7 +13,7 @@ test_expect_success 'setup ' '\n \n test_expect_success 'tag --contains <existent_tag>' '\n \tgit tag --contains \"v1.0\" >actual 2>actual.err &&\n-\tgrep \"v1.0\" actual &&\n+\ttest_grep \"v1.0\" actual &&\n \ttest_line_count = 0 actual.err\n '\n \ndiff --git a/t/t0052-simple-ipc.sh b/t/t0052-simple-ipc.sh\nindex ff98be31a5..838ccc0e46 100755\n--- a/t/t0052-simple-ipc.sh\n+++ b/t/t0052-simple-ipc.sh\n@@ -33,19 +33,19 @@ test_expect_success 'servers cannot share the same path' '\n test_expect_success 'big response' '\n \ttest-tool simple-ipc send --token=big >actual &&\n \ttest_line_count -ge 10000 actual &&\n-\tgrep -q \"big: [0]*9999\\$\" actual\n+\ttest_grep \"big: [0]*9999\\$\" actual\n '\n \n test_expect_success 'chunk response' '\n \ttest-tool simple-ipc send --token=chunk >actual &&\n \ttest_line_count -ge 10000 actual &&\n-\tgrep -q \"big: [0]*9999\\$\" actual\n+\ttest_grep \"big: [0]*9999\\$\" actual\n '\n \n test_expect_success 'slow response' '\n \ttest-tool simple-ipc send --token=slow >actual &&\n \ttest_line_count -ge 100 actual &&\n-\tgrep -q \"big: [0]*99\\$\" actual\n+\ttest_grep \"big: [0]*99\\$\" actual\n '\n \n # Send an IPC with n=100,000 bytes of ballast.  This should be large enough\n@@ -54,7 +54,7 @@ test_expect_success 'slow response' '\n #\n test_expect_success 'sendbytes' '\n \ttest-tool simple-ipc sendbytes --bytecount=100000 --byte=A >actual &&\n-\tgrep \"sent:A00100000 rcvd:A00100000\" actual\n+\ttest_grep \"sent:A00100000 rcvd:A00100000\" actual\n '\n \n # Start a series of <threads> client threads that each make <batchsize>\n@@ -93,7 +93,7 @@ test_expect_success 'stress test threads' '\n \t\t--batchsize=13 \\\n \t\t>actual &&\n \ttest_line_count = 92 actual &&\n-\tgrep \"good 91\" actual &&\n+\ttest_grep \"good 91\" actual &&\n \tgrep \"sent:A\" <actual >actual_a &&\n \tcat >expect_a <<-EOF &&\n \t\tsent:A00000019 rcvd:A00000019\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 60cfe65979..dc9d77b0e1 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -97,7 +97,7 @@ test_expect_success POSIXPERM 'run_command reports EACCES' '\n \tchmod -x hello.sh &&\n \ttest_must_fail test-tool run-command run-command ./hello.sh 2>err &&\n \n-\tgrep \"fatal: cannot exec.*hello.sh\" err\n+\ttest_grep \"fatal: cannot exec.*hello.sh\" err\n '\n \n test_expect_success POSIXPERM,SANITY 'unreadable directory in PATH' '\ndiff --git a/t/t0066-dir-iterator.sh b/t/t0066-dir-iterator.sh\nindex df3e9f5fa5..9fbb41bac5 100755\n--- a/t/t0066-dir-iterator.sh\n+++ b/t/t0066-dir-iterator.sh\n@@ -127,7 +127,7 @@ test_expect_success SYMLINKS 'dir-iterator should not follow symlinks by default\n test_expect_success SYMLINKS 'dir-iterator does not resolve top-level symlinks' '\n \ttest_must_fail test-tool dir-iterator ./dir5 >out &&\n \n-\tgrep \"ENOTDIR\" out\n+\ttest_grep \"ENOTDIR\" out\n '\n \n test_done\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 80b163ea99..55b37d895a 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -21,23 +21,23 @@ test_expect_success 'run based on configured value' '\n \n \tgit for-each-repo --config=run.key commit --allow-empty -m \"ran\" &&\n \tgit -C one log -1 --pretty=format:%s >message &&\n-\tgrep ran message &&\n+\ttest_grep ran message &&\n \tgit -C two log -1 --pretty=format:%s >message &&\n-\t! grep ran message &&\n+\ttest_grep ! ran message &&\n \tgit -C three log -1 --pretty=format:%s >message &&\n-\tgrep ran message &&\n+\ttest_grep ran message &&\n \tgit -C ~/four log -1 --pretty=format:%s >message &&\n-\tgrep ran message &&\n+\ttest_grep ran message &&\n \n \tgit for-each-repo --config=run.key -- commit --allow-empty -m \"ran again\" &&\n \tgit -C one log -1 --pretty=format:%s >message &&\n-\tgrep again message &&\n+\ttest_grep again message &&\n \tgit -C two log -1 --pretty=format:%s >message &&\n-\t! grep again message &&\n+\ttest_grep ! again message &&\n \tgit -C three log -1 --pretty=format:%s >message &&\n-\tgrep again message &&\n+\ttest_grep again message &&\n \tgit -C ~/four log -1 --pretty=format:%s >message &&\n-\tgrep again message &&\n+\ttest_grep again message &&\n \n \tgit -C three for-each-repo --config=run.key -- \\\n \t\tcommit --allow-empty -m \"ran from worktree\" &&\ndiff --git a/t/t0070-fundamental.sh b/t/t0070-fundamental.sh\nindex 6b9dcf984b..8f573c2a0e 100755\n--- a/t/t0070-fundamental.sh\n+++ b/t/t0070-fundamental.sh\n@@ -10,7 +10,7 @@ Verify wrappers and compatibility functions.\n \n test_expect_success 'mktemp to nonexistent directory prints filename' '\n \ttest_must_fail test-tool mktemp doesnotexist/testXXXXXX 2>err &&\n-\tgrep \"doesnotexist/test\" err\n+\ttest_grep \"doesnotexist/test\" err\n '\n \n test_expect_success POSIXPERM,SANITY 'mktemp to unwritable directory prints filename' '\n@@ -18,7 +18,7 @@ test_expect_success POSIXPERM,SANITY 'mktemp to unwritable directory prints file\n \ttest_when_finished \"chmod +w cannotwrite\" &&\n \tchmod -w cannotwrite &&\n \ttest_must_fail test-tool mktemp cannotwrite/testXXXXXX 2>err &&\n-\tgrep \"cannotwrite/test\" err\n+\ttest_grep \"cannotwrite/test\" err\n '\n \n test_expect_success 'git_mkstemps_mode does not fail if fd 0 is not open' '\n@@ -33,7 +33,7 @@ test_expect_success 'check for a bug in the regex routines' '\n test_expect_success 'incomplete sideband messages are reassembled' '\n \ttest-tool pkt-line send-split-sideband >split-sideband &&\n \ttest-tool pkt-line receive-sideband <split-sideband 2>err &&\n-\tgrep \"Hello, world\" err\n+\ttest_grep \"Hello, world\" err\n '\n \n test_expect_success 'eof on sideband message is reported' '\ndiff --git a/t/t0081-find-pack.sh b/t/t0081-find-pack.sh\nindex 26f017422d..ff9d56fbf1 100755\n--- a/t/t0081-find-pack.sh\n+++ b/t/t0081-find-pack.sh\n@@ -52,17 +52,17 @@ test_expect_success 'add more packfiles' '\n \n \t# HEAD^{tree} is in 2 packfiles\n \ttest-tool find-pack HEAD^{tree} >head_tree_packs &&\n-\tgrep \"$head_commit_pack\" head_tree_packs &&\n-\tgrep mypackname1 head_tree_packs &&\n-\t! grep mypackname2 head_tree_packs &&\n+\ttest_grep \"$head_commit_pack\" head_tree_packs &&\n+\ttest_grep mypackname1 head_tree_packs &&\n+\ttest_grep ! mypackname2 head_tree_packs &&\n \ttest-tool find-pack --check-count 2 HEAD^{tree} &&\n \t! test-tool find-pack --check-count 1 HEAD^{tree} &&\n \n \t# HEAD:five.t is also in 2 packfiles\n \ttest-tool find-pack HEAD:five.t >five_packs &&\n-\tgrep \"$head_commit_pack\" five_packs &&\n-\t! grep mypackname1 five_packs &&\n-\tgrep mypackname2 five_packs &&\n+\ttest_grep \"$head_commit_pack\" five_packs &&\n+\ttest_grep ! mypackname1 five_packs &&\n+\ttest_grep mypackname2 five_packs &&\n \ttest-tool find-pack -c 2 HEAD:five.t &&\n \t! test-tool find-pack --check-count=0 HEAD:five.t\n '\ndiff --git a/t/t0091-bugreport.sh b/t/t0091-bugreport.sh\nindex e38ca7a901..81ed41742c 100755\n--- a/t/t0091-bugreport.sh\n+++ b/t/t0091-bugreport.sh\n@@ -40,15 +40,15 @@ test_expect_success 'sanity check \"System Info\" section' '\n \n \t# The beginning should match \"git version --build-options\" verbatim,\n \t# but rather than checking bit-for-bit equality, just test some basics.\n-\tgrep \"git version \" system &&\n-\tgrep \"shell-path: .\" system &&\n+\ttest_grep \"git version \" system &&\n+\ttest_grep \"shell-path: .\" system &&\n \n \t# After the version, there should be some more info.\n \t# This is bound to differ from environment to environment,\n \t# so we just do some rather high-level checks.\n-\tgrep \"uname: .\" system &&\n-\tgrep \"compiler info: .\" system &&\n-\tgrep \"zlib.\" system\n+\ttest_grep \"uname: .\" system &&\n+\ttest_grep \"compiler info: .\" system &&\n+\ttest_grep \"zlib.\" system\n '\n \n test_expect_success 'dies if file with same name as report already exists' '\n@@ -112,7 +112,7 @@ test_expect_success UNZIP '--diagnose creates diagnostics zip archive' '\n \tgit bugreport --diagnose -o report -s test >out &&\n \n \tzip_path=report/git-diagnostics-test.zip &&\n-\tgrep \"Available space\" out &&\n+\ttest_grep \"Available space\" out &&\n \ttest_path_is_file \"$zip_path\" &&\n \n \t# Check zipped archive content\n@@ -120,10 +120,10 @@ test_expect_success UNZIP '--diagnose creates diagnostics zip archive' '\n \ttest_file_not_empty out &&\n \n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t\"$GIT_UNZIP\" -p \"$zip_path\" objects-local.txt >out &&\n-\tgrep \"^Total: [0-9][0-9]*\" out &&\n+\ttest_grep \"^Total: [0-9][0-9]*\" out &&\n \n \t# Should not include .git directory contents by default\n \t! \"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\"\n@@ -136,7 +136,7 @@ test_expect_success UNZIP '--diagnose=stats excludes .git dir contents' '\n \n \t# Includes pack quantity/size info\n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t# Does not include .git directory contents\n \t! \"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\"\ndiff --git a/t/t0092-diagnose.sh b/t/t0092-diagnose.sh\nindex 6cabd6e67b..0bc2b1ed5c 100755\n--- a/t/t0092-diagnose.sh\n+++ b/t/t0092-diagnose.sh\n@@ -8,7 +8,7 @@ test_expect_success UNZIP 'creates diagnostics zip archive' '\n \ttest_when_finished rm -rf report &&\n \n \tgit diagnose -o report -s test >out &&\n-\tgrep \"Available space\" out &&\n+\ttest_grep \"Available space\" out &&\n \n \tzip_path=report/git-diagnostics-test.zip &&\n \ttest_path_is_file \"$zip_path\" &&\n@@ -18,10 +18,10 @@ test_expect_success UNZIP 'creates diagnostics zip archive' '\n \ttest_file_not_empty out &&\n \n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t\"$GIT_UNZIP\" -p \"$zip_path\" objects-local.txt >out &&\n-\tgrep \"^Total: [0-9][0-9]*\" out &&\n+\ttest_grep \"^Total: [0-9][0-9]*\" out &&\n \n \t# Should not include .git directory contents by default\n \t! \"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\"\n@@ -34,7 +34,7 @@ test_expect_success UNZIP 'counts loose objects' '\n \tgit diagnose -o test-count -s 1 >out &&\n \tzip_path=test-count/git-diagnostics-1.zip &&\n \t\"$GIT_UNZIP\" -p \"$zip_path\" objects-local.txt >out &&\n-\tgrep \"^Total: [1-9][0-9]* loose objects\" out\n+\ttest_grep \"^Total: [1-9][0-9]* loose objects\" out\n '\n \n test_expect_success UNZIP '--mode=stats excludes .git dir contents' '\n@@ -45,7 +45,7 @@ test_expect_success UNZIP '--mode=stats excludes .git dir contents' '\n \t# Includes pack quantity/size info\n \tzip_path=report/git-diagnostics-test.zip &&\n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t# Does not include .git directory contents\n \t! \"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\"\n@@ -59,7 +59,7 @@ test_expect_success UNZIP '--mode=all includes .git dir contents' '\n \t# Includes pack quantity/size info\n \tzip_path=report/git-diagnostics-test.zip &&\n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t# Includes .git directory contents\n \t\"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\" &&\ndiff --git a/t/t0100-previous.sh b/t/t0100-previous.sh\nindex dd5d9b4e5e..87a9995ea4 100755\n--- a/t/t0100-previous.sh\n+++ b/t/t0100-previous.sh\n@@ -50,7 +50,7 @@ test_expect_success 'merge @{-1}~1' '\n \tgit checkout main &&\n \tgit merge @{-1}~1 &&\n \tgit cat-file commit HEAD >actual &&\n-\tgrep \"Merge branch '\\''other'\\''\" actual\n+\ttest_grep \"Merge branch '\\''other'\\''\" actual\n '\n \n test_expect_success 'merge @{-100} before checking out that many branches yet' '\ndiff --git a/t/t0200-gettext-basic.sh b/t/t0200-gettext-basic.sh\nindex 8853d8afb9..8db26c1ada 100755\n--- a/t/t0200-gettext-basic.sh\n+++ b/t/t0200-gettext-basic.sh\n@@ -16,7 +16,7 @@ test_expect_success 'sanity: $TEXTDOMAIN is git' '\n '\n \n test_expect_success 'xgettext sanity: Perl _() strings are not extracted' '\n-    ! grep \"A Perl string xgettext will not get\" \"$GIT_PO_PATH\"/is.po\n+    test_grep ! \"A Perl string xgettext will not get\" \"$GIT_PO_PATH\"/is.po\n '\n \n test_expect_success 'xgettext sanity: Comment extraction with --add-comments' '\n@@ -26,8 +26,8 @@ test_expect_success 'xgettext sanity: Comment extraction with --add-comments' '\n '\n \n test_expect_success 'xgettext sanity: Comment extraction with --add-comments stops at statements' '\n-    ! grep \"This is a phony\" \"$GIT_PO_PATH\"/is.po &&\n-    ! grep \"the above comment\" \"$GIT_PO_PATH\"/is.po\n+    test_grep ! \"This is a phony\" \"$GIT_PO_PATH\"/is.po &&\n+    test_grep ! \"the above comment\" \"$GIT_PO_PATH\"/is.po\n '\n \n test_expect_success GETTEXT 'sanity: $TEXTDOMAINDIR exists without NO_GETTEXT=YesPlease' '\n@@ -44,10 +44,10 @@ test_expect_success GETTEXT 'sanity: Icelandic locale was compiled' '\n test_expect_success GETTEXT_LOCALE 'sanity: gettext(\"\") metadata is OK' '\n     # Return value may be non-zero\n     LANGUAGE=is LC_ALL=\"$is_IS_locale\" gettext \"\" >zero-expect &&\n-    grep \"Project-Id-Version: Git\" zero-expect &&\n-    grep \"Git Mailing List <git@vger.kernel.org>\" zero-expect &&\n-    grep \"Content-Type: text/plain; charset=UTF-8\" zero-expect &&\n-    grep \"Content-Transfer-Encoding: 8bit\" zero-expect\n+    test_grep \"Project-Id-Version: Git\" zero-expect &&\n+    test_grep \"Git Mailing List <git@vger.kernel.org>\" zero-expect &&\n+    test_grep \"Content-Type: text/plain; charset=UTF-8\" zero-expect &&\n+    test_grep \"Content-Transfer-Encoding: 8bit\" zero-expect\n '\n \n test_expect_success GETTEXT_LOCALE 'sanity: gettext(unknown) is passed through' '\ndiff --git a/t/t0203-gettext-setlocale-sanity.sh b/t/t0203-gettext-setlocale-sanity.sh\nindex 0ce1f22eff..d8af97de23 100755\n--- a/t/t0203-gettext-setlocale-sanity.sh\n+++ b/t/t0203-gettext-setlocale-sanity.sh\n@@ -12,7 +12,7 @@ test_expect_success 'git show a ISO-8859-1 commit under C locale' '\n \ttest_commit \"iso-c-commit\" iso-under-c &&\n \tgit show >out 2>err &&\n \ttest_must_be_empty err &&\n-\tgrep -q \"iso-c-commit\" out\n+\ttest_grep \"iso-c-commit\" out\n '\n \n test_expect_success GETTEXT_LOCALE 'git show a ISO-8859-1 commit under a UTF-8 locale' '\n@@ -20,7 +20,7 @@ test_expect_success GETTEXT_LOCALE 'git show a ISO-8859-1 commit under a UTF-8 l\n \ttest_commit \"iso-utf8-commit\" iso-under-utf8 &&\n \tLANGUAGE=is LC_ALL=\"$is_IS_locale\" git show >out 2>err &&\n \ttest_must_be_empty err &&\n-\tgrep -q \"iso-utf8-commit\" out\n+\ttest_grep \"iso-utf8-commit\" out\n '\n \n test_done\ndiff --git a/t/t0204-gettext-reencode-sanity.sh b/t/t0204-gettext-reencode-sanity.sh\nindex 28d92bb9b7..48ccddaf7d 100755\n--- a/t/t0204-gettext-reencode-sanity.sh\n+++ b/t/t0204-gettext-reencode-sanity.sh\n@@ -66,22 +66,22 @@ test_expect_success GETTEXT_LOCALE 'gettext: Fetching a UTF-8 msgid -> UTF-8' '\n # eyes.\n test_expect_success GETTEXT_ISO_LOCALE 'gettext: Fetching a UTF-8 msgid -> ISO-8859-1' '\n     LANGUAGE=is LC_ALL=\"$is_IS_iso_locale\" gettext \"TEST: ‘single’ and “double” quotes\" >actual &&\n-    grep \"einfaldar\" actual &&\n-    grep \"$(echo tvöfaldar | iconv -f UTF-8 -t ISO8859-1)\" actual\n+    test_grep \"einfaldar\" actual &&\n+    test_grep \"$(echo tvöfaldar | iconv -f UTF-8 -t ISO8859-1)\" actual\n '\n \n test_expect_success GETTEXT_LOCALE 'gettext.c: git init UTF-8 -> UTF-8' '\n     printf \"Bjó til tóma Git lind\" >expect &&\n     LANGUAGE=is LC_ALL=\"$is_IS_locale\" git init repo >actual &&\n     test_when_finished \"rm -rf repo\" &&\n-    grep \"^$(cat expect) \" actual\n+    test_grep \"^$(cat expect) \" actual\n '\n \n test_expect_success GETTEXT_ISO_LOCALE 'gettext.c: git init UTF-8 -> ISO-8859-1' '\n     printf \"Bjó til tóma Git lind\" >expect &&\n     LANGUAGE=is LC_ALL=\"$is_IS_iso_locale\" git init repo >actual &&\n     test_when_finished \"rm -rf repo\" &&\n-    grep \"^$(iconv -f UTF-8 -t ISO8859-1 <expect) \" actual\n+    test_grep \"^$(iconv -f UTF-8 -t ISO8859-1 <expect) \" actual\n '\n \n test_done\ndiff --git a/t/t0210-trace2-normal.sh b/t/t0210-trace2-normal.sh\nindex 7e1e7af862..beed2b27a5 100755\n--- a/t/t0210-trace2-normal.sh\n+++ b/t/t0210-trace2-normal.sh\n@@ -333,12 +333,12 @@ test_expect_success 'unsafe URLs are redacted by default' '\n \n \tGIT_TRACE2=\"$(pwd)/trace.normal\" \\\n \t\tgit clone https://user:pwd@example.com/ clone &&\n-\t! grep user:pwd trace.normal &&\n+\ttest_grep ! user:pwd trace.normal &&\n \n \tGIT_TRACE2_REDACT=0 GIT_TRACE2=\"$(pwd)/unredacted.normal\" \\\n \t\tgit clone https://user:pwd@example.com/ clone2 &&\n-\tgrep \"start .* clone https://user:pwd@example.com\" unredacted.normal &&\n-\tgrep \"remote.origin.url=https://user:pwd@example.com\" unredacted.normal\n+\ttest_grep \"start .* clone https://user:pwd@example.com\" unredacted.normal &&\n+\ttest_grep \"remote.origin.url=https://user:pwd@example.com\" unredacted.normal\n '\n \n test_done\ndiff --git a/t/t0211-trace2-perf.sh b/t/t0211-trace2-perf.sh\nindex 760cf69087..665c8b3669 100755\n--- a/t/t0211-trace2-perf.sh\n+++ b/t/t0211-trace2-perf.sh\n@@ -283,13 +283,13 @@ test_expect_success 'unsafe URLs are redacted by default' '\n \n \tGIT_TRACE2_PERF=\"$(pwd)/trace.perf\" \\\n \t\tgit clone https://user:pwd@example.com/ clone &&\n-\t! grep user:pwd trace.perf &&\n+\ttest_grep ! user:pwd trace.perf &&\n \n \tGIT_TRACE2_REDACT=0 GIT_TRACE2_PERF=\"$(pwd)/unredacted.perf\" \\\n \t\tgit clone https://user:pwd@example.com/ clone2 &&\n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <unredacted.perf >actual &&\n-\tgrep \"d0|main|start|.* clone https://user:pwd@example.com\" actual &&\n-\tgrep \"d0|main|def_param|.*|remote.origin.url:https://user:pwd@example.com\" actual\n+\ttest_grep \"d0|main|start|.* clone https://user:pwd@example.com\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|remote.origin.url:https://user:pwd@example.com\" actual\n '\n \n # Confirm that the requested command produces a \"cmd_name\" and a\n@@ -358,13 +358,13 @@ test_expect_success LIBCURL \\\n \n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <prop.perf >actual &&\n \n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_\" actual &&\n-\tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n-\tgrep \"d1|main|cmd_name|.*|remote-curl\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|remote-curl\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n # Similarly, `git-http-fetch` is not built from git.c so do a\n@@ -389,13 +389,13 @@ test_expect_success LIBCURL \\\n \n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <prop.perf >actual &&\n \n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_\" actual &&\n-\tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n-\tgrep \"d1|main|cmd_name|.*|http-fetch\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|http-fetch\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n # Historically, alias expansion explicitly emitted the def_param\n@@ -421,22 +421,22 @@ test_expect_success 'expect def_params during git alias expansion' '\n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <prop.perf >actual &&\n \n \t# \"git xxx\" is first mapped to \"git-xxx\" and the child will fail.\n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n \n \t# We unpeel that and substitute \"version\" into \"xxx\" (giving\n \t# \"git version\") and update the cmd_name event.\n-\tgrep \"d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_git_alias_)\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_git_alias_)\" actual &&\n \n \t# These def_param events could be associated with either of the\n \t# above cmd_name events.  It does not matter.\n-\tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n \t# The \"git version\" child sees a different cmd_name hierarchy.\n \t# Also test the def_param (only for completeness).\n-\tgrep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_git_alias_/version)\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_git_alias_/version)\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n test_expect_success 'expect def_params during shell alias expansion' '\n@@ -456,25 +456,25 @@ test_expect_success 'expect def_params during shell alias expansion' '\n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <prop.perf >actual &&\n \n \t# \"git xxx\" is first mapped to \"git-xxx\" and the child will fail.\n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n \n \t# We unpeel that and substitute \"git version\" for \"git xxx\" (as a\n \t# shell command.  Another cmd_name event is emitted as we unpeel.\n-\tgrep \"d0|main|cmd_name|.*|_run_shell_alias_ (_run_dashed_/_run_shell_alias_)\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_shell_alias_ (_run_dashed_/_run_shell_alias_)\" actual &&\n \n \t# These def_param events could be associated with either of the\n \t# above cmd_name events.  It does not matter.\n-\tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n \t# We get the following only because we used a git command for the\n \t# shell command. In general, it could have been a shell script and\n \t# we would see nothing.\n \t#\n \t# The child knows the cmd_name hierarchy so it includes it.\n-\tgrep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_shell_alias_/version)\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_shell_alias_/version)\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n test_expect_success 'expect def_params during nested git alias expansion' '\n@@ -496,33 +496,33 @@ test_expect_success 'expect def_params during nested git alias expansion' '\n \n \t# \"git xxx\" is first mapped to \"git-xxx\" and try to spawn \"git-xxx\"\n \t# and the child will fail.\n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n-\tgrep \"d0|main|child_start|.*|.* class:dashed argv:\\[git-xxx\\]\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n+\ttest_grep \"d0|main|child_start|.*|.* class:dashed argv:\\[git-xxx\\]\" actual &&\n \n \t# We unpeel that and substitute \"yyy\" into \"xxx\" (giving \"git yyy\")\n \t# and spawn \"git-yyy\" and the child will fail.\n-\tgrep \"d0|main|alias|.*|alias:xxx argv:\\[yyy\\]\" actual &&\n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_/_run_dashed_)\" actual &&\n-\tgrep \"d0|main|child_start|.*|.* class:dashed argv:\\[git-yyy\\]\" actual &&\n+\ttest_grep \"d0|main|alias|.*|alias:xxx argv:\\[yyy\\]\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_/_run_dashed_)\" actual &&\n+\ttest_grep \"d0|main|child_start|.*|.* class:dashed argv:\\[git-yyy\\]\" actual &&\n \n \t# We unpeel that and substitute \"version\" into \"xxx\" (giving\n \t# \"git version\") and update the cmd_name event.\n-\tgrep \"d0|main|alias|.*|alias:yyy argv:\\[version\\]\" actual &&\n-\tgrep \"d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_dashed_/_run_git_alias_)\" actual &&\n+\ttest_grep \"d0|main|alias|.*|alias:yyy argv:\\[version\\]\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_dashed_/_run_git_alias_)\" actual &&\n \n \t# These def_param events could be associated with any of the\n \t# above cmd_name events.  It does not matter.\n \tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual >actual.matches &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n \t# However, we do not want them repeated each time we unpeel.\n \ttest_line_count = 1 actual.matches &&\n \n \t# The \"git version\" child sees a different cmd_name hierarchy.\n \t# Also test the def_param (only for completeness).\n-\tgrep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_dashed_/_run_git_alias_/version)\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_dashed_/_run_git_alias_/version)\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n test_done\ndiff --git a/t/t0212-trace2-event.sh b/t/t0212-trace2-event.sh\nindex 1211db9f46..f5358a1dd4 100755\n--- a/t/t0212-trace2-event.sh\n+++ b/t/t0212-trace2-event.sh\n@@ -332,7 +332,7 @@ test_expect_success 'unsafe URLs are redacted by default in cmd_start events' '\n \n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.event\" \\\n \t\ttest-tool trace2 300redact_start git clone https://user:pwd@example.com/ clone2 &&\n-\t! grep user:pwd trace.event\n+\ttest_grep ! user:pwd trace.event\n '\n \n test_expect_success 'unsafe URLs are redacted by default in child_start events' '\n@@ -341,7 +341,7 @@ test_expect_success 'unsafe URLs are redacted by default in child_start events'\n \n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.event\" \\\n \t\ttest-tool trace2 301redact_child_start git clone https://user:pwd@example.com/ clone2 &&\n-\t! grep user:pwd trace.event\n+\ttest_grep ! user:pwd trace.event\n '\n \n test_expect_success 'unsafe URLs are redacted by default in exec events' '\n@@ -350,7 +350,7 @@ test_expect_success 'unsafe URLs are redacted by default in exec events' '\n \n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.event\" \\\n \t\ttest-tool trace2 302redact_exec git clone https://user:pwd@example.com/ clone2 &&\n-\t! grep user:pwd trace.event\n+\ttest_grep ! user:pwd trace.event\n '\n \n test_expect_success 'unsafe URLs are redacted by default in def_param events' '\n@@ -359,7 +359,7 @@ test_expect_success 'unsafe URLs are redacted by default in def_param events' '\n \n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.event\" \\\n \t\ttest-tool trace2 303redact_def_param url https://user:pwd@example.com/ &&\n-\t! grep user:pwd trace.event\n+\ttest_grep ! user:pwd trace.event\n '\n \n test_done\ndiff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh\nindex 64ead1571a..ea11cdb4dc 100755\n--- a/t/t0300-credentials.sh\n+++ b/t/t0300-credentials.sh\n@@ -1014,7 +1014,7 @@ test_expect_success 'credential config with partial URLs' '\n \tdo\n \t\tgit -c credential.$partial.helper=yep \\\n \t\t\tcredential fill <stdin >stdout &&\n-\t\tgrep yep stdout ||\n+\t\ttest_grep yep stdout ||\n \t\treturn 1\n \tdone &&\n \n@@ -1030,7 +1030,7 @@ test_expect_success 'credential config with partial URLs' '\n \tdo\n \t\tgit -c credential.$partial.helper=yep \\\n \t\t\tcredential fill <stdin >stdout &&\n-\t\t! grep yep stdout ||\n+\t\ttest_grep ! yep stdout ||\n \t\treturn 1\n \tdone &&\n \ndiff --git a/t/t0410-partial-clone.sh b/t/t0410-partial-clone.sh\nindex 52e19728a3..afa890c470 100755\n--- a/t/t0410-partial-clone.sh\n+++ b/t/t0410-partial-clone.sh\n@@ -206,7 +206,7 @@ test_expect_success 'fetching of missing objects' '\n \tgit -C repo cat-file -p \"$HASH\" 2>err &&\n \n \t# Ensure that no spurious FETCH_HEAD messages are written\n-\t! grep FETCH_HEAD err &&\n+\ttest_grep ! FETCH_HEAD err &&\n \n \t# Ensure that the .promisor file is written, and check that its\n \t# associated packfile contains the object\n@@ -214,7 +214,7 @@ test_expect_success 'fetching of missing objects' '\n \ttest_line_count = 1 promisorlist &&\n \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n \tgit verify-pack --verbose \"$IDX\" >out &&\n-\tgrep \"$HASH\" out\n+\ttest_grep \"$HASH\" out\n '\n \n test_expect_success 'fetching of a promised object that promisor remote no longer has' '\n@@ -228,7 +228,7 @@ test_expect_success 'fetching of a promised object that promisor remote no longe\n \n \trm -rf unreliable-server/.git/objects/* &&\n \ttest_must_fail git -C unreliable-client checkout HEAD 2>err &&\n-\tgrep \"could not fetch.*from promisor remote\" err\n+\ttest_grep \"could not fetch.*from promisor remote\" err\n '\n \n test_expect_success 'fetching of missing objects works with ref-in-want enabled' '\n@@ -240,7 +240,7 @@ test_expect_success 'fetching of missing objects works with ref-in-want enabled'\n \trm -rf repo/.git/objects/* &&\n \trm -f trace &&\n \tGIT_TRACE_PACKET=\"$(pwd)/trace\" git -C repo cat-file -p \"$HASH\" &&\n-\tgrep \"fetch< fetch=.*ref-in-want\" trace\n+\ttest_grep \"fetch< fetch=.*ref-in-want\" trace\n '\n \n test_expect_success 'fetching from another promisor remote' '\n@@ -263,7 +263,7 @@ test_expect_success 'fetching from another promisor remote' '\n \ttest_line_count = 1 promisorlist &&\n \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n \tgit verify-pack --verbose \"$IDX\" >out &&\n-\tgrep \"$HASH2\" out\n+\ttest_grep \"$HASH2\" out\n '\n \n test_expect_success 'fetching with --filter configures a promisor remote' '\n@@ -286,7 +286,7 @@ test_expect_success 'fetching with --filter configures a promisor remote' '\n \ttest_line_count = 1 promisorlist &&\n \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n \tgit verify-pack --verbose \"$IDX\" >out &&\n-\tgrep \"$HASH3\" out\n+\ttest_grep \"$HASH3\" out\n '\n \n test_expect_success 'fetching of missing blobs works' '\n@@ -327,8 +327,8 @@ test_expect_success 'fetching of missing trees does not fetch blobs' '\n \n \t# Ensure that the tree, but not the blob, is fetched\n \tgit -C repo rev-list --objects --missing=print $(cat treehash) >objects &&\n-\tgrep \"^$(cat treehash)\" objects &&\n-\tgrep \"^[?]$(cat blobhash)\" objects\n+\ttest_grep \"^$(cat treehash)\" objects &&\n+\ttest_grep \"^[?]$(cat blobhash)\" objects\n '\n \n test_expect_success 'rev-list stops traversal at missing and promised commit' '\n@@ -343,8 +343,8 @@ test_expect_success 'rev-list stops traversal at missing and promised commit' '\n \tgit -C repo config core.repositoryformatversion 1 &&\n \tgit -C repo config extensions.partialclone \"arbitrary string\" &&\n \tgit -C repo rev-list --exclude-promisor-objects --objects bar >out &&\n-\tgrep $(git -C repo rev-parse bar) out &&\n-\t! grep $FOO out\n+\ttest_grep $(git -C repo rev-parse bar) out &&\n+\ttest_grep ! $FOO out\n '\n \n test_expect_success 'missing tree objects with --missing=allow-promisor and --exclude-promisor-objects' '\n@@ -413,10 +413,10 @@ test_expect_success 'rev-list stops traversal at missing and promised tree' '\n \tgit -C repo config core.repositoryformatversion 1 &&\n \tgit -C repo config extensions.partialclone \"arbitrary string\" &&\n \tgit -C repo rev-list --exclude-promisor-objects --objects HEAD >out &&\n-\tgrep $(git -C repo rev-parse foo) out &&\n-\t! grep $TREE out &&\n-\tgrep $(git -C repo rev-parse HEAD) out &&\n-\t! grep $TREE2 out\n+\ttest_grep $(git -C repo rev-parse foo) out &&\n+\ttest_grep ! $TREE out &&\n+\ttest_grep $(git -C repo rev-parse HEAD) out &&\n+\ttest_grep ! $TREE2 out\n '\n \n test_expect_success 'rev-list stops traversal at missing and promised blob' '\n@@ -432,8 +432,8 @@ test_expect_success 'rev-list stops traversal at missing and promised blob' '\n \tgit -C repo config core.repositoryformatversion 1 &&\n \tgit -C repo config extensions.partialclone \"arbitrary string\" &&\n \tgit -C repo rev-list --exclude-promisor-objects --objects HEAD >out &&\n-\tgrep $(git -C repo rev-parse HEAD) out &&\n-\t! grep $BLOB out\n+\ttest_grep $(git -C repo rev-parse HEAD) out &&\n+\ttest_grep ! $BLOB out\n '\n \n test_expect_success 'rev-list stops traversal at promisor commit, tree, and blob' '\n@@ -451,10 +451,10 @@ test_expect_success 'rev-list stops traversal at promisor commit, tree, and blob\n \tgit -C repo config core.repositoryformatversion 1 &&\n \tgit -C repo config extensions.partialclone \"arbitrary string\" &&\n \tgit -C repo rev-list --exclude-promisor-objects --objects HEAD >out &&\n-\t! grep $COMMIT out &&\n-\t! grep $TREE out &&\n-\t! grep $BLOB out &&\n-\tgrep $(git -C repo rev-parse bar) out  # sanity check that some walking was done\n+\ttest_grep ! $COMMIT out &&\n+\ttest_grep ! $TREE out &&\n+\ttest_grep ! $BLOB out &&\n+\ttest_grep $(git -C repo rev-parse bar) out  # sanity check that some walking was done\n '\n \n test_expect_success 'rev-list dies for missing objects on cmd line' '\n@@ -523,10 +523,10 @@ test_expect_success 'gc repacks promisor objects separately from non-promisor ob\n \ttest_line_count = 1 promisorlist &&\n \tPROMISOR_PACKFILE=$(sed \"s/.promisor/.pack/\" <promisorlist) &&\n \tgit verify-pack $PROMISOR_PACKFILE -v >out &&\n-\tgrep \"$TREE_ONE\" out &&\n-\tgrep \"$TREE_TWO\" out &&\n-\t! grep \"$(git -C repo rev-parse one)\" out &&\n-\t! grep \"$(git -C repo rev-parse two)\" out &&\n+\ttest_grep \"$TREE_ONE\" out &&\n+\ttest_grep \"$TREE_TWO\" out &&\n+\ttest_grep ! \"$(git -C repo rev-parse one)\" out &&\n+\ttest_grep ! \"$(git -C repo rev-parse two)\" out &&\n \n \t# Remove the promisor packfile and associated files\n \trm $(sed \"s/.promisor//\" <promisorlist).* &&\n@@ -536,10 +536,10 @@ test_expect_success 'gc repacks promisor objects separately from non-promisor ob\n \tls repo/.git/objects/pack/pack-*.pack >packlist &&\n \ttest_line_count = 1 packlist &&\n \tgit verify-pack repo/.git/objects/pack/pack-*.pack -v >out &&\n-\tgrep \"$(git -C repo rev-parse one)\" out &&\n-\tgrep \"$(git -C repo rev-parse two)\" out &&\n-\t! grep \"$TREE_ONE\" out &&\n-\t! grep \"$TREE_TWO\" out\n+\ttest_grep \"$(git -C repo rev-parse one)\" out &&\n+\ttest_grep \"$(git -C repo rev-parse two)\" out &&\n+\ttest_grep ! \"$TREE_ONE\" out &&\n+\ttest_grep ! \"$TREE_TWO\" out\n '\n \n test_expect_success 'gc does not repack promisor objects if there are none' '\n@@ -616,8 +616,8 @@ test_expect_success 'gc stops traversal when a missing but promised object is re\n \tls repo/.git/objects/pack/pack-*.pack >packlist &&\n \ttest_line_count = 1 packlist &&\n \tgit verify-pack repo/.git/objects/pack/pack-*.pack -v >out &&\n-\tgrep \"$(git -C repo rev-parse HEAD)\" out &&\n-\t! grep \"$TREE_HASH\" out\n+\ttest_grep \"$(git -C repo rev-parse HEAD)\" out &&\n+\ttest_grep ! \"$TREE_HASH\" out\n '\n \n test_expect_success 'do not fetch when checking existence of tree we construct ourselves' '\n@@ -647,10 +647,10 @@ test_expect_success 'exact rename does not need to fetch the blob lazily' '\n \n \tgit clone --filter=blob:none --bare \"file://$(pwd)/repo\" partial.git &&\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\tgrep \"[?]$FILE_HASH\" out &&\n+\ttest_grep \"[?]$FILE_HASH\" out &&\n \tgit -C partial.git log --follow -- new-file.txt &&\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\tgrep \"[?]$FILE_HASH\" out\n+\ttest_grep \"[?]$FILE_HASH\" out\n '\n \n test_expect_success 'lazy-fetch when accessing object not in the_repository' '\n@@ -665,7 +665,7 @@ test_expect_success 'lazy-fetch when accessing object not in the_repository' '\n \n \t# Sanity check that the file is missing\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\tgrep \"[?]$FILE_HASH\" out &&\n+\ttest_grep \"[?]$FILE_HASH\" out &&\n \n \t# The no-lazy-fetch mechanism prevents Git from fetching\n \ttest_must_fail env GIT_NO_LAZY_FETCH=1 \\\n@@ -680,7 +680,7 @@ test_expect_success 'lazy-fetch when accessing object not in the_repository' '\n \n \t# Sanity check that the file is still missing\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\tgrep \"[?]$FILE_HASH\" out &&\n+\ttest_grep \"[?]$FILE_HASH\" out &&\n \n \tgit -C full cat-file -s \"$FILE_HASH\" >expect &&\n \ttest-tool partial-clone object-info partial.git \"$FILE_HASH\" >actual &&\n@@ -688,7 +688,7 @@ test_expect_success 'lazy-fetch when accessing object not in the_repository' '\n \n \t# Sanity check that the file is now present\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\t! grep \"[?]$FILE_HASH\" out\n+\ttest_grep ! \"[?]$FILE_HASH\" out\n '\n \n test_expect_success 'push should not fetch new commit objects' '\n@@ -705,9 +705,9 @@ test_expect_success 'push should not fetch new commit objects' '\n \tCOMMIT=$(git -C server rev-parse server2) &&\n \n \ttest_must_fail git -C client push 2>err &&\n-\tgrep \"fetch first\" err &&\n+\ttest_grep \"fetch first\" err &&\n \tgit -C client rev-list --objects --missing=print \"$COMMIT\" >objects &&\n-\tgrep \"^[?]$COMMIT\" objects\n+\ttest_grep \"^[?]$COMMIT\" objects\n '\n \n test_expect_success 'setup for promisor.quiet tests' '\n@@ -728,7 +728,7 @@ test_expect_success TTY 'promisor.quiet=false shows progress messages' '\n \ttest_terminal git -C repo cat-file -p foo:foo.t 2>err &&\n \n \t# Ensure that progress messages are written\n-\tgrep \"Receiving objects\" err\n+\ttest_grep \"Receiving objects\" err\n '\n \n test_expect_success TTY 'promisor.quiet=true does not show progress messages' '\n@@ -739,7 +739,7 @@ test_expect_success TTY 'promisor.quiet=true does not show progress messages' '\n \ttest_terminal git -C repo cat-file -p foo:foo.t 2>err &&\n \n \t# Ensure that no progress messages are written\n-\t! grep \"Receiving objects\" err\n+\ttest_grep ! \"Receiving objects\" err\n '\n \n test_expect_success TTY 'promisor.quiet=unconfigured shows progress messages' '\n@@ -749,7 +749,7 @@ test_expect_success TTY 'promisor.quiet=unconfigured shows progress messages' '\n \ttest_terminal git -C repo cat-file -p foo:foo.t 2>err &&\n \n \t# Ensure that progress messages are written\n-\tgrep \"Receiving objects\" err\n+\ttest_grep \"Receiving objects\" err\n '\n \n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n@@ -776,7 +776,7 @@ test_expect_success 'fetching of missing objects from an HTTP server' '\n \ttest_line_count = 1 promisorlist &&\n \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n \tgit verify-pack --verbose \"$IDX\" >out &&\n-\tgrep \"$HASH\" out\n+\ttest_grep \"$HASH\" out\n '\n \n # DO NOT add non-httpd-specific tests here, because the last part of this\ndiff --git a/t/t0450-txt-doc-vs-help.sh b/t/t0450-txt-doc-vs-help.sh\nindex 822b0d55a5..e7ba967223 100755\n--- a/t/t0450-txt-doc-vs-help.sh\n+++ b/t/t0450-txt-doc-vs-help.sh\n@@ -87,7 +87,7 @@ do\n \t# -h output assertions\n \ttest_expect_success \"$builtin -h output has no \\t\" '\n \t\th2s=\"$(help_to_synopsis \"$builtin\")\" &&\n-\t\t! grep \"$HT\" \"$h2s\"\n+\t\ttest_grep ! \"$HT\" \"$h2s\"\n \t'\n \n \ttest_expect_success \"$builtin -h output has dashed labels\" '\ndiff --git a/t/t0500-progress-display.sh b/t/t0500-progress-display.sh\nindex d1a498a216..fe2232cce0 100755\n--- a/t/t0500-progress-display.sh\n+++ b/t/t0500-progress-display.sh\n@@ -320,8 +320,8 @@ test_expect_success 'progress generates traces' '\n \n \t# t0212/parse_events.perl intentionally omits regions and data.\n \ttest_region progress \"Working hard\" trace.event &&\n-\tgrep \"\\\"key\\\":\\\"total_objects\\\",\\\"value\\\":\\\"40\\\"\" trace.event &&\n-\tgrep \"\\\"key\\\":\\\"total_bytes\\\",\\\"value\\\":\\\"409600\\\"\" trace.event\n+\ttest_grep \"\\\"key\\\":\\\"total_objects\\\",\\\"value\\\":\\\"40\\\"\" trace.event &&\n+\ttest_grep \"\\\"key\\\":\\\"total_bytes\\\",\\\"value\\\":\\\"409600\\\"\" trace.event\n '\n \n test_expect_success 'progress generates traces: stop / start' '\n@@ -344,8 +344,8 @@ test_expect_success 'progress generates traces: start without stop' '\n \tLSAN_OPTIONS=detect_leaks=0 \\\n \ttest-tool progress \\\n \t\t<in 2>stderr &&\n-\tgrep region_enter.*progress trace-start.event &&\n-\t! grep region_leave.*progress trace-start.event\n+\ttest_grep region_enter.*progress trace-start.event &&\n+\ttest_grep ! region_leave.*progress trace-start.event\n '\n \n test_expect_success 'progress generates traces: stop without start' '\n@@ -355,8 +355,8 @@ test_expect_success 'progress generates traces: stop without start' '\n \n \tGIT_TRACE2_EVENT=\"$PWD/trace-stop.event\" test-tool progress \\\n \t\t<in 2>stderr &&\n-\t! grep region_enter.*progress trace-stop.event &&\n-\t! grep region_leave.*progress trace-stop.event\n+\ttest_grep ! region_enter.*progress trace-stop.event &&\n+\ttest_grep ! region_leave.*progress trace-stop.event\n '\n \n test_expect_success 'progress generates traces: start with active progress bar (no stops)' '\n@@ -369,9 +369,9 @@ test_expect_success 'progress generates traces: start with active progress bar (\n \tLSAN_OPTIONS=detect_leaks=0 \\\n \ttest-tool progress \\\n \t\t<in 2>stderr &&\n-\tgrep region_enter.*progress.*One trace-2start.event &&\n-\tgrep region_enter.*progress.*Two trace-2start.event &&\n-\t! grep region_leave trace-2start.event\n+\ttest_grep region_enter.*progress.*One trace-2start.event &&\n+\ttest_grep region_enter.*progress.*Two trace-2start.event &&\n+\ttest_grep ! region_leave trace-2start.event\n '\n \n test_done\ndiff --git a/t/t0610-reftable-basics.sh b/t/t0610-reftable-basics.sh\nindex e19e036898..2f4c5e2a1b 100755\n--- a/t/t0610-reftable-basics.sh\n+++ b/t/t0610-reftable-basics.sh\n@@ -776,11 +776,11 @@ test_expect_success 'reflog: can delete separate reflog entries' '\n \t\ttest_commit file3 &&\n \t\ttest_commit file4 &&\n \t\tgit reflog >actual &&\n-\t\tgrep file3 actual &&\n+\t\ttest_grep file3 actual &&\n \n \t\tgit reflog delete HEAD@{1} &&\n \t\tgit reflog >actual &&\n-\t\t! grep file3 actual\n+\t\ttest_grep ! file3 actual\n \t)\n '\n \n@@ -902,8 +902,8 @@ test_expect_success 'reflog: garbage collection deletes reflog entries' '\n \t\tdone &&\n \t\tgit reflog refs/heads/main >actual &&\n \t\ttest_line_count = 10 actual &&\n-\t\tgrep \"commit (initial): number 1\" actual &&\n-\t\tgrep \"commit: number 10\" actual &&\n+\t\ttest_grep \"commit (initial): number 1\" actual &&\n+\t\ttest_grep \"commit: number 10\" actual &&\n \n \t\tgit gc &&\n \t\tgit reflog refs/heads/main >actual &&\ndiff --git a/t/t1004-read-tree-m-u-wf.sh b/t/t1004-read-tree-m-u-wf.sh\nindex 11bf10424f..70ba3eea4d 100755\n--- a/t/t1004-read-tree-m-u-wf.sh\n+++ b/t/t1004-read-tree-m-u-wf.sh\n@@ -142,8 +142,8 @@ test_expect_success '3-way not overwriting local changes (our side)' '\n \n \techo >>file1 \"local changes\" &&\n \tread_tree_u_must_succeed -m -u branch-point side-a side-b &&\n-\tgrep \"new line to be kept\" file1 &&\n-\tgrep \"local changes\" file1\n+\ttest_grep \"new line to be kept\" file1 &&\n+\ttest_grep \"local changes\" file1\n \n '\n \n@@ -156,8 +156,8 @@ test_expect_success '3-way not overwriting local changes (their side)' '\n \n \techo >>file2 \"local changes\" &&\n \tread_tree_u_must_fail -m -u branch-point side-a side-b &&\n-\t! grep \"new line to be kept\" file2 &&\n-\tgrep \"local changes\" file2\n+\ttest_grep ! \"new line to be kept\" file2 &&\n+\ttest_grep \"local changes\" file2\n \n '\n \ndiff --git a/t/t1006-cat-file.sh b/t/t1006-cat-file.sh\nindex 8e2c52652c..762c77c351 100755\n--- a/t/t1006-cat-file.sh\n+++ b/t/t1006-cat-file.sh\n@@ -696,8 +696,8 @@ test_expect_success '%(deltabase) reports packed delta bases' '\n \tgit repack -ad &&\n \tgit cat-file --batch-check=\"%(deltabase)\" <blobs >actual &&\n \t{\n-\t\tgrep \"$(git rev-parse HEAD:foo)\" actual ||\n-\t\tgrep \"$(git rev-parse HEAD:foo-plus)\" actual\n+\t\ttest_grep \"$(git rev-parse HEAD:foo)\" actual ||\n+\t\ttest_grep \"$(git rev-parse HEAD:foo-plus)\" actual\n \t}\n '\n \n@@ -826,7 +826,7 @@ test_expect_success 'cat-file -t and -s on corrupt loose object' '\n \t\t# Swap the two to corrupt the repository\n \t\tmv -f \"$other_path\" \"$empty_path\" &&\n \t\ttest_must_fail git fsck 2>err.fsck &&\n-\t\tgrep \"hash-path mismatch\" err.fsck &&\n+\t\ttest_grep \"hash-path mismatch\" err.fsck &&\n \n \t\t# confirm that cat-file is reading the new swapped-in\n \t\t# blob...\n@@ -1318,37 +1318,37 @@ test_expect_success 'cat-file --batch-all-objects --batch-check ignores replace'\n test_expect_success 'batch-command empty command' '\n \techo \"\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*empty command in input.*\" err\n+\ttest_grep \"^fatal:.*empty command in input.*\" err\n '\n \n test_expect_success 'batch-command whitespace before command' '\n \techo \" info deadbeef\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*whitespace before command.*\" err\n+\ttest_grep \"^fatal:.*whitespace before command.*\" err\n '\n \n test_expect_success 'batch-command unknown command' '\n \techo unknown_command >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*unknown command.*\" err\n+\ttest_grep \"^fatal:.*unknown command.*\" err\n '\n \n test_expect_success 'batch-command missing arguments' '\n \techo \"info\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*info requires arguments.*\" err\n+\ttest_grep \"^fatal:.*info requires arguments.*\" err\n '\n \n test_expect_success 'batch-command flush with arguments' '\n \techo \"flush arg\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command --buffer <cmd 2>err &&\n-\tgrep \"^fatal:.*flush takes no arguments.*\" err\n+\ttest_grep \"^fatal:.*flush takes no arguments.*\" err\n '\n \n test_expect_success 'batch-command flush without --buffer' '\n \techo \"flush\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*flush is only for --buffer mode.*\" err\n+\ttest_grep \"^fatal:.*flush is only for --buffer mode.*\" err\n '\n \n perl_script='\ndiff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh\nindex de076293b6..4782a6246a 100755\n--- a/t/t1007-hash-object.sh\n+++ b/t/t1007-hash-object.sh\n@@ -202,7 +202,7 @@ done\n test_expect_success 'too-short tree' '\n \techo abc >malformed-tree &&\n \ttest_must_fail git hash-object -t tree malformed-tree 2>err &&\n-\tgrep \"too-short tree object\" err\n+\ttest_grep \"too-short tree object\" err\n '\n \n test_expect_success PERL_TEST_HELPERS 'malformed mode in tree' '\n@@ -210,7 +210,7 @@ test_expect_success PERL_TEST_HELPERS 'malformed mode in tree' '\n \tbin_oid=$(echo $hex_oid | hex2oct) &&\n \tprintf \"9100644 \\0$bin_oid\" >tree-with-malformed-mode &&\n \ttest_must_fail git hash-object -t tree tree-with-malformed-mode 2>err &&\n-\tgrep \"malformed mode in tree entry\" err\n+\ttest_grep \"malformed mode in tree entry\" err\n '\n \n test_expect_success PERL_TEST_HELPERS 'empty filename in tree' '\n@@ -218,7 +218,7 @@ test_expect_success PERL_TEST_HELPERS 'empty filename in tree' '\n \tbin_oid=$(echo $hex_oid | hex2oct) &&\n \tprintf \"100644 \\0$bin_oid\" >tree-with-empty-filename &&\n \ttest_must_fail git hash-object -t tree tree-with-empty-filename 2>err &&\n-\tgrep \"empty filename in tree entry\" err\n+\ttest_grep \"empty filename in tree entry\" err\n '\n \n test_expect_success PERL_TEST_HELPERS 'duplicate filename in tree' '\n@@ -229,7 +229,7 @@ test_expect_success PERL_TEST_HELPERS 'duplicate filename in tree' '\n \t\tprintf \"100644 file\\0$bin_oid\"\n \t} >tree-with-duplicate-filename &&\n \ttest_must_fail git hash-object -t tree tree-with-duplicate-filename 2>err &&\n-\tgrep \"duplicateEntries\" err\n+\ttest_grep \"duplicateEntries\" err\n '\n \n test_expect_success 'corrupt commit' '\ndiff --git a/t/t1011-read-tree-sparse-checkout.sh b/t/t1011-read-tree-sparse-checkout.sh\nindex 742f0fa909..f6a5dbe519 100755\n--- a/t/t1011-read-tree-sparse-checkout.sh\n+++ b/t/t1011-read-tree-sparse-checkout.sh\n@@ -196,7 +196,7 @@ test_expect_success 'read-tree will not throw away dirty changes, non-sparse' '\n \techo dirty >init.t &&\n \tread_tree_u_must_fail -m -u HEAD^ &&\n \ttest_path_is_file init.t &&\n-\tgrep -q dirty init.t\n+\ttest_grep dirty init.t\n '\n \n test_expect_success 'read-tree will not throw away dirty changes, sparse' '\n@@ -207,7 +207,7 @@ test_expect_success 'read-tree will not throw away dirty changes, sparse' '\n \techo sub/added >.git/info/sparse-checkout &&\n \tread_tree_u_must_fail -m -u HEAD^ &&\n \ttest_path_is_file init.t &&\n-\tgrep -q dirty init.t\n+\ttest_grep dirty init.t\n '\n \n test_expect_success 'read-tree updates worktree, dirty case' '\n@@ -215,7 +215,7 @@ test_expect_success 'read-tree updates worktree, dirty case' '\n \tgit checkout -f top &&\n \techo dirty >init.t &&\n \tread_tree_u_must_fail -m -u HEAD^ &&\n-\tgrep -q dirty init.t &&\n+\ttest_grep dirty init.t &&\n \trm init.t\n '\n \n@@ -224,7 +224,7 @@ test_expect_success 'read-tree removes worktree, dirty case' '\n \tgit checkout -f top &&\n \techo dirty >added &&\n \tread_tree_u_must_succeed -m -u HEAD^ &&\n-\tgrep -q dirty added\n+\ttest_grep dirty added\n '\n \n test_expect_success 'read-tree adds to worktree, absent case' '\n@@ -240,7 +240,7 @@ test_expect_success 'read-tree adds to worktree, dirty case' '\n \tmkdir sub &&\n \techo dirty >sub/added &&\n \tread_tree_u_must_succeed -u -m HEAD^ &&\n-\tgrep -q dirty sub/added\n+\ttest_grep dirty sub/added\n '\n \n test_expect_success 'index removal and worktree narrowing at the same time' '\ndiff --git a/t/t1050-large.sh b/t/t1050-large.sh\nindex 7d40d08521..d295c265c7 100755\n--- a/t/t1050-large.sh\n+++ b/t/t1050-large.sh\n@@ -8,7 +8,7 @@ test_description='adding and checking out large blobs'\n test_expect_success 'core.bigFileThreshold must be non-negative' '\n \t: >input &&\n \ttest_must_fail git -c core.bigFileThreshold=-1 hash-object input >out 2>err &&\n-\tgrep \"bad numeric config value\" err &&\n+\ttest_grep \"bad numeric config value\" err &&\n \ttest_must_be_empty out\n '\n \n@@ -148,12 +148,12 @@ test_expect_success 'diff --stat' '\n \n test_expect_success 'diff' '\n \tgit diff HEAD^ HEAD >actual &&\n-\tgrep \"Binary files.*differ\" actual\n+\ttest_grep \"Binary files.*differ\" actual\n '\n \n test_expect_success 'diff --cached' '\n \tgit diff --cached HEAD^ >actual &&\n-\tgrep \"Binary files.*differ\" actual\n+\ttest_grep \"Binary files.*differ\" actual\n '\n \n test_expect_success 'hash-object' '\ndiff --git a/t/t1091-sparse-checkout-builtin.sh b/t/t1091-sparse-checkout-builtin.sh\nindex cd0aed9975..74b1761e0c 100755\n--- a/t/t1091-sparse-checkout-builtin.sh\n+++ b/t/t1091-sparse-checkout-builtin.sh\n@@ -129,7 +129,7 @@ test_expect_success 'switching to cone mode with non-cone mode patterns' '\n \t\tgit sparse-checkout add dir &&\n \t\tgit config --worktree core.sparseCheckoutCone true &&\n \t\ttest_must_fail git sparse-checkout add dir 2>err &&\n-\t\tgrep \"existing sparse-checkout patterns do not use cone mode\" err\n+\t\ttest_grep \"existing sparse-checkout patterns do not use cone mode\" err\n \t)\n '\n \n@@ -803,7 +803,7 @@ test_expect_success 'cone mode clears ignored subdirectories' '\n \t# When an untracked file is in the way, all untracked files\n \t# (even ignored files) are preserved.\n \tgit -C repo sparse-checkout set folder1 2>err &&\n-\tgrep \"contains untracked files\" err &&\n+\ttest_grep \"contains untracked files\" err &&\n \ttest_path_is_file repo/deep/deeper2/ignored.o &&\n \ttest_path_is_file repo/deep/deeper2/untracked &&\n \n@@ -882,8 +882,8 @@ test_expect_success 'malformed cone-mode patterns' '\n \t# of using the cone-mode translation to a set of directories.\n \tgit -C repo sparse-checkout list >actual 2>err &&\n \ttest_cmp repo/.git/info/sparse-checkout actual &&\n-\tgrep \"warning: your sparse-checkout file may have issues: pattern .* is repeated\" err &&\n-\tgrep \"warning: disabling cone pattern matching\" err\n+\ttest_grep \"warning: your sparse-checkout file may have issues: pattern .* is repeated\" err &&\n+\ttest_grep \"warning: disabling cone pattern matching\" err\n '\n \n test_expect_success 'set from subdir pays attention to prefix' '\n@@ -917,34 +917,34 @@ test_expect_success 'set from subdir in non-cone mode throws an error' '\n \tgit -C repo sparse-checkout disable &&\n \ttest_must_fail git -C repo/deep sparse-checkout set --no-cone deeper2 ../folder1 2>error &&\n \n-\tgrep \"run from the toplevel directory in non-cone mode\" error\n+\ttest_grep \"run from the toplevel directory in non-cone mode\" error\n '\n \n test_expect_success 'set from subdir in non-cone mode throws an error' '\n \tgit -C repo sparse-checkout set --no-cone deep/deeper2 &&\n \ttest_must_fail git -C repo/deep sparse-checkout add deeper1/deepest ../folder1 2>error &&\n \n-\tgrep \"run from the toplevel directory in non-cone mode\" error\n+\ttest_grep \"run from the toplevel directory in non-cone mode\" error\n '\n \n test_expect_success 'by default, cone mode will error out when passed files' '\n \tgit -C repo sparse-checkout reapply --cone &&\n \ttest_must_fail git -C repo sparse-checkout add .gitignore 2>error &&\n \n-\tgrep \".gitignore.*is not a directory\" error\n+\ttest_grep \".gitignore.*is not a directory\" error\n '\n \n test_expect_success 'error on mistyped command line options' '\n \ttest_must_fail git -C repo sparse-checkout add --sikp-checks .gitignore 2>error &&\n \n-\tgrep \"unknown option.*sikp-checks\" error\n+\ttest_grep \"unknown option.*sikp-checks\" error\n '\n \n test_expect_success 'by default, non-cone mode will warn on individual files' '\n \tgit -C repo sparse-checkout reapply --no-cone &&\n \tgit -C repo sparse-checkout add .gitignore 2>warning &&\n \n-\tgrep \"pass a leading slash before paths.*if you want a single file\" warning\n+\ttest_grep \"pass a leading slash before paths.*if you want a single file\" warning\n '\n \n test_expect_success 'setup bare repo' '\n@@ -1108,11 +1108,11 @@ test_expect_success 'clean' '\n \ttouch repo/folder1/extra/inside/file &&\n \n \ttest_must_fail git -C repo sparse-checkout clean 2>err &&\n-\tgrep \"refusing to clean\" err &&\n+\ttest_grep \"refusing to clean\" err &&\n \n \tgit -C repo config clean.requireForce true &&\n \ttest_must_fail git -C repo sparse-checkout clean 2>err &&\n-\tgrep \"refusing to clean\" err &&\n+\ttest_grep \"refusing to clean\" err &&\n \n \tcat >expect <<-\\EOF &&\n \tWould remove deep/deeper2/\n@@ -1255,7 +1255,7 @@ test_expect_success 'sparse-checkout operations with merge conflicts' '\n \t\ttest_must_fail git merge -m \"will-conflict\" right &&\n \n \t\ttest_must_fail git sparse-checkout clean -f 2>err &&\n-\t\tgrep \"failed to convert index to a sparse index\" err &&\n+\t\ttest_grep \"failed to convert index to a sparse index\" err &&\n \n \t\techo merged >folder1/even/more/dirs/file &&\n \t\tgit add --sparse folder1 &&\ndiff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh\nindex d98cb4ac11..b5b5e615e7 100755\n--- a/t/t1092-sparse-checkout-compatibility.sh\n+++ b/t/t1092-sparse-checkout-compatibility.sh\n@@ -454,10 +454,10 @@ test_expect_success 'add outside sparse cone' '\n \trun_on_sparse ../edit-contents folder1/a &&\n \trun_on_sparse ../edit-contents folder1/newfile &&\n \ttest_sparse_match test_must_fail git add folder1/a &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/a &&\n \ttest_sparse_match test_must_fail git add folder1/newfile &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/newfile\n '\n \n@@ -509,13 +509,13 @@ test_expect_success 'status/add: outside sparse cone' '\n \n \t# Adding the path outside of the sparse-checkout cone should fail.\n \ttest_sparse_match test_must_fail git add folder1/a &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/a &&\n \ttest_all_match git add --refresh folder1/a &&\n \ttest_must_be_empty sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/a &&\n \ttest_sparse_match test_must_fail git add folder1/new &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/new &&\n \ttest_sparse_match git add --sparse folder1/a &&\n \ttest_sparse_match git add --sparse folder1/new &&\n@@ -661,8 +661,8 @@ test_expect_success 'checkout and reset (mixed)' '\n \t# in sparse-checkout or sparse-index.\n \tgit -C full-checkout reset update-folder1 >full-checkout-out &&\n \ttest_sparse_match git reset update-folder1 &&\n-\tgrep \"M\tfolder1/a\" full-checkout-out &&\n-\t! grep \"M\tfolder1/a\" sparse-checkout-out &&\n+\ttest_grep \"M\tfolder1/a\" full-checkout-out &&\n+\ttest_grep ! \"M\tfolder1/a\" sparse-checkout-out &&\n \trun_on_sparse test_path_is_missing folder1\n '\n \n@@ -880,8 +880,8 @@ test_expect_success 'update-index with directories' '\n \t# update-index will exit silently when provided with a directory name\n \t# containing a trailing slash\n \ttest_all_match git update-index deep/ folder1/ &&\n-\tgrep \"Ignoring path deep/\" sparse-checkout-err &&\n-\tgrep \"Ignoring path folder1/\" sparse-checkout-err &&\n+\ttest_grep \"Ignoring path deep/\" sparse-checkout-err &&\n+\ttest_grep \"Ignoring path folder1/\" sparse-checkout-err &&\n \n \t# When update-index is given a directory name WITHOUT a trailing slash, it will\n \t# behave in different ways depending on the status of the directory on disk:\n@@ -1067,7 +1067,7 @@ test_expect_success 'merge with conflict outside cone' '\n \n \t# 2. Add the file with conflict markers\n \ttest_sparse_match test_must_fail git add folder1/a &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/a &&\n \ttest_all_match git add --sparse folder1/a &&\n \ttest_all_match git status --porcelain=v2 &&\n@@ -1076,7 +1076,7 @@ test_expect_success 'merge with conflict outside cone' '\n \t#    accept conflict markers as resolved content.\n \trun_on_all mv folder2/a folder2/z &&\n \ttest_sparse_match test_must_fail git add folder2 &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder2/z &&\n \ttest_all_match git add --sparse folder2 &&\n \ttest_all_match git status --porcelain=v2 &&\n@@ -1107,7 +1107,7 @@ test_expect_success 'cherry-pick/rebase with conflict outside cone' '\n \t\t# SKIP_WORKTREE bit from the index entry for folder1/a, we should\n \t\t# warn that this is a problematic add.\n \t\ttest_sparse_match test_must_fail git add folder1/a &&\n-\t\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\t\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \t\ttest_sparse_unstaged folder1/a &&\n \t\ttest_all_match git add --sparse folder1/a &&\n \t\ttest_all_match git status --porcelain=v2 &&\n@@ -1119,7 +1119,7 @@ test_expect_success 'cherry-pick/rebase with conflict outside cone' '\n \t\t# existing index entry with the SKIP_WORKTREE bit cleared.\n \t\trun_on_all mv folder2/a folder2/z &&\n \t\ttest_sparse_match test_must_fail git add folder2 &&\n-\t\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\t\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \t\ttest_sparse_unstaged folder2/z &&\n \t\ttest_all_match git add --sparse folder2 &&\n \t\ttest_all_match git status --porcelain=v2 &&\n@@ -1266,7 +1266,7 @@ test_expect_success 'checkout-index with folders' '\n \trun_on_all test_must_fail git checkout-index -f -- folder1/ &&\n \ttest_cmp full-checkout-err sparse-checkout-err &&\n \t! test_cmp full-checkout-err sparse-index-err &&\n-\tgrep \"is a sparse directory\" sparse-index-err\n+\ttest_grep \"is a sparse directory\" sparse-index-err\n '\n \n test_expect_success 'checkout-index --all' '\n@@ -1374,8 +1374,8 @@ test_expect_success 'submodule handling' '\n \t# having a submodule prevents \"modules\" from collapse\n \ttest_sparse_match git sparse-checkout set deep/deeper1 &&\n \tgit -C sparse-index ls-files --sparse --stage >cache &&\n-\tgrep \"100644 .*\tmodules/a\" cache &&\n-\tgrep \"160000 $(git -C initial-repo rev-parse HEAD) 0\tmodules/sub\" cache\n+\ttest_grep \"100644 .*\tmodules/a\" cache &&\n+\ttest_grep \"160000 $(git -C initial-repo rev-parse HEAD) 0\tmodules/sub\" cache\n '\n \n test_expect_success 'git apply functionality' '\n@@ -1392,7 +1392,7 @@ test_expect_success 'git apply functionality' '\n \n \t# Apply a patch to a file outside the sparse definition\n \ttest_sparse_match test_must_fail git apply ../patch-outside &&\n-\tgrep \"No such file or directory\" sparse-checkout-err &&\n+\ttest_grep \"No such file or directory\" sparse-checkout-err &&\n \n \t# But it works with --index and --cached\n \ttest_all_match git apply --index --stat ../patch-outside &&\n@@ -2013,9 +2013,9 @@ test_expect_success 'mv directory from out-of-cone to in-cone' '\n \ttest_all_match git status --porcelain=v2 &&\n \ttest_sparse_match git ls-files -t &&\n \tgit -C sparse-checkout ls-files -t >actual &&\n-\tgrep -e \"H deep/folder1/0/0/0\" actual &&\n-\tgrep -e \"H deep/folder1/0/1\" actual &&\n-\tgrep -e \"H deep/folder1/a\" actual &&\n+\ttest_grep -e \"H deep/folder1/0/0/0\" actual &&\n+\ttest_grep -e \"H deep/folder1/0/1\" actual &&\n+\ttest_grep -e \"H deep/folder1/a\" actual &&\n \n \ttest_all_match git reset --hard &&\n \n@@ -2025,8 +2025,8 @@ test_expect_success 'mv directory from out-of-cone to in-cone' '\n \ttest_sparse_match git status --porcelain=v2 &&\n \ttest_sparse_match git ls-files -t &&\n \tgit -C sparse-checkout ls-files -t >actual &&\n-\tgrep -e \"H deep/0/0/0\" actual &&\n-\tgrep -e \"H deep/0/1\" actual\n+\ttest_grep -e \"H deep/0/0/0\" actual &&\n+\ttest_grep -e \"H deep/0/1\" actual\n '\n \n test_expect_success 'rm pathspec inside sparse definition' '\n@@ -2517,7 +2517,7 @@ test_expect_success 'advice.sparseIndexExpanded' '\n \tmkdir -p sparse-index/deep/deeper2/deepest &&\n \ttouch sparse-index/deep/deeper2/deepest/bogus &&\n \tgit -C sparse-index status 2>err &&\n-\tgrep \"The sparse index is expanding to a full index\" err &&\n+\ttest_grep \"The sparse index is expanding to a full index\" err &&\n \n \tgit -C sparse-index sparse-checkout disable 2>err &&\n \ttest_line_count = 0 err\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 11fc976f3a..b337e25d92 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -800,7 +800,7 @@ test_expect_success 'renaming a section with an overly-long line' '\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n \ttest_must_fail git config ${mode_prefix}rename-section -f y a xyz 2>err &&\n-\tgrep \"refusing to work with overly long line in .y. on line 2\" err\n+\ttest_grep \"refusing to work with overly long line in .y. on line 2\" err\n '\n \n cat >> .git/config << EOF\n@@ -1615,9 +1615,9 @@ test_expect_success 'git --config-env=key=envvar support' '\n \n test_expect_success 'git --config-env with missing value' '\n \ttest_must_fail env ENVVAR=value git --config-env 2>error &&\n-\tgrep \"no config key given for --config-env\" error &&\n+\ttest_grep \"no config key given for --config-env\" error &&\n \ttest_must_fail env ENVVAR=value git --config-env config core.name 2>error &&\n-\tgrep \"invalid config format: config\" error\n+\ttest_grep \"invalid config format: config\" error\n '\n \n test_expect_success 'git --config-env fails with invalid parameters' '\n@@ -2048,7 +2048,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \tkey = true\n \tEOF\n \tgit config ${mode_unset} two.key &&\n-\t! grep two .git/config &&\n+\ttest_grep ! two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n \t[one]\n@@ -2068,7 +2068,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \tQkey = true\n \tEOF\n \tgit config ${mode_unset} two.key &&\n-\tgrep two .git/config &&\n+\ttest_grep two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n \t[one]\n@@ -2599,7 +2599,7 @@ test_expect_success '--type rejects unknown specifiers' '\n \n test_expect_success '--type=int requires at least one digit' '\n \ttest_must_fail git config --type int --default m some.key >out 2>error &&\n-\tgrep \"bad numeric config value\" error &&\n+\ttest_grep \"bad numeric config value\" error &&\n \ttest_must_be_empty out\n '\n \n@@ -2911,12 +2911,12 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such\n \n \t# test with any Git command\n \ttest_must_fail git -C hasremoteurlTest status 2>err &&\n-\tgrep \"fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url\" err\n+\ttest_grep \"fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url\" err\n '\n \n test_expect_success 'negated mode causes failure' '\n \ttest_must_fail git config --no-get 2>err &&\n-\tgrep \"unknown option \\`no-get${SQ}\" err\n+\ttest_grep \"unknown option \\`no-get${SQ}\" err\n '\n \n test_expect_success 'specifying multiple modes causes failure' '\ndiff --git a/t/t1305-config-include.sh b/t/t1305-config-include.sh\nindex f3892578e4..f6115269f9 100755\n--- a/t/t1305-config-include.sh\n+++ b/t/t1305-config-include.sh\n@@ -353,7 +353,7 @@ test_expect_success 'include cycles are detected' '\n \tgit -C cycle --git-dir=. config include.path cycle &&\n \tgit config -f cycle/cycle include.path config &&\n \ttest_must_fail git -C cycle --git-dir=. config --get-all test.value 2>stderr &&\n-\tgrep \"exceeded maximum include depth\" stderr\n+\ttest_grep \"exceeded maximum include depth\" stderr\n '\n \n test_expect_success 'onbranch with unborn branch' '\ndiff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\nindex e0e49053f0..de95161a83 100755\n--- a/t/t1308-config-set.sh\n+++ b/t/t1308-config-set.sh\n@@ -180,7 +180,7 @@ test_expect_success 'find integer if value is non parse-able' '\n \n test_expect_success 'non parse-able integer value during iteration' '\n \tcheck_config expect_code 128 git_config_int lamb.head 2>result &&\n-\tgrep \"fatal: bad numeric config value .* in file \\.git/config\" result\n+\ttest_grep \"fatal: bad numeric config value .* in file \\.git/config\" result\n '\n \n test_expect_success 'find bool value for the entered key' '\n@@ -302,7 +302,7 @@ test_expect_success 'proper error on directory \"files\"' '\n \techo \"Error (-1) reading configuration file a-directory.\" >expect &&\n \tmkdir a-directory &&\n \ttest_expect_code 2 test-tool config configset_get_value foo.bar a-directory 2>output &&\n-\tgrep \"^warning:\" output &&\n+\ttest_grep \"^warning:\" output &&\n \tgrep \"^Error\" output >actual &&\n \ttest_cmp expect actual\n '\n@@ -312,7 +312,7 @@ test_expect_success POSIXPERM,SANITY 'proper error on non-accessible files' '\n \ttest_when_finished \"chmod +r .git/config\" &&\n \techo \"Error (-1) reading configuration file .git/config.\" >expect &&\n \ttest_expect_code 2 test-tool config configset_get_value foo.bar .git/config 2>output &&\n-\tgrep \"^warning:\" output &&\n+\ttest_grep \"^warning:\" output &&\n \tgrep \"^Error\" output >actual &&\n \ttest_cmp expect actual\n '\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex 1015f335e3..7c89071662 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -92,7 +92,7 @@ test_expect_success \"deleting current branch adds message to HEAD's log\" '\n \tgit update-ref -m delete-$m -d $m &&\n \ttest_must_fail git show-ref --verify -q $m &&\n \ttest-tool ref-store main for-each-reflog-ent HEAD >actual &&\n-\tgrep \"delete-$m$\" actual\n+\ttest_grep \"delete-$m$\" actual\n '\n \n test_expect_success \"deleting by HEAD adds message to HEAD's log\" '\n@@ -102,7 +102,7 @@ test_expect_success \"deleting by HEAD adds message to HEAD's log\" '\n \tgit update-ref -m delete-by-head -d HEAD &&\n \ttest_must_fail git show-ref --verify -q $m &&\n \ttest-tool ref-store main for-each-reflog-ent HEAD >actual &&\n-\tgrep \"delete-by-head$\" actual\n+\ttest_grep \"delete-by-head$\" actual\n '\n \n test_expect_success 'update-ref does not create reflogs by default' '\n@@ -192,7 +192,7 @@ test_expect_success \"move $m (by HEAD)\" '\n test_expect_success \"delete $m (by HEAD) should remove both packed and loose $m\" '\n \ttest_when_finished \"git update-ref -d $m\" &&\n \tgit update-ref -d HEAD $B &&\n-\t! grep \"$m\" .git/packed-refs &&\n+\t! grep \"$m\" .git/packed-refs && # lint-ok: file may not exist (reftable)\n \ttest_must_fail git show-ref --verify -q $m\n '\n \n@@ -575,103 +575,103 @@ test_expect_success 'stdin works with no input' '\n test_expect_success 'stdin fails on empty line' '\n \techo \"\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: empty command in input\" err\n+\ttest_grep \"fatal: empty command in input\" err\n '\n \n test_expect_success 'stdin fails on only whitespace' '\n \techo \" \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command:  \" err\n+\ttest_grep \"fatal: whitespace before command:  \" err\n '\n \n test_expect_success 'stdin fails on leading whitespace' '\n \techo \" create $a $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command:  create $a $m\" err\n+\ttest_grep \"fatal: whitespace before command:  create $a $m\" err\n '\n \n test_expect_success 'stdin fails on unknown command' '\n \techo \"unknown $a\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: unknown $a\" err\n+\ttest_grep \"fatal: unknown command: unknown $a\" err\n '\n \n test_expect_success 'stdin fails on unbalanced quotes' '\n \techo \"create $a \\\"main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: badly quoted argument: \\\\\\\"main\" err\n+\ttest_grep \"fatal: badly quoted argument: \\\\\\\"main\" err\n '\n \n test_expect_success 'stdin fails on invalid escape' '\n \techo \"create $a \\\"ma\\zn\\\"\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: badly quoted argument: \\\\\\\"ma\\\\\\\\zn\\\\\\\"\" err\n+\ttest_grep \"fatal: badly quoted argument: \\\\\\\"ma\\\\\\\\zn\\\\\\\"\" err\n '\n \n test_expect_success 'stdin fails on junk after quoted argument' '\n \techo \"create \\\"$a\\\"main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: unexpected character after quoted argument: \\\\\\\"$a\\\\\\\"main\" err\n+\ttest_grep \"fatal: unexpected character after quoted argument: \\\\\\\"$a\\\\\\\"main\" err\n '\n \n test_expect_success 'stdin fails create with no ref' '\n \techo \"create \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create: missing <ref>\" err\n+\ttest_grep \"fatal: create: missing <ref>\" err\n '\n \n test_expect_success 'stdin fails create with no new value' '\n \techo \"create $a\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $a: missing <new-oid>\" err\n+\ttest_grep \"fatal: create $a: missing <new-oid>\" err\n '\n \n test_expect_success 'stdin fails create with too many arguments' '\n \techo \"create $a $m $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $a: extra input:  $m\" err\n+\ttest_grep \"fatal: create $a: extra input:  $m\" err\n '\n \n test_expect_success 'stdin fails update with no ref' '\n \techo \"update \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: update: missing <ref>\" err\n+\ttest_grep \"fatal: update: missing <ref>\" err\n '\n \n test_expect_success 'stdin fails update with no new value' '\n \techo \"update $a\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: missing <new-oid>\" err\n+\ttest_grep \"fatal: update $a: missing <new-oid>\" err\n '\n \n test_expect_success 'stdin fails update with too many arguments' '\n \techo \"update $a $m $m $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: extra input:  $m\" err\n+\ttest_grep \"fatal: update $a: extra input:  $m\" err\n '\n \n test_expect_success 'stdin fails delete with no ref' '\n \techo \"delete \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete: missing <ref>\" err\n+\ttest_grep \"fatal: delete: missing <ref>\" err\n '\n \n test_expect_success 'stdin fails delete with too many arguments' '\n \techo \"delete $a $m $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete $a: extra input:  $m\" err\n+\ttest_grep \"fatal: delete $a: extra input:  $m\" err\n '\n \n test_expect_success 'stdin fails verify with too many arguments' '\n \techo \"verify $a $m $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: verify $a: extra input:  $m\" err\n+\ttest_grep \"fatal: verify $a: extra input:  $m\" err\n '\n \n test_expect_success 'stdin fails option with unknown name' '\n \techo \"option unknown\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: option unknown: unknown\" err\n+\ttest_grep \"fatal: option unknown: unknown\" err\n '\n \n test_expect_success 'stdin fails with duplicate refs' '\n@@ -759,28 +759,28 @@ test_expect_success 'stdin create ref works with path with space to blob' '\n test_expect_success 'stdin update ref fails with wrong old value' '\n \techo \"update $c $m $m~1\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin update ref fails with bad old value' '\n \techo \"update $c $m does-not-exist\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $c: invalid <old-oid>: does-not-exist\" err &&\n+\ttest_grep \"fatal: update $c: invalid <old-oid>: does-not-exist\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin create ref fails with bad new value' '\n \techo \"create $c does-not-exist\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $c: invalid <new-oid>: does-not-exist\" err &&\n+\ttest_grep \"fatal: create $c: invalid <new-oid>: does-not-exist\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin create ref fails with zero new value' '\n \techo \"create $c \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $c: zero <new-oid>\" err &&\n+\ttest_grep \"fatal: create $c: zero <new-oid>\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n@@ -795,7 +795,7 @@ test_expect_success 'stdin update ref works with right old value' '\n test_expect_success 'stdin delete ref fails with wrong old value' '\n \techo \"delete $a $m~1\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$a'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$a'\"'\"'\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual\n@@ -804,7 +804,7 @@ test_expect_success 'stdin delete ref fails with wrong old value' '\n test_expect_success 'stdin delete ref fails with zero old value' '\n \techo \"delete $a \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete $a: zero <old-oid>\" err &&\n+\ttest_grep \"fatal: delete $a: zero <old-oid>\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual\n@@ -965,7 +965,7 @@ test_expect_success 'stdin update refs fails with wrong old value' '\n \tupdate $c  ''\n \tEOF\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual &&\n@@ -998,123 +998,123 @@ test_expect_success 'stdin -z works on empty input' '\n test_expect_success 'stdin -z fails on empty line' '\n \techo \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command: \" err\n+\ttest_grep \"fatal: whitespace before command: \" err\n '\n \n test_expect_success 'stdin -z fails on empty command' '\n \tprintf $F \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: empty command in input\" err\n+\ttest_grep \"fatal: empty command in input\" err\n '\n \n test_expect_success 'stdin -z fails on only whitespace' '\n \tprintf $F \" \" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command:  \" err\n+\ttest_grep \"fatal: whitespace before command:  \" err\n '\n \n test_expect_success 'stdin -z fails on leading whitespace' '\n \tprintf $F \" create $a\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command:  create $a\" err\n+\ttest_grep \"fatal: whitespace before command:  create $a\" err\n '\n \n test_expect_success 'stdin -z fails on unknown command' '\n \tprintf $F \"unknown $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: unknown $a\" err\n+\ttest_grep \"fatal: unknown command: unknown $a\" err\n '\n \n test_expect_success 'stdin -z fails create with no ref' '\n \tprintf $F \"create \" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: create: missing <ref>\" err\n+\ttest_grep \"fatal: create: missing <ref>\" err\n '\n \n test_expect_success 'stdin -z fails create with no new value' '\n \tprintf $F \"create $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $a: unexpected end of input when reading <new-oid>\" err\n+\ttest_grep \"fatal: create $a: unexpected end of input when reading <new-oid>\" err\n '\n \n test_expect_success 'stdin -z fails create with too many arguments' '\n \tprintf $F \"create $a\" \"$m\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: $m\" err\n+\ttest_grep \"fatal: unknown command: $m\" err\n '\n \n test_expect_success 'stdin -z fails update with no ref' '\n \tprintf $F \"update \" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update: missing <ref>\" err\n+\ttest_grep \"fatal: update: missing <ref>\" err\n '\n \n test_expect_success 'stdin -z fails update with too few args' '\n \tprintf $F \"update $a\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: unexpected end of input when reading <old-oid>\" err\n+\ttest_grep \"fatal: update $a: unexpected end of input when reading <old-oid>\" err\n '\n \n test_expect_success 'stdin -z emits warning with empty new value' '\n \tgit update-ref $a $m &&\n \tprintf $F \"update $a\" \"\" \"\" >stdin &&\n \tgit update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"warning: update $a: missing <new-oid>, treating as zero\" err &&\n+\ttest_grep \"warning: update $a: missing <new-oid>, treating as zero\" err &&\n \ttest_must_fail git rev-parse --verify -q $a\n '\n \n test_expect_success 'stdin -z fails update with no new value' '\n \tprintf $F \"update $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: unexpected end of input when reading <new-oid>\" err\n+\ttest_grep \"fatal: update $a: unexpected end of input when reading <new-oid>\" err\n '\n \n test_expect_success 'stdin -z fails update with no old value' '\n \tprintf $F \"update $a\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: unexpected end of input when reading <old-oid>\" err\n+\ttest_grep \"fatal: update $a: unexpected end of input when reading <old-oid>\" err\n '\n \n test_expect_success 'stdin -z fails update with too many arguments' '\n \tprintf $F \"update $a\" \"$m\" \"$m\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: $m\" err\n+\ttest_grep \"fatal: unknown command: $m\" err\n '\n \n test_expect_success 'stdin -z fails delete with no ref' '\n \tprintf $F \"delete \" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete: missing <ref>\" err\n+\ttest_grep \"fatal: delete: missing <ref>\" err\n '\n \n test_expect_success 'stdin -z fails delete with no old value' '\n \tprintf $F \"delete $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete $a: unexpected end of input when reading <old-oid>\" err\n+\ttest_grep \"fatal: delete $a: unexpected end of input when reading <old-oid>\" err\n '\n \n test_expect_success 'stdin -z fails delete with too many arguments' '\n \tprintf $F \"delete $a\" \"$m\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: $m\" err\n+\ttest_grep \"fatal: unknown command: $m\" err\n '\n \n test_expect_success 'stdin -z fails verify with too many arguments' '\n \tprintf $F \"verify $a\" \"$m\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: $m\" err\n+\ttest_grep \"fatal: unknown command: $m\" err\n '\n \n test_expect_success 'stdin -z fails verify with no old value' '\n \tprintf $F \"verify $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: verify $a: unexpected end of input when reading <old-oid>\" err\n+\ttest_grep \"fatal: verify $a: unexpected end of input when reading <old-oid>\" err\n '\n \n test_expect_success 'stdin -z fails option with unknown name' '\n \tprintf $F \"option unknown\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: option unknown: unknown\" err\n+\ttest_grep \"fatal: option unknown: unknown\" err\n '\n \n test_expect_success 'stdin -z fails with duplicate refs' '\n@@ -1160,14 +1160,14 @@ test_expect_success 'stdin -z create ref works with path with space to blob' '\n test_expect_success 'stdin -z update ref fails with wrong old value' '\n \tprintf $F \"update $c\" \"$m\" \"$m~1\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin -z update ref fails with bad old value' '\n \tprintf $F \"update $c\" \"$m\" \"does-not-exist\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $c: invalid <old-oid>: does-not-exist\" err &&\n+\ttest_grep \"fatal: update $c: invalid <old-oid>: does-not-exist\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n@@ -1176,7 +1176,7 @@ test_expect_success 'stdin -z create ref fails when ref exists' '\n \tgit rev-parse \"$c\" >expect &&\n \tprintf $F \"create $c\" \"$m~1\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \tgit rev-parse \"$c\" >actual &&\n \ttest_cmp expect actual\n '\n@@ -1185,28 +1185,28 @@ test_expect_success 'stdin -z create ref fails with bad new value' '\n \tgit update-ref -d \"$c\" &&\n \tprintf $F \"create $c\" \"does-not-exist\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $c: invalid <new-oid>: does-not-exist\" err &&\n+\ttest_grep \"fatal: create $c: invalid <new-oid>: does-not-exist\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin -z create ref fails with empty new value' '\n \tprintf $F \"create $c\" \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $c: missing <new-oid>\" err &&\n+\ttest_grep \"fatal: create $c: missing <new-oid>\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin -z create ref fails with non commit object' '\n \tprintf $F \"create $c\" \"$(test_oid 001)\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: trying to write ref ${SQ}$c${SQ} with nonexistent object\" err &&\n+\ttest_grep \"fatal: trying to write ref ${SQ}$c${SQ} with nonexistent object\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin -z update ref fails with non commit object' '\n \tprintf $F \"update $b\" \"$(test_oid 001)\" \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: trying to write ref ${SQ}$b${SQ} with nonexistent object\" err &&\n+\ttest_grep \"fatal: trying to write ref ${SQ}$b${SQ} with nonexistent object\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n@@ -1221,7 +1221,7 @@ test_expect_success 'stdin -z update ref works with right old value' '\n test_expect_success 'stdin -z delete ref fails with wrong old value' '\n \tprintf $F \"delete $a\" \"$m~1\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$a'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$a'\"'\"'\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual\n@@ -1230,7 +1230,7 @@ test_expect_success 'stdin -z delete ref fails with wrong old value' '\n test_expect_success 'stdin -z delete ref fails with zero old value' '\n \tprintf $F \"delete $a\" \"$Z\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete $a: zero <old-oid>\" err &&\n+\ttest_grep \"fatal: delete $a: zero <old-oid>\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual\n@@ -1336,7 +1336,7 @@ test_expect_success 'stdin -z update refs fails with wrong old value' '\n \tgit update-ref $c $m &&\n \tprintf $F \"update $a\" \"$m\" \"$m\" \"update $b\" \"$m\" \"$m\" \"update $c\" \"$m\" \"$Z\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual &&\n@@ -1415,13 +1415,13 @@ test_expect_success 'handle per-worktree refs in refs/bisect' '\n \t\tcd worktree &&\n \t\tgit commit --allow-empty -m \"test commit\"  &&\n \t\tgit for-each-ref >for-each-ref.out &&\n-\t\t! grep refs/bisect for-each-ref.out &&\n+\t\ttest_grep ! refs/bisect for-each-ref.out &&\n \t\tgit update-ref refs/bisect/something HEAD &&\n \t\tgit rev-parse refs/bisect/something >../worktree-head &&\n \t\tgit for-each-ref | grep refs/bisect/something\n \t) &&\n \tgit show-ref >actual &&\n-\t! grep 'refs/bisect' actual &&\n+\ttest_grep ! 'refs/bisect' actual &&\n \ttest_must_fail git rev-parse refs/bisect/something &&\n \tgit update-ref refs/bisect/something HEAD &&\n \tgit rev-parse refs/bisect/something >main-head &&\n@@ -1477,7 +1477,7 @@ test_expect_success 'transaction exits on multiple aborts' '\n \ttest_must_fail git update-ref --stdin <stdin >actual 2>err &&\n \tprintf \"%s: ok\\n\" abort >expect &&\n \ttest_cmp expect actual &&\n-\tgrep \"fatal: transaction is closed\" err\n+\ttest_grep \"fatal: transaction is closed\" err\n '\n \n test_expect_success 'transaction exits on start after prepare' '\n@@ -1488,7 +1488,7 @@ test_expect_success 'transaction exits on start after prepare' '\n \ttest_must_fail git update-ref --stdin <stdin 2>err >actual &&\n \tprintf \"%s: ok\\n\" prepare >expect &&\n \ttest_cmp expect actual &&\n-\tgrep \"fatal: prepared transactions can only be closed\" err\n+\ttest_grep \"fatal: prepared transactions can only be closed\" err\n '\n \n test_expect_success 'transaction handles empty abort with missing prepare' '\n@@ -1648,7 +1648,7 @@ test_expect_success PIPE 'transaction flushes status updates' '\n \n \t# This must now fail given that we have locked the ref.\n \ttest_must_fail git update-ref refs/heads/flush $B 2>stderr &&\n-\tgrep \"fatal: update_ref failed for ref ${SQ}refs/heads/flush${SQ}: cannot lock ref\" stderr &&\n+\ttest_grep \"fatal: update_ref failed for ref ${SQ}refs/heads/flush${SQ}: cannot lock ref\" stderr &&\n \n \techo commit >&9 &&\n \techo \"commit: ok\" >expected &&\n@@ -1674,7 +1674,7 @@ do\n \t\tgit symbolic-ref refs/heads/symref $a &&\n \t\tformat_command $type \"symref-verify refs/heads/symref\" \"$a\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: symref-verify: cannot operate with deref mode\" err\n+\t\ttest_grep \"fatal: symref-verify: cannot operate with deref mode\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-verify fails with too many arguments\" '\n@@ -1682,9 +1682,9 @@ do\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err  &&\n \t\tif test \"$type\" = \"-z\"\n \t\tthen\n-\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\t\ttest_grep \"fatal: unknown command: $a\" err\n \t\telse\n-\t\t\tgrep \"fatal: symref-verify refs/heads/symref: extra input:  $a\" err\n+\t\t\ttest_grep \"fatal: symref-verify refs/heads/symref: extra input:  $a\" err\n \t\tfi\n \t'\n \n@@ -1717,7 +1717,7 @@ do\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >before &&\n \t\tformat_command $type \"symref-verify refs/heads/missing\" \"refs/heads/unknown\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/missing${SQ}: unable to resolve reference ${SQ}refs/heads/missing${SQ}\" err &&\n+\t\ttest_grep \"fatal: cannot lock ref ${SQ}refs/heads/missing${SQ}: unable to resolve reference ${SQ}refs/heads/missing${SQ}\" err &&\n \t\ttest_must_fail git rev-parse --verify -q refs/heads/missing &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >after &&\n \t\ttest_cmp before after\n@@ -1743,13 +1743,13 @@ do\n \t\tgit symbolic-ref refs/heads/symref $a &&\n \t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: symref-delete: cannot operate with deref mode\" err\n+\t\ttest_grep \"fatal: symref-delete: cannot operate with deref mode\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-delete fails with no ref\" '\n \t\tformat_command $type \"symref-delete \" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: symref-delete: missing <ref>\" err\n+\t\ttest_grep \"fatal: symref-delete: missing <ref>\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-delete fails deleting regular ref\" '\n@@ -1757,7 +1757,7 @@ do\n \t\tgit update-ref refs/heads/regularref $a &&\n \t\tformat_command $type \"symref-delete refs/heads/regularref\" \"$a\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: expected symref with target ${SQ}$a${SQ}: but is a regular ref\" err\n+\t\ttest_grep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: expected symref with target ${SQ}$a${SQ}: but is a regular ref\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-delete fails with too many arguments\" '\n@@ -1765,16 +1765,16 @@ do\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n \t\tif test \"$type\" = \"-z\"\n \t\tthen\n-\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\t\ttest_grep \"fatal: unknown command: $a\" err\n \t\telse\n-\t\t\tgrep \"fatal: symref-delete refs/heads/symref: extra input:  $a\" err\n+\t\t\ttest_grep \"fatal: symref-delete refs/heads/symref: extra input:  $a\" err\n \t\tfi\n \t'\n \n \ttest_expect_success \"stdin $type symref-delete fails with wrong old value\" '\n \t\tformat_command $type \"symref-delete refs/heads/symref\" \"$m\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main\" err &&\n+\t\ttest_grep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main\" err &&\n \t\tgit symbolic-ref refs/heads/symref >expect &&\n \t\techo $a >actual &&\n \t\ttest_cmp expect actual\n@@ -1812,9 +1812,9 @@ do\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n \t\tif test \"$type\" = \"-z\"\n \t\tthen\n-\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\t\ttest_grep \"fatal: unknown command: $a\" err\n \t\telse\n-\t\t\tgrep \"fatal: symref-create refs/heads/symref: extra input:  $a\" err\n+\t\t\ttest_grep \"fatal: symref-create refs/heads/symref: extra input:  $a\" err\n \t\tfi\n \t'\n \n@@ -1877,16 +1877,16 @@ do\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n \t\tif test \"$type\" = \"-z\"\n \t\tthen\n-\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\t\ttest_grep \"fatal: unknown command: $a\" err\n \t\telse\n-\t\t\tgrep \"fatal: symref-update refs/heads/symref: extra input:  $a\" err\n+\t\t\ttest_grep \"fatal: symref-update refs/heads/symref: extra input:  $a\" err\n \t\tfi\n \t'\n \n \ttest_expect_success \"stdin $type symref-update fails with wrong old value argument\" '\n \t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"foo\" \"$a\" \"$a\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: symref-update refs/heads/symref: invalid arg ${SQ}foo${SQ} for old value\" err\n+\t\ttest_grep \"fatal: symref-update refs/heads/symref: invalid arg ${SQ}foo${SQ} for old value\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-update creates with zero old value\" '\n@@ -1922,7 +1922,7 @@ do\n \t\tgit symbolic-ref refs/heads/symref $a &&\n \t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"$b\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected $b\" err &&\n+\t\ttest_grep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected $b\" err &&\n \t\ttest_must_fail git rev-parse --verify -q $c\n \t'\n \n@@ -1997,7 +1997,7 @@ do\n \t\tgit symbolic-ref --no-recurse refs/heads/symref >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&\n-\t\tgrep \"$Z $(git rev-parse $a)\" actual\n+\t\ttest_grep \"$Z $(git rev-parse $a)\" actual\n \t'\n \n \ttest_expect_success \"stdin $type symref-update regular ref to symref with correct old-oid\" '\n@@ -2009,7 +2009,7 @@ do\n \t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&\n-\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t\ttest_grep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n \t'\n \n \ttest_expect_success \"stdin $type symref-update regular ref to symref fails with wrong old-oid\" '\n@@ -2017,7 +2017,7 @@ do\n \t\tgit update-ref --no-deref refs/heads/regularref $a &&\n \t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"$(git rev-parse refs/heads/target2)\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: is at $(git rev-parse $a) but expected $(git rev-parse refs/heads/target2)\" err &&\n+\t\ttest_grep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: is at $(git rev-parse $a) but expected $(git rev-parse refs/heads/target2)\" err &&\n \t\techo $(git rev-parse $a) >expect &&\n \t\tgit rev-parse refs/heads/regularref >actual &&\n \t\ttest_cmp expect actual\n@@ -2028,7 +2028,7 @@ do\n \t\tgit update-ref --no-deref refs/heads/regularref $a &&\n \t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"not-a-ref-oid\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: symref-update refs/heads/regularref: invalid oid: not-a-ref-oid\" err &&\n+\t\ttest_grep \"fatal: symref-update refs/heads/regularref: invalid oid: not-a-ref-oid\" err &&\n \t\techo $(git rev-parse $a) >expect &&\n \t\tgit rev-parse refs/heads/regularref >actual &&\n \t\ttest_cmp expect actual\n@@ -2039,7 +2039,7 @@ do\n \t\tgit symbolic-ref refs/heads/symref refs/heads/target2 &&\n \t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"oid\" \"$Z\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: reference already exists\" err &&\n+\t\ttest_grep \"fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: reference already exists\" err &&\n \t\techo refs/heads/target2 >expect &&\n \t\tgit symbolic-ref refs/heads/symref >actual &&\n \t\ttest_cmp expect actual\n@@ -2059,7 +2059,7 @@ do\n \t\tgit symbolic-ref --no-recurse refs/heads/symref >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&\n-\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t\ttest_grep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n \t'\n \n \ttest_expect_success \"stdin $type symref-update regular ref to symref\" '\n@@ -2071,7 +2071,7 @@ do\n \t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&\n-\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t\ttest_grep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n \t'\n \n \ttest_expect_success \"stdin $type batch-updates\" '\ndiff --git a/t/t1403-show-ref.sh b/t/t1403-show-ref.sh\nindex 36c903ca19..7ce5af4ab8 100755\n--- a/t/t1403-show-ref.sh\n+++ b/t/t1403-show-ref.sh\n@@ -213,19 +213,19 @@ test_expect_success 'show-ref --verify with dangling ref' '\n \n test_expect_success 'show-ref sub-modes are mutually exclusive' '\n \ttest_must_fail git show-ref --verify --exclude-existing 2>err &&\n-\tgrep \"verify\" err &&\n-\tgrep \"exclude-existing\" err &&\n-\tgrep \"cannot be used together\" err &&\n+\ttest_grep \"verify\" err &&\n+\ttest_grep \"exclude-existing\" err &&\n+\ttest_grep \"cannot be used together\" err &&\n \n \ttest_must_fail git show-ref --verify --exists 2>err &&\n-\tgrep \"verify\" err &&\n-\tgrep \"exists\" err &&\n-\tgrep \"cannot be used together\" err &&\n+\ttest_grep \"verify\" err &&\n+\ttest_grep \"exists\" err &&\n+\ttest_grep \"cannot be used together\" err &&\n \n \ttest_must_fail git show-ref --exclude-existing --exists 2>err &&\n-\tgrep \"exclude-existing\" err &&\n-\tgrep \"exists\" err &&\n-\tgrep \"cannot be used together\" err\n+\ttest_grep \"exclude-existing\" err &&\n+\ttest_grep \"exists\" err &&\n+\ttest_grep \"cannot be used together\" err\n '\n \n test_done\ndiff --git a/t/t1410-reflog.sh b/t/t1410-reflog.sh\nindex ce71f9a30a..81de1d40eb 100755\n--- a/t/t1410-reflog.sh\n+++ b/t/t1410-reflog.sh\n@@ -108,12 +108,12 @@ test_expect_success setup '\n \n test_expect_success 'correct usage on sub-command -h' '\n \ttest_expect_code 129 git reflog expire -h >err &&\n-\tgrep \"git reflog expire\" err\n+\ttest_grep \"git reflog expire\" err\n '\n \n test_expect_success 'correct usage on \"git reflog show -h\"' '\n \ttest_expect_code 129 git reflog show -h >err &&\n-\tgrep -F \"git reflog [show]\" err\n+\ttest_grep -F \"git reflog [show]\" err\n '\n \n test_expect_success 'pass through -- to sub-command' '\ndiff --git a/t/t1415-worktree-refs.sh b/t/t1415-worktree-refs.sh\nindex 51d79bae83..6b2ad04aef 100755\n--- a/t/t1415-worktree-refs.sh\n+++ b/t/t1415-worktree-refs.sh\n@@ -32,7 +32,7 @@ test_expect_success 'ambiguous main-worktree/HEAD' '\n \ttest_when_finished git update-ref -d refs/heads/main-worktree/HEAD &&\n \tgit update-ref refs/heads/main-worktree/HEAD $(git rev-parse HEAD) &&\n \tgit rev-parse main-worktree/HEAD 2>warn &&\n-\tgrep \"main-worktree/HEAD.*ambiguous\" warn\n+\ttest_grep \"main-worktree/HEAD.*ambiguous\" warn\n '\n \n test_expect_success 'resolve worktrees/xx/HEAD' '\n@@ -45,7 +45,7 @@ test_expect_success 'ambiguous worktrees/xx/HEAD' '\n \tgit update-ref refs/heads/worktrees/wt1/HEAD $(git rev-parse HEAD) &&\n \ttest_when_finished git update-ref -d refs/heads/worktrees/wt1/HEAD &&\n \tgit rev-parse worktrees/wt1/HEAD 2>warn &&\n-\tgrep \"worktrees/wt1/HEAD.*ambiguous\" warn\n+\ttest_grep \"worktrees/wt1/HEAD.*ambiguous\" warn\n '\n \n test_expect_success 'reflog of main-worktree/HEAD' '\ndiff --git a/t/t1430-bad-ref-name.sh b/t/t1430-bad-ref-name.sh\nindex 3ab65f72cd..1ed4c7d2c2 100755\n--- a/t/t1430-bad-ref-name.sh\n+++ b/t/t1430-bad-ref-name.sh\n@@ -47,7 +47,7 @@ test_expect_success 'git branch shows badly named ref as warning' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \tgit branch >output 2>error &&\n \ttest_grep -e \"ignoring ref with broken name refs/heads/broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -d can delete badly named ref' '\n@@ -55,8 +55,8 @@ test_expect_success 'branch -d can delete badly named ref' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \tgit branch -d broken...ref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -D can delete badly named ref' '\n@@ -64,8 +64,8 @@ test_expect_success 'branch -D can delete badly named ref' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \tgit branch -D broken...ref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -D cannot delete non-ref in .git dir' '\n@@ -93,8 +93,8 @@ test_expect_success 'git branch cannot create a badly named ref' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \ttest_must_fail git branch broken...ref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -m cannot rename to a bad ref name' '\n@@ -104,8 +104,8 @@ test_expect_success 'branch -m cannot rename to a bad ref name' '\n \ttest_must_fail git branch -m goodref broken...ref &&\n \ttest_cmp_rev main goodref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_failure 'branch -m can rename from a bad ref name' '\n@@ -115,16 +115,16 @@ test_expect_failure 'branch -m can rename from a bad ref name' '\n \tgit branch -m broken...ref renamed &&\n \ttest_cmp_rev main renamed &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'push cannot create a badly named ref' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \ttest_must_fail git push \"file://$(pwd)\" HEAD:refs/heads/broken...ref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_failure 'push --mirror can delete badly named ref' '\n@@ -144,8 +144,8 @@ test_expect_failure 'push --mirror can delete badly named ref' '\n \t) &&\n \tgit -C src push --mirror \"file://$top/dest\" &&\n \tgit -C dest branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'rev-parse skips symref pointing to broken name' '\n@@ -168,9 +168,9 @@ test_expect_success 'for-each-ref emits warnings for broken names' '\n \ttest-tool ref-store main create-symref refs/heads/broken...symref refs/heads/main &&\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...symref\" &&\n \tgit for-each-ref >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output &&\n-\t! grep -e \"badname\" output &&\n-\t! grep -e \"broken\\.\\.\\.symref\" output &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output &&\n+\ttest_grep ! -e \"badname\" output &&\n+\ttest_grep ! -e \"broken\\.\\.\\.symref\" output &&\n \ttest_grep \"ignoring ref with broken name refs/heads/broken\\.\\.\\.ref\" error &&\n \ttest_grep ! \"ignoring broken ref refs/heads/badname\" error &&\n \ttest_grep \"ignoring ref with broken name refs/heads/broken\\.\\.\\.symref\" error\n@@ -183,8 +183,8 @@ test_expect_success 'update-ref -d can delete broken name' '\n \ttest_must_be_empty output &&\n \ttest_must_be_empty error &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -d can delete broken name' '\n@@ -194,8 +194,8 @@ test_expect_success 'branch -d can delete broken name' '\n \ttest_grep \"Deleted branch broken...ref (was broken)\" output &&\n \ttest_must_be_empty error &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'update-ref --no-deref -d can delete symref to broken name' '\n@@ -313,37 +313,37 @@ test_expect_success 'update-ref -d cannot delete absolute path' '\n test_expect_success 'update-ref --stdin fails create with bad ref name' '\n \techo \"create ~a refs/heads/main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'update-ref --stdin fails update with bad ref name' '\n \techo \"update ~a refs/heads/main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'update-ref --stdin fails delete with bad ref name' '\n \techo \"delete ~a refs/heads/main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'update-ref --stdin -z fails create with bad ref name' '\n \tprintf \"%s\\0\" \"create ~a \" refs/heads/main >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a \" err\n+\ttest_grep \"fatal: invalid ref format: ~a \" err\n '\n \n test_expect_success 'update-ref --stdin -z fails update with bad ref name' '\n \tprintf \"%s\\0\" \"update ~a\" refs/heads/main \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'update-ref --stdin -z fails delete with bad ref name' '\n \tprintf \"%s\\0\" \"delete ~a\" refs/heads/main >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'branch rejects HEAD as a branch name' '\ndiff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh\nindex 54e81c2636..47d460a699 100755\n--- a/t/t1450-fsck.sh\n+++ b/t/t1450-fsck.sh\n@@ -68,7 +68,7 @@ test_expect_success 'object with hash mismatch' '\n \t\tgit update-ref refs/heads/bogus $cmt &&\n \n \t\ttest_must_fail git fsck 2>out &&\n-\t\tgrep \"$oldoid: hash-path mismatch, found at: .*$new\" out\n+\t\ttest_grep \"$oldoid: hash-path mismatch, found at: .*$new\" out\n \t)\n '\n \n@@ -172,7 +172,7 @@ test_expect_success 'commit with multiple signatures is okay' '\n \ttest_when_finished \"git update-ref -d refs/heads/bogus\" &&\n \tgit fsck 2>out &&\n \tcat out &&\n-\t! grep \"commit $new\" out\n+\ttest_grep ! \"commit $new\" out\n '\n \n test_expect_success 'email without @ is okay' '\n@@ -183,7 +183,7 @@ test_expect_success 'email without @ is okay' '\n \tgit update-ref refs/heads/bogus \"$new\" &&\n \ttest_when_finished \"git update-ref -d refs/heads/bogus\" &&\n \tgit fsck 2>out &&\n-\t! grep \"commit $new\" out\n+\ttest_grep ! \"commit $new\" out\n '\n \n test_expect_success 'email with embedded > is not okay' '\n@@ -626,7 +626,7 @@ test_expect_success 'fsck notices excessively large tree entry name' '\n \t\tcd large-name &&\n \t\ttest_commit a-long-name &&\n \t\tgit -c fsck.largePathname=warn:10 fsck 2>out &&\n-\t\tgrep \"warning.*large pathname\" out\n+\t\ttest_grep \"warning.*large pathname\" out\n \t)\n '\n \n@@ -849,7 +849,7 @@ test_expect_success 'fsck errors in packed objects' '\n \ttest_must_fail git fsck 2>out &&\n \ttest_grep \"error in commit $one.* - bad name\" out &&\n \ttest_grep \"error in commit $two.* - bad name\" out &&\n-\t! grep corrupt out\n+\ttest_grep ! corrupt out\n '\n \n test_expect_success 'fsck handles multiple packfiles with big blobs' '\n@@ -1027,7 +1027,7 @@ test_expect_success 'bogus head does not fallback to all heads' '\n \ttest_when_finished \"git rm --cached foo\" &&\n \tremove_object $blob &&\n \ttest_must_fail git fsck $ZERO_OID >out 2>&1 &&\n-\t! grep $blob out\n+\ttest_grep ! $blob out\n '\n \n # Corrupt the checksum on the index.\ndiff --git a/t/t1451-fsck-buffer.sh b/t/t1451-fsck-buffer.sh\nindex 3a3d33f405..14151eabbd 100755\n--- a/t/t1451-fsck-buffer.sh\n+++ b/t/t1451-fsck-buffer.sh\n@@ -46,7 +46,7 @@ check () {\n \t\t\techo \"$content\"\n \t\t} >input &&\n \t\ttest_must_fail git hash-object -t \"$type\" input 2>err &&\n-\t\tgrep \"$fsck\" err\n+\t\ttest_grep \"$fsck\" err\n \t'\n }\n \n@@ -125,7 +125,7 @@ ident_checks tag tagger\n test_expect_success 'truncated tree (short hash)' '\n \tprintf \"100644 foo\\0\\1\\1\\1\\1\" >input &&\n \ttest_must_fail git hash-object -t tree input 2>err &&\n-\tgrep badTree err\n+\ttest_grep badTree err\n '\n \n test_expect_success 'truncated tree (missing nul)' '\n@@ -135,7 +135,7 @@ test_expect_success 'truncated tree (missing nul)' '\n \t# parser does not walk past the end of the buffer).\n \tprintf \"100644 a long filename, or a hash with missing nul?\" >input &&\n \ttest_must_fail git hash-object -t tree input 2>err &&\n-\tgrep badTree err\n+\ttest_grep badTree err\n '\n \n test_done\ndiff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh\nindex 5246468024..8f42697143 100755\n--- a/t/t1460-refs-migrate.sh\n+++ b/t/t1460-refs-migrate.sh\n@@ -212,7 +212,7 @@ do\n \t\t\ttest_commit -C repo initial &&\n \t\t\tgit -C repo refs migrate --dry-run \\\n \t\t\t\t--ref-format=$to_format >output &&\n-\t\t\tgrep \"Finished dry-run migration of refs\" output &&\n+\t\t\ttest_grep \"Finished dry-run migration of refs\" output &&\n \t\t\ttest_path_is_dir repo/.git/ref_migration.* &&\n \t\t\techo $from_format >expect &&\n \t\t\tgit -C repo rev-parse --show-ref-format >actual &&\ndiff --git a/t/t1500-rev-parse.sh b/t/t1500-rev-parse.sh\nindex 38067d95f7..4174ca40c3 100755\n--- a/t/t1500-rev-parse.sh\n+++ b/t/t1500-rev-parse.sh\n@@ -204,7 +204,7 @@ test_expect_success 'rev-parse --show-object-format in repo' '\n \tgit rev-parse --show-object-format=output >actual &&\n \ttest_cmp expect actual &&\n \ttest_must_fail git rev-parse --show-object-format=squeamish-ossifrage 2>err &&\n-\tgrep \"unknown mode for --show-object-format: squeamish-ossifrage\" err\n+\ttest_grep \"unknown mode for --show-object-format: squeamish-ossifrage\" err\n '\n \n \n@@ -228,7 +228,7 @@ test_expect_success RUST 'rev-parse --show-object-format in repo with compat mod\n \t\tgit rev-parse --show-object-format=compat >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest_must_fail git rev-parse --show-object-format=squeamish-ossifrage 2>err &&\n-\t\tgrep \"unknown mode for --show-object-format: squeamish-ossifrage\" err\n+\t\ttest_grep \"unknown mode for --show-object-format: squeamish-ossifrage\" err\n \t) &&\n \tmkdir repo2 &&\n \t(\n@@ -254,7 +254,7 @@ test_expect_success 'rev-parse --show-ref-format with invalid storage' '\n \t\tcd repo &&\n \t\tgit config extensions.refstorage broken &&\n \t\ttest_must_fail git rev-parse --show-ref-format 2>err &&\n-\t\tgrep \"error: invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}broken${SQ}\" err\n+\t\ttest_grep \"error: invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}broken${SQ}\" err\n \t)\n '\n \ndiff --git a/t/t1502-rev-parse-parseopt.sh b/t/t1502-rev-parse-parseopt.sh\nindex 3962f1d288..2cab204376 100755\n--- a/t/t1502-rev-parse-parseopt.sh\n+++ b/t/t1502-rev-parse-parseopt.sh\n@@ -331,7 +331,7 @@ test_expect_success 'ambiguous: --no matches both --noble and --no-noble' '\n \tEOF\n \ttest_expect_code 129 env GIT_TEST_DISALLOW_ABBREVIATED_OPTIONS=false \\\n \tgit rev-parse --parseopt -- <spec 2>err --no &&\n-\tgrep \"error: ambiguous option: no (could be --noble or --no-noble)\" err\n+\ttest_grep \"error: ambiguous option: no (could be --noble or --no-noble)\" err\n '\n \n test_done\ndiff --git a/t/t1503-rev-parse-verify.sh b/t/t1503-rev-parse-verify.sh\nindex 75a708f9ba..87638a4a2c 100755\n--- a/t/t1503-rev-parse-verify.sh\n+++ b/t/t1503-rev-parse-verify.sh\n@@ -62,15 +62,15 @@ test_expect_success 'works with one good rev' '\n \n test_expect_success 'fails with any bad rev or many good revs' '\n \ttest_must_fail git rev-parse --verify 2>error &&\n-\tgrep \"single revision\" error &&\n+\ttest_grep \"single revision\" error &&\n \ttest_must_fail git rev-parse --verify foo 2>error &&\n-\tgrep \"single revision\" error &&\n+\ttest_grep \"single revision\" error &&\n \ttest_must_fail git rev-parse --verify HEAD bar 2>error &&\n-\tgrep \"single revision\" error &&\n+\ttest_grep \"single revision\" error &&\n \ttest_must_fail git rev-parse --verify baz HEAD 2>error &&\n-\tgrep \"single revision\" error &&\n+\ttest_grep \"single revision\" error &&\n \ttest_must_fail git rev-parse --verify $HASH2 HEAD 2>error &&\n-\tgrep \"single revision\" error\n+\ttest_grep \"single revision\" error\n '\n \n test_expect_success 'fails silently when using -q' '\ndiff --git a/t/t1510-repo-setup.sh b/t/t1510-repo-setup.sh\nindex bbfe05b8e4..d330b8df47 100755\n--- a/t/t1510-repo-setup.sh\n+++ b/t/t1510-repo-setup.sh\n@@ -604,7 +604,7 @@ test_expect_success '#20b/c: core.worktree and core.bare conflict' '\n \t\tcd 20b/.git &&\n \t\ttest_must_fail git status >/dev/null\n \t) 2>message &&\n-\tgrep \"core.bare and core.worktree\" message\n+\ttest_grep \"core.bare and core.worktree\" message\n '\n \n test_expect_success '#20d: core.worktree and core.bare OK when working tree not needed' '\n@@ -721,8 +721,8 @@ test_expect_success '#22.2: core.worktree and core.bare conflict' '\n \t\texport GIT_DIR &&\n \t\ttest_must_fail git status 2>result\n \t) &&\n-\tgrep \"core.bare and core.worktree\" 22/.git/result &&\n-\tgrep \"core.bare and core.worktree\" 22/result\n+\ttest_grep \"core.bare and core.worktree\" 22/.git/result &&\n+\ttest_grep \"core.bare and core.worktree\" 22/result\n '\n \n # Case #23: GIT_DIR + GIT_WORK_TREE(+core.worktree) suppresses bareness.\n@@ -767,7 +767,7 @@ test_expect_success '#28: core.worktree and core.bare conflict (gitfile case)' '\n \t\tcd 28 &&\n \t\ttest_must_fail git status\n \t) 2>message &&\n-\tgrep \"core.bare and core.worktree\" message\n+\ttest_grep \"core.bare and core.worktree\" message\n '\n \n # Case #29: GIT_WORK_TREE(+core.worktree) overrides core.bare (gitfile case).\n@@ -791,7 +791,7 @@ test_expect_success '#30: core.worktree and core.bare conflict (gitfile version)\n \t\tcd 30 &&\n \t\ttest_must_fail env GIT_DIR=.git git status 2>result\n \t) &&\n-\tgrep \"core.bare and core.worktree\" 30/result\n+\ttest_grep \"core.bare and core.worktree\" 30/result\n '\n \n # Case #31: GIT_DIR + GIT_WORK_TREE(+core.worktree) suppresses\ndiff --git a/t/t1512-rev-parse-disambiguation.sh b/t/t1512-rev-parse-disambiguation.sh\nindex 1a380a4184..0e6b42ec78 100755\n--- a/t/t1512-rev-parse-disambiguation.sh\n+++ b/t/t1512-rev-parse-disambiguation.sh\n@@ -378,7 +378,7 @@ test_expect_success 'ambiguous 40-hex ref' '\n \tVAL=$(git commit-tree $TREE </dev/null) &&\n \tgit update-ref refs/heads/$REF $VAL &&\n \ttest $(git rev-parse $REF 2>err) = $REF &&\n-\tgrep \"refname.*${REF}.*ambiguous\" err\n+\ttest_grep \"refname.*${REF}.*ambiguous\" err\n '\n \n test_expect_success 'ambiguous short sha1 ref' '\n@@ -387,7 +387,7 @@ test_expect_success 'ambiguous short sha1 ref' '\n \tVAL=$(git commit-tree $TREE </dev/null) &&\n \tgit update-ref refs/heads/$REF $VAL &&\n \ttest $(git rev-parse $REF 2>err) = $VAL &&\n-\tgrep \"refname.*${REF}.*ambiguous\" err\n+\ttest_grep \"refname.*${REF}.*ambiguous\" err\n '\n \n test_expect_success 'ambiguity errors are not repeated (raw)' '\ndiff --git a/t/t1515-rev-parse-outside-repo.sh b/t/t1515-rev-parse-outside-repo.sh\nindex 75e89c4b6e..2cb3c7cc9c 100755\n--- a/t/t1515-rev-parse-outside-repo.sh\n+++ b/t/t1515-rev-parse-outside-repo.sh\n@@ -32,7 +32,7 @@ test_expect_success 'rev-parse --local-env-vars' '\n \tgit rev-parse --local-env-vars >actual &&\n \t# we do not want to depend on the complete list here,\n \t# so just look for something plausible\n-\tgrep ^GIT_DIR actual\n+\ttest_grep ^GIT_DIR actual\n '\n \n test_expect_success 'rev-parse --resolve-git-dir' '\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 0132e772e4..9aae3ff074 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -79,7 +79,7 @@ test_expect_success 'git hook usage' '\n \ttest_expect_code 129 git hook run --unknown 2>err &&\n \ttest_expect_code 129 git hook list &&\n \ttest_expect_code 129 git hook list -h &&\n-\tgrep \"unknown option\" err\n+\ttest_grep \"unknown option\" err\n '\n \n test_expect_success 'git hook list: unknown hook name is rejected' '\n@@ -361,9 +361,9 @@ test_expect_success 'hook can be configured for multiple events' '\n \n \t# 'ghi' should be included in both 'pre-commit' and 'test-hook'\n \tgit hook list pre-commit >actual &&\n-\tgrep \"ghi\" actual &&\n+\ttest_grep \"ghi\" actual &&\n \tgit hook list --allow-unknown-hook-name test-hook >actual &&\n-\tgrep \"ghi\" actual\n+\ttest_grep \"ghi\" actual\n '\n \n test_expect_success 'git hook list shows hooks from the hookdir' '\n@@ -569,7 +569,7 @@ test_expect_success 'git hook run a hook with a bad shebang' '\n \t# TODO: We should emit the same (or at least a more similar)\n \t# error on MINGW (essentially Git for Windows) and all other\n \t# platforms.. See the OS-specific code in start_command()\n-\tgrep -E \"^(error|fatal): cannot (exec|spawn) .*bad-hooks/test-hook\" err\n+\ttest_grep -E \"^(error|fatal): cannot (exec|spawn) .*bad-hooks/test-hook\" err\n '\n \n test_expect_success 'stdin to hooks' '\n@@ -826,8 +826,8 @@ test_expect_success 'git hook run -j2 warns for hooks not marked parallel=true'\n \t# neither hook has parallel=true\n \n \tgit hook run --allow-unknown-hook-name -j2 test-hook >out 2>err &&\n-\tgrep \"hook .hook-1. is not marked as parallel=true\" err &&\n-\tgrep \"hook .hook-2. is not marked as parallel=true\" err\n+\ttest_grep \"hook .hook-1. is not marked as parallel=true\" err &&\n+\ttest_grep \"hook .hook-2. is not marked as parallel=true\" err\n '\n \n test_expect_success 'hook.jobs=1 config runs hooks in series' '\n@@ -1068,7 +1068,7 @@ test_expect_success 'hook.jobs=-1 resolves to online_cpus()' '\n \tcpus=$(test-tool online-cpus) &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.txt\" \\\n \t\tgit hook run --allow-unknown-hook-name test-hook >out 2>err &&\n-\tgrep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n+\ttest_grep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n '\n \n test_expect_success 'hook.<event>.jobs=-1 resolves to online_cpus()' '\n@@ -1081,7 +1081,7 @@ test_expect_success 'hook.<event>.jobs=-1 resolves to online_cpus()' '\n \tcpus=$(test-tool online-cpus) &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.txt\" \\\n \t\tgit hook run --allow-unknown-hook-name test-hook >out 2>err &&\n-\tgrep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n+\ttest_grep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n '\n \n test_expect_success 'git hook run -j-1 resolves to online_cpus()' '\n@@ -1092,7 +1092,7 @@ test_expect_success 'git hook run -j-1 resolves to online_cpus()' '\n \tcpus=$(test-tool online-cpus) &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.txt\" \\\n \t\tgit hook run --allow-unknown-hook-name -j-1 test-hook >out 2>err &&\n-\tgrep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n+\ttest_grep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n '\n \n test_expect_success 'hook.jobs rejects values less than -1' '\ndiff --git a/t/t2004-checkout-cache-temp.sh b/t/t2004-checkout-cache-temp.sh\nindex 0afe0ff7ca..db06c321ed 100755\n--- a/t/t2004-checkout-cache-temp.sh\n+++ b/t/t2004-checkout-cache-temp.sh\n@@ -132,8 +132,8 @@ test_expect_success 'overriding --stage=all resets implied --temp' '\n test_expect_success '--stage=all --no-temp is rejected' '\n \trm -f path* .merge_* actual &&\n \ttest_must_fail git checkout-index --stage=all --no-temp -- path1 2>err &&\n-\tgrep -v \"already exists\" err &&\n-\tgrep \"options .--stage=all. and .--no-temp. cannot be used together\" err\n+\ttest_grep -v \"already exists\" err &&\n+\ttest_grep \"options .--stage=all. and .--no-temp. cannot be used together\" err\n '\n \n test_expect_success 'checkout some stages/one file to temporary files' '\ndiff --git a/t/t2019-checkout-ambiguous-ref.sh b/t/t2019-checkout-ambiguous-ref.sh\nindex 1fcef4be95..0ac35798c2 100755\n--- a/t/t2019-checkout-ambiguous-ref.sh\n+++ b/t/t2019-checkout-ambiguous-ref.sh\n@@ -19,7 +19,7 @@ test_expect_success 'checkout ambiguous ref succeeds' '\n '\n \n test_expect_success 'checkout produces ambiguity warning' '\n-\tgrep \"warning.*ambiguous\" stderr\n+\ttest_grep \"warning.*ambiguous\" stderr\n '\n \n test_expect_success 'checkout chooses branch over tag' '\n@@ -41,7 +41,7 @@ test_expect_success 'checkout vague ref succeeds' '\n '\n \n test_expect_success VAGUENESS_SUCCESS 'checkout produces ambiguity warning' '\n-\tgrep \"warning.*ambiguous\" stderr\n+\ttest_grep \"warning.*ambiguous\" stderr\n '\n \n test_expect_success VAGUENESS_SUCCESS 'checkout chooses branch over tag' '\ndiff --git a/t/t2024-checkout-dwim.sh b/t/t2024-checkout-dwim.sh\nindex a3b1449ef1..752e7f0e61 100755\n--- a/t/t2024-checkout-dwim.sh\n+++ b/t/t2024-checkout-dwim.sh\n@@ -311,7 +311,7 @@ test_expect_success 'loosely defined local base branch is reported correctly' '\n \tgit checkout loose >actual.raw 2>&1 &&\n \tsed -e \"s/loose/BRANCHNAME/g\" <actual.raw >actual &&\n \tstatus_uno_is_clean &&\n-\tgrep BRANCHNAME actual &&\n+\ttest_grep BRANCHNAME actual &&\n \n \ttest_cmp expect actual\n '\n@@ -324,7 +324,7 @@ test_expect_success 'reject when arg could be part of dwim branch' '\n \techo bar >dwim-arg &&\n \ttest_must_fail git checkout dwim-arg &&\n \ttest_must_fail git rev-parse refs/heads/dwim-arg -- &&\n-\tgrep bar dwim-arg\n+\ttest_grep bar dwim-arg\n '\n \n test_expect_success 'disambiguate dwim branch and checkout path (1)' '\n@@ -334,7 +334,7 @@ test_expect_success 'disambiguate dwim branch and checkout path (1)' '\n \techo bar >dwim-arg1 &&\n \tgit checkout -- dwim-arg1 &&\n \ttest_must_fail git rev-parse refs/heads/dwim-arg1 -- &&\n-\tgrep foo dwim-arg1\n+\ttest_grep foo dwim-arg1\n '\n \n test_expect_success 'disambiguate dwim branch and checkout path (2)' '\n@@ -344,7 +344,7 @@ test_expect_success 'disambiguate dwim branch and checkout path (2)' '\n \techo bar >dwim-arg2 &&\n \tgit checkout dwim-arg2 -- &&\n \tgit rev-parse refs/heads/dwim-arg2 -- &&\n-\tgrep bar dwim-arg2\n+\ttest_grep bar dwim-arg2\n '\n \n test_done\ndiff --git a/t/t2030-unresolve-info.sh b/t/t2030-unresolve-info.sh\nindex be3fcdde07..9af24b1204 100755\n--- a/t/t2030-unresolve-info.sh\n+++ b/t/t2030-unresolve-info.sh\n@@ -122,7 +122,7 @@ test_expect_success 'add records checkout -m undoes' '\n \tcheck_resolve_undo removed &&\n \techo the index and the work tree is unmerged again &&\n \tgit diff >actual &&\n-\tgrep \"^++<<<<<<<\" actual\n+\ttest_grep \"^++<<<<<<<\" actual\n '\n \n test_expect_success 'unmerge with plumbing' '\n@@ -173,7 +173,7 @@ test_expect_success 'rerere and rerere forget' '\n \ttest -f .git/rr-cache/$rerere_id/postimage &&\n \tgit checkout -m fi/le &&\n \techo resurrect the conflict &&\n-\tgrep \"^=======\" fi/le &&\n+\ttest_grep \"^=======\" fi/le &&\n \techo reresolve the conflict &&\n \tgit rerere &&\n \ttest \"z$(cat fi/le)\" = zdifferent &&\n@@ -199,7 +199,7 @@ test_expect_success 'rerere and rerere forget (subdirectory)' '\n \ttest -f .git/rr-cache/$rerere_id/postimage &&\n \t(cd fi && git checkout -m le) &&\n \techo resurrect the conflict &&\n-\tgrep \"^=======\" fi/le &&\n+\ttest_grep \"^=======\" fi/le &&\n \techo reresolve the conflict &&\n \t(cd fi && git rerere) &&\n \ttest \"z$(cat fi/le)\" = zdifferent &&\ndiff --git a/t/t2060-switch.sh b/t/t2060-switch.sh\nindex c91c4db936..cec2fe6876 100755\n--- a/t/t2060-switch.sh\n+++ b/t/t2060-switch.sh\n@@ -34,13 +34,13 @@ test_expect_success 'switch and detach' '\n \n test_expect_success 'suggestion to detach' '\n \ttest_must_fail git switch main^{commit} 2>stderr &&\n-\tgrep \"try again with the --detach option\" stderr\n+\ttest_grep \"try again with the --detach option\" stderr\n '\n \n test_expect_success 'suggestion to detach is suppressed with advice.suggestDetachingHead=false' '\n \ttest_config advice.suggestDetachingHead false &&\n \ttest_must_fail git switch main^{commit} 2>stderr &&\n-\t! grep \"try again with the --detach option\" stderr\n+\ttest_grep ! \"try again with the --detach option\" stderr\n '\n \n test_expect_success 'switch and detach current branch' '\n@@ -76,7 +76,7 @@ test_expect_success 'new orphan branch from empty' '\n \tgit switch --orphan new-orphan &&\n \ttest_commit orphan &&\n \tgit cat-file commit refs/heads/new-orphan >commit &&\n-\t! grep ^parent commit &&\n+\ttest_grep ! ^parent commit &&\n \tgit ls-files >tracked-files &&\n \techo orphan.t >expected &&\n \ttest_cmp expected tracked-files\ndiff --git a/t/t2070-restore.sh b/t/t2070-restore.sh\nindex 16d6348b69..2c222fb934 100755\n--- a/t/t2070-restore.sh\n+++ b/t/t2070-restore.sh\n@@ -216,7 +216,7 @@ test_expect_success 'restore with merge options are incompatible with certain op\n \t\t\"--staged --worktree --conflict=zdiff3\"\n \tdo\n \t\ttest_must_fail git restore $opts . 2>err &&\n-\t\tgrep \"cannot be used\" err || return\n+\t\ttest_grep \"cannot be used\" err || return\n \tdone\n '\n \ndiff --git a/t/t2080-parallel-checkout-basics.sh b/t/t2080-parallel-checkout-basics.sh\nindex 5ffe1a41e2..88a6ec1826 100755\n--- a/t/t2080-parallel-checkout-basics.sh\n+++ b/t/t2080-parallel-checkout-basics.sh\n@@ -200,13 +200,13 @@ test_expect_success 'parallel checkout respects --[no]-force' '\n \t\t# We expect 0 workers because there is nothing to be done\n \t\ttest_checkout_workers 0 git checkout HEAD &&\n \t\ttest_path_is_file D &&\n-\t\tgrep changed D &&\n-\t\tgrep changed F.t &&\n+\t\ttest_grep changed D &&\n+\t\ttest_grep changed F.t &&\n \n \t\ttest_checkout_workers 2 git checkout --force HEAD &&\n \t\ttest_path_is_dir D &&\n-\t\tgrep D/F D/F.t &&\n-\t\tgrep F F.t\n+\t\ttest_grep D/F D/F.t &&\n+\t\ttest_grep F F.t\n \t)\n '\n \n@@ -224,8 +224,8 @@ test_expect_success SYMLINKS 'parallel checkout checks for symlinks in leading d\n \n \t\ttest_checkout_workers 2 git checkout --force HEAD &&\n \t\t! test -h D &&\n-\t\tgrep D/A D/A.t &&\n-\t\tgrep D/B D/B.t\n+\t\ttest_grep D/A D/A.t &&\n+\t\ttest_grep D/B D/B.t\n \t)\n '\n \n@@ -268,7 +268,7 @@ test_expect_success '\"git checkout .\" report should not include failed entries'\n \t\t#  - missing-delay.a: the delay filter will drop this path\n \t\t#  - parallel-*.a: the blob will be missing\n \t\t#\n-\t\tgrep \"Updated 3 paths from the index\" err &&\n+\t\ttest_grep \"Updated 3 paths from the index\" err &&\n \t\ttest_stdout_line_count = 3 ls *.b &&\n \t\t! ls *.a\n \t)\ndiff --git a/t/t2081-parallel-checkout-collisions.sh b/t/t2081-parallel-checkout-collisions.sh\nindex f6fcfc0c1e..db45f5f30f 100755\n--- a/t/t2081-parallel-checkout-collisions.sh\n+++ b/t/t2081-parallel-checkout-collisions.sh\n@@ -78,8 +78,8 @@ test_expect_success CASE_INSENSITIVE_FS 'worker detects dirname collision' '\n \n \t# Check that it used the right number of workers and detected the collisions\n \ttest_workers_in_event_trace 2 trace &&\n-\tgrep \"category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/B.}\" trace &&\n-\tgrep \"category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/C.}\" trace\n+\ttest_grep \"category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/B.}\" trace &&\n+\ttest_grep \"category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/C.}\" trace\n '\n \n test_expect_success SYMLINKS,CASE_INSENSITIVE_FS 'do not follow symlinks colliding with leading dir' '\n@@ -115,11 +115,11 @@ test_expect_success CASE_INSENSITIVE_FS 'collision report on clone (w/ racy file\n \tset_checkout_config 2 0 &&\n \ttest_checkout_workers 2 git clone . clone-repo 2>stderr &&\n \n-\tgrep FILE_X stderr &&\n-\tgrep FILE_x stderr &&\n-\tgrep file_X stderr &&\n-\tgrep file_x stderr &&\n-\tgrep \"the following paths have collided\" stderr\n+\ttest_grep FILE_X stderr &&\n+\ttest_grep FILE_x stderr &&\n+\ttest_grep file_X stderr &&\n+\ttest_grep file_x stderr &&\n+\ttest_grep \"the following paths have collided\" stderr\n '\n \n # This test ensures that the collision report code is correctly looking for\n@@ -148,11 +148,11 @@ test_expect_success CASE_INSENSITIVE_FS,!MINGW,!CYGWIN \\\n \ttest_checkout_workers 2 \\\n \t\tgit -c core.ignoreCase=false clone . clone-repo 2>stderr &&\n \n-\tgrep FILE_X stderr &&\n-\tgrep FILE_x stderr &&\n-\tgrep file_X stderr &&\n-\tgrep file_x stderr &&\n-\tgrep \"the following paths have collided\" stderr &&\n+\ttest_grep FILE_X stderr &&\n+\ttest_grep FILE_x stderr &&\n+\ttest_grep file_X stderr &&\n+\ttest_grep file_x stderr &&\n+\ttest_grep \"the following paths have collided\" stderr &&\n \n \t# Check that only \"file_x\" was filtered\n \techo file_x >expected.log &&\ndiff --git a/t/t2082-parallel-checkout-attributes.sh b/t/t2082-parallel-checkout-attributes.sh\nindex 79fb11f139..1f43c2a267 100755\n--- a/t/t2082-parallel-checkout-attributes.sh\n+++ b/t/t2082-parallel-checkout-attributes.sh\n@@ -28,8 +28,8 @@ test_expect_success 'parallel-checkout with ident' '\n \t\trm A B &&\n \t\ttest_checkout_workers 2 git reset --hard &&\n \t\thexsz=$(test_oid hexsz) &&\n-\t\tgrep -E \"\\\\\\$Id: [0-9a-f]{$hexsz} \\\\\\$\" A &&\n-\t\tgrep \"\\\\\\$Id\\\\\\$\" B\n+\t\ttest_grep -E \"\\\\\\$Id: [0-9a-f]{$hexsz} \\\\\\$\" A &&\n+\t\ttest_grep \"\\\\\\$Id\\\\\\$\" B\n \t)\n '\n \n@@ -175,15 +175,15 @@ test_expect_success 'parallel-checkout and delayed checkout' '\n \tverify_checkout delayed &&\n \n \t# Check that the *.d files got to the delay queue and were filtered\n-\tgrep \"smudge W.d .* \\[DELAYED\\]\" delayed.log &&\n-\tgrep \"smudge X.d .* \\[DELAYED\\]\" delayed.log &&\n+\ttest_grep \"smudge W.d .* \\[DELAYED\\]\" delayed.log &&\n+\ttest_grep \"smudge X.d .* \\[DELAYED\\]\" delayed.log &&\n \ttest_cmp delayed/W.d original &&\n \ttest_cmp delayed/X.d original &&\n \n \t# Check that the parallel-eligible entries went to the right queue and\n \t# were not filtered\n-\t! grep \"smudge Y .* \\[DELAYED\\]\" delayed.log &&\n-\t! grep \"smudge Z .* \\[DELAYED\\]\" delayed.log &&\n+\ttest_grep ! \"smudge Y .* \\[DELAYED\\]\" delayed.log &&\n+\ttest_grep ! \"smudge Z .* \\[DELAYED\\]\" delayed.log &&\n \ttest_cmp delayed/Y original &&\n \ttest_cmp delayed/Z original\n '\ndiff --git a/t/t2103-update-index-ignore-missing.sh b/t/t2103-update-index-ignore-missing.sh\nindex 6938ecca86..fbfc86b1fa 100755\n--- a/t/t2103-update-index-ignore-missing.sh\n+++ b/t/t2103-update-index-ignore-missing.sh\n@@ -63,9 +63,9 @@ test_expect_success '--unmerged --refresh' '\n \tgit update-index --unmerged --refresh &&\n \techo 2 >two &&\n \ttest_must_fail git update-index --unmerged --refresh >actual &&\n-\tgrep two actual &&\n-\t! grep one actual &&\n-\t! grep three actual\n+\ttest_grep two actual &&\n+\ttest_grep ! one actual &&\n+\ttest_grep ! three actual\n '\n \n test_expect_success '--ignore-submodules --refresh (1)' '\ndiff --git a/t/t2200-add-update.sh b/t/t2200-add-update.sh\nindex 0a96655cfe..bb019d7a7c 100755\n--- a/t/t2200-add-update.sh\n+++ b/t/t2200-add-update.sh\n@@ -241,7 +241,7 @@ test_expect_success 'add -u avoids rename pairing on unmerged paths' '\n test_expect_success '\"add -u non-existent\" should fail' '\n \ttest_must_fail git add -u non-existent &&\n \tgit ls-files >actual &&\n-\t! grep \"non-existent\" actual\n+\ttest_grep ! \"non-existent\" actual\n '\n \n test_expect_success '\"commit -a\" implies \"add -u\" if index becomes empty' '\ndiff --git a/t/t2203-add-intent.sh b/t/t2203-add-intent.sh\nindex 44c1936e4d..69fef7e646 100755\n--- a/t/t2203-add-intent.sh\n+++ b/t/t2203-add-intent.sh\n@@ -57,7 +57,7 @@ test_expect_success 'intent to add does not clobber existing paths' '\n \tgit add -N file elif &&\n \tempty=$(git hash-object --stdin </dev/null) &&\n \tgit ls-files -s >actual &&\n-\t! grep \"$empty\" actual\n+\ttest_grep ! \"$empty\" actual\n '\n \n test_expect_success 'i-t-a entry is simply ignored' '\n@@ -124,7 +124,7 @@ test_expect_success 'cache-tree does not ignore dir that has i-t-a entries' '\n \t\tgit add -N 2/1 &&\n \t\tgit commit -m committed &&\n \t\tgit ls-tree -r HEAD >actual &&\n-\t\tgrep 2/2 actual\n+\t\ttest_grep 2/2 actual\n \t)\n '\n \n@@ -312,7 +312,7 @@ test_expect_success 'apply --intent-to-add' '\n \techo new >new-ita &&\n \tgit add -N new-ita &&\n \tgit diff >expected &&\n-\tgrep \"new file\" expected &&\n+\ttest_grep \"new file\" expected &&\n \tgit reset --hard &&\n \tgit apply --intent-to-add expected &&\n \tgit diff >actual &&\ndiff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh\nindex 58b4445cc4..87b926728a 100755\n--- a/t/t2400-worktree-add.sh\n+++ b/t/t2400-worktree-add.sh\n@@ -122,7 +122,7 @@ test_expect_success 'die the same branch is already checked out' '\n \t(\n \t\tcd here &&\n \t\ttest_must_fail git checkout newmain 2>actual &&\n-\t\tgrep \"already used by worktree at\" actual\n+\t\ttest_grep \"already used by worktree at\" actual\n \t)\n '\n \n@@ -139,7 +139,7 @@ test_expect_success 'refuse to reset a branch in use elsewhere' '\n \t\tgit rev-parse --verify refs/heads/newmain >new.branch &&\n \t\tgit rev-parse --verify HEAD >new.head &&\n \n-\t\tgrep \"already used by worktree at\" error &&\n+\t\ttest_grep \"already used by worktree at\" error &&\n \t\ttest_cmp old.branch new.branch &&\n \t\ttest_cmp old.head new.head &&\n \n@@ -328,7 +328,7 @@ test_wt_add_excl () {\n \tlocal opts=\"$*\" &&\n \ttest_expect_success \"'worktree add' with '$opts' has mutually exclusive options\" '\n \t\ttest_must_fail git worktree add $opts 2>actual &&\n-\t\tgrep -E \"fatal:( options)? .* cannot be used together\" actual\n+\t\ttest_grep -E \"fatal:( options)? .* cannot be used together\" actual\n \t'\n }\n \n@@ -436,13 +436,13 @@ test_wt_add_orphan_hint () {\n \t\t(cd repo && test_commit commit) &&\n \t\tgit -C repo switch --orphan noref &&\n \t\ttest_must_fail git -C repo worktree add $opts foobar/ 2>actual &&\n-\t\t! grep \"error: unknown switch\" actual &&\n-\t\tgrep \"hint: If you meant to create a worktree containing a new unborn branch\" actual &&\n+\t\ttest_grep ! \"error: unknown switch\" actual &&\n+\t\ttest_grep \"hint: If you meant to create a worktree containing a new unborn branch\" actual &&\n \t\tif [ $use_branch -eq 1 ]\n \t\tthen\n-\t\t\tgrep -E \"^hint: +git worktree add --orphan -b [^ ]+ [^ ]+$\" actual\n+\t\t\ttest_grep -E \"^hint: +git worktree add --orphan -b [^ ]+ [^ ]+$\" actual\n \t\telse\n-\t\t\tgrep -E \"^hint: +git worktree add --orphan [^ ]+$\" actual\n+\t\t\ttest_grep -E \"^hint: +git worktree add --orphan [^ ]+$\" actual\n \t\tfi\n \n \t'\n@@ -457,8 +457,8 @@ test_expect_success \"'worktree add' doesn't show orphan hint in bad/orphan HEAD\n \tgit init repo &&\n \t(cd repo && test_commit commit) &&\n \ttest_must_fail git -C repo worktree add --quiet foobar_branch foobar/ 2>actual &&\n-\t! grep \"error: unknown switch\" actual &&\n-\t! grep \"hint: If you meant to create a worktree containing a new unborn branch\" actual\n+\ttest_grep ! \"error: unknown switch\" actual &&\n+\ttest_grep ! \"hint: If you meant to create a wo"},{"id":"544761","messageId":"CALnO6CCjr5xMk=GLHSgf=KQpKJ1FnpimQCYu+BqyufWrRFkh8A@mail.gmail.com","threadId":"65748","inReplyTo":"c1b90101ef5c38a21fc901bd7387acf83eb96806.1780559158.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 4/6] t: add lint-style.pl with test_grep negation rule","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-06-04T18:34:56Z","receivedAt":"2026-06-04T18:35:08Z","isPatch":true,"body":"Hi Michael,\n\nThis sounds like a neat effort!\n\nOne drive-by comment…\n\nOn Thu, Jun 4, 2026 at 3:46 AM Michael Montalbo via GitGitGadget\n<gitgitgadget@gmail.com> wrote:\n>\n> From: Michael Montalbo <mmontalbo@gmail.com>\n>\n> Add a mechanical lint checker for test scripts, similar in spirit to\n> check-non-portable-shell.pl but focused on test conventions rather\n> than portability.\n>\n> The tool defines LintParser, a subclass of ScriptParser (from the\n> shared lib-shell-parser.pl module).  ScriptParser's\n> parse_cmd() finds test_expect_success blocks and calls check_test()\n> for each body; LintParser overrides check_test() to run lint rules\n> on the parsed commands.  A \"# lint-ok\" comment suppresses all\n> checks for intentional style violations.\n>\n> The first rule detects '! test_grep' and replaces it with\n> 'test_grep !'.  Shell-level negation suppresses the diagnostic\n> output that test_grep prints on failure; the built-in negation\n> preserves it.\n>\n> Three violations inside test bodies are converted via --fix.  One\n> additional violation in a helper function outside test_expect_success\n> (t7900's test_geometric_repack_needed) is converted manually, since\n> the parser only processes test bodies.\n>\n> Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>\n> ---\n>  t/.gitattributes                           |   2 +\n>  t/Makefile                                 |  32 +++-\n>  t/lint-style.pl                            | 200 +++++++++++++++++++++\n>  t/lint-style/heredoc.expect                |   3 +\n>  t/lint-style/heredoc.test                  |  14 ++\n>  t/lint-style/test-grep-negation-fix.expect |   4 +\n>  t/lint-style/test-grep-negation-fix.test   |   4 +\n>  t/lint-style/test-grep-negation.expect     |   3 +\n>  t/lint-style/test-grep-negation.test       |   4 +\n>  t/t0031-lockfile-pid.sh                    |   2 +-\n>  t/t5300-pack-object.sh                     |   2 +-\n>  t/t5319-multi-pack-index.sh                |   2 +-\n>  t/t7900-maintenance.sh                     |   2 +-\n>  13 files changed, 268 insertions(+), 6 deletions(-)\n>  create mode 100755 t/lint-style.pl\n>  create mode 100644 t/lint-style/heredoc.expect\n>  create mode 100644 t/lint-style/heredoc.test\n>  create mode 100644 t/lint-style/test-grep-negation-fix.expect\n>  create mode 100644 t/lint-style/test-grep-negation-fix.test\n>  create mode 100644 t/lint-style/test-grep-negation.expect\n>  create mode 100644 t/lint-style/test-grep-negation.test\n>\n> diff --git a/t/.gitattributes b/t/.gitattributes\n> index 7664c6e027..aea6889d03 100644\n> --- a/t/.gitattributes\n> +++ b/t/.gitattributes\n> @@ -1,5 +1,7 @@\n>  t[0-9][0-9][0-9][0-9]/* -whitespace\n>  /chainlint/*.expect eol=lf -whitespace\n> +/lint-style/*.expect eol=lf -whitespace\n> +/lint-style/*.test eol=lf -whitespace\n>  /t0110/url-* binary\n>  /t3206/* eol=lf\n>  /t3900/*.txt eol=lf\n> diff --git a/t/Makefile b/t/Makefile\n> index 25f923fed9..3a5fa4ce37 100644\n> --- a/t/Makefile\n> +++ b/t/Makefile\n> @@ -46,6 +46,7 @@ TPERF = $(sort $(wildcard perf/p[0-9][0-9][0-9][0-9]-*.sh))\n>  TINTEROP = $(sort $(wildcard interop/i[0-9][0-9][0-9][0-9]-*.sh))\n>  CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.test)))\n>  CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl\n> +LINT_STYLE_TESTS = $(sort $(wildcard lint-style/*.test))\n>  UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)\n>  UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))\n>  UNIT_TEST_PROGRAMS += unit-tests/bin/unit-tests$(X)\n> @@ -139,7 +140,7 @@ check-meson:\n>  test-lint: test-lint-duplicates test-lint-executable \\\n>         test-lint-filenames\n>  ifneq ($(PERL_PATH),)\n> -test-lint: test-lint-shell-syntax check-shell-parser\n> +test-lint: test-lint-shell-syntax test-lint-style check-lint-style check-shell-parser\n>  else\n>  GIT_TEST_CHAIN_LINT = 0\n>  endif\n> @@ -162,6 +163,32 @@ test-lint-shell-syntax:\n>\n>  check-shell-parser:\n>         @'$(PERL_PATH_SQ)' check-shell-parser.pl\n> +\n> +test-lint-style:\n> +       @'$(PERL_PATH_SQ)' lint-style.pl $(T) $(THELPERS) $(TPERF)\n> +\n> +check-lint-style:\n> +       @rc=0; for t in $(LINT_STYLE_TESTS); do \\\n> +               base=$${t%.test}; \\\n> +               case $$base in \\\n> +               *-fix) \\\n> +                       cp \"$$t\" \"$$t.tmp\" && \\\n> +                       '$(PERL_PATH_SQ)' lint-style.pl --fix \"$$t.tmp\" >/dev/null 2>&1; \\\n> +                       fix_rc=$$?; \\\n> +                       if test $$fix_rc != 0; then \\\n> +                               echo \"FAIL: $$t (--fix exit code $$fix_rc)\"; rc=1; \\\n> +                       elif ! diff -u \"$$base.expect\" \"$$t.tmp\"; then \\\n> +                               echo \"FAIL: $$t (--fix output)\"; rc=1; \\\n> +                       fi; \\\n> +                       rm -f \"$$t.tmp\" ;; \\\n> +               *) \\\n> +                       if ! '$(PERL_PATH_SQ)' lint-style.pl \"$$t\" 2>&1 | \\\n> +                               diff -u \"$$base.expect\" -; then \\\n> +                               echo \"FAIL: $$t\"; rc=1; \\\n> +                       fi ;; \\\n> +               esac; \\\n> +       done; test $$rc = 0\n> +\n\n…I wonder if it would be easier to maintain this recipe as a separate\nshell script and have make give LINT_STYLE_TESTS and PERL_PATH (w/o\nSQ? idk) to the script. That's a lot of inline code otherwise!\n\n>  test-lint-filenames:\n>         @# We do *not* pass a glob to ls-files but use grep instead, to catch\n>         @# non-ASCII characters (which are quoted within double-quotes)\n> @@ -188,7 +215,8 @@ perf:\n>\n>  .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \\\n>         check-chainlint clean-chainlint test-chainlint \\\n> -       check-shell-parser $(UNIT_TESTS)\n> +       check-shell-parser \\\n> +       check-lint-style test-lint-style $(UNIT_TESTS)\n>\n>  .PHONY: libgit-sys-test libgit-rs-test\n>  libgit-sys-test:\n> diff --git a/t/lint-style.pl b/t/lint-style.pl\n> new file mode 100755\n> index 0000000000..9268577f9b\n> --- /dev/null\n> +++ b/t/lint-style.pl\n> @@ -0,0 +1,200 @@\n> +#!/usr/bin/perl\n> +\n> +# Check test scripts for style violations that can be detected\n> +# mechanically, such as using bare 'grep' where test_grep should\n> +# be used.  Use --fix to automatically apply suggested replacements.\n> +#\n> +# Detection uses parsed tokens from the shared shell parser for\n> +# correct handling of heredocs, $(...), pipes, and quoting.\n> +# Fixes modify the original file text to preserve formatting.\n> +\n> +use strict;\n> +use warnings;\n> +use File::Basename;\n> +# Force LF output so check-lint-style's diff against the\n> +# pre-committed .expect files works on Windows.\n> +binmode(STDOUT, ':unix');\n> +binmode(STDERR, ':unix');\n> +\n> +my $fix_mode = 0;\n> +if (@ARGV && $ARGV[0] eq '--fix') {\n> +       $fix_mode = 1;\n> +       shift @ARGV;\n> +}\n> +\n> +# Load the shared shell parser (Lexer, ShellParser, ScriptParser).\n> +my $_lib = dirname($0) . \"/lib-shell-parser.pl\";\n> +$_lib = \"./$_lib\" unless $_lib =~ m{^/};\n> +do $_lib or die \"$0: failed to load $_lib: $@$!\\n\";\n> +\n> +# LintParser is a subclass of ScriptParser which runs lint rules\n> +# on each test body.  Per-file state (file name, raw lines, dirty\n> +# flag) is stored on the instance before calling parse().\n> +#\n> +# Subroutines defined below (parse_commands, check_test_grep_negation,\n> +# etc.) are in package main and called with the main:: prefix.\n> +# File-scoped lexicals ($fix_mode, $has_fixable, etc.) are visible\n> +# across packages since 'package' does not introduce a new scope.\n> +package LintParser;\n> +our @ISA = ('ScriptParser');\n> +\n> +package main;\n> +\n> +my $exit_code = 0;\n> +my $has_fixable = 0;\n> +\n> +sub err {\n> +       my ($file, $lineno, $line, $msg, %opts) = @_;\n> +       $line =~ s/^\\s+//;\n> +       $line =~ s/\\s+$//;\n> +       $line =~ s/\\s+/ /g;\n> +       my $prefix = ($fix_mode && $opts{fixable}) ? 'fixed' : 'error';\n> +       print \"$file:$lineno: $prefix: $msg: $line\\n\";\n> +       $exit_code = 1 unless $fix_mode && $opts{fixable};\n> +}\n> +\n> +# Report a lint violation found by a rule.  In --fix mode, apply\n> +# the regex substitution on the raw line and report success.\n> +# Otherwise just report.  Returns 1 if the line was modified.\n> +sub report_violation {\n> +       my ($file, $cmd, $line_ref, $match, $fix, $from) = @_;\n> +       my $lineno = $cmd->{lineno};\n> +       my $display = join(' ', @{$cmd->{tokens}});\n> +       $has_fixable++;  # count for the \"--fix\" hint\n> +       if ($fix_mode) {\n> +               if ($$line_ref =~ s/$match/$fix/) {\n> +                       err $file, $lineno, $display,\n> +                               \"replace '$from' with '$fix'\",\n> +                               fixable => 1;\n> +                       return 1;\n> +               }\n> +               err $file, $lineno, $display,\n> +                       \"replace '$from' with '$fix' (could not auto-fix)\";\n> +       } else {\n> +               err $file, $lineno, $display,\n> +                       \"replace '$from' with '$fix'\";\n> +       }\n> +       return 0;\n> +}\n> +\n> +# Split a token stream into commands at &&, ||, ;;, and \\n.\n> +sub parse_commands {\n> +       my ($content) = @_;\n> +       my $parser = ShellParser->new(\\$content);\n> +       my @all_tokens = $parser->parse();\n> +\n> +       my @commands;\n> +       my @current;\n> +       my $lineno = 1;\n> +\n> +       for (my $ti = 0; $ti < @all_tokens; $ti++) {\n> +               my $text = $all_tokens[$ti]->[0];\n> +               if ($text =~ /^(?:&&|\\|\\||;;|\\n)$/) {\n> +                       if (@current) {\n> +                               push @commands, {\n> +                                       tokens => [@current],\n> +                                       lineno => $lineno,\n> +                               };\n> +                               @current = ();\n> +                       }\n> +               } else {\n> +                       $lineno = $all_tokens[$ti]->[3]\n> +                               if !@current && defined $all_tokens[$ti]->[3];\n> +                       push @current, $text;\n> +               }\n> +       }\n> +       if (@current) {\n> +               push @commands, {\n> +                       tokens => [@current],\n> +                       lineno => $lineno,\n> +               };\n> +       }\n> +       return @commands;\n> +}\n> +\n> +# --- Rule: '! test_grep' should be 'test_grep !' ---\n> +# Shell-level negation suppresses test_grep's diagnostic output\n> +# on failure.  Built-in negation preserves it.\n> +sub check_test_grep_negation {\n> +       my ($cmd, $file, $line_ref) = @_;\n> +       my @tokens = @{$cmd->{tokens}};\n> +       return unless @tokens >= 2 && $tokens[0] eq '!' && $tokens[1] eq 'test_grep';\n> +\n> +       return report_violation($file, $cmd, $line_ref,\n> +               qr/!\\s*test_grep/, 'test_grep !', '! test_grep');\n> +}\n> +\n> +# Map parsed commands back to raw file lines for --fix.\n> +# Detection uses parsed tokens (correct handling of quoting,\n> +# heredocs, pipes) but fixes must modify the original text\n> +# to preserve formatting.\n> +package LintParser;\n> +\n> +sub check_test {\n> +       # Called by ScriptParser::parse_cmd for each test_expect_success\n> +       # or test_expect_failure block.\n> +       my $self = shift @_;\n> +       my $title = ScriptParser::unwrap(shift @_);\n> +\n> +       # Two test body formats:\n> +       #   Quoted:  test_expect_success 'title' '..body..'\n> +       #   Heredoc: test_expect_success 'title' - <<\\EOF\n> +       #              ..body..\n> +       #            EOF\n> +       # For quoted, the body token is the quoted string.\n> +       # For heredoc, the body token is '-' and the actual\n> +       # code arrives as the next argument from the Lexer.\n> +       my $body_token = shift @_;\n> +       my $lineno_base = $body_token->[3] || 1;\n> +       my $body = ScriptParser::unwrap($body_token);\n> +\n> +       if ($body eq '-') {\n> +               my $herebody = shift @_;\n> +               if ($herebody) {\n> +                       $body = $herebody->{content};\n> +                       $lineno_base = $herebody->{start_line} || 1;\n> +               }\n> +       }\n> +       return unless $body;\n> +\n> +       # Map each command back to its file line number.\n> +       # $lineno_base is where the body starts in the file;\n> +       # $cmd->{lineno} is relative to the body (starting at 1).\n> +       my $raw_lines = $self->{raw_lines};\n> +       for my $cmd (main::parse_commands($body)) {\n> +               my $ln = ($cmd->{lineno} || 0) + $lineno_base - 1;\n> +               $cmd->{lineno} = $ln;\n> +               next unless $ln >= 1 && $ln <= @$raw_lines;\n> +               next if $raw_lines->[$ln - 1] =~ /#.*lint-ok/;\n> +\n> +               if (main::check_test_grep_negation($cmd, $self->{file}, \\$raw_lines->[$ln - 1])) {\n> +                       $self->{dirty} = 1;\n> +               }\n> +       }\n> +}\n> +\n> +package main;\n> +\n> +for my $file (@ARGV) {\n> +       # :unix:crlf strips \\r on Windows (same as chainlint.pl)\n> +       open(my $fh, '<:unix:crlf', $file) or die \"$0: $file: $!\\n\";\n> +       my @raw_lines = <$fh>;\n> +       close $fh;\n> +\n> +       my $parser = LintParser->new(\\join('', @raw_lines));\n> +       $parser->{file} = $file;\n> +       $parser->{raw_lines} = \\@raw_lines;\n> +       $parser->{dirty} = 0;\n> +       $parser->parse();\n> +\n> +       if ($fix_mode && $parser->{dirty}) {\n> +               open(my $out, '>', $file) or die \"$0: $file: $!\\n\";\n> +               print $out @{$parser->{raw_lines}};\n> +               close $out;\n> +       }\n> +}\n> +\n> +if ($has_fixable && !$fix_mode) {\n> +       print \"hint: run with --fix to apply the suggested replacements.\\n\";\n> +}\n> +exit $exit_code;\n> diff --git a/t/lint-style/heredoc.expect b/t/lint-style/heredoc.expect\n> new file mode 100644\n> index 0000000000..7ff6d4a52d\n> --- /dev/null\n> +++ b/t/lint-style/heredoc.expect\n> @@ -0,0 +1,3 @@\n> +lint-style/heredoc.test:8: error: replace '! test_grep' with 'test_grep !': ! test_grep \"after-heredoc-is-caught\" actual\n> +lint-style/heredoc.test:13: error: replace '! test_grep' with 'test_grep !': ! test_grep \"not-inside-sed-heredoc\" actual\n> +hint: run with --fix to apply the suggested replacements.\n> diff --git a/t/lint-style/heredoc.test b/t/lint-style/heredoc.test\n> new file mode 100644\n> index 0000000000..4c05831cfb\n> --- /dev/null\n> +++ b/t/lint-style/heredoc.test\n> @@ -0,0 +1,14 @@\n> +test_expect_success 'greps inside heredocs are skipped' '\n> +       cat <<-EOF &&\n> +       grep \"inside-strip-tabs\" file\n> +       EOF\n> +       cat <<-\\EOF &&\n> +       grep \"inside-no-expand\" file\n> +       EOF\n> +       ! test_grep \"after-heredoc-is-caught\" actual\n> +'\n> +\n> +test_expect_success 'sed with << does not start a heredoc' '\n> +       sed \"s/<< foo/bar/\" file &&\n> +       ! test_grep \"not-inside-sed-heredoc\" actual\n> +'\n> diff --git a/t/lint-style/test-grep-negation-fix.expect b/t/lint-style/test-grep-negation-fix.expect\n> new file mode 100644\n> index 0000000000..28ecde1073\n> --- /dev/null\n> +++ b/t/lint-style/test-grep-negation-fix.expect\n> @@ -0,0 +1,4 @@\n> +test_expect_success 'negated test_grep' '\n> +       test_grep ! \"pattern\" actual &&\n> +       test_grep ! -i \"insensitive\" actual\n> +'\n> diff --git a/t/lint-style/test-grep-negation-fix.test b/t/lint-style/test-grep-negation-fix.test\n> new file mode 100644\n> index 0000000000..571c150031\n> --- /dev/null\n> +++ b/t/lint-style/test-grep-negation-fix.test\n> @@ -0,0 +1,4 @@\n> +test_expect_success 'negated test_grep' '\n> +       ! test_grep \"pattern\" actual &&\n> +       ! test_grep -i \"insensitive\" actual\n> +'\n> diff --git a/t/lint-style/test-grep-negation.expect b/t/lint-style/test-grep-negation.expect\n> new file mode 100644\n> index 0000000000..1fa9e124aa\n> --- /dev/null\n> +++ b/t/lint-style/test-grep-negation.expect\n> @@ -0,0 +1,3 @@\n> +lint-style/test-grep-negation.test:2: error: replace '! test_grep' with 'test_grep !': ! test_grep \"pattern\" actual\n> +lint-style/test-grep-negation.test:3: error: replace '! test_grep' with 'test_grep !': ! test_grep -i \"insensitive\" actual\n> +hint: run with --fix to apply the suggested replacements.\n> diff --git a/t/lint-style/test-grep-negation.test b/t/lint-style/test-grep-negation.test\n> new file mode 100644\n> index 0000000000..571c150031\n> --- /dev/null\n> +++ b/t/lint-style/test-grep-negation.test\n> @@ -0,0 +1,4 @@\n> +test_expect_success 'negated test_grep' '\n> +       ! test_grep \"pattern\" actual &&\n> +       ! test_grep -i \"insensitive\" actual\n> +'\n> diff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh\n> index 8ef87addf5..e9e2f04049 100755\n> --- a/t/t0031-lockfile-pid.sh\n> +++ b/t/t0031-lockfile-pid.sh\n> @@ -29,7 +29,7 @@ test_expect_success 'PID info not shown by default' '\n>                 test_must_fail git add . 2>err &&\n>                 # Should not crash, just show normal error without PID\n>                 test_grep \"Unable to create\" err &&\n> -               ! test_grep \"is held by process\" err\n> +               test_grep ! \"is held by process\" err\n>         )\n>  '\n>\n> diff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh\n> index 73445782e7..3179b4963e 100755\n> --- a/t/t5300-pack-object.sh\n> +++ b/t/t5300-pack-object.sh\n> @@ -720,7 +720,7 @@ test_expect_success '--name-hash-version=2 and --write-bitmap-index are incompat\n>\n>         # --stdout option silently removes --write-bitmap-index\n>         git pack-objects --stdout --all --name-hash-version=2 --write-bitmap-index >out 2>err &&\n> -       ! test_grep \"currently, --write-bitmap-index requires --name-hash-version=1\" err\n> +       test_grep ! \"currently, --write-bitmap-index requires --name-hash-version=1\" err\n>  '\n>\n>  test_expect_success '--path-walk pack everything' '\n> diff --git a/t/t5319-multi-pack-index.sh b/t/t5319-multi-pack-index.sh\n> index fa0d4046f7..9154d9795f 100755\n> --- a/t/t5319-multi-pack-index.sh\n> +++ b/t/t5319-multi-pack-index.sh\n> @@ -1175,7 +1175,7 @@ test_expect_success 'load reverse index when missing .idx, .pack' '\n>\n>  test_expect_success 'usage shown without sub-command' '\n>         test_expect_code 129 git multi-pack-index 2>err &&\n> -       ! test_grep \"unrecognized subcommand\" err\n> +       test_grep ! \"unrecognized subcommand\" err\n>  '\n>\n>  test_expect_success 'complains when run outside of a repository' '\n> diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> index d7f82e1bec..9db4a76f67 100755\n> --- a/t/t7900-maintenance.sh\n> +++ b/t/t7900-maintenance.sh\n> @@ -664,7 +664,7 @@ test_geometric_repack_needed () {\n>         true)\n>                 test_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n>         false)\n> -               ! test_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n> +               test_grep ! \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n>         *)\n>                 BUG \"invalid parameter: $NEEDED\";;\n>         esac\n> --\n> gitgitgadget\n>\n\n\n-- \nD. Ben Knoble\n"},{"id":"544763","messageId":"CAC2QwmKGkrMVPzYsZLtVrE3H+H7uYxhyX2wh2vgNVbR3sxGv2Q@mail.gmail.com","threadId":"65748","inReplyTo":"CALnO6CCjr5xMk=GLHSgf=KQpKJ1FnpimQCYu+BqyufWrRFkh8A@mail.gmail.com","subject":"Re: [PATCH 4/6] t: add lint-style.pl with test_grep negation rule","fromName":"Michael Montalbo","fromEmail":"mmontalbo@gmail.com","sentAt":"2026-06-04T19:36:49Z","receivedAt":"2026-06-04T19:37:01Z","isPatch":true,"body":"On Thu, Jun 4, 2026 at 11:35 AM D. Ben Knoble <ben.knoble@gmail.com> wrote:\n>\n> Hi Michael,\n>\n> This sounds like a neat effort!\n>\n> One drive-by comment…\n>\n> On Thu, Jun 4, 2026 at 3:46 AM Michael Montalbo via GitGitGadget\n> <gitgitgadget@gmail.com> wrote:\n> >\n> > From: Michael Montalbo <mmontalbo@gmail.com>\n> >\n> > Add a mechanical lint checker for test scripts, similar in spirit to\n> > check-non-portable-shell.pl but focused on test conventions rather\n> > than portability.\n> >\n> > The tool defines LintParser, a subclass of ScriptParser (from the\n> > shared lib-shell-parser.pl module).  ScriptParser's\n> > parse_cmd() finds test_expect_success blocks and calls check_test()\n> > for each body; LintParser overrides check_test() to run lint rules\n> > on the parsed commands.  A \"# lint-ok\" comment suppresses all\n> > checks for intentional style violations.\n> >\n> > The first rule detects '! test_grep' and replaces it with\n> > 'test_grep !'.  Shell-level negation suppresses the diagnostic\n> > output that test_grep prints on failure; the built-in negation\n> > preserves it.\n> >\n> > Three violations inside test bodies are converted via --fix.  One\n> > additional violation in a helper function outside test_expect_success\n> > (t7900's test_geometric_repack_needed) is converted manually, since\n> > the parser only processes test bodies.\n> >\n> > Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>\n> > ---\n> >  t/.gitattributes                           |   2 +\n> >  t/Makefile                                 |  32 +++-\n> >  t/lint-style.pl                            | 200 +++++++++++++++++++++\n> >  t/lint-style/heredoc.expect                |   3 +\n> >  t/lint-style/heredoc.test                  |  14 ++\n> >  t/lint-style/test-grep-negation-fix.expect |   4 +\n> >  t/lint-style/test-grep-negation-fix.test   |   4 +\n> >  t/lint-style/test-grep-negation.expect     |   3 +\n> >  t/lint-style/test-grep-negation.test       |   4 +\n> >  t/t0031-lockfile-pid.sh                    |   2 +-\n> >  t/t5300-pack-object.sh                     |   2 +-\n> >  t/t5319-multi-pack-index.sh                |   2 +-\n> >  t/t7900-maintenance.sh                     |   2 +-\n> >  13 files changed, 268 insertions(+), 6 deletions(-)\n> >  create mode 100755 t/lint-style.pl\n> >  create mode 100644 t/lint-style/heredoc.expect\n> >  create mode 100644 t/lint-style/heredoc.test\n> >  create mode 100644 t/lint-style/test-grep-negation-fix.expect\n> >  create mode 100644 t/lint-style/test-grep-negation-fix.test\n> >  create mode 100644 t/lint-style/test-grep-negation.expect\n> >  create mode 100644 t/lint-style/test-grep-negation.test\n> >\n> > diff --git a/t/.gitattributes b/t/.gitattributes\n> > index 7664c6e027..aea6889d03 100644\n> > --- a/t/.gitattributes\n> > +++ b/t/.gitattributes\n> > @@ -1,5 +1,7 @@\n> >  t[0-9][0-9][0-9][0-9]/* -whitespace\n> >  /chainlint/*.expect eol=lf -whitespace\n> > +/lint-style/*.expect eol=lf -whitespace\n> > +/lint-style/*.test eol=lf -whitespace\n> >  /t0110/url-* binary\n> >  /t3206/* eol=lf\n> >  /t3900/*.txt eol=lf\n> > diff --git a/t/Makefile b/t/Makefile\n> > index 25f923fed9..3a5fa4ce37 100644\n> > --- a/t/Makefile\n> > +++ b/t/Makefile\n> > @@ -46,6 +46,7 @@ TPERF = $(sort $(wildcard perf/p[0-9][0-9][0-9][0-9]-*.sh))\n> >  TINTEROP = $(sort $(wildcard interop/i[0-9][0-9][0-9][0-9]-*.sh))\n> >  CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.test)))\n> >  CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl\n> > +LINT_STYLE_TESTS = $(sort $(wildcard lint-style/*.test))\n> >  UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)\n> >  UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))\n> >  UNIT_TEST_PROGRAMS += unit-tests/bin/unit-tests$(X)\n> > @@ -139,7 +140,7 @@ check-meson:\n> >  test-lint: test-lint-duplicates test-lint-executable \\\n> >         test-lint-filenames\n> >  ifneq ($(PERL_PATH),)\n> > -test-lint: test-lint-shell-syntax check-shell-parser\n> > +test-lint: test-lint-shell-syntax test-lint-style check-lint-style check-shell-parser\n> >  else\n> >  GIT_TEST_CHAIN_LINT = 0\n> >  endif\n> > @@ -162,6 +163,32 @@ test-lint-shell-syntax:\n> >\n> >  check-shell-parser:\n> >         @'$(PERL_PATH_SQ)' check-shell-parser.pl\n> > +\n> > +test-lint-style:\n> > +       @'$(PERL_PATH_SQ)' lint-style.pl $(T) $(THELPERS) $(TPERF)\n> > +\n> > +check-lint-style:\n> > +       @rc=0; for t in $(LINT_STYLE_TESTS); do \\\n> > +               base=$${t%.test}; \\\n> > +               case $$base in \\\n> > +               *-fix) \\\n> > +                       cp \"$$t\" \"$$t.tmp\" && \\\n> > +                       '$(PERL_PATH_SQ)' lint-style.pl --fix \"$$t.tmp\" >/dev/null 2>&1; \\\n> > +                       fix_rc=$$?; \\\n> > +                       if test $$fix_rc != 0; then \\\n> > +                               echo \"FAIL: $$t (--fix exit code $$fix_rc)\"; rc=1; \\\n> > +                       elif ! diff -u \"$$base.expect\" \"$$t.tmp\"; then \\\n> > +                               echo \"FAIL: $$t (--fix output)\"; rc=1; \\\n> > +                       fi; \\\n> > +                       rm -f \"$$t.tmp\" ;; \\\n> > +               *) \\\n> > +                       if ! '$(PERL_PATH_SQ)' lint-style.pl \"$$t\" 2>&1 | \\\n> > +                               diff -u \"$$base.expect\" -; then \\\n> > +                               echo \"FAIL: $$t\"; rc=1; \\\n> > +                       fi ;; \\\n> > +               esac; \\\n> > +       done; test $$rc = 0\n> > +\n>\n> …I wonder if it would be easier to maintain this recipe as a separate\n> shell script and have make give LINT_STYLE_TESTS and PERL_PATH (w/o\n> SQ? idk) to the script. That's a lot of inline code otherwise!\n>\n\nYeah that's a good call out, will fix in a follow-up. Thank you for\ntaking a look!\n\n> >  test-lint-filenames:\n> >         @# We do *not* pass a glob to ls-files but use grep instead, to catch\n> >         @# non-ASCII characters (which are quoted within double-quotes)\n> > @@ -188,7 +215,8 @@ perf:\n> >\n> >  .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \\\n> >         check-chainlint clean-chainlint test-chainlint \\\n> > -       check-shell-parser $(UNIT_TESTS)\n> > +       check-shell-parser \\\n> > +       check-lint-style test-lint-style $(UNIT_TESTS)\n> >\n> >  .PHONY: libgit-sys-test libgit-rs-test\n> >  libgit-sys-test:\n> > diff --git a/t/lint-style.pl b/t/lint-style.pl\n> > new file mode 100755\n> > index 0000000000..9268577f9b\n> > --- /dev/null\n> > +++ b/t/lint-style.pl\n> > @@ -0,0 +1,200 @@\n> > +#!/usr/bin/perl\n> > +\n> > +# Check test scripts for style violations that can be detected\n> > +# mechanically, such as using bare 'grep' where test_grep should\n> > +# be used.  Use --fix to automatically apply suggested replacements.\n> > +#\n> > +# Detection uses parsed tokens from the shared shell parser for\n> > +# correct handling of heredocs, $(...), pipes, and quoting.\n> > +# Fixes modify the original file text to preserve formatting.\n> > +\n> > +use strict;\n> > +use warnings;\n> > +use File::Basename;\n> > +# Force LF output so check-lint-style's diff against the\n> > +# pre-committed .expect files works on Windows.\n> > +binmode(STDOUT, ':unix');\n> > +binmode(STDERR, ':unix');\n> > +\n> > +my $fix_mode = 0;\n> > +if (@ARGV && $ARGV[0] eq '--fix') {\n> > +       $fix_mode = 1;\n> > +       shift @ARGV;\n> > +}\n> > +\n> > +# Load the shared shell parser (Lexer, ShellParser, ScriptParser).\n> > +my $_lib = dirname($0) . \"/lib-shell-parser.pl\";\n> > +$_lib = \"./$_lib\" unless $_lib =~ m{^/};\n> > +do $_lib or die \"$0: failed to load $_lib: $@$!\\n\";\n> > +\n> > +# LintParser is a subclass of ScriptParser which runs lint rules\n> > +# on each test body.  Per-file state (file name, raw lines, dirty\n> > +# flag) is stored on the instance before calling parse().\n> > +#\n> > +# Subroutines defined below (parse_commands, check_test_grep_negation,\n> > +# etc.) are in package main and called with the main:: prefix.\n> > +# File-scoped lexicals ($fix_mode, $has_fixable, etc.) are visible\n> > +# across packages since 'package' does not introduce a new scope.\n> > +package LintParser;\n> > +our @ISA = ('ScriptParser');\n> > +\n> > +package main;\n> > +\n> > +my $exit_code = 0;\n> > +my $has_fixable = 0;\n> > +\n> > +sub err {\n> > +       my ($file, $lineno, $line, $msg, %opts) = @_;\n> > +       $line =~ s/^\\s+//;\n> > +       $line =~ s/\\s+$//;\n> > +       $line =~ s/\\s+/ /g;\n> > +       my $prefix = ($fix_mode && $opts{fixable}) ? 'fixed' : 'error';\n> > +       print \"$file:$lineno: $prefix: $msg: $line\\n\";\n> > +       $exit_code = 1 unless $fix_mode && $opts{fixable};\n> > +}\n> > +\n> > +# Report a lint violation found by a rule.  In --fix mode, apply\n> > +# the regex substitution on the raw line and report success.\n> > +# Otherwise just report.  Returns 1 if the line was modified.\n> > +sub report_violation {\n> > +       my ($file, $cmd, $line_ref, $match, $fix, $from) = @_;\n> > +       my $lineno = $cmd->{lineno};\n> > +       my $display = join(' ', @{$cmd->{tokens}});\n> > +       $has_fixable++;  # count for the \"--fix\" hint\n> > +       if ($fix_mode) {\n> > +               if ($$line_ref =~ s/$match/$fix/) {\n> > +                       err $file, $lineno, $display,\n> > +                               \"replace '$from' with '$fix'\",\n> > +                               fixable => 1;\n> > +                       return 1;\n> > +               }\n> > +               err $file, $lineno, $display,\n> > +                       \"replace '$from' with '$fix' (could not auto-fix)\";\n> > +       } else {\n> > +               err $file, $lineno, $display,\n> > +                       \"replace '$from' with '$fix'\";\n> > +       }\n> > +       return 0;\n> > +}\n> > +\n> > +# Split a token stream into commands at &&, ||, ;;, and \\n.\n> > +sub parse_commands {\n> > +       my ($content) = @_;\n> > +       my $parser = ShellParser->new(\\$content);\n> > +       my @all_tokens = $parser->parse();\n> > +\n> > +       my @commands;\n> > +       my @current;\n> > +       my $lineno = 1;\n> > +\n> > +       for (my $ti = 0; $ti < @all_tokens; $ti++) {\n> > +               my $text = $all_tokens[$ti]->[0];\n> > +               if ($text =~ /^(?:&&|\\|\\||;;|\\n)$/) {\n> > +                       if (@current) {\n> > +                               push @commands, {\n> > +                                       tokens => [@current],\n> > +                                       lineno => $lineno,\n> > +                               };\n> > +                               @current = ();\n> > +                       }\n> > +               } else {\n> > +                       $lineno = $all_tokens[$ti]->[3]\n> > +                               if !@current && defined $all_tokens[$ti]->[3];\n> > +                       push @current, $text;\n> > +               }\n> > +       }\n> > +       if (@current) {\n> > +               push @commands, {\n> > +                       tokens => [@current],\n> > +                       lineno => $lineno,\n> > +               };\n> > +       }\n> > +       return @commands;\n> > +}\n> > +\n> > +# --- Rule: '! test_grep' should be 'test_grep !' ---\n> > +# Shell-level negation suppresses test_grep's diagnostic output\n> > +# on failure.  Built-in negation preserves it.\n> > +sub check_test_grep_negation {\n> > +       my ($cmd, $file, $line_ref) = @_;\n> > +       my @tokens = @{$cmd->{tokens}};\n> > +       return unless @tokens >= 2 && $tokens[0] eq '!' && $tokens[1] eq 'test_grep';\n> > +\n> > +       return report_violation($file, $cmd, $line_ref,\n> > +               qr/!\\s*test_grep/, 'test_grep !', '! test_grep');\n> > +}\n> > +\n> > +# Map parsed commands back to raw file lines for --fix.\n> > +# Detection uses parsed tokens (correct handling of quoting,\n> > +# heredocs, pipes) but fixes must modify the original text\n> > +# to preserve formatting.\n> > +package LintParser;\n> > +\n> > +sub check_test {\n> > +       # Called by ScriptParser::parse_cmd for each test_expect_success\n> > +       # or test_expect_failure block.\n> > +       my $self = shift @_;\n> > +       my $title = ScriptParser::unwrap(shift @_);\n> > +\n> > +       # Two test body formats:\n> > +       #   Quoted:  test_expect_success 'title' '..body..'\n> > +       #   Heredoc: test_expect_success 'title' - <<\\EOF\n> > +       #              ..body..\n> > +       #            EOF\n> > +       # For quoted, the body token is the quoted string.\n> > +       # For heredoc, the body token is '-' and the actual\n> > +       # code arrives as the next argument from the Lexer.\n> > +       my $body_token = shift @_;\n> > +       my $lineno_base = $body_token->[3] || 1;\n> > +       my $body = ScriptParser::unwrap($body_token);\n> > +\n> > +       if ($body eq '-') {\n> > +               my $herebody = shift @_;\n> > +               if ($herebody) {\n> > +                       $body = $herebody->{content};\n> > +                       $lineno_base = $herebody->{start_line} || 1;\n> > +               }\n> > +       }\n> > +       return unless $body;\n> > +\n> > +       # Map each command back to its file line number.\n> > +       # $lineno_base is where the body starts in the file;\n> > +       # $cmd->{lineno} is relative to the body (starting at 1).\n> > +       my $raw_lines = $self->{raw_lines};\n> > +       for my $cmd (main::parse_commands($body)) {\n> > +               my $ln = ($cmd->{lineno} || 0) + $lineno_base - 1;\n> > +               $cmd->{lineno} = $ln;\n> > +               next unless $ln >= 1 && $ln <= @$raw_lines;\n> > +               next if $raw_lines->[$ln - 1] =~ /#.*lint-ok/;\n> > +\n> > +               if (main::check_test_grep_negation($cmd, $self->{file}, \\$raw_lines->[$ln - 1])) {\n> > +                       $self->{dirty} = 1;\n> > +               }\n> > +       }\n> > +}\n> > +\n> > +package main;\n> > +\n> > +for my $file (@ARGV) {\n> > +       # :unix:crlf strips \\r on Windows (same as chainlint.pl)\n> > +       open(my $fh, '<:unix:crlf', $file) or die \"$0: $file: $!\\n\";\n> > +       my @raw_lines = <$fh>;\n> > +       close $fh;\n> > +\n> > +       my $parser = LintParser->new(\\join('', @raw_lines));\n> > +       $parser->{file} = $file;\n> > +       $parser->{raw_lines} = \\@raw_lines;\n> > +       $parser->{dirty} = 0;\n> > +       $parser->parse();\n> > +\n> > +       if ($fix_mode && $parser->{dirty}) {\n> > +               open(my $out, '>', $file) or die \"$0: $file: $!\\n\";\n> > +               print $out @{$parser->{raw_lines}};\n> > +               close $out;\n> > +       }\n> > +}\n> > +\n> > +if ($has_fixable && !$fix_mode) {\n> > +       print \"hint: run with --fix to apply the suggested replacements.\\n\";\n> > +}\n> > +exit $exit_code;\n> > diff --git a/t/lint-style/heredoc.expect b/t/lint-style/heredoc.expect\n> > new file mode 100644\n> > index 0000000000..7ff6d4a52d\n> > --- /dev/null\n> > +++ b/t/lint-style/heredoc.expect\n> > @@ -0,0 +1,3 @@\n> > +lint-style/heredoc.test:8: error: replace '! test_grep' with 'test_grep !': ! test_grep \"after-heredoc-is-caught\" actual\n> > +lint-style/heredoc.test:13: error: replace '! test_grep' with 'test_grep !': ! test_grep \"not-inside-sed-heredoc\" actual\n> > +hint: run with --fix to apply the suggested replacements.\n> > diff --git a/t/lint-style/heredoc.test b/t/lint-style/heredoc.test\n> > new file mode 100644\n> > index 0000000000..4c05831cfb\n> > --- /dev/null\n> > +++ b/t/lint-style/heredoc.test\n> > @@ -0,0 +1,14 @@\n> > +test_expect_success 'greps inside heredocs are skipped' '\n> > +       cat <<-EOF &&\n> > +       grep \"inside-strip-tabs\" file\n> > +       EOF\n> > +       cat <<-\\EOF &&\n> > +       grep \"inside-no-expand\" file\n> > +       EOF\n> > +       ! test_grep \"after-heredoc-is-caught\" actual\n> > +'\n> > +\n> > +test_expect_success 'sed with << does not start a heredoc' '\n> > +       sed \"s/<< foo/bar/\" file &&\n> > +       ! test_grep \"not-inside-sed-heredoc\" actual\n> > +'\n> > diff --git a/t/lint-style/test-grep-negation-fix.expect b/t/lint-style/test-grep-negation-fix.expect\n> > new file mode 100644\n> > index 0000000000..28ecde1073\n> > --- /dev/null\n> > +++ b/t/lint-style/test-grep-negation-fix.expect\n> > @@ -0,0 +1,4 @@\n> > +test_expect_success 'negated test_grep' '\n> > +       test_grep ! \"pattern\" actual &&\n> > +       test_grep ! -i \"insensitive\" actual\n> > +'\n> > diff --git a/t/lint-style/test-grep-negation-fix.test b/t/lint-style/test-grep-negation-fix.test\n> > new file mode 100644\n> > index 0000000000..571c150031\n> > --- /dev/null\n> > +++ b/t/lint-style/test-grep-negation-fix.test\n> > @@ -0,0 +1,4 @@\n> > +test_expect_success 'negated test_grep' '\n> > +       ! test_grep \"pattern\" actual &&\n> > +       ! test_grep -i \"insensitive\" actual\n> > +'\n> > diff --git a/t/lint-style/test-grep-negation.expect b/t/lint-style/test-grep-negation.expect\n> > new file mode 100644\n> > index 0000000000..1fa9e124aa\n> > --- /dev/null\n> > +++ b/t/lint-style/test-grep-negation.expect\n> > @@ -0,0 +1,3 @@\n> > +lint-style/test-grep-negation.test:2: error: replace '! test_grep' with 'test_grep !': ! test_grep \"pattern\" actual\n> > +lint-style/test-grep-negation.test:3: error: replace '! test_grep' with 'test_grep !': ! test_grep -i \"insensitive\" actual\n> > +hint: run with --fix to apply the suggested replacements.\n> > diff --git a/t/lint-style/test-grep-negation.test b/t/lint-style/test-grep-negation.test\n> > new file mode 100644\n> > index 0000000000..571c150031\n> > --- /dev/null\n> > +++ b/t/lint-style/test-grep-negation.test\n> > @@ -0,0 +1,4 @@\n> > +test_expect_success 'negated test_grep' '\n> > +       ! test_grep \"pattern\" actual &&\n> > +       ! test_grep -i \"insensitive\" actual\n> > +'\n> > diff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh\n> > index 8ef87addf5..e9e2f04049 100755\n> > --- a/t/t0031-lockfile-pid.sh\n> > +++ b/t/t0031-lockfile-pid.sh\n> > @@ -29,7 +29,7 @@ test_expect_success 'PID info not shown by default' '\n> >                 test_must_fail git add . 2>err &&\n> >                 # Should not crash, just show normal error without PID\n> >                 test_grep \"Unable to create\" err &&\n> > -               ! test_grep \"is held by process\" err\n> > +               test_grep ! \"is held by process\" err\n> >         )\n> >  '\n> >\n> > diff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh\n> > index 73445782e7..3179b4963e 100755\n> > --- a/t/t5300-pack-object.sh\n> > +++ b/t/t5300-pack-object.sh\n> > @@ -720,7 +720,7 @@ test_expect_success '--name-hash-version=2 and --write-bitmap-index are incompat\n> >\n> >         # --stdout option silently removes --write-bitmap-index\n> >         git pack-objects --stdout --all --name-hash-version=2 --write-bitmap-index >out 2>err &&\n> > -       ! test_grep \"currently, --write-bitmap-index requires --name-hash-version=1\" err\n> > +       test_grep ! \"currently, --write-bitmap-index requires --name-hash-version=1\" err\n> >  '\n> >\n> >  test_expect_success '--path-walk pack everything' '\n> > diff --git a/t/t5319-multi-pack-index.sh b/t/t5319-multi-pack-index.sh\n> > index fa0d4046f7..9154d9795f 100755\n> > --- a/t/t5319-multi-pack-index.sh\n> > +++ b/t/t5319-multi-pack-index.sh\n> > @@ -1175,7 +1175,7 @@ test_expect_success 'load reverse index when missing .idx, .pack' '\n> >\n> >  test_expect_success 'usage shown without sub-command' '\n> >         test_expect_code 129 git multi-pack-index 2>err &&\n> > -       ! test_grep \"unrecognized subcommand\" err\n> > +       test_grep ! \"unrecognized subcommand\" err\n> >  '\n> >\n> >  test_expect_success 'complains when run outside of a repository' '\n> > diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh\n> > index d7f82e1bec..9db4a76f67 100755\n> > --- a/t/t7900-maintenance.sh\n> > +++ b/t/t7900-maintenance.sh\n> > @@ -664,7 +664,7 @@ test_geometric_repack_needed () {\n> >         true)\n> >                 test_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n> >         false)\n> > -               ! test_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n> > +               test_grep ! \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n> >         *)\n> >                 BUG \"invalid parameter: $NEEDED\";;\n> >         esac\n> > --\n> > gitgitgadget\n> >\n>\n>\n> --\n> D. Ben Knoble\n"},{"id":"544968","messageId":"xmqqldcovhnf.fsf@gitster.g","threadId":"65748","inReplyTo":"pull.2135.git.1780559158.gitgitgadget@gmail.com","subject":"Re: [PATCH 0/6] t: add lint-style.pl and convert grep to test_grep","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-08T21:36:36Z","receivedAt":"2026-06-08T21:36:38Z","isPatch":true,"body":"\"Michael Montalbo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> The test suite has a test_grep wrapper that prints file contents on\n> assertion failure, making debugging easier. Many tests still use bare 'grep'\n> for assertions, which silently swallows context on failure.\n>\n> This series adds a lint tool (lint-style.pl) to mechanically detect and\n> convert these, then applies it across the test suite.\n\nI do not think we want an automated tool that rewrites the source\nfiles.  I was hoping that we would get a patch or two that _adds_ to\nexisting test-lint framework (i.e., 'test-grep' that 'test-lint'\ntarget depends on in t/Makefile) that gives diagnosis in a similar\nfashion as test-lint-shell-syntax and test-chainlint do.\n\nAlso some existing uses of \"grep\" are not end-user facing and should\nnot be rewritten to \"test_grep\".\n"},{"id":"545433","messageId":"pull.2135.v2.git.1781323575.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.git.1780559158.gitgitgadget@gmail.com","subject":"[PATCH v2 0/6] t: add greplint.pl and convert grep to test_grep","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-13T04:06:09Z","receivedAt":"2026-06-13T04:06:20Z","isPatch":true,"body":"test_grep is a wrapper around grep for test assertions that prints the file\ncontents on failure for easier debugging. Bare grep fails silently, making\nit hard to diagnose what went wrong.\n\nThis series converts existing bare grep assertions to test_grep and adds\ngreplint.pl to prevent new ones from being introduced.\n\nPatch 1 documents test_grep in t/README.\n\nPatch 2 fixes three greps missing file arguments (t2402, t7507, t7700). They\nwere reading empty stdin and passing vacuously.\n\nPatch 3 extracts chainlint's Lexer, ShellParser, and ScriptParser into a\nshared module (lib-shell-parser.pl) so greplint.pl can reuse the same\ntokenizer. No functional change to chainlint.\n\nPatch 4 fixes a latent line-counting bug in scan_dqstring where newlines\nfrom $() bodies inside double-quoted strings were counted twice. This does\nnot affect chainlint (which uses byte offsets) but matters for greplint.pl's\nline-number reporting.\n\nPatch 5 converts existing assertion greps to test_grep, including sourced\ntest helpers. Greps used as data filters or on files that may not exist are\nleft unconverted with lint-ok annotations.\n\nPatch 6 adds greplint.pl with test fixtures (modeled on chainlint/) and\nwires it into the Makefile as test-greplint and check-greplint.\n\nChanges since v1:\n\n * Dropped lint-style.pl and the --fix mode concept. Replaced with\n   greplint.pl, which more closely follows chainlint's conventions, and\n   reuses its parser logic via a shared module.\n\n * A regex approach to grep linting was prototyped in an attempt to reduce\n   the number of patches in the series, but this approach produced false\n   positives from grep inside heredoc bodies (e.g. write_script) and\n   cross-line pipelines where the pipe or redirect is on a different line\n   from the grep. The shared module's Lexer already collapses these into\n   single tokens, giving zero false positives with less code than the regex\n   heuristics would need, which is why it was retained in the current\n   version.\n\n * Reverted incorrect conversions where grep was used as a data filter\n   inside redirected compound commands, not as a test assertion.\n\nKnown limitation / follow-up:\n\n * Assertions like grep pattern file >/dev/null and grep pattern <file are\n   not converted because greplint.pl treats any redirect as a filter. The\n   former is ambiguous because >/dev/null becomes dead code under test_grep\n   (which already suppresses matching-line output). The latter requires\n   removing the redirect and passing the file as a positional argument,\n   since test_grep does not support stdin redirects. Both are left as bare\n   grep. A follow-up series can address these once a convention is\n   established.\n\nMichael Montalbo (6):\n  t/README: document test_grep helper\n  t: fix grep assertions missing file arguments\n  t: extract chainlint's parser into shared module\n  t: fix Lexer line count for $() inside double-quoted strings\n  t: convert grep assertions to test_grep\n  t: add greplint to detect bare grep assertions\n\n t/.gitattributes                              |   2 +\n t/Makefile                                    |  29 +-\n t/README                                      |  21 +\n t/chainlint.pl                                | 529 +----------------\n t/for-each-ref-tests.sh                       |  12 +-\n t/greplint-cat.pl                             |  27 +\n t/greplint.pl                                 | 241 ++++++++\n t/greplint/bare-grep-after-and.expect         |   1 +\n t/greplint/bare-grep-after-and.test           |   4 +\n t/greplint/bare-grep-after-semicolon.expect   |   1 +\n t/greplint/bare-grep-after-semicolon.test     |   4 +\n t/greplint/bare-grep-compound-body.expect     |   3 +\n t/greplint/bare-grep-compound-body.test       |  17 +\n t/greplint/bare-grep-count-mode.expect        |   1 +\n t/greplint/bare-grep-count-mode.test          |   3 +\n t/greplint/bare-grep-explicit-pattern.expect  |   1 +\n t/greplint/bare-grep-explicit-pattern.test    |   3 +\n t/greplint/bare-grep-flags.expect             |   1 +\n t/greplint/bare-grep-flags.test               |   3 +\n t/greplint/bare-grep-lint-ok.expect           |   0\n t/greplint/bare-grep-lint-ok.test             |   4 +\n t/greplint/bare-grep-negated.expect           |   1 +\n t/greplint/bare-grep-negated.test             |   3 +\n t/greplint/bare-grep-pattern-file.expect      |   1 +\n t/greplint/bare-grep-pattern-file.test        |   3 +\n t/greplint/bare-grep-simple.expect            |   1 +\n t/greplint/bare-grep-simple.test              |   3 +\n t/greplint/bare-grep-subshell.expect          |   1 +\n t/greplint/bare-grep-subshell.test            |   5 +\n .../dqstring-continuation-offset.expect       |   1 +\n t/greplint/dqstring-continuation-offset.test  |  11 +\n t/greplint/filter-command-substitution.expect |   0\n t/greplint/filter-command-substitution.test   |   3 +\n t/greplint/filter-pipe-input.expect           |   0\n t/greplint/filter-pipe-input.test             |   3 +\n t/greplint/filter-pipe-output.expect          |   0\n t/greplint/filter-pipe-output.test            |   3 +\n t/greplint/filter-redirect-output.expect      |   0\n t/greplint/filter-redirect-output.test        |   3 +\n t/greplint/filter-stdin-redirect.expect       |   0\n t/greplint/filter-stdin-redirect.test         |   3 +\n t/greplint/grep-as-argument.expect            |   0\n t/greplint/grep-as-argument.test              |   3 +\n t/greplint/grep-as-value.expect               |   0\n t/greplint/grep-as-value.test                 |   6 +\n t/greplint/wrong-negation.expect              |   1 +\n t/greplint/wrong-negation.test                |   3 +\n t/lib-bitmap.sh                               |  12 +-\n t/lib-bundle-uri-protocol.sh                  |  26 +-\n t/lib-httpd.sh                                |   2 +-\n t/lib-shell-parser.pl                         | 534 ++++++++++++++++++\n t/pack-refs-tests.sh                          |   2 +-\n t/show-ref-exists-tests.sh                    |   2 +-\n t/t0000-basic.sh                              |  16 +-\n t/t0001-init.sh                               |  18 +-\n t/t0008-ignores.sh                            |   8 +-\n t/t0009-git-dir-validation.sh                 |   6 +-\n t/t0012-help.sh                               |   4 +-\n t/t0013-sha1dc.sh                             |   2 +-\n t/t0017-env-helper.sh                         |   4 +-\n t/t0021-conversion.sh                         |  18 +-\n t/t0029-core-unsetenvvars.sh                  |   4 +-\n t/t0030-stripspace.sh                         |   4 +-\n t/t0031-lockfile-pid.sh                       |   2 +-\n t/t0040-parse-options.sh                      |  52 +-\n t/t0041-usage.sh                              |   2 +-\n t/t0052-simple-ipc.sh                         |  10 +-\n t/t0061-run-command.sh                        |   2 +-\n t/t0066-dir-iterator.sh                       |   2 +-\n t/t0068-for-each-repo.sh                      |  16 +-\n t/t0070-fundamental.sh                        |   6 +-\n t/t0081-find-pack.sh                          |  12 +-\n t/t0091-bugreport.sh                          |  18 +-\n t/t0092-diagnose.sh                           |  12 +-\n t/t0100-previous.sh                           |   2 +-\n t/t0200-gettext-basic.sh                      |  14 +-\n t/t0203-gettext-setlocale-sanity.sh           |   4 +-\n t/t0204-gettext-reencode-sanity.sh            |   8 +-\n t/t0210-trace2-normal.sh                      |   6 +-\n t/t0211-trace2-perf.sh                        |  80 +--\n t/t0212-trace2-event.sh                       |   8 +-\n t/t0300-credentials.sh                        |   4 +-\n t/t0410-partial-clone.sh                      |  82 +--\n t/t0450-txt-doc-vs-help.sh                    |   2 +-\n t/t0500-progress-display.sh                   |  18 +-\n t/t0610-reftable-basics.sh                    |   8 +-\n t/t1004-read-tree-m-u-wf.sh                   |   8 +-\n t/t1006-cat-file.sh                           |  18 +-\n t/t1007-hash-object.sh                        |   8 +-\n t/t1011-read-tree-sparse-checkout.sh          |  10 +-\n t/t1050-large.sh                              |   6 +-\n t/t1091-sparse-checkout-builtin.sh            |  24 +-\n t/t1092-sparse-checkout-compatibility.sh      |  44 +-\n t/t1300-config.sh                             |  16 +-\n t/t1305-config-include.sh                     |   2 +-\n t/t1308-config-set.sh                         |   6 +-\n t/t1400-update-ref.sh                         | 170 +++---\n t/t1403-show-ref.sh                           |  18 +-\n t/t1410-reflog.sh                             |   4 +-\n t/t1415-worktree-refs.sh                      |   4 +-\n t/t1430-bad-ref-name.sh                       |  56 +-\n t/t1450-fsck.sh                               |  12 +-\n t/t1451-fsck-buffer.sh                        |   6 +-\n t/t1460-refs-migrate.sh                       |   2 +-\n t/t1500-rev-parse.sh                          |   6 +-\n t/t1502-rev-parse-parseopt.sh                 |   2 +-\n t/t1503-rev-parse-verify.sh                   |  10 +-\n t/t1510-repo-setup.sh                         |  10 +-\n t/t1512-rev-parse-disambiguation.sh           |   4 +-\n t/t1515-rev-parse-outside-repo.sh             |   2 +-\n t/t1800-hook.sh                               |  18 +-\n t/t2004-checkout-cache-temp.sh                |   4 +-\n t/t2019-checkout-ambiguous-ref.sh             |   4 +-\n t/t2024-checkout-dwim.sh                      |   8 +-\n t/t2030-unresolve-info.sh                     |   6 +-\n t/t2060-switch.sh                             |   6 +-\n t/t2070-restore.sh                            |   2 +-\n t/t2080-parallel-checkout-basics.sh           |  14 +-\n t/t2081-parallel-checkout-collisions.sh       |  24 +-\n t/t2082-parallel-checkout-attributes.sh       |  12 +-\n t/t2103-update-index-ignore-missing.sh        |   6 +-\n t/t2200-add-update.sh                         |   2 +-\n t/t2203-add-intent.sh                         |   6 +-\n t/t2400-worktree-add.sh                       |  24 +-\n t/t2402-worktree-list.sh                      |  16 +-\n t/t2403-worktree-move.sh                      |   6 +-\n t/t2405-worktree-submodule.sh                 |   6 +-\n t/t2407-worktree-heads.sh                     |  26 +-\n t/t2500-untracked-overwriting.sh              |   8 +-\n t/t2501-cwd-empty.sh                          |   4 +-\n t/t3001-ls-files-others-exclude.sh            |   6 +-\n t/t3007-ls-files-recurse-submodules.sh        |   6 +-\n t/t3200-branch.sh                             |  12 +-\n t/t3202-show-branch.sh                        |  10 +-\n t/t3203-branch-output.sh                      |   4 +-\n t/t3206-range-diff.sh                         |  78 +--\n t/t3207-branch-submodule.sh                   |   4 +-\n t/t3301-notes.sh                              |  32 +-\n t/t3310-notes-merge-manual-resolve.sh         |  16 +-\n t/t3320-notes-merge-worktrees.sh              |   2 +-\n t/t3400-rebase.sh                             |  16 +-\n t/t3402-rebase-merge.sh                       |  16 +-\n t/t3404-rebase-interactive.sh                 |  72 +--\n t/t3406-rebase-message.sh                     |   6 +-\n t/t3415-rebase-autosquash.sh                  |  10 +-\n t/t3416-rebase-onto-threedots.sh              |   4 +-\n t/t3418-rebase-continue.sh                    |  10 +-\n t/t3420-rebase-autostash.sh                   |  26 +-\n t/t3422-rebase-incompatible-options.sh        |   4 +-\n t/t3429-rebase-edit-todo.sh                   |   2 +-\n t/t3430-rebase-merges.sh                      |  32 +-\n t/t3500-cherry.sh                             |   4 +-\n t/t3501-revert-cherry-pick.sh                 |   6 +-\n t/t3504-cherry-pick-rerere.sh                 |   6 +-\n t/t3510-cherry-pick-sequence.sh               |  24 +-\n t/t3602-rm-sparse-checkout.sh                 |   4 +-\n t/t3705-add-sparse-checkout.sh                |  10 +-\n t/t3800-mktag.sh                              |   4 +-\n t/t3901-i18n-patch.sh                         |  16 +-\n t/t3903-stash.sh                              |  28 +-\n t/t3904-stash-patch.sh                        |   4 +-\n t/t3908-stash-in-worktree.sh                  |   2 +-\n t/t4000-diff-format.sh                        |   2 +-\n t/t4001-diff-rename.sh                        |   4 +-\n t/t4011-diff-symlink.sh                       |   2 +-\n t/t4013-diff-various.sh                       |   2 +-\n t/t4014-format-patch.sh                       | 344 +++++------\n t/t4015-diff-whitespace.sh                    |  16 +-\n t/t4017-diff-retval.sh                        |   2 +-\n t/t4018-diff-funcname.sh                      |   2 +-\n t/t4019-diff-wserror.sh                       |   8 +-\n t/t4020-diff-external.sh                      |  18 +-\n t/t4021-format-patch-numbered.sh              |   4 +-\n t/t4022-diff-rewrite.sh                       |  14 +-\n t/t4028-format-patch-mime-headers.sh          |   6 +-\n t/t4031-diff-rewrite-binary.sh                |  18 +-\n t/t4033-diff-patience.sh                      |   2 +-\n t/t4036-format-patch-signer-mime.sh           |   6 +-\n t/t4038-diff-combined.sh                      |   6 +-\n t/t4051-diff-function-context.sh              |  38 +-\n t/t4053-diff-no-index.sh                      |   4 +-\n t/t4063-diff-blobs.sh                         |   2 +-\n t/t4065-diff-anchored.sh                      |  26 +-\n t/t4067-diff-partial-clone.sh                 |  12 +-\n t/t4073-diff-stat-name-width.sh               |  24 +-\n t/t4103-apply-binary.sh                       |   2 +-\n t/t4120-apply-popt.sh                         |   2 +-\n t/t4124-apply-ws-rule.sh                      |  10 +-\n t/t4128-apply-root.sh                         |   2 +-\n t/t4140-apply-ita.sh                          |   4 +-\n t/t4141-apply-too-large.sh                    |   2 +-\n t/t4150-am.sh                                 |  48 +-\n t/t4200-rerere.sh                             |   6 +-\n t/t4201-shortlog.sh                           |   2 +-\n t/t4202-log.sh                                |  84 +--\n t/t4204-patch-id.sh                           |   2 +-\n t/t4205-log-pretty-formats.sh                 |   2 +-\n t/t4209-log-pickaxe.sh                        |  10 +-\n t/t4211-line-log.sh                           |  72 +--\n t/t4216-log-bloom.sh                          |  18 +-\n t/t4252-am-options.sh                         |  22 +-\n t/t4254-am-corrupt.sh                         |   6 +-\n t/t4258-am-quoted-cr.sh                       |   2 +-\n t/t4301-merge-tree-write-tree.sh              |  18 +-\n t/t5000-tar-tree.sh                           |  10 +-\n t/t5004-archive-corner-cases.sh               |   2 +-\n t/t5100-mailinfo.sh                           |   2 +-\n t/t5150-request-pull.sh                       |  18 +-\n t/t5300-pack-object.sh                        |  22 +-\n t/t5302-pack-index.sh                         |   6 +-\n t/t5304-prune.sh                              |   8 +-\n t/t5310-pack-bitmaps.sh                       |  14 +-\n t/t5317-pack-objects-filter-objects.sh        |  12 +-\n t/t5318-commit-graph.sh                       |   8 +-\n t/t5319-multi-pack-index.sh                   |  16 +-\n t/t5324-split-commit-graph.sh                 |  10 +-\n t/t5325-reverse-index.sh                      |   2 +-\n t/t5326-multi-pack-bitmaps.sh                 |  28 +-\n t/t5328-commit-graph-64bit-time.sh            |   2 +-\n t/t5329-pack-objects-cruft.sh                 |   8 +-\n t/t5334-incremental-multi-pack-index.sh       |   2 +-\n t/t5335-compact-multi-pack-index.sh           |   4 +-\n t/t5351-unpack-large-objects.sh               |   2 +-\n t/t5402-post-merge-hook.sh                    |   4 +-\n t/t5403-post-checkout-hook.sh                 |   2 +-\n t/t5404-tracking-branches.sh                  |   2 +-\n t/t5406-remote-rejects.sh                     |   2 +-\n t/t5407-post-rewrite-hook.sh                  |   8 +-\n t/t5409-colorize-remote-messages.sh           |  36 +-\n t/t5500-fetch-pack.sh                         |  38 +-\n t/t5504-fetch-receive-strict.sh               |  14 +-\n t/t5505-remote.sh                             |  20 +-\n t/t5510-fetch.sh                              |  10 +-\n t/t5512-ls-remote.sh                          |   8 +-\n t/t5514-fetch-multiple.sh                     |   2 +-\n t/t5516-fetch-push.sh                         |  20 +-\n t/t5520-pull.sh                               |   4 +-\n t/t5524-pull-msg.sh                           |   6 +-\n t/t5526-fetch-submodules.sh                   |  16 +-\n t/t5529-push-errors.sh                        |   4 +-\n t/t5530-upload-pack-error.sh                  |  18 +-\n t/t5531-deep-submodule-push.sh                |   2 +-\n t/t5532-fetch-proxy.sh                        |   2 +-\n t/t5533-push-cas.sh                           |  12 +-\n t/t5534-push-signed.sh                        |  22 +-\n t/t5537-fetch-shallow.sh                      |   2 +-\n t/t5538-push-shallow.sh                       |   2 +-\n t/t5539-fetch-http-shallow.sh                 |   4 +-\n t/t5541-http-push-smart.sh                    |  32 +-\n t/t5544-pack-objects-hook.sh                  |  12 +-\n t/t5550-http-fetch-dumb.sh                    |   4 +-\n t/t5551-http-fetch-smart.sh                   |  46 +-\n t/t5552-skipping-fetch-negotiator.sh          |   6 +-\n t/t5554-noop-fetch-negotiator.sh              |   4 +-\n t/t5557-http-get.sh                           |   2 +-\n t/t5558-clone-bundle-uri.sh                   |  38 +-\n t/t5562-http-backend-content-length.sh        |   2 +-\n t/t5564-http-proxy.sh                         |  10 +-\n t/t5581-http-curl-verbose.sh                  |   2 +-\n t/t5583-push-branches.sh                      |   8 +-\n t/t5601-clone.sh                              |  28 +-\n t/t5604-clone-reference.sh                    |   8 +-\n t/t5605-clone-local.sh                        |   2 +-\n t/t5606-clone-options.sh                      |   6 +-\n t/t5612-clone-refspec.sh                      |   2 +-\n t/t5616-partial-clone.sh                      |  60 +-\n t/t5619-clone-local-ambiguous-transport.sh    |   2 +-\n t/t5620-backfill.sh                           |  12 +-\n t/t5700-protocol-v1.sh                        |  46 +-\n t/t5701-git-serve.sh                          |  14 +-\n t/t5702-protocol-v2.sh                        | 154 ++---\n t/t5703-upload-pack-ref-in-want.sh            |  22 +-\n t/t5705-session-id-in-capabilities.sh         |  12 +-\n t/t5750-bundle-uri-parse.sh                   |   8 +-\n t/t5801-remote-helpers.sh                     |   4 +-\n t/t5810-proto-disable-local.sh                |   2 +-\n t/t5813-proto-disable-ssh.sh                  |   4 +-\n t/t6000-rev-list-misc.sh                      |  26 +-\n t/t6005-rev-list-count.sh                     |   8 +-\n t/t6006-rev-list-format.sh                    |   4 +-\n t/t6009-rev-list-parent.sh                    |   4 +-\n t/t6020-bundle-misc.sh                        |  12 +-\n t/t6022-rev-list-missing.sh                   |   4 +-\n t/t6030-bisect-porcelain.sh                   | 150 ++---\n t/t6040-tracking-info.sh                      |   2 +-\n t/t6112-rev-list-filters-objects.sh           |  24 +-\n t/t6115-rev-list-du.sh                        |   4 +-\n t/t6120-describe.sh                           |  14 +-\n t/t6200-fmt-merge-msg.sh                      |  82 +--\n t/t6402-merge-rename.sh                       |   4 +-\n t/t6403-merge-file.sh                         |   6 +-\n t/t6404-recursive-merge.sh                    |   2 +-\n t/t6406-merge-attr.sh                         |  20 +-\n t/t6417-merge-ours-theirs.sh                  |  30 +-\n t/t6418-merge-text-auto.sh                    |   2 +-\n t/t6422-merge-rename-corner-cases.sh          |   8 +-\n t/t6423-merge-rename-directories.sh           |  72 +--\n t/t6424-merge-unrelated-index-changes.sh      |   6 +-\n t/t6427-diff3-conflict-markers.sh             |  10 +-\n t/t6432-merge-recursive-space-options.sh      |   4 +-\n t/t6436-merge-overwrite.sh                    |   6 +-\n t/t6437-submodule-merge.sh                    |  10 +-\n t/t6500-gc.sh                                 |   8 +-\n t/t6600-test-reach.sh                         |   4 +-\n t/t7001-mv.sh                                 |  16 +-\n t/t7002-mv-sparse-checkout.sh                 |  38 +-\n t/t7003-filter-branch.sh                      |  16 +-\n t/t7004-tag.sh                                |   2 +-\n t/t7006-pager.sh                              |  16 +-\n t/t7012-skip-worktree-writing.sh              |   6 +-\n t/t7030-verify-tag.sh                         |  52 +-\n t/t7031-verify-tag-signed-ssh.sh              |  46 +-\n t/t7102-reset.sh                              |   2 +-\n t/t7110-reset-merge.sh                        |  40 +-\n t/t7201-co.sh                                 |   6 +-\n t/t7300-clean.sh                              |   2 +-\n t/t7301-clean-interactive.sh                  |   2 +-\n t/t7400-submodule-basic.sh                    |  32 +-\n t/t7402-submodule-rebase.sh                   |   2 +-\n t/t7406-submodule-update.sh                   |  26 +-\n t/t7416-submodule-dash-url.sh                 |  20 +-\n t/t7417-submodule-path-url.sh                 |   2 +-\n t/t7450-bad-git-dotfiles.sh                   |  14 +-\n t/t7501-commit-basic-functionality.sh         |  16 +-\n t/t7502-commit-porcelain.sh                   |   2 +-\n t/t7507-commit-verbose.sh                     |   6 +-\n t/t7508-status.sh                             |   6 +-\n t/t7510-signed-commit.sh                      |  68 +--\n t/t7516-commit-races.sh                       |   4 +-\n t/t7519-status-fsmonitor.sh                   |  14 +-\n t/t7527-builtin-fsmonitor.sh                  |  82 +--\n t/t7528-signed-commit-ssh.sh                  |  68 +--\n t/t7600-merge.sh                              |  10 +-\n t/t7603-merge-reduce-heads.sh                 |  20 +-\n t/t7606-merge-custom.sh                       |   2 +-\n t/t7607-merge-state.sh                        |   4 +-\n t/t7610-mergetool.sh                          |  18 +-\n t/t7700-repack.sh                             |  14 +-\n t/t7703-repack-geometric.sh                   |   4 +-\n t/t7704-repack-cruft.sh                       |  12 +-\n t/t7800-difftool.sh                           |  26 +-\n t/t7810-grep.sh                               |  22 +-\n t/t7814-grep-recurse-submodules.sh            |   2 +-\n t/t7900-maintenance.sh                        |  34 +-\n t/t8008-blame-formats.sh                      |   2 +-\n t/t8010-cat-file-filters.sh                   |   2 +-\n t/t8012-blame-colors.sh                       |   2 +-\n t/t9001-send-email.sh                         | 190 +++----\n t/t9003-help-autocorrect.sh                   |   6 +-\n t/t9106-git-svn-commit-diff-clobber.sh        |   2 +-\n t/t9107-git-svn-migrate.sh                    |  30 +-\n t/t9110-git-svn-use-svm-props.sh              |  20 +-\n t/t9111-git-svn-use-svnsync-props.sh          |  18 +-\n t/t9114-git-svn-dcommit-merge.sh              |   6 +-\n t/t9116-git-svn-log.sh                        |   8 +-\n t/t9117-git-svn-init-clone.sh                 |  12 +-\n t/t9119-git-svn-info.sh                       |  16 +-\n t/t9122-git-svn-author.sh                     |   8 +-\n t/t9130-git-svn-authors-file.sh               |   8 +-\n t/t9138-git-svn-authors-prog.sh               |  14 +-\n t/t9140-git-svn-reset.sh                      |   4 +-\n t/t9153-git-svn-rewrite-uuid.sh               |   4 +-\n t/t9200-git-cvsexportcommit.sh                |   2 +-\n t/t9210-scalar.sh                             |  34 +-\n t/t9211-scalar-clone.sh                       |  16 +-\n t/t9300-fast-import.sh                        |  10 +-\n t/t9350-fast-export.sh                        |  54 +-\n t/t9351-fast-export-anonymize.sh              |  36 +-\n t/t9400-git-cvsserver-server.sh               |   4 +-\n t/t9501-gitweb-standalone-http-status.sh      |  58 +-\n t/t9502-gitweb-standalone-parse-output.sh     |  38 +-\n t/t9800-git-p4-basic.sh                       |  10 +-\n t/t9801-git-p4-branch.sh                      |  48 +-\n t/t9806-git-p4-options.sh                     |  10 +-\n t/t9807-git-p4-submit.sh                      |   2 +-\n t/t9810-git-p4-rcs.sh                         |   8 +-\n t/t9813-git-p4-preserve-users.sh              |   8 +-\n t/t9814-git-p4-rename.sh                      |   8 +-\n t/t9827-git-p4-change-filetype.sh             |   4 +-\n t/t9832-unshelve.sh                           |   6 +-\n t/t9833-errors.sh                             |   4 +-\n t/t9835-git-p4-metadata-encoding-python2.sh   |  36 +-\n t/t9836-git-p4-metadata-encoding-python3.sh   |  38 +-\n t/t9850-shell.sh                              |   2 +-\n t/t9902-completion.sh                         |  26 +-\n 385 files changed, 3772 insertions(+), 3329 deletions(-)\n create mode 100644 t/greplint-cat.pl\n create mode 100644 t/greplint.pl\n create mode 100644 t/greplint/bare-grep-after-and.expect\n create mode 100644 t/greplint/bare-grep-after-and.test\n create mode 100644 t/greplint/bare-grep-after-semicolon.expect\n create mode 100644 t/greplint/bare-grep-after-semicolon.test\n create mode 100644 t/greplint/bare-grep-compound-body.expect\n create mode 100644 t/greplint/bare-grep-compound-body.test\n create mode 100644 t/greplint/bare-grep-count-mode.expect\n create mode 100644 t/greplint/bare-grep-count-mode.test\n create mode 100644 t/greplint/bare-grep-explicit-pattern.expect\n create mode 100644 t/greplint/bare-grep-explicit-pattern.test\n create mode 100644 t/greplint/bare-grep-flags.expect\n create mode 100644 t/greplint/bare-grep-flags.test\n create mode 100644 t/greplint/bare-grep-lint-ok.expect\n create mode 100644 t/greplint/bare-grep-lint-ok.test\n create mode 100644 t/greplint/bare-grep-negated.expect\n create mode 100644 t/greplint/bare-grep-negated.test\n create mode 100644 t/greplint/bare-grep-pattern-file.expect\n create mode 100644 t/greplint/bare-grep-pattern-file.test\n create mode 100644 t/greplint/bare-grep-simple.expect\n create mode 100644 t/greplint/bare-grep-simple.test\n create mode 100644 t/greplint/bare-grep-subshell.expect\n create mode 100644 t/greplint/bare-grep-subshell.test\n create mode 100644 t/greplint/dqstring-continuation-offset.expect\n create mode 100644 t/greplint/dqstring-continuation-offset.test\n create mode 100644 t/greplint/filter-command-substitution.expect\n create mode 100644 t/greplint/filter-command-substitution.test\n create mode 100644 t/greplint/filter-pipe-input.expect\n create mode 100644 t/greplint/filter-pipe-input.test\n create mode 100644 t/greplint/filter-pipe-output.expect\n create mode 100644 t/greplint/filter-pipe-output.test\n create mode 100644 t/greplint/filter-redirect-output.expect\n create mode 100644 t/greplint/filter-redirect-output.test\n create mode 100644 t/greplint/filter-stdin-redirect.expect\n create mode 100644 t/greplint/filter-stdin-redirect.test\n create mode 100644 t/greplint/grep-as-argument.expect\n create mode 100644 t/greplint/grep-as-argument.test\n create mode 100644 t/greplint/grep-as-value.expect\n create mode 100644 t/greplint/grep-as-value.test\n create mode 100644 t/greplint/wrong-negation.expect\n create mode 100644 t/greplint/wrong-negation.test\n create mode 100644 t/lib-shell-parser.pl\n\n\nbase-commit: 1ff279f3404a482a83fb04c7457e41ab26884aea\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2135%2Fmmontalbo%2Fmm%2Ftest-grep-docs-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2135/mmontalbo/mm/test-grep-docs-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/2135\n\nRange-diff vs v1:\n\n 1:  43402040bf = 1:  5959cab258 t/README: document test_grep helper\n 5:  c0db9fdb5b = 2:  f3e8e19e6e t: fix grep assertions missing file arguments\n 2:  a1069efa8f ! 3:  460461b5fe t: extract chainlint's parser into shared module\n     @@ Metadata\n       ## Commit message ##\n          t: extract chainlint's parser into shared module\n      \n     -    Move the Lexer, ShellParser, and ScriptParser packages from\n     -    chainlint.pl into t/lib-shell-parser.pl so they can be reused by\n     -    other tools.  ScriptParser's check_test() is a no-op in the shared\n     -    module; callers subclass ScriptParser and override it.\n     +    Move chainlint.pl's Lexer, ShellParser, and ScriptParser into a\n     +    shared module (lib-shell-parser.pl) so other lint tools can reuse\n     +    the same shell parsing infrastructure.  A subsequent commit adds\n     +    greplint.pl, which needs the same tokenizer to correctly identify\n     +    command boundaries.\n      \n     -    chainlint.pl defines TestParser (&&-chain detection) and\n     -    ChainlintParser (a ScriptParser subclass whose check_test runs\n     -    TestParser and formats the results).  The shared module is loaded\n     -    via do() for portability with minimal Perl installations.\n     +    ScriptParser's check_test() becomes a no-op in the shared module.\n     +    chainlint.pl defines ChainlintParser (extending ScriptParser)\n     +    with the &&-chain check_test() implementation.\n      \n     -    A subsequent commit introduces lint-style.pl which needs the same\n     -    shell parser to properly tokenize test scripts.  Sharing the parser\n     -    avoids reimplementing heredoc handling, $(...) nesting, pipe\n     -    tracking, quoting, and test body extraction.\n     +    No functional change: chainlint produces the same output and\n     +    check-chainlint self-tests pass.\n      \n          Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>\n      \n     @@ t/chainlint.pl: my $jobs = -1;\n      -\treturn @tokens;\n      -}\n      +use File::Basename;\n     -+my $_lib = dirname($0) . \"/lib-shell-parser.pl\";\n     -+$_lib = \"./$_lib\" unless $_lib =~ m{^/};\n     -+do $_lib or die \"failed to load $_lib: $@$!\\n\";\n     ++do(dirname($0) . \"/lib-shell-parser.pl\")\n     ++\tor die \"$0: failed to load lib-shell-parser.pl: $@$!\\n\";\n       \n       # TestParser is a subclass of ShellParser which, beyond parsing shell script\n       # code, is also imbued with semantic knowledge of test construction, and checks\n      @@ t/chainlint.pl: DONE:\n     + # the tests themselves or in behaviors being exercised by the tests. As such,\n     + # TestParser is only called upon to parse test bodies, not the top-level\n       # scripts in which the tests are defined.\n     ++\n       package TestParser;\n       \n      -use base 'ShellParser';\n     @@ t/chainlint.pl: DONE:\n      -# at the top-level of test scripts but also within compound commands such as\n      -# loops and function definitions.\n      -package ScriptParser;\n     -+# ChainlintParser is a subclass of ScriptParser which checks each test\n     -+# body for broken &&-chains via TestParser, then formats and collects\n     -+# the results.\n     -+package ChainlintParser;\n     - \n     +-\n      -use base 'ShellParser';\n     -+our @ISA = ('ScriptParser');\n     - \n     - sub new {\n     - \tmy $class = shift @_;\n     -@@ t/chainlint.pl: sub new {\n     - \treturn $self;\n     - }\n     +-\n     +-sub new {\n     +-\tmy $class = shift @_;\n     +-\tmy $self = $class->SUPER::new(@_);\n     +-\t$self->{ntests} = 0;\n     +-\t$self->{nerrs} = 0;\n     +-\treturn $self;\n     +-}\n     ++# ChainlintParser extends ScriptParser with &&-chain checking\n     ++package ChainlintParser;\n       \n      -# extract the raw content of a token, which may be a single string or a\n      -# composition of multiple strings and non-string character runs; for instance,\n     @@ t/chainlint.pl: sub new {\n      -\t}\n      -\treturn $s\n      -}\n     --\n     ++our @ISA = ('ScriptParser');\n     + \n       sub format_problem {\n       \tlocal $_ = shift;\n     - \t/^AMP$/ && return \"missing '&&'\";\n      @@ t/chainlint.pl: sub format_problem {\n       \n       sub check_test {\n     @@ t/chainlint.pl: sub check_test {\n      -}\n      -\n       # main contains high-level functionality for processing command-line switches,\n     - # feeding input test scripts to ScriptParser, and reporting results.\n     +-# feeding input test scripts to ScriptParser, and reporting results.\n     ++# feeding input test scripts to ChainlintParser, and reporting results.\n       package main;\n     + \n     + my $getnow = sub { return time(); };\n      @@ t/chainlint.pl: sub check_script {\n       \t\t}\n       \t\tmy $s = do { local $/; <$fh> };\n     @@ t/chainlint.pl: sub check_script {\n      \n       ## t/lib-shell-parser.pl (new) ##\n      @@\n     ++# Copyright (c) 2021-2022 Eric Sunshine <sunshine@sunshineco.com>\n     ++#\n     ++# Shared shell script parser for test lint tools. Provides Lexer,\n     ++# ShellParser, and ScriptParser. Subclass ScriptParser and override\n     ++# check_test() to implement lint checks.\n     ++\n      +use strict;\n      +use warnings;\n      +\n     -+# Copyright (c) 2021-2022 Eric Sunshine <sunshine@sunshineco.com>\n     -+#\n      +# Lexer tokenizes POSIX shell scripts. It is roughly modeled after section 2.3\n      +# \"Token Recognition\" of POSIX chapter 2 \"Shell Command Language\". Although\n      +# similar to lexical analyzers for other languages, this one differs in a few\n     @@ t/lib-shell-parser.pl (new)\n      +# ShellParser parses POSIX shell scripts (with minor extensions for Bash). It\n      +# is a recursive descent parser very roughly modeled after section 2.10 \"Shell\n      +# Grammar\" of POSIX chapter 2 \"Shell Command Language\".\n     ++\n      +package ShellParser;\n      +\n      +sub new {\n     @@ t/lib-shell-parser.pl (new)\n      +}\n      +\n      +# ScriptParser is a subclass of ShellParser which identifies individual test\n     -+# definitions within test scripts and calls check_test() for each test body\n     -+# found.  Callers subclass ScriptParser and override check_test() to\n     -+# implement specific checks (e.g. chainlint checks &&-chains, lint-style\n     -+# checks grep usage).\n     ++# definitions within test scripts and passes each test body to check_test().\n     ++# ScriptParser detects test definitions not only at the top-level of test\n     ++# scripts but also within compound commands such as loops and function\n     ++# definitions.\n     ++\n      +package ScriptParser;\n      +\n      +our @ISA = ('ShellParser');\n      +\n     ++sub new {\n     ++\tmy $class = shift @_;\n     ++\tmy $self = $class->SUPER::new(@_);\n     ++\t$self->{ntests} = 0;\n     ++\t$self->{nerrs} = 0;\n     ++\treturn $self;\n     ++}\n     ++\n      +# extract the raw content of a token, which may be a single string or a\n      +# composition of multiple strings and non-string character runs; for instance,\n      +# `\"test body\"` unwraps to `test body`; `word\"a b\"42'c d'` to `worda b42c d`\n     @@ t/lib-shell-parser.pl (new)\n      +}\n      +\n      +sub check_test {\n     -+\t# no-op; subclasses override to implement specific checks\n     ++\t# no-op; subclass and override to implement lint checks\n      +}\n      +\n      +sub parse_cmd {\n 3:  93c2b29683 ! 4:  c1b86748d1 t: fix Lexer line count for $() inside double-quoted strings\n     @@ Commit message\n      \n          scan_dqstring's post-loop newline counter re-counts newlines that\n          were already counted during recursive parsing of $() bodies.  This\n     -    happens because scan_dollar's returned text can contain newlines\n     -    (from token text of multi-line strings and from \\n command separator\n     -    tokens), and the catch-all counter at the end of scan_dqstring\n     -    counts all of them again.\n     +    happens because scan_dollar returns text containing newlines (from\n     +    multi-line command substitutions), and the catch-all counter at the\n     +    end of scan_dqstring counts all of them again.\n      \n          Fix this by counting newlines inline as non-special characters are\n          consumed, and removing the post-loop catch-all.  Each newline is\n          now counted exactly once: literal newlines at the inline match,\n     -    line splices at the \\<newline> handler, and $() newlines by\n     +    line splices at the backslash handler, and $() newlines by\n          scan_token during the recursive parse.\n      \n     -    This does not affect chainlint's output because chainlint annotates\n     -    the original body text using byte offsets, not token line numbers.\n     -    It does matter for tools like lint-style.pl (introduced in a\n     -    subsequent commit) that use token line numbers to locate and fix\n     -    specific lines in the original file.\n     -\n     -    Add check-shell-parser.pl to verify that the Lexer reports correct\n     -    line numbers after multi-line $() in double-quoted strings.\n     +    This is a latent bug: any consumer that relies on token line\n     +    numbers rather than byte offsets would get incorrect results for\n     +    tokens following a multi-line $() inside a double-quoted string.\n     +    chainlint is not affected because it annotates the original body\n     +    text using byte offsets, not token line numbers.\n      \n          Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>\n      \n     - ## t/Makefile ##\n     -@@ t/Makefile: check-meson:\n     - test-lint: test-lint-duplicates test-lint-executable \\\n     - \ttest-lint-filenames\n     - ifneq ($(PERL_PATH),)\n     --test-lint: test-lint-shell-syntax\n     -+test-lint: test-lint-shell-syntax check-shell-parser\n     - else\n     - GIT_TEST_CHAIN_LINT = 0\n     - endif\n     -@@ t/Makefile: test-lint-executable:\n     - test-lint-shell-syntax:\n     - \t@'$(PERL_PATH_SQ)' check-non-portable-shell.pl $(T) $(THELPERS) $(TPERF)\n     - \n     -+check-shell-parser:\n     -+\t@'$(PERL_PATH_SQ)' check-shell-parser.pl\n     - test-lint-filenames:\n     - \t@# We do *not* pass a glob to ls-files but use grep instead, to catch\n     - \t@# non-ASCII characters (which are quoted within double-quotes)\n     -@@ t/Makefile: perf:\n     - \t$(MAKE) -C perf/ all\n     - \n     - .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \\\n     --\tcheck-chainlint clean-chainlint test-chainlint $(UNIT_TESTS)\n     -+\tcheck-chainlint clean-chainlint test-chainlint \\\n     -+\tcheck-shell-parser $(UNIT_TESTS)\n     - \n     - .PHONY: libgit-sys-test libgit-rs-test\n     - libgit-sys-test:\n     -\n     - ## t/check-shell-parser.pl (new) ##\n     -@@\n     -+#!/usr/bin/perl\n     -+\n     -+# Tests for the shared shell parser (lib-shell-parser.pl).\n     -+\n     -+use strict;\n     -+use warnings;\n     -+use File::Basename;\n     -+\n     -+my $_lib = dirname($0) . \"/lib-shell-parser.pl\";\n     -+$_lib = \"./$_lib\" unless $_lib =~ m{^/};\n     -+do $_lib or die \"$0: failed to load $_lib: $@$!\\n\";\n     -+\n     -+my $rc = 0;\n     -+\n     -+sub check {\n     -+\tmy ($desc, $body, $want_token, $want_line) = @_;\n     -+\tmy $parser = ShellParser->new(\\$body);\n     -+\tmy @tokens = $parser->parse();\n     -+\tfor my $t (reverse @tokens) {\n     -+\t\tnext unless $t->[0] eq $want_token && defined $t->[3];\n     -+\t\tif ($t->[3] != $want_line) {\n     -+\t\t\tprint STDERR \"FAIL: $desc: \" .\n     -+\t\t\t\t\"'$want_token' at line $t->[3], \" .\n     -+\t\t\t\t\"expected line $want_line\\n\";\n     -+\t\t\t$rc = 1;\n     -+\t\t}\n     -+\t\treturn;\n     -+\t}\n     -+\tprint STDERR \"FAIL: $desc: token '$want_token' not found\\n\";\n     -+\t$rc = 1;\n     -+}\n     -+\n     -+# Multi-line $() inside a dq-string: MARKER should be at line 3.\n     -+check('dq-string with multi-line $()', <<'BODY', 'MARKER', 3);\n     -+\tx=\"$(echo one\n     -+\techo two)\" &&\n     -+\tMARKER here\n     -+BODY\n     -+\n     -+# Two multi-line $() substitutions: verifies drift does not accumulate.\n     -+# MARKER should be at line 5.\n     -+check('two dq-string $()', <<'BODY', 'MARKER', 5);\n     -+\tx=\"$(echo a\n     -+\tb)\" &&\n     -+\ty=\"$(echo c\n     -+\td)\" &&\n     -+\tMARKER here\n     -+BODY\n     -+\n     -+# $() outside a dq-string: no double-counting either way.\n     -+# MARKER should be at line 3.\n     -+check('bare $() spanning lines', <<'BODY', 'MARKER', 3);\n     -+\tx=$(echo one\n     -+\techo two) &&\n     -+\tMARKER here\n     -+BODY\n     -+\n     -+exit $rc;\n     -\n       ## t/lib-shell-parser.pl ##\n      @@ t/lib-shell-parser.pl: sub scan_dqstring {\n       \tmy $b = $self->{buff};\n     @@ t/lib-shell-parser.pl: sub scan_dqstring {\n       \twhile (1) {\n      -\t\t# slurp up non-special characters\n      -\t\t$s .= $1 if $$b =~ /\\G([^\"\\$\\\\]+)/gc;\n     -+\t\t# slurp up non-special characters; count newlines\n     -+\t\t# inline so we don't need a catch-all counter that\n     -+\t\t# would miscount newlines from recursive $() parsing\n     ++\t\t# Slurp non-special characters; count newlines here because\n     ++\t\t# newlines inside $() are already counted by the recursive parse.\n      +\t\tif ($$b =~ /\\G([^\"\\$\\\\]+)/gc) {\n     -+\t\t\tmy $chunk = $1;\n     -+\t\t\t$self->{lineno} += () = $chunk =~ /\\n/sg;\n     -+\t\t\t$s .= $chunk;\n     ++\t\t\t$s .= $1;\n     ++\t\t\t$self->{lineno} += $1 =~ tr/\\n//;\n      +\t\t}\n       \t\t# handle special characters\n       \t\tlast unless $$b =~ /\\G(.)/sgc;\n 6:  1527293f1c ! 5:  3a589ef738 t: lint and convert grep assertions to test_grep\n     @@ Metadata\n      Author: Michael Montalbo <mmontalbo@gmail.com>\n      \n       ## Commit message ##\n     -    t: lint and convert grep assertions to test_grep\n     -\n     -    Extend lint-style.pl with a rule that detects bare 'grep' used as a\n     -    test assertion and converts it to test_grep.  test_grep prints the\n     -    file contents on failure, making test debugging significantly easier.\n     -\n     -    parse_commands() is extended to split at shell structural tokens\n     -    ({, }, (, ), |) and keywords (if, then, for, etc.), and each\n     -    command gains a token_pos index so that rules can scan backward and\n     -    forward in the token stream for context.\n     -\n     -    Three new functions implement the grep-assertion rule:\n     -\n     -     - is_filter_context() scans the surrounding tokens for pipes,\n     -       control-flow keywords (if/elif/while/until), for-in value\n     -       lists, and brace groups with output redirects.\n     -\n     -     - is_grep_assertion() classifies a grep command: convertible\n     -       assertion (pattern and file present), filter (not an assertion),\n     -       or missing file argument (flagged as a likely bug).\n     -\n     -     - check_bare_grep() ties them together and calls\n     -       report_violation() with the appropriate fix.\n     -\n     -    The --fix mode handles:\n     -     - Replacing 'grep' with 'test_grep'\n     -     - Moving negation from '! grep' to 'test_grep !'\n     -     - Stripping the -q flag (test_grep inherently checks match status)\n     -\n     -    Five files require '# lint-ok' annotations for intentional grep\n     -    usage that cannot be mechanically converted: t1400 (packed-refs\n     -    may not exist on reftable), t3901 (piped stdin via case block),\n     -    t6437 (glob argument breaks test_grep's test -f check), t7450\n     -    (file may not exist after failed MINGW clone), and t7527 ($?\n     -    capture on the next line).\n     -\n     -    The test-lint-style scope is extended to include sourced test\n     -    fragments in subdirectories (t5411/*.sh and similar) via a new\n     -    TSOURCED variable.\n     -\n     -    Run '--fix' to convert all ~2800 grep assertions across ~340 files\n     -    in the test suite.  test-lib-functions.sh and lib-rebase.sh are\n     -    excluded from linting since they implement test infrastructure\n     -    rather than test assertions.\n     +    t: convert grep assertions to test_grep\n     +\n     +    Replace bare grep with test_grep in test assertions across the\n     +    suite, including sourced test helpers (lib-*.sh, *-tests.sh).\n     +    test_grep prints the contents of the file being searched on\n     +    failure, making debugging easier than a bare grep which fails\n     +    silently.\n     +\n     +    Only assertion-style greps are converted: grep used as a filter\n     +    in pipelines, command substitutions, conditionals, or with\n     +    redirected I/O is left as-is with a \"# lint-ok\" annotation.\n     +    Existing '! test_grep' calls are rewritten to 'test_grep !' so\n     +    that the diagnostic output is preserved on failure.\n     +\n     +    The conversion was generated using a grep-assertion linter\n     +    (greplint.pl, added in the following commit) to identify bare\n     +    grep calls at command position.  To reproduce:\n     +\n     +        # Step 1: mark bare greps that should not be converted\n     +        sed -i '/! grep \"$m\" \\.git\\/packed-refs/s/$/ # lint-ok: file may not exist (reftable)/' \\\n     +            t/t1400-update-ref.sh\n     +        sed -i '/! grep dirty file3 &&/{/lint-ok/!s/$/ # lint-ok: file may not exist after --quit/}' \\\n     +            t/t3420-rebase-autostash.sh\n     +        sed -i '/grep -vf before commits\\.raw/s/$/ # lint-ok: data filter/' \\\n     +            t/t5326-multi-pack-bitmaps.sh\n     +        sed -i '/! grep $d shallow-client\\/\\.git\\/shallow/s/$/ # lint-ok: file may not exist after repack/' \\\n     +            t/t5537-fetch-shallow.sh\n     +        sed -i '/grep -E \"^\\[0-9a-f\\].*|| :/s/$/ # lint-ok: data filter/' \\\n     +            t/t5702-protocol-v2.sh\n     +        sed -i '/! grep gitdir squatting-clone/s/$/ # lint-ok: file may not exist after failed clone/' \\\n     +            t/t7450-bad-git-dotfiles.sh\n     +\n     +        # Step 2: reorder pre-existing '! test_grep' to 'test_grep !'\n     +        # (must come before steps 3-4 so greplint does not see them)\n     +        sed -i 's/! test_grep/test_grep !/' t/t0031-lockfile-pid.sh\n     +        sed -i 's/! test_grep/test_grep !/' t/t5300-pack-object.sh\n     +        sed -i 's/! test_grep/test_grep !/' t/t5319-multi-pack-index.sh\n     +\n     +        # Step 3: convert '! grep' -> 'test_grep !'\n     +        perl t/greplint.pl t/*.sh 2>&1 | cut -d: -f1,2 |\n     +        while IFS=: read f l; do\n     +            sed -i \"${l}s/! *grep/test_grep !/\" \"$f\"\n     +        done\n     +\n     +        # Step 4: convert remaining 'grep' -> 'test_grep'\n     +        perl t/greplint.pl t/*.sh 2>&1 | cut -d: -f1,2 |\n     +        while IFS=: read f l; do\n     +            sed -i \"${l}s/grep/test_grep/\" \"$f\"\n     +        done\n     +\n     +    To verify, run: make -C t test-greplint\n      \n          Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>\n      \n     - ## t/Makefile ##\n     -@@ t/Makefile: test-lint-shell-syntax:\n     - check-shell-parser:\n     - \t@'$(PERL_PATH_SQ)' check-shell-parser.pl\n     - \n     -+TSOURCED = $(sort $(wildcard t[0-9]*/*.sh))\n     -+\n     - test-lint-style:\n     --\t@'$(PERL_PATH_SQ)' lint-style.pl $(T) $(THELPERS) $(TPERF)\n     -+\t@'$(PERL_PATH_SQ)' lint-style.pl $(T) $(THELPERS) $(TPERF) $(TSOURCED)\n     - \n     - check-lint-style:\n     - \t@rc=0; for t in $(LINT_STYLE_TESTS); do \\\n     -\n       ## t/for-each-ref-tests.sh ##\n      @@ t/for-each-ref-tests.sh: test_expect_success 'Verify descending sort' '\n       \n     @@ t/lib-httpd.sh: test_http_push_nonff () {\n       \n       \ttest_expect_success 'non-fast-forward push shows help message' '\n      \n     - ## t/lint-style.pl ##\n     -@@\n     - # Detection uses parsed tokens from the shared shell parser for\n     - # correct handling of heredocs, $(...), pipes, and quoting.\n     - # Fixes modify the original file text to preserve formatting.\n     -+#\n     -+# Architecture: the harness (LintParser, parse_commands) tokenizes\n     -+# test bodies and splits them into commands.  Rules are independent\n     -+# functions that examine each command and its surrounding token\n     -+# context to decide if there is a violation.\n     - \n     - use strict;\n     - use warnings;\n     -@@ t/lint-style.pl: do $_lib or die \"$0: failed to load $_lib: $@$!\\n\";\n     - # on each test body.  Per-file state (file name, raw lines, dirty\n     - # flag) is stored on the instance before calling parse().\n     - #\n     --# Subroutines defined below (parse_commands, check_test_grep_negation,\n     --# etc.) are in package main and called with the main:: prefix.\n     --# File-scoped lexicals ($fix_mode, $has_fixable, etc.) are visible\n     --# across packages since 'package' does not introduce a new scope.\n     -+# Subroutines defined below are in package main and called with\n     -+# the main:: prefix.  File-scoped lexicals ($fix_mode, etc.) are\n     -+# visible across packages since 'package' does not introduce a\n     -+# new scope.\n     - package LintParser;\n     - our @ISA = ('ScriptParser');\n     - \n     -@@ t/lint-style.pl: package main;\n     - my $exit_code = 0;\n     - my $has_fixable = 0;\n     - \n     -+my %skip_file = map { $_ => 1 }\n     -+\tgrep { m{(?:test-lib-functions|lib-rebase)\\.sh$} } @ARGV;\n     -+\n     - sub err {\n     - \tmy ($file, $lineno, $line, $msg, %opts) = @_;\n     - \t$line =~ s/^\\s+//;\n     -@@ t/lint-style.pl: sub err {\n     - \t$exit_code = 1 unless $fix_mode && $opts{fixable};\n     - }\n     - \n     --# Report a lint violation found by a rule.  In --fix mode, apply\n     --# the regex substitution on the raw line and report success.\n     --# Otherwise just report.  Returns 1 if the line was modified.\n     -+# Report a lint violation.  In --fix mode, apply the regex\n     -+# substitution on the raw line.  Returns 1 if modified.\n     - sub report_violation {\n     - \tmy ($file, $cmd, $line_ref, $match, $fix, $from) = @_;\n     - \tmy $lineno = $cmd->{lineno};\n     -@@ t/lint-style.pl: sub report_violation {\n     - \treturn 0;\n     - }\n     - \n     -+# --- Harness: tokenize and split into commands ---\n     -+#\n     - # Split a token stream into commands at &&, ||, ;;, and \\n.\n     -+# Each command is {tokens => [...], lineno => N, token_pos => I}\n     -+# where token_pos is the index in @all_tokens where the command's\n     -+# first token appeared (so rules can look backward for context).\n     - sub parse_commands {\n     --\tmy ($content) = @_;\n     --\tmy $parser = ShellParser->new(\\$content);\n     --\tmy @all_tokens = $parser->parse();\n     --\n     -+\tmy ($all_tokens) = @_;\n     - \tmy @commands;\n     - \tmy @current;\n     - \tmy $lineno = 1;\n     -+\tmy $first_pos = 0;\n     -+\n     -+\tmy %shell_keyword;\n     -+\t@shell_keyword{qw(if then else elif fi for do done\n     -+\t\t\t   while until case in esac)} = ();\n     - \n     --\tfor (my $ti = 0; $ti < @all_tokens; $ti++) {\n     --\t\tmy $text = $all_tokens[$ti]->[0];\n     -+\tfor (my $ti = 0; $ti < @$all_tokens; $ti++) {\n     -+\t\tmy $text = $all_tokens->[$ti]->[0];\n     - \t\tif ($text =~ /^(?:&&|\\|\\||;;|\\n)$/) {\n     -+\t\t\t# Command separators: flush current command\n     -+\t\t\tif (@current) {\n     -+\t\t\t\tpush @commands, {\n     -+\t\t\t\t\ttokens    => [@current],\n     -+\t\t\t\t\tlineno    => $lineno,\n     -+\t\t\t\t\ttoken_pos => $first_pos,\n     -+\t\t\t\t};\n     -+\t\t\t\t@current = ();\n     -+\t\t\t}\n     -+\t\t} elsif ($text =~ /^[{}()|]$/ || exists $shell_keyword{$text}) {\n     -+\t\t\t# Shell structural tokens and keywords:\n     -+\t\t\t# flush current command (these are boundaries,\n     -+\t\t\t# not part of the command's arguments)\n     - \t\t\tif (@current) {\n     - \t\t\t\tpush @commands, {\n     --\t\t\t\t\ttokens => [@current],\n     --\t\t\t\t\tlineno => $lineno,\n     -+\t\t\t\t\ttokens    => [@current],\n     -+\t\t\t\t\tlineno    => $lineno,\n     -+\t\t\t\t\ttoken_pos => $first_pos,\n     - \t\t\t\t};\n     - \t\t\t\t@current = ();\n     - \t\t\t}\n     - \t\t} else {\n     --\t\t\t$lineno = $all_tokens[$ti]->[3]\n     --\t\t\t\tif !@current && defined $all_tokens[$ti]->[3];\n     -+\t\t\tif (!@current) {\n     -+\t\t\t\t# Record line number of the first token\n     -+\t\t\t\t$lineno = $all_tokens->[$ti]->[3]\n     -+\t\t\t\t\tif defined $all_tokens->[$ti]->[3];\n     -+\t\t\t\t$first_pos = $ti;\n     -+\t\t\t}\n     - \t\t\tpush @current, $text;\n     - \t\t}\n     - \t}\n     - \tif (@current) {\n     - \t\tpush @commands, {\n     --\t\t\ttokens => [@current],\n     --\t\t\tlineno => $lineno,\n     -+\t\t\ttokens    => [@current],\n     -+\t\t\tlineno    => $lineno,\n     -+\t\t\ttoken_pos => $first_pos,\n     - \t\t};\n     - \t}\n     - \treturn @commands;\n     - }\n     - \n     - # --- Rule: '! test_grep' should be 'test_grep !' ---\n     --# Shell-level negation suppresses test_grep's diagnostic output\n     --# on failure.  Built-in negation preserves it.\n     - sub check_test_grep_negation {\n     --\tmy ($cmd, $file, $line_ref) = @_;\n     -+\tmy ($cmd, $file, $line_ref, $all_tokens) = @_;\n     - \tmy @tokens = @{$cmd->{tokens}};\n     - \treturn unless @tokens >= 2 && $tokens[0] eq '!' && $tokens[1] eq 'test_grep';\n     - \n     -@@ t/lint-style.pl: sub check_test_grep_negation {\n     - \t\tqr/!\\s*test_grep/, 'test_grep !', '! test_grep');\n     - }\n     - \n     --# Map parsed commands back to raw file lines for --fix.\n     --# Detection uses parsed tokens (correct handling of quoting,\n     --# heredocs, pipes) but fixes must modify the original text\n     --# to preserve formatting.\n     -+# --- Rule: bare 'grep' should be 'test_grep' ---\n     -+\n     -+# Check if this command is in a filter context by looking at\n     -+# the surrounding tokens in the stream.  This is grep-rule\n     -+# specific: it knows what contexts make a grep not an assertion.\n     -+sub is_filter_context {\n     -+\tmy ($all_tokens, $cmd) = @_;\n     -+\tmy $pos = $cmd->{token_pos};\n     -+\n     -+\t# Scan backward to the previous command separator.\n     -+\t# If we find '|', this command is part of a pipeline.\n     -+\t# If we find if/elif/while/until, it's a condition.\n     -+\tfor (my $j = $pos - 1; $j >= 0; $j--) {\n     -+\t\tmy $t = $all_tokens->[$j]->[0];\n     -+\t\t# Stop at command separators (but not \\n after |)\n     -+\t\tlast if $t =~ /^(?:&&|\\|\\||;;)$/;\n     -+\t\tif ($t eq \"\\n\") {\n     -+\t\t\t# \\n after | is a line continuation, keep scanning\n     -+\t\t\tnext if $j > 0 && $all_tokens->[$j - 1]->[0] eq '|';\n     -+\t\t\tlast;\n     -+\t\t}\n     -+\t\treturn 1 if $t eq '|';\n     -+\t\treturn 1 if $t =~ /^(?:if|elif|while|until)$/;\n     -+\t\t# for ... in ITEMS ... do: if we're between 'in' and 'do',\n     -+\t\t# we're in a value list, not a command\n     -+\t\treturn 1 if $t eq 'in';\n     -+\t}\n     -+\n     -+\t# Forward: pipe after command\n     -+\tfor (my $j = $pos + @{$cmd->{tokens}}; $j < @$all_tokens; $j++) {\n     -+\t\tmy $t = $all_tokens->[$j]->[0];\n     -+\t\tlast if $t =~ /^(?:&&|\\|\\||;;|\\n)$/;\n     -+\t\treturn 1 if $t eq '|';\n     -+\t}\n     -+\n     -+\t# { cmd; } >output\n     -+\treturn 1 if is_in_redirected_brace($all_tokens, $pos);\n     -+\n     -+\treturn 0;\n     -+}\n     -+\n     -+# Check if position $pos is inside a brace group whose output is\n     -+# redirected: { grep ...; } >file.  Scan backward for the enclosing\n     -+# '{', then forward for the matching '}', and check what follows it.\n     -+sub is_in_redirected_brace {\n     -+\tmy ($all_tokens, $pos) = @_;\n     -+\tmy $brace_depth = 0;\n     -+\tfor (my $j = $pos - 1; $j >= 0; $j--) {\n     -+\t\tmy $t = $all_tokens->[$j]->[0];\n     -+\t\t$brace_depth++ if $t eq '}';\n     -+\t\tif ($t eq '{') {\n     -+\t\t\treturn 0 if $brace_depth > 0;\n     -+\t\t\t$brace_depth--;\n     -+\t\t\t# Found our enclosing '{'. Find matching '}'\n     -+\t\t\tmy $depth = 1;\n     -+\t\t\tfor (my $k = $j + 1; $k < @$all_tokens; $k++) {\n     -+\t\t\t\t$depth++ if $all_tokens->[$k]->[0] eq '{';\n     -+\t\t\t\t$depth-- if $all_tokens->[$k]->[0] eq '}';\n     -+\t\t\t\tif ($depth == 0) {\n     -+\t\t\t\t\tmy $after = $k + 1 < @$all_tokens ?\n     -+\t\t\t\t\t\t$all_tokens->[$k + 1]->[0] : '';\n     -+\t\t\t\t\treturn $after =~ /^>{1,2}/;\n     -+\t\t\t\t}\n     -+\t\t\t}\n     -+\t\t\treturn 0;\n     -+\t\t}\n     -+\t}\n     -+\treturn 0;\n     -+}\n     -+\n     -+# Classify a grep command: assertion, filter, or bug.\n     -+#\n     -+# Returns:\n     -+#   1  assertion (PATTERN + FILE), can be converted to test_grep\n     -+#   0  not a grep, or grep used as a filter\n     -+#  -1  likely bug (e.g., missing file argument)\n     -+sub is_grep_assertion {\n     -+\tmy ($cmd, $all_tokens) = @_;\n     -+\tmy @tokens = @{$cmd->{tokens}};\n     -+\n     -+\t# Find grep, possibly after \"!\"\n     -+\tmy $i = 0;\n     -+\t$i++ if $tokens[0] eq '!';\n     -+\treturn 0 unless defined $tokens[$i] && $tokens[$i] eq 'grep';\n     -+\treturn 0 if grep { $_ eq 'test_grep' } @tokens;\n     -+\n     -+\t# Check surrounding context (pipes, control flow, brace redirects)\n     -+\treturn 0 if is_filter_context($all_tokens, $cmd);\n     -+\n     -+\t$i++;  # skip 'grep'\n     -+\n     -+\t# Check grep's own flags and arguments\n     -+\tmy @positional;\n     -+\tmy $has_pattern_flag = 0;\n     -+\tmy $end_of_flags = 0;\n     -+\twhile ($i < @tokens) {\n     -+\t\tmy $tok = $tokens[$i];\n     -+\t\tif ($tok eq '|' || $tok eq '<') {\n     -+\t\t\treturn 0;\n     -+\t\t}\n     -+\t\tif ($tok =~ /^>{1,2}$/) {\n     -+\t\t\t# Stdout redirect means filter (grep ... >out).\n     -+\t\t\t# Stderr redirect (2>err) is fine: skip the fd\n     -+\t\t\t# and the target, and keep classifying.\n     -+\t\t\tmy $prev = $i > 0 ? $tokens[$i - 1] : '';\n     -+\t\t\treturn 0 unless $prev =~ /^\\d+$/ && $prev >= 2;\n     -+\t\t\tpop @positional if @positional && $positional[-1] eq $prev;\n     -+\t\t\t$i += 2;\n     -+\t\t\tnext;\n     -+\t\t}\n     -+\t\tif (!$end_of_flags && $tok =~ /^-\\w*[clLrR]/) {\n     -+\t\t\treturn 0;\n     -+\t\t}\n     -+\t\tif (!$end_of_flags && $tok eq '--') {\n     -+\t\t\t$end_of_flags = 1;\n     -+\t\t} elsif (!$end_of_flags && $tok =~ /^-\\w*[ef]$/) {\n     -+\t\t\t$has_pattern_flag = 1;\n     -+\t\t\t$i++;\n     -+\t\t} elsif (!$end_of_flags && $tok =~ /^-/) {\n     -+\t\t\t# skip other flags\n     -+\t\t} else {\n     -+\t\t\tpush @positional, $tok;\n     -+\t\t}\n     -+\t\t$i++;\n     -+\t}\n     -+\n     -+\tmy $need = $has_pattern_flag ? 1 : 2;\n     -+\treturn 0 if !@positional && !$has_pattern_flag;\n     -+\treturn -1 if @positional < $need;\n     -+\treturn 0 if $positional[-1] =~ /^-/;\n     -+\treturn 1;\n     -+}\n     -+\n     -+sub check_bare_grep {\n     -+\tmy ($cmd, $file, $line_ref, $all_tokens) = @_;\n     -+\tmy @tokens = @{$cmd->{tokens}};\n     -+\n     -+\tmy $result = is_grep_assertion($cmd, $all_tokens);\n     -+\treturn unless $result;\n     -+\n     -+\tif ($result == -1) {\n     -+\t\terr $file, $cmd->{lineno}, join(' ', @tokens),\n     -+\t\t\t\"grep assertion appears to be missing a file argument\";\n     -+\t\treturn 0;\n     -+\t}\n     -+\n     -+\t# Determine negation and -q flag\n     -+\tmy $negated = $tokens[0] eq '!';\n     -+\tmy $has_q = 0;\n     -+\tmy ($pre_q, $post_q) = ('', '');\n     -+\tfor my $tok (@tokens) {\n     -+\t\tif ($tok =~ /^-(\\w*)q(\\w*)$/) {\n     -+\t\t\t$has_q = 1;\n     -+\t\t\t($pre_q, $post_q) = ($1, $2);\n     -+\t\t\tlast;\n     -+\t\t}\n     -+\t\tlast if $tok !~ /^-/ && $tok ne '!' && $tok ne 'grep';\n     -+\t}\n     -+\n     -+\t# Build the replacement\n     -+\tmy $fix = \"test_grep\";\n     -+\t$fix .= \" !\" if $negated;\n     -+\tif ($has_q) {\n     -+\t\tmy $rest = \"$pre_q$post_q\";\n     -+\t\t$fix .= \" -$rest\" if $rest;\n     -+\t}\n     -+\n     -+\t# Build the match pattern\n     -+\tmy $neg_match = $negated ? '!\\s*' : '\\b';\n     -+\tmy $neg_from  = $negated ? '! '   : '';\n     -+\tmy ($match, $from);\n     -+\tif ($has_q) {\n     -+\t\t$match = qr/${neg_match}grep\\s+-\\w*q\\w*/;\n     -+\t\t$from  = \"${neg_from}grep -${pre_q}q${post_q}\";\n     -+\t} else {\n     -+\t\t$match = qr/${neg_match}grep\\b/;\n     -+\t\t$from  = \"${neg_from}grep\";\n     -+\t}\n     -+\n     -+\treturn report_violation($file, $cmd, $line_ref,\n     -+\t\t$match, $fix, $from);\n     -+}\n     -+\n     -+# --- Harness: LintParser.check_test ---\n     -+#\n     -+# Called by ScriptParser::parse_cmd for each test_expect_success\n     -+# or test_expect_failure block.  Extracts the body, tokenizes it,\n     -+# splits into commands, and runs each rule.\n     - package LintParser;\n     - \n     - sub check_test {\n     --\t# Called by ScriptParser::parse_cmd for each test_expect_success\n     --\t# or test_expect_failure block.\n     - \tmy $self = shift @_;\n     - \tmy $title = ScriptParser::unwrap(shift @_);\n     - \n     -@@ t/lint-style.pl: sub check_test {\n     - \t}\n     - \treturn unless $body;\n     - \n     -+\t# Tokenize the body once; commands and rules share the stream\n     -+\tmy $parser = ShellParser->new(\\$body);\n     -+\tmy @all_tokens = $parser->parse();\n     -+\tmy @commands = main::parse_commands(\\@all_tokens);\n     -+\n     - \t# Map each command back to its file line number.\n     - \t# $lineno_base is where the body starts in the file;\n     - \t# $cmd->{lineno} is relative to the body (starting at 1).\n     - \tmy $raw_lines = $self->{raw_lines};\n     --\tfor my $cmd (main::parse_commands($body)) {\n     -+\tfor my $cmd (@commands) {\n     - \t\tmy $ln = ($cmd->{lineno} || 0) + $lineno_base - 1;\n     - \t\t$cmd->{lineno} = $ln;\n     - \t\tnext unless $ln >= 1 && $ln <= @$raw_lines;\n     - \t\tnext if $raw_lines->[$ln - 1] =~ /#.*lint-ok/;\n     - \n     --\t\tif (main::check_test_grep_negation($cmd, $self->{file}, \\$raw_lines->[$ln - 1])) {\n     --\t\t\t$self->{dirty} = 1;\n     --\t\t}\n     -+\t\tmy $line_ref = \\$raw_lines->[$ln - 1];\n     -+\t\t# Stop after the first fix: later rules should not\n     -+\t\t# re-match against already-modified text.\n     -+\t\tmy $modified = 0;\n     -+\t\t$modified ||= main::check_test_grep_negation(\n     -+\t\t\t$cmd, $self->{file}, $line_ref, \\@all_tokens);\n     -+\t\t$modified ||= main::check_bare_grep(\n     -+\t\t\t$cmd, $self->{file}, $line_ref, \\@all_tokens);\n     -+\t\t$self->{dirty} = 1 if $modified;\n     - \t}\n     - }\n     - \n     - package main;\n     - \n     - for my $file (@ARGV) {\n     -+\tnext if $skip_file{$file};\n     - \t# :unix:crlf strips \\r on Windows (same as chainlint.pl)\n     - \topen(my $fh, '<:unix:crlf', $file) or die \"$0: $file: $!\\n\";\n     - \tmy @raw_lines = <$fh>;\n     -\n     - ## t/lint-style/grep-assert.expect (new) ##\n     -@@\n     -+lint-style/grep-assert.test:2: error: replace 'grep' with 'test_grep': grep \"pattern\" actual\n     -+lint-style/grep-assert.test:3: error: replace 'grep' with 'test_grep': grep -E \"extended\" actual\n     -+lint-style/grep-assert.test:4: error: replace 'grep' with 'test_grep': grep -e \"explicit\" actual\n     -+lint-style/grep-assert.test:5: error: replace 'grep' with 'test_grep': grep -f patterns.txt actual\n     -+lint-style/grep-assert.test:6: error: replace 'grep' with 'test_grep': grep -Fe \"fixed-explicit\" actual\n     -+lint-style/grep-assert.test:7: error: replace 'grep' with 'test_grep': grep \"^-looks-like-flag\" actual\n     -+lint-style/grep-assert.test:8: error: replace 'grep' with 'test_grep': grep -v \"inverted\" actual\n     -+lint-style/grep-assert.test:9: error: replace 'grep' with 'test_grep': grep -- \"-e\" actual\n     -+lint-style/grep-assert.test:10: error: replace 'grep' with 'test_grep': grep \"with-stderr-redirect\" actual 2 > err\n     -+lint-style/grep-assert.test:12: error: replace 'grep' with 'test_grep': grep \"after-or\" actual\n     -+lint-style/grep-assert.test:17: error: replace 'grep' with 'test_grep': grep \"after-pipe\" actual\n     -+lint-style/grep-assert.test:22: error: replace 'grep' with 'test_grep': grep \"inside-case\" actual\n     -+hint: run with --fix to apply the suggested replacements.\n     -\n     - ## t/lint-style/grep-assert.test (new) ##\n     -@@\n     -+test_expect_success 'grep assertions' '\n     -+\tgrep \"pattern\" actual &&\n     -+\tgrep -E \"extended\" actual &&\n     -+\tgrep -e \"explicit\" actual &&\n     -+\tgrep -f patterns.txt actual &&\n     -+\tgrep -Fe \"fixed-explicit\" actual &&\n     -+\tgrep \"^-looks-like-flag\" actual &&\n     -+\tgrep -v \"inverted\" actual &&\n     -+\tgrep -- \"-e\" actual &&\n     -+\tgrep \"with-stderr-redirect\" actual 2>err &&\n     -+\tcmd ||\n     -+\tgrep \"after-or\" actual\n     -+'\n     -+\n     -+test_expect_success 'pipe only suppresses first command' '\n     -+\tcmd |\n     -+\tgrep \"piped\" && grep \"after-pipe\" actual\n     -+'\n     -+\n     -+test_expect_success 'case pattern does not hide assertion' '\n     -+\tcase foo in\n     -+\t*) grep \"inside-case\" actual ;;\n     -+\tesac\n     -+'\n     -\n     - ## t/lint-style/grep-fix.expect (new) ##\n     -@@\n     -+test_expect_success 'all fixable forms' '\n     -+\ttest_grep \"pattern\" actual &&\n     -+\ttest_grep -E \"extended\" actual &&\n     -+\ttest_grep \"quiet\" actual &&\n     -+\ttest_grep -F \"combined\" actual &&\n     -+\ttest_grep -i \"quiet-insensitive\" actual &&\n     -+\ttest_grep ! \"negated\" actual &&\n     -+\ttest_grep ! \"neg-quiet\" actual &&\n     -+\ttest_grep ! \"shell-neg\" actual &&\n     -+\ttest_grep -e \"explicit\" actual &&\n     -+\ttest_grep -Fe \"fixed-explicit\" actual &&\n     -+\ttest_grep -f patterns.txt actual &&\n     -+\ttest_grep -- \"-e\" actual &&\n     -+\ttest_grep \"continuation\" \\\n     -+\t\tactual\n     -+'\n     -\n     - ## t/lint-style/grep-fix.test (new) ##\n     -@@\n     -+test_expect_success 'all fixable forms' '\n     -+\tgrep \"pattern\" actual &&\n     -+\tgrep -E \"extended\" actual &&\n     -+\tgrep -q \"quiet\" actual &&\n     -+\tgrep -qF \"combined\" actual &&\n     -+\tgrep -qi \"quiet-insensitive\" actual &&\n     -+\t! grep \"negated\" actual &&\n     -+\t! grep -q \"neg-quiet\" actual &&\n     -+\t! test_grep \"shell-neg\" actual &&\n     -+\tgrep -e \"explicit\" actual &&\n     -+\tgrep -Fe \"fixed-explicit\" actual &&\n     -+\tgrep -f patterns.txt actual &&\n     -+\tgrep -- \"-e\" actual &&\n     -+\tgrep \"continuation\" \\\n     -+\t\tactual\n     -+'\n     -\n     - ## t/lint-style/grep-missing-file.expect (new) ##\n     -@@\n     -+lint-style/grep-missing-file.test:2: error: grep assertion appears to be missing a file argument: grep \"pattern\"\n     -+lint-style/grep-missing-file.test:3: error: grep assertion appears to be missing a file argument: ! grep \"negated\"\n     -+lint-style/grep-missing-file.test:4: error: grep assertion appears to be missing a file argument: grep -e \"pattern-flag-no-file\"\n     -+lint-style/grep-missing-file.test:5: error: grep assertion appears to be missing a file argument: grep -f patterns.txt\n     -\n     - ## t/lint-style/grep-missing-file.test (new) ##\n     -@@\n     -+test_expect_success 'grep missing file argument' '\n     -+\tgrep \"pattern\" &&\n     -+\t! grep \"negated\" &&\n     -+\tgrep -e \"pattern-flag-no-file\" &&\n     -+\tgrep -f patterns.txt\n     -+'\n     -\n     - ## t/lint-style/grep-negated.expect (new) ##\n     -@@\n     -+lint-style/grep-negated.test:2: error: replace '! grep' with 'test_grep !': ! grep \"pattern\" actual\n     -+lint-style/grep-negated.test:3: error: replace '! grep' with 'test_grep !': ! grep -i \"insensitive\" actual\n     -+lint-style/grep-negated.test:7: error: replace '! grep -q' with 'test_grep !': ! grep -q \"pattern\" actual\n     -+lint-style/grep-negated.test:8: error: replace '! grep -qF' with 'test_grep ! -F': ! grep -qF \"combined\" actual\n     -+hint: run with --fix to apply the suggested replacements.\n     -\n     - ## t/lint-style/grep-negated.test (new) ##\n     -@@\n     -+test_expect_success 'negated grep' '\n     -+\t! grep \"pattern\" actual &&\n     -+\t! grep -i \"insensitive\" actual\n     -+'\n     -+\n     -+test_expect_success 'negated grep -q' '\n     -+\t! grep -q \"pattern\" actual &&\n     -+\t! grep -qF \"combined\" actual\n     -+'\n     -\n     - ## t/lint-style/grep-not-assert.expect (new) ##\n     -\n     - ## t/lint-style/grep-not-assert.test (new) ##\n     -@@\n     -+test_expect_success 'grep used as filter (not assertion)' '\n     -+\tgrep \"pattern\" file | wc -l &&\n     -+\tgrep \"pattern\" file >output &&\n     -+\tgrep \"pattern\" file 1>output &&\n     -+\tgrep -c \"count\" file &&\n     -+\tgrep -ci \"count-insensitive\" file &&\n     -+\tgrep -l \"list\" file &&\n     -+\tgrep -rl \"recursive-list\" dir &&\n     -+\tgrep -L \"list-without\" file1 file2 &&\n     -+\tresult=$(grep \"pattern\" file) &&\n     -+\tresult=$(echo $(grep \"nested-subshell\" file)) &&\n     -+\tgrep \"pattern\" <stdin &&\n     -+\tgrep \"pattern\" file && # lint-ok\n     -+\tcmd | grep \"pattern-only\" &&\n     -+\tcmd |\n     -+\tgrep \"cross-line-pipe\" &&\n     -+\tgrep -r \"recursive\" dir\n     -+'\n     -+\n     -+test_expect_success 'grep in control flow (not assertion)' '\n     -+\tif grep \"condition\" file\n     -+\tthen\n     -+\t\techo yes\n     -+\telif grep \"other-condition\" file\n     -+\tthen\n     -+\t\techo no\n     -+\tfi\n     -+'\n     -+\n     -+test_expect_success 'grep in brace group with redirect' '\n     -+\t{ grep \"captured\" out; } >result\n     -+'\n     -+\n     -+test_expect_success 'grep in for-in value list' '\n     -+\tfor cmd in grep sed awk; do\n     -+\t\techo \"$cmd\"\n     -+\tdone\n     -+'\n     -+\n     -+test_expect_success 'grep in subshell' '\n     -+\t(cd sub && grep \"pattern\" file >output) &&\n     -+\t(cmd | grep \"piped-in-subshell\")\n     -+'\n     -\n       ## t/pack-refs-tests.sh ##\n      @@ t/pack-refs-tests.sh: test_expect_success 'delete ref while another dangling packed ref' '\n       test_expect_success 'pack ref directly below refs/' '\n     @@ t/t0030-stripspace.sh: test_expect_success 'strip comments with changed comment\n       \n       test_expect_success '-c with single line' '\n      \n     + ## t/t0031-lockfile-pid.sh ##\n     +@@ t/t0031-lockfile-pid.sh: test_expect_success 'PID info not shown by default' '\n     + \t\ttest_must_fail git add . 2>err &&\n     + \t\t# Should not crash, just show normal error without PID\n     + \t\ttest_grep \"Unable to create\" err &&\n     +-\t\t! test_grep \"is held by process\" err\n     ++\t\ttest_grep ! \"is held by process\" err\n     + \t)\n     + '\n     + \n     +\n       ## t/t0040-parse-options.sh ##\n      @@ t/t0040-parse-options.sh: test_expect_success 'non ambiguous option (after two options it abbreviates)' '\n       \n     @@ t/t0052-simple-ipc.sh: test_expect_success 'servers cannot share the same path'\n       \ttest-tool simple-ipc send --token=big >actual &&\n       \ttest_line_count -ge 10000 actual &&\n      -\tgrep -q \"big: [0]*9999\\$\" actual\n     -+\ttest_grep \"big: [0]*9999\\$\" actual\n     ++\ttest_grep -q \"big: [0]*9999\\$\" actual\n       '\n       \n       test_expect_success 'chunk response' '\n       \ttest-tool simple-ipc send --token=chunk >actual &&\n       \ttest_line_count -ge 10000 actual &&\n      -\tgrep -q \"big: [0]*9999\\$\" actual\n     -+\ttest_grep \"big: [0]*9999\\$\" actual\n     ++\ttest_grep -q \"big: [0]*9999\\$\" actual\n       '\n       \n       test_expect_success 'slow response' '\n       \ttest-tool simple-ipc send --token=slow >actual &&\n       \ttest_line_count -ge 100 actual &&\n      -\tgrep -q \"big: [0]*99\\$\" actual\n     -+\ttest_grep \"big: [0]*99\\$\" actual\n     ++\ttest_grep -q \"big: [0]*99\\$\" actual\n       '\n       \n       # Send an IPC with n=100,000 bytes of ballast.  This should be large enough\n     @@ t/t0203-gettext-setlocale-sanity.sh: test_expect_success 'git show a ISO-8859-1\n       \tgit show >out 2>err &&\n       \ttest_must_be_empty err &&\n      -\tgrep -q \"iso-c-commit\" out\n     -+\ttest_grep \"iso-c-commit\" out\n     ++\ttest_grep -q \"iso-c-commit\" out\n       '\n       \n       test_expect_success GETTEXT_LOCALE 'git show a ISO-8859-1 commit under a UTF-8 locale' '\n     @@ t/t0203-gettext-setlocale-sanity.sh: test_expect_success GETTEXT_LOCALE 'git sho\n       \tLANGUAGE=is LC_ALL=\"$is_IS_locale\" git show >out 2>err &&\n       \ttest_must_be_empty err &&\n      -\tgrep -q \"iso-utf8-commit\" out\n     -+\ttest_grep \"iso-utf8-commit\" out\n     ++\ttest_grep -q \"iso-utf8-commit\" out\n       '\n       \n       test_done\n     @@ t/t0410-partial-clone.sh: test_expect_success TTY 'promisor.quiet=unconfigured s\n      +\ttest_grep \"Receiving objects\" err\n       '\n       \n     - . \"$TEST_DIRECTORY\"/lib-httpd.sh\n     + test_expect_success 'promisor.quiet from submodule repo is honored' '\n      @@ t/t0410-partial-clone.sh: test_expect_success 'fetching of missing objects from an HTTP server' '\n       \ttest_line_count = 1 promisorlist &&\n       \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n     @@ t/t1011-read-tree-sparse-checkout.sh: test_expect_success 'read-tree will not th\n       \tread_tree_u_must_fail -m -u HEAD^ &&\n       \ttest_path_is_file init.t &&\n      -\tgrep -q dirty init.t\n     -+\ttest_grep dirty init.t\n     ++\ttest_grep -q dirty init.t\n       '\n       \n       test_expect_success 'read-tree will not throw away dirty changes, sparse' '\n     @@ t/t1011-read-tree-sparse-checkout.sh: test_expect_success 'read-tree will not th\n       \tread_tree_u_must_fail -m -u HEAD^ &&\n       \ttest_path_is_file init.t &&\n      -\tgrep -q dirty init.t\n     -+\ttest_grep dirty init.t\n     ++\ttest_grep -q dirty init.t\n       '\n       \n       test_expect_success 'read-tree updates worktree, dirty case' '\n     @@ t/t1011-read-tree-sparse-checkout.sh: test_expect_success 'read-tree updates wor\n       \techo dirty >init.t &&\n       \tread_tree_u_must_fail -m -u HEAD^ &&\n      -\tgrep -q dirty init.t &&\n     -+\ttest_grep dirty init.t &&\n     ++\ttest_grep -q dirty init.t &&\n       \trm init.t\n       '\n       \n     @@ t/t1011-read-tree-sparse-checkout.sh: test_expect_success 'read-tree removes wor\n       \techo dirty >added &&\n       \tread_tree_u_must_succeed -m -u HEAD^ &&\n      -\tgrep -q dirty added\n     -+\ttest_grep dirty added\n     ++\ttest_grep -q dirty added\n       '\n       \n       test_expect_success 'read-tree adds to worktree, absent case' '\n     @@ t/t1011-read-tree-sparse-checkout.sh: test_expect_success 'read-tree adds to wor\n       \techo dirty >sub/added &&\n       \tread_tree_u_must_succeed -u -m HEAD^ &&\n      -\tgrep -q dirty sub/added\n     -+\ttest_grep dirty sub/added\n     ++\ttest_grep -q dirty sub/added\n       '\n       \n       test_expect_success 'index removal and worktree narrowing at the same time' '\n     @@ t/t3310-notes-merge-manual-resolve.sh: EOF\n       \tgit log -1 --format=%B refs/notes/m > merge_commit_msg &&\n      -\tgrep -q refs/notes/m merge_commit_msg &&\n      -\tgrep -q refs/notes/z merge_commit_msg &&\n     -+\ttest_grep refs/notes/m merge_commit_msg &&\n     -+\ttest_grep refs/notes/z merge_commit_msg &&\n     ++\ttest_grep -q refs/notes/m merge_commit_msg &&\n     ++\ttest_grep -q refs/notes/z merge_commit_msg &&\n       \t# Merge commit mentions conflicting notes\n      -\tgrep -q \"Conflicts\" merge_commit_msg &&\n     -+\ttest_grep \"Conflicts\" merge_commit_msg &&\n     ++\ttest_grep -q \"Conflicts\" merge_commit_msg &&\n       \t( for sha1 in $(cat expect_conflicts); do\n      -\t\tgrep -q \"$sha1\" merge_commit_msg ||\n     -+\t\ttest_grep \"$sha1\" merge_commit_msg ||\n     ++\t\ttest_grep -q \"$sha1\" merge_commit_msg ||\n       \t\texit 1\n       \tdone ) &&\n       \t# Verify contents of merge result\n     @@ t/t3310-notes-merge-manual-resolve.sh: EOF\n       \tgit log -1 --format=%B refs/notes/m > merge_commit_msg &&\n      -\tgrep -q refs/notes/m merge_commit_msg &&\n      -\tgrep -q refs/notes/z merge_commit_msg &&\n     -+\ttest_grep refs/notes/m merge_commit_msg &&\n     -+\ttest_grep refs/notes/z merge_commit_msg &&\n     ++\ttest_grep -q refs/notes/m merge_commit_msg &&\n     ++\ttest_grep -q refs/notes/z merge_commit_msg &&\n       \t# Merge commit mentions conflicting notes\n      -\tgrep -q \"Conflicts\" merge_commit_msg &&\n     -+\ttest_grep \"Conflicts\" merge_commit_msg &&\n     ++\ttest_grep -q \"Conflicts\" merge_commit_msg &&\n       \t( for sha1 in $(cat expect_conflicts); do\n      -\t\tgrep -q \"$sha1\" merge_commit_msg ||\n     -+\t\ttest_grep \"$sha1\" merge_commit_msg ||\n     ++\t\ttest_grep -q \"$sha1\" merge_commit_msg ||\n       \t\texit 1\n       \tdone ) &&\n       \t# Verify contents of merge result\n     @@ t/t3404-rebase-interactive.sh: test_expect_success 'rebase -i --root retain root\n       \t) &&\n       \tgit cat-file commit HEAD >output &&\n      -\tgrep -q \"^author Twerp Snog\" output &&\n     -+\ttest_grep \"^author Twerp Snog\" output &&\n     ++\ttest_grep -q \"^author Twerp Snog\" output &&\n       \tgit cat-file commit HEAD >actual &&\n      -\tgrep -q \"^different author$\" actual\n     -+\ttest_grep \"^different author$\" actual\n     ++\ttest_grep -q \"^different author$\" actual\n       '\n       \n       test_expect_success 'rebase -i --root temporary sentinel commit' '\n     @@ t/t3420-rebase-autostash.sh: testrebase () {\n       \t\ttest_when_finished git stash drop &&\n       \t\ttest_path_is_missing $dotest/autostash &&\n      -\t\t! grep dirty file3 &&\n     ++\t\t! grep dirty file3 && # lint-ok: file may not exist after --quit\n       \t\tgit stash show -p >actual &&\n       \t\ttest_cmp expect actual &&\n       \t\tgit reset --hard &&\n     @@ t/t3501-revert-cherry-pick.sh: test_expect_success 'cherry-pick works with dirty\n       \tgit cherry-pick refs/heads/unrelated &&\n       \ttest $(git rev-parse :0:renamed) = $(git rev-parse HEAD~2:to-rename.t) &&\n      -\tgrep -q \"^modified$\" renamed\n     -+\ttest_grep \"^modified$\" renamed\n     ++\ttest_grep -q \"^modified$\" renamed\n       '\n       \n       test_expect_success 'advice from failed revert' '\n     @@ t/t3800-mktag.sh: test_expect_success 'invalid header entry config & fsck' '\n       cat >tag.sig <<EOF\n      \n       ## t/t3901-i18n-patch.sh ##\n     -@@ t/t3901-i18n-patch.sh: check_encoding () {\n     - \t\tgit cat-file commit HEAD~$j |\n     - \t\tcase \"$header\" in\n     - \t\t8859)\n     --\t\t\tgrep \"^encoding ISO8859-1\" ;;\n     -+\t\t\tgrep \"^encoding ISO8859-1\" ;; # lint-ok: piped stdin via case\n     - \t\t*)\n     --\t\t\tret=0; grep \"^encoding ISO8859-1\" || ret=$?\n     -+\t\t\tret=0; grep \"^encoding ISO8859-1\" || ret=$? # lint-ok: piped stdin via case\n     - \t\t\ttest \"$ret\" != 0 ;;\n     - \t\tesac || return 1\n     - \t\tj=$i\n      @@ t/t3901-i18n-patch.sh: test_expect_success 'format-patch output (ISO-8859-1)' '\n       \n       \tgit format-patch --stdout main..HEAD^ >out-l1 &&\n     @@ t/t4000-diff-format.sh: test_expect_success 'git diff-files --no-patch --patch s\n       test_expect_success 'git diff-files --no-patch --patch-with-raw shows the patch and raw data' '\n       \tgit diff-files --no-patch --patch-with-raw >actual &&\n      -\tgrep -q \"^:100644 100755 .* $ZERO_OID M\tpath0\\$\" actual &&\n     -+\ttest_grep \"^:100644 100755 .* $ZERO_OID M\tpath0\\$\" actual &&\n     ++\ttest_grep -q \"^:100644 100755 .* $ZERO_OID M\tpath0\\$\" actual &&\n       \ttail -n +4 actual >actual-patch &&\n       \tcompare_diff_patch expected actual-patch\n       '\n     @@ t/t5300-pack-object.sh: test_expect_success !PTHREADS,!FAIL_PREREQS \\\n       '\n       \n       test_expect_success 'pack-objects in too-many-packs mode' '\n     -@@ t/t5300-pack-object.sh: test_expect_success '--path-walk pack everything' '\n     +@@ t/t5300-pack-object.sh: test_expect_success '--name-hash-version=2 and --write-bitmap-index are incompat\n     + \n     + \t# --stdout option silently removes --write-bitmap-index\n     + \tgit pack-objects --stdout --all --name-hash-version=2 --write-bitmap-index >out 2>err &&\n     +-\t! test_grep \"currently, --write-bitmap-index requires --name-hash-version=1\" err\n     ++\ttest_grep ! \"currently, --write-bitmap-index requires --name-hash-version=1\" err\n     + '\n     + \n     + test_expect_success '--path-walk pack everything' '\n       \tgit -C server rev-parse HEAD >in &&\n       \tGIT_PROGRESS_DELAY=0 git -C server pack-objects \\\n       \t\t--stdout --revs --path-walk --progress <in >out.pack 2>err &&\n     @@ t/t5319-multi-pack-index.sh: test_expect_success 'git-fsck incorrect offset' '\n       '\n       \n       test_expect_success 'corrupt MIDX is not reused' '\n     -@@ t/t5319-multi-pack-index.sh: test_expect_success 'usage shown without sub-command' '\n     +@@ t/t5319-multi-pack-index.sh: test_expect_success 'load reverse index when missing .idx, .pack' '\n     + \n     + test_expect_success 'usage shown without sub-command' '\n     + \ttest_expect_code 129 git multi-pack-index 2>err &&\n     +-\t! test_grep \"unrecognized subcommand\" err\n     ++\ttest_grep ! \"unrecognized subcommand\" err\n     + '\n       \n       test_expect_success 'complains when run outside of a repository' '\n       \tnongit test_must_fail git multi-pack-index write 2>err &&\n     @@ t/t5326-multi-pack-bitmaps.sh: test_midx_bitmap_cases () {\n       \n       \t\t\t(\n      -\t\t\t\tgrep -vf before commits.raw &&\n     -+\t\t\t\ttest_grep -vf before commits.raw &&\n     ++\t\t\t\tgrep -vf before commits.raw && # lint-ok: data filter\n       \t\t\t\t# mark missing commits as preferred\n       \t\t\t\tsed \"s/^/+/\" before\n       \t\t\t) >snapshot &&\n     @@ t/t5409-colorize-remote-messages.sh: test_expect_success 'setup' '\n       \n       test_expect_success 'disallow (color) control sequences in sideband' '\n      \n     - ## t/t5411/test-0013-bad-protocol.sh ##\n     -@@ t/t5411/test-0013-bad-protocol.sh: test_expect_success \"proc-receive: bad protocol (hook --die-read-version, $PROTO\n     - \t ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)\n     - \tEOF\n     - \ttest_cmp expect actual &&\n     --\tgrep \"remote: fatal: die with the --die-read-version option\" out-$test_count &&\n     --\tgrep \"remote: error: fail to negotiate version with proc-receive hook\" out-$test_count &&\n     -+\ttest_grep \"remote: fatal: die with the --die-read-version option\" out-$test_count &&\n     -+\ttest_grep \"remote: error: fail to negotiate version with proc-receive hook\" out-$test_count &&\n     - \n     - \ttest_cmp_refs -C \"$upstream\" <<-\\EOF\n     - \t<COMMIT-A> refs/heads/main\n     -@@ t/t5411/test-0013-bad-protocol.sh: test_expect_success \"proc-receive: bad protocol (hook --die-write-version, $PROT\n     - \t ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)\n     - \tEOF\n     - \ttest_cmp expect actual &&\n     --\tgrep \"remote: fatal: die with the --die-write-version option\" out-$test_count &&\n     --\tgrep \"remote: error: fail to negotiate version with proc-receive hook\" out-$test_count &&\n     -+\ttest_grep \"remote: fatal: die with the --die-write-version option\" out-$test_count &&\n     -+\ttest_grep \"remote: error: fail to negotiate version with proc-receive hook\" out-$test_count &&\n     - \n     - \ttest_cmp_refs -C \"$upstream\" <<-EOF\n     - \t<COMMIT-A> refs/heads/main\n     -@@ t/t5411/test-0013-bad-protocol.sh: test_expect_success \"proc-receive: bad protocol (hook --die-read-commands, $PROT\n     - \t ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)\n     - \tEOF\n     - \ttest_cmp expect actual &&\n     --\tgrep \"remote: fatal: die with the --die-read-commands option\" out-$test_count &&\n     -+\ttest_grep \"remote: fatal: die with the --die-read-commands option\" out-$test_count &&\n     - \n     - \ttest_cmp_refs -C \"$upstream\" <<-EOF\n     - \t<COMMIT-A> refs/heads/main\n     -@@ t/t5411/test-0013-bad-protocol.sh: test_expect_success \"proc-receive: bad protocol (hook --die-read-push-options, $\n     - \t ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)\n     - \tEOF\n     - \ttest_cmp expect actual &&\n     --\tgrep \"remote: fatal: die with the --die-read-push-options option\" out-$test_count &&\n     -+\ttest_grep \"remote: fatal: die with the --die-read-push-options option\" out-$test_count &&\n     - \n     - \ttest_cmp_refs -C \"$upstream\" <<-EOF\n     - \t<COMMIT-A> refs/heads/main\n     -@@ t/t5411/test-0013-bad-protocol.sh: test_expect_success \"proc-receive: bad protocol (hook --die-write-report, $PROTO\n     - \t ! [remote rejected] HEAD -> refs/for/main/topic (fail to run proc-receive hook)\n     - \tEOF\n     - \ttest_cmp expect actual &&\n     --\tgrep \"remote: fatal: die with the --die-write-report option\" out-$test_count &&\n     -+\ttest_grep \"remote: fatal: die with the --die-write-report option\" out-$test_count &&\n     - \n     - \ttest_cmp_refs -C \"$upstream\" <<-EOF\n     - \t<COMMIT-A> refs/heads/main\n     -\n     - ## t/t5411/test-0014-bad-protocol--porcelain.sh ##\n     -@@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success \"proc-receive: bad protocol (hook --die-read-version, $PROTO\n     - \tDone\n     - \tEOF\n     - \ttest_cmp expect actual &&\n     --\tgrep \"remote: fatal: die with the --die-read-version option\" out-$test_count &&\n     --\tgrep \"remote: error: fail to negotiate version with proc-receive hook\" out-$test_count &&\n     -+\ttest_grep \"remote: fatal: die with the --die-read-version option\" out-$test_count &&\n     -+\ttest_grep \"remote: error: fail to negotiate version with proc-receive hook\" out-$test_count &&\n     - \n     - \ttest_cmp_refs -C \"$upstream\" <<-EOF\n     - \t<COMMIT-A> refs/heads/main\n     -@@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success \"proc-receive: bad protocol (hook --die-write-version, $PROT\n     - \tDone\n     - \tEOF\n     - \ttest_cmp expect actual &&\n     --\tgrep \"remote: fatal: die with the --die-write-version option\" out-$test_count &&\n     --\tgrep \"remote: error: fail to negotiate version with proc-receive hook\" out-$test_count &&\n     -+\ttest_grep \"remote: fatal: die with the --die-write-version option\" out-$test_count &&\n     -+\ttest_grep \"remote: error: fail to negotiate version with proc-receive hook\" out-$test_count &&\n     - \n     - \ttest_cmp_refs -C \"$upstream\" <<-EOF\n     - \t<COMMIT-A> refs/heads/main\n     -@@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success \"proc-receive: bad protocol (hook --die-read-commands, $PROT\n     - \tDone\n     - \tEOF\n     - \ttest_cmp expect actual &&\n     --\tgrep \"remote: fatal: die with the --die-read-commands option\" out-$test_count &&\n     -+\ttest_grep \"remote: fatal: die with the --die-read-commands option\" out-$test_count &&\n     - \n     - \ttest_cmp_refs -C \"$upstream\" <<-EOF\n     - \t<COMMIT-A> refs/heads/main\n     -@@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success \"proc-receive: bad protocol (hook --die-read-push-options, $\n     - \tDone\n     - \tEOF\n     - \ttest_cmp expect actual &&\n     --\tgrep \"remote: fatal: die with the --die-read-push-options option\" out-$test_count &&\n     -+\ttest_grep \"remote: fatal: die with the --die-read-push-options option\" out-$test_count &&\n     - \n     - \ttest_cmp_refs -C \"$upstream\" <<-EOF\n     - \t<COMMIT-A> refs/heads/main\n     -@@ t/t5411/test-0014-bad-protocol--porcelain.sh: test_expect_success \"proc-receive: bad protocol (hook --die-write-report, $PROTO\n     - \tDone\n     - \tEOF\n     - \ttest_cmp expect actual &&\n     --\tgrep \"remote: fatal: die with the --die-write-report option\" out-$test_count &&\n     -+\ttest_grep \"remote: fatal: die with the --die-write-report option\" out-$test_count &&\n     - \n     - \ttest_cmp_refs -C \"$upstream\" <<-EOF\n     - \t<COMMIT-A> refs/heads/main\n     -\n       ## t/t5500-fetch-pack.sh ##\n      @@ t/t5500-fetch-pack.sh: test_expect_success 'single given branch clone' '\n       \tGIT_TRACE2_EVENT=\"$(pwd)/branch-a/trace2_event\" \\\n     @@ t/t5516-fetch-push.sh: test_expect_success 'push --porcelain' '\n       \tmk_empty testrepo &&\n       \ttest_must_fail git push >.git/bar --porcelain asdfasdfasd refs/heads/main:refs/remotes/origin/main &&\n      -\t! grep -q Done .git/bar\n     -+\ttest_grep ! Done .git/bar\n     ++\ttest_grep ! -q Done .git/bar\n       '\n       \n       test_expect_success 'push --porcelain rejected' '\n     @@ t/t5520-pull.sh: test_expect_success '--rebase with rebase.autostash succeeds on\n       \tgit -C dst pull --rebase >actual 2>&1 &&\n      -\tgrep -q \"Fast-forward\" actual &&\n      -\tgrep -q \"Applied autostash.\" actual\n     -+\ttest_grep \"Fast-forward\" actual &&\n     -+\ttest_grep \"Applied autostash.\" actual\n     ++\ttest_grep -q \"Fast-forward\" actual &&\n     ++\ttest_grep -q \"Applied autostash.\" actual\n       '\n       \n       test_expect_success '--rebase with conflicts shows advice' '\n     @@ t/t5537-fetch-shallow.sh: test_expect_success '.git/shallow is edited by repack'\n       \tgit -C shallow-client repack -adfl &&\n       \ttest_must_fail git -C shallow-client rev-parse --verify $d^0 &&\n      -\t! grep $d shallow-client/.git/shallow &&\n     -+\ttest_grep ! $d shallow-client/.git/shallow &&\n     ++\t! grep $d shallow-client/.git/shallow && # lint-ok: file may not exist after repack\n       \n       \tgit -C shallow-server branch branch-orig $d &&\n       \tgit -C shallow-client fetch --prune --depth=2 \\\n     @@ t/t5702-protocol-v2.sh: test_expect_success 'when server does not send \"ready\",\n       \ttest_grep \"expected no other sections to be sent after no .ready.\" err\n       '\n       \n     +@@ t/t5702-protocol-v2.sh: test_expect_success 'part of packfile response provided as URI' '\n     + \tdo\n     + \t\tgit verify-pack --object-format=$(test_oid algo) --verbose $idx >out &&\n     + \t\t{\n     +-\t\t\tgrep -E \"^[0-9a-f]{16,} \" out || :\n     ++\t\t\tgrep -E \"^[0-9a-f]{16,} \" out || : # lint-ok: data filter\n     + \t\t} >out.objectlist &&\n     + \t\tif test_line_count = 1 out.objectlist\n     + \t\tthen\n      @@ t/t5702-protocol-v2.sh: test_expect_success 'packfile-uri path redacted in trace' '\n       \t\t-c fetch.uriprotocols=http,https \\\n       \t\tclone \"$HTTPD_URL/smart/http_parent\" http_child &&\n     @@ t/t6040-tracking-info.sh: test_expect_success '--set-upstream-to does not change\n       \tgit config branch.from-main.merge > actual &&\n       \tgit rev-parse from-topic_2 >actual2 &&\n      -\tgrep -q \"^refs/heads/main$\" actual &&\n     -+\ttest_grep \"^refs/heads/main$\" actual &&\n     ++\ttest_grep -q \"^refs/heads/main$\" actual &&\n       \tcmp expect2 actual2\n       '\n       \n     @@ t/t6423-merge-rename-directories.sh: test_expect_success '5c: Transitive rename\n       \t\ttest_path_is_missing x/d &&\n       \t\ttest_path_is_file y/d &&\n      -\t\tgrep -q \"<<<<\" y/d  # conflict markers should be present\n     -+\t\ttest_grep \"<<<<\" y/d  # conflict markers should be present\n     ++\t\ttest_grep -q \"<<<<\" y/d  # conflict markers should be present\n       \t)\n       '\n       \n     @@ t/t6423-merge-rename-directories.sh: test_expect_success '9e: N-to-1 whammo' '\n      -\t\tgrep -q dir2/yo error_line &&\n      -\t\tgrep -q dir3/yo error_line &&\n      -\t\tgrep -q dirN/yo error_line &&\n     -+\t\ttest_grep dir1/yo error_line &&\n     -+\t\ttest_grep dir2/yo error_line &&\n     -+\t\ttest_grep dir3/yo error_line &&\n     -+\t\ttest_grep dirN/yo error_line &&\n     ++\t\ttest_grep -q dir1/yo error_line &&\n     ++\t\ttest_grep -q dir2/yo error_line &&\n     ++\t\ttest_grep -q dir3/yo error_line &&\n     ++\t\ttest_grep -q dirN/yo error_line &&\n       \n       \t\tgit ls-files -s >out &&\n       \t\ttest_line_count = 16 out &&\n     @@ t/t6423-merge-rename-directories.sh: test_expect_success '11b: Avoid losing dirt\n       \t\ttest_grep \"error: Your local changes to the following files would be overwritten by merge\" err &&\n       \n      -\t\tgrep -q stuff z/c &&\n     -+\t\ttest_grep stuff z/c &&\n     ++\t\ttest_grep -q stuff z/c &&\n       \t\ttest_seq 1 10 >expected &&\n       \t\techo stuff >>expected &&\n       \t\ttest_cmp expected z/c\n     @@ t/t6423-merge-rename-directories.sh: test_expect_success '11c: Avoid losing not-\n       \t\ttest_grep \"error: Your local changes to the following files would be overwritten by merge\" err &&\n       \n      -\t\tgrep -q stuff y/c &&\n     -+\t\ttest_grep stuff y/c &&\n     ++\t\ttest_grep -q stuff y/c &&\n       \t\ttest_seq 1 10 >expected &&\n       \t\techo stuff >>expected &&\n       \t\ttest_cmp expected y/c &&\n     @@ t/t6423-merge-rename-directories.sh: test_expect_success '11d: Avoid losing not-\n       \t\ttest_grep \"error: Your local changes to the following files would be overwritten by merge\" err &&\n       \n      -\t\tgrep -q stuff z/c &&\n     -+\t\ttest_grep stuff z/c &&\n     ++\t\ttest_grep -q stuff z/c &&\n       \t\ttest_seq 1 10 >expected &&\n       \t\techo stuff >>expected &&\n       \t\ttest_cmp expected z/c\n     @@ t/t6437-submodule-merge.sh: test_expect_success 'merging should fail for changes\n       \t)\n       '\n       \n     -@@ t/t6437-submodule-merge.sh: test_expect_success 'file/submodule conflict' '\n     - \t\t# directory, though, so just grep for its content in all\n     - \t\t# files, and ignore \"grep: path: Is a directory\" message\n     - \t\techo Checking if contents from B:path showed up anywhere &&\n     --\t\tgrep -q content * 2>/dev/null\n     -+\t\tgrep -q content * 2>/dev/null # lint-ok: glob arg breaks test_grep\n     - \t)\n     - '\n     - \n      @@ t/t6437-submodule-merge.sh: test_expect_success 'merging should fail with no merge base' '\n       \tgit commit -m \"b\" &&\n       \ttest_must_fail git merge a 2>actual &&\n     @@ t/t7012-skip-worktree-writing.sh: test_expect_success 'read-tree updates worktre\n       \techo dirty >> init.t &&\n       \ttest_must_fail git read-tree -m -u HEAD^ &&\n      -\tgrep -q dirty init.t &&\n     -+\ttest_grep dirty init.t &&\n     ++\ttest_grep -q dirty init.t &&\n       \ttest \"$(git ls-files -t init.t)\" = \"S init.t\" &&\n       \tgit update-index --no-skip-worktree init.t\n       '\n     @@ t/t7012-skip-worktree-writing.sh: test_expect_success 'read-tree removes worktre\n       \techo dirty >> added &&\n       \ttest_must_fail git read-tree -m -u HEAD^ &&\n      -\tgrep -q dirty added &&\n     -+\ttest_grep dirty added &&\n     ++\ttest_grep -q dirty added &&\n       \ttest \"$(git ls-files -t added)\" = \"S added\" &&\n       \tgit update-index --no-skip-worktree added\n       '\n     @@ t/t7450-bad-git-dotfiles.sh: test_expect_success WINDOWS 'prevent git~1 squattin\n       \t\t\tclone --recurse-submodules squatting squatting-clone 2>err &&\n       \t\ttest_grep -e \"directory not empty\" -e \"not an empty directory\" err &&\n      -\t\t! grep gitdir squatting-clone/d/a/git~2\n     -+\t\t! grep gitdir squatting-clone/d/a/git~2 # lint-ok: file may not exist\n     ++\t\t! grep gitdir squatting-clone/d/a/git~2 # lint-ok: file may not exist after failed clone\n       \tfi\n       '\n       \n     @@ t/t7501-commit-basic-functionality.sh: test_expect_success 'editor not invoked i\n       \tEDITOR=./editor git commit -a -F msg &&\n       \tgit show -s --pretty=format:%s >subject &&\n      -\tgrep -q good subject &&\n     -+\ttest_grep good subject &&\n     ++\ttest_grep -q good subject &&\n       \n       \techo quack >file &&\n       \techo Another good message. |\n       \tEDITOR=./editor git commit -a -F - &&\n       \tgit show -s --pretty=format:%s >subject &&\n      -\tgrep -q good subject\n     -+\ttest_grep good subject\n     ++\ttest_grep -q good subject\n       '\n       \n       test_expect_success 'partial commit that involves removal (1)' '\n     @@ t/t7501-commit-basic-functionality.sh: test_expect_success 'amend does not add s\n       \n       test_expect_success 'commit complains about completely bogus dates' '\n      @@ t/t7501-commit-basic-functionality.sh: test_expect_success 'git commit <file> with dirty index' '\n     + \tgit add chz &&\n     + \tgit commit elif -m \"tacocat is a palindrome\" &&\n       \tgit show --stat >stat &&\n     - \tgrep elif stat &&\n     +-\tgrep elif stat &&\n     ++\ttest_grep elif stat &&\n       \tgit diff --cached >diff &&\n      -\tgrep chz diff\n      +\ttest_grep chz diff\n     @@ t/t7510-signed-commit.sh: test_expect_success GPG 'verify and show signatures' '\n      -\tgrep -q -F -e \"No public key\" -e \"public key not found\" actual\n      +\ttest_grep ! \"Good signature from\" actual &&\n      +\ttest_grep ! \"BAD signature from\" actual &&\n     -+\ttest_grep -F -e \"No public key\" -e \"public key not found\" actual\n     ++\ttest_grep -q -F -e \"No public key\" -e \"public key not found\" actual\n       '\n       \n       test_expect_success GPG 'verify-commit exits success on untrusted signature' '\n     @@ t/t7519-status-fsmonitor.sh: test_expect_success 'setup' '\n       cat >expect <<EOF &&\n      \n       ## t/t7527-builtin-fsmonitor.sh ##\n     -@@ t/t7527-builtin-fsmonitor.sh: verify_fsmonitor_works () {\n     - \t\t\t--token 0 >/dev/null 2>&1\n     - \tmaybe_timeout 5 \\\n     - \t\tgit -C test_fsmonitor_smoke fsmonitor--daemon stop 2>/dev/null\n     --\t! grep -q \"cookie_wait timed out\" \"$PWD/smoke.trace\" 2>/dev/null\n     -+\t! grep -q \"cookie_wait timed out\" \"$PWD/smoke.trace\" 2>/dev/null # lint-ok: $? capture\n     - \tret=$?\n     - \trm -rf test_fsmonitor_smoke smoke.trace\n     - \treturn $ret\n      @@ t/t7527-builtin-fsmonitor.sh: test_expect_success 'implicit daemon start' '\n       \tGIT_TRACE2_EVENT=\"$PWD/.git/trace\" \\\n       \t\ttest-tool -C test_implicit fsmonitor-client query --token 0 >actual &&\n     @@ t/t7527-builtin-fsmonitor.sh: test_expect_success CASE_INSENSITIVE_FS 'fsmonitor\n       \n      -\tgrep -q \"AAA.*pos 0\" \"$PWD/subdir_case_wrong.log1\" &&\n      -\tgrep -q \"zzz.*pos 6\" \"$PWD/subdir_case_wrong.log1\" &&\n     -+\ttest_grep \"AAA.*pos 0\" \"$PWD/subdir_case_wrong.log1\" &&\n     -+\ttest_grep \"zzz.*pos 6\" \"$PWD/subdir_case_wrong.log1\" &&\n     ++\ttest_grep -q \"AAA.*pos 0\" \"$PWD/subdir_case_wrong.log1\" &&\n     ++\ttest_grep -q \"zzz.*pos 6\" \"$PWD/subdir_case_wrong.log1\" &&\n       \n      -\tgrep -q \"dir1/DIR2/dir3/file3.*pos -3\" \"$PWD/subdir_case_wrong.log1\" &&\n     -+\ttest_grep \"dir1/DIR2/dir3/file3.*pos -3\" \"$PWD/subdir_case_wrong.log1\" &&\n     ++\ttest_grep -q \"dir1/DIR2/dir3/file3.*pos -3\" \"$PWD/subdir_case_wrong.log1\" &&\n       \n       \t# Verify that we get a mapping event to correct the case.\n      -\tgrep -q \"MAP:.*dir1/DIR2/dir3/file3.*dir1/dir2/dir3/file3\" \\\n     -+\ttest_grep \"MAP:.*dir1/DIR2/dir3/file3.*dir1/dir2/dir3/file3\" \\\n     ++\ttest_grep -q \"MAP:.*dir1/DIR2/dir3/file3.*dir1/dir2/dir3/file3\" \\\n       \t\t\"$PWD/subdir_case_wrong.log1\" &&\n       \n       \t# The refresh-callbacks should have caused \"git status\" to clear\n     @@ t/t7527-builtin-fsmonitor.sh: test_expect_success CASE_INSENSITIVE_FS 'fsmonitor\n      -\tgrep -q \" M AAA\" \"$PWD/subdir_case_wrong.out\" &&\n      -\tgrep -q \" M zzz\" \"$PWD/subdir_case_wrong.out\" &&\n      -\tgrep -q \" M dir1/dir2/dir3/file3\" \"$PWD/subdir_case_wrong.out\"\n     -+\ttest_grep \" M AAA\" \"$PWD/subdir_case_wrong.out\" &&\n     -+\ttest_grep \" M zzz\" \"$PWD/subdir_case_wrong.out\" &&\n     -+\ttest_grep \" M dir1/dir2/dir3/file3\" \"$PWD/subdir_case_wrong.out\"\n     ++\ttest_grep -q \" M AAA\" \"$PWD/subdir_case_wrong.out\" &&\n     ++\ttest_grep -q \" M zzz\" \"$PWD/subdir_case_wrong.out\" &&\n     ++\ttest_grep -q \" M dir1/dir2/dir3/file3\" \"$PWD/subdir_case_wrong.out\"\n       '\n       \n       test_expect_success CASE_INSENSITIVE_FS 'fsmonitor file case wrong on disk' '\n     @@ t/t7527-builtin-fsmonitor.sh: test_expect_success CASE_INSENSITIVE_FS 'fsmonitor\n      -\tgrep -q \"fsmonitor_refresh_callback.*file-3-a.*pos 4\"  \"$PWD/file_case_wrong-try1.log\" &&\n      -\tgrep -q \"fsmonitor_refresh_callback.*FILE-4-A.*pos 6\"  \"$PWD/file_case_wrong-try1.log\" &&\n      -\tgrep -q \"fsmonitor_refresh_callback.*file-4-a.*pos -9\" \"$PWD/file_case_wrong-try1.log\" &&\n     -+\ttest_grep \"fsmonitor_refresh_callback.*FILE-3-A.*pos -3\" \"$PWD/file_case_wrong-try1.log\" &&\n     -+\ttest_grep \"fsmonitor_refresh_callback.*file-3-a.*pos 4\"  \"$PWD/file_case_wrong-try1.log\" &&\n     -+\ttest_grep \"fsmonitor_refresh_callback.*FILE-4-A.*pos 6\"  \"$PWD/file_case_wrong-try1.log\" &&\n     -+\ttest_grep \"fsmonitor_refresh_callback.*file-4-a.*pos -9\" \"$PWD/file_case_wrong-try1.log\" &&\n     ++\ttest_grep -q \"fsmonitor_refresh_callback.*FILE-3-A.*pos -3\" \"$PWD/file_case_wrong-try1.log\" &&\n     ++\ttest_grep -q \"fsmonitor_refresh_callback.*file-3-a.*pos 4\"  \"$PWD/file_case_wrong-try1.log\" &&\n     ++\ttest_grep -q \"fsmonitor_refresh_callback.*FILE-4-A.*pos 6\"  \"$PWD/file_case_wrong-try1.log\" &&\n     ++\ttest_grep -q \"fsmonitor_refresh_callback.*file-4-a.*pos -9\" \"$PWD/file_case_wrong-try1.log\" &&\n       \n       \t# FSM refresh will have invalidated the FSM bit and cause a regular\n       \t# (real) scan of these tracked files, so they should have \"H\" status.\n     @@ t/t7527-builtin-fsmonitor.sh: test_expect_success CASE_INSENSITIVE_FS 'fsmonitor\n       \tgit -C file_case_wrong ls-files -f >\"$PWD/file_case_wrong-lsf1.out\" &&\n      -\tgrep -q \"H dir1/dir2/dir3/file-3-a\" \"$PWD/file_case_wrong-lsf1.out\" &&\n      -\tgrep -q \"H dir1/dir2/dir4/FILE-4-A\" \"$PWD/file_case_wrong-lsf1.out\" &&\n     -+\ttest_grep \"H dir1/dir2/dir3/file-3-a\" \"$PWD/file_case_wrong-lsf1.out\" &&\n     -+\ttest_grep \"H dir1/dir2/dir4/FILE-4-A\" \"$PWD/file_case_wrong-lsf1.out\" &&\n     ++\ttest_grep -q \"H dir1/dir2/dir3/file-3-a\" \"$PWD/file_case_wrong-lsf1.out\" &&\n     ++\ttest_grep -q \"H dir1/dir2/dir4/FILE-4-A\" \"$PWD/file_case_wrong-lsf1.out\" &&\n       \n       \n       \t# Try the status again. We assume that the above status command\n     @@ t/t7527-builtin-fsmonitor.sh: test_expect_success CASE_INSENSITIVE_FS 'fsmonitor\n      -\t! grep -q \"fsmonitor_refresh_callback.*file-3-a.*pos\" \"$PWD/file_case_wrong-try2.log\" &&\n      -\t! grep -q \"fsmonitor_refresh_callback.*FILE-4-A.*pos\" \"$PWD/file_case_wrong-try2.log\" &&\n      -\t! grep -q \"fsmonitor_refresh_callback.*file-4-a.*pos\" \"$PWD/file_case_wrong-try2.log\" &&\n     -+\ttest_grep ! \"fsmonitor_refresh_callback.*FILE-3-A.*pos\" \"$PWD/file_case_wrong-try2.log\" &&\n     -+\ttest_grep ! \"fsmonitor_refresh_callback.*file-3-a.*pos\" \"$PWD/file_case_wrong-try2.log\" &&\n     -+\ttest_grep ! \"fsmonitor_refresh_callback.*FILE-4-A.*pos\" \"$PWD/file_case_wrong-try2.log\" &&\n     -+\ttest_grep ! \"fsmonitor_refresh_callback.*file-4-a.*pos\" \"$PWD/file_case_wrong-try2.log\" &&\n     ++\ttest_grep ! -q \"fsmonitor_refresh_callback.*FILE-3-A.*pos\" \"$PWD/file_case_wrong-try2.log\" &&\n     ++\ttest_grep ! -q \"fsmonitor_refresh_callback.*file-3-a.*pos\" \"$PWD/file_case_wrong-try2.log\" &&\n     ++\ttest_grep ! -q \"fsmonitor_refresh_callback.*FILE-4-A.*pos\" \"$PWD/file_case_wrong-try2.log\" &&\n     ++\ttest_grep ! -q \"fsmonitor_refresh_callback.*file-4-a.*pos\" \"$PWD/file_case_wrong-try2.log\" &&\n       \n       \t# FSM refresh saw nothing, so it will mark all files as valid,\n       \t# so they should now have \"h\" status.\n     @@ t/t7527-builtin-fsmonitor.sh: test_expect_success CASE_INSENSITIVE_FS 'fsmonitor\n       \tgit -C file_case_wrong ls-files -f >\"$PWD/file_case_wrong-lsf2.out\" &&\n      -\tgrep -q \"h dir1/dir2/dir3/file-3-a\" \"$PWD/file_case_wrong-lsf2.out\" &&\n      -\tgrep -q \"h dir1/dir2/dir4/FILE-4-A\" \"$PWD/file_case_wrong-lsf2.out\" &&\n     -+\ttest_grep \"h dir1/dir2/dir3/file-3-a\" \"$PWD/file_case_wrong-lsf2.out\" &&\n     -+\ttest_grep \"h dir1/dir2/dir4/FILE-4-A\" \"$PWD/file_case_wrong-lsf2.out\" &&\n     ++\ttest_grep -q \"h dir1/dir2/dir3/file-3-a\" \"$PWD/file_case_wrong-lsf2.out\" &&\n     ++\ttest_grep -q \"h dir1/dir2/dir4/FILE-4-A\" \"$PWD/file_case_wrong-lsf2.out\" &&\n       \n       \n       \t# We now have files with clean content, but with case-incorrect\n     @@ t/t7527-builtin-fsmonitor.sh: test_expect_success CASE_INSENSITIVE_FS 'fsmonitor\n       \n       \t# Verify that we get a mapping event to correct the case.\n      -\tgrep -q \"fsmonitor_refresh_callback MAP:.*dir1/dir2/dir3/FILE-3-A.*dir1/dir2/dir3/file-3-a\" \\\n     -+\ttest_grep \"fsmonitor_refresh_callback MAP:.*dir1/dir2/dir3/FILE-3-A.*dir1/dir2/dir3/file-3-a\" \\\n     ++\ttest_grep -q \"fsmonitor_refresh_callback MAP:.*dir1/dir2/dir3/FILE-3-A.*dir1/dir2/dir3/file-3-a\" \\\n       \t\t\"$PWD/file_case_wrong-try3.log\" &&\n      -\tgrep -q \"fsmonitor_refresh_callback MAP:.*dir1/dir2/dir4/file-4-a.*dir1/dir2/dir4/FILE-4-A\" \\\n     -+\ttest_grep \"fsmonitor_refresh_callback MAP:.*dir1/dir2/dir4/file-4-a.*dir1/dir2/dir4/FILE-4-A\" \\\n     ++\ttest_grep -q \"fsmonitor_refresh_callback MAP:.*dir1/dir2/dir4/file-4-a.*dir1/dir2/dir4/FILE-4-A\" \\\n       \t\t\"$PWD/file_case_wrong-try3.log\" &&\n       \n       \t# FSEvents are in observed case.\n      -\tgrep -q \"fsmonitor_refresh_callback.*FILE-3-A.*pos -3\" \"$PWD/file_case_wrong-try3.log\" &&\n      -\tgrep -q \"fsmonitor_refresh_callback.*file-4-a.*pos -9\" \"$PWD/file_case_wrong-try3.log\" &&\n     -+\ttest_grep \"fsmonitor_refresh_callback.*FILE-3-A.*pos -3\" \"$PWD/file_case_wrong-try3.log\" &&\n     -+\ttest_grep \"fsmonitor_refresh_callback.*file-4-a.*pos -9\" \"$PWD/file_case_wrong-try3.log\" &&\n     ++\ttest_grep -q \"fsmonitor_refresh_callback.*FILE-3-A.*pos -3\" \"$PWD/file_case_wrong-try3.log\" &&\n     ++\ttest_grep -q \"fsmonitor_refresh_callback.*file-4-a.*pos -9\" \"$PWD/file_case_wrong-try3.log\" &&\n       \n       \t# The refresh-callbacks should have caused \"git status\" to clear\n       \t# the CE_FSMONITOR_VALID bit on each of those files and caused\n       \t# the worktree scan to visit them and mark them as modified.\n      -\tgrep -q \" M dir1/dir2/dir3/file-3-a\" \"$PWD/file_case_wrong-try3.out\" &&\n      -\tgrep -q \" M dir1/dir2/dir4/FILE-4-A\" \"$PWD/file_case_wrong-try3.out\"\n     -+\ttest_grep \" M dir1/dir2/dir3/file-3-a\" \"$PWD/file_case_wrong-try3.out\" &&\n     -+\ttest_grep \" M dir1/dir2/dir4/FILE-4-A\" \"$PWD/file_case_wrong-try3.out\"\n     ++\ttest_grep -q \" M dir1/dir2/dir3/file-3-a\" \"$PWD/file_case_wrong-try3.out\" &&\n     ++\ttest_grep -q \" M dir1/dir2/dir4/FILE-4-A\" \"$PWD/file_case_wrong-try3.out\"\n       '\n       \n       test_done\n     @@ t/t7700-repack.sh: test_expect_success 'repack --keep-pack' '\n       \t\tls .git/objects/pack/*.pack >new-counts &&\n      -\t\tgrep -q $P1 new-counts &&\n      -\t\tgrep -q $P4 new-counts &&\n     -+\t\ttest_grep $P1 new-counts &&\n     -+\t\ttest_grep $P4 new-counts &&\n     ++\t\ttest_grep -q $P1 new-counts &&\n     ++\t\ttest_grep -q $P4 new-counts &&\n       \t\ttest_line_count = 3 new-counts &&\n       \t\tgit fsck &&\n       \n     @@ t/t7703-repack-geometric.sh: test_expect_success '--geometric with small-pack ro\n       \t\ttest_line_count = 3 after &&\n       \t\tcomm -3 small before | tr -d \"\\t\" >large &&\n      -\t\tgrep -qFf large after\n     -+\t\ttest_grep -Ff large after\n     ++\t\ttest_grep -qFf large after\n       \t)\n       '\n       \n     @@ t/t9117-git-svn-init-clone.sh: test_expect_success 'clone to target directory wi\n       \ttest ! -d trunk &&\n       \tgit svn init \"$svnrepo\"/project/trunk trunk 2>warning &&\n      -\t! grep -q prefix warning &&\n     -+\ttest_grep ! prefix warning &&\n     ++\ttest_grep ! -q prefix warning &&\n       \trm -rf trunk &&\n       \trm -f warning\n       \t'\n     @@ t/t9117-git-svn-init-clone.sh: test_expect_success 'init without -s/-T/-b/-t doe\n       \ttest ! -d trunk &&\n       \tgit svn clone \"$svnrepo\"/project/trunk 2>warning &&\n      -\t! grep -q prefix warning &&\n     -+\ttest_grep ! prefix warning &&\n     ++\ttest_grep ! -q prefix warning &&\n       \trm -rf trunk &&\n       \trm -f warning\n       \t'\n     @@ t/t9117-git-svn-init-clone.sh: EOF\n       \ttest ! -d project &&\n       \tgit svn init -s \"$svnrepo\"/project project 2>warning &&\n      -\t! grep -q prefix warning &&\n     -+\ttest_grep ! prefix warning &&\n     ++\ttest_grep ! -q prefix warning &&\n       \ttest_svn_configured_prefix \"origin/\" &&\n       \trm -rf project &&\n       \trm -f warning\n     @@ t/t9117-git-svn-init-clone.sh: test_expect_success 'init with -s/-T/-b/-t assume\n       \ttest ! -d project &&\n       \tgit svn clone -s \"$svnrepo\"/project 2>warning &&\n      -\t! grep -q prefix warning &&\n     -+\ttest_grep ! prefix warning &&\n     ++\ttest_grep ! -q prefix warning &&\n       \ttest_svn_configured_prefix \"origin/\" &&\n       \trm -rf project &&\n       \trm -f warning\n     @@ t/t9117-git-svn-init-clone.sh: test_expect_success 'clone with -s/-T/-b/-t assum\n       \ttest ! -d project &&\n       \tgit svn init -s \"$svnrepo\"/project project --prefix \"\" 2>warning &&\n      -\t! grep -q prefix warning &&\n     -+\ttest_grep ! prefix warning &&\n     ++\ttest_grep ! -q prefix warning &&\n       \ttest_svn_configured_prefix \"\" &&\n       \trm -rf project &&\n       \trm -f warning\n     @@ t/t9117-git-svn-init-clone.sh: test_expect_success 'init with -s/-T/-b/-t and --\n       \ttest ! -d project &&\n       \tgit svn clone -s \"$svnrepo\"/project --prefix \"\" 2>warning &&\n      -\t! grep -q prefix warning &&\n     -+\ttest_grep ! prefix warning &&\n     ++\ttest_grep ! -q prefix warning &&\n       \ttest_svn_configured_prefix \"\" &&\n       \trm -rf project &&\n       \trm -f warning\n     @@ t/t9502-gitweb-standalone-parse-output.sh: test_expect_success 'forks: setup' '\n      -\tgrep -q \">foo_baz\\\\.git<\"        gitweb.body &&\n      -\tgrep -q \">foo/foo-forked\\\\.git<\" gitweb.body &&\n      -\tgrep -q \">fork of .*<\"           gitweb.body\n     -+\ttest_grep \">\\\\.git<\"               gitweb.body &&\n     -+\ttest_grep \">foo\\\\.git<\"            gitweb.body &&\n     -+\ttest_grep \">foo_baz\\\\.git<\"        gitweb.body &&\n     -+\ttest_grep \">foo\\\\.bar\\\\.git<\"      gitweb.body &&\n     -+\ttest_grep \">foo_baz\\\\.git<\"        gitweb.body &&\n     -+\ttest_grep \">foo/foo-forked\\\\.git<\" gitweb.body &&\n     -+\ttest_grep \">fork of .*<\"           gitweb.body\n     ++\ttest_grep -q \">\\\\.git<\"               gitweb.body &&\n     ++\ttest_grep -q \">foo\\\\.git<\"            gitweb.body &&\n     ++\ttest_grep -q \">foo_baz\\\\.git<\"        gitweb.body &&\n     ++\ttest_grep -q \">foo\\\\.bar\\\\.git<\"      gitweb.body &&\n     ++\ttest_grep -q \">foo_baz\\\\.git<\"        gitweb.body &&\n     ++\ttest_grep -q \">foo/foo-forked\\\\.git<\" gitweb.body &&\n     ++\ttest_grep -q \">fork of .*<\"           gitweb.body\n       '\n       \n       test_expect_success 'enable forks feature' '\n     @@ t/t9502-gitweb-standalone-parse-output.sh: test_expect_success 'enable forks fea\n      -\tgrep -q \">foo_baz\\\\.git<\"        gitweb.body &&\n      -\tgrep -v \">foo/foo-forked\\\\.git<\" gitweb.body &&\n      -\tgrep -v \">fork of .*<\"           gitweb.body\n     -+\ttest_grep \">\\\\.git<\"               gitweb.body &&\n     -+\ttest_grep \">foo\\\\.git<\"            gitweb.body &&\n     -+\ttest_grep \">foo_baz\\\\.git<\"        gitweb.body &&\n     -+\ttest_grep \">foo\\\\.bar\\\\.git<\"      gitweb.body &&\n     -+\ttest_grep \">foo_baz\\\\.git<\"        gitweb.body &&\n     ++\ttest_grep -q \">\\\\.git<\"               gitweb.body &&\n     ++\ttest_grep -q \">foo\\\\.git<\"            gitweb.body &&\n     ++\ttest_grep -q \">foo_baz\\\\.git<\"        gitweb.body &&\n     ++\ttest_grep -q \">foo\\\\.bar\\\\.git<\"      gitweb.body &&\n     ++\ttest_grep -q \">foo_baz\\\\.git<\"        gitweb.body &&\n      +\ttest_grep -v \">foo/foo-forked\\\\.git<\" gitweb.body &&\n      +\ttest_grep -v \">fork of .*<\"           gitweb.body\n       '\n     @@ t/t9502-gitweb-standalone-parse-output.sh: test_expect_success 'enable forks fea\n       \tgitweb_run \"p=foo.git;a=forks\" &&\n      -\tgrep -q \">foo/foo-forked\\\\.git<\" gitweb.body &&\n      -\tgrep -q \">fork of foo<\"          gitweb.body\n     -+\ttest_grep \">foo/foo-forked\\\\.git<\" gitweb.body &&\n     -+\ttest_grep \">fork of foo<\"          gitweb.body\n     ++\ttest_grep -q \">foo/foo-forked\\\\.git<\" gitweb.body &&\n     ++\ttest_grep -q \">fork of foo<\"          gitweb.body\n       '\n       \n       test_expect_success 'forks: can access forked repository' '\n       \tgitweb_run \"p=foo/foo-forked.git;a=summary\" &&\n      -\tgrep -q \"200 OK\"        gitweb.headers &&\n      -\tgrep -q \">fork of foo<\" gitweb.body\n     -+\ttest_grep \"200 OK\"        gitweb.headers &&\n     -+\ttest_grep \">fork of foo<\" gitweb.body\n     ++\ttest_grep -q \"200 OK\"        gitweb.headers &&\n     ++\ttest_grep -q \">fork of foo<\" gitweb.body\n       '\n       \n       test_expect_success 'forks: project_index lists all projects (incl. forks)' '\n     @@ t/t9807-git-p4-submit.sh: test_expect_success 'submit --update-shelve' '\n       \t\tchange=$(last_shelve) &&\n       \t\tp4 unshelve -c $change -s $change &&\n      -\t\tgrep -q updated-line shelf.t &&\n     -+\t\ttest_grep updated-line shelf.t &&\n     ++\t\ttest_grep -q updated-line shelf.t &&\n       \t\tp4 describe -S $change | grep added-file.t &&\n       \t\ttest_path_is_missing shelved-change-1.t &&\n       \t\tp4 revert ...\n     @@ t/t9832-unshelve.sh: EOF\n       \t\tgit p4 unshelve $change &&\n       \t\tgit show refs/remotes/p4-unshelved/$change >actual &&\n      -\t\tgrep -q \"Further description\" actual &&\n     -+\t\ttest_grep \"Further description\" actual &&\n     ++\t\ttest_grep -q \"Further description\" actual &&\n       \t\tgit cherry-pick refs/remotes/p4-unshelved/$change &&\n       \t\ttest_path_is_file file2 &&\n       \t\ttest_cmp file1 \"$cli\"/file1 &&\n     @@ t/t9832-unshelve.sh: test_expect_success 'create shelved changelist based on p4\n       \t\tchange=$(last_shelved_change) &&\n       \t\tp4 describe -S $change >out.txt &&\n      -\t\tgrep -q \"Change to be unshelved\" out.txt\n     -+\t\ttest_grep \"Change to be unshelved\" out.txt\n     ++\t\ttest_grep -q \"Change to be unshelved\" out.txt\n       \t)\n       '\n       \n     @@ t/t9832-unshelve.sh: test_expect_success 'try to unshelve the change' '\n       \t\tcd \"$git\" &&\n       \t\tgit p4 unshelve $change >out.txt &&\n      -\t\tgrep -q \"unshelved changelist $change\" out.txt\n     -+\t\ttest_grep \"unshelved changelist $change\" out.txt\n     ++\t\ttest_grep -q \"unshelved changelist $change\" out.txt\n       \t)\n       '\n       \n     @@ t/t9833-errors.sh: test_expect_success 'error handling' '\n       \t\texport P4PASSWD &&\n       \t\ttest_must_fail git p4 clone //depot/foo 2>errmsg &&\n      -\t\tgrep -q \"failure accessing depot.*P4PASSWD\" errmsg\n     -+\t\ttest_grep \"failure accessing depot.*P4PASSWD\" errmsg\n     ++\t\ttest_grep -q \"failure accessing depot.*P4PASSWD\" errmsg\n       \t)\n       '\n       \n     @@ t/t9833-errors.sh: test_expect_success 'ticket logged out' '\n       \t\tp4 logout &&\n       \t\ttest_must_fail git p4 submit 2>errmsg &&\n      -\t\tgrep -q \"failure accessing depot\" errmsg\n     -+\t\ttest_grep \"failure accessing depot\" errmsg\n     ++\t\ttest_grep -q \"failure accessing depot\" errmsg\n       \t)\n       '\n       \n     @@ t/t9902-completion.sh: test_expect_success '__git_pretty_aliases' '\n       \trun_completion \"git \" &&\n       \t# built-in\n      -\tgrep -q \"^add \\$\" out &&\n     -+\ttest_grep \"^add \\$\" out &&\n     ++\ttest_grep -q \"^add \\$\" out &&\n       \t# script\n      -\tgrep -q \"^rebase \\$\" out &&\n     -+\ttest_grep \"^rebase \\$\" out &&\n     ++\ttest_grep -q \"^rebase \\$\" out &&\n       \t# plumbing\n      -\t! grep -q \"^ls-files \\$\" out &&\n     -+\ttest_grep ! \"^ls-files \\$\" out &&\n     ++\ttest_grep ! -q \"^ls-files \\$\" out &&\n       \n       \trun_completion \"git r\" &&\n      -\t! grep -q -v \"^r\" out\n     -+\ttest_grep ! -v \"^r\" out\n     ++\ttest_grep ! -q -v \"^r\" out\n       '\n       \n       test_expect_success 'double dash \"git\" itself' '\n 4:  c1b90101ef ! 6:  e5ecb37401 t: add lint-style.pl with test_grep negation rule\n     @@ Metadata\n      Author: Michael Montalbo <mmontalbo@gmail.com>\n      \n       ## Commit message ##\n     -    t: add lint-style.pl with test_grep negation rule\n     +    t: add greplint to detect bare grep assertions\n      \n     -    Add a mechanical lint checker for test scripts, similar in spirit to\n     -    check-non-portable-shell.pl but focused on test conventions rather\n     -    than portability.\n     +    Without a lint guard, bare grep assertions will creep back into\n     +    tests over time, defeating the previous commit's conversion.\n      \n     -    The tool defines LintParser, a subclass of ScriptParser (from the\n     -    shared lib-shell-parser.pl module).  ScriptParser's\n     -    parse_cmd() finds test_expect_success blocks and calls check_test()\n     -    for each body; LintParser overrides check_test() to run lint rules\n     -    on the parsed commands.  A \"# lint-ok\" comment suppresses all\n     -    checks for intentional style violations.\n     +    Add greplint.pl to catch bare 'grep' used as a test assertion\n     +    (where 'test_grep' should be used) and '! test_grep' (where\n     +    'test_grep !' should be used).\n      \n     -    The first rule detects '! test_grep' and replaces it with\n     -    'test_grep !'.  Shell-level negation suppresses the diagnostic\n     -    output that test_grep prints on failure; the built-in negation\n     -    preserves it.\n     +    greplint.pl reuses the shared shell parser from lib-shell-parser.pl\n     +    to tokenize test bodies.  The Lexer collapses heredocs, command\n     +    substitutions, and quoted strings into single tokens, so 'grep'\n     +    appearing inside these contexts is not flagged.  A flat walk over\n     +    the token stream tracks command position and pipeline state to\n     +    distinguish assertion greps from filter greps.\n      \n     -    Three violations inside test bodies are converted via --fix.  One\n     -    additional violation in a helper function outside test_expect_success\n     -    (t7900's test_geometric_repack_needed) is converted manually, since\n     -    the parser only processes test bodies.\n     +    For double-quoted test bodies, a source-line walk counts\n     +    backslash-continuation lines that the Lexer consumes without\n     +    emitting into the body text, adjusting the reported line number\n     +    accordingly.\n     +\n     +    Add test fixtures in greplint/ (modeled on chainlint/) covering\n     +    detection of bare grep assertions, correct skipping of filters,\n     +    pipelines, redirects, command substitutions, and lint-ok annotations.\n     +\n     +    Wire into the Makefile as:\n     +      - test-greplint: runs greplint.pl on $(T) $(THELPERS) $(TPERF)\n     +      - check-greplint: runs greplint.pl on fixtures, diffs against expected\n     +      - clean-greplint: removes temp dir\n     +\n     +    Add eol=lf entries in t/.gitattributes for greplint fixtures,\n     +    matching chainlint, so that check-greplint passes on Windows\n     +    where core.autocrlf would otherwise cause CRLF mismatches\n     +    between expected and actual output.\n      \n          Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>\n      \n     @@ t/.gitattributes\n      @@\n       t[0-9][0-9][0-9][0-9]/* -whitespace\n       /chainlint/*.expect eol=lf -whitespace\n     -+/lint-style/*.expect eol=lf -whitespace\n     -+/lint-style/*.test eol=lf -whitespace\n     ++/greplint/*.expect eol=lf -whitespace\n     ++/greplint/*.test eol=lf -whitespace\n       /t0110/url-* binary\n       /t3206/* eol=lf\n       /t3900/*.txt eol=lf\n      \n       ## t/Makefile ##\n     -@@ t/Makefile: TPERF = $(sort $(wildcard perf/p[0-9][0-9][0-9][0-9]-*.sh))\n     +@@ t/Makefile: TEST_LINT ?= test-lint\n     + ifdef TEST_OUTPUT_DIRECTORY\n     + TEST_RESULTS_DIRECTORY = $(TEST_OUTPUT_DIRECTORY)/test-results\n     + CHAINLINTTMP = $(TEST_OUTPUT_DIRECTORY)/chainlinttmp\n     ++GREPLINTTMP = $(TEST_OUTPUT_DIRECTORY)/greplinttmp\n     + else\n     + TEST_RESULTS_DIRECTORY = test-results\n     + CHAINLINTTMP = chainlinttmp\n     ++GREPLINTTMP = greplinttmp\n     + endif\n     + \n     + # Shell quote;\n     +@@ t/Makefile: TEST_SHELL_PATH_SQ = $(subst ','\\'',$(TEST_SHELL_PATH))\n     + PERL_PATH_SQ = $(subst ','\\'',$(PERL_PATH))\n     + TEST_RESULTS_DIRECTORY_SQ = $(subst ','\\'',$(TEST_RESULTS_DIRECTORY))\n     + CHAINLINTTMP_SQ = $(subst ','\\'',$(CHAINLINTTMP))\n     ++GREPLINTTMP_SQ = $(subst ','\\'',$(GREPLINTTMP))\n     + \n     + T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh))\n     + THELPERS = $(sort $(filter-out $(T),$(wildcard *.sh)))\n     +@@ t/Makefile: TLIBS = $(sort $(wildcard lib-*.sh)) annotate-tests.sh\n     + TPERF = $(sort $(wildcard perf/p[0-9][0-9][0-9][0-9]-*.sh))\n       TINTEROP = $(sort $(wildcard interop/i[0-9][0-9][0-9][0-9]-*.sh))\n       CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.test)))\n     ++GREPLINTTESTS = $(sort $(patsubst greplint/%.test,%,$(wildcard greplint/*.test)))\n       CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl\n     -+LINT_STYLE_TESTS = $(sort $(wildcard lint-style/*.test))\n       UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)\n       UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))\n     - UNIT_TEST_PROGRAMS += unit-tests/bin/unit-tests$(X)\n     +@@ t/Makefile: test: pre-clean check-meson $(TEST_LINT)\n     + \t$(CHAINLINTSUPPRESS) $(MAKE) aggregate-results-and-cleanup\n     + \n     + ifneq ($(PERL_PATH),)\n     +-test: check-chainlint\n     +-prove: check-chainlint\n     ++test: check-chainlint check-greplint\n     ++prove: check-chainlint check-greplint\n     + endif\n     + \n     + failed:\n     +@@ t/Makefile: unit-tests-test-tool:\n     + pre-clean:\n     + \t$(RM) -r '$(TEST_RESULTS_DIRECTORY_SQ)'\n     + \n     +-clean-except-prove-cache: clean-chainlint\n     ++clean-except-prove-cache: clean-chainlint clean-greplint\n     + \t$(RM) -r 'trash directory'.*\n     + \t$(RM) -r valgrind/bin\n     + \n     +@@ t/Makefile: check-chainlint:\n     + \t{ $(CHAINLINT) --emit-all '$(CHAINLINTTMP_SQ)'/tests >'$(CHAINLINTTMP_SQ)'/actual || true; } && \\\n     + \tdiff -u '$(CHAINLINTTMP_SQ)'/expect '$(CHAINLINTTMP_SQ)'/actual\n     + \n     ++clean-greplint:\n     ++\t$(RM) -r '$(GREPLINTTMP_SQ)'\n     ++\n     ++check-greplint:\n     ++\t@mkdir -p '$(GREPLINTTMP_SQ)' && \\\n     ++\t'$(PERL_PATH_SQ)' greplint-cat.pl '$(GREPLINTTMP_SQ)' $(GREPLINTTESTS) && \\\n     ++\t{ '$(PERL_PATH_SQ)' greplint.pl \\\n     ++\t\t$(patsubst %,greplint/%.test,$(GREPLINTTESTS)) \\\n     ++\t\t>'$(GREPLINTTMP_SQ)'/actual 2>&1 || true; } && \\\n     ++\tdiff -u '$(GREPLINTTMP_SQ)'/expect '$(GREPLINTTMP_SQ)'/actual\n     ++\n     + check-meson:\n     + \t@# awk acts up when trying to match single quotes, so we use \\047 instead.\n     + \t@mkdir -p mesontmp && \\\n      @@ t/Makefile: check-meson:\n       test-lint: test-lint-duplicates test-lint-executable \\\n       \ttest-lint-filenames\n       ifneq ($(PERL_PATH),)\n     --test-lint: test-lint-shell-syntax check-shell-parser\n     -+test-lint: test-lint-shell-syntax test-lint-style check-lint-style check-shell-parser\n     +-test-lint: test-lint-shell-syntax\n     ++test-lint: test-lint-shell-syntax test-greplint\n       else\n       GIT_TEST_CHAIN_LINT = 0\n       endif\n     -@@ t/Makefile: test-lint-shell-syntax:\n     +@@ t/Makefile: test-lint-executable:\n     + test-lint-shell-syntax:\n     + \t@'$(PERL_PATH_SQ)' check-non-portable-shell.pl $(T) $(THELPERS) $(TPERF)\n       \n     - check-shell-parser:\n     - \t@'$(PERL_PATH_SQ)' check-shell-parser.pl\n     -+\n     -+test-lint-style:\n     -+\t@'$(PERL_PATH_SQ)' lint-style.pl $(T) $(THELPERS) $(TPERF)\n     -+\n     -+check-lint-style:\n     -+\t@rc=0; for t in $(LINT_STYLE_TESTS); do \\\n     -+\t\tbase=$${t%.test}; \\\n     -+\t\tcase $$base in \\\n     -+\t\t*-fix) \\\n     -+\t\t\tcp \"$$t\" \"$$t.tmp\" && \\\n     -+\t\t\t'$(PERL_PATH_SQ)' lint-style.pl --fix \"$$t.tmp\" >/dev/null 2>&1; \\\n     -+\t\t\tfix_rc=$$?; \\\n     -+\t\t\tif test $$fix_rc != 0; then \\\n     -+\t\t\t\techo \"FAIL: $$t (--fix exit code $$fix_rc)\"; rc=1; \\\n     -+\t\t\telif ! diff -u \"$$base.expect\" \"$$t.tmp\"; then \\\n     -+\t\t\t\techo \"FAIL: $$t (--fix output)\"; rc=1; \\\n     -+\t\t\tfi; \\\n     -+\t\t\trm -f \"$$t.tmp\" ;; \\\n     -+\t\t*) \\\n     -+\t\t\tif ! '$(PERL_PATH_SQ)' lint-style.pl \"$$t\" 2>&1 | \\\n     -+\t\t\t\tdiff -u \"$$base.expect\" -; then \\\n     -+\t\t\t\techo \"FAIL: $$t\"; rc=1; \\\n     -+\t\t\tfi ;; \\\n     -+\t\tesac; \\\n     -+\tdone; test $$rc = 0\n     ++test-greplint:\n     ++\t@'$(PERL_PATH_SQ)' greplint.pl $(T) $(THELPERS) $(TPERF)\n      +\n       test-lint-filenames:\n       \t@# We do *not* pass a glob to ls-files but use grep instead, to catch\n       \t@# non-ASCII characters (which are quoted within double-quotes)\n      @@ t/Makefile: perf:\n     + \t$(MAKE) -C perf/ all\n       \n       .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \\\n     - \tcheck-chainlint clean-chainlint test-chainlint \\\n     --\tcheck-shell-parser $(UNIT_TESTS)\n     -+\tcheck-shell-parser \\\n     -+\tcheck-lint-style test-lint-style $(UNIT_TESTS)\n     +-\tcheck-chainlint clean-chainlint test-chainlint $(UNIT_TESTS)\n     ++\tcheck-chainlint clean-chainlint test-chainlint \\\n     ++\tcheck-greplint clean-greplint test-greplint $(UNIT_TESTS)\n       \n       .PHONY: libgit-sys-test libgit-rs-test\n       libgit-sys-test:\n      \n     - ## t/lint-style.pl (new) ##\n     + ## t/greplint-cat.pl (new) ##\n      @@\n     -+#!/usr/bin/perl\n     -+\n     -+# Check test scripts for style violations that can be detected\n     -+# mechanically, such as using bare 'grep' where test_grep should\n     -+# be used.  Use --fix to automatically apply suggested replacements.\n     -+#\n     -+# Detection uses parsed tokens from the shared shell parser for\n     -+# correct handling of heredocs, $(...), pipes, and quoting.\n     -+# Fixes modify the original file text to preserve formatting.\n     ++#!/usr/bin/env perl\n      +\n      +use strict;\n      +use warnings;\n     -+use File::Basename;\n     -+# Force LF output so check-lint-style's diff against the\n     -+# pre-committed .expect files works on Windows.\n     -+binmode(STDOUT, ':unix');\n     -+binmode(STDERR, ':unix');\n      +\n     -+my $fix_mode = 0;\n     -+if (@ARGV && $ARGV[0] eq '--fix') {\n     -+\t$fix_mode = 1;\n     -+\tshift @ARGV;\n     ++# Assemble expected output for check-greplint target.\n     ++# Usage: greplint-cat.pl <outdir> <test-name> ...\n     ++#\n     ++# For each <test-name>, reads greplint/<test-name>.expect and\n     ++# prepends \"greplint/<test-name>.test:\" to every non-empty line,\n     ++# matching the output format of greplint.pl.  Writes combined\n     ++# expected output to <outdir>/expect.\n     ++\n     ++my $outdir = shift;\n     ++open(my $expect, '>', \"$outdir/expect\")\n     ++\tor die \"unable to open $outdir/expect: $!\";\n     ++\n     ++for my $name (@ARGV) {\n     ++\topen(my $fh, '<', \"greplint/$name.expect\")\n     ++\t\tor die \"unable to open greplint/$name.expect: $!\";\n     ++\twhile (<$fh>) {\n     ++\t\tprint $expect \"greplint/$name.test:$_\";\n     ++\t}\n     ++\tclose $fh;\n      +}\n      +\n     -+# Load the shared shell parser (Lexer, ShellParser, ScriptParser).\n     -+my $_lib = dirname($0) . \"/lib-shell-parser.pl\";\n     -+$_lib = \"./$_lib\" unless $_lib =~ m{^/};\n     -+do $_lib or die \"$0: failed to load $_lib: $@$!\\n\";\n     ++close $expect;\n     +\n     + ## t/greplint.pl (new) ##\n     +@@\n     ++#!/usr/bin/env perl\n      +\n     -+# LintParser is a subclass of ScriptParser which runs lint rules\n     -+# on each test body.  Per-file state (file name, raw lines, dirty\n     -+# flag) is stored on the instance before calling parse().\n     ++# Detect bare 'grep' used as a test assertion where 'test_grep'\n     ++# should be used, and '! test_grep' where 'test_grep !' should\n     ++# be used.\n      +#\n     -+# Subroutines defined below (parse_commands, check_test_grep_negation,\n     -+# etc.) are in package main and called with the main:: prefix.\n     -+# File-scoped lexicals ($fix_mode, $has_fixable, etc.) are visible\n     -+# across packages since 'package' does not introduce a new scope.\n     -+package LintParser;\n     -+our @ISA = ('ScriptParser');\n     ++# The shared shell parser tokenizes test bodies so that 'grep'\n     ++# inside heredocs, command substitutions like $(grep ...), and\n     ++# quoted strings is collapsed into a single token and never seen\n     ++# by our check.  A line-oriented approach would need to track\n     ++# heredoc delimiters, nested $() depth, and cross-line pipe\n     ++# state to avoid false positives on patterns like:\n     ++#\n     ++#   write_script foo.sh <<-\\EOF\n     ++#   grep pattern file    # data, not an assertion\n     ++#   EOF\n     ++#\n     ++# The Lexer already handles these.\n      +\n     -+package main;\n     ++use warnings;\n     ++use strict;\n     ++use File::Basename;\n     ++do(dirname($0) . \"/lib-shell-parser.pl\")\n     ++\tor die \"$0: failed to load lib-shell-parser.pl: $@$!\\n\";\n      +\n      +my $exit_code = 0;\n     -+my $has_fixable = 0;\n      +\n     -+sub err {\n     -+\tmy ($file, $lineno, $line, $msg, %opts) = @_;\n     -+\t$line =~ s/^\\s+//;\n     -+\t$line =~ s/\\s+$//;\n     -+\t$line =~ s/\\s+/ /g;\n     -+\tmy $prefix = ($fix_mode && $opts{fixable}) ? 'fixed' : 'error';\n     -+\tprint \"$file:$lineno: $prefix: $msg: $line\\n\";\n     -+\t$exit_code = 1 unless $fix_mode && $opts{fixable};\n     -+}\n     ++# GrepLintParser inherits ScriptParser's ability to find\n     ++# test_expect_success/failure blocks and call check_test()\n     ++# on each body.  We override check_test() to walk the token\n     ++# stream looking for bare grep assertions.\n     ++package GrepLintParser;\n      +\n     -+# Report a lint violation found by a rule.  In --fix mode, apply\n     -+# the regex substitution on the raw line and report success.\n     -+# Otherwise just report.  Returns 1 if the line was modified.\n     -+sub report_violation {\n     -+\tmy ($file, $cmd, $line_ref, $match, $fix, $from) = @_;\n     -+\tmy $lineno = $cmd->{lineno};\n     -+\tmy $display = join(' ', @{$cmd->{tokens}});\n     -+\t$has_fixable++;  # count for the \"--fix\" hint\n     -+\tif ($fix_mode) {\n     -+\t\tif ($$line_ref =~ s/$match/$fix/) {\n     -+\t\t\terr $file, $lineno, $display,\n     -+\t\t\t\t\"replace '$from' with '$fix'\",\n     -+\t\t\t\tfixable => 1;\n     -+\t\t\treturn 1;\n     -+\t\t}\n     -+\t\terr $file, $lineno, $display,\n     -+\t\t\t\"replace '$from' with '$fix' (could not auto-fix)\";\n     -+\t} else {\n     -+\t\terr $file, $lineno, $display,\n     -+\t\t\t\"replace '$from' with '$fix'\";\n     ++our @ISA = ('ScriptParser');\n     ++\n     ++# After these tokens, the next token is a command word.\n     ++# For example, in 'echo foo && grep bar file', the 'grep'\n     ++# after '&&' is at command position and should be flagged.\n     ++my %cmd_start = map { $_ => 1 } qw(&& || ; ;; do then else elif), \"\\n\", '{', '(';\n     ++\n     ++# Tokens indicating grep's output is piped or redirected.\n     ++my %filter_op = map { $_ => 1 } qw(| > >> <);\n     ++\n     ++# A token is at \"command word\" position if the shell would\n     ++# interpret it as a program name rather than an argument.\n     ++# Only 'grep' at command position is an assertion we should\n     ++# flag; 'grep' as an argument ('test_must_fail grep') or\n     ++# value ('for cmd in grep sed') is not.\n     ++sub is_command_word {\n     ++\tmy ($tokens, $pos) = @_;\n     ++\treturn 1 if $pos == 0;\n     ++\tfor (my $j = $pos - 1; $j >= 0; $j--) {\n     ++\t\tmy $t = $tokens->[$j]->[0];\n     ++\t\t# After a separator or pipe, a new command starts.\n     ++\t\treturn 1 if $cmd_start{$t} || $t eq '|';\n     ++\t\t# After '}' or ')', what follows is a separator or\n     ++\t\t# redirect on the compound command, not a new command.\n     ++\t\treturn 0 if $t eq '}' || $t eq ')';\n     ++\t\t# '!' is a prefix that does not consume command\n     ++\t\t# position; keep scanning to find what precedes it.\n     ++\t\tnext if $t eq '!';\n     ++\t\t# Any other word means we are past the command word.\n     ++\t\treturn 0;\n      +\t}\n     -+\treturn 0;\n     ++\treturn 1;\n      +}\n      +\n     -+# Split a token stream into commands at &&, ||, ;;, and \\n.\n     -+sub parse_commands {\n     -+\tmy ($content) = @_;\n     -+\tmy $parser = ShellParser->new(\\$content);\n     -+\tmy @all_tokens = $parser->parse();\n     ++# Some bare greps are intentional (e.g. file may not exist,\n     ++# data filter).  A '# lint-ok' annotation on the source line\n     ++# suppresses the warning.\n     ++sub lint_ok {\n     ++\tmy ($raw_lines, $ln) = @_;\n     ++\tif ($ln < 1 || $ln > @$raw_lines) {\n     ++\t\twarn \"lint_ok: line number $ln out of range (1..\" .\n     ++\t\t    scalar(@$raw_lines) . \")\\n\";\n     ++\t\treturn 0;\n     ++\t}\n     ++\treturn $raw_lines->[$ln - 1] =~ /lint-ok/;\n     ++}\n      +\n     -+\tmy @commands;\n     -+\tmy @current;\n     -+\tmy $lineno = 1;\n     ++# Grep is a filter (not an assertion) if it receives piped\n     ++# input or sends its output to a pipe or redirect.  Check\n     ++# both directions from grep's position in the token stream.\n     ++sub is_filter {\n     ++\tmy ($tokens, $pos) = @_;\n     ++\t# Backward: is grep receiving piped input?\n     ++\t# Newlines don't break pipes ('cmd |\\n grep' is one\n     ++\t# pipeline), so skip past them.\n     ++\tfor (my $j = $pos - 1; $j >= 0; $j--) {\n     ++\t\tmy $t = $tokens->[$j]->[0];\n     ++\t\treturn 1 if $t eq '|';\n     ++\t\tnext if $t eq \"\\n\";\n     ++\t\tlast if $cmd_start{$t} || $t eq '}' || $t eq ')';\n     ++\t}\n     ++\t# Forward: is grep piping or redirecting output?\n     ++\t# Unlike the backward scan, we do not skip newlines here:\n     ++\t# a bare newline is a command boundary, and redirects or\n     ++\t# pipes must appear on the same line as grep (or after a\n     ++\t# line continuation, which the Lexer consumes).\n     ++\tfor (my $j = $pos + 1; $j < @$tokens; $j++) {\n     ++\t\tmy $t = $tokens->[$j]->[0];\n     ++\t\treturn 0 if $cmd_start{$t};\n     ++\t\treturn 1 if $filter_op{$t};\n     ++\t}\n     ++\treturn 0;\n     ++}\n      +\n     -+\tfor (my $ti = 0; $ti < @all_tokens; $ti++) {\n     -+\t\tmy $text = $all_tokens[$ti]->[0];\n     -+\t\tif ($text =~ /^(?:&&|\\|\\||;;|\\n)$/) {\n     -+\t\t\tif (@current) {\n     -+\t\t\t\tpush @commands, {\n     -+\t\t\t\t\ttokens => [@current],\n     -+\t\t\t\t\tlineno => $lineno,\n     -+\t\t\t\t};\n     -+\t\t\t\t@current = ();\n     -+\t\t\t}\n     ++# Map a body-relative line number to a file line number.\n     ++# For double-quoted bodies, backslash-continuation lines\n     ++# (\\<newline>) are consumed by the Lexer without appearing\n     ++# in the body text, so the inner parser sees fewer lines\n     ++# than the source file has.  We walk the source lines to\n     ++# count continuations and adjust accordingly.\n     ++sub body_to_file_line {\n     ++\tmy ($body_lineno, $body_token, $raw_lines, $body_start) = @_;\n     ++\tmy $body_text = $body_token->[0];\n     ++\tmy $body_end_line = $body_token->[4];\n     ++\tunless ($body_start && $body_start >= 1) {\n     ++\t\twarn \"body_start is not a positive integer\\n\";\n     ++\t\treturn $body_lineno;\n     ++\t}\n     ++\tmy $file_lineno = $body_lineno + $body_start - 1;\n     ++\t# Only double-quoted bodies have line splices.\n     ++\treturn $file_lineno unless $body_text =~ /^\"/;\n     ++\tmy $adj = 0;\n     ++\tmy $lines_seen = 0;\n     ++\tunless ($body_end_line && $body_end_line >= $body_start) {\n     ++\t\twarn \"body_end_line is not set for double-quoted body\\n\";\n     ++\t\treturn $file_lineno;\n     ++\t}\n     ++\tmy $end = $body_end_line;\n     ++\tif ($end > @$raw_lines) {\n     ++\t\twarn \"body_end_line ($end) exceeds file length (\" .\n     ++\t\t    scalar(@$raw_lines) . \")\\n\";\n     ++\t\treturn $file_lineno;\n     ++\t}\n     ++\tmy $src_ln = $body_start;\n     ++\twhile ($src_ln <= $end && $lines_seen < $body_lineno) {\n     ++\t\tmy $line = $raw_lines->[$src_ln - 1];\n     ++\t\t# Odd trailing backslashes = continuation (\\<nl>).\n     ++\t\t# Even = escaped backslashes (\\\\), not a continuation.\n     ++\t\tif ($line =~ /(\\\\*)$/ && length($1) % 2 == 1) {\n     ++\t\t\t$adj++;\n      +\t\t} else {\n     -+\t\t\t$lineno = $all_tokens[$ti]->[3]\n     -+\t\t\t\tif !@current && defined $all_tokens[$ti]->[3];\n     -+\t\t\tpush @current, $text;\n     ++\t\t\t$lines_seen++;\n      +\t\t}\n     ++\t\t$src_ln++;\n      +\t}\n     -+\tif (@current) {\n     -+\t\tpush @commands, {\n     -+\t\t\ttokens => [@current],\n     -+\t\t\tlineno => $lineno,\n     -+\t\t};\n     ++\tif ($lines_seen < $body_lineno) {\n     ++\t\twarn \"body_lineno ($body_lineno) not found within body range \" .\n     ++\t\t    \"($body_start..$end)\\n\";\n      +\t}\n     -+\treturn @commands;\n     ++\treturn $file_lineno + $adj;\n      +}\n      +\n     -+# --- Rule: '! test_grep' should be 'test_grep !' ---\n     -+# Shell-level negation suppresses test_grep's diagnostic output\n     -+# on failure.  Built-in negation preserves it.\n     -+sub check_test_grep_negation {\n     -+\tmy ($cmd, $file, $line_ref) = @_;\n     -+\tmy @tokens = @{$cmd->{tokens}};\n     -+\treturn unless @tokens >= 2 && $tokens[0] eq '!' && $tokens[1] eq 'test_grep';\n     -+\n     -+\treturn report_violation($file, $cmd, $line_ref,\n     -+\t\tqr/!\\s*test_grep/, 'test_grep !', '! test_grep');\n     -+}\n     -+\n     -+# Map parsed commands back to raw file lines for --fix.\n     -+# Detection uses parsed tokens (correct handling of quoting,\n     -+# heredocs, pipes) but fixes must modify the original text\n     -+# to preserve formatting.\n     -+package LintParser;\n     -+\n     ++# ScriptParser calls this for each test body found in the script.\n      +sub check_test {\n     -+\t# Called by ScriptParser::parse_cmd for each test_expect_success\n     -+\t# or test_expect_failure block.\n      +\tmy $self = shift @_;\n      +\tmy $title = ScriptParser::unwrap(shift @_);\n     -+\n     -+\t# Two test body formats:\n     -+\t#   Quoted:  test_expect_success 'title' '..body..'\n     -+\t#   Heredoc: test_expect_success 'title' - <<\\EOF\n     -+\t#              ..body..\n     -+\t#            EOF\n     -+\t# For quoted, the body token is the quoted string.\n     -+\t# For heredoc, the body token is '-' and the actual\n     -+\t# code arrives as the next argument from the Lexer.\n      +\tmy $body_token = shift @_;\n     -+\tmy $lineno_base = $body_token->[3] || 1;\n     ++\tmy $body_start = $body_token->[3];\n      +\tmy $body = ScriptParser::unwrap($body_token);\n     -+\n     ++\t# Handle heredoc-style test bodies:\n     ++\t#   test_expect_success 'title' - <<\\EOF\n     ++\t#   grep pattern file\n     ++\t#   EOF\n     ++\t# The '-' signals that the body follows as a heredoc.\n      +\tif ($body eq '-') {\n      +\t\tmy $herebody = shift @_;\n      +\t\tif ($herebody) {\n      +\t\t\t$body = $herebody->{content};\n     -+\t\t\t$lineno_base = $herebody->{start_line} || 1;\n     ++\t\t\t$body_start = $herebody->{start_line};\n      +\t\t}\n      +\t}\n      +\treturn unless $body;\n      +\n     -+\t# Map each command back to its file line number.\n     -+\t# $lineno_base is where the body starts in the file;\n     -+\t# $cmd->{lineno} is relative to the body (starting at 1).\n      +\tmy $raw_lines = $self->{raw_lines};\n     -+\tfor my $cmd (main::parse_commands($body)) {\n     -+\t\tmy $ln = ($cmd->{lineno} || 0) + $lineno_base - 1;\n     -+\t\t$cmd->{lineno} = $ln;\n     -+\t\tnext unless $ln >= 1 && $ln <= @$raw_lines;\n     -+\t\tnext if $raw_lines->[$ln - 1] =~ /#.*lint-ok/;\n      +\n     -+\t\tif (main::check_test_grep_negation($cmd, $self->{file}, \\$raw_lines->[$ln - 1])) {\n     -+\t\t\t$self->{dirty} = 1;\n     ++\t# The outer parser gives us the body as an opaque string.\n     ++\t# Parse it to get individual tokens with command boundaries.\n     ++\tmy $parser = ShellParser->new(\\$body);\n     ++\tmy @tokens = $parser->parse();\n     ++\n     ++\tmy $file = $self->{file};\n     ++\n     ++\tfor (my $i = 0; $i < @tokens; $i++) {\n     ++\t\tmy $text = $tokens[$i]->[0];\n     ++\t\tnext unless is_command_word(\\@tokens, $i);\n     ++\n     ++\t\tmy $token_lineno = $tokens[$i]->[3];\n     ++\t\tunless (defined($token_lineno) && $token_lineno >= 1) {\n     ++\t\t\twarn \"token has no line number\\n\";\n     ++\t\t\tnext;\n     ++\t\t}\n     ++\t\tmy $file_lineno = body_to_file_line(\n     ++\t\t\t$token_lineno,\n     ++\t\t\t$body_token, $raw_lines, $body_start);\n     ++\n     ++\t\t# '!' negates the exit code without consuming command\n     ++\t\t# position.  '! test_grep' is an anti-pattern because\n     ++\t\t# test_grep only prints diagnostics on grep failure,\n     ++\t\t# and '!' inverts after that decision is already made.\n     ++\t\tif ($text eq '!') {\n     ++\t\t\tif ($i + 1 < @tokens &&\n     ++\t\t\t    $tokens[$i + 1]->[0] eq 'test_grep' &&\n     ++\t\t\t    !lint_ok($raw_lines, $file_lineno)) {\n     ++\t\t\t\tprint \"$file:$file_lineno: error: \",\n     ++\t\t\t\t    'use \"test_grep !\" instead of ',\n     ++\t\t\t\t    '\"! test_grep\"', \"\\n\";\n     ++\t\t\t\t$exit_code = 1;\n     ++\t\t\t}\n     ++\t\t\tnext;\n     ++\t\t}\n     ++\n     ++\t\t# Bare grep as a command (not a filter) is a test\n     ++\t\t# assertion that should use test_grep for better\n     ++\t\t# failure diagnostics.\n     ++\t\tif ($text eq 'grep' &&\n     ++\t\t    !is_filter(\\@tokens, $i) &&\n     ++\t\t    !lint_ok($raw_lines, $file_lineno)) {\n     ++\t\t\tprint \"$file:$file_lineno: error: \",\n     ++\t\t\t    \"bare grep outside pipeline \",\n     ++\t\t\t    \"(use test_grep)\\n\";\n     ++\t\t\t$exit_code = 1;\n      +\t\t}\n      +\t}\n      +}\n     @@ t/lint-style.pl (new)\n      +package main;\n      +\n      +for my $file (@ARGV) {\n     -+\t# :unix:crlf strips \\r on Windows (same as chainlint.pl)\n      +\topen(my $fh, '<:unix:crlf', $file) or die \"$0: $file: $!\\n\";\n      +\tmy @raw_lines = <$fh>;\n      +\tclose $fh;\n     -+\n     -+\tmy $parser = LintParser->new(\\join('', @raw_lines));\n     ++\tmy $s = join('', @raw_lines);\n     ++\tmy $parser = GrepLintParser->new(\\$s);\n      +\t$parser->{file} = $file;\n      +\t$parser->{raw_lines} = \\@raw_lines;\n     -+\t$parser->{dirty} = 0;\n      +\t$parser->parse();\n     -+\n     -+\tif ($fix_mode && $parser->{dirty}) {\n     -+\t\topen(my $out, '>', $file) or die \"$0: $file: $!\\n\";\n     -+\t\tprint $out @{$parser->{raw_lines}};\n     -+\t\tclose $out;\n     -+\t}\n     -+}\n     -+\n     -+if ($has_fixable && !$fix_mode) {\n     -+\tprint \"hint: run with --fix to apply the suggested replacements.\\n\";\n      +}\n      +exit $exit_code;\n      \n     - ## t/lint-style/heredoc.expect (new) ##\n     + ## t/greplint/bare-grep-after-and.expect (new) ##\n      @@\n     -+lint-style/heredoc.test:8: error: replace '! test_grep' with 'test_grep !': ! test_grep \"after-heredoc-is-caught\" actual\n     -+lint-style/heredoc.test:13: error: replace '! test_grep' with 'test_grep !': ! test_grep \"not-inside-sed-heredoc\" actual\n     -+hint: run with --fix to apply the suggested replacements.\n     ++3: error: bare grep outside pipeline (use test_grep)\n      \n     - ## t/lint-style/heredoc.test (new) ##\n     + ## t/greplint/bare-grep-after-and.test (new) ##\n      @@\n     -+test_expect_success 'greps inside heredocs are skipped' '\n     -+\tcat <<-EOF &&\n     -+\tgrep \"inside-strip-tabs\" file\n     -+\tEOF\n     -+\tcat <<-\\EOF &&\n     -+\tgrep \"inside-no-expand\" file\n     -+\tEOF\n     -+\t! test_grep \"after-heredoc-is-caught\" actual\n     ++test_expect_success 'grep after && is flagged' '\n     ++\tcmd &&\n     ++\tgrep pattern file\n      +'\n     -+\n     -+test_expect_success 'sed with << does not start a heredoc' '\n     -+\tsed \"s/<< foo/bar/\" file &&\n     -+\t! test_grep \"not-inside-sed-heredoc\" actual\n     +\n     + ## t/greplint/bare-grep-after-semicolon.expect (new) ##\n     +@@\n     ++3: error: bare grep outside pipeline (use test_grep)\n     +\n     + ## t/greplint/bare-grep-after-semicolon.test (new) ##\n     +@@\n     ++test_expect_success 'grep after semicolon is flagged' '\n     ++\techo hello;\n     ++\tgrep pattern file\n      +'\n      \n     - ## t/lint-style/test-grep-negation-fix.expect (new) ##\n     + ## t/greplint/bare-grep-compound-body.expect (new) ##\n      @@\n     -+test_expect_success 'negated test_grep' '\n     -+\ttest_grep ! \"pattern\" actual &&\n     -+\ttest_grep ! -i \"insensitive\" actual\n     ++4: error: bare grep outside pipeline (use test_grep)\n     ++8: error: bare grep outside pipeline (use test_grep)\n     ++15: error: bare grep outside pipeline (use test_grep)\n     +\n     + ## t/greplint/bare-grep-compound-body.test (new) ##\n     +@@\n     ++test_expect_success 'grep after then/do/else is flagged' '\n     ++\tif true\n     ++\tthen\n     ++\t\tgrep pattern file\n     ++\tfi &&\n     ++\twhile true\n     ++\tdo\n     ++\t\tgrep pattern file &&\n     ++\t\tbreak\n     ++\tdone &&\n     ++\tif true\n     ++\tthen\n     ++\t\techo yes\n     ++\telse\n     ++\t\tgrep pattern file\n     ++\tfi\n      +'\n      \n     - ## t/lint-style/test-grep-negation-fix.test (new) ##\n     + ## t/greplint/bare-grep-count-mode.expect (new) ##\n     +@@\n     ++2: error: bare grep outside pipeline (use test_grep)\n     +\n     + ## t/greplint/bare-grep-count-mode.test (new) ##\n      @@\n     -+test_expect_success 'negated test_grep' '\n     -+\t! test_grep \"pattern\" actual &&\n     -+\t! test_grep -i \"insensitive\" actual\n     ++test_expect_success 'grep -c is flagged (not special-cased)' '\n     ++\tgrep -c pattern file\n      +'\n      \n     - ## t/lint-style/test-grep-negation.expect (new) ##\n     + ## t/greplint/bare-grep-explicit-pattern.expect (new) ##\n      @@\n     -+lint-style/test-grep-negation.test:2: error: replace '! test_grep' with 'test_grep !': ! test_grep \"pattern\" actual\n     -+lint-style/test-grep-negation.test:3: error: replace '! test_grep' with 'test_grep !': ! test_grep -i \"insensitive\" actual\n     -+hint: run with --fix to apply the suggested replacements.\n     ++2: error: bare grep outside pipeline (use test_grep)\n      \n     - ## t/lint-style/test-grep-negation.test (new) ##\n     + ## t/greplint/bare-grep-explicit-pattern.test (new) ##\n      @@\n     -+test_expect_success 'negated test_grep' '\n     -+\t! test_grep \"pattern\" actual &&\n     -+\t! test_grep -i \"insensitive\" actual\n     ++test_expect_success 'grep -e is flagged' '\n     ++\tgrep -e pattern file\n      +'\n      \n     - ## t/t0031-lockfile-pid.sh ##\n     -@@ t/t0031-lockfile-pid.sh: test_expect_success 'PID info not shown by default' '\n     - \t\ttest_must_fail git add . 2>err &&\n     - \t\t# Should not crash, just show normal error without PID\n     - \t\ttest_grep \"Unable to create\" err &&\n     --\t\t! test_grep \"is held by process\" err\n     -+\t\ttest_grep ! \"is held by process\" err\n     - \t)\n     - '\n     - \n     + ## t/greplint/bare-grep-flags.expect (new) ##\n     +@@\n     ++2: error: bare grep outside pipeline (use test_grep)\n      \n     - ## t/t5300-pack-object.sh ##\n     -@@ t/t5300-pack-object.sh: test_expect_success '--name-hash-version=2 and --write-bitmap-index are incompat\n     - \n     - \t# --stdout option silently removes --write-bitmap-index\n     - \tgit pack-objects --stdout --all --name-hash-version=2 --write-bitmap-index >out 2>err &&\n     --\t! test_grep \"currently, --write-bitmap-index requires --name-hash-version=1\" err\n     -+\ttest_grep ! \"currently, --write-bitmap-index requires --name-hash-version=1\" err\n     - '\n     - \n     - test_expect_success '--path-walk pack everything' '\n     + ## t/greplint/bare-grep-flags.test (new) ##\n     +@@\n     ++test_expect_success 'grep -E is flagged' '\n     ++\tgrep -E \"pat+ern\" file\n     ++'\n      \n     - ## t/t5319-multi-pack-index.sh ##\n     -@@ t/t5319-multi-pack-index.sh: test_expect_success 'load reverse index when missing .idx, .pack' '\n     - \n     - test_expect_success 'usage shown without sub-command' '\n     - \ttest_expect_code 129 git multi-pack-index 2>err &&\n     --\t! test_grep \"unrecognized subcommand\" err\n     -+\ttest_grep ! \"unrecognized subcommand\" err\n     - '\n     - \n     - test_expect_success 'complains when run outside of a repository' '\n     -\n     - ## t/t7900-maintenance.sh ##\n     -@@ t/t7900-maintenance.sh: test_geometric_repack_needed () {\n     - \ttrue)\n     - \t\ttest_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n     - \tfalse)\n     --\t\t! test_grep \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n     -+\t\ttest_grep ! \"\\[\\\"git\\\",\\\"repack\\\",\" trace2.txt;;\n     - \t*)\n     - \t\tBUG \"invalid parameter: $NEEDED\";;\n     - \tesac\n     + ## t/greplint/bare-grep-lint-ok.expect (new) ##\n     +\n     + ## t/greplint/bare-grep-lint-ok.test (new) ##\n     +@@\n     ++test_expect_success 'grep with lint-ok annotation is not flagged' '\n     ++\tgrep pattern file && # lint-ok\n     ++\techo done\n     ++'\n     +\n     + ## t/greplint/bare-grep-negated.expect (new) ##\n     +@@\n     ++2: error: bare grep outside pipeline (use test_grep)\n     +\n     + ## t/greplint/bare-grep-negated.test (new) ##\n     +@@\n     ++test_expect_success 'negated grep is flagged' '\n     ++\t! grep pattern file\n     ++'\n     +\n     + ## t/greplint/bare-grep-pattern-file.expect (new) ##\n     +@@\n     ++2: error: bare grep outside pipeline (use test_grep)\n     +\n     + ## t/greplint/bare-grep-pattern-file.test (new) ##\n     +@@\n     ++test_expect_success 'grep -f is flagged' '\n     ++\tgrep -f patterns.txt file\n     ++'\n     +\n     + ## t/greplint/bare-grep-simple.expect (new) ##\n     +@@\n     ++2: error: bare grep outside pipeline (use test_grep)\n     +\n     + ## t/greplint/bare-grep-simple.test (new) ##\n     +@@\n     ++test_expect_success 'bare grep is flagged' '\n     ++\tgrep pattern file\n     ++'\n     +\n     + ## t/greplint/bare-grep-subshell.expect (new) ##\n     +@@\n     ++3: error: bare grep outside pipeline (use test_grep)\n     +\n     + ## t/greplint/bare-grep-subshell.test (new) ##\n     +@@\n     ++test_expect_success 'grep in subshell is flagged' '\n     ++\t(\n     ++\t\tgrep pattern file\n     ++\t)\n     ++'\n     +\n     + ## t/greplint/dqstring-continuation-offset.expect (new) ##\n     +@@\n     ++10: error: bare grep outside pipeline (use test_grep)\n     +\n     + ## t/greplint/dqstring-continuation-offset.test (new) ##\n     +@@\n     ++# Double-quoted test bodies with backslash-continuation lines:\n     ++# the splice adjustment in check_test compensates for \\<newline>\n     ++# lines that the lexer consumes without emitting into the body\n     ++# text, so the reported line number matches the source.\n     ++test_expect_success 'dqstring continuation offset' \"\n     ++\tx=\\$(echo \\\n     ++\t\thello) &&\n     ++\ty=\\$(echo \\\n     ++\t\tworld) &&\n     ++\tgrep pattern file\n     ++\"\n     +\n     + ## t/greplint/filter-command-substitution.expect (new) ##\n     +\n     + ## t/greplint/filter-command-substitution.test (new) ##\n     +@@\n     ++test_expect_success 'grep in command substitution is not flagged' '\n     ++\tx=$(grep pattern file)\n     ++'\n     +\n     + ## t/greplint/filter-pipe-input.expect (new) ##\n     +\n     + ## t/greplint/filter-pipe-input.test (new) ##\n     +@@\n     ++test_expect_success 'grep receiving pipe input is not flagged' '\n     ++\tcmd | grep pattern\n     ++'\n     +\n     + ## t/greplint/filter-pipe-output.expect (new) ##\n     +\n     + ## t/greplint/filter-pipe-output.test (new) ##\n     +@@\n     ++test_expect_success 'grep piping to another command is not flagged' '\n     ++\tgrep pattern file | wc -l\n     ++'\n     +\n     + ## t/greplint/filter-redirect-output.expect (new) ##\n     +\n     + ## t/greplint/filter-redirect-output.test (new) ##\n     +@@\n     ++test_expect_success 'grep with output redirect is not flagged' '\n     ++\tgrep pattern file >output\n     ++'\n     +\n     + ## t/greplint/filter-stdin-redirect.expect (new) ##\n     +\n     + ## t/greplint/filter-stdin-redirect.test (new) ##\n     +@@\n     ++test_expect_success 'grep reading from stdin redirect is not flagged' '\n     ++\tgrep pattern <input\n     ++'\n     +\n     + ## t/greplint/grep-as-argument.expect (new) ##\n     +\n     + ## t/greplint/grep-as-argument.test (new) ##\n     +@@\n     ++test_expect_success 'grep as argument to another command is not flagged' '\n     ++\ttest_must_fail grep pattern file\n     ++'\n     +\n     + ## t/greplint/grep-as-value.expect (new) ##\n     +\n     + ## t/greplint/grep-as-value.test (new) ##\n     +@@\n     ++test_expect_success 'grep as value in for-loop is not flagged' '\n     ++\tfor cmd in grep sed awk\n     ++\tdo\n     ++\t\techo $cmd\n     ++\tdone\n     ++'\n     +\n     + ## t/greplint/wrong-negation.expect (new) ##\n     +@@\n     ++2: error: use \"test_grep !\" instead of \"! test_grep\"\n     +\n     + ## t/greplint/wrong-negation.test (new) ##\n     +@@\n     ++test_expect_success 'wrong negation of test_grep is flagged' '\n     ++\t! test_grep pattern file\n     ++'\n\n-- \ngitgitgadget\n"},{"id":"545431","messageId":"5959cab258ad6c5099e1aa7696416cc2376b3a3d.1781323575.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v2.git.1781323575.gitgitgadget@gmail.com","subject":"[PATCH v2 1/6] t/README: document test_grep helper","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-13T04:06:10Z","receivedAt":"2026-06-13T04:06:21Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\ntest_grep is a wrapper around grep for test assertions that prints\nthe file contents on failure for easier debugging.  It also accepts\n'!' as its first argument for negation, which preserves the\ndiagnostic output that '! test_grep' would suppress.\n\nDespite being widely used (and the preferred replacement for bare\ngrep in assertions), test_grep has no entry in t/README alongside\nthe other documented helpers like test_cmp and test_line_count.\nAdd one.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/README | 21 +++++++++++++++++++++\n 1 file changed, 21 insertions(+)\n\ndiff --git a/t/README b/t/README\nindex adbbd9acf4..c12a1c317a 100644\n--- a/t/README\n+++ b/t/README\n@@ -1039,6 +1039,27 @@ see test-lib-functions.sh for the full list and their options.\n \n    Check whether a file has the length it is expected to.\n \n+ - test_grep [!] [<grep-options>] <pattern> <file>\n+\n+   Check whether <file> contains a line matching <pattern>, or\n+   with '!' that no line matches.  Use this instead of bare\n+   'grep <pattern> <file>' in test assertions.  On failure,\n+   test_grep prints the contents of <file> for easier debugging,\n+   whereas a bare 'grep' would fail silently.\n+\n+   For negation, pass '!' as the first argument:\n+\n+\ttest_grep ! \"^diff --git\" actual\n+\n+   Do not negate by writing '! test_grep', as that suppresses the\n+   diagnostic output.\n+\n+   test_grep should only be used as a test assertion.  When grep\n+   is used as a data filter (e.g. 'grep -v \"^index\" actual >filtered')\n+   or inside a command substitution (e.g. '$(grep -c ...)'), plain\n+   'grep' is the right choice because the exit code is not the\n+   assertion itself.\n+\n  - test_path_is_file <path>\n    test_path_is_dir <path>\n    test_path_is_missing <path>\n-- \ngitgitgadget\n\n"},{"id":"545432","messageId":"f3e8e19e6ea318e25ca05574a86003b7f241c995.1781323575.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v2.git.1781323575.gitgitgadget@gmail.com","subject":"[PATCH v2 2/6] t: fix grep assertions missing file arguments","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-13T04:06:11Z","receivedAt":"2026-06-13T04:06:23Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nThree grep assertions were missing their file arguments, causing\nthem to read from empty stdin instead of the intended file:\n\n- t2402: '! grep ...' should read from 'out', matching the\n  grep on the preceding line.\n- t7507: the closing quote is in the wrong place, making the\n  entire 'diff --git actual' a single pattern with no file\n  argument instead of pattern 'diff --git' and file 'actual'.\n- t7700: '! grep ...' should read from 'packlist', matching\n  the redirect on the preceding line.\n\nWithout file arguments these greps always succeed (empty stdin\nmatches nothing), so the assertions were not actually checking\nanything.  All three tests pass with the corrected file arguments,\nconfirming the intended behavior is sound.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/t2402-worktree-list.sh  | 2 +-\n t/t7507-commit-verbose.sh | 2 +-\n t/t7700-repack.sh         | 2 +-\n 3 files changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh\nindex e0c6abd2f5..93f92e854a 100755\n--- a/t/t2402-worktree-list.sh\n+++ b/t/t2402-worktree-list.sh\n@@ -144,7 +144,7 @@ test_expect_success '\"list\" all worktrees with prunable annotation' '\n \trm -rf prunable &&\n \tgit worktree list >out &&\n \tgrep \"/prunable  *[0-9a-f].* prunable$\" out &&\n-\t! grep \"/unprunable  *[0-9a-f].* prunable$\"\n+\t! grep \"/unprunable  *[0-9a-f].* prunable$\" out\n '\n \n test_expect_success '\"list\" all worktrees --porcelain with prunable' '\ndiff --git a/t/t7507-commit-verbose.sh b/t/t7507-commit-verbose.sh\nindex b53d71c086..acdb6b1455 100755\n--- a/t/t7507-commit-verbose.sh\n+++ b/t/t7507-commit-verbose.sh\n@@ -163,7 +163,7 @@ done\n \n test_expect_success \"status ignores commit.verbose=true\" '\n \tgit -c commit.verbose=true status >actual &&\n-\t! grep \"^diff --git actual\"\n+\t! grep \"^diff --git\" actual\n '\n \n test_done\ndiff --git a/t/t7700-repack.sh b/t/t7700-repack.sh\nindex 63ef63fc50..c6ff3aed30 100755\n--- a/t/t7700-repack.sh\n+++ b/t/t7700-repack.sh\n@@ -194,7 +194,7 @@ test_expect_success 'local packed unreachable obs that exist in alternate ODB ar\n \trm -f .git/objects/pack/* &&\n \tmv pack-* .git/objects/pack/ &&\n \tgit verify-pack -v -- .git/objects/pack/*.idx >packlist &&\n-\t! grep \"^$coid \" &&\n+\t! grep \"^$coid \" packlist &&\n \techo >.git/objects/info/alternates &&\n \ttest_must_fail git show $coid\n '\n-- \ngitgitgadget\n\n"},{"id":"545434","messageId":"460461b5fe8b5399c9d0b7a6dcb36ed897bc6c3e.1781323575.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v2.git.1781323575.gitgitgadget@gmail.com","subject":"[PATCH v2 3/6] t: extract chainlint's parser into shared module","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-13T04:06:12Z","receivedAt":"2026-06-13T04:06:26Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nMove chainlint.pl's Lexer, ShellParser, and ScriptParser into a\nshared module (lib-shell-parser.pl) so other lint tools can reuse\nthe same shell parsing infrastructure.  A subsequent commit adds\ngreplint.pl, which needs the same tokenizer to correctly identify\ncommand boundaries.\n\nScriptParser's check_test() becomes a no-op in the shared module.\nchainlint.pl defines ChainlintParser (extending ScriptParser)\nwith the &&-chain check_test() implementation.\n\nNo functional change: chainlint produces the same output and\ncheck-chainlint self-tests pass.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/chainlint.pl        | 529 +----------------------------------------\n t/lib-shell-parser.pl | 531 ++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 543 insertions(+), 517 deletions(-)\n create mode 100644 t/lib-shell-parser.pl\n\ndiff --git a/t/chainlint.pl b/t/chainlint.pl\nindex f0598e3934..cededf15ee 100755\n--- a/t/chainlint.pl\n+++ b/t/chainlint.pl\n@@ -23,458 +23,9 @@ my $jobs = -1;\n my $show_stats;\n my $emit_all;\n \n-# Lexer tokenizes POSIX shell scripts. It is roughly modeled after section 2.3\n-# \"Token Recognition\" of POSIX chapter 2 \"Shell Command Language\". Although\n-# similar to lexical analyzers for other languages, this one differs in a few\n-# substantial ways due to quirks of the shell command language.\n-#\n-# For instance, in many languages, newline is just whitespace like space or\n-# TAB, but in shell a newline is a command separator, thus a distinct lexical\n-# token. A newline is significant and returned as a distinct token even at the\n-# end of a shell comment.\n-#\n-# In other languages, `1+2` would typically be scanned as three tokens\n-# (`1`, `+`, and `2`), but in shell it is a single token. However, the similar\n-# `1 + 2`, which embeds whitepace, is scanned as three token in shell, as well.\n-# In shell, several characters with special meaning lose that meaning when not\n-# surrounded by whitespace. For instance, the negation operator `!` is special\n-# when standing alone surrounded by whitespace; whereas in `foo!uucp` it is\n-# just a plain character in the longer token \"foo!uucp\". In many other\n-# languages, `\"string\"/foo:'string'` might be scanned as five tokens (\"string\",\n-# `/`, `foo`, `:`, and 'string'), but in shell, it is just a single token.\n-#\n-# The lexical analyzer for the shell command language is also somewhat unusual\n-# in that it recursively invokes the parser to handle the body of `$(...)`\n-# expressions which can contain arbitrary shell code. Such expressions may be\n-# encountered both inside and outside of double-quoted strings.\n-#\n-# The lexical analyzer is responsible for consuming shell here-doc bodies which\n-# extend from the line following a `<<TAG` operator until a line consisting\n-# solely of `TAG`. Here-doc consumption begins when a newline is encountered.\n-# It is legal for multiple here-doc `<<TAG` operators to be present on a single\n-# line, in which case their bodies must be present one following the next, and\n-# are consumed in the (left-to-right) order the `<<TAG` operators appear on the\n-# line. A special complication is that the bodies of all here-docs must be\n-# consumed when the newline is encountered even if the parse context depth has\n-# changed. For instance, in `cat <<A && x=$(cat <<B &&\\n`, bodies of here-docs\n-# \"A\" and \"B\" must be consumed even though \"A\" was introduced outside the\n-# recursive parse context in which \"B\" was introduced and in which the newline\n-# is encountered.\n-package Lexer;\n-\n-sub new {\n-\tmy ($class, $parser, $s) = @_;\n-\tbless {\n-\t\tparser => $parser,\n-\t\tbuff => $s,\n-\t\tlineno => 1,\n-\t\theretags => []\n-\t} => $class;\n-}\n-\n-sub scan_heredoc_tag {\n-\tmy $self = shift @_;\n-\t${$self->{buff}} =~ /\\G(-?)/gc;\n-\tmy $indented = $1;\n-\tmy $token = $self->scan_token();\n-\treturn \"<<$indented\" unless $token;\n-\tmy $tag = $token->[0];\n-\t$tag =~ s/['\"\\\\]//g;\n-\t$$token[0] = $indented ? \"\\t$tag\" : \"$tag\";\n-\tpush(@{$self->{heretags}}, $token);\n-\treturn \"<<$indented$tag\";\n-}\n-\n-sub scan_op {\n-\tmy ($self, $c) = @_;\n-\tmy $b = $self->{buff};\n-\treturn $c unless $$b =~ /\\G(.)/sgc;\n-\tmy $cc = $c . $1;\n-\treturn scan_heredoc_tag($self) if $cc eq '<<';\n-\treturn $cc if $cc =~ /^(?:&&|\\|\\||>>|;;|<&|>&|<>|>\\|)$/;\n-\tpos($$b)--;\n-\treturn $c;\n-}\n-\n-sub scan_sqstring {\n-\tmy $self = shift @_;\n-\t${$self->{buff}} =~ /\\G([^']*'|.*\\z)/sgc;\n-\tmy $s = $1;\n-\t$self->{lineno} += () = $s =~ /\\n/sg;\n-\treturn \"'\" . $s;\n-}\n-\n-sub scan_dqstring {\n-\tmy $self = shift @_;\n-\tmy $b = $self->{buff};\n-\tmy $s = '\"';\n-\twhile (1) {\n-\t\t# slurp up non-special characters\n-\t\t$s .= $1 if $$b =~ /\\G([^\"\\$\\\\]+)/gc;\n-\t\t# handle special characters\n-\t\tlast unless $$b =~ /\\G(.)/sgc;\n-\t\tmy $c = $1;\n-\t\t$s .= '\"', last if $c eq '\"';\n-\t\t$s .= '$' . $self->scan_dollar(), next if $c eq '$';\n-\t\tif ($c eq '\\\\') {\n-\t\t\t$s .= '\\\\', last unless $$b =~ /\\G(.)/sgc;\n-\t\t\t$c = $1;\n-\t\t\t$self->{lineno}++, next if $c eq \"\\n\"; # line splice\n-\t\t\t# backslash escapes only $, `, \", \\ in dq-string\n-\t\t\t$s .= '\\\\' unless $c =~ /^[\\$`\"\\\\]$/;\n-\t\t\t$s .= $c;\n-\t\t\tnext;\n-\t\t}\n-\t\tdie(\"internal error scanning dq-string '$c'\\n\");\n-\t}\n-\t$self->{lineno} += () = $s =~ /\\n/sg;\n-\treturn $s;\n-}\n-\n-sub scan_balanced {\n-\tmy ($self, $c1, $c2) = @_;\n-\tmy $b = $self->{buff};\n-\tmy $depth = 1;\n-\tmy $s = $c1;\n-\twhile ($$b =~ /\\G([^\\Q$c1$c2\\E]*(?:[\\Q$c1$c2\\E]|\\z))/gc) {\n-\t\t$s .= $1;\n-\t\t$depth++, next if $s =~ /\\Q$c1\\E$/;\n-\t\t$depth--;\n-\t\tlast if $depth == 0;\n-\t}\n-\t$self->{lineno} += () = $s =~ /\\n/sg;\n-\treturn $s;\n-}\n-\n-sub scan_subst {\n-\tmy $self = shift @_;\n-\tmy @tokens = $self->{parser}->parse(qr/^\\)$/);\n-\t$self->{parser}->next_token(); # closing \")\"\n-\treturn @tokens;\n-}\n-\n-sub scan_dollar {\n-\tmy $self = shift @_;\n-\tmy $b = $self->{buff};\n-\treturn $self->scan_balanced('(', ')') if $$b =~ /\\G\\((?=\\()/gc; # $((...))\n-\treturn '(' . join(' ', map {$_->[0]} $self->scan_subst()) . ')' if $$b =~ /\\G\\(/gc; # $(...)\n-\treturn $self->scan_balanced('{', '}') if $$b =~ /\\G\\{/gc; # ${...}\n-\treturn $1 if $$b =~ /\\G(\\w+)/gc; # $var\n-\treturn $1 if $$b =~ /\\G([@*#?$!0-9-])/gc; # $*, $1, $$, etc.\n-\treturn '';\n-}\n-\n-sub swallow_heredocs {\n-\tmy $self = shift @_;\n-\tmy $b = $self->{buff};\n-\tmy $tags = $self->{heretags};\n-\twhile (my $tag = shift @$tags) {\n-\t\tmy $start = pos($$b);\n-\t\tmy $indent = $$tag[0] =~ s/^\\t// ? '\\\\s*' : '';\n-\t\t$$b =~ /(?:\\G|\\n)$indent\\Q$$tag[0]\\E(?:\\n|\\z)/gc;\n-\t\tif (pos($$b) > $start) {\n-\t\t\tmy $body = substr($$b, $start, pos($$b) - $start);\n-\t\t\t$self->{parser}->{heredocs}->{$$tag[0]} = {\n-\t\t\t\tcontent => substr($body, 0, length($body) - length($&)),\n-\t\t\t\tstart_line => $self->{lineno},\n-\t\t        };\n-\t\t\t$self->{lineno} += () = $body =~ /\\n/sg;\n-\t\t\tnext;\n-\t\t}\n-\t\tpush(@{$self->{parser}->{problems}}, ['HEREDOC', $tag]);\n-\t\t$$b =~ /(?:\\G|\\n).*\\z/gc; # consume rest of input\n-\t\tmy $body = substr($$b, $start, pos($$b) - $start);\n-\t\t$self->{lineno} += () = $body =~ /\\n/sg;\n-\t\tlast;\n-\t}\n-}\n-\n-sub scan_token {\n-\tmy $self = shift @_;\n-\tmy $b = $self->{buff};\n-\tmy $token = '';\n-\tmy ($start, $startln);\n-RESTART:\n-\t$startln = $self->{lineno};\n-\t$$b =~ /\\G[ \\t]+/gc; # skip whitespace (but not newline)\n-\t$start = pos($$b) || 0;\n-\t$self->{lineno}++, return [\"\\n\", $start, pos($$b), $startln, $startln] if $$b =~ /\\G#[^\\n]*(?:\\n|\\z)/gc; # comment\n-\twhile (1) {\n-\t\t# slurp up non-special characters\n-\t\t$token .= $1 if $$b =~ /\\G([^\\\\;&|<>(){}'\"\\$\\s]+)/gc;\n-\t\t# handle special characters\n-\t\tlast unless $$b =~ /\\G(.)/sgc;\n-\t\tmy $c = $1;\n-\t\tpos($$b)--, last if $c =~ /^[ \\t]$/; # whitespace ends token\n-\t\tpos($$b)--, last if length($token) && $c =~ /^[;&|<>(){}\\n]$/;\n-\t\t$token .= $self->scan_sqstring(), next if $c eq \"'\";\n-\t\t$token .= $self->scan_dqstring(), next if $c eq '\"';\n-\t\t$token .= $c . $self->scan_dollar(), next if $c eq '$';\n-\t\t$self->{lineno}++, $self->swallow_heredocs(), $token = $c, last if $c eq \"\\n\";\n-\t\t$token = $self->scan_op($c), last if $c =~ /^[;&|<>]$/;\n-\t\t$token = $c, last if $c =~ /^[(){}]$/;\n-\t\tif ($c eq '\\\\') {\n-\t\t\t$token .= '\\\\', last unless $$b =~ /\\G(.)/sgc;\n-\t\t\t$c = $1;\n-\t\t\t$self->{lineno}++, next if $c eq \"\\n\" && length($token); # line splice\n-\t\t\t$self->{lineno}++, goto RESTART if $c eq \"\\n\"; # line splice\n-\t\t\t$token .= '\\\\' . $c;\n-\t\t\tnext;\n-\t\t}\n-\t\tdie(\"internal error scanning character '$c'\\n\");\n-\t}\n-\treturn length($token) ? [$token, $start, pos($$b), $startln, $self->{lineno}] : undef;\n-}\n-\n-# ShellParser parses POSIX shell scripts (with minor extensions for Bash). It\n-# is a recursive descent parser very roughly modeled after section 2.10 \"Shell\n-# Grammar\" of POSIX chapter 2 \"Shell Command Language\".\n-package ShellParser;\n-\n-sub new {\n-\tmy ($class, $s) = @_;\n-\tmy $self = bless {\n-\t\tbuff => [],\n-\t\tstop => [],\n-\t\toutput => [],\n-\t\theredocs => {},\n-\t\tinsubshell => 0,\n-\t} => $class;\n-\t$self->{lexer} = Lexer->new($self, $s);\n-\treturn $self;\n-}\n-\n-sub next_token {\n-\tmy $self = shift @_;\n-\treturn pop(@{$self->{buff}}) if @{$self->{buff}};\n-\treturn $self->{lexer}->scan_token();\n-}\n-\n-sub untoken {\n-\tmy $self = shift @_;\n-\tpush(@{$self->{buff}}, @_);\n-}\n-\n-sub peek {\n-\tmy $self = shift @_;\n-\tmy $token = $self->next_token();\n-\treturn undef unless defined($token);\n-\t$self->untoken($token);\n-\treturn $token;\n-}\n-\n-sub stop_at {\n-\tmy ($self, $token) = @_;\n-\treturn 1 unless defined($token);\n-\tmy $stop = ${$self->{stop}}[-1] if @{$self->{stop}};\n-\treturn defined($stop) && $token->[0] =~ $stop;\n-}\n-\n-sub expect {\n-\tmy ($self, $expect) = @_;\n-\tmy $token = $self->next_token();\n-\treturn $token if defined($token) && $token->[0] eq $expect;\n-\tpush(@{$self->{output}}, \"?!ERR?! expected '$expect' but found '\" . (defined($token) ? $token->[0] : \"<end-of-input>\") . \"'\\n\");\n-\t$self->untoken($token) if defined($token);\n-\treturn ();\n-}\n-\n-sub optional_newlines {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\twhile (my $token = $self->peek()) {\n-\t\tlast unless $token->[0] eq \"\\n\";\n-\t\tpush(@tokens, $self->next_token());\n-\t}\n-\treturn @tokens;\n-}\n-\n-sub parse_group {\n-\tmy $self = shift @_;\n-\treturn ($self->parse(qr/^}$/),\n-\t\t$self->expect('}'));\n-}\n-\n-sub parse_subshell {\n-\tmy $self = shift @_;\n-\t$self->{insubshell}++;\n-\tmy @tokens = ($self->parse(qr/^\\)$/),\n-\t\t      $self->expect(')'));\n-\t$self->{insubshell}--;\n-\treturn @tokens;\n-}\n-\n-sub parse_case_pattern {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\twhile (defined(my $token = $self->next_token())) {\n-\t\tpush(@tokens, $token);\n-\t\tlast if $token->[0] eq ')';\n-\t}\n-\treturn @tokens;\n-}\n-\n-sub parse_case {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\tpush(@tokens,\n-\t     $self->next_token(), # subject\n-\t     $self->optional_newlines(),\n-\t     $self->expect('in'),\n-\t     $self->optional_newlines());\n-\twhile (1) {\n-\t\tmy $token = $self->peek();\n-\t\tlast unless defined($token) && $token->[0] ne 'esac';\n-\t\tpush(@tokens,\n-\t\t     $self->parse_case_pattern(),\n-\t\t     $self->optional_newlines(),\n-\t\t     $self->parse(qr/^(?:;;|esac)$/)); # item body\n-\t\t$token = $self->peek();\n-\t\tlast unless defined($token) && $token->[0] ne 'esac';\n-\t\tpush(@tokens,\n-\t\t     $self->expect(';;'),\n-\t\t     $self->optional_newlines());\n-\t}\n-\tpush(@tokens, $self->expect('esac'));\n-\treturn @tokens;\n-}\n-\n-sub parse_for {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\tpush(@tokens,\n-\t     $self->next_token(), # variable\n-\t     $self->optional_newlines());\n-\tmy $token = $self->peek();\n-\tif (defined($token) && $token->[0] eq 'in') {\n-\t\tpush(@tokens,\n-\t\t     $self->expect('in'),\n-\t\t     $self->optional_newlines());\n-\t}\n-\tpush(@tokens,\n-\t     $self->parse(qr/^do$/), # items\n-\t     $self->expect('do'),\n-\t     $self->optional_newlines(),\n-\t     $self->parse_loop_body(),\n-\t     $self->expect('done'));\n-\treturn @tokens;\n-}\n-\n-sub parse_if {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\twhile (1) {\n-\t\tpush(@tokens,\n-\t\t     $self->parse(qr/^then$/), # if/elif condition\n-\t\t     $self->expect('then'),\n-\t\t     $self->optional_newlines(),\n-\t\t     $self->parse(qr/^(?:elif|else|fi)$/)); # if/elif body\n-\t\tmy $token = $self->peek();\n-\t\tlast unless defined($token) && $token->[0] eq 'elif';\n-\t\tpush(@tokens, $self->expect('elif'));\n-\t}\n-\tmy $token = $self->peek();\n-\tif (defined($token) && $token->[0] eq 'else') {\n-\t\tpush(@tokens,\n-\t\t     $self->expect('else'),\n-\t\t     $self->optional_newlines(),\n-\t\t     $self->parse(qr/^fi$/)); # else body\n-\t}\n-\tpush(@tokens, $self->expect('fi'));\n-\treturn @tokens;\n-}\n-\n-sub parse_loop_body {\n-\tmy $self = shift @_;\n-\treturn $self->parse(qr/^done$/);\n-}\n-\n-sub parse_loop {\n-\tmy $self = shift @_;\n-\treturn ($self->parse(qr/^do$/), # condition\n-\t\t$self->expect('do'),\n-\t\t$self->optional_newlines(),\n-\t\t$self->parse_loop_body(),\n-\t\t$self->expect('done'));\n-}\n-\n-sub parse_func {\n-\tmy $self = shift @_;\n-\treturn ($self->expect('('),\n-\t\t$self->expect(')'),\n-\t\t$self->optional_newlines(),\n-\t\t$self->parse_cmd()); # body\n-}\n-\n-sub parse_bash_array_assignment {\n-\tmy $self = shift @_;\n-\tmy @tokens = $self->expect('(');\n-\twhile (defined(my $token = $self->next_token())) {\n-\t\tpush(@tokens, $token);\n-\t\tlast if $token->[0] eq ')';\n-\t}\n-\treturn @tokens;\n-}\n-\n-my %compound = (\n-\t'{' => \\&parse_group,\n-\t'(' => \\&parse_subshell,\n-\t'case' => \\&parse_case,\n-\t'for' => \\&parse_for,\n-\t'if' => \\&parse_if,\n-\t'until' => \\&parse_loop,\n-\t'while' => \\&parse_loop);\n-\n-sub parse_cmd {\n-\tmy $self = shift @_;\n-\tmy $cmd = $self->next_token();\n-\treturn () unless defined($cmd);\n-\treturn $cmd if $cmd->[0] eq \"\\n\";\n-\n-\tmy $token;\n-\tmy @tokens = $cmd;\n-\tif ($cmd->[0] eq '!') {\n-\t\tpush(@tokens, $self->parse_cmd());\n-\t\treturn @tokens;\n-\t} elsif (my $f = $compound{$cmd->[0]}) {\n-\t\tpush(@tokens, $self->$f());\n-\t} elsif (defined($token = $self->peek()) && $token->[0] eq '(') {\n-\t\tif ($cmd->[0] !~ /\\w=$/) {\n-\t\t\tpush(@tokens, $self->parse_func());\n-\t\t\treturn @tokens;\n-\t\t}\n-\t\tmy @array = $self->parse_bash_array_assignment();\n-\t\t$tokens[-1]->[0] .= join(' ', map {$_->[0]} @array);\n-\t\t$tokens[-1]->[2] = $array[$#array][2] if @array;\n-\t}\n-\n-\twhile (defined(my $token = $self->next_token())) {\n-\t\t$self->untoken($token), last if $self->stop_at($token);\n-\t\tpush(@tokens, $token);\n-\t\tlast if $token->[0] =~ /^(?:[;&\\n|]|&&|\\|\\|)$/;\n-\t}\n-\tpush(@tokens, $self->next_token()) if $tokens[-1]->[0] ne \"\\n\" && defined($token = $self->peek()) && $token->[0] eq \"\\n\";\n-\treturn @tokens;\n-}\n-\n-sub accumulate {\n-\tmy ($self, $tokens, $cmd) = @_;\n-\tpush(@$tokens, @$cmd);\n-}\n-\n-sub parse {\n-\tmy ($self, $stop) = @_;\n-\tpush(@{$self->{stop}}, $stop);\n-\tgoto DONE if $self->stop_at($self->peek());\n-\tmy @tokens;\n-\twhile (my @cmd = $self->parse_cmd()) {\n-\t\t$self->accumulate(\\@tokens, \\@cmd);\n-\t\tlast if $self->stop_at($self->peek());\n-\t}\n-DONE:\n-\tpop(@{$self->{stop}});\n-\treturn @tokens;\n-}\n+use File::Basename;\n+do(dirname($0) . \"/lib-shell-parser.pl\")\n+\tor die \"$0: failed to load lib-shell-parser.pl: $@$!\\n\";\n \n # TestParser is a subclass of ShellParser which, beyond parsing shell script\n # code, is also imbued with semantic knowledge of test construction, and checks\n@@ -482,9 +33,10 @@ DONE:\n # the tests themselves or in behaviors being exercised by the tests. As such,\n # TestParser is only called upon to parse test bodies, not the top-level\n # scripts in which the tests are defined.\n+\n package TestParser;\n \n-use base 'ShellParser';\n+our @ISA = ('ShellParser');\n \n sub new {\n \tmy $class = shift @_;\n@@ -578,51 +130,10 @@ DONE:\n \t$self->SUPER::accumulate($tokens, $cmd);\n }\n \n-# ScriptParser is a subclass of ShellParser which identifies individual test\n-# definitions within test scripts, and passes each test body through TestParser\n-# to identify possible problems. ShellParser detects test definitions not only\n-# at the top-level of test scripts but also within compound commands such as\n-# loops and function definitions.\n-package ScriptParser;\n-\n-use base 'ShellParser';\n-\n-sub new {\n-\tmy $class = shift @_;\n-\tmy $self = $class->SUPER::new(@_);\n-\t$self->{ntests} = 0;\n-\t$self->{nerrs} = 0;\n-\treturn $self;\n-}\n+# ChainlintParser extends ScriptParser with &&-chain checking\n+package ChainlintParser;\n \n-# extract the raw content of a token, which may be a single string or a\n-# composition of multiple strings and non-string character runs; for instance,\n-# `\"test body\"` unwraps to `test body`; `word\"a b\"42'c d'` to `worda b42c d`\n-sub unwrap {\n-\tmy $token = (@_ ? shift @_ : $_)->[0];\n-\t# simple case: 'sqstring' or \"dqstring\"\n-\treturn $token if $token =~ s/^'([^']*)'$/$1/;\n-\treturn $token if $token =~ s/^\"([^\"]*)\"$/$1/;\n-\n-\t# composite case\n-\tmy ($s, $q, $escaped);\n-\twhile (1) {\n-\t\t# slurp up non-special characters\n-\t\t$s .= $1 if $token =~ /\\G([^\\\\'\"]*)/gc;\n-\t\t# handle special characters\n-\t\tlast unless $token =~ /\\G(.)/sgc;\n-\t\tmy $c = $1;\n-\t\t$q = undef, next if defined($q) && $c eq $q;\n-\t\t$q = $c, next if !defined($q) && $c =~ /^['\"]$/;\n-\t\tif ($c eq '\\\\') {\n-\t\t\tlast unless $token =~ /\\G(.)/sgc;\n-\t\t\t$c = $1;\n-\t\t\t$s .= '\\\\' if $c eq \"\\n\"; # preserve line splice\n-\t\t}\n-\t\t$s .= $c;\n-\t}\n-\treturn $s\n-}\n+our @ISA = ('ScriptParser');\n \n sub format_problem {\n \tlocal $_ = shift;\n@@ -635,10 +146,10 @@ sub format_problem {\n \n sub check_test {\n \tmy $self = shift @_;\n-\tmy $title = unwrap(shift @_);\n+\tmy $title = ScriptParser::unwrap(shift @_);\n \tmy $body = shift @_;\n \tmy $lineno = $body->[3];\n-\t$body = unwrap($body);\n+\t$body = ScriptParser::unwrap($body);\n \tif ($body eq '-') {\n \t\tmy $herebody = shift @_;\n \t\t$body = $herebody->{content};\n@@ -673,24 +184,8 @@ sub check_test {\n \tpush(@{$self->{output}}, \"$c->{blue}# chainlint: $title$c->{reset}\\n$checked\");\n }\n \n-sub parse_cmd {\n-\tmy $self = shift @_;\n-\tmy @tokens = $self->SUPER::parse_cmd();\n-\treturn @tokens unless @tokens && $tokens[0]->[0] =~ /^test_expect_(?:success|failure)$/;\n-\tmy $n = $#tokens;\n-\t$n-- while $n >= 0 && $tokens[$n]->[0] =~ /^(?:[;&\\n|]|&&|\\|\\|)$/;\n-\tmy $herebody;\n-\tif ($n >= 2 && $tokens[$n-1]->[0] eq '-' && $tokens[$n]->[0] =~ /^<<-?(.+)$/) {\n-\t\t$herebody = $self->{heredocs}->{$1};\n-\t\t$n--;\n-\t}\n-\t$self->check_test($tokens[1], $tokens[2], $herebody) if $n == 2; # title body\n-\t$self->check_test($tokens[2], $tokens[3], $herebody) if $n > 2;  # prereq title body\n-\treturn @tokens;\n-}\n-\n # main contains high-level functionality for processing command-line switches,\n-# feeding input test scripts to ScriptParser, and reporting results.\n+# feeding input test scripts to ChainlintParser, and reporting results.\n package main;\n \n my $getnow = sub { return time(); };\n@@ -803,7 +298,7 @@ sub check_script {\n \t\t}\n \t\tmy $s = do { local $/; <$fh> };\n \t\tclose($fh);\n-\t\tmy $parser = ScriptParser->new(\\$s);\n+\t\tmy $parser = ChainlintParser->new(\\$s);\n \t\t1 while $parser->parse_cmd();\n \t\tif (@{$parser->{output}}) {\n \t\t\tmy $c = fd_colors(1);\ndiff --git a/t/lib-shell-parser.pl b/t/lib-shell-parser.pl\nnew file mode 100644\nindex 0000000000..edbfee0839\n--- /dev/null\n+++ b/t/lib-shell-parser.pl\n@@ -0,0 +1,531 @@\n+# Copyright (c) 2021-2022 Eric Sunshine <sunshine@sunshineco.com>\n+#\n+# Shared shell script parser for test lint tools. Provides Lexer,\n+# ShellParser, and ScriptParser. Subclass ScriptParser and override\n+# check_test() to implement lint checks.\n+\n+use strict;\n+use warnings;\n+\n+# Lexer tokenizes POSIX shell scripts. It is roughly modeled after section 2.3\n+# \"Token Recognition\" of POSIX chapter 2 \"Shell Command Language\". Although\n+# similar to lexical analyzers for other languages, this one differs in a few\n+# substantial ways due to quirks of the shell command language.\n+#\n+# For instance, in many languages, newline is just whitespace like space or\n+# TAB, but in shell a newline is a command separator, thus a distinct lexical\n+# token. A newline is significant and returned as a distinct token even at the\n+# end of a shell comment.\n+#\n+# In other languages, `1+2` would typically be scanned as three tokens\n+# (`1`, `+`, and `2`), but in shell it is a single token. However, the similar\n+# `1 + 2`, which embeds whitepace, is scanned as three token in shell, as well.\n+# In shell, several characters with special meaning lose that meaning when not\n+# surrounded by whitespace. For instance, the negation operator `!` is special\n+# when standing alone surrounded by whitespace; whereas in `foo!uucp` it is\n+# just a plain character in the longer token \"foo!uucp\". In many other\n+# languages, `\"string\"/foo:'string'` might be scanned as five tokens (\"string\",\n+# `/`, `foo`, `:`, and 'string'), but in shell, it is just a single token.\n+#\n+# The lexical analyzer for the shell command language is also somewhat unusual\n+# in that it recursively invokes the parser to handle the body of `$(...)`\n+# expressions which can contain arbitrary shell code. Such expressions may be\n+# encountered both inside and outside of double-quoted strings.\n+#\n+# The lexical analyzer is responsible for consuming shell here-doc bodies which\n+# extend from the line following a `<<TAG` operator until a line consisting\n+# solely of `TAG`. Here-doc consumption begins when a newline is encountered.\n+# It is legal for multiple here-doc `<<TAG` operators to be present on a single\n+# line, in which case their bodies must be present one following the next, and\n+# are consumed in the (left-to-right) order the `<<TAG` operators appear on the\n+# line. A special complication is that the bodies of all here-docs must be\n+# consumed when the newline is encountered even if the parse context depth has\n+# changed. For instance, in `cat <<A && x=$(cat <<B &&\\n`, bodies of here-docs\n+# \"A\" and \"B\" must be consumed even though \"A\" was introduced outside the\n+# recursive parse context in which \"B\" was introduced and in which the newline\n+# is encountered.\n+package Lexer;\n+\n+sub new {\n+\tmy ($class, $parser, $s) = @_;\n+\tbless {\n+\t\tparser => $parser,\n+\t\tbuff => $s,\n+\t\tlineno => 1,\n+\t\theretags => []\n+\t} => $class;\n+}\n+\n+sub scan_heredoc_tag {\n+\tmy $self = shift @_;\n+\t${$self->{buff}} =~ /\\G(-?)/gc;\n+\tmy $indented = $1;\n+\tmy $token = $self->scan_token();\n+\treturn \"<<$indented\" unless $token;\n+\tmy $tag = $token->[0];\n+\t$tag =~ s/['\"\\\\]//g;\n+\t$$token[0] = $indented ? \"\\t$tag\" : \"$tag\";\n+\tpush(@{$self->{heretags}}, $token);\n+\treturn \"<<$indented$tag\";\n+}\n+\n+sub scan_op {\n+\tmy ($self, $c) = @_;\n+\tmy $b = $self->{buff};\n+\treturn $c unless $$b =~ /\\G(.)/sgc;\n+\tmy $cc = $c . $1;\n+\treturn scan_heredoc_tag($self) if $cc eq '<<';\n+\treturn $cc if $cc =~ /^(?:&&|\\|\\||>>|;;|<&|>&|<>|>\\|)$/;\n+\tpos($$b)--;\n+\treturn $c;\n+}\n+\n+sub scan_sqstring {\n+\tmy $self = shift @_;\n+\t${$self->{buff}} =~ /\\G([^']*'|.*\\z)/sgc;\n+\tmy $s = $1;\n+\t$self->{lineno} += () = $s =~ /\\n/sg;\n+\treturn \"'\" . $s;\n+}\n+\n+sub scan_dqstring {\n+\tmy $self = shift @_;\n+\tmy $b = $self->{buff};\n+\tmy $s = '\"';\n+\twhile (1) {\n+\t\t# slurp up non-special characters\n+\t\t$s .= $1 if $$b =~ /\\G([^\"\\$\\\\]+)/gc;\n+\t\t# handle special characters\n+\t\tlast unless $$b =~ /\\G(.)/sgc;\n+\t\tmy $c = $1;\n+\t\t$s .= '\"', last if $c eq '\"';\n+\t\t$s .= '$' . $self->scan_dollar(), next if $c eq '$';\n+\t\tif ($c eq '\\\\') {\n+\t\t\t$s .= '\\\\', last unless $$b =~ /\\G(.)/sgc;\n+\t\t\t$c = $1;\n+\t\t\t$self->{lineno}++, next if $c eq \"\\n\"; # line splice\n+\t\t\t# backslash escapes only $, `, \", \\ in dq-string\n+\t\t\t$s .= '\\\\' unless $c =~ /^[\\$`\"\\\\]$/;\n+\t\t\t$s .= $c;\n+\t\t\tnext;\n+\t\t}\n+\t\tdie(\"internal error scanning dq-string '$c'\\n\");\n+\t}\n+\t$self->{lineno} += () = $s =~ /\\n/sg;\n+\treturn $s;\n+}\n+\n+sub scan_balanced {\n+\tmy ($self, $c1, $c2) = @_;\n+\tmy $b = $self->{buff};\n+\tmy $depth = 1;\n+\tmy $s = $c1;\n+\twhile ($$b =~ /\\G([^\\Q$c1$c2\\E]*(?:[\\Q$c1$c2\\E]|\\z))/gc) {\n+\t\t$s .= $1;\n+\t\t$depth++, next if $s =~ /\\Q$c1\\E$/;\n+\t\t$depth--;\n+\t\tlast if $depth == 0;\n+\t}\n+\t$self->{lineno} += () = $s =~ /\\n/sg;\n+\treturn $s;\n+}\n+\n+sub scan_subst {\n+\tmy $self = shift @_;\n+\tmy @tokens = $self->{parser}->parse(qr/^\\)$/);\n+\t$self->{parser}->next_token(); # closing \")\"\n+\treturn @tokens;\n+}\n+\n+sub scan_dollar {\n+\tmy $self = shift @_;\n+\tmy $b = $self->{buff};\n+\treturn $self->scan_balanced('(', ')') if $$b =~ /\\G\\((?=\\()/gc; # $((...))\n+\treturn '(' . join(' ', map {$_->[0]} $self->scan_subst()) . ')' if $$b =~ /\\G\\(/gc; # $(...)\n+\treturn $self->scan_balanced('{', '}') if $$b =~ /\\G\\{/gc; # ${...}\n+\treturn $1 if $$b =~ /\\G(\\w+)/gc; # $var\n+\treturn $1 if $$b =~ /\\G([@*#?$!0-9-])/gc; # $*, $1, $$, etc.\n+\treturn '';\n+}\n+\n+sub swallow_heredocs {\n+\tmy $self = shift @_;\n+\tmy $b = $self->{buff};\n+\tmy $tags = $self->{heretags};\n+\twhile (my $tag = shift @$tags) {\n+\t\tmy $start = pos($$b);\n+\t\tmy $indent = $$tag[0] =~ s/^\\t// ? '\\\\s*' : '';\n+\t\t$$b =~ /(?:\\G|\\n)$indent\\Q$$tag[0]\\E(?:\\n|\\z)/gc;\n+\t\tif (pos($$b) > $start) {\n+\t\t\tmy $body = substr($$b, $start, pos($$b) - $start);\n+\t\t\t$self->{parser}->{heredocs}->{$$tag[0]} = {\n+\t\t\t\tcontent => substr($body, 0, length($body) - length($&)),\n+\t\t\t\tstart_line => $self->{lineno},\n+\t\t        };\n+\t\t\t$self->{lineno} += () = $body =~ /\\n/sg;\n+\t\t\tnext;\n+\t\t}\n+\t\tpush(@{$self->{parser}->{problems}}, ['HEREDOC', $tag]);\n+\t\t$$b =~ /(?:\\G|\\n).*\\z/gc; # consume rest of input\n+\t\tmy $body = substr($$b, $start, pos($$b) - $start);\n+\t\t$self->{lineno} += () = $body =~ /\\n/sg;\n+\t\tlast;\n+\t}\n+}\n+\n+sub scan_token {\n+\tmy $self = shift @_;\n+\tmy $b = $self->{buff};\n+\tmy $token = '';\n+\tmy ($start, $startln);\n+RESTART:\n+\t$startln = $self->{lineno};\n+\t$$b =~ /\\G[ \\t]+/gc; # skip whitespace (but not newline)\n+\t$start = pos($$b) || 0;\n+\t$self->{lineno}++, return [\"\\n\", $start, pos($$b), $startln, $startln] if $$b =~ /\\G#[^\\n]*(?:\\n|\\z)/gc; # comment\n+\twhile (1) {\n+\t\t# slurp up non-special characters\n+\t\t$token .= $1 if $$b =~ /\\G([^\\\\;&|<>(){}'\"\\$\\s]+)/gc;\n+\t\t# handle special characters\n+\t\tlast unless $$b =~ /\\G(.)/sgc;\n+\t\tmy $c = $1;\n+\t\tpos($$b)--, last if $c =~ /^[ \\t]$/; # whitespace ends token\n+\t\tpos($$b)--, last if length($token) && $c =~ /^[;&|<>(){}\\n]$/;\n+\t\t$token .= $self->scan_sqstring(), next if $c eq \"'\";\n+\t\t$token .= $self->scan_dqstring(), next if $c eq '\"';\n+\t\t$token .= $c . $self->scan_dollar(), next if $c eq '$';\n+\t\t$self->{lineno}++, $self->swallow_heredocs(), $token = $c, last if $c eq \"\\n\";\n+\t\t$token = $self->scan_op($c), last if $c =~ /^[;&|<>]$/;\n+\t\t$token = $c, last if $c =~ /^[(){}]$/;\n+\t\tif ($c eq '\\\\') {\n+\t\t\t$token .= '\\\\', last unless $$b =~ /\\G(.)/sgc;\n+\t\t\t$c = $1;\n+\t\t\t$self->{lineno}++, next if $c eq \"\\n\" && length($token); # line splice\n+\t\t\t$self->{lineno}++, goto RESTART if $c eq \"\\n\"; # line splice\n+\t\t\t$token .= '\\\\' . $c;\n+\t\t\tnext;\n+\t\t}\n+\t\tdie(\"internal error scanning character '$c'\\n\");\n+\t}\n+\treturn length($token) ? [$token, $start, pos($$b), $startln, $self->{lineno}] : undef;\n+}\n+\n+# ShellParser parses POSIX shell scripts (with minor extensions for Bash). It\n+# is a recursive descent parser very roughly modeled after section 2.10 \"Shell\n+# Grammar\" of POSIX chapter 2 \"Shell Command Language\".\n+\n+package ShellParser;\n+\n+sub new {\n+\tmy ($class, $s) = @_;\n+\tmy $self = bless {\n+\t\tbuff => [],\n+\t\tstop => [],\n+\t\toutput => [],\n+\t\theredocs => {},\n+\t\tinsubshell => 0,\n+\t} => $class;\n+\t$self->{lexer} = Lexer->new($self, $s);\n+\treturn $self;\n+}\n+\n+sub next_token {\n+\tmy $self = shift @_;\n+\treturn pop(@{$self->{buff}}) if @{$self->{buff}};\n+\treturn $self->{lexer}->scan_token();\n+}\n+\n+sub untoken {\n+\tmy $self = shift @_;\n+\tpush(@{$self->{buff}}, @_);\n+}\n+\n+sub peek {\n+\tmy $self = shift @_;\n+\tmy $token = $self->next_token();\n+\treturn undef unless defined($token);\n+\t$self->untoken($token);\n+\treturn $token;\n+}\n+\n+sub stop_at {\n+\tmy ($self, $token) = @_;\n+\treturn 1 unless defined($token);\n+\tmy $stop = ${$self->{stop}}[-1] if @{$self->{stop}};\n+\treturn defined($stop) && $token->[0] =~ $stop;\n+}\n+\n+sub expect {\n+\tmy ($self, $expect) = @_;\n+\tmy $token = $self->next_token();\n+\treturn $token if defined($token) && $token->[0] eq $expect;\n+\tpush(@{$self->{output}}, \"?!ERR?! expected '$expect' but found '\" . (defined($token) ? $token->[0] : \"<end-of-input>\") . \"'\\n\");\n+\t$self->untoken($token) if defined($token);\n+\treturn ();\n+}\n+\n+sub optional_newlines {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\twhile (my $token = $self->peek()) {\n+\t\tlast unless $token->[0] eq \"\\n\";\n+\t\tpush(@tokens, $self->next_token());\n+\t}\n+\treturn @tokens;\n+}\n+\n+sub parse_group {\n+\tmy $self = shift @_;\n+\treturn ($self->parse(qr/^}$/),\n+\t\t$self->expect('}'));\n+}\n+\n+sub parse_subshell {\n+\tmy $self = shift @_;\n+\t$self->{insubshell}++;\n+\tmy @tokens = ($self->parse(qr/^\\)$/),\n+\t\t      $self->expect(')'));\n+\t$self->{insubshell}--;\n+\treturn @tokens;\n+}\n+\n+sub parse_case_pattern {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\twhile (defined(my $token = $self->next_token())) {\n+\t\tpush(@tokens, $token);\n+\t\tlast if $token->[0] eq ')';\n+\t}\n+\treturn @tokens;\n+}\n+\n+sub parse_case {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\tpush(@tokens,\n+\t     $self->next_token(), # subject\n+\t     $self->optional_newlines(),\n+\t     $self->expect('in'),\n+\t     $self->optional_newlines());\n+\twhile (1) {\n+\t\tmy $token = $self->peek();\n+\t\tlast unless defined($token) && $token->[0] ne 'esac';\n+\t\tpush(@tokens,\n+\t\t     $self->parse_case_pattern(),\n+\t\t     $self->optional_newlines(),\n+\t\t     $self->parse(qr/^(?:;;|esac)$/)); # item body\n+\t\t$token = $self->peek();\n+\t\tlast unless defined($token) && $token->[0] ne 'esac';\n+\t\tpush(@tokens,\n+\t\t     $self->expect(';;'),\n+\t\t     $self->optional_newlines());\n+\t}\n+\tpush(@tokens, $self->expect('esac'));\n+\treturn @tokens;\n+}\n+\n+sub parse_for {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\tpush(@tokens,\n+\t     $self->next_token(), # variable\n+\t     $self->optional_newlines());\n+\tmy $token = $self->peek();\n+\tif (defined($token) && $token->[0] eq 'in') {\n+\t\tpush(@tokens,\n+\t\t     $self->expect('in'),\n+\t\t     $self->optional_newlines());\n+\t}\n+\tpush(@tokens,\n+\t     $self->parse(qr/^do$/), # items\n+\t     $self->expect('do'),\n+\t     $self->optional_newlines(),\n+\t     $self->parse_loop_body(),\n+\t     $self->expect('done'));\n+\treturn @tokens;\n+}\n+\n+sub parse_if {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\twhile (1) {\n+\t\tpush(@tokens,\n+\t\t     $self->parse(qr/^then$/), # if/elif condition\n+\t\t     $self->expect('then'),\n+\t\t     $self->optional_newlines(),\n+\t\t     $self->parse(qr/^(?:elif|else|fi)$/)); # if/elif body\n+\t\tmy $token = $self->peek();\n+\t\tlast unless defined($token) && $token->[0] eq 'elif';\n+\t\tpush(@tokens, $self->expect('elif'));\n+\t}\n+\tmy $token = $self->peek();\n+\tif (defined($token) && $token->[0] eq 'else') {\n+\t\tpush(@tokens,\n+\t\t     $self->expect('else'),\n+\t\t     $self->optional_newlines(),\n+\t\t     $self->parse(qr/^fi$/)); # else body\n+\t}\n+\tpush(@tokens, $self->expect('fi'));\n+\treturn @tokens;\n+}\n+\n+sub parse_loop_body {\n+\tmy $self = shift @_;\n+\treturn $self->parse(qr/^done$/);\n+}\n+\n+sub parse_loop {\n+\tmy $self = shift @_;\n+\treturn ($self->parse(qr/^do$/), # condition\n+\t\t$self->expect('do'),\n+\t\t$self->optional_newlines(),\n+\t\t$self->parse_loop_body(),\n+\t\t$self->expect('done'));\n+}\n+\n+sub parse_func {\n+\tmy $self = shift @_;\n+\treturn ($self->expect('('),\n+\t\t$self->expect(')'),\n+\t\t$self->optional_newlines(),\n+\t\t$self->parse_cmd()); # body\n+}\n+\n+sub parse_bash_array_assignment {\n+\tmy $self = shift @_;\n+\tmy @tokens = $self->expect('(');\n+\twhile (defined(my $token = $self->next_token())) {\n+\t\tpush(@tokens, $token);\n+\t\tlast if $token->[0] eq ')';\n+\t}\n+\treturn @tokens;\n+}\n+\n+my %compound = (\n+\t'{' => \\&parse_group,\n+\t'(' => \\&parse_subshell,\n+\t'case' => \\&parse_case,\n+\t'for' => \\&parse_for,\n+\t'if' => \\&parse_if,\n+\t'until' => \\&parse_loop,\n+\t'while' => \\&parse_loop);\n+\n+sub parse_cmd {\n+\tmy $self = shift @_;\n+\tmy $cmd = $self->next_token();\n+\treturn () unless defined($cmd);\n+\treturn $cmd if $cmd->[0] eq \"\\n\";\n+\n+\tmy $token;\n+\tmy @tokens = $cmd;\n+\tif ($cmd->[0] eq '!') {\n+\t\tpush(@tokens, $self->parse_cmd());\n+\t\treturn @tokens;\n+\t} elsif (my $f = $compound{$cmd->[0]}) {\n+\t\tpush(@tokens, $self->$f());\n+\t} elsif (defined($token = $self->peek()) && $token->[0] eq '(') {\n+\t\tif ($cmd->[0] !~ /\\w=$/) {\n+\t\t\tpush(@tokens, $self->parse_func());\n+\t\t\treturn @tokens;\n+\t\t}\n+\t\tmy @array = $self->parse_bash_array_assignment();\n+\t\t$tokens[-1]->[0] .= join(' ', map {$_->[0]} @array);\n+\t\t$tokens[-1]->[2] = $array[$#array][2] if @array;\n+\t}\n+\n+\twhile (defined(my $token = $self->next_token())) {\n+\t\t$self->untoken($token), last if $self->stop_at($token);\n+\t\tpush(@tokens, $token);\n+\t\tlast if $token->[0] =~ /^(?:[;&\\n|]|&&|\\|\\|)$/;\n+\t}\n+\tpush(@tokens, $self->next_token()) if $tokens[-1]->[0] ne \"\\n\" && defined($token = $self->peek()) && $token->[0] eq \"\\n\";\n+\treturn @tokens;\n+}\n+\n+sub accumulate {\n+\tmy ($self, $tokens, $cmd) = @_;\n+\tpush(@$tokens, @$cmd);\n+}\n+\n+sub parse {\n+\tmy ($self, $stop) = @_;\n+\tpush(@{$self->{stop}}, $stop);\n+\tgoto DONE if $self->stop_at($self->peek());\n+\tmy @tokens;\n+\twhile (my @cmd = $self->parse_cmd()) {\n+\t\t$self->accumulate(\\@tokens, \\@cmd);\n+\t\tlast if $self->stop_at($self->peek());\n+\t}\n+DONE:\n+\tpop(@{$self->{stop}});\n+\treturn @tokens;\n+}\n+\n+# ScriptParser is a subclass of ShellParser which identifies individual test\n+# definitions within test scripts and passes each test body to check_test().\n+# ScriptParser detects test definitions not only at the top-level of test\n+# scripts but also within compound commands such as loops and function\n+# definitions.\n+\n+package ScriptParser;\n+\n+our @ISA = ('ShellParser');\n+\n+sub new {\n+\tmy $class = shift @_;\n+\tmy $self = $class->SUPER::new(@_);\n+\t$self->{ntests} = 0;\n+\t$self->{nerrs} = 0;\n+\treturn $self;\n+}\n+\n+# extract the raw content of a token, which may be a single string or a\n+# composition of multiple strings and non-string character runs; for instance,\n+# `\"test body\"` unwraps to `test body`; `word\"a b\"42'c d'` to `worda b42c d`\n+sub unwrap {\n+\tmy $token = (@_ ? shift @_ : $_)->[0];\n+\t# simple case: 'sqstring' or \"dqstring\"\n+\treturn $token if $token =~ s/^'([^']*)'$/$1/;\n+\treturn $token if $token =~ s/^\"([^\"]*)\"$/$1/;\n+\n+\t# composite case\n+\tmy ($s, $q, $escaped);\n+\twhile (1) {\n+\t\t# slurp up non-special characters\n+\t\t$s .= $1 if $token =~ /\\G([^\\\\'\"]*)/gc;\n+\t\t# handle special characters\n+\t\tlast unless $token =~ /\\G(.)/sgc;\n+\t\tmy $c = $1;\n+\t\t$q = undef, next if defined($q) && $c eq $q;\n+\t\t$q = $c, next if !defined($q) && $c =~ /^['\"]$/;\n+\t\tif ($c eq '\\\\') {\n+\t\t\tlast unless $token =~ /\\G(.)/sgc;\n+\t\t\t$c = $1;\n+\t\t\t$s .= '\\\\' if $c eq \"\\n\"; # preserve line splice\n+\t\t}\n+\t\t$s .= $c;\n+\t}\n+\treturn $s\n+}\n+\n+sub check_test {\n+\t# no-op; subclass and override to implement lint checks\n+}\n+\n+sub parse_cmd {\n+\tmy $self = shift @_;\n+\tmy @tokens = $self->SUPER::parse_cmd();\n+\treturn @tokens unless @tokens && $tokens[0]->[0] =~ /^test_expect_(?:success|failure)$/;\n+\tmy $n = $#tokens;\n+\t$n-- while $n >= 0 && $tokens[$n]->[0] =~ /^(?:[;&\\n|]|&&|\\|\\|)$/;\n+\tmy $herebody;\n+\tif ($n >= 2 && $tokens[$n-1]->[0] eq '-' && $tokens[$n]->[0] =~ /^<<-?(.+)$/) {\n+\t\t$herebody = $self->{heredocs}->{$1};\n+\t\t$n--;\n+\t}\n+\t$self->check_test($tokens[1], $tokens[2], $herebody) if $n == 2; # title body\n+\t$self->check_test($tokens[2], $tokens[3], $herebody) if $n > 2;  # prereq title body\n+\treturn @tokens;\n+}\n+\n+1;\n-- \ngitgitgadget\n\n"},{"id":"545435","messageId":"c1b86748d1798db3f85f25654bdec34f36e7b591.1781323575.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v2.git.1781323575.gitgitgadget@gmail.com","subject":"[PATCH v2 4/6] t: fix Lexer line count for $() inside double-quoted strings","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-13T04:06:13Z","receivedAt":"2026-06-13T04:06:28Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nscan_dqstring's post-loop newline counter re-counts newlines that\nwere already counted during recursive parsing of $() bodies.  This\nhappens because scan_dollar returns text containing newlines (from\nmulti-line command substitutions), and the catch-all counter at the\nend of scan_dqstring counts all of them again.\n\nFix this by counting newlines inline as non-special characters are\nconsumed, and removing the post-loop catch-all.  Each newline is\nnow counted exactly once: literal newlines at the inline match,\nline splices at the backslash handler, and $() newlines by\nscan_token during the recursive parse.\n\nThis is a latent bug: any consumer that relies on token line\nnumbers rather than byte offsets would get incorrect results for\ntokens following a multi-line $() inside a double-quoted string.\nchainlint is not affected because it annotates the original body\ntext using byte offsets, not token line numbers.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/lib-shell-parser.pl | 9 ++++++---\n 1 file changed, 6 insertions(+), 3 deletions(-)\n\ndiff --git a/t/lib-shell-parser.pl b/t/lib-shell-parser.pl\nindex edbfee0839..c91289dfd6 100644\n--- a/t/lib-shell-parser.pl\n+++ b/t/lib-shell-parser.pl\n@@ -93,8 +93,12 @@ sub scan_dqstring {\n \tmy $b = $self->{buff};\n \tmy $s = '\"';\n \twhile (1) {\n-\t\t# slurp up non-special characters\n-\t\t$s .= $1 if $$b =~ /\\G([^\"\\$\\\\]+)/gc;\n+\t\t# Slurp non-special characters; count newlines here because\n+\t\t# newlines inside $() are already counted by the recursive parse.\n+\t\tif ($$b =~ /\\G([^\"\\$\\\\]+)/gc) {\n+\t\t\t$s .= $1;\n+\t\t\t$self->{lineno} += $1 =~ tr/\\n//;\n+\t\t}\n \t\t# handle special characters\n \t\tlast unless $$b =~ /\\G(.)/sgc;\n \t\tmy $c = $1;\n@@ -111,7 +115,6 @@ sub scan_dqstring {\n \t\t}\n \t\tdie(\"internal error scanning dq-string '$c'\\n\");\n \t}\n-\t$self->{lineno} += () = $s =~ /\\n/sg;\n \treturn $s;\n }\n \n-- \ngitgitgadget\n\n"},{"id":"545437","messageId":"3a589ef7386303075413f388e61c203c4e325d44.1781323575.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v2.git.1781323575.gitgitgadget@gmail.com","subject":"[PATCH v2 5/6] t: convert grep assertions to test_grep","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-13T04:06:14Z","receivedAt":"2026-06-13T04:06:33Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nReplace bare grep with test_grep in test assertions across the\nsuite, including sourced test helpers (lib-*.sh, *-tests.sh).\ntest_grep prints the contents of the file being searched on\nfailure, making debugging easier than a bare grep which fails\nsilently.\n\nOnly assertion-style greps are converted: grep used as a filter\nin pipelines, command substitutions, conditionals, or with\nredirected I/O is left as-is with a \"# lint-ok\" annotation.\nExisting '! test_grep' calls are rewritten to 'test_grep !' so\nthat the diagnostic output is preserved on failure.\n\nThe conversion was generated using a grep-assertion linter\n(greplint.pl, added in the following commit) to identify bare\ngrep calls at command position.  To reproduce:\n\n    # Step 1: mark bare greps that should not be converted\n    sed -i '/! grep \"$m\" \\.git\\/packed-refs/s/$/ # lint-ok: file may not exist (reftable)/' \\\n        t/t1400-update-ref.sh\n    sed -i '/! grep dirty file3 &&/{/lint-ok/!s/$/ # lint-ok: file may not exist after --quit/}' \\\n        t/t3420-rebase-autostash.sh\n    sed -i '/grep -vf before commits\\.raw/s/$/ # lint-ok: data filter/' \\\n        t/t5326-multi-pack-bitmaps.sh\n    sed -i '/! grep $d shallow-client\\/\\.git\\/shallow/s/$/ # lint-ok: file may not exist after repack/' \\\n        t/t5537-fetch-shallow.sh\n    sed -i '/grep -E \"^\\[0-9a-f\\].*|| :/s/$/ # lint-ok: data filter/' \\\n        t/t5702-protocol-v2.sh\n    sed -i '/! grep gitdir squatting-clone/s/$/ # lint-ok: file may not exist after failed clone/' \\\n        t/t7450-bad-git-dotfiles.sh\n\n    # Step 2: reorder pre-existing '! test_grep' to 'test_grep !'\n    # (must come before steps 3-4 so greplint does not see them)\n    sed -i 's/! test_grep/test_grep !/' t/t0031-lockfile-pid.sh\n    sed -i 's/! test_grep/test_grep !/' t/t5300-pack-object.sh\n    sed -i 's/! test_grep/test_grep !/' t/t5319-multi-pack-index.sh\n\n    # Step 3: convert '! grep' -> 'test_grep !'\n    perl t/greplint.pl t/*.sh 2>&1 | cut -d: -f1,2 |\n    while IFS=: read f l; do\n        sed -i \"${l}s/! *grep/test_grep !/\" \"$f\"\n    done\n\n    # Step 4: convert remaining 'grep' -> 'test_grep'\n    perl t/greplint.pl t/*.sh 2>&1 | cut -d: -f1,2 |\n    while IFS=: read f l; do\n        sed -i \"${l}s/grep/test_grep/\" \"$f\"\n    done\n\nTo verify, run: make -C t test-greplint\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/for-each-ref-tests.sh                     |  12 +-\n t/lib-bitmap.sh                             |  12 +-\n t/lib-bundle-uri-protocol.sh                |  26 +-\n t/lib-httpd.sh                              |   2 +-\n t/pack-refs-tests.sh                        |   2 +-\n t/show-ref-exists-tests.sh                  |   2 +-\n t/t0000-basic.sh                            |  16 +-\n t/t0001-init.sh                             |  18 +-\n t/t0008-ignores.sh                          |   8 +-\n t/t0009-git-dir-validation.sh               |   6 +-\n t/t0012-help.sh                             |   4 +-\n t/t0013-sha1dc.sh                           |   2 +-\n t/t0017-env-helper.sh                       |   4 +-\n t/t0021-conversion.sh                       |  18 +-\n t/t0029-core-unsetenvvars.sh                |   4 +-\n t/t0030-stripspace.sh                       |   4 +-\n t/t0031-lockfile-pid.sh                     |   2 +-\n t/t0040-parse-options.sh                    |  52 +--\n t/t0041-usage.sh                            |   2 +-\n t/t0052-simple-ipc.sh                       |  10 +-\n t/t0061-run-command.sh                      |   2 +-\n t/t0066-dir-iterator.sh                     |   2 +-\n t/t0068-for-each-repo.sh                    |  16 +-\n t/t0070-fundamental.sh                      |   6 +-\n t/t0081-find-pack.sh                        |  12 +-\n t/t0091-bugreport.sh                        |  18 +-\n t/t0092-diagnose.sh                         |  12 +-\n t/t0100-previous.sh                         |   2 +-\n t/t0200-gettext-basic.sh                    |  14 +-\n t/t0203-gettext-setlocale-sanity.sh         |   4 +-\n t/t0204-gettext-reencode-sanity.sh          |   8 +-\n t/t0210-trace2-normal.sh                    |   6 +-\n t/t0211-trace2-perf.sh                      |  80 ++---\n t/t0212-trace2-event.sh                     |   8 +-\n t/t0300-credentials.sh                      |   4 +-\n t/t0410-partial-clone.sh                    |  82 ++---\n t/t0450-txt-doc-vs-help.sh                  |   2 +-\n t/t0500-progress-display.sh                 |  18 +-\n t/t0610-reftable-basics.sh                  |   8 +-\n t/t1004-read-tree-m-u-wf.sh                 |   8 +-\n t/t1006-cat-file.sh                         |  18 +-\n t/t1007-hash-object.sh                      |   8 +-\n t/t1011-read-tree-sparse-checkout.sh        |  10 +-\n t/t1050-large.sh                            |   6 +-\n t/t1091-sparse-checkout-builtin.sh          |  24 +-\n t/t1092-sparse-checkout-compatibility.sh    |  44 +--\n t/t1300-config.sh                           |  16 +-\n t/t1305-config-include.sh                   |   2 +-\n t/t1308-config-set.sh                       |   6 +-\n t/t1400-update-ref.sh                       | 170 +++++-----\n t/t1403-show-ref.sh                         |  18 +-\n t/t1410-reflog.sh                           |   4 +-\n t/t1415-worktree-refs.sh                    |   4 +-\n t/t1430-bad-ref-name.sh                     |  56 ++--\n t/t1450-fsck.sh                             |  12 +-\n t/t1451-fsck-buffer.sh                      |   6 +-\n t/t1460-refs-migrate.sh                     |   2 +-\n t/t1500-rev-parse.sh                        |   6 +-\n t/t1502-rev-parse-parseopt.sh               |   2 +-\n t/t1503-rev-parse-verify.sh                 |  10 +-\n t/t1510-repo-setup.sh                       |  10 +-\n t/t1512-rev-parse-disambiguation.sh         |   4 +-\n t/t1515-rev-parse-outside-repo.sh           |   2 +-\n t/t1800-hook.sh                             |  18 +-\n t/t2004-checkout-cache-temp.sh              |   4 +-\n t/t2019-checkout-ambiguous-ref.sh           |   4 +-\n t/t2024-checkout-dwim.sh                    |   8 +-\n t/t2030-unresolve-info.sh                   |   6 +-\n t/t2060-switch.sh                           |   6 +-\n t/t2070-restore.sh                          |   2 +-\n t/t2080-parallel-checkout-basics.sh         |  14 +-\n t/t2081-parallel-checkout-collisions.sh     |  24 +-\n t/t2082-parallel-checkout-attributes.sh     |  12 +-\n t/t2103-update-index-ignore-missing.sh      |   6 +-\n t/t2200-add-update.sh                       |   2 +-\n t/t2203-add-intent.sh                       |   6 +-\n t/t2400-worktree-add.sh                     |  24 +-\n t/t2402-worktree-list.sh                    |  16 +-\n t/t2403-worktree-move.sh                    |   6 +-\n t/t2405-worktree-submodule.sh               |   6 +-\n t/t2407-worktree-heads.sh                   |  26 +-\n t/t2500-untracked-overwriting.sh            |   8 +-\n t/t2501-cwd-empty.sh                        |   4 +-\n t/t3001-ls-files-others-exclude.sh          |   6 +-\n t/t3007-ls-files-recurse-submodules.sh      |   6 +-\n t/t3200-branch.sh                           |  12 +-\n t/t3202-show-branch.sh                      |  10 +-\n t/t3203-branch-output.sh                    |   4 +-\n t/t3206-range-diff.sh                       |  78 ++---\n t/t3207-branch-submodule.sh                 |   4 +-\n t/t3301-notes.sh                            |  32 +-\n t/t3310-notes-merge-manual-resolve.sh       |  16 +-\n t/t3320-notes-merge-worktrees.sh            |   2 +-\n t/t3400-rebase.sh                           |  16 +-\n t/t3402-rebase-merge.sh                     |  16 +-\n t/t3404-rebase-interactive.sh               |  72 ++--\n t/t3406-rebase-message.sh                   |   6 +-\n t/t3415-rebase-autosquash.sh                |  10 +-\n t/t3416-rebase-onto-threedots.sh            |   4 +-\n t/t3418-rebase-continue.sh                  |  10 +-\n t/t3420-rebase-autostash.sh                 |  26 +-\n t/t3422-rebase-incompatible-options.sh      |   4 +-\n t/t3429-rebase-edit-todo.sh                 |   2 +-\n t/t3430-rebase-merges.sh                    |  32 +-\n t/t3500-cherry.sh                           |   4 +-\n t/t3501-revert-cherry-pick.sh               |   6 +-\n t/t3504-cherry-pick-rerere.sh               |   6 +-\n t/t3510-cherry-pick-sequence.sh             |  24 +-\n t/t3602-rm-sparse-checkout.sh               |   4 +-\n t/t3705-add-sparse-checkout.sh              |  10 +-\n t/t3800-mktag.sh                            |   4 +-\n t/t3901-i18n-patch.sh                       |  16 +-\n t/t3903-stash.sh                            |  28 +-\n t/t3904-stash-patch.sh                      |   4 +-\n t/t3908-stash-in-worktree.sh                |   2 +-\n t/t4000-diff-format.sh                      |   2 +-\n t/t4001-diff-rename.sh                      |   4 +-\n t/t4011-diff-symlink.sh                     |   2 +-\n t/t4013-diff-various.sh                     |   2 +-\n t/t4014-format-patch.sh                     | 344 ++++++++++----------\n t/t4015-diff-whitespace.sh                  |  16 +-\n t/t4017-diff-retval.sh                      |   2 +-\n t/t4018-diff-funcname.sh                    |   2 +-\n t/t4019-diff-wserror.sh                     |   8 +-\n t/t4020-diff-external.sh                    |  18 +-\n t/t4021-format-patch-numbered.sh            |   4 +-\n t/t4022-diff-rewrite.sh                     |  14 +-\n t/t4028-format-patch-mime-headers.sh        |   6 +-\n t/t4031-diff-rewrite-binary.sh              |  18 +-\n t/t4033-diff-patience.sh                    |   2 +-\n t/t4036-format-patch-signer-mime.sh         |   6 +-\n t/t4038-diff-combined.sh                    |   6 +-\n t/t4051-diff-function-context.sh            |  38 +--\n t/t4053-diff-no-index.sh                    |   4 +-\n t/t4063-diff-blobs.sh                       |   2 +-\n t/t4065-diff-anchored.sh                    |  26 +-\n t/t4067-diff-partial-clone.sh               |  12 +-\n t/t4073-diff-stat-name-width.sh             |  24 +-\n t/t4103-apply-binary.sh                     |   2 +-\n t/t4120-apply-popt.sh                       |   2 +-\n t/t4124-apply-ws-rule.sh                    |  10 +-\n t/t4128-apply-root.sh                       |   2 +-\n t/t4140-apply-ita.sh                        |   4 +-\n t/t4141-apply-too-large.sh                  |   2 +-\n t/t4150-am.sh                               |  48 +--\n t/t4200-rerere.sh                           |   6 +-\n t/t4201-shortlog.sh                         |   2 +-\n t/t4202-log.sh                              |  84 ++---\n t/t4204-patch-id.sh                         |   2 +-\n t/t4205-log-pretty-formats.sh               |   2 +-\n t/t4209-log-pickaxe.sh                      |  10 +-\n t/t4211-line-log.sh                         |  72 ++--\n t/t4216-log-bloom.sh                        |  18 +-\n t/t4252-am-options.sh                       |  22 +-\n t/t4254-am-corrupt.sh                       |   6 +-\n t/t4258-am-quoted-cr.sh                     |   2 +-\n t/t4301-merge-tree-write-tree.sh            |  18 +-\n t/t5000-tar-tree.sh                         |  10 +-\n t/t5004-archive-corner-cases.sh             |   2 +-\n t/t5100-mailinfo.sh                         |   2 +-\n t/t5150-request-pull.sh                     |  18 +-\n t/t5300-pack-object.sh                      |  22 +-\n t/t5302-pack-index.sh                       |   6 +-\n t/t5304-prune.sh                            |   8 +-\n t/t5310-pack-bitmaps.sh                     |  14 +-\n t/t5317-pack-objects-filter-objects.sh      |  12 +-\n t/t5318-commit-graph.sh                     |   8 +-\n t/t5319-multi-pack-index.sh                 |  16 +-\n t/t5324-split-commit-graph.sh               |  10 +-\n t/t5325-reverse-index.sh                    |   2 +-\n t/t5326-multi-pack-bitmaps.sh               |  28 +-\n t/t5328-commit-graph-64bit-time.sh          |   2 +-\n t/t5329-pack-objects-cruft.sh               |   8 +-\n t/t5334-incremental-multi-pack-index.sh     |   2 +-\n t/t5335-compact-multi-pack-index.sh         |   4 +-\n t/t5351-unpack-large-objects.sh             |   2 +-\n t/t5402-post-merge-hook.sh                  |   4 +-\n t/t5403-post-checkout-hook.sh               |   2 +-\n t/t5404-tracking-branches.sh                |   2 +-\n t/t5406-remote-rejects.sh                   |   2 +-\n t/t5407-post-rewrite-hook.sh                |   8 +-\n t/t5409-colorize-remote-messages.sh         |  36 +-\n t/t5500-fetch-pack.sh                       |  38 +--\n t/t5504-fetch-receive-strict.sh             |  14 +-\n t/t5505-remote.sh                           |  20 +-\n t/t5510-fetch.sh                            |  10 +-\n t/t5512-ls-remote.sh                        |   8 +-\n t/t5514-fetch-multiple.sh                   |   2 +-\n t/t5516-fetch-push.sh                       |  20 +-\n t/t5520-pull.sh                             |   4 +-\n t/t5524-pull-msg.sh                         |   6 +-\n t/t5526-fetch-submodules.sh                 |  16 +-\n t/t5529-push-errors.sh                      |   4 +-\n t/t5530-upload-pack-error.sh                |  18 +-\n t/t5531-deep-submodule-push.sh              |   2 +-\n t/t5532-fetch-proxy.sh                      |   2 +-\n t/t5533-push-cas.sh                         |  12 +-\n t/t5534-push-signed.sh                      |  22 +-\n t/t5537-fetch-shallow.sh                    |   2 +-\n t/t5538-push-shallow.sh                     |   2 +-\n t/t5539-fetch-http-shallow.sh               |   4 +-\n t/t5541-http-push-smart.sh                  |  32 +-\n t/t5544-pack-objects-hook.sh                |  12 +-\n t/t5550-http-fetch-dumb.sh                  |   4 +-\n t/t5551-http-fetch-smart.sh                 |  46 +--\n t/t5552-skipping-fetch-negotiator.sh        |   6 +-\n t/t5554-noop-fetch-negotiator.sh            |   4 +-\n t/t5557-http-get.sh                         |   2 +-\n t/t5558-clone-bundle-uri.sh                 |  38 +--\n t/t5562-http-backend-content-length.sh      |   2 +-\n t/t5564-http-proxy.sh                       |  10 +-\n t/t5581-http-curl-verbose.sh                |   2 +-\n t/t5583-push-branches.sh                    |   8 +-\n t/t5601-clone.sh                            |  28 +-\n t/t5604-clone-reference.sh                  |   8 +-\n t/t5605-clone-local.sh                      |   2 +-\n t/t5606-clone-options.sh                    |   6 +-\n t/t5612-clone-refspec.sh                    |   2 +-\n t/t5616-partial-clone.sh                    |  60 ++--\n t/t5619-clone-local-ambiguous-transport.sh  |   2 +-\n t/t5620-backfill.sh                         |  12 +-\n t/t5700-protocol-v1.sh                      |  46 +--\n t/t5701-git-serve.sh                        |  14 +-\n t/t5702-protocol-v2.sh                      | 154 ++++-----\n t/t5703-upload-pack-ref-in-want.sh          |  22 +-\n t/t5705-session-id-in-capabilities.sh       |  12 +-\n t/t5750-bundle-uri-parse.sh                 |   8 +-\n t/t5801-remote-helpers.sh                   |   4 +-\n t/t5810-proto-disable-local.sh              |   2 +-\n t/t5813-proto-disable-ssh.sh                |   4 +-\n t/t6000-rev-list-misc.sh                    |  26 +-\n t/t6005-rev-list-count.sh                   |   8 +-\n t/t6006-rev-list-format.sh                  |   4 +-\n t/t6009-rev-list-parent.sh                  |   4 +-\n t/t6020-bundle-misc.sh                      |  12 +-\n t/t6022-rev-list-missing.sh                 |   4 +-\n t/t6030-bisect-porcelain.sh                 | 150 ++++-----\n t/t6040-tracking-info.sh                    |   2 +-\n t/t6112-rev-list-filters-objects.sh         |  24 +-\n t/t6115-rev-list-du.sh                      |   4 +-\n t/t6120-describe.sh                         |  14 +-\n t/t6200-fmt-merge-msg.sh                    |  82 ++---\n t/t6402-merge-rename.sh                     |   4 +-\n t/t6403-merge-file.sh                       |   6 +-\n t/t6404-recursive-merge.sh                  |   2 +-\n t/t6406-merge-attr.sh                       |  20 +-\n t/t6417-merge-ours-theirs.sh                |  30 +-\n t/t6418-merge-text-auto.sh                  |   2 +-\n t/t6422-merge-rename-corner-cases.sh        |   8 +-\n t/t6423-merge-rename-directories.sh         |  72 ++--\n t/t6424-merge-unrelated-index-changes.sh    |   6 +-\n t/t6427-diff3-conflict-markers.sh           |  10 +-\n t/t6432-merge-recursive-space-options.sh    |   4 +-\n t/t6436-merge-overwrite.sh                  |   6 +-\n t/t6437-submodule-merge.sh                  |  10 +-\n t/t6500-gc.sh                               |   8 +-\n t/t6600-test-reach.sh                       |   4 +-\n t/t7001-mv.sh                               |  16 +-\n t/t7002-mv-sparse-checkout.sh               |  38 +--\n t/t7003-filter-branch.sh                    |  16 +-\n t/t7004-tag.sh                              |   2 +-\n t/t7006-pager.sh                            |  16 +-\n t/t7012-skip-worktree-writing.sh            |   6 +-\n t/t7030-verify-tag.sh                       |  52 +--\n t/t7031-verify-tag-signed-ssh.sh            |  46 +--\n t/t7102-reset.sh                            |   2 +-\n t/t7110-reset-merge.sh                      |  40 +--\n t/t7201-co.sh                               |   6 +-\n t/t7300-clean.sh                            |   2 +-\n t/t7301-clean-interactive.sh                |   2 +-\n t/t7400-submodule-basic.sh                  |  32 +-\n t/t7402-submodule-rebase.sh                 |   2 +-\n t/t7406-submodule-update.sh                 |  26 +-\n t/t7416-submodule-dash-url.sh               |  20 +-\n t/t7417-submodule-path-url.sh               |   2 +-\n t/t7450-bad-git-dotfiles.sh                 |  14 +-\n t/t7501-commit-basic-functionality.sh       |  16 +-\n t/t7502-commit-porcelain.sh                 |   2 +-\n t/t7507-commit-verbose.sh                   |   6 +-\n t/t7508-status.sh                           |   6 +-\n t/t7510-signed-commit.sh                    |  68 ++--\n t/t7516-commit-races.sh                     |   4 +-\n t/t7519-status-fsmonitor.sh                 |  14 +-\n t/t7527-builtin-fsmonitor.sh                |  82 ++---\n t/t7528-signed-commit-ssh.sh                |  68 ++--\n t/t7600-merge.sh                            |  10 +-\n t/t7603-merge-reduce-heads.sh               |  20 +-\n t/t7606-merge-custom.sh                     |   2 +-\n t/t7607-merge-state.sh                      |   4 +-\n t/t7610-mergetool.sh                        |  18 +-\n t/t7700-repack.sh                           |  14 +-\n t/t7703-repack-geometric.sh                 |   4 +-\n t/t7704-repack-cruft.sh                     |  12 +-\n t/t7800-difftool.sh                         |  26 +-\n t/t7810-grep.sh                             |  22 +-\n t/t7814-grep-recurse-submodules.sh          |   2 +-\n t/t7900-maintenance.sh                      |  34 +-\n t/t8008-blame-formats.sh                    |   2 +-\n t/t8010-cat-file-filters.sh                 |   2 +-\n t/t8012-blame-colors.sh                     |   2 +-\n t/t9001-send-email.sh                       | 190 +++++------\n t/t9003-help-autocorrect.sh                 |   6 +-\n t/t9106-git-svn-commit-diff-clobber.sh      |   2 +-\n t/t9107-git-svn-migrate.sh                  |  30 +-\n t/t9110-git-svn-use-svm-props.sh            |  20 +-\n t/t9111-git-svn-use-svnsync-props.sh        |  18 +-\n t/t9114-git-svn-dcommit-merge.sh            |   6 +-\n t/t9116-git-svn-log.sh                      |   8 +-\n t/t9117-git-svn-init-clone.sh               |  12 +-\n t/t9119-git-svn-info.sh                     |  16 +-\n t/t9122-git-svn-author.sh                   |   8 +-\n t/t9130-git-svn-authors-file.sh             |   8 +-\n t/t9138-git-svn-authors-prog.sh             |  14 +-\n t/t9140-git-svn-reset.sh                    |   4 +-\n t/t9153-git-svn-rewrite-uuid.sh             |   4 +-\n t/t9200-git-cvsexportcommit.sh              |   2 +-\n t/t9210-scalar.sh                           |  34 +-\n t/t9211-scalar-clone.sh                     |  16 +-\n t/t9300-fast-import.sh                      |  10 +-\n t/t9350-fast-export.sh                      |  54 +--\n t/t9351-fast-export-anonymize.sh            |  36 +-\n t/t9400-git-cvsserver-server.sh             |   4 +-\n t/t9501-gitweb-standalone-http-status.sh    |  58 ++--\n t/t9502-gitweb-standalone-parse-output.sh   |  38 +--\n t/t9800-git-p4-basic.sh                     |  10 +-\n t/t9801-git-p4-branch.sh                    |  48 +--\n t/t9806-git-p4-options.sh                   |  10 +-\n t/t9807-git-p4-submit.sh                    |   2 +-\n t/t9810-git-p4-rcs.sh                       |   8 +-\n t/t9813-git-p4-preserve-users.sh            |   8 +-\n t/t9814-git-p4-rename.sh                    |   8 +-\n t/t9827-git-p4-change-filetype.sh           |   4 +-\n t/t9832-unshelve.sh                         |   6 +-\n t/t9833-errors.sh                           |   4 +-\n t/t9835-git-p4-metadata-encoding-python2.sh |  36 +-\n t/t9836-git-p4-metadata-encoding-python3.sh |  38 +--\n t/t9850-shell.sh                            |   2 +-\n t/t9902-completion.sh                       |  26 +-\n 338 files changed, 2807 insertions(+), 2807 deletions(-)\n\ndiff --git a/t/for-each-ref-tests.sh b/t/for-each-ref-tests.sh\nindex bd2d45c971..6b359d940e 100644\n--- a/t/for-each-ref-tests.sh\n+++ b/t/for-each-ref-tests.sh\n@@ -523,7 +523,7 @@ test_expect_success 'Verify descending sort' '\n \n test_expect_success 'Give help even with invalid sort atoms' '\n \ttest_expect_code 129 ${git_for_each_ref} --sort=bogus -h >actual 2>&1 &&\n-\tgrep \"^usage: ${git_for_each_ref}\" actual\n+\ttest_grep \"^usage: ${git_for_each_ref}\" actual\n '\n \n cat >expected <<\\EOF\n@@ -622,7 +622,7 @@ test_expect_success 'Quoting style: tcl' '\n for i in \"--perl --shell\" \"-s --python\" \"--python --tcl\" \"--tcl --perl\"; do\n \ttest_expect_success \"more than one quoting style: $i\" \"\n \t\ttest_must_fail ${git_for_each_ref} $i 2>err &&\n-\t\tgrep '^error: more than one quoting style' err\n+\t\ttest_grep '^error: more than one quoting style' err\n \t\"\n done\n \n@@ -1892,7 +1892,7 @@ test_expect_success \"${git_for_each_ref} --stdin: fails if extra args\" '\n \t>in &&\n \ttest_must_fail ${git_for_each_ref} --format=\"%(refname)\" \\\n \t\t--stdin refs/heads/extra <in 2>err &&\n-\tgrep \"unknown arguments supplied with --stdin\" err\n+\ttest_grep \"unknown arguments supplied with --stdin\" err\n '\n \n test_expect_success \"${git_for_each_ref} --stdin: matches\" '\n@@ -1955,11 +1955,11 @@ test_expect_success \"${git_for_each_ref} with nested tags\" '\n \n test_expect_success 'is-base atom with non-commits' '\n \t${git_for_each_ref} --format=\"%(is-base:HEAD) %(refname)\" >out 2>err &&\n-\tgrep \"(HEAD) refs/heads/main\" out &&\n+\ttest_grep \"(HEAD) refs/heads/main\" out &&\n \n \ttest_line_count = 2 err &&\n-\tgrep \"error: object .* is a commit, not a blob\" err &&\n-\tgrep \"error: bad tag pointer to\" err\n+\ttest_grep \"error: object .* is a commit, not a blob\" err &&\n+\ttest_grep \"error: bad tag pointer to\" err\n '\n \n GRADE_FORMAT=\"%(signature:grade)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)\"\ndiff --git a/t/lib-bitmap.sh b/t/lib-bitmap.sh\nindex 62aa6744a6..2de6646128 100644\n--- a/t/lib-bitmap.sh\n+++ b/t/lib-bitmap.sh\n@@ -173,7 +173,7 @@ rev_list_tests_head () {\n \n \ttest_expect_success \"bitmap --objects handles non-commit objects ($state, $branch)\" '\n \t\tgit rev-list --objects --use-bitmap-index $branch tagged-blob >actual &&\n-\t\tgrep $blob actual\n+\t\ttest_grep $blob actual\n \t'\n }\n \n@@ -242,16 +242,16 @@ basic_bitmap_tests () {\n \t\tGIT_PROGRESS_DELAY=0 \\\n \t\t\tgit pack-objects --all --stdout --progress \\\n \t\t\t</dev/null >/dev/null 2>stderr &&\n-\t\tgrep \"Enumerating objects: $count, done\" stderr &&\n-\t\tgrep \"pack-reused $count\" stderr &&\n+\t\ttest_grep \"Enumerating objects: $count, done\" stderr &&\n+\t\ttest_grep \"pack-reused $count\" stderr &&\n \n \t\t# now the same but with one non-reused object\n \t\tgit commit --allow-empty -m \"an extra commit object\" &&\n \t\tGIT_PROGRESS_DELAY=0 \\\n \t\t\tgit pack-objects --all --stdout --progress \\\n \t\t\t</dev/null >/dev/null 2>stderr &&\n-\t\tgrep \"Enumerating objects: $((count+1)), done\" stderr &&\n-\t\tgrep \"pack-reused $count\" stderr\n+\t\ttest_grep \"Enumerating objects: $((count+1)), done\" stderr &&\n+\t\ttest_grep \"pack-reused $count\" stderr\n \t'\n }\n \n@@ -283,7 +283,7 @@ test_rev_exists () {\n \t\tthen\n \t\t\ttest_path_is_file $midx-$(midx_checksum $objdir).rev\n \t\tfi &&\n-\t\tgrep \"\\\"category\\\":\\\"load_midx_revindex\\\",\\\"key\\\":\\\"source\\\",\\\"value\\\":\\\"$kind\\\"\" event.trace\n+\t\ttest_grep \"\\\"category\\\":\\\"load_midx_revindex\\\",\\\"key\\\":\\\"source\\\",\\\"value\\\":\\\"$kind\\\"\" event.trace\n \t'\n }\n \ndiff --git a/t/lib-bundle-uri-protocol.sh b/t/lib-bundle-uri-protocol.sh\nindex de09b6b02e..ecf3284297 100644\n--- a/t/lib-bundle-uri-protocol.sh\n+++ b/t/lib-bundle-uri-protocol.sh\n@@ -63,9 +63,9 @@ test_expect_success \"connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: no\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log &&\n+\ttest_grep \"< version 2\" log &&\n \n-\t! grep bundle-uri log\n+\ttest_grep ! bundle-uri log\n '\n \n test_expect_success \"connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: have bundle-uri\" '\n@@ -78,10 +78,10 @@ test_expect_success \"connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: hav\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log &&\n+\ttest_grep \"< version 2\" log &&\n \n \t# Server advertised bundle-uri capability\n-\tgrep \"< bundle-uri\" log\n+\ttest_grep \"< bundle-uri\" log\n '\n \n test_expect_success \"clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: request bundle-uris\" '\n@@ -95,13 +95,13 @@ test_expect_success \"clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log &&\n+\ttest_grep \"< version 2\" log &&\n \n \t# Server advertised bundle-uri capability\n-\tgrep \"< bundle-uri\" log &&\n+\ttest_grep \"< bundle-uri\" log &&\n \n \t# Client did not issue bundle-uri command\n-\t! grep \"> command=bundle-uri\" log &&\n+\ttest_grep ! \"> command=bundle-uri\" log &&\n \n \tGIT_TRACE_PACKET=\"$PWD/log\" \\\n \tgit \\\n@@ -111,13 +111,13 @@ test_expect_success \"clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log &&\n+\ttest_grep \"< version 2\" log &&\n \n \t# Server advertised bundle-uri capability\n-\tgrep \"< bundle-uri\" log &&\n+\ttest_grep \"< bundle-uri\" log &&\n \n \t# Client issued bundle-uri command\n-\tgrep \"> command=bundle-uri\" log &&\n+\ttest_grep \"> command=bundle-uri\" log &&\n \n \tGIT_TRACE_PACKET=\"$PWD/log3\" \\\n \tgit \\\n@@ -128,13 +128,13 @@ test_expect_success \"clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log3 &&\n+\ttest_grep \"< version 2\" log3 &&\n \n \t# Server advertised bundle-uri capability\n-\tgrep \"< bundle-uri\" log3 &&\n+\ttest_grep \"< bundle-uri\" log3 &&\n \n \t# Client did not issue bundle-uri command (--bundle-uri override)\n-\t! grep \"> command=bundle-uri\" log3\n+\ttest_grep ! \"> command=bundle-uri\" log3\n '\n \n # The remaining tests will all assume transfer.bundleURI=true\ndiff --git a/t/lib-httpd.sh b/t/lib-httpd.sh\nindex fc646447d5..a216e5376f 100644\n--- a/t/lib-httpd.sh\n+++ b/t/lib-httpd.sh\n@@ -275,7 +275,7 @@ test_http_push_nonff () {\n \t'\n \n \ttest_expect_success 'non-fast-forward push show ref status' '\n-\t\tgrep \"^ ! \\[rejected\\][ ]*$BRANCH -> $BRANCH (non-fast-forward)$\" output\n+\t\ttest_grep \"^ ! \\[rejected\\][ ]*$BRANCH -> $BRANCH (non-fast-forward)$\" output\n \t'\n \n \ttest_expect_success 'non-fast-forward push shows help message' '\ndiff --git a/t/pack-refs-tests.sh b/t/pack-refs-tests.sh\nindex d76b087b09..a800177446 100644\n--- a/t/pack-refs-tests.sh\n+++ b/t/pack-refs-tests.sh\n@@ -195,7 +195,7 @@ test_expect_success 'delete ref while another dangling packed ref' '\n test_expect_success 'pack ref directly below refs/' '\n \tgit update-ref refs/top HEAD &&\n \tgit ${pack_refs} --all --prune &&\n-\tgrep refs/top .git/packed-refs &&\n+\ttest_grep refs/top .git/packed-refs &&\n \ttest_path_is_missing .git/refs/top\n '\n \ndiff --git a/t/show-ref-exists-tests.sh b/t/show-ref-exists-tests.sh\nindex 36e8e9df33..1351f690eb 100644\n--- a/t/show-ref-exists-tests.sh\n+++ b/t/show-ref-exists-tests.sh\n@@ -19,7 +19,7 @@ test_expect_success '--exists with missing reference' '\n \n test_expect_success '--exists does not use DWIM' '\n \ttest_expect_code 2 ${git_show_ref_exists} $GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME 2>err &&\n-\tgrep \"reference does not exist\" err\n+\ttest_grep \"reference does not exist\" err\n '\n \n test_expect_success '--exists with HEAD' '\ndiff --git a/t/t0000-basic.sh b/t/t0000-basic.sh\nindex 2b63e1c86c..7bd1f4e7b7 100755\n--- a/t/t0000-basic.sh\n+++ b/t/t0000-basic.sh\n@@ -743,7 +743,7 @@ test_expect_success 'subtest: lazy prereqs do not turn off tracing' '\n \ttest_done\n \tEOF\n \n-\tgrep \"echo trace\" lazy-prereq-and-tracing/err\n+\ttest_grep \"echo trace\" lazy-prereq-and-tracing/err\n '\n \n test_expect_success 'subtest: tests clean up after themselves' '\n@@ -815,7 +815,7 @@ test_expect_success 'subtest: test_atexit is run' '\n \n test_expect_success 'test_oid provides sane info by default' '\n \ttest_oid zero >actual &&\n-\tgrep \"^00*\\$\" actual &&\n+\ttest_grep \"^00*\\$\" actual &&\n \trawsz=\"$(test_oid rawsz)\" &&\n \thexsz=\"$(test_oid hexsz)\" &&\n \t# +1 accounts for the trailing newline\n@@ -827,7 +827,7 @@ test_expect_success 'test_oid can look up data for SHA-1' '\n \ttest_when_finished \"test_detect_hash\" &&\n \ttest_set_hash sha1 &&\n \ttest_oid zero >actual &&\n-\tgrep \"^00*\\$\" actual &&\n+\ttest_grep \"^00*\\$\" actual &&\n \trawsz=\"$(test_oid rawsz)\" &&\n \thexsz=\"$(test_oid hexsz)\" &&\n \ttest $(wc -c <actual) -eq 41 &&\n@@ -839,7 +839,7 @@ test_expect_success 'test_oid can look up data for SHA-256' '\n \ttest_when_finished \"test_detect_hash\" &&\n \ttest_set_hash sha256 &&\n \ttest_oid zero >actual &&\n-\tgrep \"^00*\\$\" actual &&\n+\ttest_grep \"^00*\\$\" actual &&\n \trawsz=\"$(test_oid rawsz)\" &&\n \thexsz=\"$(test_oid hexsz)\" &&\n \ttest $(wc -c <actual) -eq 65 &&\n@@ -884,11 +884,11 @@ test_expect_success 'test_bool_env' '\n \t\t# test script, hence the redirection of fd 7, and aborts\n \t\t# with \"exit 1\", hence the subshell.\n \t\t! ( test_bool_env envvar true ) 7>err &&\n-\t\tgrep \"error: test_bool_env requires bool values\" err &&\n+\t\ttest_grep \"error: test_bool_env requires bool values\" err &&\n \n \t\tenvvar=true &&\n \t\t! ( test_bool_env envvar invalid ) 7>err &&\n-\t\tgrep \"error: test_bool_env requires bool values\" err\n+\t\ttest_grep \"error: test_bool_env requires bool values\" err\n \t)\n '\n \n@@ -1242,12 +1242,12 @@ test_expect_success 'test_must_fail on a failing git command with env' '\n \n test_expect_success 'test_must_fail rejects a non-git command' '\n \t! test_must_fail grep ^$ notafile 2>err &&\n-\tgrep -F \"test_must_fail: only '\"'\"'git'\"'\"' is allowed\" err\n+\ttest_grep -F \"test_must_fail: only '\"'\"'git'\"'\"' is allowed\" err\n '\n \n test_expect_success 'test_must_fail rejects a non-git command with env' '\n \t! test_must_fail env var1=a var2=b grep ^$ notafile 2>err &&\n-\tgrep -F \"test_must_fail: only '\"'\"'git'\"'\"' is allowed\" err\n+\ttest_grep -F \"test_must_fail: only '\"'\"'git'\"'\"' is allowed\" err\n '\n \n test_done\ndiff --git a/t/t0001-init.sh b/t/t0001-init.sh\nindex e4d32bb4d2..7dcd1e9d48 100755\n--- a/t/t0001-init.sh\n+++ b/t/t0001-init.sh\n@@ -278,9 +278,9 @@ test_expect_success POSIXPERM 'init creates a new deep directory (umask vs. shar\n \t\tgit init --bare --shared=0660 newdir/a/b/c &&\n \t\ttest_path_is_dir newdir/a/b/c/refs &&\n \t\tls -ld newdir/a newdir/a/b > lsab.out &&\n-\t\t! grep -v \"^drwxrw[sx]r-x\" lsab.out &&\n+\t\ttest_grep ! -v \"^drwxrw[sx]r-x\" lsab.out &&\n \t\tls -ld newdir/a/b/c > lsc.out &&\n-\t\t! grep -v \"^drwxrw[sx]---\" lsc.out\n+\t\ttest_grep ! -v \"^drwxrw[sx]---\" lsc.out\n \t)\n '\n \n@@ -619,7 +619,7 @@ test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage is not allowed wi\n \tgit init refstorage &&\n \tgit -C refstorage config extensions.refStorage files &&\n \ttest_must_fail git -C refstorage rev-parse 2>err &&\n-\tgrep \"repo version is 0, but v1-only extension found\" err\n+\ttest_grep \"repo version is 0, but v1-only extension found\" err\n '\n \n test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage with files backend' '\n@@ -637,7 +637,7 @@ test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage with unknown back\n \tgit -C refstorage config core.repositoryformatversion 1 &&\n \tgit -C refstorage config extensions.refStorage garbage &&\n \ttest_must_fail git -C refstorage rev-parse 2>err &&\n-\tgrep \"invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}garbage${SQ}\" err\n+\ttest_grep \"invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}garbage${SQ}\" err\n '\n \n test_expect_success 'init with GIT_DEFAULT_REF_FORMAT=garbage' '\n@@ -848,8 +848,8 @@ test_expect_success MINGW 'redirect std handles' '\n \t\tGIT_REDIRECT_STDOUT=output.txt \\\n \t\tGIT_REDIRECT_STDERR=\"2>&1\" \\\n \t\tgit rev-parse --git-dir --verify refs/invalid &&\n-\tgrep \"^\\\\.git\\$\" output.txt &&\n-\tgrep \"Needed a single revision\" output.txt\n+\ttest_grep \"^\\\\.git\\$\" output.txt &&\n+\ttest_grep \"Needed a single revision\" output.txt\n '\n \n test_expect_success '--initial-branch' '\n@@ -862,14 +862,14 @@ test_expect_success '--initial-branch' '\n \tgit init --initial-branch=ignore initial-branch-option 2>err &&\n \ttest_grep \"ignored --initial-branch\" err &&\n \tgit -C initial-branch-option symbolic-ref HEAD >actual &&\n-\tgrep hello actual\n+\ttest_grep hello actual\n '\n \n test_expect_success 'overridden default initial branch name (config)' '\n \ttest_config_global init.defaultBranch nmb &&\n \tGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME= git init initial-branch-config &&\n \tgit -C initial-branch-config symbolic-ref HEAD >actual &&\n-\tgrep nmb actual\n+\ttest_grep nmb actual\n '\n \n test_expect_success 'advice on unconfigured init.defaultBranch' '\n@@ -907,7 +907,7 @@ test_expect_success 'overridden default main branch name (env)' '\n \ttest_config_global init.defaultBranch nmb &&\n \tGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=env git init main-branch-env &&\n \tgit -C main-branch-env symbolic-ref HEAD >actual &&\n-\tgrep env actual\n+\ttest_grep env actual\n '\n \n test_expect_success 'invalid default branch name' '\ndiff --git a/t/t0008-ignores.sh b/t/t0008-ignores.sh\nindex d77a179bdd..ed95faf327 100755\n--- a/t/t0008-ignores.sh\n+++ b/t/t0008-ignores.sh\n@@ -790,8 +790,8 @@ test_expect_success 'existing file and directory' '\n \t>one &&\n \tmkdir top-level-dir &&\n \tgit check-ignore one top-level-dir >actual &&\n-\tgrep one actual &&\n-\tgrep top-level-dir actual\n+\ttest_grep one actual &&\n+\ttest_grep top-level-dir actual\n '\n \n test_expect_success 'existing directory and file' '\n@@ -800,8 +800,8 @@ test_expect_success 'existing directory and file' '\n \t>one &&\n \tmkdir top-level-dir &&\n \tgit check-ignore top-level-dir one >actual &&\n-\tgrep one actual &&\n-\tgrep top-level-dir actual\n+\ttest_grep one actual &&\n+\ttest_grep top-level-dir actual\n '\n \n test_expect_success 'exact prefix matching (with root)' '\ndiff --git a/t/t0009-git-dir-validation.sh b/t/t0009-git-dir-validation.sh\nindex 33d21ed9ea..4cba478e50 100755\n--- a/t/t0009-git-dir-validation.sh\n+++ b/t/t0009-git-dir-validation.sh\n@@ -35,7 +35,7 @@ test_expect_success PIPE 'setup: .git as a FIFO (named pipe) is rejected' '\n \t\tcd parent/fifo-trap &&\n \t\tmkfifo .git &&\n \t\ttest_must_fail git rev-parse --git-dir 2>stderr &&\n-\t\tgrep \"not a regular file\" stderr\n+\t\ttest_grep \"not a regular file\" stderr\n \t)\n '\n \n@@ -47,7 +47,7 @@ test_expect_success SYMLINKS,PIPE 'setup: .git as a symlink to a FIFO is rejecte\n \t\tmkfifo target-fifo &&\n \t\tln -s target-fifo .git &&\n \t\ttest_must_fail git rev-parse --git-dir 2>stderr &&\n-\t\tgrep \"not a regular file\" stderr\n+\t\ttest_grep \"not a regular file\" stderr\n \t)\n '\n \n@@ -58,7 +58,7 @@ test_expect_success 'setup: .git with garbage content is rejected' '\n \t\tcd parent/garbage-trap &&\n \t\techo \"garbage\" >.git &&\n \t\ttest_must_fail git rev-parse --git-dir 2>stderr &&\n-\t\tgrep \"invalid gitfile format\" stderr\n+\t\ttest_grep \"invalid gitfile format\" stderr\n \t)\n '\n \ndiff --git a/t/t0012-help.sh b/t/t0012-help.sh\nindex c33501bdcd..48d128ad04 100755\n--- a/t/t0012-help.sh\n+++ b/t/t0012-help.sh\n@@ -131,8 +131,8 @@ test_expect_success 'git help succeeds without git.html' '\n \n test_expect_success 'git help --user-interfaces' '\n \tgit help --user-interfaces >help.output &&\n-\tgrep \"^   attributes   \" help.output &&\n-\tgrep \"^   mailmap   \" help.output\n+\ttest_grep \"^   attributes   \" help.output &&\n+\ttest_grep \"^   mailmap   \" help.output\n '\n \n test_expect_success 'git help -c' '\ndiff --git a/t/t0013-sha1dc.sh b/t/t0013-sha1dc.sh\nindex ce3d81227a..3ea3169d92 100755\n--- a/t/t0013-sha1dc.sh\n+++ b/t/t0013-sha1dc.sh\n@@ -16,7 +16,7 @@ fi\n test_expect_success 'test-sha1 detects shattered pdf' '\n \ttest_must_fail test-tool sha1 <\"$TEST_DATA/shattered-1.pdf\" 2>err &&\n \ttest_grep collision err &&\n-\tgrep 38762cf7f55934b34d179ae6a4c80cadccbb7f0a err\n+\ttest_grep 38762cf7f55934b34d179ae6a4c80cadccbb7f0a err\n '\n \n test_done\ndiff --git a/t/t0017-env-helper.sh b/t/t0017-env-helper.sh\nindex 32fe848179..da02bce0ef 100755\n--- a/t/t0017-env-helper.sh\n+++ b/t/t0017-env-helper.sh\n@@ -88,7 +88,7 @@ test_expect_success 'test-tool env-helper reads config thanks to trace2' '\n \ttest_must_fail \\\n \t\tenv HOME=\"$(pwd)/home\" \\\n \t\tgit config -l 2>err &&\n-\tgrep \"exceeded maximum include depth\" err &&\n+\ttest_grep \"exceeded maximum include depth\" err &&\n \n \t# This validates that the assumption that we attempt to\n \t# read the configuration and fail very early in the start-up\n@@ -100,7 +100,7 @@ test_expect_success 'test-tool env-helper reads config thanks to trace2' '\n \t\ttest-tool -C no-such-directory \\\n \t\tenv-helper --type=bool --default=0 \\\n \t\t--exit-code GIT_TEST_ENV_HELPER 2>err &&\n-\tgrep \"exceeded maximum include depth\" err\n+\ttest_grep \"exceeded maximum include depth\" err\n '\n \n test_done\ndiff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh\nindex f0d50d769e..50994a6ada 100755\n--- a/t/t0021-conversion.sh\n+++ b/t/t0021-conversion.sh\n@@ -731,7 +731,7 @@ test_expect_success 'process filter should restart after unexpected write failur\n \t\trm -f debug.log &&\n \t\tgit checkout --quiet --no-progress . 2>git-stderr.log &&\n \n-\t\tgrep \"smudge write error\" git-stderr.log &&\n+\t\ttest_grep \"smudge write error\" git-stderr.log &&\n \t\ttest_grep \"error: external filter\" git-stderr.log &&\n \n \t\tcat >expected.log <<-EOF &&\n@@ -853,7 +853,7 @@ test_expect_success 'invalid process filter must fail (and not hang!)' '\n \n \t\tcp \"$TEST_ROOT/test.o\" test.r &&\n \t\ttest_must_fail git add . 2>git-stderr.log &&\n-\t\tgrep \"expected git-filter-server\" git-stderr.log\n+\t\ttest_grep \"expected git-filter-server\" git-stderr.log\n \t)\n '\n \n@@ -953,7 +953,7 @@ test_expect_success 'missing file in delayed checkout' '\n \n \trm -rf repo-cloned &&\n \ttest_must_fail git clone repo repo-cloned 2>git-stderr.log &&\n-\tgrep \"error: .missing-delay\\.a. was not filtered properly\" git-stderr.log\n+\ttest_grep \"error: .missing-delay\\.a. was not filtered properly\" git-stderr.log\n '\n \n test_expect_success 'invalid file in delayed checkout' '\n@@ -974,7 +974,7 @@ test_expect_success 'invalid file in delayed checkout' '\n \n \trm -rf repo-cloned &&\n \ttest_must_fail git clone repo repo-cloned 2>git-stderr.log &&\n-\tgrep \"error: external filter .* signaled that .unfiltered. is now available although it has not been delayed earlier\" git-stderr.log\n+\ttest_grep \"error: external filter .* signaled that .unfiltered. is now available although it has not been delayed earlier\" git-stderr.log\n '\n \n for mode in 'case' 'utf-8'\n@@ -1015,7 +1015,7 @@ do\n \n \t\tgit clone $mode-collision $mode-collision-cloned &&\n \t\t# Make sure z was really delayed\n-\t\tgrep \"IN: smudge $dir/z .* \\\\[DELAYED\\\\]\" $mode-collision-cloned/delayed.log &&\n+\t\ttest_grep \"IN: smudge $dir/z .* \\\\[DELAYED\\\\]\" $mode-collision-cloned/delayed.log &&\n \n \t\t# Should not create $dir/z at $symlink/z\n \t\ttest_path_is_missing $mode-collision/target-dir/z\n@@ -1053,7 +1053,7 @@ test_expect_success SYMLINKS,CASE_INSENSITIVE_FS \\\n \t\tgit commit -m super &&\n \n \t\tgit checkout --recurse-submodules . &&\n-\t\tgrep \"IN: smudge A/B/y .* \\\\[DELAYED\\\\]\" delayed.log &&\n+\t\ttest_grep \"IN: smudge A/B/y .* \\\\[DELAYED\\\\]\" delayed.log &&\n \t\ttest_path_is_missing target-dir/y\n \t)\n '\n@@ -1144,9 +1144,9 @@ test_expect_success 'delayed checkout correctly reports the number of updated en\n \n \t\trm *.a &&\n \t\tgit checkout . 2>err &&\n-\t\tgrep \"IN: smudge test-delay10.a .* \\\\[DELAYED\\\\]\" delayed.log &&\n-\t\tgrep \"IN: smudge test-delay11.a .* \\\\[DELAYED\\\\]\" delayed.log &&\n-\t\tgrep \"Updated 2 paths from the index\" err\n+\t\ttest_grep \"IN: smudge test-delay10.a .* \\\\[DELAYED\\\\]\" delayed.log &&\n+\t\ttest_grep \"IN: smudge test-delay11.a .* \\\\[DELAYED\\\\]\" delayed.log &&\n+\t\ttest_grep \"Updated 2 paths from the index\" err\n \t)\n '\n \ndiff --git a/t/t0029-core-unsetenvvars.sh b/t/t0029-core-unsetenvvars.sh\nindex baa1b7e85b..975620b389 100755\n--- a/t/t0029-core-unsetenvvars.sh\n+++ b/t/t0029-core-unsetenvvars.sh\n@@ -20,10 +20,10 @@ test_expect_success 'core.unsetenvvars works' '\n \tHOBBES=Calvin &&\n \texport HOBBES &&\n \tgit commit --allow-empty -m with 2>err &&\n-\tgrep Calvin err &&\n+\ttest_grep Calvin err &&\n \tgit -c core.unsetenvvars=FINDUS,HOBBES,CALVIN \\\n \t\tcommit --allow-empty -m without 2>err &&\n-\t! grep Calvin err\n+\ttest_grep ! Calvin err\n '\n \n test_done\ndiff --git a/t/t0030-stripspace.sh b/t/t0030-stripspace.sh\nindex 43155f6bd8..d6e2f63c59 100755\n--- a/t/t0030-stripspace.sh\n+++ b/t/t0030-stripspace.sh\n@@ -407,12 +407,12 @@ test_expect_success 'strip comments with changed comment string' '\n \n test_expect_success 'newline as commentchar is forbidden' '\n \ttest_must_fail git -c core.commentChar=\"$LF\" stripspace -s 2>err &&\n-\tgrep \"core.commentchar cannot contain newline\" err\n+\ttest_grep \"core.commentchar cannot contain newline\" err\n '\n \n test_expect_success 'empty commentchar is forbidden' '\n \ttest_must_fail git -c core.commentchar= stripspace -s 2>err &&\n-\tgrep \"core.commentchar must have at least one character\" err\n+\ttest_grep \"core.commentchar must have at least one character\" err\n '\n \n test_expect_success '-c with single line' '\ndiff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh\nindex 8ef87addf5..e9e2f04049 100755\n--- a/t/t0031-lockfile-pid.sh\n+++ b/t/t0031-lockfile-pid.sh\n@@ -29,7 +29,7 @@ test_expect_success 'PID info not shown by default' '\n \t\ttest_must_fail git add . 2>err &&\n \t\t# Should not crash, just show normal error without PID\n \t\ttest_grep \"Unable to create\" err &&\n-\t\t! test_grep \"is held by process\" err\n+\t\ttest_grep ! \"is held by process\" err\n \t)\n '\n \ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex ca55ea8228..a8348a851c 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -324,13 +324,13 @@ test_expect_success 'non ambiguous option (after two options it abbreviates)' '\n \n test_expect_success 'Alias options do not contribute to abbreviation' '\n \ttest-tool parse-options --alias-source 123 >output &&\n-\tgrep \"^string: 123\" output &&\n+\ttest_grep \"^string: 123\" output &&\n \ttest-tool parse-options --alias-target 123 >output &&\n-\tgrep \"^string: 123\" output &&\n+\ttest_grep \"^string: 123\" output &&\n \ttest_must_fail test-tool parse-options --alias &&\n \tGIT_TEST_DISALLOW_ABBREVIATED_OPTIONS=false \\\n \ttest-tool parse-options --alias 123 >output &&\n-\tgrep \"^string: 123\" output\n+\ttest_grep \"^string: 123\" output\n '\n \n cat >typo.err <<\\EOF\n@@ -582,16 +582,16 @@ test_expect_success 'KEEP_UNKNOWN_OPT works' '\n \n test_expect_success 'NO_INTERNAL_HELP works for -h' '\n \ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&\n-\tgrep \"^error: unknown switch \\`h$SQ\" err &&\n-\tgrep \"^usage: \" err\n+\ttest_grep \"^error: unknown switch \\`h$SQ\" err &&\n+\ttest_grep \"^usage: \" err\n '\n \n for help_opt in help help-all\n do\n \ttest_expect_success \"NO_INTERNAL_HELP works for --$help_opt\" \"\n \t\ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd --$help_opt 2>err &&\n-\t\tgrep '^error: unknown option \\`'$help_opt\\' err &&\n-\t\tgrep '^usage: ' err\n+\t\ttest_grep '^error: unknown option \\`'$help_opt\\' err &&\n+\t\ttest_grep '^usage: ' err\n \t\"\n done\n \n@@ -608,38 +608,38 @@ test_expect_success 'KEEP_UNKNOWN_OPT | NO_INTERNAL_HELP works' '\n \n test_expect_success 'subcommand - no subcommand shows error and usage' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd 2>err &&\n-\tgrep \"^error: need a subcommand\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: need a subcommand\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - subcommand after -- shows error and usage' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd -- subcmd-one 2>err &&\n-\tgrep \"^error: need a subcommand\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: need a subcommand\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - subcommand after --end-of-options shows error and usage' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd --end-of-options subcmd-one 2>err &&\n-\tgrep \"^error: need a subcommand\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: need a subcommand\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - unknown subcommand shows error and usage' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd nope 2>err &&\n-\tgrep \"^error: unknown subcommand: \\`nope$SQ\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: unknown subcommand: \\`nope$SQ\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - subcommands cannot be abbreviated' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd subcmd-o 2>err &&\n-\tgrep \"^error: unknown subcommand: \\`subcmd-o$SQ$\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: unknown subcommand: \\`subcmd-o$SQ$\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - no negated subcommands' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd no-subcmd-one 2>err &&\n-\tgrep \"^error: unknown subcommand: \\`no-subcmd-one$SQ\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: unknown subcommand: \\`no-subcmd-one$SQ\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - simple' '\n@@ -709,8 +709,8 @@ test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given + u\n \n test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given + unknown option' '\n \ttest_expect_code 129 test-tool parse-subcommand --subcommand-optional cmd --subcommand-opt 2>err &&\n-\tgrep \"^error: unknown option\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: unknown option\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT + subcommand not given + unknown option' '\n@@ -778,28 +778,28 @@ test_expect_success 'subcommand - completion helper' '\n \n test_expect_success 'subcommands are incompatible with STOP_AT_NON_OPTION' '\n \ttest_must_fail test-tool parse-subcommand --stop-at-non-option cmd subcmd-one 2>err &&\n-\tgrep ^BUG err\n+\ttest_grep ^BUG err\n '\n \n test_expect_success 'subcommands are incompatible with KEEP_UNKNOWN_OPT unless in combination with SUBCOMMAND_OPTIONAL' '\n \ttest_must_fail test-tool parse-subcommand --keep-unknown-opt cmd subcmd-two 2>err &&\n-\tgrep ^BUG err\n+\ttest_grep ^BUG err\n '\n \n test_expect_success 'subcommands are incompatible with KEEP_DASHDASH unless in combination with SUBCOMMAND_OPTIONAL' '\n \ttest_must_fail test-tool parse-subcommand --keep-dashdash cmd subcmd-two 2>err &&\n-\tgrep ^BUG err\n+\ttest_grep ^BUG err\n '\n \n test_expect_success 'negative unsigned' '\n \ttest_must_fail test-tool parse-options --unsigned -1 >out 2>err &&\n-\tgrep \"non-negative integer\" err &&\n+\ttest_grep \"non-negative integer\" err &&\n \ttest_must_be_empty out\n '\n \n test_expect_success 'unsigned with units but no numbers' '\n \ttest_must_fail test-tool parse-options --unsigned m >out 2>err &&\n-\tgrep \"non-negative integer\" err &&\n+\ttest_grep \"non-negative integer\" err &&\n \ttest_must_be_empty out\n '\n \ndiff --git a/t/t0041-usage.sh b/t/t0041-usage.sh\nindex a0f6f134c7..51af7cc030 100755\n--- a/t/t0041-usage.sh\n+++ b/t/t0041-usage.sh\n@@ -13,7 +13,7 @@ test_expect_success 'setup ' '\n \n test_expect_success 'tag --contains <existent_tag>' '\n \tgit tag --contains \"v1.0\" >actual 2>actual.err &&\n-\tgrep \"v1.0\" actual &&\n+\ttest_grep \"v1.0\" actual &&\n \ttest_line_count = 0 actual.err\n '\n \ndiff --git a/t/t0052-simple-ipc.sh b/t/t0052-simple-ipc.sh\nindex ff98be31a5..14cea84920 100755\n--- a/t/t0052-simple-ipc.sh\n+++ b/t/t0052-simple-ipc.sh\n@@ -33,19 +33,19 @@ test_expect_success 'servers cannot share the same path' '\n test_expect_success 'big response' '\n \ttest-tool simple-ipc send --token=big >actual &&\n \ttest_line_count -ge 10000 actual &&\n-\tgrep -q \"big: [0]*9999\\$\" actual\n+\ttest_grep -q \"big: [0]*9999\\$\" actual\n '\n \n test_expect_success 'chunk response' '\n \ttest-tool simple-ipc send --token=chunk >actual &&\n \ttest_line_count -ge 10000 actual &&\n-\tgrep -q \"big: [0]*9999\\$\" actual\n+\ttest_grep -q \"big: [0]*9999\\$\" actual\n '\n \n test_expect_success 'slow response' '\n \ttest-tool simple-ipc send --token=slow >actual &&\n \ttest_line_count -ge 100 actual &&\n-\tgrep -q \"big: [0]*99\\$\" actual\n+\ttest_grep -q \"big: [0]*99\\$\" actual\n '\n \n # Send an IPC with n=100,000 bytes of ballast.  This should be large enough\n@@ -54,7 +54,7 @@ test_expect_success 'slow response' '\n #\n test_expect_success 'sendbytes' '\n \ttest-tool simple-ipc sendbytes --bytecount=100000 --byte=A >actual &&\n-\tgrep \"sent:A00100000 rcvd:A00100000\" actual\n+\ttest_grep \"sent:A00100000 rcvd:A00100000\" actual\n '\n \n # Start a series of <threads> client threads that each make <batchsize>\n@@ -93,7 +93,7 @@ test_expect_success 'stress test threads' '\n \t\t--batchsize=13 \\\n \t\t>actual &&\n \ttest_line_count = 92 actual &&\n-\tgrep \"good 91\" actual &&\n+\ttest_grep \"good 91\" actual &&\n \tgrep \"sent:A\" <actual >actual_a &&\n \tcat >expect_a <<-EOF &&\n \t\tsent:A00000019 rcvd:A00000019\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 60cfe65979..dc9d77b0e1 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -97,7 +97,7 @@ test_expect_success POSIXPERM 'run_command reports EACCES' '\n \tchmod -x hello.sh &&\n \ttest_must_fail test-tool run-command run-command ./hello.sh 2>err &&\n \n-\tgrep \"fatal: cannot exec.*hello.sh\" err\n+\ttest_grep \"fatal: cannot exec.*hello.sh\" err\n '\n \n test_expect_success POSIXPERM,SANITY 'unreadable directory in PATH' '\ndiff --git a/t/t0066-dir-iterator.sh b/t/t0066-dir-iterator.sh\nindex df3e9f5fa5..9fbb41bac5 100755\n--- a/t/t0066-dir-iterator.sh\n+++ b/t/t0066-dir-iterator.sh\n@@ -127,7 +127,7 @@ test_expect_success SYMLINKS 'dir-iterator should not follow symlinks by default\n test_expect_success SYMLINKS 'dir-iterator does not resolve top-level symlinks' '\n \ttest_must_fail test-tool dir-iterator ./dir5 >out &&\n \n-\tgrep \"ENOTDIR\" out\n+\ttest_grep \"ENOTDIR\" out\n '\n \n test_done\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 80b163ea99..55b37d895a 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -21,23 +21,23 @@ test_expect_success 'run based on configured value' '\n \n \tgit for-each-repo --config=run.key commit --allow-empty -m \"ran\" &&\n \tgit -C one log -1 --pretty=format:%s >message &&\n-\tgrep ran message &&\n+\ttest_grep ran message &&\n \tgit -C two log -1 --pretty=format:%s >message &&\n-\t! grep ran message &&\n+\ttest_grep ! ran message &&\n \tgit -C three log -1 --pretty=format:%s >message &&\n-\tgrep ran message &&\n+\ttest_grep ran message &&\n \tgit -C ~/four log -1 --pretty=format:%s >message &&\n-\tgrep ran message &&\n+\ttest_grep ran message &&\n \n \tgit for-each-repo --config=run.key -- commit --allow-empty -m \"ran again\" &&\n \tgit -C one log -1 --pretty=format:%s >message &&\n-\tgrep again message &&\n+\ttest_grep again message &&\n \tgit -C two log -1 --pretty=format:%s >message &&\n-\t! grep again message &&\n+\ttest_grep ! again message &&\n \tgit -C three log -1 --pretty=format:%s >message &&\n-\tgrep again message &&\n+\ttest_grep again message &&\n \tgit -C ~/four log -1 --pretty=format:%s >message &&\n-\tgrep again message &&\n+\ttest_grep again message &&\n \n \tgit -C three for-each-repo --config=run.key -- \\\n \t\tcommit --allow-empty -m \"ran from worktree\" &&\ndiff --git a/t/t0070-fundamental.sh b/t/t0070-fundamental.sh\nindex 6b9dcf984b..8f573c2a0e 100755\n--- a/t/t0070-fundamental.sh\n+++ b/t/t0070-fundamental.sh\n@@ -10,7 +10,7 @@ Verify wrappers and compatibility functions.\n \n test_expect_success 'mktemp to nonexistent directory prints filename' '\n \ttest_must_fail test-tool mktemp doesnotexist/testXXXXXX 2>err &&\n-\tgrep \"doesnotexist/test\" err\n+\ttest_grep \"doesnotexist/test\" err\n '\n \n test_expect_success POSIXPERM,SANITY 'mktemp to unwritable directory prints filename' '\n@@ -18,7 +18,7 @@ test_expect_success POSIXPERM,SANITY 'mktemp to unwritable directory prints file\n \ttest_when_finished \"chmod +w cannotwrite\" &&\n \tchmod -w cannotwrite &&\n \ttest_must_fail test-tool mktemp cannotwrite/testXXXXXX 2>err &&\n-\tgrep \"cannotwrite/test\" err\n+\ttest_grep \"cannotwrite/test\" err\n '\n \n test_expect_success 'git_mkstemps_mode does not fail if fd 0 is not open' '\n@@ -33,7 +33,7 @@ test_expect_success 'check for a bug in the regex routines' '\n test_expect_success 'incomplete sideband messages are reassembled' '\n \ttest-tool pkt-line send-split-sideband >split-sideband &&\n \ttest-tool pkt-line receive-sideband <split-sideband 2>err &&\n-\tgrep \"Hello, world\" err\n+\ttest_grep \"Hello, world\" err\n '\n \n test_expect_success 'eof on sideband message is reported' '\ndiff --git a/t/t0081-find-pack.sh b/t/t0081-find-pack.sh\nindex 26f017422d..ff9d56fbf1 100755\n--- a/t/t0081-find-pack.sh\n+++ b/t/t0081-find-pack.sh\n@@ -52,17 +52,17 @@ test_expect_success 'add more packfiles' '\n \n \t# HEAD^{tree} is in 2 packfiles\n \ttest-tool find-pack HEAD^{tree} >head_tree_packs &&\n-\tgrep \"$head_commit_pack\" head_tree_packs &&\n-\tgrep mypackname1 head_tree_packs &&\n-\t! grep mypackname2 head_tree_packs &&\n+\ttest_grep \"$head_commit_pack\" head_tree_packs &&\n+\ttest_grep mypackname1 head_tree_packs &&\n+\ttest_grep ! mypackname2 head_tree_packs &&\n \ttest-tool find-pack --check-count 2 HEAD^{tree} &&\n \t! test-tool find-pack --check-count 1 HEAD^{tree} &&\n \n \t# HEAD:five.t is also in 2 packfiles\n \ttest-tool find-pack HEAD:five.t >five_packs &&\n-\tgrep \"$head_commit_pack\" five_packs &&\n-\t! grep mypackname1 five_packs &&\n-\tgrep mypackname2 five_packs &&\n+\ttest_grep \"$head_commit_pack\" five_packs &&\n+\ttest_grep ! mypackname1 five_packs &&\n+\ttest_grep mypackname2 five_packs &&\n \ttest-tool find-pack -c 2 HEAD:five.t &&\n \t! test-tool find-pack --check-count=0 HEAD:five.t\n '\ndiff --git a/t/t0091-bugreport.sh b/t/t0091-bugreport.sh\nindex e38ca7a901..81ed41742c 100755\n--- a/t/t0091-bugreport.sh\n+++ b/t/t0091-bugreport.sh\n@@ -40,15 +40,15 @@ test_expect_success 'sanity check \"System Info\" section' '\n \n \t# The beginning should match \"git version --build-options\" verbatim,\n \t# but rather than checking bit-for-bit equality, just test some basics.\n-\tgrep \"git version \" system &&\n-\tgrep \"shell-path: .\" system &&\n+\ttest_grep \"git version \" system &&\n+\ttest_grep \"shell-path: .\" system &&\n \n \t# After the version, there should be some more info.\n \t# This is bound to differ from environment to environment,\n \t# so we just do some rather high-level checks.\n-\tgrep \"uname: .\" system &&\n-\tgrep \"compiler info: .\" system &&\n-\tgrep \"zlib.\" system\n+\ttest_grep \"uname: .\" system &&\n+\ttest_grep \"compiler info: .\" system &&\n+\ttest_grep \"zlib.\" system\n '\n \n test_expect_success 'dies if file with same name as report already exists' '\n@@ -112,7 +112,7 @@ test_expect_success UNZIP '--diagnose creates diagnostics zip archive' '\n \tgit bugreport --diagnose -o report -s test >out &&\n \n \tzip_path=report/git-diagnostics-test.zip &&\n-\tgrep \"Available space\" out &&\n+\ttest_grep \"Available space\" out &&\n \ttest_path_is_file \"$zip_path\" &&\n \n \t# Check zipped archive content\n@@ -120,10 +120,10 @@ test_expect_success UNZIP '--diagnose creates diagnostics zip archive' '\n \ttest_file_not_empty out &&\n \n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t\"$GIT_UNZIP\" -p \"$zip_path\" objects-local.txt >out &&\n-\tgrep \"^Total: [0-9][0-9]*\" out &&\n+\ttest_grep \"^Total: [0-9][0-9]*\" out &&\n \n \t# Should not include .git directory contents by default\n \t! \"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\"\n@@ -136,7 +136,7 @@ test_expect_success UNZIP '--diagnose=stats excludes .git dir contents' '\n \n \t# Includes pack quantity/size info\n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t# Does not include .git directory contents\n \t! \"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\"\ndiff --git a/t/t0092-diagnose.sh b/t/t0092-diagnose.sh\nindex 6cabd6e67b..0bc2b1ed5c 100755\n--- a/t/t0092-diagnose.sh\n+++ b/t/t0092-diagnose.sh\n@@ -8,7 +8,7 @@ test_expect_success UNZIP 'creates diagnostics zip archive' '\n \ttest_when_finished rm -rf report &&\n \n \tgit diagnose -o report -s test >out &&\n-\tgrep \"Available space\" out &&\n+\ttest_grep \"Available space\" out &&\n \n \tzip_path=report/git-diagnostics-test.zip &&\n \ttest_path_is_file \"$zip_path\" &&\n@@ -18,10 +18,10 @@ test_expect_success UNZIP 'creates diagnostics zip archive' '\n \ttest_file_not_empty out &&\n \n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t\"$GIT_UNZIP\" -p \"$zip_path\" objects-local.txt >out &&\n-\tgrep \"^Total: [0-9][0-9]*\" out &&\n+\ttest_grep \"^Total: [0-9][0-9]*\" out &&\n \n \t# Should not include .git directory contents by default\n \t! \"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\"\n@@ -34,7 +34,7 @@ test_expect_success UNZIP 'counts loose objects' '\n \tgit diagnose -o test-count -s 1 >out &&\n \tzip_path=test-count/git-diagnostics-1.zip &&\n \t\"$GIT_UNZIP\" -p \"$zip_path\" objects-local.txt >out &&\n-\tgrep \"^Total: [1-9][0-9]* loose objects\" out\n+\ttest_grep \"^Total: [1-9][0-9]* loose objects\" out\n '\n \n test_expect_success UNZIP '--mode=stats excludes .git dir contents' '\n@@ -45,7 +45,7 @@ test_expect_success UNZIP '--mode=stats excludes .git dir contents' '\n \t# Includes pack quantity/size info\n \tzip_path=report/git-diagnostics-test.zip &&\n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t# Does not include .git directory contents\n \t! \"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\"\n@@ -59,7 +59,7 @@ test_expect_success UNZIP '--mode=all includes .git dir contents' '\n \t# Includes pack quantity/size info\n \tzip_path=report/git-diagnostics-test.zip &&\n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t# Includes .git directory contents\n \t\"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\" &&\ndiff --git a/t/t0100-previous.sh b/t/t0100-previous.sh\nindex dd5d9b4e5e..87a9995ea4 100755\n--- a/t/t0100-previous.sh\n+++ b/t/t0100-previous.sh\n@@ -50,7 +50,7 @@ test_expect_success 'merge @{-1}~1' '\n \tgit checkout main &&\n \tgit merge @{-1}~1 &&\n \tgit cat-file commit HEAD >actual &&\n-\tgrep \"Merge branch '\\''other'\\''\" actual\n+\ttest_grep \"Merge branch '\\''other'\\''\" actual\n '\n \n test_expect_success 'merge @{-100} before checking out that many branches yet' '\ndiff --git a/t/t0200-gettext-basic.sh b/t/t0200-gettext-basic.sh\nindex 8853d8afb9..8db26c1ada 100755\n--- a/t/t0200-gettext-basic.sh\n+++ b/t/t0200-gettext-basic.sh\n@@ -16,7 +16,7 @@ test_expect_success 'sanity: $TEXTDOMAIN is git' '\n '\n \n test_expect_success 'xgettext sanity: Perl _() strings are not extracted' '\n-    ! grep \"A Perl string xgettext will not get\" \"$GIT_PO_PATH\"/is.po\n+    test_grep ! \"A Perl string xgettext will not get\" \"$GIT_PO_PATH\"/is.po\n '\n \n test_expect_success 'xgettext sanity: Comment extraction with --add-comments' '\n@@ -26,8 +26,8 @@ test_expect_success 'xgettext sanity: Comment extraction with --add-comments' '\n '\n \n test_expect_success 'xgettext sanity: Comment extraction with --add-comments stops at statements' '\n-    ! grep \"This is a phony\" \"$GIT_PO_PATH\"/is.po &&\n-    ! grep \"the above comment\" \"$GIT_PO_PATH\"/is.po\n+    test_grep ! \"This is a phony\" \"$GIT_PO_PATH\"/is.po &&\n+    test_grep ! \"the above comment\" \"$GIT_PO_PATH\"/is.po\n '\n \n test_expect_success GETTEXT 'sanity: $TEXTDOMAINDIR exists without NO_GETTEXT=YesPlease' '\n@@ -44,10 +44,10 @@ test_expect_success GETTEXT 'sanity: Icelandic locale was compiled' '\n test_expect_success GETTEXT_LOCALE 'sanity: gettext(\"\") metadata is OK' '\n     # Return value may be non-zero\n     LANGUAGE=is LC_ALL=\"$is_IS_locale\" gettext \"\" >zero-expect &&\n-    grep \"Project-Id-Version: Git\" zero-expect &&\n-    grep \"Git Mailing List <git@vger.kernel.org>\" zero-expect &&\n-    grep \"Content-Type: text/plain; charset=UTF-8\" zero-expect &&\n-    grep \"Content-Transfer-Encoding: 8bit\" zero-expect\n+    test_grep \"Project-Id-Version: Git\" zero-expect &&\n+    test_grep \"Git Mailing List <git@vger.kernel.org>\" zero-expect &&\n+    test_grep \"Content-Type: text/plain; charset=UTF-8\" zero-expect &&\n+    test_grep \"Content-Transfer-Encoding: 8bit\" zero-expect\n '\n \n test_expect_success GETTEXT_LOCALE 'sanity: gettext(unknown) is passed through' '\ndiff --git a/t/t0203-gettext-setlocale-sanity.sh b/t/t0203-gettext-setlocale-sanity.sh\nindex 0ce1f22eff..670082bf09 100755\n--- a/t/t0203-gettext-setlocale-sanity.sh\n+++ b/t/t0203-gettext-setlocale-sanity.sh\n@@ -12,7 +12,7 @@ test_expect_success 'git show a ISO-8859-1 commit under C locale' '\n \ttest_commit \"iso-c-commit\" iso-under-c &&\n \tgit show >out 2>err &&\n \ttest_must_be_empty err &&\n-\tgrep -q \"iso-c-commit\" out\n+\ttest_grep -q \"iso-c-commit\" out\n '\n \n test_expect_success GETTEXT_LOCALE 'git show a ISO-8859-1 commit under a UTF-8 locale' '\n@@ -20,7 +20,7 @@ test_expect_success GETTEXT_LOCALE 'git show a ISO-8859-1 commit under a UTF-8 l\n \ttest_commit \"iso-utf8-commit\" iso-under-utf8 &&\n \tLANGUAGE=is LC_ALL=\"$is_IS_locale\" git show >out 2>err &&\n \ttest_must_be_empty err &&\n-\tgrep -q \"iso-utf8-commit\" out\n+\ttest_grep -q \"iso-utf8-commit\" out\n '\n \n test_done\ndiff --git a/t/t0204-gettext-reencode-sanity.sh b/t/t0204-gettext-reencode-sanity.sh\nindex 28d92bb9b7..48ccddaf7d 100755\n--- a/t/t0204-gettext-reencode-sanity.sh\n+++ b/t/t0204-gettext-reencode-sanity.sh\n@@ -66,22 +66,22 @@ test_expect_success GETTEXT_LOCALE 'gettext: Fetching a UTF-8 msgid -> UTF-8' '\n # eyes.\n test_expect_success GETTEXT_ISO_LOCALE 'gettext: Fetching a UTF-8 msgid -> ISO-8859-1' '\n     LANGUAGE=is LC_ALL=\"$is_IS_iso_locale\" gettext \"TEST: ‘single’ and “double” quotes\" >actual &&\n-    grep \"einfaldar\" actual &&\n-    grep \"$(echo tvöfaldar | iconv -f UTF-8 -t ISO8859-1)\" actual\n+    test_grep \"einfaldar\" actual &&\n+    test_grep \"$(echo tvöfaldar | iconv -f UTF-8 -t ISO8859-1)\" actual\n '\n \n test_expect_success GETTEXT_LOCALE 'gettext.c: git init UTF-8 -> UTF-8' '\n     printf \"Bjó til tóma Git lind\" >expect &&\n     LANGUAGE=is LC_ALL=\"$is_IS_locale\" git init repo >actual &&\n     test_when_finished \"rm -rf repo\" &&\n-    grep \"^$(cat expect) \" actual\n+    test_grep \"^$(cat expect) \" actual\n '\n \n test_expect_success GETTEXT_ISO_LOCALE 'gettext.c: git init UTF-8 -> ISO-8859-1' '\n     printf \"Bjó til tóma Git lind\" >expect &&\n     LANGUAGE=is LC_ALL=\"$is_IS_iso_locale\" git init repo >actual &&\n     test_when_finished \"rm -rf repo\" &&\n-    grep \"^$(iconv -f UTF-8 -t ISO8859-1 <expect) \" actual\n+    test_grep \"^$(iconv -f UTF-8 -t ISO8859-1 <expect) \" actual\n '\n \n test_done\ndiff --git a/t/t0210-trace2-normal.sh b/t/t0210-trace2-normal.sh\nindex 7e1e7af862..beed2b27a5 100755\n--- a/t/t0210-trace2-normal.sh\n+++ b/t/t0210-trace2-normal.sh\n@@ -333,12 +333,12 @@ test_expect_success 'unsafe URLs are redacted by default' '\n \n \tGIT_TRACE2=\"$(pwd)/trace.normal\" \\\n \t\tgit clone https://user:pwd@example.com/ clone &&\n-\t! grep user:pwd trace.normal &&\n+\ttest_grep ! user:pwd trace.normal &&\n \n \tGIT_TRACE2_REDACT=0 GIT_TRACE2=\"$(pwd)/unredacted.normal\" \\\n \t\tgit clone https://user:pwd@example.com/ clone2 &&\n-\tgrep \"start .* clone https://user:pwd@example.com\" unredacted.normal &&\n-\tgrep \"remote.origin.url=https://user:pwd@example.com\" unredacted.normal\n+\ttest_grep \"start .* clone https://user:pwd@example.com\" unredacted.normal &&\n+\ttest_grep \"remote.origin.url=https://user:pwd@example.com\" unredacted.normal\n '\n \n test_done\ndiff --git a/t/t0211-trace2-perf.sh b/t/t0211-trace2-perf.sh\nindex 760cf69087..665c8b3669 100755\n--- a/t/t0211-trace2-perf.sh\n+++ b/t/t0211-trace2-perf.sh\n@@ -283,13 +283,13 @@ test_expect_success 'unsafe URLs are redacted by default' '\n \n \tGIT_TRACE2_PERF=\"$(pwd)/trace.perf\" \\\n \t\tgit clone https://user:pwd@example.com/ clone &&\n-\t! grep user:pwd trace.perf &&\n+\ttest_grep ! user:pwd trace.perf &&\n \n \tGIT_TRACE2_REDACT=0 GIT_TRACE2_PERF=\"$(pwd)/unredacted.perf\" \\\n \t\tgit clone https://user:pwd@example.com/ clone2 &&\n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <unredacted.perf >actual &&\n-\tgrep \"d0|main|start|.* clone https://user:pwd@example.com\" actual &&\n-\tgrep \"d0|main|def_param|.*|remote.origin.url:https://user:pwd@example.com\" actual\n+\ttest_grep \"d0|main|start|.* clone https://user:pwd@example.com\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|remote.origin.url:https://user:pwd@example.com\" actual\n '\n \n # Confirm that the requested command produces a \"cmd_name\" and a\n@@ -358,13 +358,13 @@ test_expect_success LIBCURL \\\n \n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <prop.perf >actual &&\n \n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_\" actual &&\n-\tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n-\tgrep \"d1|main|cmd_name|.*|remote-curl\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|remote-curl\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n # Similarly, `git-http-fetch` is not built from git.c so do a\n@@ -389,13 +389,13 @@ test_expect_success LIBCURL \\\n \n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <prop.perf >actual &&\n \n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_\" actual &&\n-\tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n-\tgrep \"d1|main|cmd_name|.*|http-fetch\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|http-fetch\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n # Historically, alias expansion explicitly emitted the def_param\n@@ -421,22 +421,22 @@ test_expect_success 'expect def_params during git alias expansion' '\n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <prop.perf >actual &&\n \n \t# \"git xxx\" is first mapped to \"git-xxx\" and the child will fail.\n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n \n \t# We unpeel that and substitute \"version\" into \"xxx\" (giving\n \t# \"git version\") and update the cmd_name event.\n-\tgrep \"d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_git_alias_)\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_git_alias_)\" actual &&\n \n \t# These def_param events could be associated with either of the\n \t# above cmd_name events.  It does not matter.\n-\tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n \t# The \"git version\" child sees a different cmd_name hierarchy.\n \t# Also test the def_param (only for completeness).\n-\tgrep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_git_alias_/version)\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_git_alias_/version)\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n test_expect_success 'expect def_params during shell alias expansion' '\n@@ -456,25 +456,25 @@ test_expect_success 'expect def_params during shell alias expansion' '\n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <prop.perf >actual &&\n \n \t# \"git xxx\" is first mapped to \"git-xxx\" and the child will fail.\n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n \n \t# We unpeel that and substitute \"git version\" for \"git xxx\" (as a\n \t# shell command.  Another cmd_name event is emitted as we unpeel.\n-\tgrep \"d0|main|cmd_name|.*|_run_shell_alias_ (_run_dashed_/_run_shell_alias_)\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_shell_alias_ (_run_dashed_/_run_shell_alias_)\" actual &&\n \n \t# These def_param events could be associated with either of the\n \t# above cmd_name events.  It does not matter.\n-\tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n \t# We get the following only because we used a git command for the\n \t# shell command. In general, it could have been a shell script and\n \t# we would see nothing.\n \t#\n \t# The child knows the cmd_name hierarchy so it includes it.\n-\tgrep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_shell_alias_/version)\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_shell_alias_/version)\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n test_expect_success 'expect def_params during nested git alias expansion' '\n@@ -496,33 +496,33 @@ test_expect_success 'expect def_params during nested git alias expansion' '\n \n \t# \"git xxx\" is first mapped to \"git-xxx\" and try to spawn \"git-xxx\"\n \t# and the child will fail.\n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n-\tgrep \"d0|main|child_start|.*|.* class:dashed argv:\\[git-xxx\\]\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n+\ttest_grep \"d0|main|child_start|.*|.* class:dashed argv:\\[git-xxx\\]\" actual &&\n \n \t# We unpeel that and substitute \"yyy\" into \"xxx\" (giving \"git yyy\")\n \t# and spawn \"git-yyy\" and the child will fail.\n-\tgrep \"d0|main|alias|.*|alias:xxx argv:\\[yyy\\]\" actual &&\n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_/_run_dashed_)\" actual &&\n-\tgrep \"d0|main|child_start|.*|.* class:dashed argv:\\[git-yyy\\]\" actual &&\n+\ttest_grep \"d0|main|alias|.*|alias:xxx argv:\\[yyy\\]\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_/_run_dashed_)\" actual &&\n+\ttest_grep \"d0|main|child_start|.*|.* class:dashed argv:\\[git-yyy\\]\" actual &&\n \n \t# We unpeel that and substitute \"version\" into \"xxx\" (giving\n \t# \"git version\") and update the cmd_name event.\n-\tgrep \"d0|main|alias|.*|alias:yyy argv:\\[version\\]\" actual &&\n-\tgrep \"d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_dashed_/_run_git_alias_)\" actual &&\n+\ttest_grep \"d0|main|alias|.*|alias:yyy argv:\\[version\\]\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_dashed_/_run_git_alias_)\" actual &&\n \n \t# These def_param events could be associated with any of the\n \t# above cmd_name events.  It does not matter.\n \tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual >actual.matches &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n \t# However, we do not want them repeated each time we unpeel.\n \ttest_line_count = 1 actual.matches &&\n \n \t# The \"git version\" child sees a different cmd_name hierarchy.\n \t# Also test the def_param (only for completeness).\n-\tgrep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_dashed_/_run_git_alias_/version)\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_dashed_/_run_git_alias_/version)\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n test_done\ndiff --git a/t/t0212-trace2-event.sh b/t/t0212-trace2-event.sh\nindex 1211db9f46..f5358a1dd4 100755\n--- a/t/t0212-trace2-event.sh\n+++ b/t/t0212-trace2-event.sh\n@@ -332,7 +332,7 @@ test_expect_success 'unsafe URLs are redacted by default in cmd_start events' '\n \n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.event\" \\\n \t\ttest-tool trace2 300redact_start git clone https://user:pwd@example.com/ clone2 &&\n-\t! grep user:pwd trace.event\n+\ttest_grep ! user:pwd trace.event\n '\n \n test_expect_success 'unsafe URLs are redacted by default in child_start events' '\n@@ -341,7 +341,7 @@ test_expect_success 'unsafe URLs are redacted by default in child_start events'\n \n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.event\" \\\n \t\ttest-tool trace2 301redact_child_start git clone https://user:pwd@example.com/ clone2 &&\n-\t! grep user:pwd trace.event\n+\ttest_grep ! user:pwd trace.event\n '\n \n test_expect_success 'unsafe URLs are redacted by default in exec events' '\n@@ -350,7 +350,7 @@ test_expect_success 'unsafe URLs are redacted by default in exec events' '\n \n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.event\" \\\n \t\ttest-tool trace2 302redact_exec git clone https://user:pwd@example.com/ clone2 &&\n-\t! grep user:pwd trace.event\n+\ttest_grep ! user:pwd trace.event\n '\n \n test_expect_success 'unsafe URLs are redacted by default in def_param events' '\n@@ -359,7 +359,7 @@ test_expect_success 'unsafe URLs are redacted by default in def_param events' '\n \n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.event\" \\\n \t\ttest-tool trace2 303redact_def_param url https://user:pwd@example.com/ &&\n-\t! grep user:pwd trace.event\n+\ttest_grep ! user:pwd trace.event\n '\n \n test_done\ndiff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh\nindex 64ead1571a..ea11cdb4dc 100755\n--- a/t/t0300-credentials.sh\n+++ b/t/t0300-credentials.sh\n@@ -1014,7 +1014,7 @@ test_expect_success 'credential config with partial URLs' '\n \tdo\n \t\tgit -c credential.$partial.helper=yep \\\n \t\t\tcredential fill <stdin >stdout &&\n-\t\tgrep yep stdout ||\n+\t\ttest_grep yep stdout ||\n \t\treturn 1\n \tdone &&\n \n@@ -1030,7 +1030,7 @@ test_expect_success 'credential config with partial URLs' '\n \tdo\n \t\tgit -c credential.$partial.helper=yep \\\n \t\t\tcredential fill <stdin >stdout &&\n-\t\t! grep yep stdout ||\n+\t\ttest_grep ! yep stdout ||\n \t\treturn 1\n \tdone &&\n \ndiff --git a/t/t0410-partial-clone.sh b/t/t0410-partial-clone.sh\nindex dff442da20..dba4246100 100755\n--- a/t/t0410-partial-clone.sh\n+++ b/t/t0410-partial-clone.sh\n@@ -206,7 +206,7 @@ test_expect_success 'fetching of missing objects' '\n \tgit -C repo cat-file -p \"$HASH\" 2>err &&\n \n \t# Ensure that no spurious FETCH_HEAD messages are written\n-\t! grep FETCH_HEAD err &&\n+\ttest_grep ! FETCH_HEAD err &&\n \n \t# Ensure that the .promisor file is written, and check that its\n \t# associated packfile contains the object\n@@ -214,7 +214,7 @@ test_expect_success 'fetching of missing objects' '\n \ttest_line_count = 1 promisorlist &&\n \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n \tgit verify-pack --verbose \"$IDX\" >out &&\n-\tgrep \"$HASH\" out\n+\ttest_grep \"$HASH\" out\n '\n \n test_expect_success 'fetching of a promised object that promisor remote no longer has' '\n@@ -228,7 +228,7 @@ test_expect_success 'fetching of a promised object that promisor remote no longe\n \n \trm -rf unreliable-server/.git/objects/* &&\n \ttest_must_fail git -C unreliable-client checkout HEAD 2>err &&\n-\tgrep \"could not fetch.*from promisor remote\" err\n+\ttest_grep \"could not fetch.*from promisor remote\" err\n '\n \n test_expect_success 'fetching of missing objects works with ref-in-want enabled' '\n@@ -240,7 +240,7 @@ test_expect_success 'fetching of missing objects works with ref-in-want enabled'\n \trm -rf repo/.git/objects/* &&\n \trm -f trace &&\n \tGIT_TRACE_PACKET=\"$(pwd)/trace\" git -C repo cat-file -p \"$HASH\" &&\n-\tgrep \"fetch< fetch=.*ref-in-want\" trace\n+\ttest_grep \"fetch< fetch=.*ref-in-want\" trace\n '\n \n test_expect_success 'fetching from another promisor remote' '\n@@ -263,7 +263,7 @@ test_expect_success 'fetching from another promisor remote' '\n \ttest_line_count = 1 promisorlist &&\n \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n \tgit verify-pack --verbose \"$IDX\" >out &&\n-\tgrep \"$HASH2\" out\n+\ttest_grep \"$HASH2\" out\n '\n \n test_expect_success 'fetching with --filter configures a promisor remote' '\n@@ -286,7 +286,7 @@ test_expect_success 'fetching with --filter configures a promisor remote' '\n \ttest_line_count = 1 promisorlist &&\n \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n \tgit verify-pack --verbose \"$IDX\" >out &&\n-\tgrep \"$HASH3\" out\n+\ttest_grep \"$HASH3\" out\n '\n \n test_expect_success 'fetching of missing blobs works' '\n@@ -327,8 +327,8 @@ test_expect_success 'fetching of missing trees does not fetch blobs' '\n \n \t# Ensure that the tree, but not the blob, is fetched\n \tgit -C repo rev-list --objects --missing=print $(cat treehash) >objects &&\n-\tgrep \"^$(cat treehash)\" objects &&\n-\tgrep \"^[?]$(cat blobhash)\" objects\n+\ttest_grep \"^$(cat treehash)\" objects &&\n+\ttest_grep \"^[?]$(cat blobhash)\" objects\n '\n \n test_expect_success 'rev-list stops traversal at missing and promised commit' '\n@@ -343,8 +343,8 @@ test_expect_success 'rev-list stops traversal at missing and promised commit' '\n \tgit -C repo config core.repositoryformatversion 1 &&\n \tgit -C repo config extensions.partialclone \"arbitrary string\" &&\n \tgit -C repo rev-list --exclude-promisor-objects --objects bar >out &&\n-\tgrep $(git -C repo rev-parse bar) out &&\n-\t! grep $FOO out\n+\ttest_grep $(git -C repo rev-parse bar) out &&\n+\ttest_grep ! $FOO out\n '\n \n test_expect_success 'missing tree objects with --missing=allow-promisor and --exclude-promisor-objects' '\n@@ -413,10 +413,10 @@ test_expect_success 'rev-list stops traversal at missing and promised tree' '\n \tgit -C repo config core.repositoryformatversion 1 &&\n \tgit -C repo config extensions.partialclone \"arbitrary string\" &&\n \tgit -C repo rev-list --exclude-promisor-objects --objects HEAD >out &&\n-\tgrep $(git -C repo rev-parse foo) out &&\n-\t! grep $TREE out &&\n-\tgrep $(git -C repo rev-parse HEAD) out &&\n-\t! grep $TREE2 out\n+\ttest_grep $(git -C repo rev-parse foo) out &&\n+\ttest_grep ! $TREE out &&\n+\ttest_grep $(git -C repo rev-parse HEAD) out &&\n+\ttest_grep ! $TREE2 out\n '\n \n test_expect_success 'rev-list stops traversal at missing and promised blob' '\n@@ -432,8 +432,8 @@ test_expect_success 'rev-list stops traversal at missing and promised blob' '\n \tgit -C repo config core.repositoryformatversion 1 &&\n \tgit -C repo config extensions.partialclone \"arbitrary string\" &&\n \tgit -C repo rev-list --exclude-promisor-objects --objects HEAD >out &&\n-\tgrep $(git -C repo rev-parse HEAD) out &&\n-\t! grep $BLOB out\n+\ttest_grep $(git -C repo rev-parse HEAD) out &&\n+\ttest_grep ! $BLOB out\n '\n \n test_expect_success 'rev-list stops traversal at promisor commit, tree, and blob' '\n@@ -451,10 +451,10 @@ test_expect_success 'rev-list stops traversal at promisor commit, tree, and blob\n \tgit -C repo config core.repositoryformatversion 1 &&\n \tgit -C repo config extensions.partialclone \"arbitrary string\" &&\n \tgit -C repo rev-list --exclude-promisor-objects --objects HEAD >out &&\n-\t! grep $COMMIT out &&\n-\t! grep $TREE out &&\n-\t! grep $BLOB out &&\n-\tgrep $(git -C repo rev-parse bar) out  # sanity check that some walking was done\n+\ttest_grep ! $COMMIT out &&\n+\ttest_grep ! $TREE out &&\n+\ttest_grep ! $BLOB out &&\n+\ttest_grep $(git -C repo rev-parse bar) out  # sanity check that some walking was done\n '\n \n test_expect_success 'rev-list dies for missing objects on cmd line' '\n@@ -523,10 +523,10 @@ test_expect_success 'gc repacks promisor objects separately from non-promisor ob\n \ttest_line_count = 1 promisorlist &&\n \tPROMISOR_PACKFILE=$(sed \"s/.promisor/.pack/\" <promisorlist) &&\n \tgit verify-pack $PROMISOR_PACKFILE -v >out &&\n-\tgrep \"$TREE_ONE\" out &&\n-\tgrep \"$TREE_TWO\" out &&\n-\t! grep \"$(git -C repo rev-parse one)\" out &&\n-\t! grep \"$(git -C repo rev-parse two)\" out &&\n+\ttest_grep \"$TREE_ONE\" out &&\n+\ttest_grep \"$TREE_TWO\" out &&\n+\ttest_grep ! \"$(git -C repo rev-parse one)\" out &&\n+\ttest_grep ! \"$(git -C repo rev-parse two)\" out &&\n \n \t# Remove the promisor packfile and associated files\n \trm $(sed \"s/.promisor//\" <promisorlist).* &&\n@@ -536,10 +536,10 @@ test_expect_success 'gc repacks promisor objects separately from non-promisor ob\n \tls repo/.git/objects/pack/pack-*.pack >packlist &&\n \ttest_line_count = 1 packlist &&\n \tgit verify-pack repo/.git/objects/pack/pack-*.pack -v >out &&\n-\tgrep \"$(git -C repo rev-parse one)\" out &&\n-\tgrep \"$(git -C repo rev-parse two)\" out &&\n-\t! grep \"$TREE_ONE\" out &&\n-\t! grep \"$TREE_TWO\" out\n+\ttest_grep \"$(git -C repo rev-parse one)\" out &&\n+\ttest_grep \"$(git -C repo rev-parse two)\" out &&\n+\ttest_grep ! \"$TREE_ONE\" out &&\n+\ttest_grep ! \"$TREE_TWO\" out\n '\n \n test_expect_success 'gc does not repack promisor objects if there are none' '\n@@ -616,8 +616,8 @@ test_expect_success 'gc stops traversal when a missing but promised object is re\n \tls repo/.git/objects/pack/pack-*.pack >packlist &&\n \ttest_line_count = 1 packlist &&\n \tgit verify-pack repo/.git/objects/pack/pack-*.pack -v >out &&\n-\tgrep \"$(git -C repo rev-parse HEAD)\" out &&\n-\t! grep \"$TREE_HASH\" out\n+\ttest_grep \"$(git -C repo rev-parse HEAD)\" out &&\n+\ttest_grep ! \"$TREE_HASH\" out\n '\n \n test_expect_success 'do not fetch when checking existence of tree we construct ourselves' '\n@@ -647,10 +647,10 @@ test_expect_success 'exact rename does not need to fetch the blob lazily' '\n \n \tgit clone --filter=blob:none --bare \"file://$(pwd)/repo\" partial.git &&\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\tgrep \"[?]$FILE_HASH\" out &&\n+\ttest_grep \"[?]$FILE_HASH\" out &&\n \tgit -C partial.git log --follow -- new-file.txt &&\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\tgrep \"[?]$FILE_HASH\" out\n+\ttest_grep \"[?]$FILE_HASH\" out\n '\n \n test_expect_success 'lazy-fetch when accessing object not in the_repository' '\n@@ -665,7 +665,7 @@ test_expect_success 'lazy-fetch when accessing object not in the_repository' '\n \n \t# Sanity check that the file is missing\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\tgrep \"[?]$FILE_HASH\" out &&\n+\ttest_grep \"[?]$FILE_HASH\" out &&\n \n \t# The no-lazy-fetch mechanism prevents Git from fetching\n \ttest_must_fail env GIT_NO_LAZY_FETCH=1 \\\n@@ -680,7 +680,7 @@ test_expect_success 'lazy-fetch when accessing object not in the_repository' '\n \n \t# Sanity check that the file is still missing\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\tgrep \"[?]$FILE_HASH\" out &&\n+\ttest_grep \"[?]$FILE_HASH\" out &&\n \n \tgit -C full cat-file -s \"$FILE_HASH\" >expect &&\n \ttest-tool partial-clone object-info partial.git \"$FILE_HASH\" >actual &&\n@@ -688,7 +688,7 @@ test_expect_success 'lazy-fetch when accessing object not in the_repository' '\n \n \t# Sanity check that the file is now present\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\t! grep \"[?]$FILE_HASH\" out\n+\ttest_grep ! \"[?]$FILE_HASH\" out\n '\n \n test_expect_success 'push should not fetch new commit objects' '\n@@ -705,9 +705,9 @@ test_expect_success 'push should not fetch new commit objects' '\n \tCOMMIT=$(git -C server rev-parse server2) &&\n \n \ttest_must_fail git -C client push 2>err &&\n-\tgrep \"fetch first\" err &&\n+\ttest_grep \"fetch first\" err &&\n \tgit -C client rev-list --objects --missing=print \"$COMMIT\" >objects &&\n-\tgrep \"^[?]$COMMIT\" objects\n+\ttest_grep \"^[?]$COMMIT\" objects\n '\n \n test_expect_success 'setup for promisor.quiet tests' '\n@@ -750,7 +750,7 @@ test_expect_success TTY 'promisor.quiet=false shows progress messages' '\n \ttest_terminal git -C repo cat-file -p foo:foo.t 2>err &&\n \n \t# Ensure that progress messages are written\n-\tgrep \"Receiving objects\" err\n+\ttest_grep \"Receiving objects\" err\n '\n \n test_expect_success TTY 'promisor.quiet=true does not show progress messages' '\n@@ -761,7 +761,7 @@ test_expect_success TTY 'promisor.quiet=true does not show progress messages' '\n \ttest_terminal git -C repo cat-file -p foo:foo.t 2>err &&\n \n \t# Ensure that no progress messages are written\n-\t! grep \"Receiving objects\" err\n+\ttest_grep ! \"Receiving objects\" err\n '\n \n test_expect_success TTY 'promisor.quiet=unconfigured shows progress messages' '\n@@ -771,7 +771,7 @@ test_expect_success TTY 'promisor.quiet=unconfigured shows progress messages' '\n \ttest_terminal git -C repo cat-file -p foo:foo.t 2>err &&\n \n \t# Ensure that progress messages are written\n-\tgrep \"Receiving objects\" err\n+\ttest_grep \"Receiving objects\" err\n '\n \n test_expect_success 'promisor.quiet from submodule repo is honored' '\n@@ -819,7 +819,7 @@ test_expect_success 'fetching of missing objects from an HTTP server' '\n \ttest_line_count = 1 promisorlist &&\n \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n \tgit verify-pack --verbose \"$IDX\" >out &&\n-\tgrep \"$HASH\" out\n+\ttest_grep \"$HASH\" out\n '\n \n # DO NOT add non-httpd-specific tests here, because the last part of this\ndiff --git a/t/t0450-txt-doc-vs-help.sh b/t/t0450-txt-doc-vs-help.sh\nindex 822b0d55a5..e7ba967223 100755\n--- a/t/t0450-txt-doc-vs-help.sh\n+++ b/t/t0450-txt-doc-vs-help.sh\n@@ -87,7 +87,7 @@ do\n \t# -h output assertions\n \ttest_expect_success \"$builtin -h output has no \\t\" '\n \t\th2s=\"$(help_to_synopsis \"$builtin\")\" &&\n-\t\t! grep \"$HT\" \"$h2s\"\n+\t\ttest_grep ! \"$HT\" \"$h2s\"\n \t'\n \n \ttest_expect_success \"$builtin -h output has dashed labels\" '\ndiff --git a/t/t0500-progress-display.sh b/t/t0500-progress-display.sh\nindex d1a498a216..fe2232cce0 100755\n--- a/t/t0500-progress-display.sh\n+++ b/t/t0500-progress-display.sh\n@@ -320,8 +320,8 @@ test_expect_success 'progress generates traces' '\n \n \t# t0212/parse_events.perl intentionally omits regions and data.\n \ttest_region progress \"Working hard\" trace.event &&\n-\tgrep \"\\\"key\\\":\\\"total_objects\\\",\\\"value\\\":\\\"40\\\"\" trace.event &&\n-\tgrep \"\\\"key\\\":\\\"total_bytes\\\",\\\"value\\\":\\\"409600\\\"\" trace.event\n+\ttest_grep \"\\\"key\\\":\\\"total_objects\\\",\\\"value\\\":\\\"40\\\"\" trace.event &&\n+\ttest_grep \"\\\"key\\\":\\\"total_bytes\\\",\\\"value\\\":\\\"409600\\\"\" trace.event\n '\n \n test_expect_success 'progress generates traces: stop / start' '\n@@ -344,8 +344,8 @@ test_expect_success 'progress generates traces: start without stop' '\n \tLSAN_OPTIONS=detect_leaks=0 \\\n \ttest-tool progress \\\n \t\t<in 2>stderr &&\n-\tgrep region_enter.*progress trace-start.event &&\n-\t! grep region_leave.*progress trace-start.event\n+\ttest_grep region_enter.*progress trace-start.event &&\n+\ttest_grep ! region_leave.*progress trace-start.event\n '\n \n test_expect_success 'progress generates traces: stop without start' '\n@@ -355,8 +355,8 @@ test_expect_success 'progress generates traces: stop without start' '\n \n \tGIT_TRACE2_EVENT=\"$PWD/trace-stop.event\" test-tool progress \\\n \t\t<in 2>stderr &&\n-\t! grep region_enter.*progress trace-stop.event &&\n-\t! grep region_leave.*progress trace-stop.event\n+\ttest_grep ! region_enter.*progress trace-stop.event &&\n+\ttest_grep ! region_leave.*progress trace-stop.event\n '\n \n test_expect_success 'progress generates traces: start with active progress bar (no stops)' '\n@@ -369,9 +369,9 @@ test_expect_success 'progress generates traces: start with active progress bar (\n \tLSAN_OPTIONS=detect_leaks=0 \\\n \ttest-tool progress \\\n \t\t<in 2>stderr &&\n-\tgrep region_enter.*progress.*One trace-2start.event &&\n-\tgrep region_enter.*progress.*Two trace-2start.event &&\n-\t! grep region_leave trace-2start.event\n+\ttest_grep region_enter.*progress.*One trace-2start.event &&\n+\ttest_grep region_enter.*progress.*Two trace-2start.event &&\n+\ttest_grep ! region_leave trace-2start.event\n '\n \n test_done\ndiff --git a/t/t0610-reftable-basics.sh b/t/t0610-reftable-basics.sh\nindex e19e036898..2f4c5e2a1b 100755\n--- a/t/t0610-reftable-basics.sh\n+++ b/t/t0610-reftable-basics.sh\n@@ -776,11 +776,11 @@ test_expect_success 'reflog: can delete separate reflog entries' '\n \t\ttest_commit file3 &&\n \t\ttest_commit file4 &&\n \t\tgit reflog >actual &&\n-\t\tgrep file3 actual &&\n+\t\ttest_grep file3 actual &&\n \n \t\tgit reflog delete HEAD@{1} &&\n \t\tgit reflog >actual &&\n-\t\t! grep file3 actual\n+\t\ttest_grep ! file3 actual\n \t)\n '\n \n@@ -902,8 +902,8 @@ test_expect_success 'reflog: garbage collection deletes reflog entries' '\n \t\tdone &&\n \t\tgit reflog refs/heads/main >actual &&\n \t\ttest_line_count = 10 actual &&\n-\t\tgrep \"commit (initial): number 1\" actual &&\n-\t\tgrep \"commit: number 10\" actual &&\n+\t\ttest_grep \"commit (initial): number 1\" actual &&\n+\t\ttest_grep \"commit: number 10\" actual &&\n \n \t\tgit gc &&\n \t\tgit reflog refs/heads/main >actual &&\ndiff --git a/t/t1004-read-tree-m-u-wf.sh b/t/t1004-read-tree-m-u-wf.sh\nindex 11bf10424f..70ba3eea4d 100755\n--- a/t/t1004-read-tree-m-u-wf.sh\n+++ b/t/t1004-read-tree-m-u-wf.sh\n@@ -142,8 +142,8 @@ test_expect_success '3-way not overwriting local changes (our side)' '\n \n \techo >>file1 \"local changes\" &&\n \tread_tree_u_must_succeed -m -u branch-point side-a side-b &&\n-\tgrep \"new line to be kept\" file1 &&\n-\tgrep \"local changes\" file1\n+\ttest_grep \"new line to be kept\" file1 &&\n+\ttest_grep \"local changes\" file1\n \n '\n \n@@ -156,8 +156,8 @@ test_expect_success '3-way not overwriting local changes (their side)' '\n \n \techo >>file2 \"local changes\" &&\n \tread_tree_u_must_fail -m -u branch-point side-a side-b &&\n-\t! grep \"new line to be kept\" file2 &&\n-\tgrep \"local changes\" file2\n+\ttest_grep ! \"new line to be kept\" file2 &&\n+\ttest_grep \"local changes\" file2\n \n '\n \ndiff --git a/t/t1006-cat-file.sh b/t/t1006-cat-file.sh\nindex 8e2c52652c..762c77c351 100755\n--- a/t/t1006-cat-file.sh\n+++ b/t/t1006-cat-file.sh\n@@ -696,8 +696,8 @@ test_expect_success '%(deltabase) reports packed delta bases' '\n \tgit repack -ad &&\n \tgit cat-file --batch-check=\"%(deltabase)\" <blobs >actual &&\n \t{\n-\t\tgrep \"$(git rev-parse HEAD:foo)\" actual ||\n-\t\tgrep \"$(git rev-parse HEAD:foo-plus)\" actual\n+\t\ttest_grep \"$(git rev-parse HEAD:foo)\" actual ||\n+\t\ttest_grep \"$(git rev-parse HEAD:foo-plus)\" actual\n \t}\n '\n \n@@ -826,7 +826,7 @@ test_expect_success 'cat-file -t and -s on corrupt loose object' '\n \t\t# Swap the two to corrupt the repository\n \t\tmv -f \"$other_path\" \"$empty_path\" &&\n \t\ttest_must_fail git fsck 2>err.fsck &&\n-\t\tgrep \"hash-path mismatch\" err.fsck &&\n+\t\ttest_grep \"hash-path mismatch\" err.fsck &&\n \n \t\t# confirm that cat-file is reading the new swapped-in\n \t\t# blob...\n@@ -1318,37 +1318,37 @@ test_expect_success 'cat-file --batch-all-objects --batch-check ignores replace'\n test_expect_success 'batch-command empty command' '\n \techo \"\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*empty command in input.*\" err\n+\ttest_grep \"^fatal:.*empty command in input.*\" err\n '\n \n test_expect_success 'batch-command whitespace before command' '\n \techo \" info deadbeef\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*whitespace before command.*\" err\n+\ttest_grep \"^fatal:.*whitespace before command.*\" err\n '\n \n test_expect_success 'batch-command unknown command' '\n \techo unknown_command >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*unknown command.*\" err\n+\ttest_grep \"^fatal:.*unknown command.*\" err\n '\n \n test_expect_success 'batch-command missing arguments' '\n \techo \"info\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*info requires arguments.*\" err\n+\ttest_grep \"^fatal:.*info requires arguments.*\" err\n '\n \n test_expect_success 'batch-command flush with arguments' '\n \techo \"flush arg\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command --buffer <cmd 2>err &&\n-\tgrep \"^fatal:.*flush takes no arguments.*\" err\n+\ttest_grep \"^fatal:.*flush takes no arguments.*\" err\n '\n \n test_expect_success 'batch-command flush without --buffer' '\n \techo \"flush\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*flush is only for --buffer mode.*\" err\n+\ttest_grep \"^fatal:.*flush is only for --buffer mode.*\" err\n '\n \n perl_script='\ndiff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh\nindex de076293b6..4782a6246a 100755\n--- a/t/t1007-hash-object.sh\n+++ b/t/t1007-hash-object.sh\n@@ -202,7 +202,7 @@ done\n test_expect_success 'too-short tree' '\n \techo abc >malformed-tree &&\n \ttest_must_fail git hash-object -t tree malformed-tree 2>err &&\n-\tgrep \"too-short tree object\" err\n+\ttest_grep \"too-short tree object\" err\n '\n \n test_expect_success PERL_TEST_HELPERS 'malformed mode in tree' '\n@@ -210,7 +210,7 @@ test_expect_success PERL_TEST_HELPERS 'malformed mode in tree' '\n \tbin_oid=$(echo $hex_oid | hex2oct) &&\n \tprintf \"9100644 \\0$bin_oid\" >tree-with-malformed-mode &&\n \ttest_must_fail git hash-object -t tree tree-with-malformed-mode 2>err &&\n-\tgrep \"malformed mode in tree entry\" err\n+\ttest_grep \"malformed mode in tree entry\" err\n '\n \n test_expect_success PERL_TEST_HELPERS 'empty filename in tree' '\n@@ -218,7 +218,7 @@ test_expect_success PERL_TEST_HELPERS 'empty filename in tree' '\n \tbin_oid=$(echo $hex_oid | hex2oct) &&\n \tprintf \"100644 \\0$bin_oid\" >tree-with-empty-filename &&\n \ttest_must_fail git hash-object -t tree tree-with-empty-filename 2>err &&\n-\tgrep \"empty filename in tree entry\" err\n+\ttest_grep \"empty filename in tree entry\" err\n '\n \n test_expect_success PERL_TEST_HELPERS 'duplicate filename in tree' '\n@@ -229,7 +229,7 @@ test_expect_success PERL_TEST_HELPERS 'duplicate filename in tree' '\n \t\tprintf \"100644 file\\0$bin_oid\"\n \t} >tree-with-duplicate-filename &&\n \ttest_must_fail git hash-object -t tree tree-with-duplicate-filename 2>err &&\n-\tgrep \"duplicateEntries\" err\n+\ttest_grep \"duplicateEntries\" err\n '\n \n test_expect_success 'corrupt commit' '\ndiff --git a/t/t1011-read-tree-sparse-checkout.sh b/t/t1011-read-tree-sparse-checkout.sh\nindex 742f0fa909..93244ee134 100755\n--- a/t/t1011-read-tree-sparse-checkout.sh\n+++ b/t/t1011-read-tree-sparse-checkout.sh\n@@ -196,7 +196,7 @@ test_expect_success 'read-tree will not throw away dirty changes, non-sparse' '\n \techo dirty >init.t &&\n \tread_tree_u_must_fail -m -u HEAD^ &&\n \ttest_path_is_file init.t &&\n-\tgrep -q dirty init.t\n+\ttest_grep -q dirty init.t\n '\n \n test_expect_success 'read-tree will not throw away dirty changes, sparse' '\n@@ -207,7 +207,7 @@ test_expect_success 'read-tree will not throw away dirty changes, sparse' '\n \techo sub/added >.git/info/sparse-checkout &&\n \tread_tree_u_must_fail -m -u HEAD^ &&\n \ttest_path_is_file init.t &&\n-\tgrep -q dirty init.t\n+\ttest_grep -q dirty init.t\n '\n \n test_expect_success 'read-tree updates worktree, dirty case' '\n@@ -215,7 +215,7 @@ test_expect_success 'read-tree updates worktree, dirty case' '\n \tgit checkout -f top &&\n \techo dirty >init.t &&\n \tread_tree_u_must_fail -m -u HEAD^ &&\n-\tgrep -q dirty init.t &&\n+\ttest_grep -q dirty init.t &&\n \trm init.t\n '\n \n@@ -224,7 +224,7 @@ test_expect_success 'read-tree removes worktree, dirty case' '\n \tgit checkout -f top &&\n \techo dirty >added &&\n \tread_tree_u_must_succeed -m -u HEAD^ &&\n-\tgrep -q dirty added\n+\ttest_grep -q dirty added\n '\n \n test_expect_success 'read-tree adds to worktree, absent case' '\n@@ -240,7 +240,7 @@ test_expect_success 'read-tree adds to worktree, dirty case' '\n \tmkdir sub &&\n \techo dirty >sub/added &&\n \tread_tree_u_must_succeed -u -m HEAD^ &&\n-\tgrep -q dirty sub/added\n+\ttest_grep -q dirty sub/added\n '\n \n test_expect_success 'index removal and worktree narrowing at the same time' '\ndiff --git a/t/t1050-large.sh b/t/t1050-large.sh\nindex 7d40d08521..d295c265c7 100755\n--- a/t/t1050-large.sh\n+++ b/t/t1050-large.sh\n@@ -8,7 +8,7 @@ test_description='adding and checking out large blobs'\n test_expect_success 'core.bigFileThreshold must be non-negative' '\n \t: >input &&\n \ttest_must_fail git -c core.bigFileThreshold=-1 hash-object input >out 2>err &&\n-\tgrep \"bad numeric config value\" err &&\n+\ttest_grep \"bad numeric config value\" err &&\n \ttest_must_be_empty out\n '\n \n@@ -148,12 +148,12 @@ test_expect_success 'diff --stat' '\n \n test_expect_success 'diff' '\n \tgit diff HEAD^ HEAD >actual &&\n-\tgrep \"Binary files.*differ\" actual\n+\ttest_grep \"Binary files.*differ\" actual\n '\n \n test_expect_success 'diff --cached' '\n \tgit diff --cached HEAD^ >actual &&\n-\tgrep \"Binary files.*differ\" actual\n+\ttest_grep \"Binary files.*differ\" actual\n '\n \n test_expect_success 'hash-object' '\ndiff --git a/t/t1091-sparse-checkout-builtin.sh b/t/t1091-sparse-checkout-builtin.sh\nindex cd0aed9975..74b1761e0c 100755\n--- a/t/t1091-sparse-checkout-builtin.sh\n+++ b/t/t1091-sparse-checkout-builtin.sh\n@@ -129,7 +129,7 @@ test_expect_success 'switching to cone mode with non-cone mode patterns' '\n \t\tgit sparse-checkout add dir &&\n \t\tgit config --worktree core.sparseCheckoutCone true &&\n \t\ttest_must_fail git sparse-checkout add dir 2>err &&\n-\t\tgrep \"existing sparse-checkout patterns do not use cone mode\" err\n+\t\ttest_grep \"existing sparse-checkout patterns do not use cone mode\" err\n \t)\n '\n \n@@ -803,7 +803,7 @@ test_expect_success 'cone mode clears ignored subdirectories' '\n \t# When an untracked file is in the way, all untracked files\n \t# (even ignored files) are preserved.\n \tgit -C repo sparse-checkout set folder1 2>err &&\n-\tgrep \"contains untracked files\" err &&\n+\ttest_grep \"contains untracked files\" err &&\n \ttest_path_is_file repo/deep/deeper2/ignored.o &&\n \ttest_path_is_file repo/deep/deeper2/untracked &&\n \n@@ -882,8 +882,8 @@ test_expect_success 'malformed cone-mode patterns' '\n \t# of using the cone-mode translation to a set of directories.\n \tgit -C repo sparse-checkout list >actual 2>err &&\n \ttest_cmp repo/.git/info/sparse-checkout actual &&\n-\tgrep \"warning: your sparse-checkout file may have issues: pattern .* is repeated\" err &&\n-\tgrep \"warning: disabling cone pattern matching\" err\n+\ttest_grep \"warning: your sparse-checkout file may have issues: pattern .* is repeated\" err &&\n+\ttest_grep \"warning: disabling cone pattern matching\" err\n '\n \n test_expect_success 'set from subdir pays attention to prefix' '\n@@ -917,34 +917,34 @@ test_expect_success 'set from subdir in non-cone mode throws an error' '\n \tgit -C repo sparse-checkout disable &&\n \ttest_must_fail git -C repo/deep sparse-checkout set --no-cone deeper2 ../folder1 2>error &&\n \n-\tgrep \"run from the toplevel directory in non-cone mode\" error\n+\ttest_grep \"run from the toplevel directory in non-cone mode\" error\n '\n \n test_expect_success 'set from subdir in non-cone mode throws an error' '\n \tgit -C repo sparse-checkout set --no-cone deep/deeper2 &&\n \ttest_must_fail git -C repo/deep sparse-checkout add deeper1/deepest ../folder1 2>error &&\n \n-\tgrep \"run from the toplevel directory in non-cone mode\" error\n+\ttest_grep \"run from the toplevel directory in non-cone mode\" error\n '\n \n test_expect_success 'by default, cone mode will error out when passed files' '\n \tgit -C repo sparse-checkout reapply --cone &&\n \ttest_must_fail git -C repo sparse-checkout add .gitignore 2>error &&\n \n-\tgrep \".gitignore.*is not a directory\" error\n+\ttest_grep \".gitignore.*is not a directory\" error\n '\n \n test_expect_success 'error on mistyped command line options' '\n \ttest_must_fail git -C repo sparse-checkout add --sikp-checks .gitignore 2>error &&\n \n-\tgrep \"unknown option.*sikp-checks\" error\n+\ttest_grep \"unknown option.*sikp-checks\" error\n '\n \n test_expect_success 'by default, non-cone mode will warn on individual files' '\n \tgit -C repo sparse-checkout reapply --no-cone &&\n \tgit -C repo sparse-checkout add .gitignore 2>warning &&\n \n-\tgrep \"pass a leading slash before paths.*if you want a single file\" warning\n+\ttest_grep \"pass a leading slash before paths.*if you want a single file\" warning\n '\n \n test_expect_success 'setup bare repo' '\n@@ -1108,11 +1108,11 @@ test_expect_success 'clean' '\n \ttouch repo/folder1/extra/inside/file &&\n \n \ttest_must_fail git -C repo sparse-checkout clean 2>err &&\n-\tgrep \"refusing to clean\" err &&\n+\ttest_grep \"refusing to clean\" err &&\n \n \tgit -C repo config clean.requireForce true &&\n \ttest_must_fail git -C repo sparse-checkout clean 2>err &&\n-\tgrep \"refusing to clean\" err &&\n+\ttest_grep \"refusing to clean\" err &&\n \n \tcat >expect <<-\\EOF &&\n \tWould remove deep/deeper2/\n@@ -1255,7 +1255,7 @@ test_expect_success 'sparse-checkout operations with merge conflicts' '\n \t\ttest_must_fail git merge -m \"will-conflict\" right &&\n \n \t\ttest_must_fail git sparse-checkout clean -f 2>err &&\n-\t\tgrep \"failed to convert index to a sparse index\" err &&\n+\t\ttest_grep \"failed to convert index to a sparse index\" err &&\n \n \t\techo merged >folder1/even/more/dirs/file &&\n \t\tgit add --sparse folder1 &&\ndiff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh\nindex 8186da5c88..9814431cd7 100755\n--- a/t/t1092-sparse-checkout-compatibility.sh\n+++ b/t/t1092-sparse-checkout-compatibility.sh\n@@ -454,10 +454,10 @@ test_expect_success 'add outside sparse cone' '\n \trun_on_sparse ../edit-contents folder1/a &&\n \trun_on_sparse ../edit-contents folder1/newfile &&\n \ttest_sparse_match test_must_fail git add folder1/a &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/a &&\n \ttest_sparse_match test_must_fail git add folder1/newfile &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/newfile\n '\n \n@@ -509,13 +509,13 @@ test_expect_success 'status/add: outside sparse cone' '\n \n \t# Adding the path outside of the sparse-checkout cone should fail.\n \ttest_sparse_match test_must_fail git add folder1/a &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/a &&\n \ttest_all_match git add --refresh folder1/a &&\n \ttest_must_be_empty sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/a &&\n \ttest_sparse_match test_must_fail git add folder1/new &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/new &&\n \ttest_sparse_match git add --sparse folder1/a &&\n \ttest_sparse_match git add --sparse folder1/new &&\n@@ -661,8 +661,8 @@ test_expect_success 'checkout and reset (mixed)' '\n \t# in sparse-checkout or sparse-index.\n \tgit -C full-checkout reset update-folder1 >full-checkout-out &&\n \ttest_sparse_match git reset update-folder1 &&\n-\tgrep \"M\tfolder1/a\" full-checkout-out &&\n-\t! grep \"M\tfolder1/a\" sparse-checkout-out &&\n+\ttest_grep \"M\tfolder1/a\" full-checkout-out &&\n+\ttest_grep ! \"M\tfolder1/a\" sparse-checkout-out &&\n \trun_on_sparse test_path_is_missing folder1\n '\n \n@@ -880,8 +880,8 @@ test_expect_success 'update-index with directories' '\n \t# update-index will exit silently when provided with a directory name\n \t# containing a trailing slash\n \ttest_all_match git update-index deep/ folder1/ &&\n-\tgrep \"Ignoring path deep/\" sparse-checkout-err &&\n-\tgrep \"Ignoring path folder1/\" sparse-checkout-err &&\n+\ttest_grep \"Ignoring path deep/\" sparse-checkout-err &&\n+\ttest_grep \"Ignoring path folder1/\" sparse-checkout-err &&\n \n \t# When update-index is given a directory name WITHOUT a trailing slash, it will\n \t# behave in different ways depending on the status of the directory on disk:\n@@ -1067,7 +1067,7 @@ test_expect_success 'merge with conflict outside cone' '\n \n \t# 2. Add the file with conflict markers\n \ttest_sparse_match test_must_fail git add folder1/a &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/a &&\n \ttest_all_match git add --sparse folder1/a &&\n \ttest_all_match git status --porcelain=v2 &&\n@@ -1076,7 +1076,7 @@ test_expect_success 'merge with conflict outside cone' '\n \t#    accept conflict markers as resolved content.\n \trun_on_all mv folder2/a folder2/z &&\n \ttest_sparse_match test_must_fail git add folder2 &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder2/z &&\n \ttest_all_match git add --sparse folder2 &&\n \ttest_all_match git status --porcelain=v2 &&\n@@ -1107,7 +1107,7 @@ test_expect_success 'cherry-pick/rebase with conflict outside cone' '\n \t\t# SKIP_WORKTREE bit from the index entry for folder1/a, we should\n \t\t# warn that this is a problematic add.\n \t\ttest_sparse_match test_must_fail git add folder1/a &&\n-\t\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\t\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \t\ttest_sparse_unstaged folder1/a &&\n \t\ttest_all_match git add --sparse folder1/a &&\n \t\ttest_all_match git status --porcelain=v2 &&\n@@ -1119,7 +1119,7 @@ test_expect_success 'cherry-pick/rebase with conflict outside cone' '\n \t\t# existing index entry with the SKIP_WORKTREE bit cleared.\n \t\trun_on_all mv folder2/a folder2/z &&\n \t\ttest_sparse_match test_must_fail git add folder2 &&\n-\t\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\t\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \t\ttest_sparse_unstaged folder2/z &&\n \t\ttest_all_match git add --sparse folder2 &&\n \t\ttest_all_match git status --porcelain=v2 &&\n@@ -1266,7 +1266,7 @@ test_expect_success 'checkout-index with folders' '\n \trun_on_all test_must_fail git checkout-index -f -- folder1/ &&\n \ttest_cmp full-checkout-err sparse-checkout-err &&\n \t! test_cmp full-checkout-err sparse-index-err &&\n-\tgrep \"is a sparse directory\" sparse-index-err\n+\ttest_grep \"is a sparse directory\" sparse-index-err\n '\n \n test_expect_success 'checkout-index --all' '\n@@ -1374,8 +1374,8 @@ test_expect_success 'submodule handling' '\n \t# having a submodule prevents \"modules\" from collapse\n \ttest_sparse_match git sparse-checkout set deep/deeper1 &&\n \tgit -C sparse-index ls-files --sparse --stage >cache &&\n-\tgrep \"100644 .*\tmodules/a\" cache &&\n-\tgrep \"160000 $(git -C initial-repo rev-parse HEAD) 0\tmodules/sub\" cache\n+\ttest_grep \"100644 .*\tmodules/a\" cache &&\n+\ttest_grep \"160000 $(git -C initial-repo rev-parse HEAD) 0\tmodules/sub\" cache\n '\n \n test_expect_success 'git apply functionality' '\n@@ -1392,7 +1392,7 @@ test_expect_success 'git apply functionality' '\n \n \t# Apply a patch to a file outside the sparse definition\n \ttest_sparse_match test_must_fail git apply ../patch-outside &&\n-\tgrep \"No such file or directory\" sparse-checkout-err &&\n+\ttest_grep \"No such file or directory\" sparse-checkout-err &&\n \n \t# But it works with --index and --cached\n \ttest_all_match git apply --index --stat ../patch-outside &&\n@@ -2013,9 +2013,9 @@ test_expect_success 'mv directory from out-of-cone to in-cone' '\n \ttest_all_match git status --porcelain=v2 &&\n \ttest_sparse_match git ls-files -t &&\n \tgit -C sparse-checkout ls-files -t >actual &&\n-\tgrep -e \"H deep/folder1/0/0/0\" actual &&\n-\tgrep -e \"H deep/folder1/0/1\" actual &&\n-\tgrep -e \"H deep/folder1/a\" actual &&\n+\ttest_grep -e \"H deep/folder1/0/0/0\" actual &&\n+\ttest_grep -e \"H deep/folder1/0/1\" actual &&\n+\ttest_grep -e \"H deep/folder1/a\" actual &&\n \n \ttest_all_match git reset --hard &&\n \n@@ -2025,8 +2025,8 @@ test_expect_success 'mv directory from out-of-cone to in-cone' '\n \ttest_sparse_match git status --porcelain=v2 &&\n \ttest_sparse_match git ls-files -t &&\n \tgit -C sparse-checkout ls-files -t >actual &&\n-\tgrep -e \"H deep/0/0/0\" actual &&\n-\tgrep -e \"H deep/0/1\" actual\n+\ttest_grep -e \"H deep/0/0/0\" actual &&\n+\ttest_grep -e \"H deep/0/1\" actual\n '\n \n test_expect_success 'rm pathspec inside sparse definition' '\n@@ -2517,7 +2517,7 @@ test_expect_success 'advice.sparseIndexExpanded' '\n \tmkdir -p sparse-index/deep/deeper2/deepest &&\n \ttouch sparse-index/deep/deeper2/deepest/bogus &&\n \tgit -C sparse-index status 2>err &&\n-\tgrep \"The sparse index is expanding to a full index\" err &&\n+\ttest_grep \"The sparse index is expanding to a full index\" err &&\n \n \tgit -C sparse-index sparse-checkout disable 2>err &&\n \ttest_line_count = 0 err\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 11fc976f3a..b337e25d92 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -800,7 +800,7 @@ test_expect_success 'renaming a section with an overly-long line' '\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n \ttest_must_fail git config ${mode_prefix}rename-section -f y a xyz 2>err &&\n-\tgrep \"refusing to work with overly long line in .y. on line 2\" err\n+\ttest_grep \"refusing to work with overly long line in .y. on line 2\" err\n '\n \n cat >> .git/config << EOF\n@@ -1615,9 +1615,9 @@ test_expect_success 'git --config-env=key=envvar support' '\n \n test_expect_success 'git --config-env with missing value' '\n \ttest_must_fail env ENVVAR=value git --config-env 2>error &&\n-\tgrep \"no config key given for --config-env\" error &&\n+\ttest_grep \"no config key given for --config-env\" error &&\n \ttest_must_fail env ENVVAR=value git --config-env config core.name 2>error &&\n-\tgrep \"invalid config format: config\" error\n+\ttest_grep \"invalid config format: config\" error\n '\n \n test_expect_success 'git --config-env fails with invalid parameters' '\n@@ -2048,7 +2048,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \tkey = true\n \tEOF\n \tgit config ${mode_unset} two.key &&\n-\t! grep two .git/config &&\n+\ttest_grep ! two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n \t[one]\n@@ -2068,7 +2068,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \tQkey = true\n \tEOF\n \tgit config ${mode_unset} two.key &&\n-\tgrep two .git/config &&\n+\ttest_grep two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n \t[one]\n@@ -2599,7 +2599,7 @@ test_expect_success '--type rejects unknown specifiers' '\n \n test_expect_success '--type=int requires at least one digit' '\n \ttest_must_fail git config --type int --default m some.key >out 2>error &&\n-\tgrep \"bad numeric config value\" error &&\n+\ttest_grep \"bad numeric config value\" error &&\n \ttest_must_be_empty out\n '\n \n@@ -2911,12 +2911,12 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such\n \n \t# test with any Git command\n \ttest_must_fail git -C hasremoteurlTest status 2>err &&\n-\tgrep \"fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url\" err\n+\ttest_grep \"fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url\" err\n '\n \n test_expect_success 'negated mode causes failure' '\n \ttest_must_fail git config --no-get 2>err &&\n-\tgrep \"unknown option \\`no-get${SQ}\" err\n+\ttest_grep \"unknown option \\`no-get${SQ}\" err\n '\n \n test_expect_success 'specifying multiple modes causes failure' '\ndiff --git a/t/t1305-config-include.sh b/t/t1305-config-include.sh\nindex f3892578e4..f6115269f9 100755\n--- a/t/t1305-config-include.sh\n+++ b/t/t1305-config-include.sh\n@@ -353,7 +353,7 @@ test_expect_success 'include cycles are detected' '\n \tgit -C cycle --git-dir=. config include.path cycle &&\n \tgit config -f cycle/cycle include.path config &&\n \ttest_must_fail git -C cycle --git-dir=. config --get-all test.value 2>stderr &&\n-\tgrep \"exceeded maximum include depth\" stderr\n+\ttest_grep \"exceeded maximum include depth\" stderr\n '\n \n test_expect_success 'onbranch with unborn branch' '\ndiff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\nindex e0e49053f0..de95161a83 100755\n--- a/t/t1308-config-set.sh\n+++ b/t/t1308-config-set.sh\n@@ -180,7 +180,7 @@ test_expect_success 'find integer if value is non parse-able' '\n \n test_expect_success 'non parse-able integer value during iteration' '\n \tcheck_config expect_code 128 git_config_int lamb.head 2>result &&\n-\tgrep \"fatal: bad numeric config value .* in file \\.git/config\" result\n+\ttest_grep \"fatal: bad numeric config value .* in file \\.git/config\" result\n '\n \n test_expect_success 'find bool value for the entered key' '\n@@ -302,7 +302,7 @@ test_expect_success 'proper error on directory \"files\"' '\n \techo \"Error (-1) reading configuration file a-directory.\" >expect &&\n \tmkdir a-directory &&\n \ttest_expect_code 2 test-tool config configset_get_value foo.bar a-directory 2>output &&\n-\tgrep \"^warning:\" output &&\n+\ttest_grep \"^warning:\" output &&\n \tgrep \"^Error\" output >actual &&\n \ttest_cmp expect actual\n '\n@@ -312,7 +312,7 @@ test_expect_success POSIXPERM,SANITY 'proper error on non-accessible files' '\n \ttest_when_finished \"chmod +r .git/config\" &&\n \techo \"Error (-1) reading configuration file .git/config.\" >expect &&\n \ttest_expect_code 2 test-tool config configset_get_value foo.bar .git/config 2>output &&\n-\tgrep \"^warning:\" output &&\n+\ttest_grep \"^warning:\" output &&\n \tgrep \"^Error\" output >actual &&\n \ttest_cmp expect actual\n '\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex 1015f335e3..7c89071662 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -92,7 +92,7 @@ test_expect_success \"deleting current branch adds message to HEAD's log\" '\n \tgit update-ref -m delete-$m -d $m &&\n \ttest_must_fail git show-ref --verify -q $m &&\n \ttest-tool ref-store main for-each-reflog-ent HEAD >actual &&\n-\tgrep \"delete-$m$\" actual\n+\ttest_grep \"delete-$m$\" actual\n '\n \n test_expect_success \"deleting by HEAD adds message to HEAD's log\" '\n@@ -102,7 +102,7 @@ test_expect_success \"deleting by HEAD adds message to HEAD's log\" '\n \tgit update-ref -m delete-by-head -d HEAD &&\n \ttest_must_fail git show-ref --verify -q $m &&\n \ttest-tool ref-store main for-each-reflog-ent HEAD >actual &&\n-\tgrep \"delete-by-head$\" actual\n+\ttest_grep \"delete-by-head$\" actual\n '\n \n test_expect_success 'update-ref does not create reflogs by default' '\n@@ -192,7 +192,7 @@ test_expect_success \"move $m (by HEAD)\" '\n test_expect_success \"delete $m (by HEAD) should remove both packed and loose $m\" '\n \ttest_when_finished \"git update-ref -d $m\" &&\n \tgit update-ref -d HEAD $B &&\n-\t! grep \"$m\" .git/packed-refs &&\n+\t! grep \"$m\" .git/packed-refs && # lint-ok: file may not exist (reftable)\n \ttest_must_fail git show-ref --verify -q $m\n '\n \n@@ -575,103 +575,103 @@ test_expect_success 'stdin works with no input' '\n test_expect_success 'stdin fails on empty line' '\n \techo \"\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: empty command in input\" err\n+\ttest_grep \"fatal: empty command in input\" err\n '\n \n test_expect_success 'stdin fails on only whitespace' '\n \techo \" \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command:  \" err\n+\ttest_grep \"fatal: whitespace before command:  \" err\n '\n \n test_expect_success 'stdin fails on leading whitespace' '\n \techo \" create $a $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command:  create $a $m\" err\n+\ttest_grep \"fatal: whitespace before command:  create $a $m\" err\n '\n \n test_expect_success 'stdin fails on unknown command' '\n \techo \"unknown $a\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: unknown $a\" err\n+\ttest_grep \"fatal: unknown command: unknown $a\" err\n '\n \n test_expect_success 'stdin fails on unbalanced quotes' '\n \techo \"create $a \\\"main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: badly quoted argument: \\\\\\\"main\" err\n+\ttest_grep \"fatal: badly quoted argument: \\\\\\\"main\" err\n '\n \n test_expect_success 'stdin fails on invalid escape' '\n \techo \"create $a \\\"ma\\zn\\\"\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: badly quoted argument: \\\\\\\"ma\\\\\\\\zn\\\\\\\"\" err\n+\ttest_grep \"fatal: badly quoted argument: \\\\\\\"ma\\\\\\\\zn\\\\\\\"\" err\n '\n \n test_expect_success 'stdin fails on junk after quoted argument' '\n \techo \"create \\\"$a\\\"main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: unexpected character after quoted argument: \\\\\\\"$a\\\\\\\"main\" err\n+\ttest_grep \"fatal: unexpected character after quoted argument: \\\\\\\"$a\\\\\\\"main\" err\n '\n \n test_expect_success 'stdin fails create with no ref' '\n \techo \"create \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create: missing <ref>\" err\n+\ttest_grep \"fatal: create: missing <ref>\" err\n '\n \n test_expect_success 'stdin fails create with no new value' '\n \techo \"create $a\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $a: missing <new-oid>\" err\n+\ttest_grep \"fatal: create $a: missing <new-oid>\" err\n '\n \n test_expect_success 'stdin fails create with too many arguments' '\n \techo \"create $a $m $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $a: extra input:  $m\" err\n+\ttest_grep \"fatal: create $a: extra input:  $m\" err\n '\n \n test_expect_success 'stdin fails update with no ref' '\n \techo \"update \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: update: missing <ref>\" err\n+\ttest_grep \"fatal: update: missing <ref>\" err\n '\n \n test_expect_success 'stdin fails update with no new value' '\n \techo \"update $a\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: missing <new-oid>\" err\n+\ttest_grep \"fatal: update $a: missing <new-oid>\" err\n '\n \n test_expect_success 'stdin fails update with too many arguments' '\n \techo \"update $a $m $m $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: extra input:  $m\" err\n+\ttest_grep \"fatal: update $a: extra input:  $m\" err\n '\n \n test_expect_success 'stdin fails delete with no ref' '\n \techo \"delete \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete: missing <ref>\" err\n+\ttest_grep \"fatal: delete: missing <ref>\" err\n '\n \n test_expect_success 'stdin fails delete with too many arguments' '\n \techo \"delete $a $m $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete $a: extra input:  $m\" err\n+\ttest_grep \"fatal: delete $a: extra input:  $m\" err\n '\n \n test_expect_success 'stdin fails verify with too many arguments' '\n \techo \"verify $a $m $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: verify $a: extra input:  $m\" err\n+\ttest_grep \"fatal: verify $a: extra input:  $m\" err\n '\n \n test_expect_success 'stdin fails option with unknown name' '\n \techo \"option unknown\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: option unknown: unknown\" err\n+\ttest_grep \"fatal: option unknown: unknown\" err\n '\n \n test_expect_success 'stdin fails with duplicate refs' '\n@@ -759,28 +759,28 @@ test_expect_success 'stdin create ref works with path with space to blob' '\n test_expect_success 'stdin update ref fails with wrong old value' '\n \techo \"update $c $m $m~1\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin update ref fails with bad old value' '\n \techo \"update $c $m does-not-exist\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $c: invalid <old-oid>: does-not-exist\" err &&\n+\ttest_grep \"fatal: update $c: invalid <old-oid>: does-not-exist\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin create ref fails with bad new value' '\n \techo \"create $c does-not-exist\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $c: invalid <new-oid>: does-not-exist\" err &&\n+\ttest_grep \"fatal: create $c: invalid <new-oid>: does-not-exist\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin create ref fails with zero new value' '\n \techo \"create $c \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $c: zero <new-oid>\" err &&\n+\ttest_grep \"fatal: create $c: zero <new-oid>\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n@@ -795,7 +795,7 @@ test_expect_success 'stdin update ref works with right old value' '\n test_expect_success 'stdin delete ref fails with wrong old value' '\n \techo \"delete $a $m~1\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$a'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$a'\"'\"'\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual\n@@ -804,7 +804,7 @@ test_expect_success 'stdin delete ref fails with wrong old value' '\n test_expect_success 'stdin delete ref fails with zero old value' '\n \techo \"delete $a \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete $a: zero <old-oid>\" err &&\n+\ttest_grep \"fatal: delete $a: zero <old-oid>\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual\n@@ -965,7 +965,7 @@ test_expect_success 'stdin update refs fails with wrong old value' '\n \tupdate $c  ''\n \tEOF\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual &&\n@@ -998,123 +998,123 @@ test_expect_success 'stdin -z works on empty input' '\n test_expect_success 'stdin -z fails on empty line' '\n \techo \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command: \" err\n+\ttest_grep \"fatal: whitespace before command: \" err\n '\n \n test_expect_success 'stdin -z fails on empty command' '\n \tprintf $F \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: empty command in input\" err\n+\ttest_grep \"fatal: empty command in input\" err\n '\n \n test_expect_success 'stdin -z fails on only whitespace' '\n \tprintf $F \" \" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command:  \" err\n+\ttest_grep \"fatal: whitespace before command:  \" err\n '\n \n test_expect_success 'stdin -z fails on leading whitespace' '\n \tprintf $F \" create $a\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command:  create $a\" err\n+\ttest_grep \"fatal: whitespace before command:  create $a\" err\n '\n \n test_expect_success 'stdin -z fails on unknown command' '\n \tprintf $F \"unknown $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: unknown $a\" err\n+\ttest_grep \"fatal: unknown command: unknown $a\" err\n '\n \n test_expect_success 'stdin -z fails create with no ref' '\n \tprintf $F \"create \" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: create: missing <ref>\" err\n+\ttest_grep \"fatal: create: missing <ref>\" err\n '\n \n test_expect_success 'stdin -z fails create with no new value' '\n \tprintf $F \"create $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $a: unexpected end of input when reading <new-oid>\" err\n+\ttest_grep \"fatal: create $a: unexpected end of input when reading <new-oid>\" err\n '\n \n test_expect_success 'stdin -z fails create with too many arguments' '\n \tprintf $F \"create $a\" \"$m\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: $m\" err\n+\ttest_grep \"fatal: unknown command: $m\" err\n '\n \n test_expect_success 'stdin -z fails update with no ref' '\n \tprintf $F \"update \" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update: missing <ref>\" err\n+\ttest_grep \"fatal: update: missing <ref>\" err\n '\n \n test_expect_success 'stdin -z fails update with too few args' '\n \tprintf $F \"update $a\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: unexpected end of input when reading <old-oid>\" err\n+\ttest_grep \"fatal: update $a: unexpected end of input when reading <old-oid>\" err\n '\n \n test_expect_success 'stdin -z emits warning with empty new value' '\n \tgit update-ref $a $m &&\n \tprintf $F \"update $a\" \"\" \"\" >stdin &&\n \tgit update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"warning: update $a: missing <new-oid>, treating as zero\" err &&\n+\ttest_grep \"warning: update $a: missing <new-oid>, treating as zero\" err &&\n \ttest_must_fail git rev-parse --verify -q $a\n '\n \n test_expect_success 'stdin -z fails update with no new value' '\n \tprintf $F \"update $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: unexpected end of input when reading <new-oid>\" err\n+\ttest_grep \"fatal: update $a: unexpected end of input when reading <new-oid>\" err\n '\n \n test_expect_success 'stdin -z fails update with no old value' '\n \tprintf $F \"update $a\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: unexpected end of input when reading <old-oid>\" err\n+\ttest_grep \"fatal: update $a: unexpected end of input when reading <old-oid>\" err\n '\n \n test_expect_success 'stdin -z fails update with too many arguments' '\n \tprintf $F \"update $a\" \"$m\" \"$m\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: $m\" err\n+\ttest_grep \"fatal: unknown command: $m\" err\n '\n \n test_expect_success 'stdin -z fails delete with no ref' '\n \tprintf $F \"delete \" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete: missing <ref>\" err\n+\ttest_grep \"fatal: delete: missing <ref>\" err\n '\n \n test_expect_success 'stdin -z fails delete with no old value' '\n \tprintf $F \"delete $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete $a: unexpected end of input when reading <old-oid>\" err\n+\ttest_grep \"fatal: delete $a: unexpected end of input when reading <old-oid>\" err\n '\n \n test_expect_success 'stdin -z fails delete with too many arguments' '\n \tprintf $F \"delete $a\" \"$m\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: $m\" err\n+\ttest_grep \"fatal: unknown command: $m\" err\n '\n \n test_expect_success 'stdin -z fails verify with too many arguments' '\n \tprintf $F \"verify $a\" \"$m\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: $m\" err\n+\ttest_grep \"fatal: unknown command: $m\" err\n '\n \n test_expect_success 'stdin -z fails verify with no old value' '\n \tprintf $F \"verify $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: verify $a: unexpected end of input when reading <old-oid>\" err\n+\ttest_grep \"fatal: verify $a: unexpected end of input when reading <old-oid>\" err\n '\n \n test_expect_success 'stdin -z fails option with unknown name' '\n \tprintf $F \"option unknown\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: option unknown: unknown\" err\n+\ttest_grep \"fatal: option unknown: unknown\" err\n '\n \n test_expect_success 'stdin -z fails with duplicate refs' '\n@@ -1160,14 +1160,14 @@ test_expect_success 'stdin -z create ref works with path with space to blob' '\n test_expect_success 'stdin -z update ref fails with wrong old value' '\n \tprintf $F \"update $c\" \"$m\" \"$m~1\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin -z update ref fails with bad old value' '\n \tprintf $F \"update $c\" \"$m\" \"does-not-exist\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $c: invalid <old-oid>: does-not-exist\" err &&\n+\ttest_grep \"fatal: update $c: invalid <old-oid>: does-not-exist\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n@@ -1176,7 +1176,7 @@ test_expect_success 'stdin -z create ref fails when ref exists' '\n \tgit rev-parse \"$c\" >expect &&\n \tprintf $F \"create $c\" \"$m~1\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \tgit rev-parse \"$c\" >actual &&\n \ttest_cmp expect actual\n '\n@@ -1185,28 +1185,28 @@ test_expect_success 'stdin -z create ref fails with bad new value' '\n \tgit update-ref -d \"$c\" &&\n \tprintf $F \"create $c\" \"does-not-exist\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $c: invalid <new-oid>: does-not-exist\" err &&\n+\ttest_grep \"fatal: create $c: invalid <new-oid>: does-not-exist\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin -z create ref fails with empty new value' '\n \tprintf $F \"create $c\" \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $c: missing <new-oid>\" err &&\n+\ttest_grep \"fatal: create $c: missing <new-oid>\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin -z create ref fails with non commit object' '\n \tprintf $F \"create $c\" \"$(test_oid 001)\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: trying to write ref ${SQ}$c${SQ} with nonexistent object\" err &&\n+\ttest_grep \"fatal: trying to write ref ${SQ}$c${SQ} with nonexistent object\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin -z update ref fails with non commit object' '\n \tprintf $F \"update $b\" \"$(test_oid 001)\" \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: trying to write ref ${SQ}$b${SQ} with nonexistent object\" err &&\n+\ttest_grep \"fatal: trying to write ref ${SQ}$b${SQ} with nonexistent object\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n@@ -1221,7 +1221,7 @@ test_expect_success 'stdin -z update ref works with right old value' '\n test_expect_success 'stdin -z delete ref fails with wrong old value' '\n \tprintf $F \"delete $a\" \"$m~1\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$a'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$a'\"'\"'\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual\n@@ -1230,7 +1230,7 @@ test_expect_success 'stdin -z delete ref fails with wrong old value' '\n test_expect_success 'stdin -z delete ref fails with zero old value' '\n \tprintf $F \"delete $a\" \"$Z\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete $a: zero <old-oid>\" err &&\n+\ttest_grep \"fatal: delete $a: zero <old-oid>\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual\n@@ -1336,7 +1336,7 @@ test_expect_success 'stdin -z update refs fails with wrong old value' '\n \tgit update-ref $c $m &&\n \tprintf $F \"update $a\" \"$m\" \"$m\" \"update $b\" \"$m\" \"$m\" \"update $c\" \"$m\" \"$Z\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual &&\n@@ -1415,13 +1415,13 @@ test_expect_success 'handle per-worktree refs in refs/bisect' '\n \t\tcd worktree &&\n \t\tgit commit --allow-empty -m \"test commit\"  &&\n \t\tgit for-each-ref >for-each-ref.out &&\n-\t\t! grep refs/bisect for-each-ref.out &&\n+\t\ttest_grep ! refs/bisect for-each-ref.out &&\n \t\tgit update-ref refs/bisect/something HEAD &&\n \t\tgit rev-parse refs/bisect/something >../worktree-head &&\n \t\tgit for-each-ref | grep refs/bisect/something\n \t) &&\n \tgit show-ref >actual &&\n-\t! grep 'refs/bisect' actual &&\n+\ttest_grep ! 'refs/bisect' actual &&\n \ttest_must_fail git rev-parse refs/bisect/something &&\n \tgit update-ref refs/bisect/something HEAD &&\n \tgit rev-parse refs/bisect/something >main-head &&\n@@ -1477,7 +1477,7 @@ test_expect_success 'transaction exits on multiple aborts' '\n \ttest_must_fail git update-ref --stdin <stdin >actual 2>err &&\n \tprintf \"%s: ok\\n\" abort >expect &&\n \ttest_cmp expect actual &&\n-\tgrep \"fatal: transaction is closed\" err\n+\ttest_grep \"fatal: transaction is closed\" err\n '\n \n test_expect_success 'transaction exits on start after prepare' '\n@@ -1488,7 +1488,7 @@ test_expect_success 'transaction exits on start after prepare' '\n \ttest_must_fail git update-ref --stdin <stdin 2>err >actual &&\n \tprintf \"%s: ok\\n\" prepare >expect &&\n \ttest_cmp expect actual &&\n-\tgrep \"fatal: prepared transactions can only be closed\" err\n+\ttest_grep \"fatal: prepared transactions can only be closed\" err\n '\n \n test_expect_success 'transaction handles empty abort with missing prepare' '\n@@ -1648,7 +1648,7 @@ test_expect_success PIPE 'transaction flushes status updates' '\n \n \t# This must now fail given that we have locked the ref.\n \ttest_must_fail git update-ref refs/heads/flush $B 2>stderr &&\n-\tgrep \"fatal: update_ref failed for ref ${SQ}refs/heads/flush${SQ}: cannot lock ref\" stderr &&\n+\ttest_grep \"fatal: update_ref failed for ref ${SQ}refs/heads/flush${SQ}: cannot lock ref\" stderr &&\n \n \techo commit >&9 &&\n \techo \"commit: ok\" >expected &&\n@@ -1674,7 +1674,7 @@ do\n \t\tgit symbolic-ref refs/heads/symref $a &&\n \t\tformat_command $type \"symref-verify refs/heads/symref\" \"$a\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: symref-verify: cannot operate with deref mode\" err\n+\t\ttest_grep \"fatal: symref-verify: cannot operate with deref mode\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-verify fails with too many arguments\" '\n@@ -1682,9 +1682,9 @@ do\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err  &&\n \t\tif test \"$type\" = \"-z\"\n \t\tthen\n-\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\t\ttest_grep \"fatal: unknown command: $a\" err\n \t\telse\n-\t\t\tgrep \"fatal: symref-verify refs/heads/symref: extra input:  $a\" err\n+\t\t\ttest_grep \"fatal: symref-verify refs/heads/symref: extra input:  $a\" err\n \t\tfi\n \t'\n \n@@ -1717,7 +1717,7 @@ do\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >before &&\n \t\tformat_command $type \"symref-verify refs/heads/missing\" \"refs/heads/unknown\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/missing${SQ}: unable to resolve reference ${SQ}refs/heads/missing${SQ}\" err &&\n+\t\ttest_grep \"fatal: cannot lock ref ${SQ}refs/heads/missing${SQ}: unable to resolve reference ${SQ}refs/heads/missing${SQ}\" err &&\n \t\ttest_must_fail git rev-parse --verify -q refs/heads/missing &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >after &&\n \t\ttest_cmp before after\n@@ -1743,13 +1743,13 @@ do\n \t\tgit symbolic-ref refs/heads/symref $a &&\n \t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: symref-delete: cannot operate with deref mode\" err\n+\t\ttest_grep \"fatal: symref-delete: cannot operate with deref mode\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-delete fails with no ref\" '\n \t\tformat_command $type \"symref-delete \" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: symref-delete: missing <ref>\" err\n+\t\ttest_grep \"fatal: symref-delete: missing <ref>\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-delete fails deleting regular ref\" '\n@@ -1757,7 +1757,7 @@ do\n \t\tgit update-ref refs/heads/regularref $a &&\n \t\tformat_command $type \"symref-delete refs/heads/regularref\" \"$a\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: expected symref with target ${SQ}$a${SQ}: but is a regular ref\" err\n+\t\ttest_grep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: expected symref with target ${SQ}$a${SQ}: but is a regular ref\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-delete fails with too many arguments\" '\n@@ -1765,16 +1765,16 @@ do\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n \t\tif test \"$type\" = \"-z\"\n \t\tthen\n-\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\t\ttest_grep \"fatal: unknown command: $a\" err\n \t\telse\n-\t\t\tgrep \"fatal: symref-delete refs/heads/symref: extra input:  $a\" err\n+\t\t\ttest_grep \"fatal: symref-delete refs/heads/symref: extra input:  $a\" err\n \t\tfi\n \t'\n \n \ttest_expect_success \"stdin $type symref-delete fails with wrong old value\" '\n \t\tformat_command $type \"symref-delete refs/heads/symref\" \"$m\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main\" err &&\n+\t\ttest_grep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main\" err &&\n \t\tgit symbolic-ref refs/heads/symref >expect &&\n \t\techo $a >actual &&\n \t\ttest_cmp expect actual\n@@ -1812,9 +1812,9 @@ do\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n \t\tif test \"$type\" = \"-z\"\n \t\tthen\n-\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\t\ttest_grep \"fatal: unknown command: $a\" err\n \t\telse\n-\t\t\tgrep \"fatal: symref-create refs/heads/symref: extra input:  $a\" err\n+\t\t\ttest_grep \"fatal: symref-create refs/heads/symref: extra input:  $a\" err\n \t\tfi\n \t'\n \n@@ -1877,16 +1877,16 @@ do\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n \t\tif test \"$type\" = \"-z\"\n \t\tthen\n-\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\t\ttest_grep \"fatal: unknown command: $a\" err\n \t\telse\n-\t\t\tgrep \"fatal: symref-update refs/heads/symref: extra input:  $a\" err\n+\t\t\ttest_grep \"fatal: symref-update refs/heads/symref: extra input:  $a\" err\n \t\tfi\n \t'\n \n \ttest_expect_success \"stdin $type symref-update fails with wrong old value argument\" '\n \t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"foo\" \"$a\" \"$a\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: symref-update refs/heads/symref: invalid arg ${SQ}foo${SQ} for old value\" err\n+\t\ttest_grep \"fatal: symref-update refs/heads/symref: invalid arg ${SQ}foo${SQ} for old value\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-update creates with zero old value\" '\n@@ -1922,7 +1922,7 @@ do\n \t\tgit symbolic-ref refs/heads/symref $a &&\n \t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"$b\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected $b\" err &&\n+\t\ttest_grep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected $b\" err &&\n \t\ttest_must_fail git rev-parse --verify -q $c\n \t'\n \n@@ -1997,7 +1997,7 @@ do\n \t\tgit symbolic-ref --no-recurse refs/heads/symref >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&\n-\t\tgrep \"$Z $(git rev-parse $a)\" actual\n+\t\ttest_grep \"$Z $(git rev-parse $a)\" actual\n \t'\n \n \ttest_expect_success \"stdin $type symref-update regular ref to symref with correct old-oid\" '\n@@ -2009,7 +2009,7 @@ do\n \t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&\n-\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t\ttest_grep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n \t'\n \n \ttest_expect_success \"stdin $type symref-update regular ref to symref fails with wrong old-oid\" '\n@@ -2017,7 +2017,7 @@ do\n \t\tgit update-ref --no-deref refs/heads/regularref $a &&\n \t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"$(git rev-parse refs/heads/target2)\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: is at $(git rev-parse $a) but expected $(git rev-parse refs/heads/target2)\" err &&\n+\t\ttest_grep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: is at $(git rev-parse $a) but expected $(git rev-parse refs/heads/target2)\" err &&\n \t\techo $(git rev-parse $a) >expect &&\n \t\tgit rev-parse refs/heads/regularref >actual &&\n \t\ttest_cmp expect actual\n@@ -2028,7 +2028,7 @@ do\n \t\tgit update-ref --no-deref refs/heads/regularref $a &&\n \t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"not-a-ref-oid\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: symref-update refs/heads/regularref: invalid oid: not-a-ref-oid\" err &&\n+\t\ttest_grep \"fatal: symref-update refs/heads/regularref: invalid oid: not-a-ref-oid\" err &&\n \t\techo $(git rev-parse $a) >expect &&\n \t\tgit rev-parse refs/heads/regularref >actual &&\n \t\ttest_cmp expect actual\n@@ -2039,7 +2039,7 @@ do\n \t\tgit symbolic-ref refs/heads/symref refs/heads/target2 &&\n \t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"oid\" \"$Z\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: reference already exists\" err &&\n+\t\ttest_grep \"fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: reference already exists\" err &&\n \t\techo refs/heads/target2 >expect &&\n \t\tgit symbolic-ref refs/heads/symref >actual &&\n \t\ttest_cmp expect actual\n@@ -2059,7 +2059,7 @@ do\n \t\tgit symbolic-ref --no-recurse refs/heads/symref >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&\n-\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t\ttest_grep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n \t'\n \n \ttest_expect_success \"stdin $type symref-update regular ref to symref\" '\n@@ -2071,7 +2071,7 @@ do\n \t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&\n-\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t\ttest_grep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n \t'\n \n \ttest_expect_success \"stdin $type batch-updates\" '\ndiff --git a/t/t1403-show-ref.sh b/t/t1403-show-ref.sh\nindex 36c903ca19..7ce5af4ab8 100755\n--- a/t/t1403-show-ref.sh\n+++ b/t/t1403-show-ref.sh\n@@ -213,19 +213,19 @@ test_expect_success 'show-ref --verify with dangling ref' '\n \n test_expect_success 'show-ref sub-modes are mutually exclusive' '\n \ttest_must_fail git show-ref --verify --exclude-existing 2>err &&\n-\tgrep \"verify\" err &&\n-\tgrep \"exclude-existing\" err &&\n-\tgrep \"cannot be used together\" err &&\n+\ttest_grep \"verify\" err &&\n+\ttest_grep \"exclude-existing\" err &&\n+\ttest_grep \"cannot be used together\" err &&\n \n \ttest_must_fail git show-ref --verify --exists 2>err &&\n-\tgrep \"verify\" err &&\n-\tgrep \"exists\" err &&\n-\tgrep \"cannot be used together\" err &&\n+\ttest_grep \"verify\" err &&\n+\ttest_grep \"exists\" err &&\n+\ttest_grep \"cannot be used together\" err &&\n \n \ttest_must_fail git show-ref --exclude-existing --exists 2>err &&\n-\tgrep \"exclude-existing\" err &&\n-\tgrep \"exists\" err &&\n-\tgrep \"cannot be used together\" err\n+\ttest_grep \"exclude-existing\" err &&\n+\ttest_grep \"exists\" err &&\n+\ttest_grep \"cannot be used together\" err\n '\n \n test_done\ndiff --git a/t/t1410-reflog.sh b/t/t1410-reflog.sh\nindex ce71f9a30a..81de1d40eb 100755\n--- a/t/t1410-reflog.sh\n+++ b/t/t1410-reflog.sh\n@@ -108,12 +108,12 @@ test_expect_success setup '\n \n test_expect_success 'correct usage on sub-command -h' '\n \ttest_expect_code 129 git reflog expire -h >err &&\n-\tgrep \"git reflog expire\" err\n+\ttest_grep \"git reflog expire\" err\n '\n \n test_expect_success 'correct usage on \"git reflog show -h\"' '\n \ttest_expect_code 129 git reflog show -h >err &&\n-\tgrep -F \"git reflog [show]\" err\n+\ttest_grep -F \"git reflog [show]\" err\n '\n \n test_expect_success 'pass through -- to sub-command' '\ndiff --git a/t/t1415-worktree-refs.sh b/t/t1415-worktree-refs.sh\nindex 51d79bae83..6b2ad04aef 100755\n--- a/t/t1415-worktree-refs.sh\n+++ b/t/t1415-worktree-refs.sh\n@@ -32,7 +32,7 @@ test_expect_success 'ambiguous main-worktree/HEAD' '\n \ttest_when_finished git update-ref -d refs/heads/main-worktree/HEAD &&\n \tgit update-ref refs/heads/main-worktree/HEAD $(git rev-parse HEAD) &&\n \tgit rev-parse main-worktree/HEAD 2>warn &&\n-\tgrep \"main-worktree/HEAD.*ambiguous\" warn\n+\ttest_grep \"main-worktree/HEAD.*ambiguous\" warn\n '\n \n test_expect_success 'resolve worktrees/xx/HEAD' '\n@@ -45,7 +45,7 @@ test_expect_success 'ambiguous worktrees/xx/HEAD' '\n \tgit update-ref refs/heads/worktrees/wt1/HEAD $(git rev-parse HEAD) &&\n \ttest_when_finished git update-ref -d refs/heads/worktrees/wt1/HEAD &&\n \tgit rev-parse worktrees/wt1/HEAD 2>warn &&\n-\tgrep \"worktrees/wt1/HEAD.*ambiguous\" warn\n+\ttest_grep \"worktrees/wt1/HEAD.*ambiguous\" warn\n '\n \n test_expect_success 'reflog of main-worktree/HEAD' '\ndiff --git a/t/t1430-bad-ref-name.sh b/t/t1430-bad-ref-name.sh\nindex 3ab65f72cd..1ed4c7d2c2 100755\n--- a/t/t1430-bad-ref-name.sh\n+++ b/t/t1430-bad-ref-name.sh\n@@ -47,7 +47,7 @@ test_expect_success 'git branch shows badly named ref as warning' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \tgit branch >output 2>error &&\n \ttest_grep -e \"ignoring ref with broken name refs/heads/broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -d can delete badly named ref' '\n@@ -55,8 +55,8 @@ test_expect_success 'branch -d can delete badly named ref' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \tgit branch -d broken...ref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -D can delete badly named ref' '\n@@ -64,8 +64,8 @@ test_expect_success 'branch -D can delete badly named ref' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \tgit branch -D broken...ref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -D cannot delete non-ref in .git dir' '\n@@ -93,8 +93,8 @@ test_expect_success 'git branch cannot create a badly named ref' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \ttest_must_fail git branch broken...ref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -m cannot rename to a bad ref name' '\n@@ -104,8 +104,8 @@ test_expect_success 'branch -m cannot rename to a bad ref name' '\n \ttest_must_fail git branch -m goodref broken...ref &&\n \ttest_cmp_rev main goodref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_failure 'branch -m can rename from a bad ref name' '\n@@ -115,16 +115,16 @@ test_expect_failure 'branch -m can rename from a bad ref name' '\n \tgit branch -m broken...ref renamed &&\n \ttest_cmp_rev main renamed &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'push cannot create a badly named ref' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \ttest_must_fail git push \"file://$(pwd)\" HEAD:refs/heads/broken...ref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_failure 'push --mirror can delete badly named ref' '\n@@ -144,8 +144,8 @@ test_expect_failure 'push --mirror can delete badly named ref' '\n \t) &&\n \tgit -C src push --mirror \"file://$top/dest\" &&\n \tgit -C dest branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'rev-parse skips symref pointing to broken name' '\n@@ -168,9 +168,9 @@ test_expect_success 'for-each-ref emits warnings for broken names' '\n \ttest-tool ref-store main create-symref refs/heads/broken...symref refs/heads/main &&\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...symref\" &&\n \tgit for-each-ref >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output &&\n-\t! grep -e \"badname\" output &&\n-\t! grep -e \"broken\\.\\.\\.symref\" output &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output &&\n+\ttest_grep ! -e \"badname\" output &&\n+\ttest_grep ! -e \"broken\\.\\.\\.symref\" output &&\n \ttest_grep \"ignoring ref with broken name refs/heads/broken\\.\\.\\.ref\" error &&\n \ttest_grep ! \"ignoring broken ref refs/heads/badname\" error &&\n \ttest_grep \"ignoring ref with broken name refs/heads/broken\\.\\.\\.symref\" error\n@@ -183,8 +183,8 @@ test_expect_success 'update-ref -d can delete broken name' '\n \ttest_must_be_empty output &&\n \ttest_must_be_empty error &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -d can delete broken name' '\n@@ -194,8 +194,8 @@ test_expect_success 'branch -d can delete broken name' '\n \ttest_grep \"Deleted branch broken...ref (was broken)\" output &&\n \ttest_must_be_empty error &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'update-ref --no-deref -d can delete symref to broken name' '\n@@ -313,37 +313,37 @@ test_expect_success 'update-ref -d cannot delete absolute path' '\n test_expect_success 'update-ref --stdin fails create with bad ref name' '\n \techo \"create ~a refs/heads/main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'update-ref --stdin fails update with bad ref name' '\n \techo \"update ~a refs/heads/main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'update-ref --stdin fails delete with bad ref name' '\n \techo \"delete ~a refs/heads/main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'update-ref --stdin -z fails create with bad ref name' '\n \tprintf \"%s\\0\" \"create ~a \" refs/heads/main >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a \" err\n+\ttest_grep \"fatal: invalid ref format: ~a \" err\n '\n \n test_expect_success 'update-ref --stdin -z fails update with bad ref name' '\n \tprintf \"%s\\0\" \"update ~a\" refs/heads/main \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'update-ref --stdin -z fails delete with bad ref name' '\n \tprintf \"%s\\0\" \"delete ~a\" refs/heads/main >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'branch rejects HEAD as a branch name' '\ndiff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh\nindex 54e81c2636..47d460a699 100755\n--- a/t/t1450-fsck.sh\n+++ b/t/t1450-fsck.sh\n@@ -68,7 +68,7 @@ test_expect_success 'object with hash mismatch' '\n \t\tgit update-ref refs/heads/bogus $cmt &&\n \n \t\ttest_must_fail git fsck 2>out &&\n-\t\tgrep \"$oldoid: hash-path mismatch, found at: .*$new\" out\n+\t\ttest_grep \"$oldoid: hash-path mismatch, found at: .*$new\" out\n \t)\n '\n \n@@ -172,7 +172,7 @@ test_expect_success 'commit with multiple signatures is okay' '\n \ttest_when_finished \"git update-ref -d refs/heads/bogus\" &&\n \tgit fsck 2>out &&\n \tcat out &&\n-\t! grep \"commit $new\" out\n+\ttest_grep ! \"commit $new\" out\n '\n \n test_expect_success 'email without @ is okay' '\n@@ -183,7 +183,7 @@ test_expect_success 'email without @ is okay' '\n \tgit update-ref refs/heads/bogus \"$new\" &&\n \ttest_when_finished \"git update-ref -d refs/heads/bogus\" &&\n \tgit fsck 2>out &&\n-\t! grep \"commit $new\" out\n+\ttest_grep ! \"commit $new\" out\n '\n \n test_expect_success 'email with embedded > is not okay' '\n@@ -626,7 +626,7 @@ test_expect_success 'fsck notices excessively large tree entry name' '\n \t\tcd large-name &&\n \t\ttest_commit a-long-name &&\n \t\tgit -c fsck.largePathname=warn:10 fsck 2>out &&\n-\t\tgrep \"warning.*large pathname\" out\n+\t\ttest_grep \"warning.*large pathname\" out\n \t)\n '\n \n@@ -849,7 +849,7 @@ test_expect_success 'fsck errors in packed objects' '\n \ttest_must_fail git fsck 2>out &&\n \ttest_grep \"error in commit $one.* - bad name\" out &&\n \ttest_grep \"error in commit $two.* - bad name\" out &&\n-\t! grep corrupt out\n+\ttest_grep ! corrupt out\n '\n \n test_expect_success 'fsck handles multiple packfiles with big blobs' '\n@@ -1027,7 +1027,7 @@ test_expect_success 'bogus head does not fallback to all heads' '\n \ttest_when_finished \"git rm --cached foo\" &&\n \tremove_object $blob &&\n \ttest_must_fail git fsck $ZERO_OID >out 2>&1 &&\n-\t! grep $blob out\n+\ttest_grep ! $blob out\n '\n \n # Corrupt the checksum on the index.\ndiff --git a/t/t1451-fsck-buffer.sh b/t/t1451-fsck-buffer.sh\nindex 3a3d33f405..14151eabbd 100755\n--- a/t/t1451-fsck-buffer.sh\n+++ b/t/t1451-fsck-buffer.sh\n@@ -46,7 +46,7 @@ check () {\n \t\t\techo \"$content\"\n \t\t} >input &&\n \t\ttest_must_fail git hash-object -t \"$type\" input 2>err &&\n-\t\tgrep \"$fsck\" err\n+\t\ttest_grep \"$fsck\" err\n \t'\n }\n \n@@ -125,7 +125,7 @@ ident_checks tag tagger\n test_expect_success 'truncated tree (short hash)' '\n \tprintf \"100644 foo\\0\\1\\1\\1\\1\" >input &&\n \ttest_must_fail git hash-object -t tree input 2>err &&\n-\tgrep badTree err\n+\ttest_grep badTree err\n '\n \n test_expect_success 'truncated tree (missing nul)' '\n@@ -135,7 +135,7 @@ test_expect_success 'truncated tree (missing nul)' '\n \t# parser does not walk past the end of the buffer).\n \tprintf \"100644 a long filename, or a hash with missing nul?\" >input &&\n \ttest_must_fail git hash-object -t tree input 2>err &&\n-\tgrep badTree err\n+\ttest_grep badTree err\n '\n \n test_done\ndiff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh\nindex 5246468024..8f42697143 100755\n--- a/t/t1460-refs-migrate.sh\n+++ b/t/t1460-refs-migrate.sh\n@@ -212,7 +212,7 @@ do\n \t\t\ttest_commit -C repo initial &&\n \t\t\tgit -C repo refs migrate --dry-run \\\n \t\t\t\t--ref-format=$to_format >output &&\n-\t\t\tgrep \"Finished dry-run migration of refs\" output &&\n+\t\t\ttest_grep \"Finished dry-run migration of refs\" output &&\n \t\t\ttest_path_is_dir repo/.git/ref_migration.* &&\n \t\t\techo $from_format >expect &&\n \t\t\tgit -C repo rev-parse --show-ref-format >actual &&\ndiff --git a/t/t1500-rev-parse.sh b/t/t1500-rev-parse.sh\nindex 38067d95f7..4174ca40c3 100755\n--- a/t/t1500-rev-parse.sh\n+++ b/t/t1500-rev-parse.sh\n@@ -204,7 +204,7 @@ test_expect_success 'rev-parse --show-object-format in repo' '\n \tgit rev-parse --show-object-format=output >actual &&\n \ttest_cmp expect actual &&\n \ttest_must_fail git rev-parse --show-object-format=squeamish-ossifrage 2>err &&\n-\tgrep \"unknown mode for --show-object-format: squeamish-ossifrage\" err\n+\ttest_grep \"unknown mode for --show-object-format: squeamish-ossifrage\" err\n '\n \n \n@@ -228,7 +228,7 @@ test_expect_success RUST 'rev-parse --show-object-format in repo with compat mod\n \t\tgit rev-parse --show-object-format=compat >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest_must_fail git rev-parse --show-object-format=squeamish-ossifrage 2>err &&\n-\t\tgrep \"unknown mode for --show-object-format: squeamish-ossifrage\" err\n+\t\ttest_grep \"unknown mode for --show-object-format: squeamish-ossifrage\" err\n \t) &&\n \tmkdir repo2 &&\n \t(\n@@ -254,7 +254,7 @@ test_expect_success 'rev-parse --show-ref-format with invalid storage' '\n \t\tcd repo &&\n \t\tgit config extensions.refstorage broken &&\n \t\ttest_must_fail git rev-parse --show-ref-format 2>err &&\n-\t\tgrep \"error: invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}broken${SQ}\" err\n+\t\ttest_grep \"error: invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}broken${SQ}\" err\n \t)\n '\n \ndiff --git a/t/t1502-rev-parse-parseopt.sh b/t/t1502-rev-parse-parseopt.sh\nindex 3962f1d288..2cab204376 100755\n--- a/t/t1502-rev-parse-parseopt.sh\n+++ b/t/t1502-rev-parse-parseopt.sh\n@@ -331,7 +331,7 @@ test_expect_success 'ambiguous: --no matches both --noble and --no-noble' '\n \tEOF\n \ttest_expect_code 129 env GIT_TEST_DISALLOW_ABBREVIATED_OPTIONS=false \\\n \tgit rev-parse --parseopt -- <spec 2>err --no &&\n-\tgrep \"error: ambiguous option: no (could be --noble or --no-noble)\" err\n+\ttest_grep \"error: ambiguous option: no (could be --noble or --no-noble)\" err\n '\n \n test_done\ndiff --git a/t/t1503-rev-parse-verify.sh b/t/t1503-rev-parse-verify.sh\nindex 75a708f9ba..87638a4a2c 100755\n--- a/t/t1503-rev-parse-verify.sh\n+++ b/t/t1503-rev-parse-verify.sh\n@@ -62,15 +62,15 @@ test_expect_success 'works with one good rev' '\n \n test_expect_success 'fails with any bad rev or many good revs' '\n \ttest_must_fail git rev-parse --verify 2>error &&\n-\tgrep \"single revision\" error &&\n+\ttest_grep \"single revision\" error &&\n \ttest_must_fail git rev-parse --verify foo 2>error &&\n-\tgrep \"single revision\" error &&\n+\ttest_grep \"single revision\" error &&\n \ttest_must_fail git rev-parse --verify HEAD bar 2>error &&\n-\tgrep \"single revision\" error &&\n+\ttest_grep \"single revision\" error &&\n \ttest_must_fail git rev-parse --verify baz HEAD 2>error &&\n-\tgrep \"single revision\" error &&\n+\ttest_grep \"single revision\" error &&\n \ttest_must_fail git rev-parse --verify $HASH2 HEAD 2>error &&\n-\tgrep \"single revision\" error\n+\ttest_grep \"single revision\" error\n '\n \n test_expect_success 'fails silently when using -q' '\ndiff --git a/t/t1510-repo-setup.sh b/t/t1510-repo-setup.sh\nindex bbfe05b8e4..d330b8df47 100755\n--- a/t/t1510-repo-setup.sh\n+++ b/t/t1510-repo-setup.sh\n@@ -604,7 +604,7 @@ test_expect_success '#20b/c: core.worktree and core.bare conflict' '\n \t\tcd 20b/.git &&\n \t\ttest_must_fail git status >/dev/null\n \t) 2>message &&\n-\tgrep \"core.bare and core.worktree\" message\n+\ttest_grep \"core.bare and core.worktree\" message\n '\n \n test_expect_success '#20d: core.worktree and core.bare OK when working tree not needed' '\n@@ -721,8 +721,8 @@ test_expect_success '#22.2: core.worktree and core.bare conflict' '\n \t\texport GIT_DIR &&\n \t\ttest_must_fail git status 2>result\n \t) &&\n-\tgrep \"core.bare and core.worktree\" 22/.git/result &&\n-\tgrep \"core.bare and core.worktree\" 22/result\n+\ttest_grep \"core.bare and core.worktree\" 22/.git/result &&\n+\ttest_grep \"core.bare and core.worktree\" 22/result\n '\n \n # Case #23: GIT_DIR + GIT_WORK_TREE(+core.worktree) suppresses bareness.\n@@ -767,7 +767,7 @@ test_expect_success '#28: core.worktree and core.bare conflict (gitfile case)' '\n \t\tcd 28 &&\n \t\ttest_must_fail git status\n \t) 2>message &&\n-\tgrep \"core.bare and core.worktree\" message\n+\ttest_grep \"core.bare and core.worktree\" message\n '\n \n # Case #29: GIT_WORK_TREE(+core.worktree) overrides core.bare (gitfile case).\n@@ -791,7 +791,7 @@ test_expect_success '#30: core.worktree and core.bare conflict (gitfile version)\n \t\tcd 30 &&\n \t\ttest_must_fail env GIT_DIR=.git git status 2>result\n \t) &&\n-\tgrep \"core.bare and core.worktree\" 30/result\n+\ttest_grep \"core.bare and core.worktree\" 30/result\n '\n \n # Case #31: GIT_DIR + GIT_WORK_TREE(+core.worktree) suppresses\ndiff --git a/t/t1512-rev-parse-disambiguation.sh b/t/t1512-rev-parse-disambiguation.sh\nindex 1a380a4184..0e6b42ec78 100755\n--- a/t/t1512-rev-parse-disambiguation.sh\n+++ b/t/t1512-rev-parse-disambiguation.sh\n@@ -378,7 +378,7 @@ test_expect_success 'ambiguous 40-hex ref' '\n \tVAL=$(git commit-tree $TREE </dev/null) &&\n \tgit update-ref refs/heads/$REF $VAL &&\n \ttest $(git rev-parse $REF 2>err) = $REF &&\n-\tgrep \"refname.*${REF}.*ambiguous\" err\n+\ttest_grep \"refname.*${REF}.*ambiguous\" err\n '\n \n test_expect_success 'ambiguous short sha1 ref' '\n@@ -387,7 +387,7 @@ test_expect_success 'ambiguous short sha1 ref' '\n \tVAL=$(git commit-tree $TREE </dev/null) &&\n \tgit update-ref refs/heads/$REF $VAL &&\n \ttest $(git rev-parse $REF 2>err) = $VAL &&\n-\tgrep \"refname.*${REF}.*ambiguous\" err\n+\ttest_grep \"refname.*${REF}.*ambiguous\" err\n '\n \n test_expect_success 'ambiguity errors are not repeated (raw)' '\ndiff --git a/t/t1515-rev-parse-outside-repo.sh b/t/t1515-rev-parse-outside-repo.sh\nindex 75e89c4b6e..2cb3c7cc9c 100755\n--- a/t/t1515-rev-parse-outside-repo.sh\n+++ b/t/t1515-rev-parse-outside-repo.sh\n@@ -32,7 +32,7 @@ test_expect_success 'rev-parse --local-env-vars' '\n \tgit rev-parse --local-env-vars >actual &&\n \t# we do not want to depend on the complete list here,\n \t# so just look for something plausible\n-\tgrep ^GIT_DIR actual\n+\ttest_grep ^GIT_DIR actual\n '\n \n test_expect_success 'rev-parse --resolve-git-dir' '\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 0132e772e4..9aae3ff074 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -79,7 +79,7 @@ test_expect_success 'git hook usage' '\n \ttest_expect_code 129 git hook run --unknown 2>err &&\n \ttest_expect_code 129 git hook list &&\n \ttest_expect_code 129 git hook list -h &&\n-\tgrep \"unknown option\" err\n+\ttest_grep \"unknown option\" err\n '\n \n test_expect_success 'git hook list: unknown hook name is rejected' '\n@@ -361,9 +361,9 @@ test_expect_success 'hook can be configured for multiple events' '\n \n \t# 'ghi' should be included in both 'pre-commit' and 'test-hook'\n \tgit hook list pre-commit >actual &&\n-\tgrep \"ghi\" actual &&\n+\ttest_grep \"ghi\" actual &&\n \tgit hook list --allow-unknown-hook-name test-hook >actual &&\n-\tgrep \"ghi\" actual\n+\ttest_grep \"ghi\" actual\n '\n \n test_expect_success 'git hook list shows hooks from the hookdir' '\n@@ -569,7 +569,7 @@ test_expect_success 'git hook run a hook with a bad shebang' '\n \t# TODO: We should emit the same (or at least a more similar)\n \t# error on MINGW (essentially Git for Windows) and all other\n \t# platforms.. See the OS-specific code in start_command()\n-\tgrep -E \"^(error|fatal): cannot (exec|spawn) .*bad-hooks/test-hook\" err\n+\ttest_grep -E \"^(error|fatal): cannot (exec|spawn) .*bad-hooks/test-hook\" err\n '\n \n test_expect_success 'stdin to hooks' '\n@@ -826,8 +826,8 @@ test_expect_success 'git hook run -j2 warns for hooks not marked parallel=true'\n \t# neither hook has parallel=true\n \n \tgit hook run --allow-unknown-hook-name -j2 test-hook >out 2>err &&\n-\tgrep \"hook .hook-1. is not marked as parallel=true\" err &&\n-\tgrep \"hook .hook-2. is not marked as parallel=true\" err\n+\ttest_grep \"hook .hook-1. is not marked as parallel=true\" err &&\n+\ttest_grep \"hook .hook-2. is not marked as parallel=true\" err\n '\n \n test_expect_success 'hook.jobs=1 config runs hooks in series' '\n@@ -1068,7 +1068,7 @@ test_expect_success 'hook.jobs=-1 resolves to online_cpus()' '\n \tcpus=$(test-tool online-cpus) &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.txt\" \\\n \t\tgit hook run --allow-unknown-hook-name test-hook >out 2>err &&\n-\tgrep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n+\ttest_grep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n '\n \n test_expect_success 'hook.<event>.jobs=-1 resolves to online_cpus()' '\n@@ -1081,7 +1081,7 @@ test_expect_success 'hook.<event>.jobs=-1 resolves to online_cpus()' '\n \tcpus=$(test-tool online-cpus) &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.txt\" \\\n \t\tgit hook run --allow-unknown-hook-name test-hook >out 2>err &&\n-\tgrep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n+\ttest_grep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n '\n \n test_expect_success 'git hook run -j-1 resolves to online_cpus()' '\n@@ -1092,7 +1092,7 @@ test_expect_success 'git hook run -j-1 resolves to online_cpus()' '\n \tcpus=$(test-tool online-cpus) &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.txt\" \\\n \t\tgit hook run --allow-unknown-hook-name -j-1 test-hook >out 2>err &&\n-\tgrep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n+\ttest_grep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n '\n \n test_expect_success 'hook.jobs rejects values less than -1' '\ndiff --git a/t/t2004-checkout-cache-temp.sh b/t/t2004-checkout-cache-temp.sh\nindex 0afe0ff7ca..db06c321ed 100755\n--- a/t/t2004-checkout-cache-temp.sh\n+++ b/t/t2004-checkout-cache-temp.sh\n@@ -132,8 +132,8 @@ test_expect_success 'overriding --stage=all resets implied --temp' '\n test_expect_success '--stage=all --no-temp is rejected' '\n \trm -f path* .merge_* actual &&\n \ttest_must_fail git checkout-index --stage=all --no-temp -- path1 2>err &&\n-\tgrep -v \"already exists\" err &&\n-\tgrep \"options .--stage=all. and .--no-temp. cannot be used together\" err\n+\ttest_grep -v \"already exists\" err &&\n+\ttest_grep \"options .--stage=all. and .--no-temp. cannot be used together\" err\n '\n \n test_expect_success 'checkout some stages/one file to temporary files' '\ndiff --git a/t/t2019-checkout-ambiguous-ref.sh b/t/t2019-checkout-ambiguous-ref.sh\nindex 1fcef4be95..0ac35798c2 100755\n--- a/t/t2019-checkout-ambiguous-ref.sh\n+++ b/t/t2019-checkout-ambiguous-ref.sh\n@@ -19,7 +19,7 @@ test_expect_success 'checkout ambiguous ref succeeds' '\n '\n \n test_expect_success 'checkout produces ambiguity warning' '\n-\tgrep \"warning.*ambiguous\" stderr\n+\ttest_grep \"warning.*ambiguous\" stderr\n '\n \n test_expect_success 'checkout chooses branch over tag' '\n@@ -41,7 +41,7 @@ test_expect_success 'checkout vague ref succeeds' '\n '\n \n test_expect_success VAGUENESS_SUCCESS 'checkout produces ambiguity warning' '\n-\tgrep \"warning.*ambiguous\" stderr\n+\ttest_grep \"warning.*ambiguous\" stderr\n '\n \n test_expect_success VAGUENESS_SUCCESS 'checkout chooses branch over tag' '\ndiff --git a/t/t2024-checkout-dwim.sh b/t/t2024-checkout-dwim.sh\nindex a3b1449ef1..752e7f0e61 100755\n--- a/t/t2024-checkout-dwim.sh\n+++ b/t/t2024-checkout-dwim.sh\n@@ -311,7 +311,7 @@ test_expect_success 'loosely defined local base branch is reported correctly' '\n \tgit checkout loose >actual.raw 2>&1 &&\n \tsed -e \"s/loose/BRANCHNAME/g\" <actual.raw >actual &&\n \tstatus_uno_is_clean &&\n-\tgrep BRANCHNAME actual &&\n+\ttest_grep BRANCHNAME actual &&\n \n \ttest_cmp expect actual\n '\n@@ -324,7 +324,7 @@ test_expect_success 'reject when arg could be part of dwim branch' '\n \techo bar >dwim-arg &&\n \ttest_must_fail git checkout dwim-arg &&\n \ttest_must_fail git rev-parse refs/heads/dwim-arg -- &&\n-\tgrep bar dwim-arg\n+\ttest_grep bar dwim-arg\n '\n \n test_expect_success 'disambiguate dwim branch and checkout path (1)' '\n@@ -334,7 +334,7 @@ test_expect_success 'disambiguate dwim branch and checkout path (1)' '\n \techo bar >dwim-arg1 &&\n \tgit checkout -- dwim-arg1 &&\n \ttest_must_fail git rev-parse refs/heads/dwim-arg1 -- &&\n-\tgrep foo dwim-arg1\n+\ttest_grep foo dwim-arg1\n '\n \n test_expect_success 'disambiguate dwim branch and checkout path (2)' '\n@@ -344,7 +344,7 @@ test_expect_success 'disambiguate dwim branch and checkout path (2)' '\n \techo bar >dwim-arg2 &&\n \tgit checkout dwim-arg2 -- &&\n \tgit rev-parse refs/heads/dwim-arg2 -- &&\n-\tgrep bar dwim-arg2\n+\ttest_grep bar dwim-arg2\n '\n \n test_done\ndiff --git a/t/t2030-unresolve-info.sh b/t/t2030-unresolve-info.sh\nindex be3fcdde07..9af24b1204 100755\n--- a/t/t2030-unresolve-info.sh\n+++ b/t/t2030-unresolve-info.sh\n@@ -122,7 +122,7 @@ test_expect_success 'add records checkout -m undoes' '\n \tcheck_resolve_undo removed &&\n \techo the index and the work tree is unmerged again &&\n \tgit diff >actual &&\n-\tgrep \"^++<<<<<<<\" actual\n+\ttest_grep \"^++<<<<<<<\" actual\n '\n \n test_expect_success 'unmerge with plumbing' '\n@@ -173,7 +173,7 @@ test_expect_success 'rerere and rerere forget' '\n \ttest -f .git/rr-cache/$rerere_id/postimage &&\n \tgit checkout -m fi/le &&\n \techo resurrect the conflict &&\n-\tgrep \"^=======\" fi/le &&\n+\ttest_grep \"^=======\" fi/le &&\n \techo reresolve the conflict &&\n \tgit rerere &&\n \ttest \"z$(cat fi/le)\" = zdifferent &&\n@@ -199,7 +199,7 @@ test_expect_success 'rerere and rerere forget (subdirectory)' '\n \ttest -f .git/rr-cache/$rerere_id/postimage &&\n \t(cd fi && git checkout -m le) &&\n \techo resurrect the conflict &&\n-\tgrep \"^=======\" fi/le &&\n+\ttest_grep \"^=======\" fi/le &&\n \techo reresolve the conflict &&\n \t(cd fi && git rerere) &&\n \ttest \"z$(cat fi/le)\" = zdifferent &&\ndiff --git a/t/t2060-switch.sh b/t/t2060-switch.sh\nindex c91c4db936..cec2fe6876 100755\n--- a/t/t2060-switch.sh\n+++ b/t/t2060-switch.sh\n@@ -34,13 +34,13 @@ test_expect_success 'switch and detach' '\n \n test_expect_success 'suggestion to detach' '\n \ttest_must_fail git switch main^{commit} 2>stderr &&\n-\tgrep \"try again with the --detach option\" stderr\n+\ttest_grep \"try again with the --detach option\" stderr\n '\n \n test_expect_success 'suggestion to detach is suppressed with advice.suggestDetachingHead=false' '\n \ttest_config advice.suggestDetachingHead false &&\n \ttest_must_fail git switch main^{commit} 2>stderr &&\n-\t! grep \"try again with the --detach option\" stderr\n+\ttest_grep ! \"try again with the --detach option\" stderr\n '\n \n test_expect_success 'switch and detach current branch' '\n@@ -76,7 +76,7 @@ test_expect_success 'new orphan branch from empty' '\n \tgit switch --orphan new-orphan &&\n \ttest_commit orphan &&\n \tgit cat-file commit refs/heads/new-orphan >commit &&\n-\t! grep ^parent commit &&\n+\ttest_grep ! ^parent commit &&\n \tgit ls-files >tracked-files &&\n \techo orphan.t >expected &&\n \ttest_cmp expected tracked-files\ndiff --git a/t/t2070-restore.sh b/t/t2070-restore.sh\nindex 16d6348b69..2c222fb934 100755\n--- a/t/t2070-restore.sh\n+++ b/t/t2070-restore.sh\n@@ -216,7 +216,7 @@ test_expect_success 'restore with merge options are incompatible with certain op\n \t\t\"--staged --worktree --conflict=zdiff3\"\n \tdo\n \t\ttest_must_fail git restore $opts . 2>err &&\n-\t\tgrep \"cannot be used\" err || return\n+\t\ttest_grep \"cannot be used\" err || return\n \tdone\n '\n \ndiff --git a/t/t2080-parallel-checkout-basics.sh b/t/t2080-parallel-checkout-basics.sh\nindex 5ffe1a41e2..88a6ec1826 100755\n--- a/t/t2080-parallel-checkout-basics.sh\n+++ b/t/t2080-parallel-checkout-basics.sh\n@@ -200,13 +200,13 @@ test_expect_success 'parallel checkout respects --[no]-force' '\n \t\t# We expect 0 workers because there is nothing to be done\n \t\ttest_checkout_workers 0 git checkout HEAD &&\n \t\ttest_path_is_file D &&\n-\t\tgrep changed D &&\n-\t\tgrep changed F.t &&\n+\t\ttest_grep changed D &&\n+\t\ttest_grep changed F.t &&\n \n \t\ttest_checkout_workers 2 git checkout --force HEAD &&\n \t\ttest_path_is_dir D &&\n-\t\tgrep D/F D/F.t &&\n-\t\tgrep F F.t\n+\t\ttest_grep D/F D/F.t &&\n+\t\ttest_grep F F.t\n \t)\n '\n \n@@ -224,8 +224,8 @@ test_expect_success SYMLINKS 'parallel checkout checks for symlinks in leading d\n \n \t\ttest_checkout_workers 2 git checkout --force HEAD &&\n \t\t! test -h D &&\n-\t\tgrep D/A D/A.t &&\n-\t\tgrep D/B D/B.t\n+\t\ttest_grep D/A D/A.t &&\n+\t\ttest_grep D/B D/B.t\n \t)\n '\n \n@@ -268,7 +268,7 @@ test_expect_success '\"git checkout .\" report should not include failed entries'\n \t\t#  - missing-delay.a: the delay filter will drop this path\n \t\t#  - parallel-*.a: the blob will be missing\n \t\t#\n-\t\tgrep \"Updated 3 paths from the index\" err &&\n+\t\ttest_grep \"Updated 3 paths from the index\" err &&\n \t\ttest_stdout_line_count = 3 ls *.b &&\n \t\t! ls *.a\n \t)\ndiff --git a/t/t2081-parallel-checkout-collisions.sh b/t/t2081-parallel-checkout-collisions.sh\nindex f6fcfc0c1e..db45f5f30f 100755\n--- a/t/t2081-parallel-checkout-collisions.sh\n+++ b/t/t2081-parallel-checkout-collisions.sh\n@@ -78,8 +78,8 @@ test_expect_success CASE_INSENSITIVE_FS 'worker detects dirname collision' '\n \n \t# Check that it used the right number of workers and detected the collisions\n \ttest_workers_in_event_trace 2 trace &&\n-\tgrep \"category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/B.}\" trace &&\n-\tgrep \"category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/C.}\" trace\n+\ttest_grep \"category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/B.}\" trace &&\n+\ttest_grep \"category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/C.}\" trace\n '\n \n test_expect_success SYMLINKS,CASE_INSENSITIVE_FS 'do not follow symlinks colliding with leading dir' '\n@@ -115,11 +115,11 @@ test_expect_success CASE_INSENSITIVE_FS 'collision report on clone (w/ racy file\n \tset_checkout_config 2 0 &&\n \ttest_checkout_workers 2 git clone . clone-repo 2>stderr &&\n \n-\tgrep FILE_X stderr &&\n-\tgrep FILE_x stderr &&\n-\tgrep file_X stderr &&\n-\tgrep file_x stderr &&\n-\tgrep \"the following paths have collided\" stderr\n+\ttest_grep FILE_X stderr &&\n+\ttest_grep FILE_x stderr &&\n+\ttest_grep file_X stderr &&\n+\ttest_grep file_x stderr &&\n+\ttest_grep \"the following paths have collided\" stderr\n '\n \n # This test ensures that the collision report code is correctly looking for\n@@ -148,11 +148,11 @@ test_expect_success CASE_INSENSITIVE_FS,!MINGW,!CYGWIN \\\n \ttest_checkout_workers 2 \\\n \t\tgit -c core.ignoreCase=false clone . clone-repo 2>stderr &&\n \n-\tgrep FILE_X stderr &&\n-\tgrep FILE_x stderr &&\n-\tgrep file_X stderr &&\n-\tgrep file_x stderr &&\n-\tgrep \"the following paths have collided\" stderr &&\n+\ttest_grep FILE_X stderr &&\n+\ttest_grep FILE_x stderr &&\n+\ttest_grep file_X stderr &&\n+\ttest_grep file_x stderr &&\n+\ttest_grep \"the following paths have collided\" stderr &&\n \n \t# Check that only \"file_x\" was filtered\n \techo file_x >expected.log &&\ndiff --git a/t/t2082-parallel-checkout-attributes.sh b/t/t2082-parallel-checkout-attributes.sh\nindex 79fb11f139..1f43c2a267 100755\n--- a/t/t2082-parallel-checkout-attributes.sh\n+++ b/t/t2082-parallel-checkout-attributes.sh\n@@ -28,8 +28,8 @@ test_expect_success 'parallel-checkout with ident' '\n \t\trm A B &&\n \t\ttest_checkout_workers 2 git reset --hard &&\n \t\thexsz=$(test_oid hexsz) &&\n-\t\tgrep -E \"\\\\\\$Id: [0-9a-f]{$hexsz} \\\\\\$\" A &&\n-\t\tgrep \"\\\\\\$Id\\\\\\$\" B\n+\t\ttest_grep -E \"\\\\\\$Id: [0-9a-f]{$hexsz} \\\\\\$\" A &&\n+\t\ttest_grep \"\\\\\\$Id\\\\\\$\" B\n \t)\n '\n \n@@ -175,15 +175,15 @@ test_expect_success 'parallel-checkout and delayed checkout' '\n \tverify_checkout delayed &&\n \n \t# Check that the *.d files got to the delay queue and were filtered\n-\tgrep \"smudge W.d .* \\[DELAYED\\]\" delayed.log &&\n-\tgrep \"smudge X.d .* \\[DELAYED\\]\" delayed.log &&\n+\ttest_grep \"smudge W.d .* \\[DELAYED\\]\" delayed.log &&\n+\ttest_grep \"smudge X.d .* \\[DELAYED\\]\" delayed.log &&\n \ttest_cmp delayed/W.d original &&\n \ttest_cmp delayed/X.d original &&\n \n \t# Check that the parallel-eligible entries went to the right queue and\n \t# were not filtered\n-\t! grep \"smudge Y .* \\[DELAYED\\]\" delayed.log &&\n-\t! grep \"smudge Z .* \\[DELAYED\\]\" delayed.log &&\n+\ttest_grep ! \"smudge Y .* \\[DELAYED\\]\" delayed.log &&\n+\ttest_grep ! \"smudge Z .* \\[DELAYED\\]\" delayed.log &&\n \ttest_cmp delayed/Y original &&\n \ttest_cmp delayed/Z original\n '\ndiff --git a/t/t2103-update-index-ignore-missing.sh b/t/t2103-update-index-ignore-missing.sh\nindex 6938ecca86..fbfc86b1fa 100755\n--- a/t/t2103-update-index-ignore-missing.sh\n+++ b/t/t2103-update-index-ignore-missing.sh\n@@ -63,9 +63,9 @@ test_expect_success '--unmerged --refresh' '\n \tgit update-index --unmerged --refresh &&\n \techo 2 >two &&\n \ttest_must_fail git update-index --unmerged --refresh >actual &&\n-\tgrep two actual &&\n-\t! grep one actual &&\n-\t! grep three actual\n+\ttest_grep two actual &&\n+\ttest_grep ! one actual &&\n+\ttest_grep ! three actual\n '\n \n test_expect_success '--ignore-submodules --refresh (1)' '\ndiff --git a/t/t2200-add-update.sh b/t/t2200-add-update.sh\nindex 0a96655cfe..bb019d7a7c 100755\n--- a/t/t2200-add-update.sh\n+++ b/t/t2200-add-update.sh\n@@ -241,7 +241,7 @@ test_expect_success 'add -u avoids rename pairing on unmerged paths' '\n test_expect_success '\"add -u non-existent\" should fail' '\n \ttest_must_fail git add -u non-existent &&\n \tgit ls-files >actual &&\n-\t! grep \"non-existent\" actual\n+\ttest_grep ! \"non-existent\" actual\n '\n \n test_expect_success '\"commit -a\" implies \"add -u\" if index becomes empty' '\ndiff --git a/t/t2203-add-intent.sh b/t/t2203-add-intent.sh\nindex 44c1936e4d..69fef7e646 100755\n--- a/t/t2203-add-intent.sh\n+++ b/t/t2203-add-intent.sh\n@@ -57,7 +57,7 @@ test_expect_success 'intent to add does not clobber existing paths' '\n \tgit add -N file elif &&\n \tempty=$(git hash-object --stdin </dev/null) &&\n \tgit ls-files -s >actual &&\n-\t! grep \"$empty\" actual\n+\ttest_grep ! \"$empty\" actual\n '\n \n test_expect_success 'i-t-a entry is simply ignored' '\n@@ -124,7 +124,7 @@ test_expect_success 'cache-tree does not ignore dir that has i-t-a entries' '\n \t\tgit add -N 2/1 &&\n \t\tgit commit -m committed &&\n \t\tgit ls-tree -r HEAD >actual &&\n-\t\tgrep 2/2 actual\n+\t\ttest_grep 2/2 actual\n \t)\n '\n \n@@ -312,7 +312,7 @@ test_expect_success 'apply --intent-to-add' '\n \techo new >new-ita &&\n \tgit add -N new-ita &&\n \tgit diff >expected &&\n-\tgrep \"new file\" expected &&\n+\ttest_grep \"new file\" expected &&\n \tgit reset --hard &&\n \tgit apply --intent-to-add expected &&\n \tgit diff >actual &&\ndiff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh\nindex 58b4445cc4..87b926728a 100755\n--- a/t/t2400-worktree-add.sh\n+++ b/t/t2400-worktree-add.sh\n@@ -122,7 +122,7 @@ test_expect_success 'die the same branch is already checked out' '\n \t(\n \t\tcd here &&\n \t\ttest_must_fail git checkout newmain 2>actual &&\n-\t\tgrep \"already used by worktree at\" actual\n+\t\ttest_grep \"already used by worktree at\" actual\n \t)\n '\n \n@@ -139,7 +139,7 @@ test_expect_success 'refuse to reset a branch in use elsewhere' '\n \t\tgit rev-parse --verify refs/heads/newmain >new.branch &&\n \t\tgit rev-parse --verify HEAD >new.head &&\n \n-\t\tgrep \"already used by worktree at\" error &&\n+\t\ttest_grep \"already used by worktree at\" error &&\n \t\ttest_cmp old.branch new.branch &&\n \t\ttest_cmp old.head new.head &&\n \n@@ -328,7 +328,7 @@ test_wt_add_excl () {\n \tlocal opts=\"$*\" &&\n \ttest_expect_success \"'worktree add' with '$opts' has mutually exclusive options\" '\n \t\ttest_must_fail git worktree add $opts 2>actual &&\n-\t\tgrep -E \"fatal:( options)? .* cannot be used together\" actual\n+\t\ttest_grep -E \"fatal:( options)? .* cannot be used together\" actual\n \t'\n }\n \n@@ -436,13 +436,13 @@ test_wt_add_orphan_hint () {\n \t\t(cd repo && test_commit commit) &&\n \t\tgit -C repo switch --orphan noref &&\n \t\ttest_must_fail git -C repo worktree add $opts foobar/ 2>actual &&\n-\t\t! grep \"error: unknown switch\" actual &&\n-\t\tgrep \"hint: If you meant to create a worktree containing a new unborn branch\" actual &&\n+\t\ttest_grep ! \"error: unknown switch\" actual &&\n+\t\ttest_grep \"hint: If you meant to create a worktree containing a new unborn branch\" actual &&\n \t\tif [ $use_branch -eq 1 ]\n \t\tthen\n-\t\t\tgrep -E \"^hint: +git worktree add --orphan -b [^ ]+ [^ ]+$\" actual\n+\t\t\ttest_grep -E \"^hint: +git worktree add --orphan -b [^ ]+ [^ ]+$\" actual\n \t\telse\n-\t\t\tgrep -E \"^hint: +git worktree add --orphan [^ ]+$\" actual\n+\t\t\ttest_grep -E \"^hint: +git worktree add --orphan [^ ]+$\" actual\n \t\tfi\n \n \t'\n@@ -457,8 +457,8 @@ test_expect_success \"'worktree add' doesn't show orphan hint in bad/orphan HEAD\n \tgit init repo &&\n \t(cd repo && test_commit commit) &&\n \ttest_must_fail git -C repo worktree add --quiet foobar_branch foobar/ 2>actual &&\n-\t! grep \"error: unknown switch\" actual &&\n-\t! grep \"hint: If you meant to create a worktree containing a new unborn branch\" actual\n+\ttest_grep ! \"error: unknown switch\" actual &&\n+\ttest_grep ! \"hint: If you meant to create a worktree containing a new unborn branch\" actual\n '\n \n test_expect_success 'local clone from linked checkout' '\n@@ -469,7 +469,7 @@ test_expect_success 'local clone from linked checkout' '\n test_expect_success 'local clone --shared from linked checkout' '\n \tgit -C bare worktree add --detach ../baretree &&\n \tgit clone --local --shared baretree bare-clone &&\n-\tgrep /bare/ bare-clone/.git/objects/info/alternates\n+\ttest_grep /bare/ bare-clone/.git/objects/info/alternates\n '\n \n test_expect_success '\"add\" worktree with --no-checkout' '\n@@ -491,7 +491,7 @@ test_expect_success 'put a worktree under rebase' '\n \t\tset_fake_editor &&\n \t\tFAKE_LINES=\"edit 1\" git rebase -i HEAD^ &&\n \t\tgit worktree list >actual &&\n-\t\tgrep \"under-rebase.*detached HEAD\" actual\n+\t\ttest_grep \"under-rebase.*detached HEAD\" actual\n \t)\n '\n \n@@ -533,7 +533,7 @@ test_expect_success 'checkout a branch under bisect' '\n \t\tgit bisect bad &&\n \t\tgit bisect good HEAD~2 &&\n \t\tgit worktree list >actual &&\n-\t\tgrep \"under-bisect.*detached HEAD\" actual &&\n+\t\ttest_grep \"under-bisect.*detached HEAD\" actual &&\n \t\ttest_must_fail git worktree add new-bisect under-bisect &&\n \t\ttest_path_is_missing new-bisect\n \t)\ndiff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh\nindex 93f92e854a..eeaf800d74 100755\n--- a/t/t2402-worktree-list.sh\n+++ b/t/t2402-worktree-list.sh\n@@ -101,8 +101,8 @@ test_expect_success '\"list\" all worktrees with locked annotation' '\n \tgit worktree lock locked &&\n \ttest_when_finished \"git worktree unlock locked\" &&\n \tgit worktree list >out &&\n-\tgrep \"/locked  *[0-9a-f].* locked$\" out &&\n-\t! grep \"/unlocked  *[0-9a-f].* locked$\" out\n+\ttest_grep \"/locked  *[0-9a-f].* locked$\" out &&\n+\ttest_grep ! \"/unlocked  *[0-9a-f].* locked$\" out\n '\n \n test_expect_success '\"list\" all worktrees --porcelain with locked' '\n@@ -143,8 +143,8 @@ test_expect_success '\"list\" all worktrees with prunable annotation' '\n \tgit worktree add --detach unprunable &&\n \trm -rf prunable &&\n \tgit worktree list >out &&\n-\tgrep \"/prunable  *[0-9a-f].* prunable$\" out &&\n-\t! grep \"/unprunable  *[0-9a-f].* prunable$\" out\n+\ttest_grep \"/prunable  *[0-9a-f].* prunable$\" out &&\n+\ttest_grep ! \"/unprunable  *[0-9a-f].* prunable$\" out\n '\n \n test_expect_success '\"list\" all worktrees --porcelain with prunable' '\n@@ -162,8 +162,8 @@ test_expect_success '\"list\" all worktrees with prunable consistent with \"prune\"'\n \tgit worktree add --detach unprunable &&\n \trm -rf prunable &&\n \tgit worktree list >out &&\n-\tgrep \"/prunable  *[0-9a-f].* prunable$\" out &&\n-\t! grep \"/unprunable  *[0-9a-f].* unprunable$\" out &&\n+\ttest_grep \"/prunable  *[0-9a-f].* prunable$\" out &&\n+\ttest_grep ! \"/unprunable  *[0-9a-f].* unprunable$\" out &&\n \tgit worktree prune --verbose 2>out &&\n \ttest_grep \"^Removing worktrees/prunable\" out &&\n \ttest_grep ! \"^Removing worktrees/unprunable\" out\n@@ -184,7 +184,7 @@ test_expect_success '\"list\" all worktrees --verbose with locked' '\n \techo \"$(git -C locked2 rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)\" >expect &&\n \tprintf \"\\tlocked: with reason\\n\" >>expect &&\n \tgit worktree list --verbose >out &&\n-\tgrep \"/locked1  *[0-9a-f].* locked$\" out &&\n+\ttest_grep \"/locked1  *[0-9a-f].* locked$\" out &&\n \tsed -n \"s/  */ /g;/\\/locked2  *[0-9a-f].*$/,/locked: .*$/p\" <out >actual &&\n \ttest_cmp actual expect\n '\n@@ -266,7 +266,7 @@ test_expect_success 'broken main worktree still at the top' '\n \t\ttest_cmp ../expected actual &&\n \t\tgit worktree list >out &&\n \t\thead -n 1 out >actual.2 &&\n-\t\tgrep -F \"(error)\" actual.2\n+\t\ttest_grep -F \"(error)\" actual.2\n \t)\n '\n \ndiff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh\nindex 0bb33e8b1b..69768c1207 100755\n--- a/t/t2403-worktree-move.sh\n+++ b/t/t2403-worktree-move.sh\n@@ -75,8 +75,8 @@ test_expect_success 'move worktree' '\n \tgit worktree move source destination &&\n \ttest_path_is_missing source &&\n \tgit worktree list --porcelain >out &&\n-\tgrep \"^worktree.*/destination$\" out &&\n-\t! grep \"^worktree.*/source$\" out &&\n+\ttest_grep \"^worktree.*/destination$\" out &&\n+\ttest_grep ! \"^worktree.*/source$\" out &&\n \tgit -C destination log --format=%s >actual2 &&\n \techo init >expected2 &&\n \ttest_cmp expected2 actual2\n@@ -92,7 +92,7 @@ test_expect_success 'move worktree to another dir' '\n \ttest_when_finished \"git worktree move some-dir/destination destination\" &&\n \ttest_path_is_missing destination &&\n \tgit worktree list --porcelain >out &&\n-\tgrep \"^worktree.*/some-dir/destination$\" out &&\n+\ttest_grep \"^worktree.*/some-dir/destination$\" out &&\n \tgit -C some-dir/destination log --format=%s >actual2 &&\n \techo init >expected2 &&\n \ttest_cmp expected2 actual2\ndiff --git a/t/t2405-worktree-submodule.sh b/t/t2405-worktree-submodule.sh\nindex 11018f37c7..f5c94d490b 100755\n--- a/t/t2405-worktree-submodule.sh\n+++ b/t/t2405-worktree-submodule.sh\n@@ -36,7 +36,7 @@ test_expect_success 'add superproject worktree' '\n \n test_expect_failure 'submodule is checked out just after worktree add' '\n \tgit -C worktree diff --submodule main\"^!\" >out &&\n-\tgrep \"file1 updated\" out\n+\ttest_grep \"file1 updated\" out\n '\n \n test_expect_success 'add superproject worktree and initialize submodules' '\n@@ -46,7 +46,7 @@ test_expect_success 'add superproject worktree and initialize submodules' '\n \n test_expect_success 'submodule is checked out just after submodule update in linked worktree' '\n \tgit -C worktree-submodule-update diff --submodule main\"^!\" >out &&\n-\tgrep \"file1 updated\" out\n+\ttest_grep \"file1 updated\" out\n '\n \n test_expect_success 'add superproject worktree and manually add submodule worktree' '\n@@ -56,7 +56,7 @@ test_expect_success 'add superproject worktree and manually add submodule worktr\n \n test_expect_success 'submodule is checked out after manually adding submodule worktree' '\n \tgit -C linked_submodule diff --submodule main\"^!\" >out &&\n-\tgrep \"file1 updated\" out\n+\ttest_grep \"file1 updated\" out\n '\n \n test_expect_success 'checkout --recurse-submodules uses $GIT_DIR for submodules in a linked worktree' '\ndiff --git a/t/t2407-worktree-heads.sh b/t/t2407-worktree-heads.sh\nindex 57c201869f..a6d7e0f83b 100755\n--- a/t/t2407-worktree-heads.sh\n+++ b/t/t2407-worktree-heads.sh\n@@ -41,10 +41,10 @@ test_expect_success 'refuse to overwrite: checked out in worktree' '\n \tfor i in 1 2 3 4\n \tdo\n \t\ttest_must_fail git branch -f wt-$i HEAD 2>err &&\n-\t\tgrep \"cannot force update the branch\" err &&\n+\t\ttest_grep \"cannot force update the branch\" err &&\n \n \t\ttest_must_fail git branch -D wt-$i 2>err &&\n-\t\tgrep \"cannot delete branch\" err || return 1\n+\t\ttest_grep \"cannot delete branch\" err || return 1\n \tdone\n '\n \n@@ -57,7 +57,7 @@ test_expect_success 'refuse to overwrite: worktree in bisect' '\n \tgit -C wt-4 bisect good wt-1 &&\n \n \ttest_must_fail git branch -f wt-4 HEAD 2>err &&\n-\tgrep \"cannot force update the branch '\\''wt-4'\\'' used by worktree at.*wt-4\" err\n+\ttest_grep \"cannot force update the branch '\\''wt-4'\\'' used by worktree at.*wt-4\" err\n '\n \n test_expect_success 'refuse to overwrite: worktree in rebase (apply)' '\n@@ -67,7 +67,7 @@ test_expect_success 'refuse to overwrite: worktree in rebase (apply)' '\n \ttest_must_fail git -C wt-2 rebase --apply conflict-2 &&\n \n \ttest_must_fail git branch -f wt-2 HEAD 2>err &&\n-\tgrep \"cannot force update the branch '\\''wt-2'\\'' used by worktree at.*wt-2\" err\n+\ttest_grep \"cannot force update the branch '\\''wt-2'\\'' used by worktree at.*wt-2\" err\n '\n \n test_expect_success 'refuse to overwrite: worktree in rebase (merge)' '\n@@ -77,7 +77,7 @@ test_expect_success 'refuse to overwrite: worktree in rebase (merge)' '\n \ttest_must_fail git -C wt-2 rebase conflict-2 &&\n \n \ttest_must_fail git branch -f wt-2 HEAD 2>err &&\n-\tgrep \"cannot force update the branch '\\''wt-2'\\'' used by worktree at.*wt-2\" err\n+\ttest_grep \"cannot force update the branch '\\''wt-2'\\'' used by worktree at.*wt-2\" err\n '\n \n test_expect_success 'refuse to overwrite: worktree in rebase with --update-refs' '\n@@ -89,19 +89,19 @@ test_expect_success 'refuse to overwrite: worktree in rebase with --update-refs'\n \tfor i in 3 4\n \tdo\n \t\ttest_must_fail git branch -f can-be-updated HEAD 2>err &&\n-\t\tgrep \"cannot force update the branch '\\''can-be-updated'\\'' used by worktree at.*wt-3\" err ||\n+\t\ttest_grep \"cannot force update the branch '\\''can-be-updated'\\'' used by worktree at.*wt-3\" err ||\n \t\t\treturn 1\n \tdone\n '\n \n test_expect_success 'refuse to fetch over ref: checked out' '\n \ttest_must_fail git fetch server +refs/heads/wt-3:refs/heads/wt-3 2>err &&\n-\tgrep \"refusing to fetch into branch '\\''refs/heads/wt-3'\\''\" err &&\n+\ttest_grep \"refusing to fetch into branch '\\''refs/heads/wt-3'\\''\" err &&\n \n \t# General fetch into refs/heads/ will fail on first ref,\n \t# so use a generic error message check.\n \ttest_must_fail git fetch server +refs/heads/*:refs/heads/* 2>err &&\n-\tgrep \"refusing to fetch into branch\" err\n+\ttest_grep \"refusing to fetch into branch\" err\n '\n \n test_expect_success 'refuse to fetch over ref: worktree in bisect' '\n@@ -113,7 +113,7 @@ test_expect_success 'refuse to fetch over ref: worktree in bisect' '\n \tgit -C wt-4 bisect good wt-1 &&\n \n \ttest_must_fail git fetch server +refs/heads/wt-4:refs/heads/wt-4 2>err &&\n-\tgrep \"refusing to fetch into branch\" err\n+\ttest_grep \"refusing to fetch into branch\" err\n '\n \n test_expect_success 'refuse to fetch over ref: worktree in rebase' '\n@@ -123,7 +123,7 @@ test_expect_success 'refuse to fetch over ref: worktree in rebase' '\n \ttest_must_fail git -C wt-3 rebase conflict-3 &&\n \n \ttest_must_fail git fetch server +refs/heads/wt-3:refs/heads/wt-3 2>err &&\n-\tgrep \"refusing to fetch into branch\" err\n+\ttest_grep \"refusing to fetch into branch\" err\n '\n \n test_expect_success 'refuse to overwrite when in error states' '\n@@ -149,7 +149,7 @@ test_expect_success 'refuse to overwrite when in error states' '\n \tfor i in 1 2\n \tdo\n \t\ttest_must_fail git branch -f fake-$i HEAD 2>err &&\n-\t\tgrep \"cannot force update the branch '\\''fake-$i'\\'' used by worktree at\" err ||\n+\t\ttest_grep \"cannot force update the branch '\\''fake-$i'\\'' used by worktree at\" err ||\n \t\t\treturn 1\n \tdone\n '\n@@ -161,13 +161,13 @@ test_expect_success 'refuse to overwrite during rebase with --update-refs' '\n \t(\n \t\tset_cat_todo_editor &&\n \t\ttest_must_fail git rebase -i --update-refs HEAD~3 >todo &&\n-\t\t! grep \"update-refs\" todo\n+\t\ttest_grep ! \"update-refs\" todo\n \t) &&\n \tgit branch -f allow-update HEAD~2 &&\n \t(\n \t\tset_cat_todo_editor &&\n \t\ttest_must_fail git rebase -i --update-refs HEAD~3 >todo &&\n-\t\tgrep \"update-ref refs/heads/allow-update\" todo\n+\t\ttest_grep \"update-ref refs/heads/allow-update\" todo\n \t)\n '\n \ndiff --git a/t/t2500-untracked-overwriting.sh b/t/t2500-untracked-overwriting.sh\nindex 5c0bf4d21f..8e84f29e8d 100755\n--- a/t/t2500-untracked-overwriting.sh\n+++ b/t/t2500-untracked-overwriting.sh\n@@ -51,7 +51,7 @@ test_expect_success 'reset --merge will preserve untracked files/dirs' '\n \n \t\ttest_must_fail git reset --merge work 2>error &&\n \t\ttest_cmp expect foo.t/file &&\n-\t\tgrep \"Updating .foo.t. would lose untracked files\" error\n+\t\ttest_grep \"Updating .foo.t. would lose untracked files\" error\n \t)\n '\n \n@@ -66,7 +66,7 @@ test_expect_success 'reset --keep will preserve untracked files/dirs' '\n \n \t\ttest_must_fail git reset --merge work 2>error &&\n \t\ttest_cmp expect foo.t/file &&\n-\t\tgrep \"Updating.*foo.t.*would lose untracked files\" error\n+\t\ttest_grep \"Updating.*foo.t.*would lose untracked files\" error\n \t)\n '\n \n@@ -214,7 +214,7 @@ test_expect_success 'git am --abort and untracked dir vs. unmerged file' '\n \n \t\ttest_must_fail git am --abort 2>errors &&\n \t\ttest_path_is_dir filler &&\n-\t\tgrep \"Updating .filler. would lose untracked files in it\" errors\n+\t\ttest_grep \"Updating .filler. would lose untracked files in it\" errors\n \t)\n '\n \n@@ -237,7 +237,7 @@ test_expect_success 'git am --skip and untracked dir vs deleted file' '\n \t\t# Change our mind about resolutions, just skip this patch\n \t\ttest_must_fail git am --skip 2>errors &&\n \t\ttest_path_is_dir newfile &&\n-\t\tgrep \"Updating .newfile. would lose untracked files in it\" errors\n+\t\ttest_grep \"Updating .newfile. would lose untracked files in it\" errors\n \t)\n '\n \ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex be9140bbaa..eb6451d71d 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -114,7 +114,7 @@ test_expect_success 'merge fails if cwd needs to be removed; recursive friendly'\n \t) &&\n \n \ttest_path_is_dir dirORfile &&\n-\tgrep \"Refusing to remove the current working directory\" error\n+\ttest_grep \"Refusing to remove the current working directory\" error\n '\n \n test_expect_success 'merge fails if cwd needs to be removed' '\n@@ -179,7 +179,7 @@ test_incidental_untracked_dir_removal () {\n test_expect_success 'clean does not remove cwd incidentally' '\n \ttest_incidental_untracked_dir_removal \\\n \t\tgit -C .. clean -fd -e warnings . >warnings &&\n-\tgrep \"Refusing to remove current working directory\" warnings\n+\ttest_grep \"Refusing to remove current working directory\" warnings\n '\n \n test_expect_success 'stash does not remove cwd incidentally' '\ndiff --git a/t/t3001-ls-files-others-exclude.sh b/t/t3001-ls-files-others-exclude.sh\nindex 202fb8d9ea..29a0a25b30 100755\n--- a/t/t3001-ls-files-others-exclude.sh\n+++ b/t/t3001-ls-files-others-exclude.sh\n@@ -161,21 +161,21 @@ test_expect_success 'trailing slash in exclude forces directory match (1)' '\n \n \t>two &&\n \tgit ls-files --others --exclude=two/ >output &&\n-\tgrep \"^two\" output\n+\ttest_grep \"^two\" output\n \n '\n \n test_expect_success 'trailing slash in exclude forces directory match (2)' '\n \n \tgit ls-files --others --exclude=one/a.1/ >output &&\n-\tgrep \"^one/a.1\" output\n+\ttest_grep \"^one/a.1\" output\n \n '\n \n test_expect_success 'negated exclude matches can override previous ones' '\n \n \tgit ls-files --others --exclude=\"a.*\" --exclude=\"!a.1\" >output &&\n-\tgrep \"^a.1\" output\n+\ttest_grep \"^a.1\" output\n '\n \n test_expect_success 'excluded directory overrides content patterns' '\ndiff --git a/t/t3007-ls-files-recurse-submodules.sh b/t/t3007-ls-files-recurse-submodules.sh\nindex 61771eec83..218964edab 100755\n--- a/t/t3007-ls-files-recurse-submodules.sh\n+++ b/t/t3007-ls-files-recurse-submodules.sh\n@@ -302,7 +302,7 @@ test_expect_success '--recurse-submodules does not support --error-unmatch' '\n test_expect_success '--recurse-submodules parses submodule repo config' '\n \ttest_config -C submodule index.sparse \"invalid non-boolean value\" &&\n \ttest_must_fail git ls-files --recurse-submodules 2>err &&\n-\tgrep \"bad boolean config value\" err\n+\ttest_grep \"bad boolean config value\" err\n '\n \n test_expect_success '--recurse-submodules parses submodule worktree config' '\n@@ -310,7 +310,7 @@ test_expect_success '--recurse-submodules parses submodule worktree config' '\n \ttest_config -C submodule --worktree index.sparse \"invalid non-boolean value\" &&\n \n \ttest_must_fail git ls-files --recurse-submodules 2>err &&\n-\tgrep \"bad boolean config value\" err\n+\ttest_grep \"bad boolean config value\" err\n '\n \n test_expect_success '--recurse-submodules submodules ignore super project worktreeConfig extension' '\n@@ -329,7 +329,7 @@ test_expect_success '--recurse-submodules submodules ignore super project worktr\n \t# With extensions.worktreeConfig disabled in the submodule, the invalid\n \t# worktree config is not picked up.\n \tgit ls-files --recurse-submodules 2>err &&\n-\t! grep \"bad boolean config value\" err\n+\ttest_grep ! \"bad boolean config value\" err\n '\n \n test_incompatible_with_recurse_submodules () {\ndiff --git a/t/t3200-branch.sh b/t/t3200-branch.sh\nindex e7829c2c4b..bd839998b1 100755\n--- a/t/t3200-branch.sh\n+++ b/t/t3200-branch.sh\n@@ -204,7 +204,7 @@ test_expect_success 'git branch -M baz bam should succeed when baz is checked ou\n \n test_expect_success 'git branch -M baz bam should add entries to HEAD reflog' '\n \tgit reflog show HEAD >actual &&\n-\tgrep \"HEAD@{0}: Branch: renamed refs/heads/baz to refs/heads/bam\" actual\n+\ttest_grep \"HEAD@{0}: Branch: renamed refs/heads/baz to refs/heads/bam\" actual\n '\n \n test_expect_success 'git branch -M should leave orphaned HEAD alone' '\n@@ -339,7 +339,7 @@ test_expect_success 'git branch -d on orphan HEAD (unmerged)' '\n \ttest_when_finished \"git branch -D to-delete\" &&\n \tgit branch to-delete main &&\n \ttest_must_fail git branch -d to-delete 2>err &&\n-\tgrep \"not fully merged\" err\n+\ttest_grep \"not fully merged\" err\n '\n \n test_expect_success 'git branch -d on orphan HEAD (unmerged, graph)' '\n@@ -350,7 +350,7 @@ test_expect_success 'git branch -d on orphan HEAD (unmerged, graph)' '\n \ttest_when_finished \"rm -rf .git/objects/commit-graph*\" &&\n \tgit commit-graph write --reachable &&\n \ttest_must_fail git branch -d to-delete 2>err &&\n-\tgrep \"not fully merged\" err\n+\ttest_grep \"not fully merged\" err\n '\n \n test_expect_success 'git branch -v -d t should work' '\n@@ -712,7 +712,7 @@ test_expect_success 'git branch -C c1 c2 should succeed when c1 is checked out'\n test_expect_success 'git branch -C c1 c2 should never touch HEAD' '\n \tmsg=\"Branch: copied refs/heads/c1 to refs/heads/c2\" &&\n \tgit reflog HEAD >actual &&\n-\t! grep \"$msg$\" actual\n+\ttest_grep ! \"$msg$\" actual\n '\n \n test_expect_success 'git branch -C main should work when main is checked out' '\n@@ -930,7 +930,7 @@ test_expect_success 'deleting currently checked out branch fails' '\n \tgit worktree add -b my7 my7 &&\n \ttest_must_fail git -C my7 branch -d my7 &&\n \ttest_must_fail git branch -d my7 2>actual &&\n-\tgrep \"^error: cannot delete branch .my7. used by worktree at \" actual &&\n+\ttest_grep \"^error: cannot delete branch .my7. used by worktree at \" actual &&\n \trm -r my7 &&\n \tgit worktree prune\n '\n@@ -941,7 +941,7 @@ test_expect_success 'deleting in-use branch fails' '\n \tgit -C my7 bisect start HEAD HEAD~2 &&\n \ttest_must_fail git -C my7 branch -d my7 &&\n \ttest_must_fail git branch -d my7 2>actual &&\n-\tgrep \"^error: cannot delete branch .my7. used by worktree at \" actual &&\n+\ttest_grep \"^error: cannot delete branch .my7. used by worktree at \" actual &&\n \trm -r my7 &&\n \tgit worktree prune\n '\ndiff --git a/t/t3202-show-branch.sh b/t/t3202-show-branch.sh\nindex a1139f79e2..f402334936 100755\n--- a/t/t3202-show-branch.sh\n+++ b/t/t3202-show-branch.sh\n@@ -123,13 +123,13 @@ test_expect_success 'show-branch --sparse' '\n \tgit commit --allow-empty -m \"another\" &&\n \n \tgit show-branch --sparse >out &&\n-\tgrep \"merge 1 and 10 to make A\" out &&\n+\ttest_grep \"merge 1 and 10 to make A\" out &&\n \n \tgit show-branch >out &&\n-\t! grep \"merge 1 and 10 to make A\" out &&\n+\ttest_grep ! \"merge 1 and 10 to make A\" out &&\n \n \tgit show-branch --no-sparse >out &&\n-\t! grep \"merge 1 and 10 to make A\" out\n+\ttest_grep ! \"merge 1 and 10 to make A\" out\n '\n \n test_expect_success 'setup show branch --list' '\n@@ -189,7 +189,7 @@ while read combo\n do\n \ttest_expect_success \"show-branch $combo (should fail)\" '\n \t\ttest_must_fail git show-branch $combo 2>error &&\n-\t\tgrep -e \"cannot be used together\" -e \"usage:\" error\n+\t\ttest_grep -e \"cannot be used together\" -e \"usage:\" error\n \t'\n done <<\\EOF\n --all --reflog\n@@ -203,7 +203,7 @@ for opt in topo-order date-order reflog\n do\n \ttest_expect_success \"show-branch --no-$opt (should fail)\" '\n \t\ttest_must_fail git show-branch --no-$opt 2>err &&\n-\t\tgrep \"unknown option .no-$opt.\" err\n+\t\ttest_grep \"unknown option .no-$opt.\" err\n \t'\n done\n \ndiff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh\nindex a6bd88a58d..7c44186341 100755\n--- a/t/t3203-branch-output.sh\n+++ b/t/t3203-branch-output.sh\n@@ -64,7 +64,7 @@ test_expect_success 'git branch -r shows remote branches' '\n \n test_expect_success 'git branch --no-remotes is rejected' '\n \ttest_must_fail git branch --no-remotes 2>err &&\n-\tgrep \"unknown option .no-remotes.\" err\n+\ttest_grep \"unknown option .no-remotes.\" err\n '\n \n cat >expect <<'EOF'\n@@ -85,7 +85,7 @@ test_expect_success 'git branch -a shows local and remote branches' '\n \n test_expect_success 'git branch --no-all is rejected' '\n \ttest_must_fail git branch --no-all 2>err &&\n-\tgrep \"unknown option .no-all.\" err\n+\ttest_grep \"unknown option .no-all.\" err\n '\n \n cat >expect <<'EOF'\ndiff --git a/t/t3206-range-diff.sh b/t/t3206-range-diff.sh\nindex 1e812df806..ef92704de3 100755\n--- a/t/t3206-range-diff.sh\n+++ b/t/t3206-range-diff.sh\n@@ -538,10 +538,10 @@ do\n \t\t\tmain..unmodified >actual &&\n \t\ttest_line_count = 5 actual &&\n \t\ttest_grep \"^Range-diff:$\" 0000-* &&\n-\t\tgrep \"= 1: .* s/5/A\" 0000-* &&\n-\t\tgrep \"= 2: .* s/4/A\" 0000-* &&\n-\t\tgrep \"= 3: .* s/11/B\" 0000-* &&\n-\t\tgrep \"= 4: .* s/12/B\" 0000-*\n+\t\ttest_grep \"= 1: ."},{"id":"545436","messageId":"e5ecb37401502ddcdee4249a9e6ce783a4a7a487.1781323575.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v2.git.1781323575.gitgitgadget@gmail.com","subject":"[PATCH v2 6/6] t: add greplint to detect bare grep assertions","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-06-13T04:06:15Z","receivedAt":"2026-06-13T04:06:35Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nWithout a lint guard, bare grep assertions will creep back into\ntests over time, defeating the previous commit's conversion.\n\nAdd greplint.pl to catch bare 'grep' used as a test assertion\n(where 'test_grep' should be used) and '! test_grep' (where\n'test_grep !' should be used).\n\ngreplint.pl reuses the shared shell parser from lib-shell-parser.pl\nto tokenize test bodies.  The Lexer collapses heredocs, command\nsubstitutions, and quoted strings into single tokens, so 'grep'\nappearing inside these contexts is not flagged.  A flat walk over\nthe token stream tracks command position and pipeline state to\ndistinguish assertion greps from filter greps.\n\nFor double-quoted test bodies, a source-line walk counts\nbackslash-continuation lines that the Lexer consumes without\nemitting into the body text, adjusting the reported line number\naccordingly.\n\nAdd test fixtures in greplint/ (modeled on chainlint/) covering\ndetection of bare grep assertions, correct skipping of filters,\npipelines, redirects, command substitutions, and lint-ok annotations.\n\nWire into the Makefile as:\n  - test-greplint: runs greplint.pl on $(T) $(THELPERS) $(TPERF)\n  - check-greplint: runs greplint.pl on fixtures, diffs against expected\n  - clean-greplint: removes temp dir\n\nAdd eol=lf entries in t/.gitattributes for greplint fixtures,\nmatching chainlint, so that check-greplint passes on Windows\nwhere core.autocrlf would otherwise cause CRLF mismatches\nbetween expected and actual output.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/.gitattributes                              |   2 +\n t/Makefile                                    |  29 ++-\n t/greplint-cat.pl                             |  27 ++\n t/greplint.pl                                 | 241 ++++++++++++++++++\n t/greplint/bare-grep-after-and.expect         |   1 +\n t/greplint/bare-grep-after-and.test           |   4 +\n t/greplint/bare-grep-after-semicolon.expect   |   1 +\n t/greplint/bare-grep-after-semicolon.test     |   4 +\n t/greplint/bare-grep-compound-body.expect     |   3 +\n t/greplint/bare-grep-compound-body.test       |  17 ++\n t/greplint/bare-grep-count-mode.expect        |   1 +\n t/greplint/bare-grep-count-mode.test          |   3 +\n t/greplint/bare-grep-explicit-pattern.expect  |   1 +\n t/greplint/bare-grep-explicit-pattern.test    |   3 +\n t/greplint/bare-grep-flags.expect             |   1 +\n t/greplint/bare-grep-flags.test               |   3 +\n t/greplint/bare-grep-lint-ok.expect           |   0\n t/greplint/bare-grep-lint-ok.test             |   4 +\n t/greplint/bare-grep-negated.expect           |   1 +\n t/greplint/bare-grep-negated.test             |   3 +\n t/greplint/bare-grep-pattern-file.expect      |   1 +\n t/greplint/bare-grep-pattern-file.test        |   3 +\n t/greplint/bare-grep-simple.expect            |   1 +\n t/greplint/bare-grep-simple.test              |   3 +\n t/greplint/bare-grep-subshell.expect          |   1 +\n t/greplint/bare-grep-subshell.test            |   5 +\n .../dqstring-continuation-offset.expect       |   1 +\n t/greplint/dqstring-continuation-offset.test  |  11 +\n t/greplint/filter-command-substitution.expect |   0\n t/greplint/filter-command-substitution.test   |   3 +\n t/greplint/filter-pipe-input.expect           |   0\n t/greplint/filter-pipe-input.test             |   3 +\n t/greplint/filter-pipe-output.expect          |   0\n t/greplint/filter-pipe-output.test            |   3 +\n t/greplint/filter-redirect-output.expect      |   0\n t/greplint/filter-redirect-output.test        |   3 +\n t/greplint/filter-stdin-redirect.expect       |   0\n t/greplint/filter-stdin-redirect.test         |   3 +\n t/greplint/grep-as-argument.expect            |   0\n t/greplint/grep-as-argument.test              |   3 +\n t/greplint/grep-as-value.expect               |   0\n t/greplint/grep-as-value.test                 |   6 +\n t/greplint/wrong-negation.expect              |   1 +\n t/greplint/wrong-negation.test                |   3 +\n 44 files changed, 398 insertions(+), 5 deletions(-)\n create mode 100644 t/greplint-cat.pl\n create mode 100644 t/greplint.pl\n create mode 100644 t/greplint/bare-grep-after-and.expect\n create mode 100644 t/greplint/bare-grep-after-and.test\n create mode 100644 t/greplint/bare-grep-after-semicolon.expect\n create mode 100644 t/greplint/bare-grep-after-semicolon.test\n create mode 100644 t/greplint/bare-grep-compound-body.expect\n create mode 100644 t/greplint/bare-grep-compound-body.test\n create mode 100644 t/greplint/bare-grep-count-mode.expect\n create mode 100644 t/greplint/bare-grep-count-mode.test\n create mode 100644 t/greplint/bare-grep-explicit-pattern.expect\n create mode 100644 t/greplint/bare-grep-explicit-pattern.test\n create mode 100644 t/greplint/bare-grep-flags.expect\n create mode 100644 t/greplint/bare-grep-flags.test\n create mode 100644 t/greplint/bare-grep-lint-ok.expect\n create mode 100644 t/greplint/bare-grep-lint-ok.test\n create mode 100644 t/greplint/bare-grep-negated.expect\n create mode 100644 t/greplint/bare-grep-negated.test\n create mode 100644 t/greplint/bare-grep-pattern-file.expect\n create mode 100644 t/greplint/bare-grep-pattern-file.test\n create mode 100644 t/greplint/bare-grep-simple.expect\n create mode 100644 t/greplint/bare-grep-simple.test\n create mode 100644 t/greplint/bare-grep-subshell.expect\n create mode 100644 t/greplint/bare-grep-subshell.test\n create mode 100644 t/greplint/dqstring-continuation-offset.expect\n create mode 100644 t/greplint/dqstring-continuation-offset.test\n create mode 100644 t/greplint/filter-command-substitution.expect\n create mode 100644 t/greplint/filter-command-substitution.test\n create mode 100644 t/greplint/filter-pipe-input.expect\n create mode 100644 t/greplint/filter-pipe-input.test\n create mode 100644 t/greplint/filter-pipe-output.expect\n create mode 100644 t/greplint/filter-pipe-output.test\n create mode 100644 t/greplint/filter-redirect-output.expect\n create mode 100644 t/greplint/filter-redirect-output.test\n create mode 100644 t/greplint/filter-stdin-redirect.expect\n create mode 100644 t/greplint/filter-stdin-redirect.test\n create mode 100644 t/greplint/grep-as-argument.expect\n create mode 100644 t/greplint/grep-as-argument.test\n create mode 100644 t/greplint/grep-as-value.expect\n create mode 100644 t/greplint/grep-as-value.test\n create mode 100644 t/greplint/wrong-negation.expect\n create mode 100644 t/greplint/wrong-negation.test\n\ndiff --git a/t/.gitattributes b/t/.gitattributes\nindex 7664c6e027..e867f38c71 100644\n--- a/t/.gitattributes\n+++ b/t/.gitattributes\n@@ -1,5 +1,7 @@\n t[0-9][0-9][0-9][0-9]/* -whitespace\n /chainlint/*.expect eol=lf -whitespace\n+/greplint/*.expect eol=lf -whitespace\n+/greplint/*.test eol=lf -whitespace\n /t0110/url-* binary\n /t3206/* eol=lf\n /t3900/*.txt eol=lf\ndiff --git a/t/Makefile b/t/Makefile\nindex ab8a5b54aa..e3249879c4 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -27,9 +27,11 @@ TEST_LINT ?= test-lint\n ifdef TEST_OUTPUT_DIRECTORY\n TEST_RESULTS_DIRECTORY = $(TEST_OUTPUT_DIRECTORY)/test-results\n CHAINLINTTMP = $(TEST_OUTPUT_DIRECTORY)/chainlinttmp\n+GREPLINTTMP = $(TEST_OUTPUT_DIRECTORY)/greplinttmp\n else\n TEST_RESULTS_DIRECTORY = test-results\n CHAINLINTTMP = chainlinttmp\n+GREPLINTTMP = greplinttmp\n endif\n \n # Shell quote;\n@@ -38,6 +40,7 @@ TEST_SHELL_PATH_SQ = $(subst ','\\'',$(TEST_SHELL_PATH))\n PERL_PATH_SQ = $(subst ','\\'',$(PERL_PATH))\n TEST_RESULTS_DIRECTORY_SQ = $(subst ','\\'',$(TEST_RESULTS_DIRECTORY))\n CHAINLINTTMP_SQ = $(subst ','\\'',$(CHAINLINTTMP))\n+GREPLINTTMP_SQ = $(subst ','\\'',$(GREPLINTTMP))\n \n T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh))\n THELPERS = $(sort $(filter-out $(T),$(wildcard *.sh)))\n@@ -45,6 +48,7 @@ TLIBS = $(sort $(wildcard lib-*.sh)) annotate-tests.sh\n TPERF = $(sort $(wildcard perf/p[0-9][0-9][0-9][0-9]-*.sh))\n TINTEROP = $(sort $(wildcard interop/i[0-9][0-9][0-9][0-9]-*.sh))\n CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.test)))\n+GREPLINTTESTS = $(sort $(patsubst greplint/%.test,%,$(wildcard greplint/*.test)))\n CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl\n UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)\n UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))\n@@ -63,8 +67,8 @@ test: pre-clean check-meson $(TEST_LINT)\n \t$(CHAINLINTSUPPRESS) $(MAKE) aggregate-results-and-cleanup\n \n ifneq ($(PERL_PATH),)\n-test: check-chainlint\n-prove: check-chainlint\n+test: check-chainlint check-greplint\n+prove: check-chainlint check-greplint\n endif\n \n failed:\n@@ -102,7 +106,7 @@ unit-tests-test-tool:\n pre-clean:\n \t$(RM) -r '$(TEST_RESULTS_DIRECTORY_SQ)'\n \n-clean-except-prove-cache: clean-chainlint\n+clean-except-prove-cache: clean-chainlint clean-greplint\n \t$(RM) -r 'trash directory'.*\n \t$(RM) -r valgrind/bin\n \n@@ -120,6 +124,17 @@ check-chainlint:\n \t{ $(CHAINLINT) --emit-all '$(CHAINLINTTMP_SQ)'/tests >'$(CHAINLINTTMP_SQ)'/actual || true; } && \\\n \tdiff -u '$(CHAINLINTTMP_SQ)'/expect '$(CHAINLINTTMP_SQ)'/actual\n \n+clean-greplint:\n+\t$(RM) -r '$(GREPLINTTMP_SQ)'\n+\n+check-greplint:\n+\t@mkdir -p '$(GREPLINTTMP_SQ)' && \\\n+\t'$(PERL_PATH_SQ)' greplint-cat.pl '$(GREPLINTTMP_SQ)' $(GREPLINTTESTS) && \\\n+\t{ '$(PERL_PATH_SQ)' greplint.pl \\\n+\t\t$(patsubst %,greplint/%.test,$(GREPLINTTESTS)) \\\n+\t\t>'$(GREPLINTTMP_SQ)'/actual 2>&1 || true; } && \\\n+\tdiff -u '$(GREPLINTTMP_SQ)'/expect '$(GREPLINTTMP_SQ)'/actual\n+\n check-meson:\n \t@# awk acts up when trying to match single quotes, so we use \\047 instead.\n \t@mkdir -p mesontmp && \\\n@@ -139,7 +154,7 @@ check-meson:\n test-lint: test-lint-duplicates test-lint-executable \\\n \ttest-lint-filenames\n ifneq ($(PERL_PATH),)\n-test-lint: test-lint-shell-syntax\n+test-lint: test-lint-shell-syntax test-greplint\n else\n GIT_TEST_CHAIN_LINT = 0\n endif\n@@ -160,6 +175,9 @@ test-lint-executable:\n test-lint-shell-syntax:\n \t@'$(PERL_PATH_SQ)' check-non-portable-shell.pl $(T) $(THELPERS) $(TPERF)\n \n+test-greplint:\n+\t@'$(PERL_PATH_SQ)' greplint.pl $(T) $(THELPERS) $(TPERF)\n+\n test-lint-filenames:\n \t@# We do *not* pass a glob to ls-files but use grep instead, to catch\n \t@# non-ASCII characters (which are quoted within double-quotes)\n@@ -185,7 +203,8 @@ perf:\n \t$(MAKE) -C perf/ all\n \n .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \\\n-\tcheck-chainlint clean-chainlint test-chainlint $(UNIT_TESTS)\n+\tcheck-chainlint clean-chainlint test-chainlint \\\n+\tcheck-greplint clean-greplint test-greplint $(UNIT_TESTS)\n \n .PHONY: libgit-sys-test libgit-rs-test\n libgit-sys-test:\ndiff --git a/t/greplint-cat.pl b/t/greplint-cat.pl\nnew file mode 100644\nindex 0000000000..fa9c3b8aab\n--- /dev/null\n+++ b/t/greplint-cat.pl\n@@ -0,0 +1,27 @@\n+#!/usr/bin/env perl\n+\n+use strict;\n+use warnings;\n+\n+# Assemble expected output for check-greplint target.\n+# Usage: greplint-cat.pl <outdir> <test-name> ...\n+#\n+# For each <test-name>, reads greplint/<test-name>.expect and\n+# prepends \"greplint/<test-name>.test:\" to every non-empty line,\n+# matching the output format of greplint.pl.  Writes combined\n+# expected output to <outdir>/expect.\n+\n+my $outdir = shift;\n+open(my $expect, '>', \"$outdir/expect\")\n+\tor die \"unable to open $outdir/expect: $!\";\n+\n+for my $name (@ARGV) {\n+\topen(my $fh, '<', \"greplint/$name.expect\")\n+\t\tor die \"unable to open greplint/$name.expect: $!\";\n+\twhile (<$fh>) {\n+\t\tprint $expect \"greplint/$name.test:$_\";\n+\t}\n+\tclose $fh;\n+}\n+\n+close $expect;\ndiff --git a/t/greplint.pl b/t/greplint.pl\nnew file mode 100644\nindex 0000000000..553bdbdf9a\n--- /dev/null\n+++ b/t/greplint.pl\n@@ -0,0 +1,241 @@\n+#!/usr/bin/env perl\n+\n+# Detect bare 'grep' used as a test assertion where 'test_grep'\n+# should be used, and '! test_grep' where 'test_grep !' should\n+# be used.\n+#\n+# The shared shell parser tokenizes test bodies so that 'grep'\n+# inside heredocs, command substitutions like $(grep ...), and\n+# quoted strings is collapsed into a single token and never seen\n+# by our check.  A line-oriented approach would need to track\n+# heredoc delimiters, nested $() depth, and cross-line pipe\n+# state to avoid false positives on patterns like:\n+#\n+#   write_script foo.sh <<-\\EOF\n+#   grep pattern file    # data, not an assertion\n+#   EOF\n+#\n+# The Lexer already handles these.\n+\n+use warnings;\n+use strict;\n+use File::Basename;\n+do(dirname($0) . \"/lib-shell-parser.pl\")\n+\tor die \"$0: failed to load lib-shell-parser.pl: $@$!\\n\";\n+\n+my $exit_code = 0;\n+\n+# GrepLintParser inherits ScriptParser's ability to find\n+# test_expect_success/failure blocks and call check_test()\n+# on each body.  We override check_test() to walk the token\n+# stream looking for bare grep assertions.\n+package GrepLintParser;\n+\n+our @ISA = ('ScriptParser');\n+\n+# After these tokens, the next token is a command word.\n+# For example, in 'echo foo && grep bar file', the 'grep'\n+# after '&&' is at command position and should be flagged.\n+my %cmd_start = map { $_ => 1 } qw(&& || ; ;; do then else elif), \"\\n\", '{', '(';\n+\n+# Tokens indicating grep's output is piped or redirected.\n+my %filter_op = map { $_ => 1 } qw(| > >> <);\n+\n+# A token is at \"command word\" position if the shell would\n+# interpret it as a program name rather than an argument.\n+# Only 'grep' at command position is an assertion we should\n+# flag; 'grep' as an argument ('test_must_fail grep') or\n+# value ('for cmd in grep sed') is not.\n+sub is_command_word {\n+\tmy ($tokens, $pos) = @_;\n+\treturn 1 if $pos == 0;\n+\tfor (my $j = $pos - 1; $j >= 0; $j--) {\n+\t\tmy $t = $tokens->[$j]->[0];\n+\t\t# After a separator or pipe, a new command starts.\n+\t\treturn 1 if $cmd_start{$t} || $t eq '|';\n+\t\t# After '}' or ')', what follows is a separator or\n+\t\t# redirect on the compound command, not a new command.\n+\t\treturn 0 if $t eq '}' || $t eq ')';\n+\t\t# '!' is a prefix that does not consume command\n+\t\t# position; keep scanning to find what precedes it.\n+\t\tnext if $t eq '!';\n+\t\t# Any other word means we are past the command word.\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n+# Some bare greps are intentional (e.g. file may not exist,\n+# data filter).  A '# lint-ok' annotation on the source line\n+# suppresses the warning.\n+sub lint_ok {\n+\tmy ($raw_lines, $ln) = @_;\n+\tif ($ln < 1 || $ln > @$raw_lines) {\n+\t\twarn \"lint_ok: line number $ln out of range (1..\" .\n+\t\t    scalar(@$raw_lines) . \")\\n\";\n+\t\treturn 0;\n+\t}\n+\treturn $raw_lines->[$ln - 1] =~ /lint-ok/;\n+}\n+\n+# Grep is a filter (not an assertion) if it receives piped\n+# input or sends its output to a pipe or redirect.  Check\n+# both directions from grep's position in the token stream.\n+sub is_filter {\n+\tmy ($tokens, $pos) = @_;\n+\t# Backward: is grep receiving piped input?\n+\t# Newlines don't break pipes ('cmd |\\n grep' is one\n+\t# pipeline), so skip past them.\n+\tfor (my $j = $pos - 1; $j >= 0; $j--) {\n+\t\tmy $t = $tokens->[$j]->[0];\n+\t\treturn 1 if $t eq '|';\n+\t\tnext if $t eq \"\\n\";\n+\t\tlast if $cmd_start{$t} || $t eq '}' || $t eq ')';\n+\t}\n+\t# Forward: is grep piping or redirecting output?\n+\t# Unlike the backward scan, we do not skip newlines here:\n+\t# a bare newline is a command boundary, and redirects or\n+\t# pipes must appear on the same line as grep (or after a\n+\t# line continuation, which the Lexer consumes).\n+\tfor (my $j = $pos + 1; $j < @$tokens; $j++) {\n+\t\tmy $t = $tokens->[$j]->[0];\n+\t\treturn 0 if $cmd_start{$t};\n+\t\treturn 1 if $filter_op{$t};\n+\t}\n+\treturn 0;\n+}\n+\n+# Map a body-relative line number to a file line number.\n+# For double-quoted bodies, backslash-continuation lines\n+# (\\<newline>) are consumed by the Lexer without appearing\n+# in the body text, so the inner parser sees fewer lines\n+# than the source file has.  We walk the source lines to\n+# count continuations and adjust accordingly.\n+sub body_to_file_line {\n+\tmy ($body_lineno, $body_token, $raw_lines, $body_start) = @_;\n+\tmy $body_text = $body_token->[0];\n+\tmy $body_end_line = $body_token->[4];\n+\tunless ($body_start && $body_start >= 1) {\n+\t\twarn \"body_start is not a positive integer\\n\";\n+\t\treturn $body_lineno;\n+\t}\n+\tmy $file_lineno = $body_lineno + $body_start - 1;\n+\t# Only double-quoted bodies have line splices.\n+\treturn $file_lineno unless $body_text =~ /^\"/;\n+\tmy $adj = 0;\n+\tmy $lines_seen = 0;\n+\tunless ($body_end_line && $body_end_line >= $body_start) {\n+\t\twarn \"body_end_line is not set for double-quoted body\\n\";\n+\t\treturn $file_lineno;\n+\t}\n+\tmy $end = $body_end_line;\n+\tif ($end > @$raw_lines) {\n+\t\twarn \"body_end_line ($end) exceeds file length (\" .\n+\t\t    scalar(@$raw_lines) . \")\\n\";\n+\t\treturn $file_lineno;\n+\t}\n+\tmy $src_ln = $body_start;\n+\twhile ($src_ln <= $end && $lines_seen < $body_lineno) {\n+\t\tmy $line = $raw_lines->[$src_ln - 1];\n+\t\t# Odd trailing backslashes = continuation (\\<nl>).\n+\t\t# Even = escaped backslashes (\\\\), not a continuation.\n+\t\tif ($line =~ /(\\\\*)$/ && length($1) % 2 == 1) {\n+\t\t\t$adj++;\n+\t\t} else {\n+\t\t\t$lines_seen++;\n+\t\t}\n+\t\t$src_ln++;\n+\t}\n+\tif ($lines_seen < $body_lineno) {\n+\t\twarn \"body_lineno ($body_lineno) not found within body range \" .\n+\t\t    \"($body_start..$end)\\n\";\n+\t}\n+\treturn $file_lineno + $adj;\n+}\n+\n+# ScriptParser calls this for each test body found in the script.\n+sub check_test {\n+\tmy $self = shift @_;\n+\tmy $title = ScriptParser::unwrap(shift @_);\n+\tmy $body_token = shift @_;\n+\tmy $body_start = $body_token->[3];\n+\tmy $body = ScriptParser::unwrap($body_token);\n+\t# Handle heredoc-style test bodies:\n+\t#   test_expect_success 'title' - <<\\EOF\n+\t#   grep pattern file\n+\t#   EOF\n+\t# The '-' signals that the body follows as a heredoc.\n+\tif ($body eq '-') {\n+\t\tmy $herebody = shift @_;\n+\t\tif ($herebody) {\n+\t\t\t$body = $herebody->{content};\n+\t\t\t$body_start = $herebody->{start_line};\n+\t\t}\n+\t}\n+\treturn unless $body;\n+\n+\tmy $raw_lines = $self->{raw_lines};\n+\n+\t# The outer parser gives us the body as an opaque string.\n+\t# Parse it to get individual tokens with command boundaries.\n+\tmy $parser = ShellParser->new(\\$body);\n+\tmy @tokens = $parser->parse();\n+\n+\tmy $file = $self->{file};\n+\n+\tfor (my $i = 0; $i < @tokens; $i++) {\n+\t\tmy $text = $tokens[$i]->[0];\n+\t\tnext unless is_command_word(\\@tokens, $i);\n+\n+\t\tmy $token_lineno = $tokens[$i]->[3];\n+\t\tunless (defined($token_lineno) && $token_lineno >= 1) {\n+\t\t\twarn \"token has no line number\\n\";\n+\t\t\tnext;\n+\t\t}\n+\t\tmy $file_lineno = body_to_file_line(\n+\t\t\t$token_lineno,\n+\t\t\t$body_token, $raw_lines, $body_start);\n+\n+\t\t# '!' negates the exit code without consuming command\n+\t\t# position.  '! test_grep' is an anti-pattern because\n+\t\t# test_grep only prints diagnostics on grep failure,\n+\t\t# and '!' inverts after that decision is already made.\n+\t\tif ($text eq '!') {\n+\t\t\tif ($i + 1 < @tokens &&\n+\t\t\t    $tokens[$i + 1]->[0] eq 'test_grep' &&\n+\t\t\t    !lint_ok($raw_lines, $file_lineno)) {\n+\t\t\t\tprint \"$file:$file_lineno: error: \",\n+\t\t\t\t    'use \"test_grep !\" instead of ',\n+\t\t\t\t    '\"! test_grep\"', \"\\n\";\n+\t\t\t\t$exit_code = 1;\n+\t\t\t}\n+\t\t\tnext;\n+\t\t}\n+\n+\t\t# Bare grep as a command (not a filter) is a test\n+\t\t# assertion that should use test_grep for better\n+\t\t# failure diagnostics.\n+\t\tif ($text eq 'grep' &&\n+\t\t    !is_filter(\\@tokens, $i) &&\n+\t\t    !lint_ok($raw_lines, $file_lineno)) {\n+\t\t\tprint \"$file:$file_lineno: error: \",\n+\t\t\t    \"bare grep outside pipeline \",\n+\t\t\t    \"(use test_grep)\\n\";\n+\t\t\t$exit_code = 1;\n+\t\t}\n+\t}\n+}\n+\n+package main;\n+\n+for my $file (@ARGV) {\n+\topen(my $fh, '<:unix:crlf', $file) or die \"$0: $file: $!\\n\";\n+\tmy @raw_lines = <$fh>;\n+\tclose $fh;\n+\tmy $s = join('', @raw_lines);\n+\tmy $parser = GrepLintParser->new(\\$s);\n+\t$parser->{file} = $file;\n+\t$parser->{raw_lines} = \\@raw_lines;\n+\t$parser->parse();\n+}\n+exit $exit_code;\ndiff --git a/t/greplint/bare-grep-after-and.expect b/t/greplint/bare-grep-after-and.expect\nnew file mode 100644\nindex 0000000000..7da1b21aa8\n--- /dev/null\n+++ b/t/greplint/bare-grep-after-and.expect\n@@ -0,0 +1 @@\n+3: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-after-and.test b/t/greplint/bare-grep-after-and.test\nnew file mode 100644\nindex 0000000000..c5b26e4572\n--- /dev/null\n+++ b/t/greplint/bare-grep-after-and.test\n@@ -0,0 +1,4 @@\n+test_expect_success 'grep after && is flagged' '\n+\tcmd &&\n+\tgrep pattern file\n+'\ndiff --git a/t/greplint/bare-grep-after-semicolon.expect b/t/greplint/bare-grep-after-semicolon.expect\nnew file mode 100644\nindex 0000000000..7da1b21aa8\n--- /dev/null\n+++ b/t/greplint/bare-grep-after-semicolon.expect\n@@ -0,0 +1 @@\n+3: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-after-semicolon.test b/t/greplint/bare-grep-after-semicolon.test\nnew file mode 100644\nindex 0000000000..c1e468ddf8\n--- /dev/null\n+++ b/t/greplint/bare-grep-after-semicolon.test\n@@ -0,0 +1,4 @@\n+test_expect_success 'grep after semicolon is flagged' '\n+\techo hello;\n+\tgrep pattern file\n+'\ndiff --git a/t/greplint/bare-grep-compound-body.expect b/t/greplint/bare-grep-compound-body.expect\nnew file mode 100644\nindex 0000000000..eb6210f0c9\n--- /dev/null\n+++ b/t/greplint/bare-grep-compound-body.expect\n@@ -0,0 +1,3 @@\n+4: error: bare grep outside pipeline (use test_grep)\n+8: error: bare grep outside pipeline (use test_grep)\n+15: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-compound-body.test b/t/greplint/bare-grep-compound-body.test\nnew file mode 100644\nindex 0000000000..72472edcf6\n--- /dev/null\n+++ b/t/greplint/bare-grep-compound-body.test\n@@ -0,0 +1,17 @@\n+test_expect_success 'grep after then/do/else is flagged' '\n+\tif true\n+\tthen\n+\t\tgrep pattern file\n+\tfi &&\n+\twhile true\n+\tdo\n+\t\tgrep pattern file &&\n+\t\tbreak\n+\tdone &&\n+\tif true\n+\tthen\n+\t\techo yes\n+\telse\n+\t\tgrep pattern file\n+\tfi\n+'\ndiff --git a/t/greplint/bare-grep-count-mode.expect b/t/greplint/bare-grep-count-mode.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-count-mode.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-count-mode.test b/t/greplint/bare-grep-count-mode.test\nnew file mode 100644\nindex 0000000000..b9c0f18783\n--- /dev/null\n+++ b/t/greplint/bare-grep-count-mode.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep -c is flagged (not special-cased)' '\n+\tgrep -c pattern file\n+'\ndiff --git a/t/greplint/bare-grep-explicit-pattern.expect b/t/greplint/bare-grep-explicit-pattern.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-explicit-pattern.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-explicit-pattern.test b/t/greplint/bare-grep-explicit-pattern.test\nnew file mode 100644\nindex 0000000000..69d8651019\n--- /dev/null\n+++ b/t/greplint/bare-grep-explicit-pattern.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep -e is flagged' '\n+\tgrep -e pattern file\n+'\ndiff --git a/t/greplint/bare-grep-flags.expect b/t/greplint/bare-grep-flags.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-flags.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-flags.test b/t/greplint/bare-grep-flags.test\nnew file mode 100644\nindex 0000000000..9ca0e10233\n--- /dev/null\n+++ b/t/greplint/bare-grep-flags.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep -E is flagged' '\n+\tgrep -E \"pat+ern\" file\n+'\ndiff --git a/t/greplint/bare-grep-lint-ok.expect b/t/greplint/bare-grep-lint-ok.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/bare-grep-lint-ok.test b/t/greplint/bare-grep-lint-ok.test\nnew file mode 100644\nindex 0000000000..335b902a42\n--- /dev/null\n+++ b/t/greplint/bare-grep-lint-ok.test\n@@ -0,0 +1,4 @@\n+test_expect_success 'grep with lint-ok annotation is not flagged' '\n+\tgrep pattern file && # lint-ok\n+\techo done\n+'\ndiff --git a/t/greplint/bare-grep-negated.expect b/t/greplint/bare-grep-negated.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-negated.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-negated.test b/t/greplint/bare-grep-negated.test\nnew file mode 100644\nindex 0000000000..ef789bc8b7\n--- /dev/null\n+++ b/t/greplint/bare-grep-negated.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'negated grep is flagged' '\n+\t! grep pattern file\n+'\ndiff --git a/t/greplint/bare-grep-pattern-file.expect b/t/greplint/bare-grep-pattern-file.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-pattern-file.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-pattern-file.test b/t/greplint/bare-grep-pattern-file.test\nnew file mode 100644\nindex 0000000000..d70035e449\n--- /dev/null\n+++ b/t/greplint/bare-grep-pattern-file.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep -f is flagged' '\n+\tgrep -f patterns.txt file\n+'\ndiff --git a/t/greplint/bare-grep-simple.expect b/t/greplint/bare-grep-simple.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-simple.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-simple.test b/t/greplint/bare-grep-simple.test\nnew file mode 100644\nindex 0000000000..3a8d5f9bbd\n--- /dev/null\n+++ b/t/greplint/bare-grep-simple.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'bare grep is flagged' '\n+\tgrep pattern file\n+'\ndiff --git a/t/greplint/bare-grep-subshell.expect b/t/greplint/bare-grep-subshell.expect\nnew file mode 100644\nindex 0000000000..7da1b21aa8\n--- /dev/null\n+++ b/t/greplint/bare-grep-subshell.expect\n@@ -0,0 +1 @@\n+3: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-subshell.test b/t/greplint/bare-grep-subshell.test\nnew file mode 100644\nindex 0000000000..1fab2d3363\n--- /dev/null\n+++ b/t/greplint/bare-grep-subshell.test\n@@ -0,0 +1,5 @@\n+test_expect_success 'grep in subshell is flagged' '\n+\t(\n+\t\tgrep pattern file\n+\t)\n+'\ndiff --git a/t/greplint/dqstring-continuation-offset.expect b/t/greplint/dqstring-continuation-offset.expect\nnew file mode 100644\nindex 0000000000..8185bfdca4\n--- /dev/null\n+++ b/t/greplint/dqstring-continuation-offset.expect\n@@ -0,0 +1 @@\n+10: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/dqstring-continuation-offset.test b/t/greplint/dqstring-continuation-offset.test\nnew file mode 100644\nindex 0000000000..b332bd8f3b\n--- /dev/null\n+++ b/t/greplint/dqstring-continuation-offset.test\n@@ -0,0 +1,11 @@\n+# Double-quoted test bodies with backslash-continuation lines:\n+# the splice adjustment in check_test compensates for \\<newline>\n+# lines that the lexer consumes without emitting into the body\n+# text, so the reported line number matches the source.\n+test_expect_success 'dqstring continuation offset' \"\n+\tx=\\$(echo \\\n+\t\thello) &&\n+\ty=\\$(echo \\\n+\t\tworld) &&\n+\tgrep pattern file\n+\"\ndiff --git a/t/greplint/filter-command-substitution.expect b/t/greplint/filter-command-substitution.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/filter-command-substitution.test b/t/greplint/filter-command-substitution.test\nnew file mode 100644\nindex 0000000000..bd9a100a5d\n--- /dev/null\n+++ b/t/greplint/filter-command-substitution.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep in command substitution is not flagged' '\n+\tx=$(grep pattern file)\n+'\ndiff --git a/t/greplint/filter-pipe-input.expect b/t/greplint/filter-pipe-input.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/filter-pipe-input.test b/t/greplint/filter-pipe-input.test\nnew file mode 100644\nindex 0000000000..cb5c0d9e23\n--- /dev/null\n+++ b/t/greplint/filter-pipe-input.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep receiving pipe input is not flagged' '\n+\tcmd | grep pattern\n+'\ndiff --git a/t/greplint/filter-pipe-output.expect b/t/greplint/filter-pipe-output.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/filter-pipe-output.test b/t/greplint/filter-pipe-output.test\nnew file mode 100644\nindex 0000000000..01340086d7\n--- /dev/null\n+++ b/t/greplint/filter-pipe-output.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep piping to another command is not flagged' '\n+\tgrep pattern file | wc -l\n+'\ndiff --git a/t/greplint/filter-redirect-output.expect b/t/greplint/filter-redirect-output.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/filter-redirect-output.test b/t/greplint/filter-redirect-output.test\nnew file mode 100644\nindex 0000000000..ee70ba7cde\n--- /dev/null\n+++ b/t/greplint/filter-redirect-output.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep with output redirect is not flagged' '\n+\tgrep pattern file >output\n+'\ndiff --git a/t/greplint/filter-stdin-redirect.expect b/t/greplint/filter-stdin-redirect.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/filter-stdin-redirect.test b/t/greplint/filter-stdin-redirect.test\nnew file mode 100644\nindex 0000000000..292db7766e\n--- /dev/null\n+++ b/t/greplint/filter-stdin-redirect.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep reading from stdin redirect is not flagged' '\n+\tgrep pattern <input\n+'\ndiff --git a/t/greplint/grep-as-argument.expect b/t/greplint/grep-as-argument.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/grep-as-argument.test b/t/greplint/grep-as-argument.test\nnew file mode 100644\nindex 0000000000..7157228b60\n--- /dev/null\n+++ b/t/greplint/grep-as-argument.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep as argument to another command is not flagged' '\n+\ttest_must_fail grep pattern file\n+'\ndiff --git a/t/greplint/grep-as-value.expect b/t/greplint/grep-as-value.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/grep-as-value.test b/t/greplint/grep-as-value.test\nnew file mode 100644\nindex 0000000000..92926f6378\n--- /dev/null\n+++ b/t/greplint/grep-as-value.test\n@@ -0,0 +1,6 @@\n+test_expect_success 'grep as value in for-loop is not flagged' '\n+\tfor cmd in grep sed awk\n+\tdo\n+\t\techo $cmd\n+\tdone\n+'\ndiff --git a/t/greplint/wrong-negation.expect b/t/greplint/wrong-negation.expect\nnew file mode 100644\nindex 0000000000..7dc52f65a8\n--- /dev/null\n+++ b/t/greplint/wrong-negation.expect\n@@ -0,0 +1 @@\n+2: error: use \"test_grep !\" instead of \"! test_grep\"\ndiff --git a/t/greplint/wrong-negation.test b/t/greplint/wrong-negation.test\nnew file mode 100644\nindex 0000000000..542fbd9b28\n--- /dev/null\n+++ b/t/greplint/wrong-negation.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'wrong negation of test_grep is flagged' '\n+\t! test_grep pattern file\n+'\n-- \ngitgitgadget\n"},{"id":"545461","messageId":"CAC2Qwm+-aNRa5S1UiTqXbMZi9hxE_FCOjCQ6cL2trgufktsNiQ@mail.gmail.com","threadId":"65748","inReplyTo":"xmqqldcovhnf.fsf@gitster.g","subject":"Re: [PATCH 0/6] t: add lint-style.pl and convert grep to test_grep","fromName":"Michael Montalbo","fromEmail":"mmontalbo@gmail.com","sentAt":"2026-06-13T16:28:14Z","receivedAt":"2026-06-13T16:28:27Z","isPatch":true,"body":"On Mon, Jun 8, 2026 at 2:36 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> I do not think we want an automated tool that rewrites the source\n> files.  I was hoping that we would get a patch or two that _adds_ to\n> existing test-lint framework (i.e., 'test-grep' that 'test-lint'\n> target depends on in t/Makefile) that gives diagnosis in a similar\n> fashion as test-lint-shell-syntax and test-chainlint do.\n>\n> Also some existing uses of \"grep\" are not end-user facing and should\n> not be rewritten to \"test_grep\".\n\nApologies for not responding explicitly before sending a re-roll, I\nwill do that in\nthe future. v2 attempts to address these points as noted in the \"changes\"\nsection.\n"},{"id":"546530","messageId":"aj93BE8MYatQAjoy@szeder.dev","threadId":"65748","inReplyTo":"3a589ef7386303075413f388e61c203c4e325d44.1781323575.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 5/6] t: convert grep assertions to test_grep","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2026-06-27T07:08:52Z","receivedAt":"2026-06-27T07:08:57Z","isPatch":true,"body":"On Sat, Jun 13, 2026 at 04:06:14AM +0000, Michael Montalbo via GitGitGadget wrote:\n> From: Michael Montalbo <mmontalbo@gmail.com>\n> \n> Replace bare grep with test_grep in test assertions across the\n> suite, including sourced test helpers (lib-*.sh, *-tests.sh).\n> test_grep prints the contents of the file being searched on\n> failure, making debugging easier than a bare grep which fails\n> silently.\n> \n> Only assertion-style greps are converted: grep used as a filter\n> in pipelines, command substitutions, conditionals, or with\n> redirected I/O is left as-is with a \"# lint-ok\" annotation.\n> Existing '! test_grep' calls are rewritten to 'test_grep !' so\n> that the diagnostic output is preserved on failure.\n\nThanks for taking the effort for cleaning up all those negated '!\ngrep' and '! test_grep' callsites and turning them into 'test_grep !'.\n\n> The conversion was generated using a grep-assertion linter\n> (greplint.pl, added in the following commit) to identify bare\n> grep calls at command position.  To reproduce:\n> \n>     # Step 1: mark bare greps that should not be converted\n>     sed -i '/! grep \"$m\" \\.git\\/packed-refs/s/$/ # lint-ok: file may not exist (reftable)/' \\\n>         t/t1400-update-ref.sh\n>     sed -i '/! grep dirty file3 &&/{/lint-ok/!s/$/ # lint-ok: file may not exist after --quit/}' \\\n>         t/t3420-rebase-autostash.sh\n\nI think in this case checking the file3's contents is wrong, because\nat this point file3 should not exist in the first place.  I've sent a\npatch to fix this long ago, but apparently didn't manage to follow\nthrough back then.\n\n  https://lore.kernel.org/git/20211010172809.1472914-1-szeder.dev@gmail.com/\n\n\n> diff --git a/t/t3420-rebase-autostash.sh b/t/t3420-rebase-autostash.sh\n> index f0bbc476ff..f6cb3dd72e 100755\n> --- a/t/t3420-rebase-autostash.sh\n> +++ b/t/t3420-rebase-autostash.sh\n> @@ -141,8 +141,8 @@ testrebase () {\n>  \t\tgit checkout -b rebased-feature-branch feature-branch &&\n>  \t\techo dirty >>file3 &&\n>  \t\tgit rebase$type unrelated-onto-branch >actual 2>&1 &&\n> -\t\tgrep unrelated file4 &&\n> -\t\tgrep dirty file3 &&\n> +\t\ttest_grep unrelated file4 &&\n> +\t\ttest_grep dirty file3 &&\n>  \t\tgit checkout feature-branch\n>  \t'\n>  \n> @@ -165,8 +165,8 @@ testrebase () {\n>  \t\techo dirty >>file3 &&\n>  \t\tgit add file3 &&\n>  \t\tgit rebase$type unrelated-onto-branch &&\n> -\t\tgrep unrelated file4 &&\n> -\t\tgrep dirty file3 &&\n> +\t\ttest_grep unrelated file4 &&\n> +\t\ttest_grep dirty file3 &&\n>  \t\tgit checkout feature-branch\n>  \t'\n>  \n> @@ -197,7 +197,7 @@ testrebase () {\n>  \t\tgit add file2 &&\n>  \t\tgit rebase --continue &&\n>  \t\ttest_path_is_missing $dotest/autostash &&\n> -\t\tgrep dirty file3 &&\n> +\t\ttest_grep dirty file3 &&\n>  \t\tgit checkout feature-branch\n>  \t'\n>  \n> @@ -212,7 +212,7 @@ testrebase () {\n>  \t\ttest_path_is_missing file3 &&\n>  \t\tgit rebase --skip &&\n>  \t\ttest_path_is_missing $dotest/autostash &&\n> -\t\tgrep dirty file3 &&\n> +\t\ttest_grep dirty file3 &&\n>  \t\tgit checkout feature-branch\n>  \t'\n>  \n> @@ -227,7 +227,7 @@ testrebase () {\n>  \t\ttest_path_is_missing file3 &&\n>  \t\tgit rebase --abort &&\n>  \t\ttest_path_is_missing $dotest/autostash &&\n> -\t\tgrep dirty file3 &&\n> +\t\ttest_grep dirty file3 &&\n>  \t\tgit checkout feature-branch\n>  \t'\n>  \n> @@ -244,7 +244,7 @@ testrebase () {\n>  \t\tgit rebase --quit &&\n>  \t\ttest_when_finished git stash drop &&\n>  \t\ttest_path_is_missing $dotest/autostash &&\n> -\t\t! grep dirty file3 &&\n> +\t\t! grep dirty file3 && # lint-ok: file may not exist after --quit\n>  \t\tgit stash show -p >actual &&\n>  \t\ttest_cmp expect actual &&\n>  \t\tgit reset --hard &&\n> @@ -260,11 +260,11 @@ testrebase () {\n>  \t\tgit rebase$type unrelated-onto-branch >actual 2>&1 &&\n>  \t\ttest_path_is_missing $dotest &&\n>  \t\tgit reset --hard &&\n> -\t\tgrep unrelated file4 &&\n> -\t\t! grep dirty file4 &&\n> +\t\ttest_grep unrelated file4 &&\n> +\t\ttest_grep ! dirty file4 &&\n>  \t\tgit checkout feature-branch &&\n>  \t\tgit stash pop &&\n> -\t\tgrep dirty file4\n> +\t\ttest_grep dirty file4\n>  \t'\n>  \n>  \ttest_expect_success \"rebase$type: check output with conflicting stash\" '\n> @@ -286,7 +286,7 @@ test_expect_success \"rebase: fast-forward rebase\" '\n>  \ttest_when_finished git branch -D behind-feature-branch &&\n>  \techo dirty >>file1 &&\n>  \tgit rebase feature-branch &&\n> -\tgrep dirty file1 &&\n> +\ttest_grep dirty file1 &&\n>  \tgit checkout feature-branch\n>  '\n>  \n> @@ -297,7 +297,7 @@ test_expect_success \"rebase: noop rebase\" '\n>  \ttest_when_finished git branch -D same-feature-branch &&\n>  \techo dirty >>file1 &&\n>  \tgit rebase feature-branch &&\n> -\tgrep dirty file1 &&\n> +\ttest_grep dirty file1 &&\n>  \tgit checkout feature-branch\n>  '\n>  \n"},{"id":"546539","messageId":"xmqq4iio59uv.fsf@gitster.g","threadId":"65748","inReplyTo":"aj93BE8MYatQAjoy@szeder.dev","subject":"Re: [PATCH v2 5/6] t: convert grep assertions to test_grep","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-27T14:36:08Z","receivedAt":"2026-06-27T14:36:10Z","isPatch":true,"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n>> The conversion was generated using a grep-assertion linter\n>> (greplint.pl, added in the following commit) to identify bare\n>> grep calls at command position.  To reproduce:\n>> \n>>     # Step 1: mark bare greps that should not be converted\n>>     sed -i '/! grep \"$m\" \\.git\\/packed-refs/s/$/ # lint-ok: file may not exist (reftable)/' \\\n>>         t/t1400-update-ref.sh\n>>     sed -i '/! grep dirty file3 &&/{/lint-ok/!s/$/ # lint-ok: file may not exist after --quit/}' \\\n>>         t/t3420-rebase-autostash.sh\n>\n> I think in this case checking the file3's contents is wrong, because\n> at this point file3 should not exist in the first place.  I've sent a\n> patch to fix this long ago, but apparently didn't manage to follow\n> through back then.\n>\n>   https://lore.kernel.org/git/20211010172809.1472914-1-szeder.dev@gmail.com/\n\nThanks.  I guess the test_grep can be extended to catch this case,\nwhere \n\n    test_grep ! -e pattern1 -e pattern2 file\n\ndoes not find any hits, but only because 'file' is missing, as an\nerror, just like \"test_must_fail git foo\" that segfaults is flagged\nas \"yes, it fails but that is not the kind of failure we expect\".\n\n"},{"id":"546573","messageId":"xmqqldbz4f1a.fsf@gitster.g","threadId":"65748","inReplyTo":"xmqq4iio59uv.fsf@gitster.g","subject":"Re: [PATCH v2 5/6] t: convert grep assertions to test_grep","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-28T01:41:53Z","receivedAt":"2026-06-28T01:41:55Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> SZEDER Gábor <szeder.dev@gmail.com> writes:\n>\n>> I think in this case checking the file3's contents is wrong, because\n>> at this point file3 should not exist in the first place.  I've sent a\n>> patch to fix this long ago, but apparently didn't manage to follow\n>> through back then.\n>>\n>>   https://lore.kernel.org/git/20211010172809.1472914-1-szeder.dev@gmail.com/\n>\n> Thanks.  I guess the test_grep can be extended to catch this case,\n> where \n>\n>     test_grep ! -e pattern1 -e pattern2 file\n>\n> does not find any hits, but only because 'file' is missing, as an\n> error, ...\n\nWait.  The necessary check is already there, isn't it?\n\n        test_grep () {\n                eval \"last_arg=\\${$#}\"\n\n                test -f \"$last_arg\" ||\n                BUG \"test_grep requires a file to read as the last parameter\"\n\nSo why don't we see it every time we run that test that inspects\nfile3's contents with Michael's series merged in?  Puzzled...\n\n"},{"id":"546577","messageId":"xmqq4iin4e1i.fsf@gitster.g","threadId":"65748","inReplyTo":"xmqqldbz4f1a.fsf@gitster.g","subject":"Re: [PATCH v2 5/6] t: convert grep assertions to test_grep","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-28T02:03:21Z","receivedAt":"2026-06-28T02:03:24Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n>> SZEDER Gábor <szeder.dev@gmail.com> writes:\n>>\n>>> I think in this case checking the file3's contents is wrong, because\n>>> at this point file3 should not exist in the first place.  I've sent a\n>>> patch to fix this long ago, but apparently didn't manage to follow\n>>> through back then.\n>>>\n>>>   https://lore.kernel.org/git/20211010172809.1472914-1-szeder.dev@gmail.com/\n>>\n>> Thanks.  I guess the test_grep can be extended to catch this case,\n>> where \n>>\n>>     test_grep ! -e pattern1 -e pattern2 file\n>>\n>> does not find any hits, but only because 'file' is missing, as an\n>> error, ...\n>\n> Wait.  The necessary check is already there, isn't it?\n>\n>         test_grep () {\n>                 eval \"last_arg=\\${$#}\"\n>\n>                 test -f \"$last_arg\" ||\n>                 BUG \"test_grep requires a file to read as the last parameter\"\n>\n> So why don't we see it every time we run that test that inspects\n> file3's contents with Michael's series merged in?  Puzzled...\n\nAh, of course.  Michael sidesteps this mechanism by not using\n\"test_grep !\", with\n\n       ! grep dirty file3 && # lint-ok: file may not exist after --quit\n\nand if we realize that \"may not exist\" is actually \"never exists\",\nthen your other patch from 5 years ago would become the most\nsensible fix for this line.\n\nIt may not be a bad idea to go through \"# lint-ok:\" introduced by\nMichael's series with finer toothed comb (there are only a handful\nof them) and see if there are similar \"look, the file we are\ngrepping in never exists with correctly running Git\" gotchas.\n\nThanks.\n\n"},{"id":"546716","messageId":"xmqqqzlpt543.fsf@gitster.g","threadId":"65748","inReplyTo":"xmqq4iin4e1i.fsf@gitster.g","subject":"Re: [PATCH v2 5/6] t: convert grep assertions to test_grep","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-29T21:21:32Z","receivedAt":"2026-06-29T21:21:35Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Ah, of course.  Michael sidesteps this mechanism by not using\n> \"test_grep !\", with\n>\n>        ! grep dirty file3 && # lint-ok: file may not exist after --quit\n>\n> and if we realize that \"may not exist\" is actually \"never exists\",\n> then your other patch from 5 years ago would become the most\n> sensible fix for this line.\n>\n> It may not be a bad idea to go through \"# lint-ok:\" introduced by\n> Michael's series with finer toothed comb (there are only a handful\n> of them) and see if there are similar \"look, the file we are\n> grepping in never exists with correctly running Git\" gotchas.\n\nIn any case, I think SZEDER's fix to stop grepping in the file but\ninstead insisting on its absense does make sense and it is now in\n'next'.  So perhaps this topic can have a small and final reroll v3\nthat omits change to this particular line (and possibly fix other\nlines that punts with \"# lint-ok\" if needed) and we can declare\nvictory after that?\n\nThanks, all.\n"},{"id":"546954","messageId":"CAC2Qwm+-r0RphATu7UiO7yLn4Phuzpjhx8NC339+DwL6q=nMDw@mail.gmail.com","threadId":"65748","inReplyTo":"xmqqqzlpt543.fsf@gitster.g","subject":"Re: [PATCH v2 5/6] t: convert grep assertions to test_grep","fromName":"Michael Montalbo","fromEmail":"mmontalbo@gmail.com","sentAt":"2026-07-02T04:14:21Z","receivedAt":"2026-07-02T04:14:34Z","isPatch":true,"body":"On Mon, Jun 29, 2026 at 2:21 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> > It may not be a bad idea to go through \"# lint-ok:\" introduced by\n> > Michael's series with finer toothed comb (there are only a handful\n> > of them) and see if there are similar \"look, the file we are\n> > grepping in never exists with correctly running Git\" gotchas.\n>\n> In any case, I think SZEDER's fix to stop grepping in the file but\n> instead insisting on its absense does make sense and it is now in\n> 'next'.  So perhaps this topic can have a small and final reroll v3\n> that omits change to this particular line (and possibly fix other\n> lines that punts with \"# lint-ok\" if needed) and we can declare\n> victory after that?\n>\n> Thanks, all.\n\nThank you, SZEDER, for the nice catch.\n\nI will apply the suggested fix to the series locally, and go through\nthe other #lint-ok's with a fine toothed comb as Junio suggests.\n\nAppreciate the eyes on the series, will send a reroll soon.\n"},{"id":"547027","messageId":"pull.2135.v3.git.1783054466.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v2.git.1781323575.gitgitgadget@gmail.com","subject":"[PATCH v3 0/6] t: add greplint.pl and convert grep to test_grep","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-03T04:54:19Z","receivedAt":"2026-07-03T04:54:30Z","isPatch":true,"body":"test_grep is a wrapper around grep for test assertions that prints the file\ncontents on failure for easier debugging. Bare grep fails silently, making\nit hard to diagnose what went wrong.\n\nThis series converts existing bare grep assertions to test_grep and adds\ngreplint.pl to prevent new ones from being introduced.\n\nPatch 1 documents test_grep in t/README.\n\nPatch 2 fixes three greps missing file arguments (t2402, t7507, t7700). They\nwere reading empty stdin and passing vacuously.\n\nPatch 3 extracts chainlint's Lexer, ShellParser, and ScriptParser into a\nshared module (lib-shell-parser.pl) so greplint.pl can reuse the same\ntokenizer. No functional change to chainlint.\n\nPatch 4 fixes a latent line-counting bug in scan_dqstring where newlines\nfrom $() bodies inside double-quoted strings were counted twice. This does\nnot affect chainlint (which uses byte offsets) but matters for greplint.pl's\nline-number reporting.\n\nPatch 5 converts existing assertion greps to test_grep, including sourced\ntest helpers. Greps used as data filters or on files that may not exist are\nleft unconverted with lint-ok annotations.\n\nPatch 6 adds greplint.pl with test fixtures (modeled on chainlint/) and\nwires it into the Makefile as test-greplint and check-greplint.\n\nChanges since v2:\n\n * t3420-rebase-autostash: dropped the change to the '! grep dirty file3'\n   line under 'rebase --quit'. As SZEDER pointed out, file3 never exists in\n   the conflicted state, so that grep was passing only because it could not\n   open the file. SZEDER's fix (sg/t3420-do-not-grep-in-missing-file, now in\n   'next') replaces the line with 'test_path_is_missing file3', which is the\n   right check; this series simply leaves that line to his fix.\n\n * Audited the remaining '# lint-ok' annotations for the same \"grep a file\n   that never exists with correctly running Git\" gotcha, as Junio suggested.\n   The rule the audit applies: 'grep' becomes 'test_grep' only where its\n   exit code is the assertion; grep that produces data (a filter) or that\n   reads a file whose presence is conditional stays a plain 'grep', because\n   test_grep BUGs on a missing file.\n   \n   * t5537 (.git/shallow): the file is still present after the repack (the\n     client stays shallow), so the assertion is converted to 'test_grep !'\n     like any other; the \"may not exist\" note was wrong.\n   \n   * t1400 (.git/packed-refs): the file exists only with the files backend.\n     Guarded the packed-refs check with a REFFILES prerequisite; the\n     backend-agnostic 'git show-ref' check that follows still runs under\n     every backend.\n   \n   * t7450 (squatting-clone/d/a/git~2): kept as '! grep' with an improved '#\n     lint-ok'. 'git~2' is the NTFS 8.3 short name of a planted '..git' decoy\n     and only exists when 8.3 short-name generation is enabled. Verified on\n     a Windows VM: with 8.3 disabled (the modern default) the short name is\n     absent, the '! grep' correctly tolerates it, and a plain test_grep\n     would BUG. So this one deliberately stays a missing-file-tolerant grep.\n   \n   * t5326 and t5702 remain annotated: these are genuine data filters (grep\n     produces data that is redirected/captured, not an assertion).\n\n * Rebased onto master. The only conflict was a one-line codespell typo fix\n   that upstream applied to chainlint.pl in the meantime; it now lives in\n   the extracted lib-shell-parser.pl.\n\nKnown limitation / follow-up:\n\nAssertions like grep pattern file >/dev/null and grep pattern <file are not\nconverted because greplint.pl treats any redirect as a filter, so it does\nnot flag them. The former could be converted, but test_grep prints the\nmatching line on success just as grep does, so the >/dev/null would have to\nbe kept or dropped as a judgment call. The latter requires turning the <file\nredirect into a positional file argument, since test_grep reads a named file\nrather than stdin. Both are left as bare grep. A follow-up series can\naddress these once a convention is established.\n\nMichael Montalbo (6):\n  t/README: document test_grep helper\n  t: fix grep assertions missing file arguments\n  t: extract chainlint's parser into shared module\n  t: fix Lexer line count for $() inside double-quoted strings\n  t: convert grep assertions to test_grep\n  t: add greplint to detect bare grep assertions\n\n t/.gitattributes                              |   2 +\n t/Makefile                                    |  29 +-\n t/README                                      |  26 +\n t/chainlint.pl                                | 529 +----------------\n t/for-each-ref-tests.sh                       |  12 +-\n t/greplint-cat.pl                             |  27 +\n t/greplint.pl                                 | 241 ++++++++\n t/greplint/bare-grep-after-and.expect         |   1 +\n t/greplint/bare-grep-after-and.test           |   4 +\n t/greplint/bare-grep-after-semicolon.expect   |   1 +\n t/greplint/bare-grep-after-semicolon.test     |   4 +\n t/greplint/bare-grep-compound-body.expect     |   3 +\n t/greplint/bare-grep-compound-body.test       |  17 +\n t/greplint/bare-grep-count-mode.expect        |   1 +\n t/greplint/bare-grep-count-mode.test          |   3 +\n t/greplint/bare-grep-explicit-pattern.expect  |   1 +\n t/greplint/bare-grep-explicit-pattern.test    |   3 +\n t/greplint/bare-grep-flags.expect             |   1 +\n t/greplint/bare-grep-flags.test               |   3 +\n t/greplint/bare-grep-lint-ok.expect           |   0\n t/greplint/bare-grep-lint-ok.test             |   4 +\n t/greplint/bare-grep-negated.expect           |   1 +\n t/greplint/bare-grep-negated.test             |   3 +\n t/greplint/bare-grep-pattern-file.expect      |   1 +\n t/greplint/bare-grep-pattern-file.test        |   3 +\n t/greplint/bare-grep-simple.expect            |   1 +\n t/greplint/bare-grep-simple.test              |   3 +\n t/greplint/bare-grep-subshell.expect          |   1 +\n t/greplint/bare-grep-subshell.test            |   5 +\n .../dqstring-continuation-offset.expect       |   1 +\n t/greplint/dqstring-continuation-offset.test  |  11 +\n t/greplint/filter-command-substitution.expect |   0\n t/greplint/filter-command-substitution.test   |   3 +\n t/greplint/filter-pipe-input.expect           |   0\n t/greplint/filter-pipe-input.test             |   3 +\n t/greplint/filter-pipe-output.expect          |   0\n t/greplint/filter-pipe-output.test            |   3 +\n t/greplint/filter-redirect-output.expect      |   0\n t/greplint/filter-redirect-output.test        |   3 +\n t/greplint/filter-stdin-redirect.expect       |   0\n t/greplint/filter-stdin-redirect.test         |   3 +\n t/greplint/grep-as-argument.expect            |   0\n t/greplint/grep-as-argument.test              |   3 +\n t/greplint/grep-as-value.expect               |   0\n t/greplint/grep-as-value.test                 |   6 +\n t/greplint/wrong-negation.expect              |   1 +\n t/greplint/wrong-negation.test                |   3 +\n t/lib-bitmap.sh                               |  12 +-\n t/lib-bundle-uri-protocol.sh                  |  26 +-\n t/lib-httpd.sh                                |   2 +-\n t/lib-shell-parser.pl                         | 534 ++++++++++++++++++\n t/pack-refs-tests.sh                          |   2 +-\n t/show-ref-exists-tests.sh                    |   2 +-\n t/t0000-basic.sh                              |  16 +-\n t/t0001-init.sh                               |  18 +-\n t/t0008-ignores.sh                            |   8 +-\n t/t0009-git-dir-validation.sh                 |   6 +-\n t/t0012-help.sh                               |   4 +-\n t/t0013-sha1dc.sh                             |   2 +-\n t/t0017-env-helper.sh                         |   4 +-\n t/t0021-conversion.sh                         |  18 +-\n t/t0029-core-unsetenvvars.sh                  |   4 +-\n t/t0030-stripspace.sh                         |   4 +-\n t/t0031-lockfile-pid.sh                       |   2 +-\n t/t0040-parse-options.sh                      |  52 +-\n t/t0041-usage.sh                              |   2 +-\n t/t0052-simple-ipc.sh                         |  10 +-\n t/t0061-run-command.sh                        |   2 +-\n t/t0066-dir-iterator.sh                       |   2 +-\n t/t0068-for-each-repo.sh                      |  16 +-\n t/t0070-fundamental.sh                        |   6 +-\n t/t0081-find-pack.sh                          |  12 +-\n t/t0091-bugreport.sh                          |  18 +-\n t/t0092-diagnose.sh                           |  12 +-\n t/t0100-previous.sh                           |   2 +-\n t/t0200-gettext-basic.sh                      |  14 +-\n t/t0203-gettext-setlocale-sanity.sh           |   4 +-\n t/t0204-gettext-reencode-sanity.sh            |   8 +-\n t/t0210-trace2-normal.sh                      |   6 +-\n t/t0211-trace2-perf.sh                        |  80 +--\n t/t0212-trace2-event.sh                       |   8 +-\n t/t0300-credentials.sh                        |   4 +-\n t/t0410-partial-clone.sh                      |  82 +--\n t/t0450-txt-doc-vs-help.sh                    |   2 +-\n t/t0500-progress-display.sh                   |  18 +-\n t/t0610-reftable-basics.sh                    |   8 +-\n t/t1004-read-tree-m-u-wf.sh                   |   8 +-\n t/t1006-cat-file.sh                           |  18 +-\n t/t1007-hash-object.sh                        |   8 +-\n t/t1011-read-tree-sparse-checkout.sh          |  10 +-\n t/t1050-large.sh                              |   6 +-\n t/t1091-sparse-checkout-builtin.sh            |  24 +-\n t/t1092-sparse-checkout-compatibility.sh      |  44 +-\n t/t1300-config.sh                             |  16 +-\n t/t1305-config-include.sh                     |   2 +-\n t/t1308-config-set.sh                         |   6 +-\n t/t1400-update-ref.sh                         | 173 +++---\n t/t1403-show-ref.sh                           |  18 +-\n t/t1410-reflog.sh                             |   4 +-\n t/t1415-worktree-refs.sh                      |   4 +-\n t/t1430-bad-ref-name.sh                       |  56 +-\n t/t1450-fsck.sh                               |  12 +-\n t/t1451-fsck-buffer.sh                        |   6 +-\n t/t1460-refs-migrate.sh                       |   2 +-\n t/t1500-rev-parse.sh                          |   6 +-\n t/t1502-rev-parse-parseopt.sh                 |   2 +-\n t/t1503-rev-parse-verify.sh                   |  10 +-\n t/t1510-repo-setup.sh                         |  10 +-\n t/t1512-rev-parse-disambiguation.sh           |   4 +-\n t/t1515-rev-parse-outside-repo.sh             |   2 +-\n t/t1800-hook.sh                               |  18 +-\n t/t2004-checkout-cache-temp.sh                |   4 +-\n t/t2019-checkout-ambiguous-ref.sh             |   4 +-\n t/t2024-checkout-dwim.sh                      |   8 +-\n t/t2030-unresolve-info.sh                     |   6 +-\n t/t2060-switch.sh                             |   6 +-\n t/t2070-restore.sh                            |   2 +-\n t/t2080-parallel-checkout-basics.sh           |  14 +-\n t/t2081-parallel-checkout-collisions.sh       |  24 +-\n t/t2082-parallel-checkout-attributes.sh       |  12 +-\n t/t2103-update-index-ignore-missing.sh        |   6 +-\n t/t2200-add-update.sh                         |   2 +-\n t/t2203-add-intent.sh                         |   6 +-\n t/t2400-worktree-add.sh                       |  24 +-\n t/t2402-worktree-list.sh                      |  16 +-\n t/t2403-worktree-move.sh                      |   6 +-\n t/t2405-worktree-submodule.sh                 |   6 +-\n t/t2407-worktree-heads.sh                     |  26 +-\n t/t2500-untracked-overwriting.sh              |   8 +-\n t/t2501-cwd-empty.sh                          |   4 +-\n t/t3001-ls-files-others-exclude.sh            |   6 +-\n t/t3007-ls-files-recurse-submodules.sh        |   6 +-\n t/t3200-branch.sh                             |  12 +-\n t/t3202-show-branch.sh                        |  10 +-\n t/t3203-branch-output.sh                      |   4 +-\n t/t3206-range-diff.sh                         |  78 +--\n t/t3207-branch-submodule.sh                   |   4 +-\n t/t3301-notes.sh                              |  32 +-\n t/t3310-notes-merge-manual-resolve.sh         |  16 +-\n t/t3320-notes-merge-worktrees.sh              |   2 +-\n t/t3400-rebase.sh                             |  16 +-\n t/t3402-rebase-merge.sh                       |  16 +-\n t/t3404-rebase-interactive.sh                 |  72 +--\n t/t3406-rebase-message.sh                     |   6 +-\n t/t3415-rebase-autosquash.sh                  |  10 +-\n t/t3416-rebase-onto-threedots.sh              |   4 +-\n t/t3418-rebase-continue.sh                    |  10 +-\n t/t3420-rebase-autostash.sh                   |  24 +-\n t/t3422-rebase-incompatible-options.sh        |   4 +-\n t/t3429-rebase-edit-todo.sh                   |   2 +-\n t/t3430-rebase-merges.sh                      |  32 +-\n t/t3500-cherry.sh                             |   4 +-\n t/t3501-revert-cherry-pick.sh                 |   6 +-\n t/t3504-cherry-pick-rerere.sh                 |   6 +-\n t/t3510-cherry-pick-sequence.sh               |  24 +-\n t/t3602-rm-sparse-checkout.sh                 |   4 +-\n t/t3705-add-sparse-checkout.sh                |  10 +-\n t/t3800-mktag.sh                              |   4 +-\n t/t3901-i18n-patch.sh                         |  16 +-\n t/t3903-stash.sh                              |  28 +-\n t/t3904-stash-patch.sh                        |   4 +-\n t/t3908-stash-in-worktree.sh                  |   2 +-\n t/t4000-diff-format.sh                        |   2 +-\n t/t4001-diff-rename.sh                        |   4 +-\n t/t4011-diff-symlink.sh                       |   2 +-\n t/t4013-diff-various.sh                       |   2 +-\n t/t4014-format-patch.sh                       | 344 +++++------\n t/t4015-diff-whitespace.sh                    |  16 +-\n t/t4017-diff-retval.sh                        |   2 +-\n t/t4018-diff-funcname.sh                      |   2 +-\n t/t4019-diff-wserror.sh                       |   8 +-\n t/t4020-diff-external.sh                      |  18 +-\n t/t4021-format-patch-numbered.sh              |   4 +-\n t/t4022-diff-rewrite.sh                       |  14 +-\n t/t4028-format-patch-mime-headers.sh          |   6 +-\n t/t4031-diff-rewrite-binary.sh                |  18 +-\n t/t4033-diff-patience.sh                      |   2 +-\n t/t4036-format-patch-signer-mime.sh           |   6 +-\n t/t4038-diff-combined.sh                      |   6 +-\n t/t4051-diff-function-context.sh              |  38 +-\n t/t4053-diff-no-index.sh                      |   4 +-\n t/t4063-diff-blobs.sh                         |   2 +-\n t/t4065-diff-anchored.sh                      |  26 +-\n t/t4067-diff-partial-clone.sh                 |  12 +-\n t/t4073-diff-stat-name-width.sh               |  24 +-\n t/t4103-apply-binary.sh                       |   2 +-\n t/t4120-apply-popt.sh                         |   2 +-\n t/t4124-apply-ws-rule.sh                      |  10 +-\n t/t4128-apply-root.sh                         |   2 +-\n t/t4140-apply-ita.sh                          |   4 +-\n t/t4141-apply-too-large.sh                    |   2 +-\n t/t4150-am.sh                                 |  48 +-\n t/t4200-rerere.sh                             |   6 +-\n t/t4201-shortlog.sh                           |   2 +-\n t/t4202-log.sh                                |  84 +--\n t/t4204-patch-id.sh                           |   2 +-\n t/t4205-log-pretty-formats.sh                 |   2 +-\n t/t4209-log-pickaxe.sh                        |  10 +-\n t/t4211-line-log.sh                           |  72 +--\n t/t4216-log-bloom.sh                          |  18 +-\n t/t4252-am-options.sh                         |  22 +-\n t/t4254-am-corrupt.sh                         |   6 +-\n t/t4258-am-quoted-cr.sh                       |   2 +-\n t/t4301-merge-tree-write-tree.sh              |  18 +-\n t/t5000-tar-tree.sh                           |  10 +-\n t/t5004-archive-corner-cases.sh               |   2 +-\n t/t5100-mailinfo.sh                           |   2 +-\n t/t5150-request-pull.sh                       |  18 +-\n t/t5300-pack-object.sh                        |  22 +-\n t/t5302-pack-index.sh                         |   6 +-\n t/t5304-prune.sh                              |   8 +-\n t/t5310-pack-bitmaps.sh                       |  14 +-\n t/t5317-pack-objects-filter-objects.sh        |  12 +-\n t/t5318-commit-graph.sh                       |   8 +-\n t/t5319-multi-pack-index.sh                   |  16 +-\n t/t5324-split-commit-graph.sh                 |  10 +-\n t/t5325-reverse-index.sh                      |   2 +-\n t/t5326-multi-pack-bitmaps.sh                 |  28 +-\n t/t5328-commit-graph-64bit-time.sh            |   2 +-\n t/t5329-pack-objects-cruft.sh                 |   8 +-\n t/t5334-incremental-multi-pack-index.sh       |   2 +-\n t/t5335-compact-multi-pack-index.sh           |   4 +-\n t/t5351-unpack-large-objects.sh               |   2 +-\n t/t5402-post-merge-hook.sh                    |   4 +-\n t/t5403-post-checkout-hook.sh                 |   2 +-\n t/t5404-tracking-branches.sh                  |   2 +-\n t/t5406-remote-rejects.sh                     |   2 +-\n t/t5407-post-rewrite-hook.sh                  |   8 +-\n t/t5409-colorize-remote-messages.sh           |  36 +-\n t/t5500-fetch-pack.sh                         |  38 +-\n t/t5504-fetch-receive-strict.sh               |  14 +-\n t/t5505-remote.sh                             |  20 +-\n t/t5510-fetch.sh                              |  10 +-\n t/t5512-ls-remote.sh                          |   8 +-\n t/t5514-fetch-multiple.sh                     |   2 +-\n t/t5516-fetch-push.sh                         |  20 +-\n t/t5520-pull.sh                               |   4 +-\n t/t5524-pull-msg.sh                           |   6 +-\n t/t5526-fetch-submodules.sh                   |  16 +-\n t/t5529-push-errors.sh                        |   4 +-\n t/t5530-upload-pack-error.sh                  |  18 +-\n t/t5531-deep-submodule-push.sh                |   2 +-\n t/t5532-fetch-proxy.sh                        |   2 +-\n t/t5533-push-cas.sh                           |  12 +-\n t/t5534-push-signed.sh                        |  22 +-\n t/t5537-fetch-shallow.sh                      |   2 +-\n t/t5538-push-shallow.sh                       |   2 +-\n t/t5539-fetch-http-shallow.sh                 |   4 +-\n t/t5541-http-push-smart.sh                    |  32 +-\n t/t5544-pack-objects-hook.sh                  |  12 +-\n t/t5550-http-fetch-dumb.sh                    |   4 +-\n t/t5551-http-fetch-smart.sh                   |  46 +-\n t/t5552-skipping-fetch-negotiator.sh          |   6 +-\n t/t5554-noop-fetch-negotiator.sh              |   4 +-\n t/t5557-http-get.sh                           |   2 +-\n t/t5558-clone-bundle-uri.sh                   |  38 +-\n t/t5562-http-backend-content-length.sh        |   2 +-\n t/t5564-http-proxy.sh                         |  10 +-\n t/t5581-http-curl-verbose.sh                  |   2 +-\n t/t5583-push-branches.sh                      |   8 +-\n t/t5601-clone.sh                              |  28 +-\n t/t5604-clone-reference.sh                    |   8 +-\n t/t5605-clone-local.sh                        |   2 +-\n t/t5606-clone-options.sh                      |   6 +-\n t/t5612-clone-refspec.sh                      |   2 +-\n t/t5616-partial-clone.sh                      |  60 +-\n t/t5619-clone-local-ambiguous-transport.sh    |   2 +-\n t/t5620-backfill.sh                           |  12 +-\n t/t5700-protocol-v1.sh                        |  46 +-\n t/t5701-git-serve.sh                          |  14 +-\n t/t5702-protocol-v2.sh                        | 154 ++---\n t/t5703-upload-pack-ref-in-want.sh            |  22 +-\n t/t5705-session-id-in-capabilities.sh         |  12 +-\n t/t5750-bundle-uri-parse.sh                   |   8 +-\n t/t5801-remote-helpers.sh                     |   4 +-\n t/t5810-proto-disable-local.sh                |   2 +-\n t/t5813-proto-disable-ssh.sh                  |   4 +-\n t/t6000-rev-list-misc.sh                      |  26 +-\n t/t6005-rev-list-count.sh                     |   8 +-\n t/t6006-rev-list-format.sh                    |   4 +-\n t/t6009-rev-list-parent.sh                    |   4 +-\n t/t6020-bundle-misc.sh                        |  12 +-\n t/t6022-rev-list-missing.sh                   |   4 +-\n t/t6030-bisect-porcelain.sh                   | 150 ++---\n t/t6040-tracking-info.sh                      |   2 +-\n t/t6112-rev-list-filters-objects.sh           |  24 +-\n t/t6115-rev-list-du.sh                        |   4 +-\n t/t6120-describe.sh                           |  14 +-\n t/t6200-fmt-merge-msg.sh                      |  82 +--\n t/t6402-merge-rename.sh                       |   4 +-\n t/t6403-merge-file.sh                         |   6 +-\n t/t6404-recursive-merge.sh                    |   2 +-\n t/t6406-merge-attr.sh                         |  20 +-\n t/t6417-merge-ours-theirs.sh                  |  30 +-\n t/t6418-merge-text-auto.sh                    |   2 +-\n t/t6422-merge-rename-corner-cases.sh          |   8 +-\n t/t6423-merge-rename-directories.sh           |  72 +--\n t/t6424-merge-unrelated-index-changes.sh      |   6 +-\n t/t6427-diff3-conflict-markers.sh             |  10 +-\n t/t6432-merge-recursive-space-options.sh      |   4 +-\n t/t6436-merge-overwrite.sh                    |   6 +-\n t/t6437-submodule-merge.sh                    |  10 +-\n t/t6500-gc.sh                                 |   8 +-\n t/t6600-test-reach.sh                         |   4 +-\n t/t7001-mv.sh                                 |  16 +-\n t/t7002-mv-sparse-checkout.sh                 |  38 +-\n t/t7003-filter-branch.sh                      |  16 +-\n t/t7004-tag.sh                                |   2 +-\n t/t7006-pager.sh                              |  16 +-\n t/t7012-skip-worktree-writing.sh              |   6 +-\n t/t7030-verify-tag.sh                         |  52 +-\n t/t7031-verify-tag-signed-ssh.sh              |  46 +-\n t/t7102-reset.sh                              |   2 +-\n t/t7110-reset-merge.sh                        |  40 +-\n t/t7201-co.sh                                 |   6 +-\n t/t7300-clean.sh                              |   2 +-\n t/t7301-clean-interactive.sh                  |   2 +-\n t/t7400-submodule-basic.sh                    |  32 +-\n t/t7402-submodule-rebase.sh                   |   2 +-\n t/t7406-submodule-update.sh                   |  26 +-\n t/t7416-submodule-dash-url.sh                 |  20 +-\n t/t7417-submodule-path-url.sh                 |   2 +-\n t/t7450-bad-git-dotfiles.sh                   |  18 +-\n t/t7501-commit-basic-functionality.sh         |  16 +-\n t/t7502-commit-porcelain.sh                   |   2 +-\n t/t7507-commit-verbose.sh                     |   6 +-\n t/t7508-status.sh                             |   6 +-\n t/t7510-signed-commit.sh                      |  68 +--\n t/t7516-commit-races.sh                       |   4 +-\n t/t7519-status-fsmonitor.sh                   |  14 +-\n t/t7527-builtin-fsmonitor.sh                  |  82 +--\n t/t7528-signed-commit-ssh.sh                  |  68 +--\n t/t7600-merge.sh                              |  10 +-\n t/t7603-merge-reduce-heads.sh                 |  20 +-\n t/t7606-merge-custom.sh                       |   2 +-\n t/t7607-merge-state.sh                        |   4 +-\n t/t7610-mergetool.sh                          |  18 +-\n t/t7700-repack.sh                             |  14 +-\n t/t7703-repack-geometric.sh                   |   4 +-\n t/t7704-repack-cruft.sh                       |  12 +-\n t/t7800-difftool.sh                           |  26 +-\n t/t7810-grep.sh                               |  22 +-\n t/t7814-grep-recurse-submodules.sh            |   2 +-\n t/t7900-maintenance.sh                        |  34 +-\n t/t8008-blame-formats.sh                      |   2 +-\n t/t8010-cat-file-filters.sh                   |   2 +-\n t/t8012-blame-colors.sh                       |   2 +-\n t/t9001-send-email.sh                         | 190 +++----\n t/t9003-help-autocorrect.sh                   |   6 +-\n t/t9106-git-svn-commit-diff-clobber.sh        |   2 +-\n t/t9107-git-svn-migrate.sh                    |  30 +-\n t/t9110-git-svn-use-svm-props.sh              |  20 +-\n t/t9111-git-svn-use-svnsync-props.sh          |  18 +-\n t/t9114-git-svn-dcommit-merge.sh              |   6 +-\n t/t9116-git-svn-log.sh                        |   8 +-\n t/t9117-git-svn-init-clone.sh                 |  12 +-\n t/t9119-git-svn-info.sh                       |  16 +-\n t/t9122-git-svn-author.sh                     |   8 +-\n t/t9130-git-svn-authors-file.sh               |   8 +-\n t/t9138-git-svn-authors-prog.sh               |  14 +-\n t/t9140-git-svn-reset.sh                      |   4 +-\n t/t9153-git-svn-rewrite-uuid.sh               |   4 +-\n t/t9200-git-cvsexportcommit.sh                |   2 +-\n t/t9210-scalar.sh                             |  34 +-\n t/t9211-scalar-clone.sh                       |  16 +-\n t/t9300-fast-import.sh                        |  10 +-\n t/t9350-fast-export.sh                        |  54 +-\n t/t9351-fast-export-anonymize.sh              |  36 +-\n t/t9400-git-cvsserver-server.sh               |   4 +-\n t/t9501-gitweb-standalone-http-status.sh      |  58 +-\n t/t9502-gitweb-standalone-parse-output.sh     |  38 +-\n t/t9800-git-p4-basic.sh                       |  10 +-\n t/t9801-git-p4-branch.sh                      |  48 +-\n t/t9806-git-p4-options.sh                     |  10 +-\n t/t9807-git-p4-submit.sh                      |   2 +-\n t/t9810-git-p4-rcs.sh                         |   8 +-\n t/t9813-git-p4-preserve-users.sh              |   8 +-\n t/t9814-git-p4-rename.sh                      |   8 +-\n t/t9827-git-p4-change-filetype.sh             |   4 +-\n t/t9832-unshelve.sh                           |   6 +-\n t/t9833-errors.sh                             |   4 +-\n t/t9835-git-p4-metadata-encoding-python2.sh   |  36 +-\n t/t9836-git-p4-metadata-encoding-python3.sh   |  38 +-\n t/t9850-shell.sh                              |   2 +-\n t/t9902-completion.sh                         |  26 +-\n 385 files changed, 3783 insertions(+), 3328 deletions(-)\n create mode 100644 t/greplint-cat.pl\n create mode 100644 t/greplint.pl\n create mode 100644 t/greplint/bare-grep-after-and.expect\n create mode 100644 t/greplint/bare-grep-after-and.test\n create mode 100644 t/greplint/bare-grep-after-semicolon.expect\n create mode 100644 t/greplint/bare-grep-after-semicolon.test\n create mode 100644 t/greplint/bare-grep-compound-body.expect\n create mode 100644 t/greplint/bare-grep-compound-body.test\n create mode 100644 t/greplint/bare-grep-count-mode.expect\n create mode 100644 t/greplint/bare-grep-count-mode.test\n create mode 100644 t/greplint/bare-grep-explicit-pattern.expect\n create mode 100644 t/greplint/bare-grep-explicit-pattern.test\n create mode 100644 t/greplint/bare-grep-flags.expect\n create mode 100644 t/greplint/bare-grep-flags.test\n create mode 100644 t/greplint/bare-grep-lint-ok.expect\n create mode 100644 t/greplint/bare-grep-lint-ok.test\n create mode 100644 t/greplint/bare-grep-negated.expect\n create mode 100644 t/greplint/bare-grep-negated.test\n create mode 100644 t/greplint/bare-grep-pattern-file.expect\n create mode 100644 t/greplint/bare-grep-pattern-file.test\n create mode 100644 t/greplint/bare-grep-simple.expect\n create mode 100644 t/greplint/bare-grep-simple.test\n create mode 100644 t/greplint/bare-grep-subshell.expect\n create mode 100644 t/greplint/bare-grep-subshell.test\n create mode 100644 t/greplint/dqstring-continuation-offset.expect\n create mode 100644 t/greplint/dqstring-continuation-offset.test\n create mode 100644 t/greplint/filter-command-substitution.expect\n create mode 100644 t/greplint/filter-command-substitution.test\n create mode 100644 t/greplint/filter-pipe-input.expect\n create mode 100644 t/greplint/filter-pipe-input.test\n create mode 100644 t/greplint/filter-pipe-output.expect\n create mode 100644 t/greplint/filter-pipe-output.test\n create mode 100644 t/greplint/filter-redirect-output.expect\n create mode 100644 t/greplint/filter-redirect-output.test\n create mode 100644 t/greplint/filter-stdin-redirect.expect\n create mode 100644 t/greplint/filter-stdin-redirect.test\n create mode 100644 t/greplint/grep-as-argument.expect\n create mode 100644 t/greplint/grep-as-argument.test\n create mode 100644 t/greplint/grep-as-value.expect\n create mode 100644 t/greplint/grep-as-value.test\n create mode 100644 t/greplint/wrong-negation.expect\n create mode 100644 t/greplint/wrong-negation.test\n create mode 100644 t/lib-shell-parser.pl\n\n\nbase-commit: e9019fcafe0040228b8631c30f97ae1adb61bcdc\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2135%2Fmmontalbo%2Fmm%2Ftest-grep-docs-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2135/mmontalbo/mm/test-grep-docs-v3\nPull-Request: https://github.com/gitgitgadget/git/pull/2135\n\nRange-diff vs v2:\n\n 1:  5959cab258 ! 1:  ec6b478c4c t/README: document test_grep helper\n     @@ t/README: see test-lib-functions.sh for the full list and their options.\n      +   or inside a command substitution (e.g. '$(grep -c ...)'), plain\n      +   'grep' is the right choice because the exit code is not the\n      +   assertion itself.\n     ++\n     ++   test_grep requires <file> to exist and will BUG otherwise.\n     ++   When a file's presence is conditional (a backend-specific\n     ++   file, or a path that only exists on some platforms, such as\n     ++   an NTFS 8.3 short name), keep a plain guarded 'grep' instead.\n      +\n        - test_path_is_file <path>\n          test_path_is_dir <path>\n 2:  f3e8e19e6e = 2:  7a837b77cd t: fix grep assertions missing file arguments\n 3:  460461b5fe ! 3:  5894ca1724 t: extract chainlint's parser into shared module\n     @@ t/chainlint.pl: my $jobs = -1;\n      -#\n      -# In other languages, `1+2` would typically be scanned as three tokens\n      -# (`1`, `+`, and `2`), but in shell it is a single token. However, the similar\n     --# `1 + 2`, which embeds whitepace, is scanned as three token in shell, as well.\n     +-# `1 + 2`, which embeds whitespace, is scanned as three token in shell, as well.\n      -# In shell, several characters with special meaning lose that meaning when not\n      -# surrounded by whitespace. For instance, the negation operator `!` is special\n      -# when standing alone surrounded by whitespace; whereas in `foo!uucp` it is\n     @@ t/lib-shell-parser.pl (new)\n      +#\n      +# In other languages, `1+2` would typically be scanned as three tokens\n      +# (`1`, `+`, and `2`), but in shell it is a single token. However, the similar\n     -+# `1 + 2`, which embeds whitepace, is scanned as three token in shell, as well.\n     ++# `1 + 2`, which embeds whitespace, is scanned as three token in shell, as well.\n      +# In shell, several characters with special meaning lose that meaning when not\n      +# surrounded by whitespace. For instance, the negation operator `!` is special\n      +# when standing alone surrounded by whitespace; whereas in `foo!uucp` it is\n 4:  c1b86748d1 = 4:  416c95fee5 t: fix Lexer line count for $() inside double-quoted strings\n 5:  3a589ef738 ! 5:  7518445733 t: convert grep assertions to test_grep\n     @@ Commit message\n          Existing '! test_grep' calls are rewritten to 'test_grep !' so\n          that the diagnostic output is preserved on failure.\n      \n     +    test_grep requires the file it reads to exist, so '! grep'\n     +    assertions that inspect a file whose presence is conditional need\n     +    care.  In t5537 the '.git/shallow' file is still present after the\n     +    repack (the client remains shallow), so the assertion is\n     +    converted like any other.  In t1400 the '.git/packed-refs' file\n     +    exists only with the files backend, so its check is guarded with a\n     +    REFFILES prerequisite; the backend-agnostic 'git show-ref' check\n     +    that follows still runs under every backend.  In t7450 'git~2' is\n     +    the NTFS 8.3 short name of a decoy '..git' file and only exists\n     +    when 8.3 short-name generation is enabled; that '! grep' tolerates\n     +    the missing file on purpose, so it is left as-is with a\n     +    \"# lint-ok\" annotation rather than converted (a plain test_grep\n     +    would BUG when the short name is absent).\n     +\n          The conversion was generated using a grep-assertion linter\n          (greplint.pl, added in the following commit) to identify bare\n     -    grep calls at command position.  To reproduce:\n     +    grep calls at command position.  To reproduce, from the t/\n     +    directory:\n      \n     -        # Step 1: mark bare greps that should not be converted\n     -        sed -i '/! grep \"$m\" \\.git\\/packed-refs/s/$/ # lint-ok: file may not exist (reftable)/' \\\n     -            t/t1400-update-ref.sh\n     -        sed -i '/! grep dirty file3 &&/{/lint-ok/!s/$/ # lint-ok: file may not exist after --quit/}' \\\n     -            t/t3420-rebase-autostash.sh\n     +        # Step 1: annotate the two data-filter greps (grep produces\n     +        # data, not a verdict) so the linter skips them.\n              sed -i '/grep -vf before commits\\.raw/s/$/ # lint-ok: data filter/' \\\n     -            t/t5326-multi-pack-bitmaps.sh\n     -        sed -i '/! grep $d shallow-client\\/\\.git\\/shallow/s/$/ # lint-ok: file may not exist after repack/' \\\n     -            t/t5537-fetch-shallow.sh\n     +            t5326-multi-pack-bitmaps.sh\n              sed -i '/grep -E \"^\\[0-9a-f\\].*|| :/s/$/ # lint-ok: data filter/' \\\n     -            t/t5702-protocol-v2.sh\n     -        sed -i '/! grep gitdir squatting-clone/s/$/ # lint-ok: file may not exist after failed clone/' \\\n     -            t/t7450-bad-git-dotfiles.sh\n     +            t5702-protocol-v2.sh\n     +\n     +        # Step 1b: two '! grep' assertions need more than a mechanical\n     +        # conversion; handle them by hand before the linter-driven steps\n     +        # below so it leaves them alone.\n     +        #\n     +        # t1400: '.git/packed-refs' is absent under reftable, so guard the\n     +        # check with REFFILES (a plain test_grep would BUG on the missing\n     +        # file):\n     +        #\n     +        #      git update-ref -d HEAD $B &&\n     +        #  -   ! grep \"$m\" .git/packed-refs &&\n     +        #  +   if test_have_prereq REFFILES\n     +        #  +   then\n     +        #  +           test_grep ! \"$m\" .git/packed-refs\n     +        #  +   fi &&\n     +        #      test_must_fail git show-ref --verify -q $m\n     +        #\n     +        # t7450: git~2 is an NTFS 8.3 short name that exists only when\n     +        # short-name generation is enabled, so this stays a missing-file-\n     +        # tolerant '! grep'; add a comment plus \"# lint-ok\" so the linter\n     +        # skips it.\n      \n              # Step 2: reorder pre-existing '! test_grep' to 'test_grep !'\n              # (must come before steps 3-4 so greplint does not see them)\n     -        sed -i 's/! test_grep/test_grep !/' t/t0031-lockfile-pid.sh\n     -        sed -i 's/! test_grep/test_grep !/' t/t5300-pack-object.sh\n     -        sed -i 's/! test_grep/test_grep !/' t/t5319-multi-pack-index.sh\n     +        sed -i 's/! test_grep/test_grep !/' t0031-lockfile-pid.sh\n     +        sed -i 's/! test_grep/test_grep !/' t5300-pack-object.sh\n     +        sed -i 's/! test_grep/test_grep !/' t5319-multi-pack-index.sh\n      \n              # Step 3: convert '! grep' -> 'test_grep !'\n     -        perl t/greplint.pl t/*.sh 2>&1 | cut -d: -f1,2 |\n     +        perl greplint.pl *.sh 2>&1 | cut -d: -f1,2 |\n              while IFS=: read f l; do\n                  sed -i \"${l}s/! *grep/test_grep !/\" \"$f\"\n              done\n      \n              # Step 4: convert remaining 'grep' -> 'test_grep'\n     -        perl t/greplint.pl t/*.sh 2>&1 | cut -d: -f1,2 |\n     +        perl greplint.pl *.sh 2>&1 | cut -d: -f1,2 |\n              while IFS=: read f l; do\n                  sed -i \"${l}s/grep/test_grep/\" \"$f\"\n              done\n     @@ t/t1400-update-ref.sh: test_expect_success \"move $m (by HEAD)\" '\n       \ttest_when_finished \"git update-ref -d $m\" &&\n       \tgit update-ref -d HEAD $B &&\n      -\t! grep \"$m\" .git/packed-refs &&\n     -+\t! grep \"$m\" .git/packed-refs && # lint-ok: file may not exist (reftable)\n     ++\tif test_have_prereq REFFILES\n     ++\tthen\n     ++\t\ttest_grep ! \"$m\" .git/packed-refs\n     ++\tfi &&\n       \ttest_must_fail git show-ref --verify -q $m\n       '\n       \n     @@ t/t3420-rebase-autostash.sh: testrebase () {\n       \t\tgit checkout feature-branch\n       \t'\n       \n     -@@ t/t3420-rebase-autostash.sh: testrebase () {\n     - \t\tgit rebase --quit &&\n     - \t\ttest_when_finished git stash drop &&\n     - \t\ttest_path_is_missing $dotest/autostash &&\n     --\t\t! grep dirty file3 &&\n     -+\t\t! grep dirty file3 && # lint-ok: file may not exist after --quit\n     - \t\tgit stash show -p >actual &&\n     - \t\ttest_cmp expect actual &&\n     - \t\tgit reset --hard &&\n      @@ t/t3420-rebase-autostash.sh: testrebase () {\n       \t\tgit rebase$type unrelated-onto-branch >actual 2>&1 &&\n       \t\ttest_path_is_missing $dotest &&\n     @@ t/t4211-line-log.sh: test_expect_success '-L with -G filters to diff-text matche\n      +\ttest_grep \"F2 + 2\" actual\n       '\n       \n     - test_done\n     + test_expect_success '-L with --diff-filter=M excludes root commit' '\n      \n       ## t/t4216-log-bloom.sh ##\n      @@ t/t4216-log-bloom.sh: test_expect_success 'persist filter settings' '\n     @@ t/t5537-fetch-shallow.sh: test_expect_success '.git/shallow is edited by repack'\n       \tgit -C shallow-client repack -adfl &&\n       \ttest_must_fail git -C shallow-client rev-parse --verify $d^0 &&\n      -\t! grep $d shallow-client/.git/shallow &&\n     -+\t! grep $d shallow-client/.git/shallow && # lint-ok: file may not exist after repack\n     ++\ttest_grep ! $d shallow-client/.git/shallow &&\n       \n       \tgit -C shallow-server branch branch-orig $d &&\n       \tgit -C shallow-client fetch --prune --depth=2 \\\n     @@ t/t7450-bad-git-dotfiles.sh: test_expect_success WINDOWS 'prevent git~1 squattin\n       \t\t\tclone --recurse-submodules squatting squatting-clone 2>err &&\n       \t\ttest_grep -e \"directory not empty\" -e \"not an empty directory\" err &&\n      -\t\t! grep gitdir squatting-clone/d/a/git~2\n     -+\t\t! grep gitdir squatting-clone/d/a/git~2 # lint-ok: file may not exist after failed clone\n     ++\t\t# git~2 is the 8.3 short name of the ..git decoy, present\n     ++\t\t# only when 8.3 name generation is on; a missing git~2 is\n     ++\t\t# fine because the \"directory not empty\" check above is the\n     ++\t\t# real assertion.\n     ++\t\t! grep gitdir squatting-clone/d/a/git~2 # lint-ok: 8.3 short name git~2 may not exist\n       \tfi\n       '\n       \n 6:  e5ecb37401 = 6:  3b12a959fd t: add greplint to detect bare grep assertions\n\n-- \ngitgitgadget\n"},{"id":"547025","messageId":"ec6b478c4ce0da2c963c4cee4eacde50d3d877cc.1783054466.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v3.git.1783054466.gitgitgadget@gmail.com","subject":"[PATCH v3 1/6] t/README: document test_grep helper","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-03T04:54:20Z","receivedAt":"2026-07-03T04:54:32Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\ntest_grep is a wrapper around grep for test assertions that prints\nthe file contents on failure for easier debugging.  It also accepts\n'!' as its first argument for negation, which preserves the\ndiagnostic output that '! test_grep' would suppress.\n\nDespite being widely used (and the preferred replacement for bare\ngrep in assertions), test_grep has no entry in t/README alongside\nthe other documented helpers like test_cmp and test_line_count.\nAdd one.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/README | 26 ++++++++++++++++++++++++++\n 1 file changed, 26 insertions(+)\n\ndiff --git a/t/README b/t/README\nindex 085921be4b..928331fc7d 100644\n--- a/t/README\n+++ b/t/README\n@@ -1039,6 +1039,32 @@ see test-lib-functions.sh for the full list and their options.\n \n    Check whether a file has the length it is expected to.\n \n+ - test_grep [!] [<grep-options>] <pattern> <file>\n+\n+   Check whether <file> contains a line matching <pattern>, or\n+   with '!' that no line matches.  Use this instead of bare\n+   'grep <pattern> <file>' in test assertions.  On failure,\n+   test_grep prints the contents of <file> for easier debugging,\n+   whereas a bare 'grep' would fail silently.\n+\n+   For negation, pass '!' as the first argument:\n+\n+\ttest_grep ! \"^diff --git\" actual\n+\n+   Do not negate by writing '! test_grep', as that suppresses the\n+   diagnostic output.\n+\n+   test_grep should only be used as a test assertion.  When grep\n+   is used as a data filter (e.g. 'grep -v \"^index\" actual >filtered')\n+   or inside a command substitution (e.g. '$(grep -c ...)'), plain\n+   'grep' is the right choice because the exit code is not the\n+   assertion itself.\n+\n+   test_grep requires <file> to exist and will BUG otherwise.\n+   When a file's presence is conditional (a backend-specific\n+   file, or a path that only exists on some platforms, such as\n+   an NTFS 8.3 short name), keep a plain guarded 'grep' instead.\n+\n  - test_path_is_file <path>\n    test_path_is_dir <path>\n    test_path_is_missing <path>\n-- \ngitgitgadget\n\n"},{"id":"547026","messageId":"7a837b77cdaf76052a68b03f643e791726e525a1.1783054466.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v3.git.1783054466.gitgitgadget@gmail.com","subject":"[PATCH v3 2/6] t: fix grep assertions missing file arguments","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-03T04:54:21Z","receivedAt":"2026-07-03T04:54:34Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nThree grep assertions were missing their file arguments, causing\nthem to read from empty stdin instead of the intended file:\n\n- t2402: '! grep ...' should read from 'out', matching the\n  grep on the preceding line.\n- t7507: the closing quote is in the wrong place, making the\n  entire 'diff --git actual' a single pattern with no file\n  argument instead of pattern 'diff --git' and file 'actual'.\n- t7700: '! grep ...' should read from 'packlist', matching\n  the redirect on the preceding line.\n\nWithout file arguments these greps always succeed (empty stdin\nmatches nothing), so the assertions were not actually checking\nanything.  All three tests pass with the corrected file arguments,\nconfirming the intended behavior is sound.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/t2402-worktree-list.sh  | 2 +-\n t/t7507-commit-verbose.sh | 2 +-\n t/t7700-repack.sh         | 2 +-\n 3 files changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh\nindex e0c6abd2f5..93f92e854a 100755\n--- a/t/t2402-worktree-list.sh\n+++ b/t/t2402-worktree-list.sh\n@@ -144,7 +144,7 @@ test_expect_success '\"list\" all worktrees with prunable annotation' '\n \trm -rf prunable &&\n \tgit worktree list >out &&\n \tgrep \"/prunable  *[0-9a-f].* prunable$\" out &&\n-\t! grep \"/unprunable  *[0-9a-f].* prunable$\"\n+\t! grep \"/unprunable  *[0-9a-f].* prunable$\" out\n '\n \n test_expect_success '\"list\" all worktrees --porcelain with prunable' '\ndiff --git a/t/t7507-commit-verbose.sh b/t/t7507-commit-verbose.sh\nindex b53d71c086..acdb6b1455 100755\n--- a/t/t7507-commit-verbose.sh\n+++ b/t/t7507-commit-verbose.sh\n@@ -163,7 +163,7 @@ done\n \n test_expect_success \"status ignores commit.verbose=true\" '\n \tgit -c commit.verbose=true status >actual &&\n-\t! grep \"^diff --git actual\"\n+\t! grep \"^diff --git\" actual\n '\n \n test_done\ndiff --git a/t/t7700-repack.sh b/t/t7700-repack.sh\nindex 63ef63fc50..c6ff3aed30 100755\n--- a/t/t7700-repack.sh\n+++ b/t/t7700-repack.sh\n@@ -194,7 +194,7 @@ test_expect_success 'local packed unreachable obs that exist in alternate ODB ar\n \trm -f .git/objects/pack/* &&\n \tmv pack-* .git/objects/pack/ &&\n \tgit verify-pack -v -- .git/objects/pack/*.idx >packlist &&\n-\t! grep \"^$coid \" &&\n+\t! grep \"^$coid \" packlist &&\n \techo >.git/objects/info/alternates &&\n \ttest_must_fail git show $coid\n '\n-- \ngitgitgadget\n\n"},{"id":"547029","messageId":"5894ca1724c29a6a7dfd5b5eface11b7721c9f92.1783054466.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v3.git.1783054466.gitgitgadget@gmail.com","subject":"[PATCH v3 3/6] t: extract chainlint's parser into shared module","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-03T04:54:22Z","receivedAt":"2026-07-03T04:54:35Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nMove chainlint.pl's Lexer, ShellParser, and ScriptParser into a\nshared module (lib-shell-parser.pl) so other lint tools can reuse\nthe same shell parsing infrastructure.  A subsequent commit adds\ngreplint.pl, which needs the same tokenizer to correctly identify\ncommand boundaries.\n\nScriptParser's check_test() becomes a no-op in the shared module.\nchainlint.pl defines ChainlintParser (extending ScriptParser)\nwith the &&-chain check_test() implementation.\n\nNo functional change: chainlint produces the same output and\ncheck-chainlint self-tests pass.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/chainlint.pl        | 529 +----------------------------------------\n t/lib-shell-parser.pl | 531 ++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 543 insertions(+), 517 deletions(-)\n create mode 100644 t/lib-shell-parser.pl\n\ndiff --git a/t/chainlint.pl b/t/chainlint.pl\nindex 2d07a99700..cededf15ee 100755\n--- a/t/chainlint.pl\n+++ b/t/chainlint.pl\n@@ -23,458 +23,9 @@ my $jobs = -1;\n my $show_stats;\n my $emit_all;\n \n-# Lexer tokenizes POSIX shell scripts. It is roughly modeled after section 2.3\n-# \"Token Recognition\" of POSIX chapter 2 \"Shell Command Language\". Although\n-# similar to lexical analyzers for other languages, this one differs in a few\n-# substantial ways due to quirks of the shell command language.\n-#\n-# For instance, in many languages, newline is just whitespace like space or\n-# TAB, but in shell a newline is a command separator, thus a distinct lexical\n-# token. A newline is significant and returned as a distinct token even at the\n-# end of a shell comment.\n-#\n-# In other languages, `1+2` would typically be scanned as three tokens\n-# (`1`, `+`, and `2`), but in shell it is a single token. However, the similar\n-# `1 + 2`, which embeds whitespace, is scanned as three token in shell, as well.\n-# In shell, several characters with special meaning lose that meaning when not\n-# surrounded by whitespace. For instance, the negation operator `!` is special\n-# when standing alone surrounded by whitespace; whereas in `foo!uucp` it is\n-# just a plain character in the longer token \"foo!uucp\". In many other\n-# languages, `\"string\"/foo:'string'` might be scanned as five tokens (\"string\",\n-# `/`, `foo`, `:`, and 'string'), but in shell, it is just a single token.\n-#\n-# The lexical analyzer for the shell command language is also somewhat unusual\n-# in that it recursively invokes the parser to handle the body of `$(...)`\n-# expressions which can contain arbitrary shell code. Such expressions may be\n-# encountered both inside and outside of double-quoted strings.\n-#\n-# The lexical analyzer is responsible for consuming shell here-doc bodies which\n-# extend from the line following a `<<TAG` operator until a line consisting\n-# solely of `TAG`. Here-doc consumption begins when a newline is encountered.\n-# It is legal for multiple here-doc `<<TAG` operators to be present on a single\n-# line, in which case their bodies must be present one following the next, and\n-# are consumed in the (left-to-right) order the `<<TAG` operators appear on the\n-# line. A special complication is that the bodies of all here-docs must be\n-# consumed when the newline is encountered even if the parse context depth has\n-# changed. For instance, in `cat <<A && x=$(cat <<B &&\\n`, bodies of here-docs\n-# \"A\" and \"B\" must be consumed even though \"A\" was introduced outside the\n-# recursive parse context in which \"B\" was introduced and in which the newline\n-# is encountered.\n-package Lexer;\n-\n-sub new {\n-\tmy ($class, $parser, $s) = @_;\n-\tbless {\n-\t\tparser => $parser,\n-\t\tbuff => $s,\n-\t\tlineno => 1,\n-\t\theretags => []\n-\t} => $class;\n-}\n-\n-sub scan_heredoc_tag {\n-\tmy $self = shift @_;\n-\t${$self->{buff}} =~ /\\G(-?)/gc;\n-\tmy $indented = $1;\n-\tmy $token = $self->scan_token();\n-\treturn \"<<$indented\" unless $token;\n-\tmy $tag = $token->[0];\n-\t$tag =~ s/['\"\\\\]//g;\n-\t$$token[0] = $indented ? \"\\t$tag\" : \"$tag\";\n-\tpush(@{$self->{heretags}}, $token);\n-\treturn \"<<$indented$tag\";\n-}\n-\n-sub scan_op {\n-\tmy ($self, $c) = @_;\n-\tmy $b = $self->{buff};\n-\treturn $c unless $$b =~ /\\G(.)/sgc;\n-\tmy $cc = $c . $1;\n-\treturn scan_heredoc_tag($self) if $cc eq '<<';\n-\treturn $cc if $cc =~ /^(?:&&|\\|\\||>>|;;|<&|>&|<>|>\\|)$/;\n-\tpos($$b)--;\n-\treturn $c;\n-}\n-\n-sub scan_sqstring {\n-\tmy $self = shift @_;\n-\t${$self->{buff}} =~ /\\G([^']*'|.*\\z)/sgc;\n-\tmy $s = $1;\n-\t$self->{lineno} += () = $s =~ /\\n/sg;\n-\treturn \"'\" . $s;\n-}\n-\n-sub scan_dqstring {\n-\tmy $self = shift @_;\n-\tmy $b = $self->{buff};\n-\tmy $s = '\"';\n-\twhile (1) {\n-\t\t# slurp up non-special characters\n-\t\t$s .= $1 if $$b =~ /\\G([^\"\\$\\\\]+)/gc;\n-\t\t# handle special characters\n-\t\tlast unless $$b =~ /\\G(.)/sgc;\n-\t\tmy $c = $1;\n-\t\t$s .= '\"', last if $c eq '\"';\n-\t\t$s .= '$' . $self->scan_dollar(), next if $c eq '$';\n-\t\tif ($c eq '\\\\') {\n-\t\t\t$s .= '\\\\', last unless $$b =~ /\\G(.)/sgc;\n-\t\t\t$c = $1;\n-\t\t\t$self->{lineno}++, next if $c eq \"\\n\"; # line splice\n-\t\t\t# backslash escapes only $, `, \", \\ in dq-string\n-\t\t\t$s .= '\\\\' unless $c =~ /^[\\$`\"\\\\]$/;\n-\t\t\t$s .= $c;\n-\t\t\tnext;\n-\t\t}\n-\t\tdie(\"internal error scanning dq-string '$c'\\n\");\n-\t}\n-\t$self->{lineno} += () = $s =~ /\\n/sg;\n-\treturn $s;\n-}\n-\n-sub scan_balanced {\n-\tmy ($self, $c1, $c2) = @_;\n-\tmy $b = $self->{buff};\n-\tmy $depth = 1;\n-\tmy $s = $c1;\n-\twhile ($$b =~ /\\G([^\\Q$c1$c2\\E]*(?:[\\Q$c1$c2\\E]|\\z))/gc) {\n-\t\t$s .= $1;\n-\t\t$depth++, next if $s =~ /\\Q$c1\\E$/;\n-\t\t$depth--;\n-\t\tlast if $depth == 0;\n-\t}\n-\t$self->{lineno} += () = $s =~ /\\n/sg;\n-\treturn $s;\n-}\n-\n-sub scan_subst {\n-\tmy $self = shift @_;\n-\tmy @tokens = $self->{parser}->parse(qr/^\\)$/);\n-\t$self->{parser}->next_token(); # closing \")\"\n-\treturn @tokens;\n-}\n-\n-sub scan_dollar {\n-\tmy $self = shift @_;\n-\tmy $b = $self->{buff};\n-\treturn $self->scan_balanced('(', ')') if $$b =~ /\\G\\((?=\\()/gc; # $((...))\n-\treturn '(' . join(' ', map {$_->[0]} $self->scan_subst()) . ')' if $$b =~ /\\G\\(/gc; # $(...)\n-\treturn $self->scan_balanced('{', '}') if $$b =~ /\\G\\{/gc; # ${...}\n-\treturn $1 if $$b =~ /\\G(\\w+)/gc; # $var\n-\treturn $1 if $$b =~ /\\G([@*#?$!0-9-])/gc; # $*, $1, $$, etc.\n-\treturn '';\n-}\n-\n-sub swallow_heredocs {\n-\tmy $self = shift @_;\n-\tmy $b = $self->{buff};\n-\tmy $tags = $self->{heretags};\n-\twhile (my $tag = shift @$tags) {\n-\t\tmy $start = pos($$b);\n-\t\tmy $indent = $$tag[0] =~ s/^\\t// ? '\\\\s*' : '';\n-\t\t$$b =~ /(?:\\G|\\n)$indent\\Q$$tag[0]\\E(?:\\n|\\z)/gc;\n-\t\tif (pos($$b) > $start) {\n-\t\t\tmy $body = substr($$b, $start, pos($$b) - $start);\n-\t\t\t$self->{parser}->{heredocs}->{$$tag[0]} = {\n-\t\t\t\tcontent => substr($body, 0, length($body) - length($&)),\n-\t\t\t\tstart_line => $self->{lineno},\n-\t\t        };\n-\t\t\t$self->{lineno} += () = $body =~ /\\n/sg;\n-\t\t\tnext;\n-\t\t}\n-\t\tpush(@{$self->{parser}->{problems}}, ['HEREDOC', $tag]);\n-\t\t$$b =~ /(?:\\G|\\n).*\\z/gc; # consume rest of input\n-\t\tmy $body = substr($$b, $start, pos($$b) - $start);\n-\t\t$self->{lineno} += () = $body =~ /\\n/sg;\n-\t\tlast;\n-\t}\n-}\n-\n-sub scan_token {\n-\tmy $self = shift @_;\n-\tmy $b = $self->{buff};\n-\tmy $token = '';\n-\tmy ($start, $startln);\n-RESTART:\n-\t$startln = $self->{lineno};\n-\t$$b =~ /\\G[ \\t]+/gc; # skip whitespace (but not newline)\n-\t$start = pos($$b) || 0;\n-\t$self->{lineno}++, return [\"\\n\", $start, pos($$b), $startln, $startln] if $$b =~ /\\G#[^\\n]*(?:\\n|\\z)/gc; # comment\n-\twhile (1) {\n-\t\t# slurp up non-special characters\n-\t\t$token .= $1 if $$b =~ /\\G([^\\\\;&|<>(){}'\"\\$\\s]+)/gc;\n-\t\t# handle special characters\n-\t\tlast unless $$b =~ /\\G(.)/sgc;\n-\t\tmy $c = $1;\n-\t\tpos($$b)--, last if $c =~ /^[ \\t]$/; # whitespace ends token\n-\t\tpos($$b)--, last if length($token) && $c =~ /^[;&|<>(){}\\n]$/;\n-\t\t$token .= $self->scan_sqstring(), next if $c eq \"'\";\n-\t\t$token .= $self->scan_dqstring(), next if $c eq '\"';\n-\t\t$token .= $c . $self->scan_dollar(), next if $c eq '$';\n-\t\t$self->{lineno}++, $self->swallow_heredocs(), $token = $c, last if $c eq \"\\n\";\n-\t\t$token = $self->scan_op($c), last if $c =~ /^[;&|<>]$/;\n-\t\t$token = $c, last if $c =~ /^[(){}]$/;\n-\t\tif ($c eq '\\\\') {\n-\t\t\t$token .= '\\\\', last unless $$b =~ /\\G(.)/sgc;\n-\t\t\t$c = $1;\n-\t\t\t$self->{lineno}++, next if $c eq \"\\n\" && length($token); # line splice\n-\t\t\t$self->{lineno}++, goto RESTART if $c eq \"\\n\"; # line splice\n-\t\t\t$token .= '\\\\' . $c;\n-\t\t\tnext;\n-\t\t}\n-\t\tdie(\"internal error scanning character '$c'\\n\");\n-\t}\n-\treturn length($token) ? [$token, $start, pos($$b), $startln, $self->{lineno}] : undef;\n-}\n-\n-# ShellParser parses POSIX shell scripts (with minor extensions for Bash). It\n-# is a recursive descent parser very roughly modeled after section 2.10 \"Shell\n-# Grammar\" of POSIX chapter 2 \"Shell Command Language\".\n-package ShellParser;\n-\n-sub new {\n-\tmy ($class, $s) = @_;\n-\tmy $self = bless {\n-\t\tbuff => [],\n-\t\tstop => [],\n-\t\toutput => [],\n-\t\theredocs => {},\n-\t\tinsubshell => 0,\n-\t} => $class;\n-\t$self->{lexer} = Lexer->new($self, $s);\n-\treturn $self;\n-}\n-\n-sub next_token {\n-\tmy $self = shift @_;\n-\treturn pop(@{$self->{buff}}) if @{$self->{buff}};\n-\treturn $self->{lexer}->scan_token();\n-}\n-\n-sub untoken {\n-\tmy $self = shift @_;\n-\tpush(@{$self->{buff}}, @_);\n-}\n-\n-sub peek {\n-\tmy $self = shift @_;\n-\tmy $token = $self->next_token();\n-\treturn undef unless defined($token);\n-\t$self->untoken($token);\n-\treturn $token;\n-}\n-\n-sub stop_at {\n-\tmy ($self, $token) = @_;\n-\treturn 1 unless defined($token);\n-\tmy $stop = ${$self->{stop}}[-1] if @{$self->{stop}};\n-\treturn defined($stop) && $token->[0] =~ $stop;\n-}\n-\n-sub expect {\n-\tmy ($self, $expect) = @_;\n-\tmy $token = $self->next_token();\n-\treturn $token if defined($token) && $token->[0] eq $expect;\n-\tpush(@{$self->{output}}, \"?!ERR?! expected '$expect' but found '\" . (defined($token) ? $token->[0] : \"<end-of-input>\") . \"'\\n\");\n-\t$self->untoken($token) if defined($token);\n-\treturn ();\n-}\n-\n-sub optional_newlines {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\twhile (my $token = $self->peek()) {\n-\t\tlast unless $token->[0] eq \"\\n\";\n-\t\tpush(@tokens, $self->next_token());\n-\t}\n-\treturn @tokens;\n-}\n-\n-sub parse_group {\n-\tmy $self = shift @_;\n-\treturn ($self->parse(qr/^}$/),\n-\t\t$self->expect('}'));\n-}\n-\n-sub parse_subshell {\n-\tmy $self = shift @_;\n-\t$self->{insubshell}++;\n-\tmy @tokens = ($self->parse(qr/^\\)$/),\n-\t\t      $self->expect(')'));\n-\t$self->{insubshell}--;\n-\treturn @tokens;\n-}\n-\n-sub parse_case_pattern {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\twhile (defined(my $token = $self->next_token())) {\n-\t\tpush(@tokens, $token);\n-\t\tlast if $token->[0] eq ')';\n-\t}\n-\treturn @tokens;\n-}\n-\n-sub parse_case {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\tpush(@tokens,\n-\t     $self->next_token(), # subject\n-\t     $self->optional_newlines(),\n-\t     $self->expect('in'),\n-\t     $self->optional_newlines());\n-\twhile (1) {\n-\t\tmy $token = $self->peek();\n-\t\tlast unless defined($token) && $token->[0] ne 'esac';\n-\t\tpush(@tokens,\n-\t\t     $self->parse_case_pattern(),\n-\t\t     $self->optional_newlines(),\n-\t\t     $self->parse(qr/^(?:;;|esac)$/)); # item body\n-\t\t$token = $self->peek();\n-\t\tlast unless defined($token) && $token->[0] ne 'esac';\n-\t\tpush(@tokens,\n-\t\t     $self->expect(';;'),\n-\t\t     $self->optional_newlines());\n-\t}\n-\tpush(@tokens, $self->expect('esac'));\n-\treturn @tokens;\n-}\n-\n-sub parse_for {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\tpush(@tokens,\n-\t     $self->next_token(), # variable\n-\t     $self->optional_newlines());\n-\tmy $token = $self->peek();\n-\tif (defined($token) && $token->[0] eq 'in') {\n-\t\tpush(@tokens,\n-\t\t     $self->expect('in'),\n-\t\t     $self->optional_newlines());\n-\t}\n-\tpush(@tokens,\n-\t     $self->parse(qr/^do$/), # items\n-\t     $self->expect('do'),\n-\t     $self->optional_newlines(),\n-\t     $self->parse_loop_body(),\n-\t     $self->expect('done'));\n-\treturn @tokens;\n-}\n-\n-sub parse_if {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\twhile (1) {\n-\t\tpush(@tokens,\n-\t\t     $self->parse(qr/^then$/), # if/elif condition\n-\t\t     $self->expect('then'),\n-\t\t     $self->optional_newlines(),\n-\t\t     $self->parse(qr/^(?:elif|else|fi)$/)); # if/elif body\n-\t\tmy $token = $self->peek();\n-\t\tlast unless defined($token) && $token->[0] eq 'elif';\n-\t\tpush(@tokens, $self->expect('elif'));\n-\t}\n-\tmy $token = $self->peek();\n-\tif (defined($token) && $token->[0] eq 'else') {\n-\t\tpush(@tokens,\n-\t\t     $self->expect('else'),\n-\t\t     $self->optional_newlines(),\n-\t\t     $self->parse(qr/^fi$/)); # else body\n-\t}\n-\tpush(@tokens, $self->expect('fi'));\n-\treturn @tokens;\n-}\n-\n-sub parse_loop_body {\n-\tmy $self = shift @_;\n-\treturn $self->parse(qr/^done$/);\n-}\n-\n-sub parse_loop {\n-\tmy $self = shift @_;\n-\treturn ($self->parse(qr/^do$/), # condition\n-\t\t$self->expect('do'),\n-\t\t$self->optional_newlines(),\n-\t\t$self->parse_loop_body(),\n-\t\t$self->expect('done'));\n-}\n-\n-sub parse_func {\n-\tmy $self = shift @_;\n-\treturn ($self->expect('('),\n-\t\t$self->expect(')'),\n-\t\t$self->optional_newlines(),\n-\t\t$self->parse_cmd()); # body\n-}\n-\n-sub parse_bash_array_assignment {\n-\tmy $self = shift @_;\n-\tmy @tokens = $self->expect('(');\n-\twhile (defined(my $token = $self->next_token())) {\n-\t\tpush(@tokens, $token);\n-\t\tlast if $token->[0] eq ')';\n-\t}\n-\treturn @tokens;\n-}\n-\n-my %compound = (\n-\t'{' => \\&parse_group,\n-\t'(' => \\&parse_subshell,\n-\t'case' => \\&parse_case,\n-\t'for' => \\&parse_for,\n-\t'if' => \\&parse_if,\n-\t'until' => \\&parse_loop,\n-\t'while' => \\&parse_loop);\n-\n-sub parse_cmd {\n-\tmy $self = shift @_;\n-\tmy $cmd = $self->next_token();\n-\treturn () unless defined($cmd);\n-\treturn $cmd if $cmd->[0] eq \"\\n\";\n-\n-\tmy $token;\n-\tmy @tokens = $cmd;\n-\tif ($cmd->[0] eq '!') {\n-\t\tpush(@tokens, $self->parse_cmd());\n-\t\treturn @tokens;\n-\t} elsif (my $f = $compound{$cmd->[0]}) {\n-\t\tpush(@tokens, $self->$f());\n-\t} elsif (defined($token = $self->peek()) && $token->[0] eq '(') {\n-\t\tif ($cmd->[0] !~ /\\w=$/) {\n-\t\t\tpush(@tokens, $self->parse_func());\n-\t\t\treturn @tokens;\n-\t\t}\n-\t\tmy @array = $self->parse_bash_array_assignment();\n-\t\t$tokens[-1]->[0] .= join(' ', map {$_->[0]} @array);\n-\t\t$tokens[-1]->[2] = $array[$#array][2] if @array;\n-\t}\n-\n-\twhile (defined(my $token = $self->next_token())) {\n-\t\t$self->untoken($token), last if $self->stop_at($token);\n-\t\tpush(@tokens, $token);\n-\t\tlast if $token->[0] =~ /^(?:[;&\\n|]|&&|\\|\\|)$/;\n-\t}\n-\tpush(@tokens, $self->next_token()) if $tokens[-1]->[0] ne \"\\n\" && defined($token = $self->peek()) && $token->[0] eq \"\\n\";\n-\treturn @tokens;\n-}\n-\n-sub accumulate {\n-\tmy ($self, $tokens, $cmd) = @_;\n-\tpush(@$tokens, @$cmd);\n-}\n-\n-sub parse {\n-\tmy ($self, $stop) = @_;\n-\tpush(@{$self->{stop}}, $stop);\n-\tgoto DONE if $self->stop_at($self->peek());\n-\tmy @tokens;\n-\twhile (my @cmd = $self->parse_cmd()) {\n-\t\t$self->accumulate(\\@tokens, \\@cmd);\n-\t\tlast if $self->stop_at($self->peek());\n-\t}\n-DONE:\n-\tpop(@{$self->{stop}});\n-\treturn @tokens;\n-}\n+use File::Basename;\n+do(dirname($0) . \"/lib-shell-parser.pl\")\n+\tor die \"$0: failed to load lib-shell-parser.pl: $@$!\\n\";\n \n # TestParser is a subclass of ShellParser which, beyond parsing shell script\n # code, is also imbued with semantic knowledge of test construction, and checks\n@@ -482,9 +33,10 @@ DONE:\n # the tests themselves or in behaviors being exercised by the tests. As such,\n # TestParser is only called upon to parse test bodies, not the top-level\n # scripts in which the tests are defined.\n+\n package TestParser;\n \n-use base 'ShellParser';\n+our @ISA = ('ShellParser');\n \n sub new {\n \tmy $class = shift @_;\n@@ -578,51 +130,10 @@ DONE:\n \t$self->SUPER::accumulate($tokens, $cmd);\n }\n \n-# ScriptParser is a subclass of ShellParser which identifies individual test\n-# definitions within test scripts, and passes each test body through TestParser\n-# to identify possible problems. ShellParser detects test definitions not only\n-# at the top-level of test scripts but also within compound commands such as\n-# loops and function definitions.\n-package ScriptParser;\n-\n-use base 'ShellParser';\n-\n-sub new {\n-\tmy $class = shift @_;\n-\tmy $self = $class->SUPER::new(@_);\n-\t$self->{ntests} = 0;\n-\t$self->{nerrs} = 0;\n-\treturn $self;\n-}\n+# ChainlintParser extends ScriptParser with &&-chain checking\n+package ChainlintParser;\n \n-# extract the raw content of a token, which may be a single string or a\n-# composition of multiple strings and non-string character runs; for instance,\n-# `\"test body\"` unwraps to `test body`; `word\"a b\"42'c d'` to `worda b42c d`\n-sub unwrap {\n-\tmy $token = (@_ ? shift @_ : $_)->[0];\n-\t# simple case: 'sqstring' or \"dqstring\"\n-\treturn $token if $token =~ s/^'([^']*)'$/$1/;\n-\treturn $token if $token =~ s/^\"([^\"]*)\"$/$1/;\n-\n-\t# composite case\n-\tmy ($s, $q, $escaped);\n-\twhile (1) {\n-\t\t# slurp up non-special characters\n-\t\t$s .= $1 if $token =~ /\\G([^\\\\'\"]*)/gc;\n-\t\t# handle special characters\n-\t\tlast unless $token =~ /\\G(.)/sgc;\n-\t\tmy $c = $1;\n-\t\t$q = undef, next if defined($q) && $c eq $q;\n-\t\t$q = $c, next if !defined($q) && $c =~ /^['\"]$/;\n-\t\tif ($c eq '\\\\') {\n-\t\t\tlast unless $token =~ /\\G(.)/sgc;\n-\t\t\t$c = $1;\n-\t\t\t$s .= '\\\\' if $c eq \"\\n\"; # preserve line splice\n-\t\t}\n-\t\t$s .= $c;\n-\t}\n-\treturn $s\n-}\n+our @ISA = ('ScriptParser');\n \n sub format_problem {\n \tlocal $_ = shift;\n@@ -635,10 +146,10 @@ sub format_problem {\n \n sub check_test {\n \tmy $self = shift @_;\n-\tmy $title = unwrap(shift @_);\n+\tmy $title = ScriptParser::unwrap(shift @_);\n \tmy $body = shift @_;\n \tmy $lineno = $body->[3];\n-\t$body = unwrap($body);\n+\t$body = ScriptParser::unwrap($body);\n \tif ($body eq '-') {\n \t\tmy $herebody = shift @_;\n \t\t$body = $herebody->{content};\n@@ -673,24 +184,8 @@ sub check_test {\n \tpush(@{$self->{output}}, \"$c->{blue}# chainlint: $title$c->{reset}\\n$checked\");\n }\n \n-sub parse_cmd {\n-\tmy $self = shift @_;\n-\tmy @tokens = $self->SUPER::parse_cmd();\n-\treturn @tokens unless @tokens && $tokens[0]->[0] =~ /^test_expect_(?:success|failure)$/;\n-\tmy $n = $#tokens;\n-\t$n-- while $n >= 0 && $tokens[$n]->[0] =~ /^(?:[;&\\n|]|&&|\\|\\|)$/;\n-\tmy $herebody;\n-\tif ($n >= 2 && $tokens[$n-1]->[0] eq '-' && $tokens[$n]->[0] =~ /^<<-?(.+)$/) {\n-\t\t$herebody = $self->{heredocs}->{$1};\n-\t\t$n--;\n-\t}\n-\t$self->check_test($tokens[1], $tokens[2], $herebody) if $n == 2; # title body\n-\t$self->check_test($tokens[2], $tokens[3], $herebody) if $n > 2;  # prereq title body\n-\treturn @tokens;\n-}\n-\n # main contains high-level functionality for processing command-line switches,\n-# feeding input test scripts to ScriptParser, and reporting results.\n+# feeding input test scripts to ChainlintParser, and reporting results.\n package main;\n \n my $getnow = sub { return time(); };\n@@ -803,7 +298,7 @@ sub check_script {\n \t\t}\n \t\tmy $s = do { local $/; <$fh> };\n \t\tclose($fh);\n-\t\tmy $parser = ScriptParser->new(\\$s);\n+\t\tmy $parser = ChainlintParser->new(\\$s);\n \t\t1 while $parser->parse_cmd();\n \t\tif (@{$parser->{output}}) {\n \t\t\tmy $c = fd_colors(1);\ndiff --git a/t/lib-shell-parser.pl b/t/lib-shell-parser.pl\nnew file mode 100644\nindex 0000000000..5c435c5d05\n--- /dev/null\n+++ b/t/lib-shell-parser.pl\n@@ -0,0 +1,531 @@\n+# Copyright (c) 2021-2022 Eric Sunshine <sunshine@sunshineco.com>\n+#\n+# Shared shell script parser for test lint tools. Provides Lexer,\n+# ShellParser, and ScriptParser. Subclass ScriptParser and override\n+# check_test() to implement lint checks.\n+\n+use strict;\n+use warnings;\n+\n+# Lexer tokenizes POSIX shell scripts. It is roughly modeled after section 2.3\n+# \"Token Recognition\" of POSIX chapter 2 \"Shell Command Language\". Although\n+# similar to lexical analyzers for other languages, this one differs in a few\n+# substantial ways due to quirks of the shell command language.\n+#\n+# For instance, in many languages, newline is just whitespace like space or\n+# TAB, but in shell a newline is a command separator, thus a distinct lexical\n+# token. A newline is significant and returned as a distinct token even at the\n+# end of a shell comment.\n+#\n+# In other languages, `1+2` would typically be scanned as three tokens\n+# (`1`, `+`, and `2`), but in shell it is a single token. However, the similar\n+# `1 + 2`, which embeds whitespace, is scanned as three token in shell, as well.\n+# In shell, several characters with special meaning lose that meaning when not\n+# surrounded by whitespace. For instance, the negation operator `!` is special\n+# when standing alone surrounded by whitespace; whereas in `foo!uucp` it is\n+# just a plain character in the longer token \"foo!uucp\". In many other\n+# languages, `\"string\"/foo:'string'` might be scanned as five tokens (\"string\",\n+# `/`, `foo`, `:`, and 'string'), but in shell, it is just a single token.\n+#\n+# The lexical analyzer for the shell command language is also somewhat unusual\n+# in that it recursively invokes the parser to handle the body of `$(...)`\n+# expressions which can contain arbitrary shell code. Such expressions may be\n+# encountered both inside and outside of double-quoted strings.\n+#\n+# The lexical analyzer is responsible for consuming shell here-doc bodies which\n+# extend from the line following a `<<TAG` operator until a line consisting\n+# solely of `TAG`. Here-doc consumption begins when a newline is encountered.\n+# It is legal for multiple here-doc `<<TAG` operators to be present on a single\n+# line, in which case their bodies must be present one following the next, and\n+# are consumed in the (left-to-right) order the `<<TAG` operators appear on the\n+# line. A special complication is that the bodies of all here-docs must be\n+# consumed when the newline is encountered even if the parse context depth has\n+# changed. For instance, in `cat <<A && x=$(cat <<B &&\\n`, bodies of here-docs\n+# \"A\" and \"B\" must be consumed even though \"A\" was introduced outside the\n+# recursive parse context in which \"B\" was introduced and in which the newline\n+# is encountered.\n+package Lexer;\n+\n+sub new {\n+\tmy ($class, $parser, $s) = @_;\n+\tbless {\n+\t\tparser => $parser,\n+\t\tbuff => $s,\n+\t\tlineno => 1,\n+\t\theretags => []\n+\t} => $class;\n+}\n+\n+sub scan_heredoc_tag {\n+\tmy $self = shift @_;\n+\t${$self->{buff}} =~ /\\G(-?)/gc;\n+\tmy $indented = $1;\n+\tmy $token = $self->scan_token();\n+\treturn \"<<$indented\" unless $token;\n+\tmy $tag = $token->[0];\n+\t$tag =~ s/['\"\\\\]//g;\n+\t$$token[0] = $indented ? \"\\t$tag\" : \"$tag\";\n+\tpush(@{$self->{heretags}}, $token);\n+\treturn \"<<$indented$tag\";\n+}\n+\n+sub scan_op {\n+\tmy ($self, $c) = @_;\n+\tmy $b = $self->{buff};\n+\treturn $c unless $$b =~ /\\G(.)/sgc;\n+\tmy $cc = $c . $1;\n+\treturn scan_heredoc_tag($self) if $cc eq '<<';\n+\treturn $cc if $cc =~ /^(?:&&|\\|\\||>>|;;|<&|>&|<>|>\\|)$/;\n+\tpos($$b)--;\n+\treturn $c;\n+}\n+\n+sub scan_sqstring {\n+\tmy $self = shift @_;\n+\t${$self->{buff}} =~ /\\G([^']*'|.*\\z)/sgc;\n+\tmy $s = $1;\n+\t$self->{lineno} += () = $s =~ /\\n/sg;\n+\treturn \"'\" . $s;\n+}\n+\n+sub scan_dqstring {\n+\tmy $self = shift @_;\n+\tmy $b = $self->{buff};\n+\tmy $s = '\"';\n+\twhile (1) {\n+\t\t# slurp up non-special characters\n+\t\t$s .= $1 if $$b =~ /\\G([^\"\\$\\\\]+)/gc;\n+\t\t# handle special characters\n+\t\tlast unless $$b =~ /\\G(.)/sgc;\n+\t\tmy $c = $1;\n+\t\t$s .= '\"', last if $c eq '\"';\n+\t\t$s .= '$' . $self->scan_dollar(), next if $c eq '$';\n+\t\tif ($c eq '\\\\') {\n+\t\t\t$s .= '\\\\', last unless $$b =~ /\\G(.)/sgc;\n+\t\t\t$c = $1;\n+\t\t\t$self->{lineno}++, next if $c eq \"\\n\"; # line splice\n+\t\t\t# backslash escapes only $, `, \", \\ in dq-string\n+\t\t\t$s .= '\\\\' unless $c =~ /^[\\$`\"\\\\]$/;\n+\t\t\t$s .= $c;\n+\t\t\tnext;\n+\t\t}\n+\t\tdie(\"internal error scanning dq-string '$c'\\n\");\n+\t}\n+\t$self->{lineno} += () = $s =~ /\\n/sg;\n+\treturn $s;\n+}\n+\n+sub scan_balanced {\n+\tmy ($self, $c1, $c2) = @_;\n+\tmy $b = $self->{buff};\n+\tmy $depth = 1;\n+\tmy $s = $c1;\n+\twhile ($$b =~ /\\G([^\\Q$c1$c2\\E]*(?:[\\Q$c1$c2\\E]|\\z))/gc) {\n+\t\t$s .= $1;\n+\t\t$depth++, next if $s =~ /\\Q$c1\\E$/;\n+\t\t$depth--;\n+\t\tlast if $depth == 0;\n+\t}\n+\t$self->{lineno} += () = $s =~ /\\n/sg;\n+\treturn $s;\n+}\n+\n+sub scan_subst {\n+\tmy $self = shift @_;\n+\tmy @tokens = $self->{parser}->parse(qr/^\\)$/);\n+\t$self->{parser}->next_token(); # closing \")\"\n+\treturn @tokens;\n+}\n+\n+sub scan_dollar {\n+\tmy $self = shift @_;\n+\tmy $b = $self->{buff};\n+\treturn $self->scan_balanced('(', ')') if $$b =~ /\\G\\((?=\\()/gc; # $((...))\n+\treturn '(' . join(' ', map {$_->[0]} $self->scan_subst()) . ')' if $$b =~ /\\G\\(/gc; # $(...)\n+\treturn $self->scan_balanced('{', '}') if $$b =~ /\\G\\{/gc; # ${...}\n+\treturn $1 if $$b =~ /\\G(\\w+)/gc; # $var\n+\treturn $1 if $$b =~ /\\G([@*#?$!0-9-])/gc; # $*, $1, $$, etc.\n+\treturn '';\n+}\n+\n+sub swallow_heredocs {\n+\tmy $self = shift @_;\n+\tmy $b = $self->{buff};\n+\tmy $tags = $self->{heretags};\n+\twhile (my $tag = shift @$tags) {\n+\t\tmy $start = pos($$b);\n+\t\tmy $indent = $$tag[0] =~ s/^\\t// ? '\\\\s*' : '';\n+\t\t$$b =~ /(?:\\G|\\n)$indent\\Q$$tag[0]\\E(?:\\n|\\z)/gc;\n+\t\tif (pos($$b) > $start) {\n+\t\t\tmy $body = substr($$b, $start, pos($$b) - $start);\n+\t\t\t$self->{parser}->{heredocs}->{$$tag[0]} = {\n+\t\t\t\tcontent => substr($body, 0, length($body) - length($&)),\n+\t\t\t\tstart_line => $self->{lineno},\n+\t\t        };\n+\t\t\t$self->{lineno} += () = $body =~ /\\n/sg;\n+\t\t\tnext;\n+\t\t}\n+\t\tpush(@{$self->{parser}->{problems}}, ['HEREDOC', $tag]);\n+\t\t$$b =~ /(?:\\G|\\n).*\\z/gc; # consume rest of input\n+\t\tmy $body = substr($$b, $start, pos($$b) - $start);\n+\t\t$self->{lineno} += () = $body =~ /\\n/sg;\n+\t\tlast;\n+\t}\n+}\n+\n+sub scan_token {\n+\tmy $self = shift @_;\n+\tmy $b = $self->{buff};\n+\tmy $token = '';\n+\tmy ($start, $startln);\n+RESTART:\n+\t$startln = $self->{lineno};\n+\t$$b =~ /\\G[ \\t]+/gc; # skip whitespace (but not newline)\n+\t$start = pos($$b) || 0;\n+\t$self->{lineno}++, return [\"\\n\", $start, pos($$b), $startln, $startln] if $$b =~ /\\G#[^\\n]*(?:\\n|\\z)/gc; # comment\n+\twhile (1) {\n+\t\t# slurp up non-special characters\n+\t\t$token .= $1 if $$b =~ /\\G([^\\\\;&|<>(){}'\"\\$\\s]+)/gc;\n+\t\t# handle special characters\n+\t\tlast unless $$b =~ /\\G(.)/sgc;\n+\t\tmy $c = $1;\n+\t\tpos($$b)--, last if $c =~ /^[ \\t]$/; # whitespace ends token\n+\t\tpos($$b)--, last if length($token) && $c =~ /^[;&|<>(){}\\n]$/;\n+\t\t$token .= $self->scan_sqstring(), next if $c eq \"'\";\n+\t\t$token .= $self->scan_dqstring(), next if $c eq '\"';\n+\t\t$token .= $c . $self->scan_dollar(), next if $c eq '$';\n+\t\t$self->{lineno}++, $self->swallow_heredocs(), $token = $c, last if $c eq \"\\n\";\n+\t\t$token = $self->scan_op($c), last if $c =~ /^[;&|<>]$/;\n+\t\t$token = $c, last if $c =~ /^[(){}]$/;\n+\t\tif ($c eq '\\\\') {\n+\t\t\t$token .= '\\\\', last unless $$b =~ /\\G(.)/sgc;\n+\t\t\t$c = $1;\n+\t\t\t$self->{lineno}++, next if $c eq \"\\n\" && length($token); # line splice\n+\t\t\t$self->{lineno}++, goto RESTART if $c eq \"\\n\"; # line splice\n+\t\t\t$token .= '\\\\' . $c;\n+\t\t\tnext;\n+\t\t}\n+\t\tdie(\"internal error scanning character '$c'\\n\");\n+\t}\n+\treturn length($token) ? [$token, $start, pos($$b), $startln, $self->{lineno}] : undef;\n+}\n+\n+# ShellParser parses POSIX shell scripts (with minor extensions for Bash). It\n+# is a recursive descent parser very roughly modeled after section 2.10 \"Shell\n+# Grammar\" of POSIX chapter 2 \"Shell Command Language\".\n+\n+package ShellParser;\n+\n+sub new {\n+\tmy ($class, $s) = @_;\n+\tmy $self = bless {\n+\t\tbuff => [],\n+\t\tstop => [],\n+\t\toutput => [],\n+\t\theredocs => {},\n+\t\tinsubshell => 0,\n+\t} => $class;\n+\t$self->{lexer} = Lexer->new($self, $s);\n+\treturn $self;\n+}\n+\n+sub next_token {\n+\tmy $self = shift @_;\n+\treturn pop(@{$self->{buff}}) if @{$self->{buff}};\n+\treturn $self->{lexer}->scan_token();\n+}\n+\n+sub untoken {\n+\tmy $self = shift @_;\n+\tpush(@{$self->{buff}}, @_);\n+}\n+\n+sub peek {\n+\tmy $self = shift @_;\n+\tmy $token = $self->next_token();\n+\treturn undef unless defined($token);\n+\t$self->untoken($token);\n+\treturn $token;\n+}\n+\n+sub stop_at {\n+\tmy ($self, $token) = @_;\n+\treturn 1 unless defined($token);\n+\tmy $stop = ${$self->{stop}}[-1] if @{$self->{stop}};\n+\treturn defined($stop) && $token->[0] =~ $stop;\n+}\n+\n+sub expect {\n+\tmy ($self, $expect) = @_;\n+\tmy $token = $self->next_token();\n+\treturn $token if defined($token) && $token->[0] eq $expect;\n+\tpush(@{$self->{output}}, \"?!ERR?! expected '$expect' but found '\" . (defined($token) ? $token->[0] : \"<end-of-input>\") . \"'\\n\");\n+\t$self->untoken($token) if defined($token);\n+\treturn ();\n+}\n+\n+sub optional_newlines {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\twhile (my $token = $self->peek()) {\n+\t\tlast unless $token->[0] eq \"\\n\";\n+\t\tpush(@tokens, $self->next_token());\n+\t}\n+\treturn @tokens;\n+}\n+\n+sub parse_group {\n+\tmy $self = shift @_;\n+\treturn ($self->parse(qr/^}$/),\n+\t\t$self->expect('}'));\n+}\n+\n+sub parse_subshell {\n+\tmy $self = shift @_;\n+\t$self->{insubshell}++;\n+\tmy @tokens = ($self->parse(qr/^\\)$/),\n+\t\t      $self->expect(')'));\n+\t$self->{insubshell}--;\n+\treturn @tokens;\n+}\n+\n+sub parse_case_pattern {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\twhile (defined(my $token = $self->next_token())) {\n+\t\tpush(@tokens, $token);\n+\t\tlast if $token->[0] eq ')';\n+\t}\n+\treturn @tokens;\n+}\n+\n+sub parse_case {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\tpush(@tokens,\n+\t     $self->next_token(), # subject\n+\t     $self->optional_newlines(),\n+\t     $self->expect('in'),\n+\t     $self->optional_newlines());\n+\twhile (1) {\n+\t\tmy $token = $self->peek();\n+\t\tlast unless defined($token) && $token->[0] ne 'esac';\n+\t\tpush(@tokens,\n+\t\t     $self->parse_case_pattern(),\n+\t\t     $self->optional_newlines(),\n+\t\t     $self->parse(qr/^(?:;;|esac)$/)); # item body\n+\t\t$token = $self->peek();\n+\t\tlast unless defined($token) && $token->[0] ne 'esac';\n+\t\tpush(@tokens,\n+\t\t     $self->expect(';;'),\n+\t\t     $self->optional_newlines());\n+\t}\n+\tpush(@tokens, $self->expect('esac'));\n+\treturn @tokens;\n+}\n+\n+sub parse_for {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\tpush(@tokens,\n+\t     $self->next_token(), # variable\n+\t     $self->optional_newlines());\n+\tmy $token = $self->peek();\n+\tif (defined($token) && $token->[0] eq 'in') {\n+\t\tpush(@tokens,\n+\t\t     $self->expect('in'),\n+\t\t     $self->optional_newlines());\n+\t}\n+\tpush(@tokens,\n+\t     $self->parse(qr/^do$/), # items\n+\t     $self->expect('do'),\n+\t     $self->optional_newlines(),\n+\t     $self->parse_loop_body(),\n+\t     $self->expect('done'));\n+\treturn @tokens;\n+}\n+\n+sub parse_if {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\twhile (1) {\n+\t\tpush(@tokens,\n+\t\t     $self->parse(qr/^then$/), # if/elif condition\n+\t\t     $self->expect('then'),\n+\t\t     $self->optional_newlines(),\n+\t\t     $self->parse(qr/^(?:elif|else|fi)$/)); # if/elif body\n+\t\tmy $token = $self->peek();\n+\t\tlast unless defined($token) && $token->[0] eq 'elif';\n+\t\tpush(@tokens, $self->expect('elif'));\n+\t}\n+\tmy $token = $self->peek();\n+\tif (defined($token) && $token->[0] eq 'else') {\n+\t\tpush(@tokens,\n+\t\t     $self->expect('else'),\n+\t\t     $self->optional_newlines(),\n+\t\t     $self->parse(qr/^fi$/)); # else body\n+\t}\n+\tpush(@tokens, $self->expect('fi'));\n+\treturn @tokens;\n+}\n+\n+sub parse_loop_body {\n+\tmy $self = shift @_;\n+\treturn $self->parse(qr/^done$/);\n+}\n+\n+sub parse_loop {\n+\tmy $self = shift @_;\n+\treturn ($self->parse(qr/^do$/), # condition\n+\t\t$self->expect('do'),\n+\t\t$self->optional_newlines(),\n+\t\t$self->parse_loop_body(),\n+\t\t$self->expect('done'));\n+}\n+\n+sub parse_func {\n+\tmy $self = shift @_;\n+\treturn ($self->expect('('),\n+\t\t$self->expect(')'),\n+\t\t$self->optional_newlines(),\n+\t\t$self->parse_cmd()); # body\n+}\n+\n+sub parse_bash_array_assignment {\n+\tmy $self = shift @_;\n+\tmy @tokens = $self->expect('(');\n+\twhile (defined(my $token = $self->next_token())) {\n+\t\tpush(@tokens, $token);\n+\t\tlast if $token->[0] eq ')';\n+\t}\n+\treturn @tokens;\n+}\n+\n+my %compound = (\n+\t'{' => \\&parse_group,\n+\t'(' => \\&parse_subshell,\n+\t'case' => \\&parse_case,\n+\t'for' => \\&parse_for,\n+\t'if' => \\&parse_if,\n+\t'until' => \\&parse_loop,\n+\t'while' => \\&parse_loop);\n+\n+sub parse_cmd {\n+\tmy $self = shift @_;\n+\tmy $cmd = $self->next_token();\n+\treturn () unless defined($cmd);\n+\treturn $cmd if $cmd->[0] eq \"\\n\";\n+\n+\tmy $token;\n+\tmy @tokens = $cmd;\n+\tif ($cmd->[0] eq '!') {\n+\t\tpush(@tokens, $self->parse_cmd());\n+\t\treturn @tokens;\n+\t} elsif (my $f = $compound{$cmd->[0]}) {\n+\t\tpush(@tokens, $self->$f());\n+\t} elsif (defined($token = $self->peek()) && $token->[0] eq '(') {\n+\t\tif ($cmd->[0] !~ /\\w=$/) {\n+\t\t\tpush(@tokens, $self->parse_func());\n+\t\t\treturn @tokens;\n+\t\t}\n+\t\tmy @array = $self->parse_bash_array_assignment();\n+\t\t$tokens[-1]->[0] .= join(' ', map {$_->[0]} @array);\n+\t\t$tokens[-1]->[2] = $array[$#array][2] if @array;\n+\t}\n+\n+\twhile (defined(my $token = $self->next_token())) {\n+\t\t$self->untoken($token), last if $self->stop_at($token);\n+\t\tpush(@tokens, $token);\n+\t\tlast if $token->[0] =~ /^(?:[;&\\n|]|&&|\\|\\|)$/;\n+\t}\n+\tpush(@tokens, $self->next_token()) if $tokens[-1]->[0] ne \"\\n\" && defined($token = $self->peek()) && $token->[0] eq \"\\n\";\n+\treturn @tokens;\n+}\n+\n+sub accumulate {\n+\tmy ($self, $tokens, $cmd) = @_;\n+\tpush(@$tokens, @$cmd);\n+}\n+\n+sub parse {\n+\tmy ($self, $stop) = @_;\n+\tpush(@{$self->{stop}}, $stop);\n+\tgoto DONE if $self->stop_at($self->peek());\n+\tmy @tokens;\n+\twhile (my @cmd = $self->parse_cmd()) {\n+\t\t$self->accumulate(\\@tokens, \\@cmd);\n+\t\tlast if $self->stop_at($self->peek());\n+\t}\n+DONE:\n+\tpop(@{$self->{stop}});\n+\treturn @tokens;\n+}\n+\n+# ScriptParser is a subclass of ShellParser which identifies individual test\n+# definitions within test scripts and passes each test body to check_test().\n+# ScriptParser detects test definitions not only at the top-level of test\n+# scripts but also within compound commands such as loops and function\n+# definitions.\n+\n+package ScriptParser;\n+\n+our @ISA = ('ShellParser');\n+\n+sub new {\n+\tmy $class = shift @_;\n+\tmy $self = $class->SUPER::new(@_);\n+\t$self->{ntests} = 0;\n+\t$self->{nerrs} = 0;\n+\treturn $self;\n+}\n+\n+# extract the raw content of a token, which may be a single string or a\n+# composition of multiple strings and non-string character runs; for instance,\n+# `\"test body\"` unwraps to `test body`; `word\"a b\"42'c d'` to `worda b42c d`\n+sub unwrap {\n+\tmy $token = (@_ ? shift @_ : $_)->[0];\n+\t# simple case: 'sqstring' or \"dqstring\"\n+\treturn $token if $token =~ s/^'([^']*)'$/$1/;\n+\treturn $token if $token =~ s/^\"([^\"]*)\"$/$1/;\n+\n+\t# composite case\n+\tmy ($s, $q, $escaped);\n+\twhile (1) {\n+\t\t# slurp up non-special characters\n+\t\t$s .= $1 if $token =~ /\\G([^\\\\'\"]*)/gc;\n+\t\t# handle special characters\n+\t\tlast unless $token =~ /\\G(.)/sgc;\n+\t\tmy $c = $1;\n+\t\t$q = undef, next if defined($q) && $c eq $q;\n+\t\t$q = $c, next if !defined($q) && $c =~ /^['\"]$/;\n+\t\tif ($c eq '\\\\') {\n+\t\t\tlast unless $token =~ /\\G(.)/sgc;\n+\t\t\t$c = $1;\n+\t\t\t$s .= '\\\\' if $c eq \"\\n\"; # preserve line splice\n+\t\t}\n+\t\t$s .= $c;\n+\t}\n+\treturn $s\n+}\n+\n+sub check_test {\n+\t# no-op; subclass and override to implement lint checks\n+}\n+\n+sub parse_cmd {\n+\tmy $self = shift @_;\n+\tmy @tokens = $self->SUPER::parse_cmd();\n+\treturn @tokens unless @tokens && $tokens[0]->[0] =~ /^test_expect_(?:success|failure)$/;\n+\tmy $n = $#tokens;\n+\t$n-- while $n >= 0 && $tokens[$n]->[0] =~ /^(?:[;&\\n|]|&&|\\|\\|)$/;\n+\tmy $herebody;\n+\tif ($n >= 2 && $tokens[$n-1]->[0] eq '-' && $tokens[$n]->[0] =~ /^<<-?(.+)$/) {\n+\t\t$herebody = $self->{heredocs}->{$1};\n+\t\t$n--;\n+\t}\n+\t$self->check_test($tokens[1], $tokens[2], $herebody) if $n == 2; # title body\n+\t$self->check_test($tokens[2], $tokens[3], $herebody) if $n > 2;  # prereq title body\n+\treturn @tokens;\n+}\n+\n+1;\n-- \ngitgitgadget\n\n"},{"id":"547028","messageId":"416c95fee5994b03364b853845e2efe87f4efb54.1783054466.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v3.git.1783054466.gitgitgadget@gmail.com","subject":"[PATCH v3 4/6] t: fix Lexer line count for $() inside double-quoted strings","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-03T04:54:23Z","receivedAt":"2026-07-03T04:54:36Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nscan_dqstring's post-loop newline counter re-counts newlines that\nwere already counted during recursive parsing of $() bodies.  This\nhappens because scan_dollar returns text containing newlines (from\nmulti-line command substitutions), and the catch-all counter at the\nend of scan_dqstring counts all of them again.\n\nFix this by counting newlines inline as non-special characters are\nconsumed, and removing the post-loop catch-all.  Each newline is\nnow counted exactly once: literal newlines at the inline match,\nline splices at the backslash handler, and $() newlines by\nscan_token during the recursive parse.\n\nThis is a latent bug: any consumer that relies on token line\nnumbers rather than byte offsets would get incorrect results for\ntokens following a multi-line $() inside a double-quoted string.\nchainlint is not affected because it annotates the original body\ntext using byte offsets, not token line numbers.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/lib-shell-parser.pl | 9 ++++++---\n 1 file changed, 6 insertions(+), 3 deletions(-)\n\ndiff --git a/t/lib-shell-parser.pl b/t/lib-shell-parser.pl\nindex 5c435c5d05..17fbf461b1 100644\n--- a/t/lib-shell-parser.pl\n+++ b/t/lib-shell-parser.pl\n@@ -93,8 +93,12 @@ sub scan_dqstring {\n \tmy $b = $self->{buff};\n \tmy $s = '\"';\n \twhile (1) {\n-\t\t# slurp up non-special characters\n-\t\t$s .= $1 if $$b =~ /\\G([^\"\\$\\\\]+)/gc;\n+\t\t# Slurp non-special characters; count newlines here because\n+\t\t# newlines inside $() are already counted by the recursive parse.\n+\t\tif ($$b =~ /\\G([^\"\\$\\\\]+)/gc) {\n+\t\t\t$s .= $1;\n+\t\t\t$self->{lineno} += $1 =~ tr/\\n//;\n+\t\t}\n \t\t# handle special characters\n \t\tlast unless $$b =~ /\\G(.)/sgc;\n \t\tmy $c = $1;\n@@ -111,7 +115,6 @@ sub scan_dqstring {\n \t\t}\n \t\tdie(\"internal error scanning dq-string '$c'\\n\");\n \t}\n-\t$self->{lineno} += () = $s =~ /\\n/sg;\n \treturn $s;\n }\n \n-- \ngitgitgadget\n\n"},{"id":"547030","messageId":"3b12a959fd6c448ce624c07a1f533bb7ae346243.1783054466.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v3.git.1783054466.gitgitgadget@gmail.com","subject":"[PATCH v3 6/6] t: add greplint to detect bare grep assertions","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-03T04:54:25Z","receivedAt":"2026-07-03T04:54:42Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nWithout a lint guard, bare grep assertions will creep back into\ntests over time, defeating the previous commit's conversion.\n\nAdd greplint.pl to catch bare 'grep' used as a test assertion\n(where 'test_grep' should be used) and '! test_grep' (where\n'test_grep !' should be used).\n\ngreplint.pl reuses the shared shell parser from lib-shell-parser.pl\nto tokenize test bodies.  The Lexer collapses heredocs, command\nsubstitutions, and quoted strings into single tokens, so 'grep'\nappearing inside these contexts is not flagged.  A flat walk over\nthe token stream tracks command position and pipeline state to\ndistinguish assertion greps from filter greps.\n\nFor double-quoted test bodies, a source-line walk counts\nbackslash-continuation lines that the Lexer consumes without\nemitting into the body text, adjusting the reported line number\naccordingly.\n\nAdd test fixtures in greplint/ (modeled on chainlint/) covering\ndetection of bare grep assertions, correct skipping of filters,\npipelines, redirects, command substitutions, and lint-ok annotations.\n\nWire into the Makefile as:\n  - test-greplint: runs greplint.pl on $(T) $(THELPERS) $(TPERF)\n  - check-greplint: runs greplint.pl on fixtures, diffs against expected\n  - clean-greplint: removes temp dir\n\nAdd eol=lf entries in t/.gitattributes for greplint fixtures,\nmatching chainlint, so that check-greplint passes on Windows\nwhere core.autocrlf would otherwise cause CRLF mismatches\nbetween expected and actual output.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/.gitattributes                              |   2 +\n t/Makefile                                    |  29 ++-\n t/greplint-cat.pl                             |  27 ++\n t/greplint.pl                                 | 241 ++++++++++++++++++\n t/greplint/bare-grep-after-and.expect         |   1 +\n t/greplint/bare-grep-after-and.test           |   4 +\n t/greplint/bare-grep-after-semicolon.expect   |   1 +\n t/greplint/bare-grep-after-semicolon.test     |   4 +\n t/greplint/bare-grep-compound-body.expect     |   3 +\n t/greplint/bare-grep-compound-body.test       |  17 ++\n t/greplint/bare-grep-count-mode.expect        |   1 +\n t/greplint/bare-grep-count-mode.test          |   3 +\n t/greplint/bare-grep-explicit-pattern.expect  |   1 +\n t/greplint/bare-grep-explicit-pattern.test    |   3 +\n t/greplint/bare-grep-flags.expect             |   1 +\n t/greplint/bare-grep-flags.test               |   3 +\n t/greplint/bare-grep-lint-ok.expect           |   0\n t/greplint/bare-grep-lint-ok.test             |   4 +\n t/greplint/bare-grep-negated.expect           |   1 +\n t/greplint/bare-grep-negated.test             |   3 +\n t/greplint/bare-grep-pattern-file.expect      |   1 +\n t/greplint/bare-grep-pattern-file.test        |   3 +\n t/greplint/bare-grep-simple.expect            |   1 +\n t/greplint/bare-grep-simple.test              |   3 +\n t/greplint/bare-grep-subshell.expect          |   1 +\n t/greplint/bare-grep-subshell.test            |   5 +\n .../dqstring-continuation-offset.expect       |   1 +\n t/greplint/dqstring-continuation-offset.test  |  11 +\n t/greplint/filter-command-substitution.expect |   0\n t/greplint/filter-command-substitution.test   |   3 +\n t/greplint/filter-pipe-input.expect           |   0\n t/greplint/filter-pipe-input.test             |   3 +\n t/greplint/filter-pipe-output.expect          |   0\n t/greplint/filter-pipe-output.test            |   3 +\n t/greplint/filter-redirect-output.expect      |   0\n t/greplint/filter-redirect-output.test        |   3 +\n t/greplint/filter-stdin-redirect.expect       |   0\n t/greplint/filter-stdin-redirect.test         |   3 +\n t/greplint/grep-as-argument.expect            |   0\n t/greplint/grep-as-argument.test              |   3 +\n t/greplint/grep-as-value.expect               |   0\n t/greplint/grep-as-value.test                 |   6 +\n t/greplint/wrong-negation.expect              |   1 +\n t/greplint/wrong-negation.test                |   3 +\n 44 files changed, 398 insertions(+), 5 deletions(-)\n create mode 100644 t/greplint-cat.pl\n create mode 100644 t/greplint.pl\n create mode 100644 t/greplint/bare-grep-after-and.expect\n create mode 100644 t/greplint/bare-grep-after-and.test\n create mode 100644 t/greplint/bare-grep-after-semicolon.expect\n create mode 100644 t/greplint/bare-grep-after-semicolon.test\n create mode 100644 t/greplint/bare-grep-compound-body.expect\n create mode 100644 t/greplint/bare-grep-compound-body.test\n create mode 100644 t/greplint/bare-grep-count-mode.expect\n create mode 100644 t/greplint/bare-grep-count-mode.test\n create mode 100644 t/greplint/bare-grep-explicit-pattern.expect\n create mode 100644 t/greplint/bare-grep-explicit-pattern.test\n create mode 100644 t/greplint/bare-grep-flags.expect\n create mode 100644 t/greplint/bare-grep-flags.test\n create mode 100644 t/greplint/bare-grep-lint-ok.expect\n create mode 100644 t/greplint/bare-grep-lint-ok.test\n create mode 100644 t/greplint/bare-grep-negated.expect\n create mode 100644 t/greplint/bare-grep-negated.test\n create mode 100644 t/greplint/bare-grep-pattern-file.expect\n create mode 100644 t/greplint/bare-grep-pattern-file.test\n create mode 100644 t/greplint/bare-grep-simple.expect\n create mode 100644 t/greplint/bare-grep-simple.test\n create mode 100644 t/greplint/bare-grep-subshell.expect\n create mode 100644 t/greplint/bare-grep-subshell.test\n create mode 100644 t/greplint/dqstring-continuation-offset.expect\n create mode 100644 t/greplint/dqstring-continuation-offset.test\n create mode 100644 t/greplint/filter-command-substitution.expect\n create mode 100644 t/greplint/filter-command-substitution.test\n create mode 100644 t/greplint/filter-pipe-input.expect\n create mode 100644 t/greplint/filter-pipe-input.test\n create mode 100644 t/greplint/filter-pipe-output.expect\n create mode 100644 t/greplint/filter-pipe-output.test\n create mode 100644 t/greplint/filter-redirect-output.expect\n create mode 100644 t/greplint/filter-redirect-output.test\n create mode 100644 t/greplint/filter-stdin-redirect.expect\n create mode 100644 t/greplint/filter-stdin-redirect.test\n create mode 100644 t/greplint/grep-as-argument.expect\n create mode 100644 t/greplint/grep-as-argument.test\n create mode 100644 t/greplint/grep-as-value.expect\n create mode 100644 t/greplint/grep-as-value.test\n create mode 100644 t/greplint/wrong-negation.expect\n create mode 100644 t/greplint/wrong-negation.test\n\ndiff --git a/t/.gitattributes b/t/.gitattributes\nindex 7664c6e027..e867f38c71 100644\n--- a/t/.gitattributes\n+++ b/t/.gitattributes\n@@ -1,5 +1,7 @@\n t[0-9][0-9][0-9][0-9]/* -whitespace\n /chainlint/*.expect eol=lf -whitespace\n+/greplint/*.expect eol=lf -whitespace\n+/greplint/*.test eol=lf -whitespace\n /t0110/url-* binary\n /t3206/* eol=lf\n /t3900/*.txt eol=lf\ndiff --git a/t/Makefile b/t/Makefile\nindex ab8a5b54aa..e3249879c4 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -27,9 +27,11 @@ TEST_LINT ?= test-lint\n ifdef TEST_OUTPUT_DIRECTORY\n TEST_RESULTS_DIRECTORY = $(TEST_OUTPUT_DIRECTORY)/test-results\n CHAINLINTTMP = $(TEST_OUTPUT_DIRECTORY)/chainlinttmp\n+GREPLINTTMP = $(TEST_OUTPUT_DIRECTORY)/greplinttmp\n else\n TEST_RESULTS_DIRECTORY = test-results\n CHAINLINTTMP = chainlinttmp\n+GREPLINTTMP = greplinttmp\n endif\n \n # Shell quote;\n@@ -38,6 +40,7 @@ TEST_SHELL_PATH_SQ = $(subst ','\\'',$(TEST_SHELL_PATH))\n PERL_PATH_SQ = $(subst ','\\'',$(PERL_PATH))\n TEST_RESULTS_DIRECTORY_SQ = $(subst ','\\'',$(TEST_RESULTS_DIRECTORY))\n CHAINLINTTMP_SQ = $(subst ','\\'',$(CHAINLINTTMP))\n+GREPLINTTMP_SQ = $(subst ','\\'',$(GREPLINTTMP))\n \n T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh))\n THELPERS = $(sort $(filter-out $(T),$(wildcard *.sh)))\n@@ -45,6 +48,7 @@ TLIBS = $(sort $(wildcard lib-*.sh)) annotate-tests.sh\n TPERF = $(sort $(wildcard perf/p[0-9][0-9][0-9][0-9]-*.sh))\n TINTEROP = $(sort $(wildcard interop/i[0-9][0-9][0-9][0-9]-*.sh))\n CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.test)))\n+GREPLINTTESTS = $(sort $(patsubst greplint/%.test,%,$(wildcard greplint/*.test)))\n CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl\n UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)\n UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))\n@@ -63,8 +67,8 @@ test: pre-clean check-meson $(TEST_LINT)\n \t$(CHAINLINTSUPPRESS) $(MAKE) aggregate-results-and-cleanup\n \n ifneq ($(PERL_PATH),)\n-test: check-chainlint\n-prove: check-chainlint\n+test: check-chainlint check-greplint\n+prove: check-chainlint check-greplint\n endif\n \n failed:\n@@ -102,7 +106,7 @@ unit-tests-test-tool:\n pre-clean:\n \t$(RM) -r '$(TEST_RESULTS_DIRECTORY_SQ)'\n \n-clean-except-prove-cache: clean-chainlint\n+clean-except-prove-cache: clean-chainlint clean-greplint\n \t$(RM) -r 'trash directory'.*\n \t$(RM) -r valgrind/bin\n \n@@ -120,6 +124,17 @@ check-chainlint:\n \t{ $(CHAINLINT) --emit-all '$(CHAINLINTTMP_SQ)'/tests >'$(CHAINLINTTMP_SQ)'/actual || true; } && \\\n \tdiff -u '$(CHAINLINTTMP_SQ)'/expect '$(CHAINLINTTMP_SQ)'/actual\n \n+clean-greplint:\n+\t$(RM) -r '$(GREPLINTTMP_SQ)'\n+\n+check-greplint:\n+\t@mkdir -p '$(GREPLINTTMP_SQ)' && \\\n+\t'$(PERL_PATH_SQ)' greplint-cat.pl '$(GREPLINTTMP_SQ)' $(GREPLINTTESTS) && \\\n+\t{ '$(PERL_PATH_SQ)' greplint.pl \\\n+\t\t$(patsubst %,greplint/%.test,$(GREPLINTTESTS)) \\\n+\t\t>'$(GREPLINTTMP_SQ)'/actual 2>&1 || true; } && \\\n+\tdiff -u '$(GREPLINTTMP_SQ)'/expect '$(GREPLINTTMP_SQ)'/actual\n+\n check-meson:\n \t@# awk acts up when trying to match single quotes, so we use \\047 instead.\n \t@mkdir -p mesontmp && \\\n@@ -139,7 +154,7 @@ check-meson:\n test-lint: test-lint-duplicates test-lint-executable \\\n \ttest-lint-filenames\n ifneq ($(PERL_PATH),)\n-test-lint: test-lint-shell-syntax\n+test-lint: test-lint-shell-syntax test-greplint\n else\n GIT_TEST_CHAIN_LINT = 0\n endif\n@@ -160,6 +175,9 @@ test-lint-executable:\n test-lint-shell-syntax:\n \t@'$(PERL_PATH_SQ)' check-non-portable-shell.pl $(T) $(THELPERS) $(TPERF)\n \n+test-greplint:\n+\t@'$(PERL_PATH_SQ)' greplint.pl $(T) $(THELPERS) $(TPERF)\n+\n test-lint-filenames:\n \t@# We do *not* pass a glob to ls-files but use grep instead, to catch\n \t@# non-ASCII characters (which are quoted within double-quotes)\n@@ -185,7 +203,8 @@ perf:\n \t$(MAKE) -C perf/ all\n \n .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \\\n-\tcheck-chainlint clean-chainlint test-chainlint $(UNIT_TESTS)\n+\tcheck-chainlint clean-chainlint test-chainlint \\\n+\tcheck-greplint clean-greplint test-greplint $(UNIT_TESTS)\n \n .PHONY: libgit-sys-test libgit-rs-test\n libgit-sys-test:\ndiff --git a/t/greplint-cat.pl b/t/greplint-cat.pl\nnew file mode 100644\nindex 0000000000..fa9c3b8aab\n--- /dev/null\n+++ b/t/greplint-cat.pl\n@@ -0,0 +1,27 @@\n+#!/usr/bin/env perl\n+\n+use strict;\n+use warnings;\n+\n+# Assemble expected output for check-greplint target.\n+# Usage: greplint-cat.pl <outdir> <test-name> ...\n+#\n+# For each <test-name>, reads greplint/<test-name>.expect and\n+# prepends \"greplint/<test-name>.test:\" to every non-empty line,\n+# matching the output format of greplint.pl.  Writes combined\n+# expected output to <outdir>/expect.\n+\n+my $outdir = shift;\n+open(my $expect, '>', \"$outdir/expect\")\n+\tor die \"unable to open $outdir/expect: $!\";\n+\n+for my $name (@ARGV) {\n+\topen(my $fh, '<', \"greplint/$name.expect\")\n+\t\tor die \"unable to open greplint/$name.expect: $!\";\n+\twhile (<$fh>) {\n+\t\tprint $expect \"greplint/$name.test:$_\";\n+\t}\n+\tclose $fh;\n+}\n+\n+close $expect;\ndiff --git a/t/greplint.pl b/t/greplint.pl\nnew file mode 100644\nindex 0000000000..553bdbdf9a\n--- /dev/null\n+++ b/t/greplint.pl\n@@ -0,0 +1,241 @@\n+#!/usr/bin/env perl\n+\n+# Detect bare 'grep' used as a test assertion where 'test_grep'\n+# should be used, and '! test_grep' where 'test_grep !' should\n+# be used.\n+#\n+# The shared shell parser tokenizes test bodies so that 'grep'\n+# inside heredocs, command substitutions like $(grep ...), and\n+# quoted strings is collapsed into a single token and never seen\n+# by our check.  A line-oriented approach would need to track\n+# heredoc delimiters, nested $() depth, and cross-line pipe\n+# state to avoid false positives on patterns like:\n+#\n+#   write_script foo.sh <<-\\EOF\n+#   grep pattern file    # data, not an assertion\n+#   EOF\n+#\n+# The Lexer already handles these.\n+\n+use warnings;\n+use strict;\n+use File::Basename;\n+do(dirname($0) . \"/lib-shell-parser.pl\")\n+\tor die \"$0: failed to load lib-shell-parser.pl: $@$!\\n\";\n+\n+my $exit_code = 0;\n+\n+# GrepLintParser inherits ScriptParser's ability to find\n+# test_expect_success/failure blocks and call check_test()\n+# on each body.  We override check_test() to walk the token\n+# stream looking for bare grep assertions.\n+package GrepLintParser;\n+\n+our @ISA = ('ScriptParser');\n+\n+# After these tokens, the next token is a command word.\n+# For example, in 'echo foo && grep bar file', the 'grep'\n+# after '&&' is at command position and should be flagged.\n+my %cmd_start = map { $_ => 1 } qw(&& || ; ;; do then else elif), \"\\n\", '{', '(';\n+\n+# Tokens indicating grep's output is piped or redirected.\n+my %filter_op = map { $_ => 1 } qw(| > >> <);\n+\n+# A token is at \"command word\" position if the shell would\n+# interpret it as a program name rather than an argument.\n+# Only 'grep' at command position is an assertion we should\n+# flag; 'grep' as an argument ('test_must_fail grep') or\n+# value ('for cmd in grep sed') is not.\n+sub is_command_word {\n+\tmy ($tokens, $pos) = @_;\n+\treturn 1 if $pos == 0;\n+\tfor (my $j = $pos - 1; $j >= 0; $j--) {\n+\t\tmy $t = $tokens->[$j]->[0];\n+\t\t# After a separator or pipe, a new command starts.\n+\t\treturn 1 if $cmd_start{$t} || $t eq '|';\n+\t\t# After '}' or ')', what follows is a separator or\n+\t\t# redirect on the compound command, not a new command.\n+\t\treturn 0 if $t eq '}' || $t eq ')';\n+\t\t# '!' is a prefix that does not consume command\n+\t\t# position; keep scanning to find what precedes it.\n+\t\tnext if $t eq '!';\n+\t\t# Any other word means we are past the command word.\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n+# Some bare greps are intentional (e.g. file may not exist,\n+# data filter).  A '# lint-ok' annotation on the source line\n+# suppresses the warning.\n+sub lint_ok {\n+\tmy ($raw_lines, $ln) = @_;\n+\tif ($ln < 1 || $ln > @$raw_lines) {\n+\t\twarn \"lint_ok: line number $ln out of range (1..\" .\n+\t\t    scalar(@$raw_lines) . \")\\n\";\n+\t\treturn 0;\n+\t}\n+\treturn $raw_lines->[$ln - 1] =~ /lint-ok/;\n+}\n+\n+# Grep is a filter (not an assertion) if it receives piped\n+# input or sends its output to a pipe or redirect.  Check\n+# both directions from grep's position in the token stream.\n+sub is_filter {\n+\tmy ($tokens, $pos) = @_;\n+\t# Backward: is grep receiving piped input?\n+\t# Newlines don't break pipes ('cmd |\\n grep' is one\n+\t# pipeline), so skip past them.\n+\tfor (my $j = $pos - 1; $j >= 0; $j--) {\n+\t\tmy $t = $tokens->[$j]->[0];\n+\t\treturn 1 if $t eq '|';\n+\t\tnext if $t eq \"\\n\";\n+\t\tlast if $cmd_start{$t} || $t eq '}' || $t eq ')';\n+\t}\n+\t# Forward: is grep piping or redirecting output?\n+\t# Unlike the backward scan, we do not skip newlines here:\n+\t# a bare newline is a command boundary, and redirects or\n+\t# pipes must appear on the same line as grep (or after a\n+\t# line continuation, which the Lexer consumes).\n+\tfor (my $j = $pos + 1; $j < @$tokens; $j++) {\n+\t\tmy $t = $tokens->[$j]->[0];\n+\t\treturn 0 if $cmd_start{$t};\n+\t\treturn 1 if $filter_op{$t};\n+\t}\n+\treturn 0;\n+}\n+\n+# Map a body-relative line number to a file line number.\n+# For double-quoted bodies, backslash-continuation lines\n+# (\\<newline>) are consumed by the Lexer without appearing\n+# in the body text, so the inner parser sees fewer lines\n+# than the source file has.  We walk the source lines to\n+# count continuations and adjust accordingly.\n+sub body_to_file_line {\n+\tmy ($body_lineno, $body_token, $raw_lines, $body_start) = @_;\n+\tmy $body_text = $body_token->[0];\n+\tmy $body_end_line = $body_token->[4];\n+\tunless ($body_start && $body_start >= 1) {\n+\t\twarn \"body_start is not a positive integer\\n\";\n+\t\treturn $body_lineno;\n+\t}\n+\tmy $file_lineno = $body_lineno + $body_start - 1;\n+\t# Only double-quoted bodies have line splices.\n+\treturn $file_lineno unless $body_text =~ /^\"/;\n+\tmy $adj = 0;\n+\tmy $lines_seen = 0;\n+\tunless ($body_end_line && $body_end_line >= $body_start) {\n+\t\twarn \"body_end_line is not set for double-quoted body\\n\";\n+\t\treturn $file_lineno;\n+\t}\n+\tmy $end = $body_end_line;\n+\tif ($end > @$raw_lines) {\n+\t\twarn \"body_end_line ($end) exceeds file length (\" .\n+\t\t    scalar(@$raw_lines) . \")\\n\";\n+\t\treturn $file_lineno;\n+\t}\n+\tmy $src_ln = $body_start;\n+\twhile ($src_ln <= $end && $lines_seen < $body_lineno) {\n+\t\tmy $line = $raw_lines->[$src_ln - 1];\n+\t\t# Odd trailing backslashes = continuation (\\<nl>).\n+\t\t# Even = escaped backslashes (\\\\), not a continuation.\n+\t\tif ($line =~ /(\\\\*)$/ && length($1) % 2 == 1) {\n+\t\t\t$adj++;\n+\t\t} else {\n+\t\t\t$lines_seen++;\n+\t\t}\n+\t\t$src_ln++;\n+\t}\n+\tif ($lines_seen < $body_lineno) {\n+\t\twarn \"body_lineno ($body_lineno) not found within body range \" .\n+\t\t    \"($body_start..$end)\\n\";\n+\t}\n+\treturn $file_lineno + $adj;\n+}\n+\n+# ScriptParser calls this for each test body found in the script.\n+sub check_test {\n+\tmy $self = shift @_;\n+\tmy $title = ScriptParser::unwrap(shift @_);\n+\tmy $body_token = shift @_;\n+\tmy $body_start = $body_token->[3];\n+\tmy $body = ScriptParser::unwrap($body_token);\n+\t# Handle heredoc-style test bodies:\n+\t#   test_expect_success 'title' - <<\\EOF\n+\t#   grep pattern file\n+\t#   EOF\n+\t# The '-' signals that the body follows as a heredoc.\n+\tif ($body eq '-') {\n+\t\tmy $herebody = shift @_;\n+\t\tif ($herebody) {\n+\t\t\t$body = $herebody->{content};\n+\t\t\t$body_start = $herebody->{start_line};\n+\t\t}\n+\t}\n+\treturn unless $body;\n+\n+\tmy $raw_lines = $self->{raw_lines};\n+\n+\t# The outer parser gives us the body as an opaque string.\n+\t# Parse it to get individual tokens with command boundaries.\n+\tmy $parser = ShellParser->new(\\$body);\n+\tmy @tokens = $parser->parse();\n+\n+\tmy $file = $self->{file};\n+\n+\tfor (my $i = 0; $i < @tokens; $i++) {\n+\t\tmy $text = $tokens[$i]->[0];\n+\t\tnext unless is_command_word(\\@tokens, $i);\n+\n+\t\tmy $token_lineno = $tokens[$i]->[3];\n+\t\tunless (defined($token_lineno) && $token_lineno >= 1) {\n+\t\t\twarn \"token has no line number\\n\";\n+\t\t\tnext;\n+\t\t}\n+\t\tmy $file_lineno = body_to_file_line(\n+\t\t\t$token_lineno,\n+\t\t\t$body_token, $raw_lines, $body_start);\n+\n+\t\t# '!' negates the exit code without consuming command\n+\t\t# position.  '! test_grep' is an anti-pattern because\n+\t\t# test_grep only prints diagnostics on grep failure,\n+\t\t# and '!' inverts after that decision is already made.\n+\t\tif ($text eq '!') {\n+\t\t\tif ($i + 1 < @tokens &&\n+\t\t\t    $tokens[$i + 1]->[0] eq 'test_grep' &&\n+\t\t\t    !lint_ok($raw_lines, $file_lineno)) {\n+\t\t\t\tprint \"$file:$file_lineno: error: \",\n+\t\t\t\t    'use \"test_grep !\" instead of ',\n+\t\t\t\t    '\"! test_grep\"', \"\\n\";\n+\t\t\t\t$exit_code = 1;\n+\t\t\t}\n+\t\t\tnext;\n+\t\t}\n+\n+\t\t# Bare grep as a command (not a filter) is a test\n+\t\t# assertion that should use test_grep for better\n+\t\t# failure diagnostics.\n+\t\tif ($text eq 'grep' &&\n+\t\t    !is_filter(\\@tokens, $i) &&\n+\t\t    !lint_ok($raw_lines, $file_lineno)) {\n+\t\t\tprint \"$file:$file_lineno: error: \",\n+\t\t\t    \"bare grep outside pipeline \",\n+\t\t\t    \"(use test_grep)\\n\";\n+\t\t\t$exit_code = 1;\n+\t\t}\n+\t}\n+}\n+\n+package main;\n+\n+for my $file (@ARGV) {\n+\topen(my $fh, '<:unix:crlf', $file) or die \"$0: $file: $!\\n\";\n+\tmy @raw_lines = <$fh>;\n+\tclose $fh;\n+\tmy $s = join('', @raw_lines);\n+\tmy $parser = GrepLintParser->new(\\$s);\n+\t$parser->{file} = $file;\n+\t$parser->{raw_lines} = \\@raw_lines;\n+\t$parser->parse();\n+}\n+exit $exit_code;\ndiff --git a/t/greplint/bare-grep-after-and.expect b/t/greplint/bare-grep-after-and.expect\nnew file mode 100644\nindex 0000000000..7da1b21aa8\n--- /dev/null\n+++ b/t/greplint/bare-grep-after-and.expect\n@@ -0,0 +1 @@\n+3: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-after-and.test b/t/greplint/bare-grep-after-and.test\nnew file mode 100644\nindex 0000000000..c5b26e4572\n--- /dev/null\n+++ b/t/greplint/bare-grep-after-and.test\n@@ -0,0 +1,4 @@\n+test_expect_success 'grep after && is flagged' '\n+\tcmd &&\n+\tgrep pattern file\n+'\ndiff --git a/t/greplint/bare-grep-after-semicolon.expect b/t/greplint/bare-grep-after-semicolon.expect\nnew file mode 100644\nindex 0000000000..7da1b21aa8\n--- /dev/null\n+++ b/t/greplint/bare-grep-after-semicolon.expect\n@@ -0,0 +1 @@\n+3: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-after-semicolon.test b/t/greplint/bare-grep-after-semicolon.test\nnew file mode 100644\nindex 0000000000..c1e468ddf8\n--- /dev/null\n+++ b/t/greplint/bare-grep-after-semicolon.test\n@@ -0,0 +1,4 @@\n+test_expect_success 'grep after semicolon is flagged' '\n+\techo hello;\n+\tgrep pattern file\n+'\ndiff --git a/t/greplint/bare-grep-compound-body.expect b/t/greplint/bare-grep-compound-body.expect\nnew file mode 100644\nindex 0000000000..eb6210f0c9\n--- /dev/null\n+++ b/t/greplint/bare-grep-compound-body.expect\n@@ -0,0 +1,3 @@\n+4: error: bare grep outside pipeline (use test_grep)\n+8: error: bare grep outside pipeline (use test_grep)\n+15: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-compound-body.test b/t/greplint/bare-grep-compound-body.test\nnew file mode 100644\nindex 0000000000..72472edcf6\n--- /dev/null\n+++ b/t/greplint/bare-grep-compound-body.test\n@@ -0,0 +1,17 @@\n+test_expect_success 'grep after then/do/else is flagged' '\n+\tif true\n+\tthen\n+\t\tgrep pattern file\n+\tfi &&\n+\twhile true\n+\tdo\n+\t\tgrep pattern file &&\n+\t\tbreak\n+\tdone &&\n+\tif true\n+\tthen\n+\t\techo yes\n+\telse\n+\t\tgrep pattern file\n+\tfi\n+'\ndiff --git a/t/greplint/bare-grep-count-mode.expect b/t/greplint/bare-grep-count-mode.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-count-mode.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-count-mode.test b/t/greplint/bare-grep-count-mode.test\nnew file mode 100644\nindex 0000000000..b9c0f18783\n--- /dev/null\n+++ b/t/greplint/bare-grep-count-mode.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep -c is flagged (not special-cased)' '\n+\tgrep -c pattern file\n+'\ndiff --git a/t/greplint/bare-grep-explicit-pattern.expect b/t/greplint/bare-grep-explicit-pattern.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-explicit-pattern.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-explicit-pattern.test b/t/greplint/bare-grep-explicit-pattern.test\nnew file mode 100644\nindex 0000000000..69d8651019\n--- /dev/null\n+++ b/t/greplint/bare-grep-explicit-pattern.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep -e is flagged' '\n+\tgrep -e pattern file\n+'\ndiff --git a/t/greplint/bare-grep-flags.expect b/t/greplint/bare-grep-flags.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-flags.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-flags.test b/t/greplint/bare-grep-flags.test\nnew file mode 100644\nindex 0000000000..9ca0e10233\n--- /dev/null\n+++ b/t/greplint/bare-grep-flags.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep -E is flagged' '\n+\tgrep -E \"pat+ern\" file\n+'\ndiff --git a/t/greplint/bare-grep-lint-ok.expect b/t/greplint/bare-grep-lint-ok.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/bare-grep-lint-ok.test b/t/greplint/bare-grep-lint-ok.test\nnew file mode 100644\nindex 0000000000..335b902a42\n--- /dev/null\n+++ b/t/greplint/bare-grep-lint-ok.test\n@@ -0,0 +1,4 @@\n+test_expect_success 'grep with lint-ok annotation is not flagged' '\n+\tgrep pattern file && # lint-ok\n+\techo done\n+'\ndiff --git a/t/greplint/bare-grep-negated.expect b/t/greplint/bare-grep-negated.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-negated.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-negated.test b/t/greplint/bare-grep-negated.test\nnew file mode 100644\nindex 0000000000..ef789bc8b7\n--- /dev/null\n+++ b/t/greplint/bare-grep-negated.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'negated grep is flagged' '\n+\t! grep pattern file\n+'\ndiff --git a/t/greplint/bare-grep-pattern-file.expect b/t/greplint/bare-grep-pattern-file.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-pattern-file.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-pattern-file.test b/t/greplint/bare-grep-pattern-file.test\nnew file mode 100644\nindex 0000000000..d70035e449\n--- /dev/null\n+++ b/t/greplint/bare-grep-pattern-file.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep -f is flagged' '\n+\tgrep -f patterns.txt file\n+'\ndiff --git a/t/greplint/bare-grep-simple.expect b/t/greplint/bare-grep-simple.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-simple.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-simple.test b/t/greplint/bare-grep-simple.test\nnew file mode 100644\nindex 0000000000..3a8d5f9bbd\n--- /dev/null\n+++ b/t/greplint/bare-grep-simple.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'bare grep is flagged' '\n+\tgrep pattern file\n+'\ndiff --git a/t/greplint/bare-grep-subshell.expect b/t/greplint/bare-grep-subshell.expect\nnew file mode 100644\nindex 0000000000..7da1b21aa8\n--- /dev/null\n+++ b/t/greplint/bare-grep-subshell.expect\n@@ -0,0 +1 @@\n+3: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-subshell.test b/t/greplint/bare-grep-subshell.test\nnew file mode 100644\nindex 0000000000..1fab2d3363\n--- /dev/null\n+++ b/t/greplint/bare-grep-subshell.test\n@@ -0,0 +1,5 @@\n+test_expect_success 'grep in subshell is flagged' '\n+\t(\n+\t\tgrep pattern file\n+\t)\n+'\ndiff --git a/t/greplint/dqstring-continuation-offset.expect b/t/greplint/dqstring-continuation-offset.expect\nnew file mode 100644\nindex 0000000000..8185bfdca4\n--- /dev/null\n+++ b/t/greplint/dqstring-continuation-offset.expect\n@@ -0,0 +1 @@\n+10: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/dqstring-continuation-offset.test b/t/greplint/dqstring-continuation-offset.test\nnew file mode 100644\nindex 0000000000..b332bd8f3b\n--- /dev/null\n+++ b/t/greplint/dqstring-continuation-offset.test\n@@ -0,0 +1,11 @@\n+# Double-quoted test bodies with backslash-continuation lines:\n+# the splice adjustment in check_test compensates for \\<newline>\n+# lines that the lexer consumes without emitting into the body\n+# text, so the reported line number matches the source.\n+test_expect_success 'dqstring continuation offset' \"\n+\tx=\\$(echo \\\n+\t\thello) &&\n+\ty=\\$(echo \\\n+\t\tworld) &&\n+\tgrep pattern file\n+\"\ndiff --git a/t/greplint/filter-command-substitution.expect b/t/greplint/filter-command-substitution.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/filter-command-substitution.test b/t/greplint/filter-command-substitution.test\nnew file mode 100644\nindex 0000000000..bd9a100a5d\n--- /dev/null\n+++ b/t/greplint/filter-command-substitution.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep in command substitution is not flagged' '\n+\tx=$(grep pattern file)\n+'\ndiff --git a/t/greplint/filter-pipe-input.expect b/t/greplint/filter-pipe-input.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/filter-pipe-input.test b/t/greplint/filter-pipe-input.test\nnew file mode 100644\nindex 0000000000..cb5c0d9e23\n--- /dev/null\n+++ b/t/greplint/filter-pipe-input.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep receiving pipe input is not flagged' '\n+\tcmd | grep pattern\n+'\ndiff --git a/t/greplint/filter-pipe-output.expect b/t/greplint/filter-pipe-output.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/filter-pipe-output.test b/t/greplint/filter-pipe-output.test\nnew file mode 100644\nindex 0000000000..01340086d7\n--- /dev/null\n+++ b/t/greplint/filter-pipe-output.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep piping to another command is not flagged' '\n+\tgrep pattern file | wc -l\n+'\ndiff --git a/t/greplint/filter-redirect-output.expect b/t/greplint/filter-redirect-output.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/filter-redirect-output.test b/t/greplint/filter-redirect-output.test\nnew file mode 100644\nindex 0000000000..ee70ba7cde\n--- /dev/null\n+++ b/t/greplint/filter-redirect-output.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep with output redirect is not flagged' '\n+\tgrep pattern file >output\n+'\ndiff --git a/t/greplint/filter-stdin-redirect.expect b/t/greplint/filter-stdin-redirect.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/filter-stdin-redirect.test b/t/greplint/filter-stdin-redirect.test\nnew file mode 100644\nindex 0000000000..292db7766e\n--- /dev/null\n+++ b/t/greplint/filter-stdin-redirect.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep reading from stdin redirect is not flagged' '\n+\tgrep pattern <input\n+'\ndiff --git a/t/greplint/grep-as-argument.expect b/t/greplint/grep-as-argument.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/grep-as-argument.test b/t/greplint/grep-as-argument.test\nnew file mode 100644\nindex 0000000000..7157228b60\n--- /dev/null\n+++ b/t/greplint/grep-as-argument.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep as argument to another command is not flagged' '\n+\ttest_must_fail grep pattern file\n+'\ndiff --git a/t/greplint/grep-as-value.expect b/t/greplint/grep-as-value.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/grep-as-value.test b/t/greplint/grep-as-value.test\nnew file mode 100644\nindex 0000000000..92926f6378\n--- /dev/null\n+++ b/t/greplint/grep-as-value.test\n@@ -0,0 +1,6 @@\n+test_expect_success 'grep as value in for-loop is not flagged' '\n+\tfor cmd in grep sed awk\n+\tdo\n+\t\techo $cmd\n+\tdone\n+'\ndiff --git a/t/greplint/wrong-negation.expect b/t/greplint/wrong-negation.expect\nnew file mode 100644\nindex 0000000000..7dc52f65a8\n--- /dev/null\n+++ b/t/greplint/wrong-negation.expect\n@@ -0,0 +1 @@\n+2: error: use \"test_grep !\" instead of \"! test_grep\"\ndiff --git a/t/greplint/wrong-negation.test b/t/greplint/wrong-negation.test\nnew file mode 100644\nindex 0000000000..542fbd9b28\n--- /dev/null\n+++ b/t/greplint/wrong-negation.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'wrong negation of test_grep is flagged' '\n+\t! test_grep pattern file\n+'\n-- \ngitgitgadget\n"},{"id":"547031","messageId":"751844573398cf21eea3af85f63c7ed2900b31e8.1783054466.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v3.git.1783054466.gitgitgadget@gmail.com","subject":"[PATCH v3 5/6] t: convert grep assertions to test_grep","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-03T04:54:24Z","receivedAt":"2026-07-03T04:54:42Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nReplace bare grep with test_grep in test assertions across the\nsuite, including sourced test helpers (lib-*.sh, *-tests.sh).\ntest_grep prints the contents of the file being searched on\nfailure, making debugging easier than a bare grep which fails\nsilently.\n\nOnly assertion-style greps are converted: grep used as a filter\nin pipelines, command substitutions, conditionals, or with\nredirected I/O is left as-is with a \"# lint-ok\" annotation.\nExisting '! test_grep' calls are rewritten to 'test_grep !' so\nthat the diagnostic output is preserved on failure.\n\ntest_grep requires the file it reads to exist, so '! grep'\nassertions that inspect a file whose presence is conditional need\ncare.  In t5537 the '.git/shallow' file is still present after the\nrepack (the client remains shallow), so the assertion is\nconverted like any other.  In t1400 the '.git/packed-refs' file\nexists only with the files backend, so its check is guarded with a\nREFFILES prerequisite; the backend-agnostic 'git show-ref' check\nthat follows still runs under every backend.  In t7450 'git~2' is\nthe NTFS 8.3 short name of a decoy '..git' file and only exists\nwhen 8.3 short-name generation is enabled; that '! grep' tolerates\nthe missing file on purpose, so it is left as-is with a\n\"# lint-ok\" annotation rather than converted (a plain test_grep\nwould BUG when the short name is absent).\n\nThe conversion was generated using a grep-assertion linter\n(greplint.pl, added in the following commit) to identify bare\ngrep calls at command position.  To reproduce, from the t/\ndirectory:\n\n    # Step 1: annotate the two data-filter greps (grep produces\n    # data, not a verdict) so the linter skips them.\n    sed -i '/grep -vf before commits\\.raw/s/$/ # lint-ok: data filter/' \\\n        t5326-multi-pack-bitmaps.sh\n    sed -i '/grep -E \"^\\[0-9a-f\\].*|| :/s/$/ # lint-ok: data filter/' \\\n        t5702-protocol-v2.sh\n\n    # Step 1b: two '! grep' assertions need more than a mechanical\n    # conversion; handle them by hand before the linter-driven steps\n    # below so it leaves them alone.\n    #\n    # t1400: '.git/packed-refs' is absent under reftable, so guard the\n    # check with REFFILES (a plain test_grep would BUG on the missing\n    # file):\n    #\n    #      git update-ref -d HEAD $B &&\n    #  -   ! grep \"$m\" .git/packed-refs &&\n    #  +   if test_have_prereq REFFILES\n    #  +   then\n    #  +           test_grep ! \"$m\" .git/packed-refs\n    #  +   fi &&\n    #      test_must_fail git show-ref --verify -q $m\n    #\n    # t7450: git~2 is an NTFS 8.3 short name that exists only when\n    # short-name generation is enabled, so this stays a missing-file-\n    # tolerant '! grep'; add a comment plus \"# lint-ok\" so the linter\n    # skips it.\n\n    # Step 2: reorder pre-existing '! test_grep' to 'test_grep !'\n    # (must come before steps 3-4 so greplint does not see them)\n    sed -i 's/! test_grep/test_grep !/' t0031-lockfile-pid.sh\n    sed -i 's/! test_grep/test_grep !/' t5300-pack-object.sh\n    sed -i 's/! test_grep/test_grep !/' t5319-multi-pack-index.sh\n\n    # Step 3: convert '! grep' -> 'test_grep !'\n    perl greplint.pl *.sh 2>&1 | cut -d: -f1,2 |\n    while IFS=: read f l; do\n        sed -i \"${l}s/! *grep/test_grep !/\" \"$f\"\n    done\n\n    # Step 4: convert remaining 'grep' -> 'test_grep'\n    perl greplint.pl *.sh 2>&1 | cut -d: -f1,2 |\n    while IFS=: read f l; do\n        sed -i \"${l}s/grep/test_grep/\" \"$f\"\n    done\n\nTo verify, run: make -C t test-greplint\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/for-each-ref-tests.sh                     |  12 +-\n t/lib-bitmap.sh                             |  12 +-\n t/lib-bundle-uri-protocol.sh                |  26 +-\n t/lib-httpd.sh                              |   2 +-\n t/pack-refs-tests.sh                        |   2 +-\n t/show-ref-exists-tests.sh                  |   2 +-\n t/t0000-basic.sh                            |  16 +-\n t/t0001-init.sh                             |  18 +-\n t/t0008-ignores.sh                          |   8 +-\n t/t0009-git-dir-validation.sh               |   6 +-\n t/t0012-help.sh                             |   4 +-\n t/t0013-sha1dc.sh                           |   2 +-\n t/t0017-env-helper.sh                       |   4 +-\n t/t0021-conversion.sh                       |  18 +-\n t/t0029-core-unsetenvvars.sh                |   4 +-\n t/t0030-stripspace.sh                       |   4 +-\n t/t0031-lockfile-pid.sh                     |   2 +-\n t/t0040-parse-options.sh                    |  52 +--\n t/t0041-usage.sh                            |   2 +-\n t/t0052-simple-ipc.sh                       |  10 +-\n t/t0061-run-command.sh                      |   2 +-\n t/t0066-dir-iterator.sh                     |   2 +-\n t/t0068-for-each-repo.sh                    |  16 +-\n t/t0070-fundamental.sh                      |   6 +-\n t/t0081-find-pack.sh                        |  12 +-\n t/t0091-bugreport.sh                        |  18 +-\n t/t0092-diagnose.sh                         |  12 +-\n t/t0100-previous.sh                         |   2 +-\n t/t0200-gettext-basic.sh                    |  14 +-\n t/t0203-gettext-setlocale-sanity.sh         |   4 +-\n t/t0204-gettext-reencode-sanity.sh          |   8 +-\n t/t0210-trace2-normal.sh                    |   6 +-\n t/t0211-trace2-perf.sh                      |  80 ++---\n t/t0212-trace2-event.sh                     |   8 +-\n t/t0300-credentials.sh                      |   4 +-\n t/t0410-partial-clone.sh                    |  82 ++---\n t/t0450-txt-doc-vs-help.sh                  |   2 +-\n t/t0500-progress-display.sh                 |  18 +-\n t/t0610-reftable-basics.sh                  |   8 +-\n t/t1004-read-tree-m-u-wf.sh                 |   8 +-\n t/t1006-cat-file.sh                         |  18 +-\n t/t1007-hash-object.sh                      |   8 +-\n t/t1011-read-tree-sparse-checkout.sh        |  10 +-\n t/t1050-large.sh                            |   6 +-\n t/t1091-sparse-checkout-builtin.sh          |  24 +-\n t/t1092-sparse-checkout-compatibility.sh    |  44 +--\n t/t1300-config.sh                           |  16 +-\n t/t1305-config-include.sh                   |   2 +-\n t/t1308-config-set.sh                       |   6 +-\n t/t1400-update-ref.sh                       | 173 +++++-----\n t/t1403-show-ref.sh                         |  18 +-\n t/t1410-reflog.sh                           |   4 +-\n t/t1415-worktree-refs.sh                    |   4 +-\n t/t1430-bad-ref-name.sh                     |  56 ++--\n t/t1450-fsck.sh                             |  12 +-\n t/t1451-fsck-buffer.sh                      |   6 +-\n t/t1460-refs-migrate.sh                     |   2 +-\n t/t1500-rev-parse.sh                        |   6 +-\n t/t1502-rev-parse-parseopt.sh               |   2 +-\n t/t1503-rev-parse-verify.sh                 |  10 +-\n t/t1510-repo-setup.sh                       |  10 +-\n t/t1512-rev-parse-disambiguation.sh         |   4 +-\n t/t1515-rev-parse-outside-repo.sh           |   2 +-\n t/t1800-hook.sh                             |  18 +-\n t/t2004-checkout-cache-temp.sh              |   4 +-\n t/t2019-checkout-ambiguous-ref.sh           |   4 +-\n t/t2024-checkout-dwim.sh                    |   8 +-\n t/t2030-unresolve-info.sh                   |   6 +-\n t/t2060-switch.sh                           |   6 +-\n t/t2070-restore.sh                          |   2 +-\n t/t2080-parallel-checkout-basics.sh         |  14 +-\n t/t2081-parallel-checkout-collisions.sh     |  24 +-\n t/t2082-parallel-checkout-attributes.sh     |  12 +-\n t/t2103-update-index-ignore-missing.sh      |   6 +-\n t/t2200-add-update.sh                       |   2 +-\n t/t2203-add-intent.sh                       |   6 +-\n t/t2400-worktree-add.sh                     |  24 +-\n t/t2402-worktree-list.sh                    |  16 +-\n t/t2403-worktree-move.sh                    |   6 +-\n t/t2405-worktree-submodule.sh               |   6 +-\n t/t2407-worktree-heads.sh                   |  26 +-\n t/t2500-untracked-overwriting.sh            |   8 +-\n t/t2501-cwd-empty.sh                        |   4 +-\n t/t3001-ls-files-others-exclude.sh          |   6 +-\n t/t3007-ls-files-recurse-submodules.sh      |   6 +-\n t/t3200-branch.sh                           |  12 +-\n t/t3202-show-branch.sh                      |  10 +-\n t/t3203-branch-output.sh                    |   4 +-\n t/t3206-range-diff.sh                       |  78 ++---\n t/t3207-branch-submodule.sh                 |   4 +-\n t/t3301-notes.sh                            |  32 +-\n t/t3310-notes-merge-manual-resolve.sh       |  16 +-\n t/t3320-notes-merge-worktrees.sh            |   2 +-\n t/t3400-rebase.sh                           |  16 +-\n t/t3402-rebase-merge.sh                     |  16 +-\n t/t3404-rebase-interactive.sh               |  72 ++--\n t/t3406-rebase-message.sh                   |   6 +-\n t/t3415-rebase-autosquash.sh                |  10 +-\n t/t3416-rebase-onto-threedots.sh            |   4 +-\n t/t3418-rebase-continue.sh                  |  10 +-\n t/t3420-rebase-autostash.sh                 |  24 +-\n t/t3422-rebase-incompatible-options.sh      |   4 +-\n t/t3429-rebase-edit-todo.sh                 |   2 +-\n t/t3430-rebase-merges.sh                    |  32 +-\n t/t3500-cherry.sh                           |   4 +-\n t/t3501-revert-cherry-pick.sh               |   6 +-\n t/t3504-cherry-pick-rerere.sh               |   6 +-\n t/t3510-cherry-pick-sequence.sh             |  24 +-\n t/t3602-rm-sparse-checkout.sh               |   4 +-\n t/t3705-add-sparse-checkout.sh              |  10 +-\n t/t3800-mktag.sh                            |   4 +-\n t/t3901-i18n-patch.sh                       |  16 +-\n t/t3903-stash.sh                            |  28 +-\n t/t3904-stash-patch.sh                      |   4 +-\n t/t3908-stash-in-worktree.sh                |   2 +-\n t/t4000-diff-format.sh                      |   2 +-\n t/t4001-diff-rename.sh                      |   4 +-\n t/t4011-diff-symlink.sh                     |   2 +-\n t/t4013-diff-various.sh                     |   2 +-\n t/t4014-format-patch.sh                     | 344 ++++++++++----------\n t/t4015-diff-whitespace.sh                  |  16 +-\n t/t4017-diff-retval.sh                      |   2 +-\n t/t4018-diff-funcname.sh                    |   2 +-\n t/t4019-diff-wserror.sh                     |   8 +-\n t/t4020-diff-external.sh                    |  18 +-\n t/t4021-format-patch-numbered.sh            |   4 +-\n t/t4022-diff-rewrite.sh                     |  14 +-\n t/t4028-format-patch-mime-headers.sh        |   6 +-\n t/t4031-diff-rewrite-binary.sh              |  18 +-\n t/t4033-diff-patience.sh                    |   2 +-\n t/t4036-format-patch-signer-mime.sh         |   6 +-\n t/t4038-diff-combined.sh                    |   6 +-\n t/t4051-diff-function-context.sh            |  38 +--\n t/t4053-diff-no-index.sh                    |   4 +-\n t/t4063-diff-blobs.sh                       |   2 +-\n t/t4065-diff-anchored.sh                    |  26 +-\n t/t4067-diff-partial-clone.sh               |  12 +-\n t/t4073-diff-stat-name-width.sh             |  24 +-\n t/t4103-apply-binary.sh                     |   2 +-\n t/t4120-apply-popt.sh                       |   2 +-\n t/t4124-apply-ws-rule.sh                    |  10 +-\n t/t4128-apply-root.sh                       |   2 +-\n t/t4140-apply-ita.sh                        |   4 +-\n t/t4141-apply-too-large.sh                  |   2 +-\n t/t4150-am.sh                               |  48 +--\n t/t4200-rerere.sh                           |   6 +-\n t/t4201-shortlog.sh                         |   2 +-\n t/t4202-log.sh                              |  84 ++---\n t/t4204-patch-id.sh                         |   2 +-\n t/t4205-log-pretty-formats.sh               |   2 +-\n t/t4209-log-pickaxe.sh                      |  10 +-\n t/t4211-line-log.sh                         |  72 ++--\n t/t4216-log-bloom.sh                        |  18 +-\n t/t4252-am-options.sh                       |  22 +-\n t/t4254-am-corrupt.sh                       |   6 +-\n t/t4258-am-quoted-cr.sh                     |   2 +-\n t/t4301-merge-tree-write-tree.sh            |  18 +-\n t/t5000-tar-tree.sh                         |  10 +-\n t/t5004-archive-corner-cases.sh             |   2 +-\n t/t5100-mailinfo.sh                         |   2 +-\n t/t5150-request-pull.sh                     |  18 +-\n t/t5300-pack-object.sh                      |  22 +-\n t/t5302-pack-index.sh                       |   6 +-\n t/t5304-prune.sh                            |   8 +-\n t/t5310-pack-bitmaps.sh                     |  14 +-\n t/t5317-pack-objects-filter-objects.sh      |  12 +-\n t/t5318-commit-graph.sh                     |   8 +-\n t/t5319-multi-pack-index.sh                 |  16 +-\n t/t5324-split-commit-graph.sh               |  10 +-\n t/t5325-reverse-index.sh                    |   2 +-\n t/t5326-multi-pack-bitmaps.sh               |  28 +-\n t/t5328-commit-graph-64bit-time.sh          |   2 +-\n t/t5329-pack-objects-cruft.sh               |   8 +-\n t/t5334-incremental-multi-pack-index.sh     |   2 +-\n t/t5335-compact-multi-pack-index.sh         |   4 +-\n t/t5351-unpack-large-objects.sh             |   2 +-\n t/t5402-post-merge-hook.sh                  |   4 +-\n t/t5403-post-checkout-hook.sh               |   2 +-\n t/t5404-tracking-branches.sh                |   2 +-\n t/t5406-remote-rejects.sh                   |   2 +-\n t/t5407-post-rewrite-hook.sh                |   8 +-\n t/t5409-colorize-remote-messages.sh         |  36 +-\n t/t5500-fetch-pack.sh                       |  38 +--\n t/t5504-fetch-receive-strict.sh             |  14 +-\n t/t5505-remote.sh                           |  20 +-\n t/t5510-fetch.sh                            |  10 +-\n t/t5512-ls-remote.sh                        |   8 +-\n t/t5514-fetch-multiple.sh                   |   2 +-\n t/t5516-fetch-push.sh                       |  20 +-\n t/t5520-pull.sh                             |   4 +-\n t/t5524-pull-msg.sh                         |   6 +-\n t/t5526-fetch-submodules.sh                 |  16 +-\n t/t5529-push-errors.sh                      |   4 +-\n t/t5530-upload-pack-error.sh                |  18 +-\n t/t5531-deep-submodule-push.sh              |   2 +-\n t/t5532-fetch-proxy.sh                      |   2 +-\n t/t5533-push-cas.sh                         |  12 +-\n t/t5534-push-signed.sh                      |  22 +-\n t/t5537-fetch-shallow.sh                    |   2 +-\n t/t5538-push-shallow.sh                     |   2 +-\n t/t5539-fetch-http-shallow.sh               |   4 +-\n t/t5541-http-push-smart.sh                  |  32 +-\n t/t5544-pack-objects-hook.sh                |  12 +-\n t/t5550-http-fetch-dumb.sh                  |   4 +-\n t/t5551-http-fetch-smart.sh                 |  46 +--\n t/t5552-skipping-fetch-negotiator.sh        |   6 +-\n t/t5554-noop-fetch-negotiator.sh            |   4 +-\n t/t5557-http-get.sh                         |   2 +-\n t/t5558-clone-bundle-uri.sh                 |  38 +--\n t/t5562-http-backend-content-length.sh      |   2 +-\n t/t5564-http-proxy.sh                       |  10 +-\n t/t5581-http-curl-verbose.sh                |   2 +-\n t/t5583-push-branches.sh                    |   8 +-\n t/t5601-clone.sh                            |  28 +-\n t/t5604-clone-reference.sh                  |   8 +-\n t/t5605-clone-local.sh                      |   2 +-\n t/t5606-clone-options.sh                    |   6 +-\n t/t5612-clone-refspec.sh                    |   2 +-\n t/t5616-partial-clone.sh                    |  60 ++--\n t/t5619-clone-local-ambiguous-transport.sh  |   2 +-\n t/t5620-backfill.sh                         |  12 +-\n t/t5700-protocol-v1.sh                      |  46 +--\n t/t5701-git-serve.sh                        |  14 +-\n t/t5702-protocol-v2.sh                      | 154 ++++-----\n t/t5703-upload-pack-ref-in-want.sh          |  22 +-\n t/t5705-session-id-in-capabilities.sh       |  12 +-\n t/t5750-bundle-uri-parse.sh                 |   8 +-\n t/t5801-remote-helpers.sh                   |   4 +-\n t/t5810-proto-disable-local.sh              |   2 +-\n t/t5813-proto-disable-ssh.sh                |   4 +-\n t/t6000-rev-list-misc.sh                    |  26 +-\n t/t6005-rev-list-count.sh                   |   8 +-\n t/t6006-rev-list-format.sh                  |   4 +-\n t/t6009-rev-list-parent.sh                  |   4 +-\n t/t6020-bundle-misc.sh                      |  12 +-\n t/t6022-rev-list-missing.sh                 |   4 +-\n t/t6030-bisect-porcelain.sh                 | 150 ++++-----\n t/t6040-tracking-info.sh                    |   2 +-\n t/t6112-rev-list-filters-objects.sh         |  24 +-\n t/t6115-rev-list-du.sh                      |   4 +-\n t/t6120-describe.sh                         |  14 +-\n t/t6200-fmt-merge-msg.sh                    |  82 ++---\n t/t6402-merge-rename.sh                     |   4 +-\n t/t6403-merge-file.sh                       |   6 +-\n t/t6404-recursive-merge.sh                  |   2 +-\n t/t6406-merge-attr.sh                       |  20 +-\n t/t6417-merge-ours-theirs.sh                |  30 +-\n t/t6418-merge-text-auto.sh                  |   2 +-\n t/t6422-merge-rename-corner-cases.sh        |   8 +-\n t/t6423-merge-rename-directories.sh         |  72 ++--\n t/t6424-merge-unrelated-index-changes.sh    |   6 +-\n t/t6427-diff3-conflict-markers.sh           |  10 +-\n t/t6432-merge-recursive-space-options.sh    |   4 +-\n t/t6436-merge-overwrite.sh                  |   6 +-\n t/t6437-submodule-merge.sh                  |  10 +-\n t/t6500-gc.sh                               |   8 +-\n t/t6600-test-reach.sh                       |   4 +-\n t/t7001-mv.sh                               |  16 +-\n t/t7002-mv-sparse-checkout.sh               |  38 +--\n t/t7003-filter-branch.sh                    |  16 +-\n t/t7004-tag.sh                              |   2 +-\n t/t7006-pager.sh                            |  16 +-\n t/t7012-skip-worktree-writing.sh            |   6 +-\n t/t7030-verify-tag.sh                       |  52 +--\n t/t7031-verify-tag-signed-ssh.sh            |  46 +--\n t/t7102-reset.sh                            |   2 +-\n t/t7110-reset-merge.sh                      |  40 +--\n t/t7201-co.sh                               |   6 +-\n t/t7300-clean.sh                            |   2 +-\n t/t7301-clean-interactive.sh                |   2 +-\n t/t7400-submodule-basic.sh                  |  32 +-\n t/t7402-submodule-rebase.sh                 |   2 +-\n t/t7406-submodule-update.sh                 |  26 +-\n t/t7416-submodule-dash-url.sh               |  20 +-\n t/t7417-submodule-path-url.sh               |   2 +-\n t/t7450-bad-git-dotfiles.sh                 |  18 +-\n t/t7501-commit-basic-functionality.sh       |  16 +-\n t/t7502-commit-porcelain.sh                 |   2 +-\n t/t7507-commit-verbose.sh                   |   6 +-\n t/t7508-status.sh                           |   6 +-\n t/t7510-signed-commit.sh                    |  68 ++--\n t/t7516-commit-races.sh                     |   4 +-\n t/t7519-status-fsmonitor.sh                 |  14 +-\n t/t7527-builtin-fsmonitor.sh                |  82 ++---\n t/t7528-signed-commit-ssh.sh                |  68 ++--\n t/t7600-merge.sh                            |  10 +-\n t/t7603-merge-reduce-heads.sh               |  20 +-\n t/t7606-merge-custom.sh                     |   2 +-\n t/t7607-merge-state.sh                      |   4 +-\n t/t7610-mergetool.sh                        |  18 +-\n t/t7700-repack.sh                           |  14 +-\n t/t7703-repack-geometric.sh                 |   4 +-\n t/t7704-repack-cruft.sh                     |  12 +-\n t/t7800-difftool.sh                         |  26 +-\n t/t7810-grep.sh                             |  22 +-\n t/t7814-grep-recurse-submodules.sh          |   2 +-\n t/t7900-maintenance.sh                      |  34 +-\n t/t8008-blame-formats.sh                    |   2 +-\n t/t8010-cat-file-filters.sh                 |   2 +-\n t/t8012-blame-colors.sh                     |   2 +-\n t/t9001-send-email.sh                       | 190 +++++------\n t/t9003-help-autocorrect.sh                 |   6 +-\n t/t9106-git-svn-commit-diff-clobber.sh      |   2 +-\n t/t9107-git-svn-migrate.sh                  |  30 +-\n t/t9110-git-svn-use-svm-props.sh            |  20 +-\n t/t9111-git-svn-use-svnsync-props.sh        |  18 +-\n t/t9114-git-svn-dcommit-merge.sh            |   6 +-\n t/t9116-git-svn-log.sh                      |   8 +-\n t/t9117-git-svn-init-clone.sh               |  12 +-\n t/t9119-git-svn-info.sh                     |  16 +-\n t/t9122-git-svn-author.sh                   |   8 +-\n t/t9130-git-svn-authors-file.sh             |   8 +-\n t/t9138-git-svn-authors-prog.sh             |  14 +-\n t/t9140-git-svn-reset.sh                    |   4 +-\n t/t9153-git-svn-rewrite-uuid.sh             |   4 +-\n t/t9200-git-cvsexportcommit.sh              |   2 +-\n t/t9210-scalar.sh                           |  34 +-\n t/t9211-scalar-clone.sh                     |  16 +-\n t/t9300-fast-import.sh                      |  10 +-\n t/t9350-fast-export.sh                      |  54 +--\n t/t9351-fast-export-anonymize.sh            |  36 +-\n t/t9400-git-cvsserver-server.sh             |   4 +-\n t/t9501-gitweb-standalone-http-status.sh    |  58 ++--\n t/t9502-gitweb-standalone-parse-output.sh   |  38 +--\n t/t9800-git-p4-basic.sh                     |  10 +-\n t/t9801-git-p4-branch.sh                    |  48 +--\n t/t9806-git-p4-options.sh                   |  10 +-\n t/t9807-git-p4-submit.sh                    |   2 +-\n t/t9810-git-p4-rcs.sh                       |   8 +-\n t/t9813-git-p4-preserve-users.sh            |   8 +-\n t/t9814-git-p4-rename.sh                    |   8 +-\n t/t9827-git-p4-change-filetype.sh           |   4 +-\n t/t9832-unshelve.sh                         |   6 +-\n t/t9833-errors.sh                           |   4 +-\n t/t9835-git-p4-metadata-encoding-python2.sh |  36 +-\n t/t9836-git-p4-metadata-encoding-python3.sh |  38 +--\n t/t9850-shell.sh                            |   2 +-\n t/t9902-completion.sh                       |  26 +-\n 338 files changed, 2813 insertions(+), 2806 deletions(-)\n\ndiff --git a/t/for-each-ref-tests.sh b/t/for-each-ref-tests.sh\nindex bd2d45c971..6b359d940e 100644\n--- a/t/for-each-ref-tests.sh\n+++ b/t/for-each-ref-tests.sh\n@@ -523,7 +523,7 @@ test_expect_success 'Verify descending sort' '\n \n test_expect_success 'Give help even with invalid sort atoms' '\n \ttest_expect_code 129 ${git_for_each_ref} --sort=bogus -h >actual 2>&1 &&\n-\tgrep \"^usage: ${git_for_each_ref}\" actual\n+\ttest_grep \"^usage: ${git_for_each_ref}\" actual\n '\n \n cat >expected <<\\EOF\n@@ -622,7 +622,7 @@ test_expect_success 'Quoting style: tcl' '\n for i in \"--perl --shell\" \"-s --python\" \"--python --tcl\" \"--tcl --perl\"; do\n \ttest_expect_success \"more than one quoting style: $i\" \"\n \t\ttest_must_fail ${git_for_each_ref} $i 2>err &&\n-\t\tgrep '^error: more than one quoting style' err\n+\t\ttest_grep '^error: more than one quoting style' err\n \t\"\n done\n \n@@ -1892,7 +1892,7 @@ test_expect_success \"${git_for_each_ref} --stdin: fails if extra args\" '\n \t>in &&\n \ttest_must_fail ${git_for_each_ref} --format=\"%(refname)\" \\\n \t\t--stdin refs/heads/extra <in 2>err &&\n-\tgrep \"unknown arguments supplied with --stdin\" err\n+\ttest_grep \"unknown arguments supplied with --stdin\" err\n '\n \n test_expect_success \"${git_for_each_ref} --stdin: matches\" '\n@@ -1955,11 +1955,11 @@ test_expect_success \"${git_for_each_ref} with nested tags\" '\n \n test_expect_success 'is-base atom with non-commits' '\n \t${git_for_each_ref} --format=\"%(is-base:HEAD) %(refname)\" >out 2>err &&\n-\tgrep \"(HEAD) refs/heads/main\" out &&\n+\ttest_grep \"(HEAD) refs/heads/main\" out &&\n \n \ttest_line_count = 2 err &&\n-\tgrep \"error: object .* is a commit, not a blob\" err &&\n-\tgrep \"error: bad tag pointer to\" err\n+\ttest_grep \"error: object .* is a commit, not a blob\" err &&\n+\ttest_grep \"error: bad tag pointer to\" err\n '\n \n GRADE_FORMAT=\"%(signature:grade)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)\"\ndiff --git a/t/lib-bitmap.sh b/t/lib-bitmap.sh\nindex 62aa6744a6..2de6646128 100644\n--- a/t/lib-bitmap.sh\n+++ b/t/lib-bitmap.sh\n@@ -173,7 +173,7 @@ rev_list_tests_head () {\n \n \ttest_expect_success \"bitmap --objects handles non-commit objects ($state, $branch)\" '\n \t\tgit rev-list --objects --use-bitmap-index $branch tagged-blob >actual &&\n-\t\tgrep $blob actual\n+\t\ttest_grep $blob actual\n \t'\n }\n \n@@ -242,16 +242,16 @@ basic_bitmap_tests () {\n \t\tGIT_PROGRESS_DELAY=0 \\\n \t\t\tgit pack-objects --all --stdout --progress \\\n \t\t\t</dev/null >/dev/null 2>stderr &&\n-\t\tgrep \"Enumerating objects: $count, done\" stderr &&\n-\t\tgrep \"pack-reused $count\" stderr &&\n+\t\ttest_grep \"Enumerating objects: $count, done\" stderr &&\n+\t\ttest_grep \"pack-reused $count\" stderr &&\n \n \t\t# now the same but with one non-reused object\n \t\tgit commit --allow-empty -m \"an extra commit object\" &&\n \t\tGIT_PROGRESS_DELAY=0 \\\n \t\t\tgit pack-objects --all --stdout --progress \\\n \t\t\t</dev/null >/dev/null 2>stderr &&\n-\t\tgrep \"Enumerating objects: $((count+1)), done\" stderr &&\n-\t\tgrep \"pack-reused $count\" stderr\n+\t\ttest_grep \"Enumerating objects: $((count+1)), done\" stderr &&\n+\t\ttest_grep \"pack-reused $count\" stderr\n \t'\n }\n \n@@ -283,7 +283,7 @@ test_rev_exists () {\n \t\tthen\n \t\t\ttest_path_is_file $midx-$(midx_checksum $objdir).rev\n \t\tfi &&\n-\t\tgrep \"\\\"category\\\":\\\"load_midx_revindex\\\",\\\"key\\\":\\\"source\\\",\\\"value\\\":\\\"$kind\\\"\" event.trace\n+\t\ttest_grep \"\\\"category\\\":\\\"load_midx_revindex\\\",\\\"key\\\":\\\"source\\\",\\\"value\\\":\\\"$kind\\\"\" event.trace\n \t'\n }\n \ndiff --git a/t/lib-bundle-uri-protocol.sh b/t/lib-bundle-uri-protocol.sh\nindex de09b6b02e..ecf3284297 100644\n--- a/t/lib-bundle-uri-protocol.sh\n+++ b/t/lib-bundle-uri-protocol.sh\n@@ -63,9 +63,9 @@ test_expect_success \"connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: no\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log &&\n+\ttest_grep \"< version 2\" log &&\n \n-\t! grep bundle-uri log\n+\ttest_grep ! bundle-uri log\n '\n \n test_expect_success \"connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: have bundle-uri\" '\n@@ -78,10 +78,10 @@ test_expect_success \"connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: hav\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log &&\n+\ttest_grep \"< version 2\" log &&\n \n \t# Server advertised bundle-uri capability\n-\tgrep \"< bundle-uri\" log\n+\ttest_grep \"< bundle-uri\" log\n '\n \n test_expect_success \"clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: request bundle-uris\" '\n@@ -95,13 +95,13 @@ test_expect_success \"clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log &&\n+\ttest_grep \"< version 2\" log &&\n \n \t# Server advertised bundle-uri capability\n-\tgrep \"< bundle-uri\" log &&\n+\ttest_grep \"< bundle-uri\" log &&\n \n \t# Client did not issue bundle-uri command\n-\t! grep \"> command=bundle-uri\" log &&\n+\ttest_grep ! \"> command=bundle-uri\" log &&\n \n \tGIT_TRACE_PACKET=\"$PWD/log\" \\\n \tgit \\\n@@ -111,13 +111,13 @@ test_expect_success \"clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log &&\n+\ttest_grep \"< version 2\" log &&\n \n \t# Server advertised bundle-uri capability\n-\tgrep \"< bundle-uri\" log &&\n+\ttest_grep \"< bundle-uri\" log &&\n \n \t# Client issued bundle-uri command\n-\tgrep \"> command=bundle-uri\" log &&\n+\ttest_grep \"> command=bundle-uri\" log &&\n \n \tGIT_TRACE_PACKET=\"$PWD/log3\" \\\n \tgit \\\n@@ -128,13 +128,13 @@ test_expect_success \"clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log3 &&\n+\ttest_grep \"< version 2\" log3 &&\n \n \t# Server advertised bundle-uri capability\n-\tgrep \"< bundle-uri\" log3 &&\n+\ttest_grep \"< bundle-uri\" log3 &&\n \n \t# Client did not issue bundle-uri command (--bundle-uri override)\n-\t! grep \"> command=bundle-uri\" log3\n+\ttest_grep ! \"> command=bundle-uri\" log3\n '\n \n # The remaining tests will all assume transfer.bundleURI=true\ndiff --git a/t/lib-httpd.sh b/t/lib-httpd.sh\nindex fc646447d5..a216e5376f 100644\n--- a/t/lib-httpd.sh\n+++ b/t/lib-httpd.sh\n@@ -275,7 +275,7 @@ test_http_push_nonff () {\n \t'\n \n \ttest_expect_success 'non-fast-forward push show ref status' '\n-\t\tgrep \"^ ! \\[rejected\\][ ]*$BRANCH -> $BRANCH (non-fast-forward)$\" output\n+\t\ttest_grep \"^ ! \\[rejected\\][ ]*$BRANCH -> $BRANCH (non-fast-forward)$\" output\n \t'\n \n \ttest_expect_success 'non-fast-forward push shows help message' '\ndiff --git a/t/pack-refs-tests.sh b/t/pack-refs-tests.sh\nindex d76b087b09..a800177446 100644\n--- a/t/pack-refs-tests.sh\n+++ b/t/pack-refs-tests.sh\n@@ -195,7 +195,7 @@ test_expect_success 'delete ref while another dangling packed ref' '\n test_expect_success 'pack ref directly below refs/' '\n \tgit update-ref refs/top HEAD &&\n \tgit ${pack_refs} --all --prune &&\n-\tgrep refs/top .git/packed-refs &&\n+\ttest_grep refs/top .git/packed-refs &&\n \ttest_path_is_missing .git/refs/top\n '\n \ndiff --git a/t/show-ref-exists-tests.sh b/t/show-ref-exists-tests.sh\nindex 36e8e9df33..1351f690eb 100644\n--- a/t/show-ref-exists-tests.sh\n+++ b/t/show-ref-exists-tests.sh\n@@ -19,7 +19,7 @@ test_expect_success '--exists with missing reference' '\n \n test_expect_success '--exists does not use DWIM' '\n \ttest_expect_code 2 ${git_show_ref_exists} $GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME 2>err &&\n-\tgrep \"reference does not exist\" err\n+\ttest_grep \"reference does not exist\" err\n '\n \n test_expect_success '--exists with HEAD' '\ndiff --git a/t/t0000-basic.sh b/t/t0000-basic.sh\nindex 2b63e1c86c..7bd1f4e7b7 100755\n--- a/t/t0000-basic.sh\n+++ b/t/t0000-basic.sh\n@@ -743,7 +743,7 @@ test_expect_success 'subtest: lazy prereqs do not turn off tracing' '\n \ttest_done\n \tEOF\n \n-\tgrep \"echo trace\" lazy-prereq-and-tracing/err\n+\ttest_grep \"echo trace\" lazy-prereq-and-tracing/err\n '\n \n test_expect_success 'subtest: tests clean up after themselves' '\n@@ -815,7 +815,7 @@ test_expect_success 'subtest: test_atexit is run' '\n \n test_expect_success 'test_oid provides sane info by default' '\n \ttest_oid zero >actual &&\n-\tgrep \"^00*\\$\" actual &&\n+\ttest_grep \"^00*\\$\" actual &&\n \trawsz=\"$(test_oid rawsz)\" &&\n \thexsz=\"$(test_oid hexsz)\" &&\n \t# +1 accounts for the trailing newline\n@@ -827,7 +827,7 @@ test_expect_success 'test_oid can look up data for SHA-1' '\n \ttest_when_finished \"test_detect_hash\" &&\n \ttest_set_hash sha1 &&\n \ttest_oid zero >actual &&\n-\tgrep \"^00*\\$\" actual &&\n+\ttest_grep \"^00*\\$\" actual &&\n \trawsz=\"$(test_oid rawsz)\" &&\n \thexsz=\"$(test_oid hexsz)\" &&\n \ttest $(wc -c <actual) -eq 41 &&\n@@ -839,7 +839,7 @@ test_expect_success 'test_oid can look up data for SHA-256' '\n \ttest_when_finished \"test_detect_hash\" &&\n \ttest_set_hash sha256 &&\n \ttest_oid zero >actual &&\n-\tgrep \"^00*\\$\" actual &&\n+\ttest_grep \"^00*\\$\" actual &&\n \trawsz=\"$(test_oid rawsz)\" &&\n \thexsz=\"$(test_oid hexsz)\" &&\n \ttest $(wc -c <actual) -eq 65 &&\n@@ -884,11 +884,11 @@ test_expect_success 'test_bool_env' '\n \t\t# test script, hence the redirection of fd 7, and aborts\n \t\t# with \"exit 1\", hence the subshell.\n \t\t! ( test_bool_env envvar true ) 7>err &&\n-\t\tgrep \"error: test_bool_env requires bool values\" err &&\n+\t\ttest_grep \"error: test_bool_env requires bool values\" err &&\n \n \t\tenvvar=true &&\n \t\t! ( test_bool_env envvar invalid ) 7>err &&\n-\t\tgrep \"error: test_bool_env requires bool values\" err\n+\t\ttest_grep \"error: test_bool_env requires bool values\" err\n \t)\n '\n \n@@ -1242,12 +1242,12 @@ test_expect_success 'test_must_fail on a failing git command with env' '\n \n test_expect_success 'test_must_fail rejects a non-git command' '\n \t! test_must_fail grep ^$ notafile 2>err &&\n-\tgrep -F \"test_must_fail: only '\"'\"'git'\"'\"' is allowed\" err\n+\ttest_grep -F \"test_must_fail: only '\"'\"'git'\"'\"' is allowed\" err\n '\n \n test_expect_success 'test_must_fail rejects a non-git command with env' '\n \t! test_must_fail env var1=a var2=b grep ^$ notafile 2>err &&\n-\tgrep -F \"test_must_fail: only '\"'\"'git'\"'\"' is allowed\" err\n+\ttest_grep -F \"test_must_fail: only '\"'\"'git'\"'\"' is allowed\" err\n '\n \n test_done\ndiff --git a/t/t0001-init.sh b/t/t0001-init.sh\nindex e89feca544..5cf2e5a35a 100755\n--- a/t/t0001-init.sh\n+++ b/t/t0001-init.sh\n@@ -278,9 +278,9 @@ test_expect_success POSIXPERM 'init creates a new deep directory (umask vs. shar\n \t\tgit init --bare --shared=0660 newdir/a/b/c &&\n \t\ttest_path_is_dir newdir/a/b/c/refs &&\n \t\tls -ld newdir/a newdir/a/b > lsab.out &&\n-\t\t! grep -v \"^drwxrw[sx]r-x\" lsab.out &&\n+\t\ttest_grep ! -v \"^drwxrw[sx]r-x\" lsab.out &&\n \t\tls -ld newdir/a/b/c > lsc.out &&\n-\t\t! grep -v \"^drwxrw[sx]---\" lsc.out\n+\t\ttest_grep ! -v \"^drwxrw[sx]---\" lsc.out\n \t)\n '\n \n@@ -619,7 +619,7 @@ test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage is not allowed wi\n \tgit init refstorage &&\n \tgit -C refstorage config extensions.refStorage files &&\n \ttest_must_fail git -C refstorage rev-parse 2>err &&\n-\tgrep \"repo version is 0, but v1-only extension found\" err\n+\ttest_grep \"repo version is 0, but v1-only extension found\" err\n '\n \n test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage with files backend' '\n@@ -637,7 +637,7 @@ test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage with unknown back\n \tgit -C refstorage config core.repositoryformatversion 1 &&\n \tgit -C refstorage config extensions.refStorage garbage &&\n \ttest_must_fail git -C refstorage rev-parse 2>err &&\n-\tgrep \"invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}garbage${SQ}\" err\n+\ttest_grep \"invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}garbage${SQ}\" err\n '\n \n test_expect_success 'init with GIT_DEFAULT_REF_FORMAT=garbage' '\n@@ -848,8 +848,8 @@ test_expect_success MINGW 'redirect std handles' '\n \t\tGIT_REDIRECT_STDOUT=output.txt \\\n \t\tGIT_REDIRECT_STDERR=\"2>&1\" \\\n \t\tgit rev-parse --git-dir --verify refs/invalid &&\n-\tgrep \"^\\\\.git\\$\" output.txt &&\n-\tgrep \"Needed a single revision\" output.txt\n+\ttest_grep \"^\\\\.git\\$\" output.txt &&\n+\ttest_grep \"Needed a single revision\" output.txt\n '\n \n test_expect_success '--initial-branch' '\n@@ -862,14 +862,14 @@ test_expect_success '--initial-branch' '\n \tgit init --initial-branch=ignore initial-branch-option 2>err &&\n \ttest_grep \"ignored --initial-branch\" err &&\n \tgit -C initial-branch-option symbolic-ref HEAD >actual &&\n-\tgrep hello actual\n+\ttest_grep hello actual\n '\n \n test_expect_success 'overridden default initial branch name (config)' '\n \ttest_config_global init.defaultBranch nmb &&\n \tGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME= git init initial-branch-config &&\n \tgit -C initial-branch-config symbolic-ref HEAD >actual &&\n-\tgrep nmb actual\n+\ttest_grep nmb actual\n '\n \n test_expect_success 'advice on unconfigured init.defaultBranch' '\n@@ -907,7 +907,7 @@ test_expect_success 'overridden default main branch name (env)' '\n \ttest_config_global init.defaultBranch nmb &&\n \tGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=env git init main-branch-env &&\n \tgit -C main-branch-env symbolic-ref HEAD >actual &&\n-\tgrep env actual\n+\ttest_grep env actual\n '\n \n test_expect_success 'invalid default branch name' '\ndiff --git a/t/t0008-ignores.sh b/t/t0008-ignores.sh\nindex d77a179bdd..ed95faf327 100755\n--- a/t/t0008-ignores.sh\n+++ b/t/t0008-ignores.sh\n@@ -790,8 +790,8 @@ test_expect_success 'existing file and directory' '\n \t>one &&\n \tmkdir top-level-dir &&\n \tgit check-ignore one top-level-dir >actual &&\n-\tgrep one actual &&\n-\tgrep top-level-dir actual\n+\ttest_grep one actual &&\n+\ttest_grep top-level-dir actual\n '\n \n test_expect_success 'existing directory and file' '\n@@ -800,8 +800,8 @@ test_expect_success 'existing directory and file' '\n \t>one &&\n \tmkdir top-level-dir &&\n \tgit check-ignore top-level-dir one >actual &&\n-\tgrep one actual &&\n-\tgrep top-level-dir actual\n+\ttest_grep one actual &&\n+\ttest_grep top-level-dir actual\n '\n \n test_expect_success 'exact prefix matching (with root)' '\ndiff --git a/t/t0009-git-dir-validation.sh b/t/t0009-git-dir-validation.sh\nindex 33d21ed9ea..4cba478e50 100755\n--- a/t/t0009-git-dir-validation.sh\n+++ b/t/t0009-git-dir-validation.sh\n@@ -35,7 +35,7 @@ test_expect_success PIPE 'setup: .git as a FIFO (named pipe) is rejected' '\n \t\tcd parent/fifo-trap &&\n \t\tmkfifo .git &&\n \t\ttest_must_fail git rev-parse --git-dir 2>stderr &&\n-\t\tgrep \"not a regular file\" stderr\n+\t\ttest_grep \"not a regular file\" stderr\n \t)\n '\n \n@@ -47,7 +47,7 @@ test_expect_success SYMLINKS,PIPE 'setup: .git as a symlink to a FIFO is rejecte\n \t\tmkfifo target-fifo &&\n \t\tln -s target-fifo .git &&\n \t\ttest_must_fail git rev-parse --git-dir 2>stderr &&\n-\t\tgrep \"not a regular file\" stderr\n+\t\ttest_grep \"not a regular file\" stderr\n \t)\n '\n \n@@ -58,7 +58,7 @@ test_expect_success 'setup: .git with garbage content is rejected' '\n \t\tcd parent/garbage-trap &&\n \t\techo \"garbage\" >.git &&\n \t\ttest_must_fail git rev-parse --git-dir 2>stderr &&\n-\t\tgrep \"invalid gitfile format\" stderr\n+\t\ttest_grep \"invalid gitfile format\" stderr\n \t)\n '\n \ndiff --git a/t/t0012-help.sh b/t/t0012-help.sh\nindex c33501bdcd..48d128ad04 100755\n--- a/t/t0012-help.sh\n+++ b/t/t0012-help.sh\n@@ -131,8 +131,8 @@ test_expect_success 'git help succeeds without git.html' '\n \n test_expect_success 'git help --user-interfaces' '\n \tgit help --user-interfaces >help.output &&\n-\tgrep \"^   attributes   \" help.output &&\n-\tgrep \"^   mailmap   \" help.output\n+\ttest_grep \"^   attributes   \" help.output &&\n+\ttest_grep \"^   mailmap   \" help.output\n '\n \n test_expect_success 'git help -c' '\ndiff --git a/t/t0013-sha1dc.sh b/t/t0013-sha1dc.sh\nindex ce3d81227a..3ea3169d92 100755\n--- a/t/t0013-sha1dc.sh\n+++ b/t/t0013-sha1dc.sh\n@@ -16,7 +16,7 @@ fi\n test_expect_success 'test-sha1 detects shattered pdf' '\n \ttest_must_fail test-tool sha1 <\"$TEST_DATA/shattered-1.pdf\" 2>err &&\n \ttest_grep collision err &&\n-\tgrep 38762cf7f55934b34d179ae6a4c80cadccbb7f0a err\n+\ttest_grep 38762cf7f55934b34d179ae6a4c80cadccbb7f0a err\n '\n \n test_done\ndiff --git a/t/t0017-env-helper.sh b/t/t0017-env-helper.sh\nindex 32fe848179..da02bce0ef 100755\n--- a/t/t0017-env-helper.sh\n+++ b/t/t0017-env-helper.sh\n@@ -88,7 +88,7 @@ test_expect_success 'test-tool env-helper reads config thanks to trace2' '\n \ttest_must_fail \\\n \t\tenv HOME=\"$(pwd)/home\" \\\n \t\tgit config -l 2>err &&\n-\tgrep \"exceeded maximum include depth\" err &&\n+\ttest_grep \"exceeded maximum include depth\" err &&\n \n \t# This validates that the assumption that we attempt to\n \t# read the configuration and fail very early in the start-up\n@@ -100,7 +100,7 @@ test_expect_success 'test-tool env-helper reads config thanks to trace2' '\n \t\ttest-tool -C no-such-directory \\\n \t\tenv-helper --type=bool --default=0 \\\n \t\t--exit-code GIT_TEST_ENV_HELPER 2>err &&\n-\tgrep \"exceeded maximum include depth\" err\n+\ttest_grep \"exceeded maximum include depth\" err\n '\n \n test_done\ndiff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh\nindex 033b00a364..4f29d088f3 100755\n--- a/t/t0021-conversion.sh\n+++ b/t/t0021-conversion.sh\n@@ -731,7 +731,7 @@ test_expect_success 'process filter should restart after unexpected write failur\n \t\trm -f debug.log &&\n \t\tgit checkout --quiet --no-progress . 2>git-stderr.log &&\n \n-\t\tgrep \"smudge write error\" git-stderr.log &&\n+\t\ttest_grep \"smudge write error\" git-stderr.log &&\n \t\ttest_grep \"error: external filter\" git-stderr.log &&\n \n \t\tcat >expected.log <<-EOF &&\n@@ -853,7 +853,7 @@ test_expect_success 'invalid process filter must fail (and not hang!)' '\n \n \t\tcp \"$TEST_ROOT/test.o\" test.r &&\n \t\ttest_must_fail git add . 2>git-stderr.log &&\n-\t\tgrep \"expected git-filter-server\" git-stderr.log\n+\t\ttest_grep \"expected git-filter-server\" git-stderr.log\n \t)\n '\n \n@@ -970,7 +970,7 @@ test_expect_success 'missing file in delayed checkout' '\n \n \trm -rf repo-cloned &&\n \ttest_must_fail git clone repo repo-cloned 2>git-stderr.log &&\n-\tgrep \"error: .missing-delay\\.a. was not filtered properly\" git-stderr.log\n+\ttest_grep \"error: .missing-delay\\.a. was not filtered properly\" git-stderr.log\n '\n \n test_expect_success 'invalid file in delayed checkout' '\n@@ -991,7 +991,7 @@ test_expect_success 'invalid file in delayed checkout' '\n \n \trm -rf repo-cloned &&\n \ttest_must_fail git clone repo repo-cloned 2>git-stderr.log &&\n-\tgrep \"error: external filter .* signaled that .unfiltered. is now available although it has not been delayed earlier\" git-stderr.log\n+\ttest_grep \"error: external filter .* signaled that .unfiltered. is now available although it has not been delayed earlier\" git-stderr.log\n '\n \n for mode in 'case' 'utf-8'\n@@ -1032,7 +1032,7 @@ do\n \n \t\tgit clone $mode-collision $mode-collision-cloned &&\n \t\t# Make sure z was really delayed\n-\t\tgrep \"IN: smudge $dir/z .* \\\\[DELAYED\\\\]\" $mode-collision-cloned/delayed.log &&\n+\t\ttest_grep \"IN: smudge $dir/z .* \\\\[DELAYED\\\\]\" $mode-collision-cloned/delayed.log &&\n \n \t\t# Should not create $dir/z at $symlink/z\n \t\ttest_path_is_missing $mode-collision/target-dir/z\n@@ -1070,7 +1070,7 @@ test_expect_success SYMLINKS,CASE_INSENSITIVE_FS \\\n \t\tgit commit -m super &&\n \n \t\tgit checkout --recurse-submodules . &&\n-\t\tgrep \"IN: smudge A/B/y .* \\\\[DELAYED\\\\]\" delayed.log &&\n+\t\ttest_grep \"IN: smudge A/B/y .* \\\\[DELAYED\\\\]\" delayed.log &&\n \t\ttest_path_is_missing target-dir/y\n \t)\n '\n@@ -1161,9 +1161,9 @@ test_expect_success 'delayed checkout correctly reports the number of updated en\n \n \t\trm *.a &&\n \t\tgit checkout . 2>err &&\n-\t\tgrep \"IN: smudge test-delay10.a .* \\\\[DELAYED\\\\]\" delayed.log &&\n-\t\tgrep \"IN: smudge test-delay11.a .* \\\\[DELAYED\\\\]\" delayed.log &&\n-\t\tgrep \"Updated 2 paths from the index\" err\n+\t\ttest_grep \"IN: smudge test-delay10.a .* \\\\[DELAYED\\\\]\" delayed.log &&\n+\t\ttest_grep \"IN: smudge test-delay11.a .* \\\\[DELAYED\\\\]\" delayed.log &&\n+\t\ttest_grep \"Updated 2 paths from the index\" err\n \t)\n '\n \ndiff --git a/t/t0029-core-unsetenvvars.sh b/t/t0029-core-unsetenvvars.sh\nindex baa1b7e85b..975620b389 100755\n--- a/t/t0029-core-unsetenvvars.sh\n+++ b/t/t0029-core-unsetenvvars.sh\n@@ -20,10 +20,10 @@ test_expect_success 'core.unsetenvvars works' '\n \tHOBBES=Calvin &&\n \texport HOBBES &&\n \tgit commit --allow-empty -m with 2>err &&\n-\tgrep Calvin err &&\n+\ttest_grep Calvin err &&\n \tgit -c core.unsetenvvars=FINDUS,HOBBES,CALVIN \\\n \t\tcommit --allow-empty -m without 2>err &&\n-\t! grep Calvin err\n+\ttest_grep ! Calvin err\n '\n \n test_done\ndiff --git a/t/t0030-stripspace.sh b/t/t0030-stripspace.sh\nindex 43155f6bd8..d6e2f63c59 100755\n--- a/t/t0030-stripspace.sh\n+++ b/t/t0030-stripspace.sh\n@@ -407,12 +407,12 @@ test_expect_success 'strip comments with changed comment string' '\n \n test_expect_success 'newline as commentchar is forbidden' '\n \ttest_must_fail git -c core.commentChar=\"$LF\" stripspace -s 2>err &&\n-\tgrep \"core.commentchar cannot contain newline\" err\n+\ttest_grep \"core.commentchar cannot contain newline\" err\n '\n \n test_expect_success 'empty commentchar is forbidden' '\n \ttest_must_fail git -c core.commentchar= stripspace -s 2>err &&\n-\tgrep \"core.commentchar must have at least one character\" err\n+\ttest_grep \"core.commentchar must have at least one character\" err\n '\n \n test_expect_success '-c with single line' '\ndiff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh\nindex 8ef87addf5..e9e2f04049 100755\n--- a/t/t0031-lockfile-pid.sh\n+++ b/t/t0031-lockfile-pid.sh\n@@ -29,7 +29,7 @@ test_expect_success 'PID info not shown by default' '\n \t\ttest_must_fail git add . 2>err &&\n \t\t# Should not crash, just show normal error without PID\n \t\ttest_grep \"Unable to create\" err &&\n-\t\t! test_grep \"is held by process\" err\n+\t\ttest_grep ! \"is held by process\" err\n \t)\n '\n \ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex ca55ea8228..a8348a851c 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -324,13 +324,13 @@ test_expect_success 'non ambiguous option (after two options it abbreviates)' '\n \n test_expect_success 'Alias options do not contribute to abbreviation' '\n \ttest-tool parse-options --alias-source 123 >output &&\n-\tgrep \"^string: 123\" output &&\n+\ttest_grep \"^string: 123\" output &&\n \ttest-tool parse-options --alias-target 123 >output &&\n-\tgrep \"^string: 123\" output &&\n+\ttest_grep \"^string: 123\" output &&\n \ttest_must_fail test-tool parse-options --alias &&\n \tGIT_TEST_DISALLOW_ABBREVIATED_OPTIONS=false \\\n \ttest-tool parse-options --alias 123 >output &&\n-\tgrep \"^string: 123\" output\n+\ttest_grep \"^string: 123\" output\n '\n \n cat >typo.err <<\\EOF\n@@ -582,16 +582,16 @@ test_expect_success 'KEEP_UNKNOWN_OPT works' '\n \n test_expect_success 'NO_INTERNAL_HELP works for -h' '\n \ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&\n-\tgrep \"^error: unknown switch \\`h$SQ\" err &&\n-\tgrep \"^usage: \" err\n+\ttest_grep \"^error: unknown switch \\`h$SQ\" err &&\n+\ttest_grep \"^usage: \" err\n '\n \n for help_opt in help help-all\n do\n \ttest_expect_success \"NO_INTERNAL_HELP works for --$help_opt\" \"\n \t\ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd --$help_opt 2>err &&\n-\t\tgrep '^error: unknown option \\`'$help_opt\\' err &&\n-\t\tgrep '^usage: ' err\n+\t\ttest_grep '^error: unknown option \\`'$help_opt\\' err &&\n+\t\ttest_grep '^usage: ' err\n \t\"\n done\n \n@@ -608,38 +608,38 @@ test_expect_success 'KEEP_UNKNOWN_OPT | NO_INTERNAL_HELP works' '\n \n test_expect_success 'subcommand - no subcommand shows error and usage' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd 2>err &&\n-\tgrep \"^error: need a subcommand\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: need a subcommand\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - subcommand after -- shows error and usage' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd -- subcmd-one 2>err &&\n-\tgrep \"^error: need a subcommand\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: need a subcommand\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - subcommand after --end-of-options shows error and usage' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd --end-of-options subcmd-one 2>err &&\n-\tgrep \"^error: need a subcommand\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: need a subcommand\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - unknown subcommand shows error and usage' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd nope 2>err &&\n-\tgrep \"^error: unknown subcommand: \\`nope$SQ\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: unknown subcommand: \\`nope$SQ\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - subcommands cannot be abbreviated' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd subcmd-o 2>err &&\n-\tgrep \"^error: unknown subcommand: \\`subcmd-o$SQ$\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: unknown subcommand: \\`subcmd-o$SQ$\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - no negated subcommands' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd no-subcmd-one 2>err &&\n-\tgrep \"^error: unknown subcommand: \\`no-subcmd-one$SQ\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: unknown subcommand: \\`no-subcmd-one$SQ\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - simple' '\n@@ -709,8 +709,8 @@ test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given + u\n \n test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given + unknown option' '\n \ttest_expect_code 129 test-tool parse-subcommand --subcommand-optional cmd --subcommand-opt 2>err &&\n-\tgrep \"^error: unknown option\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: unknown option\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT + subcommand not given + unknown option' '\n@@ -778,28 +778,28 @@ test_expect_success 'subcommand - completion helper' '\n \n test_expect_success 'subcommands are incompatible with STOP_AT_NON_OPTION' '\n \ttest_must_fail test-tool parse-subcommand --stop-at-non-option cmd subcmd-one 2>err &&\n-\tgrep ^BUG err\n+\ttest_grep ^BUG err\n '\n \n test_expect_success 'subcommands are incompatible with KEEP_UNKNOWN_OPT unless in combination with SUBCOMMAND_OPTIONAL' '\n \ttest_must_fail test-tool parse-subcommand --keep-unknown-opt cmd subcmd-two 2>err &&\n-\tgrep ^BUG err\n+\ttest_grep ^BUG err\n '\n \n test_expect_success 'subcommands are incompatible with KEEP_DASHDASH unless in combination with SUBCOMMAND_OPTIONAL' '\n \ttest_must_fail test-tool parse-subcommand --keep-dashdash cmd subcmd-two 2>err &&\n-\tgrep ^BUG err\n+\ttest_grep ^BUG err\n '\n \n test_expect_success 'negative unsigned' '\n \ttest_must_fail test-tool parse-options --unsigned -1 >out 2>err &&\n-\tgrep \"non-negative integer\" err &&\n+\ttest_grep \"non-negative integer\" err &&\n \ttest_must_be_empty out\n '\n \n test_expect_success 'unsigned with units but no numbers' '\n \ttest_must_fail test-tool parse-options --unsigned m >out 2>err &&\n-\tgrep \"non-negative integer\" err &&\n+\ttest_grep \"non-negative integer\" err &&\n \ttest_must_be_empty out\n '\n \ndiff --git a/t/t0041-usage.sh b/t/t0041-usage.sh\nindex a0f6f134c7..51af7cc030 100755\n--- a/t/t0041-usage.sh\n+++ b/t/t0041-usage.sh\n@@ -13,7 +13,7 @@ test_expect_success 'setup ' '\n \n test_expect_success 'tag --contains <existent_tag>' '\n \tgit tag --contains \"v1.0\" >actual 2>actual.err &&\n-\tgrep \"v1.0\" actual &&\n+\ttest_grep \"v1.0\" actual &&\n \ttest_line_count = 0 actual.err\n '\n \ndiff --git a/t/t0052-simple-ipc.sh b/t/t0052-simple-ipc.sh\nindex ff98be31a5..14cea84920 100755\n--- a/t/t0052-simple-ipc.sh\n+++ b/t/t0052-simple-ipc.sh\n@@ -33,19 +33,19 @@ test_expect_success 'servers cannot share the same path' '\n test_expect_success 'big response' '\n \ttest-tool simple-ipc send --token=big >actual &&\n \ttest_line_count -ge 10000 actual &&\n-\tgrep -q \"big: [0]*9999\\$\" actual\n+\ttest_grep -q \"big: [0]*9999\\$\" actual\n '\n \n test_expect_success 'chunk response' '\n \ttest-tool simple-ipc send --token=chunk >actual &&\n \ttest_line_count -ge 10000 actual &&\n-\tgrep -q \"big: [0]*9999\\$\" actual\n+\ttest_grep -q \"big: [0]*9999\\$\" actual\n '\n \n test_expect_success 'slow response' '\n \ttest-tool simple-ipc send --token=slow >actual &&\n \ttest_line_count -ge 100 actual &&\n-\tgrep -q \"big: [0]*99\\$\" actual\n+\ttest_grep -q \"big: [0]*99\\$\" actual\n '\n \n # Send an IPC with n=100,000 bytes of ballast.  This should be large enough\n@@ -54,7 +54,7 @@ test_expect_success 'slow response' '\n #\n test_expect_success 'sendbytes' '\n \ttest-tool simple-ipc sendbytes --bytecount=100000 --byte=A >actual &&\n-\tgrep \"sent:A00100000 rcvd:A00100000\" actual\n+\ttest_grep \"sent:A00100000 rcvd:A00100000\" actual\n '\n \n # Start a series of <threads> client threads that each make <batchsize>\n@@ -93,7 +93,7 @@ test_expect_success 'stress test threads' '\n \t\t--batchsize=13 \\\n \t\t>actual &&\n \ttest_line_count = 92 actual &&\n-\tgrep \"good 91\" actual &&\n+\ttest_grep \"good 91\" actual &&\n \tgrep \"sent:A\" <actual >actual_a &&\n \tcat >expect_a <<-EOF &&\n \t\tsent:A00000019 rcvd:A00000019\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 60cfe65979..dc9d77b0e1 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -97,7 +97,7 @@ test_expect_success POSIXPERM 'run_command reports EACCES' '\n \tchmod -x hello.sh &&\n \ttest_must_fail test-tool run-command run-command ./hello.sh 2>err &&\n \n-\tgrep \"fatal: cannot exec.*hello.sh\" err\n+\ttest_grep \"fatal: cannot exec.*hello.sh\" err\n '\n \n test_expect_success POSIXPERM,SANITY 'unreadable directory in PATH' '\ndiff --git a/t/t0066-dir-iterator.sh b/t/t0066-dir-iterator.sh\nindex df3e9f5fa5..9fbb41bac5 100755\n--- a/t/t0066-dir-iterator.sh\n+++ b/t/t0066-dir-iterator.sh\n@@ -127,7 +127,7 @@ test_expect_success SYMLINKS 'dir-iterator should not follow symlinks by default\n test_expect_success SYMLINKS 'dir-iterator does not resolve top-level symlinks' '\n \ttest_must_fail test-tool dir-iterator ./dir5 >out &&\n \n-\tgrep \"ENOTDIR\" out\n+\ttest_grep \"ENOTDIR\" out\n '\n \n test_done\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 80b163ea99..55b37d895a 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -21,23 +21,23 @@ test_expect_success 'run based on configured value' '\n \n \tgit for-each-repo --config=run.key commit --allow-empty -m \"ran\" &&\n \tgit -C one log -1 --pretty=format:%s >message &&\n-\tgrep ran message &&\n+\ttest_grep ran message &&\n \tgit -C two log -1 --pretty=format:%s >message &&\n-\t! grep ran message &&\n+\ttest_grep ! ran message &&\n \tgit -C three log -1 --pretty=format:%s >message &&\n-\tgrep ran message &&\n+\ttest_grep ran message &&\n \tgit -C ~/four log -1 --pretty=format:%s >message &&\n-\tgrep ran message &&\n+\ttest_grep ran message &&\n \n \tgit for-each-repo --config=run.key -- commit --allow-empty -m \"ran again\" &&\n \tgit -C one log -1 --pretty=format:%s >message &&\n-\tgrep again message &&\n+\ttest_grep again message &&\n \tgit -C two log -1 --pretty=format:%s >message &&\n-\t! grep again message &&\n+\ttest_grep ! again message &&\n \tgit -C three log -1 --pretty=format:%s >message &&\n-\tgrep again message &&\n+\ttest_grep again message &&\n \tgit -C ~/four log -1 --pretty=format:%s >message &&\n-\tgrep again message &&\n+\ttest_grep again message &&\n \n \tgit -C three for-each-repo --config=run.key -- \\\n \t\tcommit --allow-empty -m \"ran from worktree\" &&\ndiff --git a/t/t0070-fundamental.sh b/t/t0070-fundamental.sh\nindex 6b9dcf984b..8f573c2a0e 100755\n--- a/t/t0070-fundamental.sh\n+++ b/t/t0070-fundamental.sh\n@@ -10,7 +10,7 @@ Verify wrappers and compatibility functions.\n \n test_expect_success 'mktemp to nonexistent directory prints filename' '\n \ttest_must_fail test-tool mktemp doesnotexist/testXXXXXX 2>err &&\n-\tgrep \"doesnotexist/test\" err\n+\ttest_grep \"doesnotexist/test\" err\n '\n \n test_expect_success POSIXPERM,SANITY 'mktemp to unwritable directory prints filename' '\n@@ -18,7 +18,7 @@ test_expect_success POSIXPERM,SANITY 'mktemp to unwritable directory prints file\n \ttest_when_finished \"chmod +w cannotwrite\" &&\n \tchmod -w cannotwrite &&\n \ttest_must_fail test-tool mktemp cannotwrite/testXXXXXX 2>err &&\n-\tgrep \"cannotwrite/test\" err\n+\ttest_grep \"cannotwrite/test\" err\n '\n \n test_expect_success 'git_mkstemps_mode does not fail if fd 0 is not open' '\n@@ -33,7 +33,7 @@ test_expect_success 'check for a bug in the regex routines' '\n test_expect_success 'incomplete sideband messages are reassembled' '\n \ttest-tool pkt-line send-split-sideband >split-sideband &&\n \ttest-tool pkt-line receive-sideband <split-sideband 2>err &&\n-\tgrep \"Hello, world\" err\n+\ttest_grep \"Hello, world\" err\n '\n \n test_expect_success 'eof on sideband message is reported' '\ndiff --git a/t/t0081-find-pack.sh b/t/t0081-find-pack.sh\nindex 26f017422d..ff9d56fbf1 100755\n--- a/t/t0081-find-pack.sh\n+++ b/t/t0081-find-pack.sh\n@@ -52,17 +52,17 @@ test_expect_success 'add more packfiles' '\n \n \t# HEAD^{tree} is in 2 packfiles\n \ttest-tool find-pack HEAD^{tree} >head_tree_packs &&\n-\tgrep \"$head_commit_pack\" head_tree_packs &&\n-\tgrep mypackname1 head_tree_packs &&\n-\t! grep mypackname2 head_tree_packs &&\n+\ttest_grep \"$head_commit_pack\" head_tree_packs &&\n+\ttest_grep mypackname1 head_tree_packs &&\n+\ttest_grep ! mypackname2 head_tree_packs &&\n \ttest-tool find-pack --check-count 2 HEAD^{tree} &&\n \t! test-tool find-pack --check-count 1 HEAD^{tree} &&\n \n \t# HEAD:five.t is also in 2 packfiles\n \ttest-tool find-pack HEAD:five.t >five_packs &&\n-\tgrep \"$head_commit_pack\" five_packs &&\n-\t! grep mypackname1 five_packs &&\n-\tgrep mypackname2 five_packs &&\n+\ttest_grep \"$head_commit_pack\" five_packs &&\n+\ttest_grep ! mypackname1 five_packs &&\n+\ttest_grep mypackname2 five_packs &&\n \ttest-tool find-pack -c 2 HEAD:five.t &&\n \t! test-tool find-pack --check-count=0 HEAD:five.t\n '\ndiff --git a/t/t0091-bugreport.sh b/t/t0091-bugreport.sh\nindex e38ca7a901..81ed41742c 100755\n--- a/t/t0091-bugreport.sh\n+++ b/t/t0091-bugreport.sh\n@@ -40,15 +40,15 @@ test_expect_success 'sanity check \"System Info\" section' '\n \n \t# The beginning should match \"git version --build-options\" verbatim,\n \t# but rather than checking bit-for-bit equality, just test some basics.\n-\tgrep \"git version \" system &&\n-\tgrep \"shell-path: .\" system &&\n+\ttest_grep \"git version \" system &&\n+\ttest_grep \"shell-path: .\" system &&\n \n \t# After the version, there should be some more info.\n \t# This is bound to differ from environment to environment,\n \t# so we just do some rather high-level checks.\n-\tgrep \"uname: .\" system &&\n-\tgrep \"compiler info: .\" system &&\n-\tgrep \"zlib.\" system\n+\ttest_grep \"uname: .\" system &&\n+\ttest_grep \"compiler info: .\" system &&\n+\ttest_grep \"zlib.\" system\n '\n \n test_expect_success 'dies if file with same name as report already exists' '\n@@ -112,7 +112,7 @@ test_expect_success UNZIP '--diagnose creates diagnostics zip archive' '\n \tgit bugreport --diagnose -o report -s test >out &&\n \n \tzip_path=report/git-diagnostics-test.zip &&\n-\tgrep \"Available space\" out &&\n+\ttest_grep \"Available space\" out &&\n \ttest_path_is_file \"$zip_path\" &&\n \n \t# Check zipped archive content\n@@ -120,10 +120,10 @@ test_expect_success UNZIP '--diagnose creates diagnostics zip archive' '\n \ttest_file_not_empty out &&\n \n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t\"$GIT_UNZIP\" -p \"$zip_path\" objects-local.txt >out &&\n-\tgrep \"^Total: [0-9][0-9]*\" out &&\n+\ttest_grep \"^Total: [0-9][0-9]*\" out &&\n \n \t# Should not include .git directory contents by default\n \t! \"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\"\n@@ -136,7 +136,7 @@ test_expect_success UNZIP '--diagnose=stats excludes .git dir contents' '\n \n \t# Includes pack quantity/size info\n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t# Does not include .git directory contents\n \t! \"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\"\ndiff --git a/t/t0092-diagnose.sh b/t/t0092-diagnose.sh\nindex 6cabd6e67b..0bc2b1ed5c 100755\n--- a/t/t0092-diagnose.sh\n+++ b/t/t0092-diagnose.sh\n@@ -8,7 +8,7 @@ test_expect_success UNZIP 'creates diagnostics zip archive' '\n \ttest_when_finished rm -rf report &&\n \n \tgit diagnose -o report -s test >out &&\n-\tgrep \"Available space\" out &&\n+\ttest_grep \"Available space\" out &&\n \n \tzip_path=report/git-diagnostics-test.zip &&\n \ttest_path_is_file \"$zip_path\" &&\n@@ -18,10 +18,10 @@ test_expect_success UNZIP 'creates diagnostics zip archive' '\n \ttest_file_not_empty out &&\n \n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t\"$GIT_UNZIP\" -p \"$zip_path\" objects-local.txt >out &&\n-\tgrep \"^Total: [0-9][0-9]*\" out &&\n+\ttest_grep \"^Total: [0-9][0-9]*\" out &&\n \n \t# Should not include .git directory contents by default\n \t! \"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\"\n@@ -34,7 +34,7 @@ test_expect_success UNZIP 'counts loose objects' '\n \tgit diagnose -o test-count -s 1 >out &&\n \tzip_path=test-count/git-diagnostics-1.zip &&\n \t\"$GIT_UNZIP\" -p \"$zip_path\" objects-local.txt >out &&\n-\tgrep \"^Total: [1-9][0-9]* loose objects\" out\n+\ttest_grep \"^Total: [1-9][0-9]* loose objects\" out\n '\n \n test_expect_success UNZIP '--mode=stats excludes .git dir contents' '\n@@ -45,7 +45,7 @@ test_expect_success UNZIP '--mode=stats excludes .git dir contents' '\n \t# Includes pack quantity/size info\n \tzip_path=report/git-diagnostics-test.zip &&\n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t# Does not include .git directory contents\n \t! \"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\"\n@@ -59,7 +59,7 @@ test_expect_success UNZIP '--mode=all includes .git dir contents' '\n \t# Includes pack quantity/size info\n \tzip_path=report/git-diagnostics-test.zip &&\n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t# Includes .git directory contents\n \t\"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\" &&\ndiff --git a/t/t0100-previous.sh b/t/t0100-previous.sh\nindex dd5d9b4e5e..87a9995ea4 100755\n--- a/t/t0100-previous.sh\n+++ b/t/t0100-previous.sh\n@@ -50,7 +50,7 @@ test_expect_success 'merge @{-1}~1' '\n \tgit checkout main &&\n \tgit merge @{-1}~1 &&\n \tgit cat-file commit HEAD >actual &&\n-\tgrep \"Merge branch '\\''other'\\''\" actual\n+\ttest_grep \"Merge branch '\\''other'\\''\" actual\n '\n \n test_expect_success 'merge @{-100} before checking out that many branches yet' '\ndiff --git a/t/t0200-gettext-basic.sh b/t/t0200-gettext-basic.sh\nindex 8853d8afb9..8db26c1ada 100755\n--- a/t/t0200-gettext-basic.sh\n+++ b/t/t0200-gettext-basic.sh\n@@ -16,7 +16,7 @@ test_expect_success 'sanity: $TEXTDOMAIN is git' '\n '\n \n test_expect_success 'xgettext sanity: Perl _() strings are not extracted' '\n-    ! grep \"A Perl string xgettext will not get\" \"$GIT_PO_PATH\"/is.po\n+    test_grep ! \"A Perl string xgettext will not get\" \"$GIT_PO_PATH\"/is.po\n '\n \n test_expect_success 'xgettext sanity: Comment extraction with --add-comments' '\n@@ -26,8 +26,8 @@ test_expect_success 'xgettext sanity: Comment extraction with --add-comments' '\n '\n \n test_expect_success 'xgettext sanity: Comment extraction with --add-comments stops at statements' '\n-    ! grep \"This is a phony\" \"$GIT_PO_PATH\"/is.po &&\n-    ! grep \"the above comment\" \"$GIT_PO_PATH\"/is.po\n+    test_grep ! \"This is a phony\" \"$GIT_PO_PATH\"/is.po &&\n+    test_grep ! \"the above comment\" \"$GIT_PO_PATH\"/is.po\n '\n \n test_expect_success GETTEXT 'sanity: $TEXTDOMAINDIR exists without NO_GETTEXT=YesPlease' '\n@@ -44,10 +44,10 @@ test_expect_success GETTEXT 'sanity: Icelandic locale was compiled' '\n test_expect_success GETTEXT_LOCALE 'sanity: gettext(\"\") metadata is OK' '\n     # Return value may be non-zero\n     LANGUAGE=is LC_ALL=\"$is_IS_locale\" gettext \"\" >zero-expect &&\n-    grep \"Project-Id-Version: Git\" zero-expect &&\n-    grep \"Git Mailing List <git@vger.kernel.org>\" zero-expect &&\n-    grep \"Content-Type: text/plain; charset=UTF-8\" zero-expect &&\n-    grep \"Content-Transfer-Encoding: 8bit\" zero-expect\n+    test_grep \"Project-Id-Version: Git\" zero-expect &&\n+    test_grep \"Git Mailing List <git@vger.kernel.org>\" zero-expect &&\n+    test_grep \"Content-Type: text/plain; charset=UTF-8\" zero-expect &&\n+    test_grep \"Content-Transfer-Encoding: 8bit\" zero-expect\n '\n \n test_expect_success GETTEXT_LOCALE 'sanity: gettext(unknown) is passed through' '\ndiff --git a/t/t0203-gettext-setlocale-sanity.sh b/t/t0203-gettext-setlocale-sanity.sh\nindex 0ce1f22eff..670082bf09 100755\n--- a/t/t0203-gettext-setlocale-sanity.sh\n+++ b/t/t0203-gettext-setlocale-sanity.sh\n@@ -12,7 +12,7 @@ test_expect_success 'git show a ISO-8859-1 commit under C locale' '\n \ttest_commit \"iso-c-commit\" iso-under-c &&\n \tgit show >out 2>err &&\n \ttest_must_be_empty err &&\n-\tgrep -q \"iso-c-commit\" out\n+\ttest_grep -q \"iso-c-commit\" out\n '\n \n test_expect_success GETTEXT_LOCALE 'git show a ISO-8859-1 commit under a UTF-8 locale' '\n@@ -20,7 +20,7 @@ test_expect_success GETTEXT_LOCALE 'git show a ISO-8859-1 commit under a UTF-8 l\n \ttest_commit \"iso-utf8-commit\" iso-under-utf8 &&\n \tLANGUAGE=is LC_ALL=\"$is_IS_locale\" git show >out 2>err &&\n \ttest_must_be_empty err &&\n-\tgrep -q \"iso-utf8-commit\" out\n+\ttest_grep -q \"iso-utf8-commit\" out\n '\n \n test_done\ndiff --git a/t/t0204-gettext-reencode-sanity.sh b/t/t0204-gettext-reencode-sanity.sh\nindex 28d92bb9b7..48ccddaf7d 100755\n--- a/t/t0204-gettext-reencode-sanity.sh\n+++ b/t/t0204-gettext-reencode-sanity.sh\n@@ -66,22 +66,22 @@ test_expect_success GETTEXT_LOCALE 'gettext: Fetching a UTF-8 msgid -> UTF-8' '\n # eyes.\n test_expect_success GETTEXT_ISO_LOCALE 'gettext: Fetching a UTF-8 msgid -> ISO-8859-1' '\n     LANGUAGE=is LC_ALL=\"$is_IS_iso_locale\" gettext \"TEST: ‘single’ and “double” quotes\" >actual &&\n-    grep \"einfaldar\" actual &&\n-    grep \"$(echo tvöfaldar | iconv -f UTF-8 -t ISO8859-1)\" actual\n+    test_grep \"einfaldar\" actual &&\n+    test_grep \"$(echo tvöfaldar | iconv -f UTF-8 -t ISO8859-1)\" actual\n '\n \n test_expect_success GETTEXT_LOCALE 'gettext.c: git init UTF-8 -> UTF-8' '\n     printf \"Bjó til tóma Git lind\" >expect &&\n     LANGUAGE=is LC_ALL=\"$is_IS_locale\" git init repo >actual &&\n     test_when_finished \"rm -rf repo\" &&\n-    grep \"^$(cat expect) \" actual\n+    test_grep \"^$(cat expect) \" actual\n '\n \n test_expect_success GETTEXT_ISO_LOCALE 'gettext.c: git init UTF-8 -> ISO-8859-1' '\n     printf \"Bjó til tóma Git lind\" >expect &&\n     LANGUAGE=is LC_ALL=\"$is_IS_iso_locale\" git init repo >actual &&\n     test_when_finished \"rm -rf repo\" &&\n-    grep \"^$(iconv -f UTF-8 -t ISO8859-1 <expect) \" actual\n+    test_grep \"^$(iconv -f UTF-8 -t ISO8859-1 <expect) \" actual\n '\n \n test_done\ndiff --git a/t/t0210-trace2-normal.sh b/t/t0210-trace2-normal.sh\nindex 7e1e7af862..beed2b27a5 100755\n--- a/t/t0210-trace2-normal.sh\n+++ b/t/t0210-trace2-normal.sh\n@@ -333,12 +333,12 @@ test_expect_success 'unsafe URLs are redacted by default' '\n \n \tGIT_TRACE2=\"$(pwd)/trace.normal\" \\\n \t\tgit clone https://user:pwd@example.com/ clone &&\n-\t! grep user:pwd trace.normal &&\n+\ttest_grep ! user:pwd trace.normal &&\n \n \tGIT_TRACE2_REDACT=0 GIT_TRACE2=\"$(pwd)/unredacted.normal\" \\\n \t\tgit clone https://user:pwd@example.com/ clone2 &&\n-\tgrep \"start .* clone https://user:pwd@example.com\" unredacted.normal &&\n-\tgrep \"remote.origin.url=https://user:pwd@example.com\" unredacted.normal\n+\ttest_grep \"start .* clone https://user:pwd@example.com\" unredacted.normal &&\n+\ttest_grep \"remote.origin.url=https://user:pwd@example.com\" unredacted.normal\n '\n \n test_done\ndiff --git a/t/t0211-trace2-perf.sh b/t/t0211-trace2-perf.sh\nindex 760cf69087..665c8b3669 100755\n--- a/t/t0211-trace2-perf.sh\n+++ b/t/t0211-trace2-perf.sh\n@@ -283,13 +283,13 @@ test_expect_success 'unsafe URLs are redacted by default' '\n \n \tGIT_TRACE2_PERF=\"$(pwd)/trace.perf\" \\\n \t\tgit clone https://user:pwd@example.com/ clone &&\n-\t! grep user:pwd trace.perf &&\n+\ttest_grep ! user:pwd trace.perf &&\n \n \tGIT_TRACE2_REDACT=0 GIT_TRACE2_PERF=\"$(pwd)/unredacted.perf\" \\\n \t\tgit clone https://user:pwd@example.com/ clone2 &&\n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <unredacted.perf >actual &&\n-\tgrep \"d0|main|start|.* clone https://user:pwd@example.com\" actual &&\n-\tgrep \"d0|main|def_param|.*|remote.origin.url:https://user:pwd@example.com\" actual\n+\ttest_grep \"d0|main|start|.* clone https://user:pwd@example.com\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|remote.origin.url:https://user:pwd@example.com\" actual\n '\n \n # Confirm that the requested command produces a \"cmd_name\" and a\n@@ -358,13 +358,13 @@ test_expect_success LIBCURL \\\n \n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <prop.perf >actual &&\n \n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_\" actual &&\n-\tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n-\tgrep \"d1|main|cmd_name|.*|remote-curl\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|remote-curl\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n # Similarly, `git-http-fetch` is not built from git.c so do a\n@@ -389,13 +389,13 @@ test_expect_success LIBCURL \\\n \n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <prop.perf >actual &&\n \n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_\" actual &&\n-\tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n-\tgrep \"d1|main|cmd_name|.*|http-fetch\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|http-fetch\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n # Historically, alias expansion explicitly emitted the def_param\n@@ -421,22 +421,22 @@ test_expect_success 'expect def_params during git alias expansion' '\n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <prop.perf >actual &&\n \n \t# \"git xxx\" is first mapped to \"git-xxx\" and the child will fail.\n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n \n \t# We unpeel that and substitute \"version\" into \"xxx\" (giving\n \t# \"git version\") and update the cmd_name event.\n-\tgrep \"d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_git_alias_)\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_git_alias_)\" actual &&\n \n \t# These def_param events could be associated with either of the\n \t# above cmd_name events.  It does not matter.\n-\tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n \t# The \"git version\" child sees a different cmd_name hierarchy.\n \t# Also test the def_param (only for completeness).\n-\tgrep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_git_alias_/version)\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_git_alias_/version)\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n test_expect_success 'expect def_params during shell alias expansion' '\n@@ -456,25 +456,25 @@ test_expect_success 'expect def_params during shell alias expansion' '\n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <prop.perf >actual &&\n \n \t# \"git xxx\" is first mapped to \"git-xxx\" and the child will fail.\n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n \n \t# We unpeel that and substitute \"git version\" for \"git xxx\" (as a\n \t# shell command.  Another cmd_name event is emitted as we unpeel.\n-\tgrep \"d0|main|cmd_name|.*|_run_shell_alias_ (_run_dashed_/_run_shell_alias_)\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_shell_alias_ (_run_dashed_/_run_shell_alias_)\" actual &&\n \n \t# These def_param events could be associated with either of the\n \t# above cmd_name events.  It does not matter.\n-\tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n \t# We get the following only because we used a git command for the\n \t# shell command. In general, it could have been a shell script and\n \t# we would see nothing.\n \t#\n \t# The child knows the cmd_name hierarchy so it includes it.\n-\tgrep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_shell_alias_/version)\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_shell_alias_/version)\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n test_expect_success 'expect def_params during nested git alias expansion' '\n@@ -496,33 +496,33 @@ test_expect_success 'expect def_params during nested git alias expansion' '\n \n \t# \"git xxx\" is first mapped to \"git-xxx\" and try to spawn \"git-xxx\"\n \t# and the child will fail.\n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n-\tgrep \"d0|main|child_start|.*|.* class:dashed argv:\\[git-xxx\\]\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n+\ttest_grep \"d0|main|child_start|.*|.* class:dashed argv:\\[git-xxx\\]\" actual &&\n \n \t# We unpeel that and substitute \"yyy\" into \"xxx\" (giving \"git yyy\")\n \t# and spawn \"git-yyy\" and the child will fail.\n-\tgrep \"d0|main|alias|.*|alias:xxx argv:\\[yyy\\]\" actual &&\n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_/_run_dashed_)\" actual &&\n-\tgrep \"d0|main|child_start|.*|.* class:dashed argv:\\[git-yyy\\]\" actual &&\n+\ttest_grep \"d0|main|alias|.*|alias:xxx argv:\\[yyy\\]\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_/_run_dashed_)\" actual &&\n+\ttest_grep \"d0|main|child_start|.*|.* class:dashed argv:\\[git-yyy\\]\" actual &&\n \n \t# We unpeel that and substitute \"version\" into \"xxx\" (giving\n \t# \"git version\") and update the cmd_name event.\n-\tgrep \"d0|main|alias|.*|alias:yyy argv:\\[version\\]\" actual &&\n-\tgrep \"d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_dashed_/_run_git_alias_)\" actual &&\n+\ttest_grep \"d0|main|alias|.*|alias:yyy argv:\\[version\\]\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_dashed_/_run_git_alias_)\" actual &&\n \n \t# These def_param events could be associated with any of the\n \t# above cmd_name events.  It does not matter.\n \tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual >actual.matches &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n \t# However, we do not want them repeated each time we unpeel.\n \ttest_line_count = 1 actual.matches &&\n \n \t# The \"git version\" child sees a different cmd_name hierarchy.\n \t# Also test the def_param (only for completeness).\n-\tgrep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_dashed_/_run_git_alias_/version)\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_dashed_/_run_git_alias_/version)\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n test_done\ndiff --git a/t/t0212-trace2-event.sh b/t/t0212-trace2-event.sh\nindex 1211db9f46..f5358a1dd4 100755\n--- a/t/t0212-trace2-event.sh\n+++ b/t/t0212-trace2-event.sh\n@@ -332,7 +332,7 @@ test_expect_success 'unsafe URLs are redacted by default in cmd_start events' '\n \n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.event\" \\\n \t\ttest-tool trace2 300redact_start git clone https://user:pwd@example.com/ clone2 &&\n-\t! grep user:pwd trace.event\n+\ttest_grep ! user:pwd trace.event\n '\n \n test_expect_success 'unsafe URLs are redacted by default in child_start events' '\n@@ -341,7 +341,7 @@ test_expect_success 'unsafe URLs are redacted by default in child_start events'\n \n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.event\" \\\n \t\ttest-tool trace2 301redact_child_start git clone https://user:pwd@example.com/ clone2 &&\n-\t! grep user:pwd trace.event\n+\ttest_grep ! user:pwd trace.event\n '\n \n test_expect_success 'unsafe URLs are redacted by default in exec events' '\n@@ -350,7 +350,7 @@ test_expect_success 'unsafe URLs are redacted by default in exec events' '\n \n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.event\" \\\n \t\ttest-tool trace2 302redact_exec git clone https://user:pwd@example.com/ clone2 &&\n-\t! grep user:pwd trace.event\n+\ttest_grep ! user:pwd trace.event\n '\n \n test_expect_success 'unsafe URLs are redacted by default in def_param events' '\n@@ -359,7 +359,7 @@ test_expect_success 'unsafe URLs are redacted by default in def_param events' '\n \n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.event\" \\\n \t\ttest-tool trace2 303redact_def_param url https://user:pwd@example.com/ &&\n-\t! grep user:pwd trace.event\n+\ttest_grep ! user:pwd trace.event\n '\n \n test_done\ndiff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh\nindex 64ead1571a..ea11cdb4dc 100755\n--- a/t/t0300-credentials.sh\n+++ b/t/t0300-credentials.sh\n@@ -1014,7 +1014,7 @@ test_expect_success 'credential config with partial URLs' '\n \tdo\n \t\tgit -c credential.$partial.helper=yep \\\n \t\t\tcredential fill <stdin >stdout &&\n-\t\tgrep yep stdout ||\n+\t\ttest_grep yep stdout ||\n \t\treturn 1\n \tdone &&\n \n@@ -1030,7 +1030,7 @@ test_expect_success 'credential config with partial URLs' '\n \tdo\n \t\tgit -c credential.$partial.helper=yep \\\n \t\t\tcredential fill <stdin >stdout &&\n-\t\t! grep yep stdout ||\n+\t\ttest_grep ! yep stdout ||\n \t\treturn 1\n \tdone &&\n \ndiff --git a/t/t0410-partial-clone.sh b/t/t0410-partial-clone.sh\nindex dff442da20..dba4246100 100755\n--- a/t/t0410-partial-clone.sh\n+++ b/t/t0410-partial-clone.sh\n@@ -206,7 +206,7 @@ test_expect_success 'fetching of missing objects' '\n \tgit -C repo cat-file -p \"$HASH\" 2>err &&\n \n \t# Ensure that no spurious FETCH_HEAD messages are written\n-\t! grep FETCH_HEAD err &&\n+\ttest_grep ! FETCH_HEAD err &&\n \n \t# Ensure that the .promisor file is written, and check that its\n \t# associated packfile contains the object\n@@ -214,7 +214,7 @@ test_expect_success 'fetching of missing objects' '\n \ttest_line_count = 1 promisorlist &&\n \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n \tgit verify-pack --verbose \"$IDX\" >out &&\n-\tgrep \"$HASH\" out\n+\ttest_grep \"$HASH\" out\n '\n \n test_expect_success 'fetching of a promised object that promisor remote no longer has' '\n@@ -228,7 +228,7 @@ test_expect_success 'fetching of a promised object that promisor remote no longe\n \n \trm -rf unreliable-server/.git/objects/* &&\n \ttest_must_fail git -C unreliable-client checkout HEAD 2>err &&\n-\tgrep \"could not fetch.*from promisor remote\" err\n+\ttest_grep \"could not fetch.*from promisor remote\" err\n '\n \n test_expect_success 'fetching of missing objects works with ref-in-want enabled' '\n@@ -240,7 +240,7 @@ test_expect_success 'fetching of missing objects works with ref-in-want enabled'\n \trm -rf repo/.git/objects/* &&\n \trm -f trace &&\n \tGIT_TRACE_PACKET=\"$(pwd)/trace\" git -C repo cat-file -p \"$HASH\" &&\n-\tgrep \"fetch< fetch=.*ref-in-want\" trace\n+\ttest_grep \"fetch< fetch=.*ref-in-want\" trace\n '\n \n test_expect_success 'fetching from another promisor remote' '\n@@ -263,7 +263,7 @@ test_expect_success 'fetching from another promisor remote' '\n \ttest_line_count = 1 promisorlist &&\n \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n \tgit verify-pack --verbose \"$IDX\" >out &&\n-\tgrep \"$HASH2\" out\n+\ttest_grep \"$HASH2\" out\n '\n \n test_expect_success 'fetching with --filter configures a promisor remote' '\n@@ -286,7 +286,7 @@ test_expect_success 'fetching with --filter configures a promisor remote' '\n \ttest_line_count = 1 promisorlist &&\n \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n \tgit verify-pack --verbose \"$IDX\" >out &&\n-\tgrep \"$HASH3\" out\n+\ttest_grep \"$HASH3\" out\n '\n \n test_expect_success 'fetching of missing blobs works' '\n@@ -327,8 +327,8 @@ test_expect_success 'fetching of missing trees does not fetch blobs' '\n \n \t# Ensure that the tree, but not the blob, is fetched\n \tgit -C repo rev-list --objects --missing=print $(cat treehash) >objects &&\n-\tgrep \"^$(cat treehash)\" objects &&\n-\tgrep \"^[?]$(cat blobhash)\" objects\n+\ttest_grep \"^$(cat treehash)\" objects &&\n+\ttest_grep \"^[?]$(cat blobhash)\" objects\n '\n \n test_expect_success 'rev-list stops traversal at missing and promised commit' '\n@@ -343,8 +343,8 @@ test_expect_success 'rev-list stops traversal at missing and promised commit' '\n \tgit -C repo config core.repositoryformatversion 1 &&\n \tgit -C repo config extensions.partialclone \"arbitrary string\" &&\n \tgit -C repo rev-list --exclude-promisor-objects --objects bar >out &&\n-\tgrep $(git -C repo rev-parse bar) out &&\n-\t! grep $FOO out\n+\ttest_grep $(git -C repo rev-parse bar) out &&\n+\ttest_grep ! $FOO out\n '\n \n test_expect_success 'missing tree objects with --missing=allow-promisor and --exclude-promisor-objects' '\n@@ -413,10 +413,10 @@ test_expect_success 'rev-list stops traversal at missing and promised tree' '\n \tgit -C repo config core.repositoryformatversion 1 &&\n \tgit -C repo config extensions.partialclone \"arbitrary string\" &&\n \tgit -C repo rev-list --exclude-promisor-objects --objects HEAD >out &&\n-\tgrep $(git -C repo rev-parse foo) out &&\n-\t! grep $TREE out &&\n-\tgrep $(git -C repo rev-parse HEAD) out &&\n-\t! grep $TREE2 out\n+\ttest_grep $(git -C repo rev-parse foo) out &&\n+\ttest_grep ! $TREE out &&\n+\ttest_grep $(git -C repo rev-parse HEAD) out &&\n+\ttest_grep ! $TREE2 out\n '\n \n test_expect_success 'rev-list stops traversal at missing and promised blob' '\n@@ -432,8 +432,8 @@ test_expect_success 'rev-list stops traversal at missing and promised blob' '\n \tgit -C repo config core.repositoryformatversion 1 &&\n \tgit -C repo config extensions.partialclone \"arbitrary string\" &&\n \tgit -C repo rev-list --exclude-promisor-objects --objects HEAD >out &&\n-\tgrep $(git -C repo rev-parse HEAD) out &&\n-\t! grep $BLOB out\n+\ttest_grep $(git -C repo rev-parse HEAD) out &&\n+\ttest_grep ! $BLOB out\n '\n \n test_expect_success 'rev-list stops traversal at promisor commit, tree, and blob' '\n@@ -451,10 +451,10 @@ test_expect_success 'rev-list stops traversal at promisor commit, tree, and blob\n \tgit -C repo config core.repositoryformatversion 1 &&\n \tgit -C repo config extensions.partialclone \"arbitrary string\" &&\n \tgit -C repo rev-list --exclude-promisor-objects --objects HEAD >out &&\n-\t! grep $COMMIT out &&\n-\t! grep $TREE out &&\n-\t! grep $BLOB out &&\n-\tgrep $(git -C repo rev-parse bar) out  # sanity check that some walking was done\n+\ttest_grep ! $COMMIT out &&\n+\ttest_grep ! $TREE out &&\n+\ttest_grep ! $BLOB out &&\n+\ttest_grep $(git -C repo rev-parse bar) out  # sanity check that some walking was done\n '\n \n test_expect_success 'rev-list dies for missing objects on cmd line' '\n@@ -523,10 +523,10 @@ test_expect_success 'gc repacks promisor objects separately from non-promisor ob\n \ttest_line_count = 1 promisorlist &&\n \tPROMISOR_PACKFILE=$(sed \"s/.promisor/.pack/\" <promisorlist) &&\n \tgit verify-pack $PROMISOR_PACKFILE -v >out &&\n-\tgrep \"$TREE_ONE\" out &&\n-\tgrep \"$TREE_TWO\" out &&\n-\t! grep \"$(git -C repo rev-parse one)\" out &&\n-\t! grep \"$(git -C repo rev-parse two)\" out &&\n+\ttest_grep \"$TREE_ONE\" out &&\n+\ttest_grep \"$TREE_TWO\" out &&\n+\ttest_grep ! \"$(git -C repo rev-parse one)\" out &&\n+\ttest_grep ! \"$(git -C repo rev-parse two)\" out &&\n \n \t# Remove the promisor packfile and associated files\n \trm $(sed \"s/.promisor//\" <promisorlist).* &&\n@@ -536,10 +536,10 @@ test_expect_success 'gc repacks promisor objects separately from non-promisor ob\n \tls repo/.git/objects/pack/pack-*.pack >packlist &&\n \ttest_line_count = 1 packlist &&\n \tgit verify-pack repo/.git/objects/pack/pack-*.pack -v >out &&\n-\tgrep \"$(git -C repo rev-parse one)\" out &&\n-\tgrep \"$(git -C repo rev-parse two)\" out &&\n-\t! grep \"$TREE_ONE\" out &&\n-\t! grep \"$TREE_TWO\" out\n+\ttest_grep \"$(git -C repo rev-parse one)\" out &&\n+\ttest_grep \"$(git -C repo rev-parse two)\" out &&\n+\ttest_grep ! \"$TREE_ONE\" out &&\n+\ttest_grep ! \"$TREE_TWO\" out\n '\n \n test_expect_success 'gc does not repack promisor objects if there are none' '\n@@ -616,8 +616,8 @@ test_expect_success 'gc stops traversal when a missing but promised object is re\n \tls repo/.git/objects/pack/pack-*.pack >packlist &&\n \ttest_line_count = 1 packlist &&\n \tgit verify-pack repo/.git/objects/pack/pack-*.pack -v >out &&\n-\tgrep \"$(git -C repo rev-parse HEAD)\" out &&\n-\t! grep \"$TREE_HASH\" out\n+\ttest_grep \"$(git -C repo rev-parse HEAD)\" out &&\n+\ttest_grep ! \"$TREE_HASH\" out\n '\n \n test_expect_success 'do not fetch when checking existence of tree we construct ourselves' '\n@@ -647,10 +647,10 @@ test_expect_success 'exact rename does not need to fetch the blob lazily' '\n \n \tgit clone --filter=blob:none --bare \"file://$(pwd)/repo\" partial.git &&\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\tgrep \"[?]$FILE_HASH\" out &&\n+\ttest_grep \"[?]$FILE_HASH\" out &&\n \tgit -C partial.git log --follow -- new-file.txt &&\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\tgrep \"[?]$FILE_HASH\" out\n+\ttest_grep \"[?]$FILE_HASH\" out\n '\n \n test_expect_success 'lazy-fetch when accessing object not in the_repository' '\n@@ -665,7 +665,7 @@ test_expect_success 'lazy-fetch when accessing object not in the_repository' '\n \n \t# Sanity check that the file is missing\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\tgrep \"[?]$FILE_HASH\" out &&\n+\ttest_grep \"[?]$FILE_HASH\" out &&\n \n \t# The no-lazy-fetch mechanism prevents Git from fetching\n \ttest_must_fail env GIT_NO_LAZY_FETCH=1 \\\n@@ -680,7 +680,7 @@ test_expect_success 'lazy-fetch when accessing object not in the_repository' '\n \n \t# Sanity check that the file is still missing\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\tgrep \"[?]$FILE_HASH\" out &&\n+\ttest_grep \"[?]$FILE_HASH\" out &&\n \n \tgit -C full cat-file -s \"$FILE_HASH\" >expect &&\n \ttest-tool partial-clone object-info partial.git \"$FILE_HASH\" >actual &&\n@@ -688,7 +688,7 @@ test_expect_success 'lazy-fetch when accessing object not in the_repository' '\n \n \t# Sanity check that the file is now present\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\t! grep \"[?]$FILE_HASH\" out\n+\ttest_grep ! \"[?]$FILE_HASH\" out\n '\n \n test_expect_success 'push should not fetch new commit objects' '\n@@ -705,9 +705,9 @@ test_expect_success 'push should not fetch new commit objects' '\n \tCOMMIT=$(git -C server rev-parse server2) &&\n \n \ttest_must_fail git -C client push 2>err &&\n-\tgrep \"fetch first\" err &&\n+\ttest_grep \"fetch first\" err &&\n \tgit -C client rev-list --objects --missing=print \"$COMMIT\" >objects &&\n-\tgrep \"^[?]$COMMIT\" objects\n+\ttest_grep \"^[?]$COMMIT\" objects\n '\n \n test_expect_success 'setup for promisor.quiet tests' '\n@@ -750,7 +750,7 @@ test_expect_success TTY 'promisor.quiet=false shows progress messages' '\n \ttest_terminal git -C repo cat-file -p foo:foo.t 2>err &&\n \n \t# Ensure that progress messages are written\n-\tgrep \"Receiving objects\" err\n+\ttest_grep \"Receiving objects\" err\n '\n \n test_expect_success TTY 'promisor.quiet=true does not show progress messages' '\n@@ -761,7 +761,7 @@ test_expect_success TTY 'promisor.quiet=true does not show progress messages' '\n \ttest_terminal git -C repo cat-file -p foo:foo.t 2>err &&\n \n \t# Ensure that no progress messages are written\n-\t! grep \"Receiving objects\" err\n+\ttest_grep ! \"Receiving objects\" err\n '\n \n test_expect_success TTY 'promisor.quiet=unconfigured shows progress messages' '\n@@ -771,7 +771,7 @@ test_expect_success TTY 'promisor.quiet=unconfigured shows progress messages' '\n \ttest_terminal git -C repo cat-file -p foo:foo.t 2>err &&\n \n \t# Ensure that progress messages are written\n-\tgrep \"Receiving objects\" err\n+\ttest_grep \"Receiving objects\" err\n '\n \n test_expect_success 'promisor.quiet from submodule repo is honored' '\n@@ -819,7 +819,7 @@ test_expect_success 'fetching of missing objects from an HTTP server' '\n \ttest_line_count = 1 promisorlist &&\n \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n \tgit verify-pack --verbose \"$IDX\" >out &&\n-\tgrep \"$HASH\" out\n+\ttest_grep \"$HASH\" out\n '\n \n # DO NOT add non-httpd-specific tests here, because the last part of this\ndiff --git a/t/t0450-txt-doc-vs-help.sh b/t/t0450-txt-doc-vs-help.sh\nindex 822b0d55a5..e7ba967223 100755\n--- a/t/t0450-txt-doc-vs-help.sh\n+++ b/t/t0450-txt-doc-vs-help.sh\n@@ -87,7 +87,7 @@ do\n \t# -h output assertions\n \ttest_expect_success \"$builtin -h output has no \\t\" '\n \t\th2s=\"$(help_to_synopsis \"$builtin\")\" &&\n-\t\t! grep \"$HT\" \"$h2s\"\n+\t\ttest_grep ! \"$HT\" \"$h2s\"\n \t'\n \n \ttest_expect_success \"$builtin -h output has dashed labels\" '\ndiff --git a/t/t0500-progress-display.sh b/t/t0500-progress-display.sh\nindex d1a498a216..fe2232cce0 100755\n--- a/t/t0500-progress-display.sh\n+++ b/t/t0500-progress-display.sh\n@@ -320,8 +320,8 @@ test_expect_success 'progress generates traces' '\n \n \t# t0212/parse_events.perl intentionally omits regions and data.\n \ttest_region progress \"Working hard\" trace.event &&\n-\tgrep \"\\\"key\\\":\\\"total_objects\\\",\\\"value\\\":\\\"40\\\"\" trace.event &&\n-\tgrep \"\\\"key\\\":\\\"total_bytes\\\",\\\"value\\\":\\\"409600\\\"\" trace.event\n+\ttest_grep \"\\\"key\\\":\\\"total_objects\\\",\\\"value\\\":\\\"40\\\"\" trace.event &&\n+\ttest_grep \"\\\"key\\\":\\\"total_bytes\\\",\\\"value\\\":\\\"409600\\\"\" trace.event\n '\n \n test_expect_success 'progress generates traces: stop / start' '\n@@ -344,8 +344,8 @@ test_expect_success 'progress generates traces: start without stop' '\n \tLSAN_OPTIONS=detect_leaks=0 \\\n \ttest-tool progress \\\n \t\t<in 2>stderr &&\n-\tgrep region_enter.*progress trace-start.event &&\n-\t! grep region_leave.*progress trace-start.event\n+\ttest_grep region_enter.*progress trace-start.event &&\n+\ttest_grep ! region_leave.*progress trace-start.event\n '\n \n test_expect_success 'progress generates traces: stop without start' '\n@@ -355,8 +355,8 @@ test_expect_success 'progress generates traces: stop without start' '\n \n \tGIT_TRACE2_EVENT=\"$PWD/trace-stop.event\" test-tool progress \\\n \t\t<in 2>stderr &&\n-\t! grep region_enter.*progress trace-stop.event &&\n-\t! grep region_leave.*progress trace-stop.event\n+\ttest_grep ! region_enter.*progress trace-stop.event &&\n+\ttest_grep ! region_leave.*progress trace-stop.event\n '\n \n test_expect_success 'progress generates traces: start with active progress bar (no stops)' '\n@@ -369,9 +369,9 @@ test_expect_success 'progress generates traces: start with active progress bar (\n \tLSAN_OPTIONS=detect_leaks=0 \\\n \ttest-tool progress \\\n \t\t<in 2>stderr &&\n-\tgrep region_enter.*progress.*One trace-2start.event &&\n-\tgrep region_enter.*progress.*Two trace-2start.event &&\n-\t! grep region_leave trace-2start.event\n+\ttest_grep region_enter.*progress.*One trace-2start.event &&\n+\ttest_grep region_enter.*progress.*Two trace-2start.event &&\n+\ttest_grep ! region_leave trace-2start.event\n '\n \n test_done\ndiff --git a/t/t0610-reftable-basics.sh b/t/t0610-reftable-basics.sh\nindex e19e036898..2f4c5e2a1b 100755\n--- a/t/t0610-reftable-basics.sh\n+++ b/t/t0610-reftable-basics.sh\n@@ -776,11 +776,11 @@ test_expect_success 'reflog: can delete separate reflog entries' '\n \t\ttest_commit file3 &&\n \t\ttest_commit file4 &&\n \t\tgit reflog >actual &&\n-\t\tgrep file3 actual &&\n+\t\ttest_grep file3 actual &&\n \n \t\tgit reflog delete HEAD@{1} &&\n \t\tgit reflog >actual &&\n-\t\t! grep file3 actual\n+\t\ttest_grep ! file3 actual\n \t)\n '\n \n@@ -902,8 +902,8 @@ test_expect_success 'reflog: garbage collection deletes reflog entries' '\n \t\tdone &&\n \t\tgit reflog refs/heads/main >actual &&\n \t\ttest_line_count = 10 actual &&\n-\t\tgrep \"commit (initial): number 1\" actual &&\n-\t\tgrep \"commit: number 10\" actual &&\n+\t\ttest_grep \"commit (initial): number 1\" actual &&\n+\t\ttest_grep \"commit: number 10\" actual &&\n \n \t\tgit gc &&\n \t\tgit reflog refs/heads/main >actual &&\ndiff --git a/t/t1004-read-tree-m-u-wf.sh b/t/t1004-read-tree-m-u-wf.sh\nindex 11bf10424f..70ba3eea4d 100755\n--- a/t/t1004-read-tree-m-u-wf.sh\n+++ b/t/t1004-read-tree-m-u-wf.sh\n@@ -142,8 +142,8 @@ test_expect_success '3-way not overwriting local changes (our side)' '\n \n \techo >>file1 \"local changes\" &&\n \tread_tree_u_must_succeed -m -u branch-point side-a side-b &&\n-\tgrep \"new line to be kept\" file1 &&\n-\tgrep \"local changes\" file1\n+\ttest_grep \"new line to be kept\" file1 &&\n+\ttest_grep \"local changes\" file1\n \n '\n \n@@ -156,8 +156,8 @@ test_expect_success '3-way not overwriting local changes (their side)' '\n \n \techo >>file2 \"local changes\" &&\n \tread_tree_u_must_fail -m -u branch-point side-a side-b &&\n-\t! grep \"new line to be kept\" file2 &&\n-\tgrep \"local changes\" file2\n+\ttest_grep ! \"new line to be kept\" file2 &&\n+\ttest_grep \"local changes\" file2\n \n '\n \ndiff --git a/t/t1006-cat-file.sh b/t/t1006-cat-file.sh\nindex 8e2c52652c..762c77c351 100755\n--- a/t/t1006-cat-file.sh\n+++ b/t/t1006-cat-file.sh\n@@ -696,8 +696,8 @@ test_expect_success '%(deltabase) reports packed delta bases' '\n \tgit repack -ad &&\n \tgit cat-file --batch-check=\"%(deltabase)\" <blobs >actual &&\n \t{\n-\t\tgrep \"$(git rev-parse HEAD:foo)\" actual ||\n-\t\tgrep \"$(git rev-parse HEAD:foo-plus)\" actual\n+\t\ttest_grep \"$(git rev-parse HEAD:foo)\" actual ||\n+\t\ttest_grep \"$(git rev-parse HEAD:foo-plus)\" actual\n \t}\n '\n \n@@ -826,7 +826,7 @@ test_expect_success 'cat-file -t and -s on corrupt loose object' '\n \t\t# Swap the two to corrupt the repository\n \t\tmv -f \"$other_path\" \"$empty_path\" &&\n \t\ttest_must_fail git fsck 2>err.fsck &&\n-\t\tgrep \"hash-path mismatch\" err.fsck &&\n+\t\ttest_grep \"hash-path mismatch\" err.fsck &&\n \n \t\t# confirm that cat-file is reading the new swapped-in\n \t\t# blob...\n@@ -1318,37 +1318,37 @@ test_expect_success 'cat-file --batch-all-objects --batch-check ignores replace'\n test_expect_success 'batch-command empty command' '\n \techo \"\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*empty command in input.*\" err\n+\ttest_grep \"^fatal:.*empty command in input.*\" err\n '\n \n test_expect_success 'batch-command whitespace before command' '\n \techo \" info deadbeef\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*whitespace before command.*\" err\n+\ttest_grep \"^fatal:.*whitespace before command.*\" err\n '\n \n test_expect_success 'batch-command unknown command' '\n \techo unknown_command >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*unknown command.*\" err\n+\ttest_grep \"^fatal:.*unknown command.*\" err\n '\n \n test_expect_success 'batch-command missing arguments' '\n \techo \"info\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*info requires arguments.*\" err\n+\ttest_grep \"^fatal:.*info requires arguments.*\" err\n '\n \n test_expect_success 'batch-command flush with arguments' '\n \techo \"flush arg\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command --buffer <cmd 2>err &&\n-\tgrep \"^fatal:.*flush takes no arguments.*\" err\n+\ttest_grep \"^fatal:.*flush takes no arguments.*\" err\n '\n \n test_expect_success 'batch-command flush without --buffer' '\n \techo \"flush\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*flush is only for --buffer mode.*\" err\n+\ttest_grep \"^fatal:.*flush is only for --buffer mode.*\" err\n '\n \n perl_script='\ndiff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh\nindex de076293b6..4782a6246a 100755\n--- a/t/t1007-hash-object.sh\n+++ b/t/t1007-hash-object.sh\n@@ -202,7 +202,7 @@ done\n test_expect_success 'too-short tree' '\n \techo abc >malformed-tree &&\n \ttest_must_fail git hash-object -t tree malformed-tree 2>err &&\n-\tgrep \"too-short tree object\" err\n+\ttest_grep \"too-short tree object\" err\n '\n \n test_expect_success PERL_TEST_HELPERS 'malformed mode in tree' '\n@@ -210,7 +210,7 @@ test_expect_success PERL_TEST_HELPERS 'malformed mode in tree' '\n \tbin_oid=$(echo $hex_oid | hex2oct) &&\n \tprintf \"9100644 \\0$bin_oid\" >tree-with-malformed-mode &&\n \ttest_must_fail git hash-object -t tree tree-with-malformed-mode 2>err &&\n-\tgrep \"malformed mode in tree entry\" err\n+\ttest_grep \"malformed mode in tree entry\" err\n '\n \n test_expect_success PERL_TEST_HELPERS 'empty filename in tree' '\n@@ -218,7 +218,7 @@ test_expect_success PERL_TEST_HELPERS 'empty filename in tree' '\n \tbin_oid=$(echo $hex_oid | hex2oct) &&\n \tprintf \"100644 \\0$bin_oid\" >tree-with-empty-filename &&\n \ttest_must_fail git hash-object -t tree tree-with-empty-filename 2>err &&\n-\tgrep \"empty filename in tree entry\" err\n+\ttest_grep \"empty filename in tree entry\" err\n '\n \n test_expect_success PERL_TEST_HELPERS 'duplicate filename in tree' '\n@@ -229,7 +229,7 @@ test_expect_success PERL_TEST_HELPERS 'duplicate filename in tree' '\n \t\tprintf \"100644 file\\0$bin_oid\"\n \t} >tree-with-duplicate-filename &&\n \ttest_must_fail git hash-object -t tree tree-with-duplicate-filename 2>err &&\n-\tgrep \"duplicateEntries\" err\n+\ttest_grep \"duplicateEntries\" err\n '\n \n test_expect_success 'corrupt commit' '\ndiff --git a/t/t1011-read-tree-sparse-checkout.sh b/t/t1011-read-tree-sparse-checkout.sh\nindex 742f0fa909..93244ee134 100755\n--- a/t/t1011-read-tree-sparse-checkout.sh\n+++ b/t/t1011-read-tree-sparse-checkout.sh\n@@ -196,7 +196,7 @@ test_expect_success 'read-tree will not throw away dirty changes, non-sparse' '\n \techo dirty >init.t &&\n \tread_tree_u_must_fail -m -u HEAD^ &&\n \ttest_path_is_file init.t &&\n-\tgrep -q dirty init.t\n+\ttest_grep -q dirty init.t\n '\n \n test_expect_success 'read-tree will not throw away dirty changes, sparse' '\n@@ -207,7 +207,7 @@ test_expect_success 'read-tree will not throw away dirty changes, sparse' '\n \techo sub/added >.git/info/sparse-checkout &&\n \tread_tree_u_must_fail -m -u HEAD^ &&\n \ttest_path_is_file init.t &&\n-\tgrep -q dirty init.t\n+\ttest_grep -q dirty init.t\n '\n \n test_expect_success 'read-tree updates worktree, dirty case' '\n@@ -215,7 +215,7 @@ test_expect_success 'read-tree updates worktree, dirty case' '\n \tgit checkout -f top &&\n \techo dirty >init.t &&\n \tread_tree_u_must_fail -m -u HEAD^ &&\n-\tgrep -q dirty init.t &&\n+\ttest_grep -q dirty init.t &&\n \trm init.t\n '\n \n@@ -224,7 +224,7 @@ test_expect_success 'read-tree removes worktree, dirty case' '\n \tgit checkout -f top &&\n \techo dirty >added &&\n \tread_tree_u_must_succeed -m -u HEAD^ &&\n-\tgrep -q dirty added\n+\ttest_grep -q dirty added\n '\n \n test_expect_success 'read-tree adds to worktree, absent case' '\n@@ -240,7 +240,7 @@ test_expect_success 'read-tree adds to worktree, dirty case' '\n \tmkdir sub &&\n \techo dirty >sub/added &&\n \tread_tree_u_must_succeed -u -m HEAD^ &&\n-\tgrep -q dirty sub/added\n+\ttest_grep -q dirty sub/added\n '\n \n test_expect_success 'index removal and worktree narrowing at the same time' '\ndiff --git a/t/t1050-large.sh b/t/t1050-large.sh\nindex 7d40d08521..d295c265c7 100755\n--- a/t/t1050-large.sh\n+++ b/t/t1050-large.sh\n@@ -8,7 +8,7 @@ test_description='adding and checking out large blobs'\n test_expect_success 'core.bigFileThreshold must be non-negative' '\n \t: >input &&\n \ttest_must_fail git -c core.bigFileThreshold=-1 hash-object input >out 2>err &&\n-\tgrep \"bad numeric config value\" err &&\n+\ttest_grep \"bad numeric config value\" err &&\n \ttest_must_be_empty out\n '\n \n@@ -148,12 +148,12 @@ test_expect_success 'diff --stat' '\n \n test_expect_success 'diff' '\n \tgit diff HEAD^ HEAD >actual &&\n-\tgrep \"Binary files.*differ\" actual\n+\ttest_grep \"Binary files.*differ\" actual\n '\n \n test_expect_success 'diff --cached' '\n \tgit diff --cached HEAD^ >actual &&\n-\tgrep \"Binary files.*differ\" actual\n+\ttest_grep \"Binary files.*differ\" actual\n '\n \n test_expect_success 'hash-object' '\ndiff --git a/t/t1091-sparse-checkout-builtin.sh b/t/t1091-sparse-checkout-builtin.sh\nindex cd0aed9975..74b1761e0c 100755\n--- a/t/t1091-sparse-checkout-builtin.sh\n+++ b/t/t1091-sparse-checkout-builtin.sh\n@@ -129,7 +129,7 @@ test_expect_success 'switching to cone mode with non-cone mode patterns' '\n \t\tgit sparse-checkout add dir &&\n \t\tgit config --worktree core.sparseCheckoutCone true &&\n \t\ttest_must_fail git sparse-checkout add dir 2>err &&\n-\t\tgrep \"existing sparse-checkout patterns do not use cone mode\" err\n+\t\ttest_grep \"existing sparse-checkout patterns do not use cone mode\" err\n \t)\n '\n \n@@ -803,7 +803,7 @@ test_expect_success 'cone mode clears ignored subdirectories' '\n \t# When an untracked file is in the way, all untracked files\n \t# (even ignored files) are preserved.\n \tgit -C repo sparse-checkout set folder1 2>err &&\n-\tgrep \"contains untracked files\" err &&\n+\ttest_grep \"contains untracked files\" err &&\n \ttest_path_is_file repo/deep/deeper2/ignored.o &&\n \ttest_path_is_file repo/deep/deeper2/untracked &&\n \n@@ -882,8 +882,8 @@ test_expect_success 'malformed cone-mode patterns' '\n \t# of using the cone-mode translation to a set of directories.\n \tgit -C repo sparse-checkout list >actual 2>err &&\n \ttest_cmp repo/.git/info/sparse-checkout actual &&\n-\tgrep \"warning: your sparse-checkout file may have issues: pattern .* is repeated\" err &&\n-\tgrep \"warning: disabling cone pattern matching\" err\n+\ttest_grep \"warning: your sparse-checkout file may have issues: pattern .* is repeated\" err &&\n+\ttest_grep \"warning: disabling cone pattern matching\" err\n '\n \n test_expect_success 'set from subdir pays attention to prefix' '\n@@ -917,34 +917,34 @@ test_expect_success 'set from subdir in non-cone mode throws an error' '\n \tgit -C repo sparse-checkout disable &&\n \ttest_must_fail git -C repo/deep sparse-checkout set --no-cone deeper2 ../folder1 2>error &&\n \n-\tgrep \"run from the toplevel directory in non-cone mode\" error\n+\ttest_grep \"run from the toplevel directory in non-cone mode\" error\n '\n \n test_expect_success 'set from subdir in non-cone mode throws an error' '\n \tgit -C repo sparse-checkout set --no-cone deep/deeper2 &&\n \ttest_must_fail git -C repo/deep sparse-checkout add deeper1/deepest ../folder1 2>error &&\n \n-\tgrep \"run from the toplevel directory in non-cone mode\" error\n+\ttest_grep \"run from the toplevel directory in non-cone mode\" error\n '\n \n test_expect_success 'by default, cone mode will error out when passed files' '\n \tgit -C repo sparse-checkout reapply --cone &&\n \ttest_must_fail git -C repo sparse-checkout add .gitignore 2>error &&\n \n-\tgrep \".gitignore.*is not a directory\" error\n+\ttest_grep \".gitignore.*is not a directory\" error\n '\n \n test_expect_success 'error on mistyped command line options' '\n \ttest_must_fail git -C repo sparse-checkout add --sikp-checks .gitignore 2>error &&\n \n-\tgrep \"unknown option.*sikp-checks\" error\n+\ttest_grep \"unknown option.*sikp-checks\" error\n '\n \n test_expect_success 'by default, non-cone mode will warn on individual files' '\n \tgit -C repo sparse-checkout reapply --no-cone &&\n \tgit -C repo sparse-checkout add .gitignore 2>warning &&\n \n-\tgrep \"pass a leading slash before paths.*if you want a single file\" warning\n+\ttest_grep \"pass a leading slash before paths.*if you want a single file\" warning\n '\n \n test_expect_success 'setup bare repo' '\n@@ -1108,11 +1108,11 @@ test_expect_success 'clean' '\n \ttouch repo/folder1/extra/inside/file &&\n \n \ttest_must_fail git -C repo sparse-checkout clean 2>err &&\n-\tgrep \"refusing to clean\" err &&\n+\ttest_grep \"refusing to clean\" err &&\n \n \tgit -C repo config clean.requireForce true &&\n \ttest_must_fail git -C repo sparse-checkout clean 2>err &&\n-\tgrep \"refusing to clean\" err &&\n+\ttest_grep \"refusing to clean\" err &&\n \n \tcat >expect <<-\\EOF &&\n \tWould remove deep/deeper2/\n@@ -1255,7 +1255,7 @@ test_expect_success 'sparse-checkout operations with merge conflicts' '\n \t\ttest_must_fail git merge -m \"will-conflict\" right &&\n \n \t\ttest_must_fail git sparse-checkout clean -f 2>err &&\n-\t\tgrep \"failed to convert index to a sparse index\" err &&\n+\t\ttest_grep \"failed to convert index to a sparse index\" err &&\n \n \t\techo merged >folder1/even/more/dirs/file &&\n \t\tgit add --sparse folder1 &&\ndiff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh\nindex 8186da5c88..9814431cd7 100755\n--- a/t/t1092-sparse-checkout-compatibility.sh\n+++ b/t/t1092-sparse-checkout-compatibility.sh\n@@ -454,10 +454,10 @@ test_expect_success 'add outside sparse cone' '\n \trun_on_sparse ../edit-contents folder1/a &&\n \trun_on_sparse ../edit-contents folder1/newfile &&\n \ttest_sparse_match test_must_fail git add folder1/a &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/a &&\n \ttest_sparse_match test_must_fail git add folder1/newfile &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/newfile\n '\n \n@@ -509,13 +509,13 @@ test_expect_success 'status/add: outside sparse cone' '\n \n \t# Adding the path outside of the sparse-checkout cone should fail.\n \ttest_sparse_match test_must_fail git add folder1/a &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/a &&\n \ttest_all_match git add --refresh folder1/a &&\n \ttest_must_be_empty sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/a &&\n \ttest_sparse_match test_must_fail git add folder1/new &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/new &&\n \ttest_sparse_match git add --sparse folder1/a &&\n \ttest_sparse_match git add --sparse folder1/new &&\n@@ -661,8 +661,8 @@ test_expect_success 'checkout and reset (mixed)' '\n \t# in sparse-checkout or sparse-index.\n \tgit -C full-checkout reset update-folder1 >full-checkout-out &&\n \ttest_sparse_match git reset update-folder1 &&\n-\tgrep \"M\tfolder1/a\" full-checkout-out &&\n-\t! grep \"M\tfolder1/a\" sparse-checkout-out &&\n+\ttest_grep \"M\tfolder1/a\" full-checkout-out &&\n+\ttest_grep ! \"M\tfolder1/a\" sparse-checkout-out &&\n \trun_on_sparse test_path_is_missing folder1\n '\n \n@@ -880,8 +880,8 @@ test_expect_success 'update-index with directories' '\n \t# update-index will exit silently when provided with a directory name\n \t# containing a trailing slash\n \ttest_all_match git update-index deep/ folder1/ &&\n-\tgrep \"Ignoring path deep/\" sparse-checkout-err &&\n-\tgrep \"Ignoring path folder1/\" sparse-checkout-err &&\n+\ttest_grep \"Ignoring path deep/\" sparse-checkout-err &&\n+\ttest_grep \"Ignoring path folder1/\" sparse-checkout-err &&\n \n \t# When update-index is given a directory name WITHOUT a trailing slash, it will\n \t# behave in different ways depending on the status of the directory on disk:\n@@ -1067,7 +1067,7 @@ test_expect_success 'merge with conflict outside cone' '\n \n \t# 2. Add the file with conflict markers\n \ttest_sparse_match test_must_fail git add folder1/a &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/a &&\n \ttest_all_match git add --sparse folder1/a &&\n \ttest_all_match git status --porcelain=v2 &&\n@@ -1076,7 +1076,7 @@ test_expect_success 'merge with conflict outside cone' '\n \t#    accept conflict markers as resolved content.\n \trun_on_all mv folder2/a folder2/z &&\n \ttest_sparse_match test_must_fail git add folder2 &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder2/z &&\n \ttest_all_match git add --sparse folder2 &&\n \ttest_all_match git status --porcelain=v2 &&\n@@ -1107,7 +1107,7 @@ test_expect_success 'cherry-pick/rebase with conflict outside cone' '\n \t\t# SKIP_WORKTREE bit from the index entry for folder1/a, we should\n \t\t# warn that this is a problematic add.\n \t\ttest_sparse_match test_must_fail git add folder1/a &&\n-\t\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\t\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \t\ttest_sparse_unstaged folder1/a &&\n \t\ttest_all_match git add --sparse folder1/a &&\n \t\ttest_all_match git status --porcelain=v2 &&\n@@ -1119,7 +1119,7 @@ test_expect_success 'cherry-pick/rebase with conflict outside cone' '\n \t\t# existing index entry with the SKIP_WORKTREE bit cleared.\n \t\trun_on_all mv folder2/a folder2/z &&\n \t\ttest_sparse_match test_must_fail git add folder2 &&\n-\t\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\t\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \t\ttest_sparse_unstaged folder2/z &&\n \t\ttest_all_match git add --sparse folder2 &&\n \t\ttest_all_match git status --porcelain=v2 &&\n@@ -1266,7 +1266,7 @@ test_expect_success 'checkout-index with folders' '\n \trun_on_all test_must_fail git checkout-index -f -- folder1/ &&\n \ttest_cmp full-checkout-err sparse-checkout-err &&\n \t! test_cmp full-checkout-err sparse-index-err &&\n-\tgrep \"is a sparse directory\" sparse-index-err\n+\ttest_grep \"is a sparse directory\" sparse-index-err\n '\n \n test_expect_success 'checkout-index --all' '\n@@ -1374,8 +1374,8 @@ test_expect_success 'submodule handling' '\n \t# having a submodule prevents \"modules\" from collapse\n \ttest_sparse_match git sparse-checkout set deep/deeper1 &&\n \tgit -C sparse-index ls-files --sparse --stage >cache &&\n-\tgrep \"100644 .*\tmodules/a\" cache &&\n-\tgrep \"160000 $(git -C initial-repo rev-parse HEAD) 0\tmodules/sub\" cache\n+\ttest_grep \"100644 .*\tmodules/a\" cache &&\n+\ttest_grep \"160000 $(git -C initial-repo rev-parse HEAD) 0\tmodules/sub\" cache\n '\n \n test_expect_success 'git apply functionality' '\n@@ -1392,7 +1392,7 @@ test_expect_success 'git apply functionality' '\n \n \t# Apply a patch to a file outside the sparse definition\n \ttest_sparse_match test_must_fail git apply ../patch-outside &&\n-\tgrep \"No such file or directory\" sparse-checkout-err &&\n+\ttest_grep \"No such file or directory\" sparse-checkout-err &&\n \n \t# But it works with --index and --cached\n \ttest_all_match git apply --index --stat ../patch-outside &&\n@@ -2013,9 +2013,9 @@ test_expect_success 'mv directory from out-of-cone to in-cone' '\n \ttest_all_match git status --porcelain=v2 &&\n \ttest_sparse_match git ls-files -t &&\n \tgit -C sparse-checkout ls-files -t >actual &&\n-\tgrep -e \"H deep/folder1/0/0/0\" actual &&\n-\tgrep -e \"H deep/folder1/0/1\" actual &&\n-\tgrep -e \"H deep/folder1/a\" actual &&\n+\ttest_grep -e \"H deep/folder1/0/0/0\" actual &&\n+\ttest_grep -e \"H deep/folder1/0/1\" actual &&\n+\ttest_grep -e \"H deep/folder1/a\" actual &&\n \n \ttest_all_match git reset --hard &&\n \n@@ -2025,8 +2025,8 @@ test_expect_success 'mv directory from out-of-cone to in-cone' '\n \ttest_sparse_match git status --porcelain=v2 &&\n \ttest_sparse_match git ls-files -t &&\n \tgit -C sparse-checkout ls-files -t >actual &&\n-\tgrep -e \"H deep/0/0/0\" actual &&\n-\tgrep -e \"H deep/0/1\" actual\n+\ttest_grep -e \"H deep/0/0/0\" actual &&\n+\ttest_grep -e \"H deep/0/1\" actual\n '\n \n test_expect_success 'rm pathspec inside sparse definition' '\n@@ -2517,7 +2517,7 @@ test_expect_success 'advice.sparseIndexExpanded' '\n \tmkdir -p sparse-index/deep/deeper2/deepest &&\n \ttouch sparse-index/deep/deeper2/deepest/bogus &&\n \tgit -C sparse-index status 2>err &&\n-\tgrep \"The sparse index is expanding to a full index\" err &&\n+\ttest_grep \"The sparse index is expanding to a full index\" err &&\n \n \tgit -C sparse-index sparse-checkout disable 2>err &&\n \ttest_line_count = 0 err\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 87ca11a127..b99f782d5d 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -856,7 +856,7 @@ test_expect_success 'renaming a section with an overly-long line' '\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n \ttest_must_fail git config ${mode_prefix}rename-section -f y a xyz 2>err &&\n-\tgrep \"refusing to work with overly long line in .y. on line 2\" err\n+\ttest_grep \"refusing to work with overly long line in .y. on line 2\" err\n '\n \n cat >> .git/config << EOF\n@@ -1671,9 +1671,9 @@ test_expect_success 'git --config-env=key=envvar support' '\n \n test_expect_success 'git --config-env with missing value' '\n \ttest_must_fail env ENVVAR=value git --config-env 2>error &&\n-\tgrep \"no config key given for --config-env\" error &&\n+\ttest_grep \"no config key given for --config-env\" error &&\n \ttest_must_fail env ENVVAR=value git --config-env config core.name 2>error &&\n-\tgrep \"invalid config format: config\" error\n+\ttest_grep \"invalid config format: config\" error\n '\n \n test_expect_success 'git --config-env fails with invalid parameters' '\n@@ -2104,7 +2104,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \tkey = true\n \tEOF\n \tgit config ${mode_unset} two.key &&\n-\t! grep two .git/config &&\n+\ttest_grep ! two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n \t[one]\n@@ -2124,7 +2124,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \tQkey = true\n \tEOF\n \tgit config ${mode_unset} two.key &&\n-\tgrep two .git/config &&\n+\ttest_grep two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n \t[one]\n@@ -2655,7 +2655,7 @@ test_expect_success '--type rejects unknown specifiers' '\n \n test_expect_success '--type=int requires at least one digit' '\n \ttest_must_fail git config --type int --default m some.key >out 2>error &&\n-\tgrep \"bad numeric config value\" error &&\n+\ttest_grep \"bad numeric config value\" error &&\n \ttest_must_be_empty out\n '\n \n@@ -2967,12 +2967,12 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such\n \n \t# test with any Git command\n \ttest_must_fail git -C hasremoteurlTest status 2>err &&\n-\tgrep \"fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url\" err\n+\ttest_grep \"fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url\" err\n '\n \n test_expect_success 'negated mode causes failure' '\n \ttest_must_fail git config --no-get 2>err &&\n-\tgrep \"unknown option \\`no-get${SQ}\" err\n+\ttest_grep \"unknown option \\`no-get${SQ}\" err\n '\n \n test_expect_success 'specifying multiple modes causes failure' '\ndiff --git a/t/t1305-config-include.sh b/t/t1305-config-include.sh\nindex f3892578e4..f6115269f9 100755\n--- a/t/t1305-config-include.sh\n+++ b/t/t1305-config-include.sh\n@@ -353,7 +353,7 @@ test_expect_success 'include cycles are detected' '\n \tgit -C cycle --git-dir=. config include.path cycle &&\n \tgit config -f cycle/cycle include.path config &&\n \ttest_must_fail git -C cycle --git-dir=. config --get-all test.value 2>stderr &&\n-\tgrep \"exceeded maximum include depth\" stderr\n+\ttest_grep \"exceeded maximum include depth\" stderr\n '\n \n test_expect_success 'onbranch with unborn branch' '\ndiff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\nindex e0e49053f0..de95161a83 100755\n--- a/t/t1308-config-set.sh\n+++ b/t/t1308-config-set.sh\n@@ -180,7 +180,7 @@ test_expect_success 'find integer if value is non parse-able' '\n \n test_expect_success 'non parse-able integer value during iteration' '\n \tcheck_config expect_code 128 git_config_int lamb.head 2>result &&\n-\tgrep \"fatal: bad numeric config value .* in file \\.git/config\" result\n+\ttest_grep \"fatal: bad numeric config value .* in file \\.git/config\" result\n '\n \n test_expect_success 'find bool value for the entered key' '\n@@ -302,7 +302,7 @@ test_expect_success 'proper error on directory \"files\"' '\n \techo \"Error (-1) reading configuration file a-directory.\" >expect &&\n \tmkdir a-directory &&\n \ttest_expect_code 2 test-tool config configset_get_value foo.bar a-directory 2>output &&\n-\tgrep \"^warning:\" output &&\n+\ttest_grep \"^warning:\" output &&\n \tgrep \"^Error\" output >actual &&\n \ttest_cmp expect actual\n '\n@@ -312,7 +312,7 @@ test_expect_success POSIXPERM,SANITY 'proper error on non-accessible files' '\n \ttest_when_finished \"chmod +r .git/config\" &&\n \techo \"Error (-1) reading configuration file .git/config.\" >expect &&\n \ttest_expect_code 2 test-tool config configset_get_value foo.bar .git/config 2>output &&\n-\tgrep \"^warning:\" output &&\n+\ttest_grep \"^warning:\" output &&\n \tgrep \"^Error\" output >actual &&\n \ttest_cmp expect actual\n '\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex f059d63e38..c3b02ae1d2 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -92,7 +92,7 @@ test_expect_success \"deleting current branch adds message to HEAD's log\" '\n \tgit update-ref -m delete-$m -d $m &&\n \ttest_must_fail git show-ref --verify -q $m &&\n \ttest-tool ref-store main for-each-reflog-ent HEAD >actual &&\n-\tgrep \"delete-$m$\" actual\n+\ttest_grep \"delete-$m$\" actual\n '\n \n test_expect_success \"deleting by HEAD adds message to HEAD's log\" '\n@@ -102,7 +102,7 @@ test_expect_success \"deleting by HEAD adds message to HEAD's log\" '\n \tgit update-ref -m delete-by-head -d HEAD &&\n \ttest_must_fail git show-ref --verify -q $m &&\n \ttest-tool ref-store main for-each-reflog-ent HEAD >actual &&\n-\tgrep \"delete-by-head$\" actual\n+\ttest_grep \"delete-by-head$\" actual\n '\n \n test_expect_success 'update-ref does not create reflogs by default' '\n@@ -192,7 +192,10 @@ test_expect_success \"move $m (by HEAD)\" '\n test_expect_success \"delete $m (by HEAD) should remove both packed and loose $m\" '\n \ttest_when_finished \"git update-ref -d $m\" &&\n \tgit update-ref -d HEAD $B &&\n-\t! grep \"$m\" .git/packed-refs &&\n+\tif test_have_prereq REFFILES\n+\tthen\n+\t\ttest_grep ! \"$m\" .git/packed-refs\n+\tfi &&\n \ttest_must_fail git show-ref --verify -q $m\n '\n \n@@ -575,103 +578,103 @@ test_expect_success 'stdin works with no input' '\n test_expect_success 'stdin fails on empty line' '\n \techo \"\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: empty command in input\" err\n+\ttest_grep \"fatal: empty command in input\" err\n '\n \n test_expect_success 'stdin fails on only whitespace' '\n \techo \" \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command:  \" err\n+\ttest_grep \"fatal: whitespace before command:  \" err\n '\n \n test_expect_success 'stdin fails on leading whitespace' '\n \techo \" create $a $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command:  create $a $m\" err\n+\ttest_grep \"fatal: whitespace before command:  create $a $m\" err\n '\n \n test_expect_success 'stdin fails on unknown command' '\n \techo \"unknown $a\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: unknown $a\" err\n+\ttest_grep \"fatal: unknown command: unknown $a\" err\n '\n \n test_expect_success 'stdin fails on unbalanced quotes' '\n \techo \"create $a \\\"main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: badly quoted argument: \\\\\\\"main\" err\n+\ttest_grep \"fatal: badly quoted argument: \\\\\\\"main\" err\n '\n \n test_expect_success 'stdin fails on invalid escape' '\n \techo \"create $a \\\"ma\\zn\\\"\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: badly quoted argument: \\\\\\\"ma\\\\\\\\zn\\\\\\\"\" err\n+\ttest_grep \"fatal: badly quoted argument: \\\\\\\"ma\\\\\\\\zn\\\\\\\"\" err\n '\n \n test_expect_success 'stdin fails on junk after quoted argument' '\n \techo \"create \\\"$a\\\"main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: unexpected character after quoted argument: \\\\\\\"$a\\\\\\\"main\" err\n+\ttest_grep \"fatal: unexpected character after quoted argument: \\\\\\\"$a\\\\\\\"main\" err\n '\n \n test_expect_success 'stdin fails create with no ref' '\n \techo \"create \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create: missing <ref>\" err\n+\ttest_grep \"fatal: create: missing <ref>\" err\n '\n \n test_expect_success 'stdin fails create with no new value' '\n \techo \"create $a\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $a: missing <new-oid>\" err\n+\ttest_grep \"fatal: create $a: missing <new-oid>\" err\n '\n \n test_expect_success 'stdin fails create with too many arguments' '\n \techo \"create $a $m $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $a: extra input:  $m\" err\n+\ttest_grep \"fatal: create $a: extra input:  $m\" err\n '\n \n test_expect_success 'stdin fails update with no ref' '\n \techo \"update \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: update: missing <ref>\" err\n+\ttest_grep \"fatal: update: missing <ref>\" err\n '\n \n test_expect_success 'stdin fails update with no new value' '\n \techo \"update $a\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: missing <new-oid>\" err\n+\ttest_grep \"fatal: update $a: missing <new-oid>\" err\n '\n \n test_expect_success 'stdin fails update with too many arguments' '\n \techo \"update $a $m $m $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: extra input:  $m\" err\n+\ttest_grep \"fatal: update $a: extra input:  $m\" err\n '\n \n test_expect_success 'stdin fails delete with no ref' '\n \techo \"delete \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete: missing <ref>\" err\n+\ttest_grep \"fatal: delete: missing <ref>\" err\n '\n \n test_expect_success 'stdin fails delete with too many arguments' '\n \techo \"delete $a $m $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete $a: extra input:  $m\" err\n+\ttest_grep \"fatal: delete $a: extra input:  $m\" err\n '\n \n test_expect_success 'stdin fails verify with too many arguments' '\n \techo \"verify $a $m $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: verify $a: extra input:  $m\" err\n+\ttest_grep \"fatal: verify $a: extra input:  $m\" err\n '\n \n test_expect_success 'stdin fails option with unknown name' '\n \techo \"option unknown\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: option unknown: unknown\" err\n+\ttest_grep \"fatal: option unknown: unknown\" err\n '\n \n test_expect_success 'stdin fails with duplicate refs' '\n@@ -759,28 +762,28 @@ test_expect_success 'stdin create ref works with path with space to blob' '\n test_expect_success 'stdin update ref fails with wrong old value' '\n \techo \"update $c $m $m~1\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin update ref fails with bad old value' '\n \techo \"update $c $m does-not-exist\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $c: invalid <old-oid>: does-not-exist\" err &&\n+\ttest_grep \"fatal: update $c: invalid <old-oid>: does-not-exist\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin create ref fails with bad new value' '\n \techo \"create $c does-not-exist\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $c: invalid <new-oid>: does-not-exist\" err &&\n+\ttest_grep \"fatal: create $c: invalid <new-oid>: does-not-exist\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin create ref fails with zero new value' '\n \techo \"create $c \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $c: zero <new-oid>\" err &&\n+\ttest_grep \"fatal: create $c: zero <new-oid>\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n@@ -795,7 +798,7 @@ test_expect_success 'stdin update ref works with right old value' '\n test_expect_success 'stdin delete ref fails with wrong old value' '\n \techo \"delete $a $m~1\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$a'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$a'\"'\"'\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual\n@@ -804,7 +807,7 @@ test_expect_success 'stdin delete ref fails with wrong old value' '\n test_expect_success 'stdin delete ref fails with zero old value' '\n \techo \"delete $a \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete $a: zero <old-oid>\" err &&\n+\ttest_grep \"fatal: delete $a: zero <old-oid>\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual\n@@ -965,7 +968,7 @@ test_expect_success 'stdin update refs fails with wrong old value' '\n \tupdate $c  ''\n \tEOF\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual &&\n@@ -998,123 +1001,123 @@ test_expect_success 'stdin -z works on empty input' '\n test_expect_success 'stdin -z fails on empty line' '\n \techo \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command: \" err\n+\ttest_grep \"fatal: whitespace before command: \" err\n '\n \n test_expect_success 'stdin -z fails on empty command' '\n \tprintf $F \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: empty command in input\" err\n+\ttest_grep \"fatal: empty command in input\" err\n '\n \n test_expect_success 'stdin -z fails on only whitespace' '\n \tprintf $F \" \" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command:  \" err\n+\ttest_grep \"fatal: whitespace before command:  \" err\n '\n \n test_expect_success 'stdin -z fails on leading whitespace' '\n \tprintf $F \" create $a\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command:  create $a\" err\n+\ttest_grep \"fatal: whitespace before command:  create $a\" err\n '\n \n test_expect_success 'stdin -z fails on unknown command' '\n \tprintf $F \"unknown $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: unknown $a\" err\n+\ttest_grep \"fatal: unknown command: unknown $a\" err\n '\n \n test_expect_success 'stdin -z fails create with no ref' '\n \tprintf $F \"create \" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: create: missing <ref>\" err\n+\ttest_grep \"fatal: create: missing <ref>\" err\n '\n \n test_expect_success 'stdin -z fails create with no new value' '\n \tprintf $F \"create $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $a: unexpected end of input when reading <new-oid>\" err\n+\ttest_grep \"fatal: create $a: unexpected end of input when reading <new-oid>\" err\n '\n \n test_expect_success 'stdin -z fails create with too many arguments' '\n \tprintf $F \"create $a\" \"$m\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: $m\" err\n+\ttest_grep \"fatal: unknown command: $m\" err\n '\n \n test_expect_success 'stdin -z fails update with no ref' '\n \tprintf $F \"update \" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update: missing <ref>\" err\n+\ttest_grep \"fatal: update: missing <ref>\" err\n '\n \n test_expect_success 'stdin -z fails update with too few args' '\n \tprintf $F \"update $a\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: unexpected end of input when reading <old-oid>\" err\n+\ttest_grep \"fatal: update $a: unexpected end of input when reading <old-oid>\" err\n '\n \n test_expect_success 'stdin -z emits warning with empty new value' '\n \tgit update-ref $a $m &&\n \tprintf $F \"update $a\" \"\" \"\" >stdin &&\n \tgit update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"warning: update $a: missing <new-oid>, treating as zero\" err &&\n+\ttest_grep \"warning: update $a: missing <new-oid>, treating as zero\" err &&\n \ttest_must_fail git rev-parse --verify -q $a\n '\n \n test_expect_success 'stdin -z fails update with no new value' '\n \tprintf $F \"update $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: unexpected end of input when reading <new-oid>\" err\n+\ttest_grep \"fatal: update $a: unexpected end of input when reading <new-oid>\" err\n '\n \n test_expect_success 'stdin -z fails update with no old value' '\n \tprintf $F \"update $a\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: unexpected end of input when reading <old-oid>\" err\n+\ttest_grep \"fatal: update $a: unexpected end of input when reading <old-oid>\" err\n '\n \n test_expect_success 'stdin -z fails update with too many arguments' '\n \tprintf $F \"update $a\" \"$m\" \"$m\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: $m\" err\n+\ttest_grep \"fatal: unknown command: $m\" err\n '\n \n test_expect_success 'stdin -z fails delete with no ref' '\n \tprintf $F \"delete \" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete: missing <ref>\" err\n+\ttest_grep \"fatal: delete: missing <ref>\" err\n '\n \n test_expect_success 'stdin -z fails delete with no old value' '\n \tprintf $F \"delete $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete $a: unexpected end of input when reading <old-oid>\" err\n+\ttest_grep \"fatal: delete $a: unexpected end of input when reading <old-oid>\" err\n '\n \n test_expect_success 'stdin -z fails delete with too many arguments' '\n \tprintf $F \"delete $a\" \"$m\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: $m\" err\n+\ttest_grep \"fatal: unknown command: $m\" err\n '\n \n test_expect_success 'stdin -z fails verify with too many arguments' '\n \tprintf $F \"verify $a\" \"$m\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: $m\" err\n+\ttest_grep \"fatal: unknown command: $m\" err\n '\n \n test_expect_success 'stdin -z fails verify with no old value' '\n \tprintf $F \"verify $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: verify $a: unexpected end of input when reading <old-oid>\" err\n+\ttest_grep \"fatal: verify $a: unexpected end of input when reading <old-oid>\" err\n '\n \n test_expect_success 'stdin -z fails option with unknown name' '\n \tprintf $F \"option unknown\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: option unknown: unknown\" err\n+\ttest_grep \"fatal: option unknown: unknown\" err\n '\n \n test_expect_success 'stdin -z fails with duplicate refs' '\n@@ -1160,14 +1163,14 @@ test_expect_success 'stdin -z create ref works with path with space to blob' '\n test_expect_success 'stdin -z update ref fails with wrong old value' '\n \tprintf $F \"update $c\" \"$m\" \"$m~1\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin -z update ref fails with bad old value' '\n \tprintf $F \"update $c\" \"$m\" \"does-not-exist\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $c: invalid <old-oid>: does-not-exist\" err &&\n+\ttest_grep \"fatal: update $c: invalid <old-oid>: does-not-exist\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n@@ -1176,7 +1179,7 @@ test_expect_success 'stdin -z create ref fails when ref exists' '\n \tgit rev-parse \"$c\" >expect &&\n \tprintf $F \"create $c\" \"$m~1\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \tgit rev-parse \"$c\" >actual &&\n \ttest_cmp expect actual\n '\n@@ -1185,28 +1188,28 @@ test_expect_success 'stdin -z create ref fails with bad new value' '\n \tgit update-ref -d \"$c\" &&\n \tprintf $F \"create $c\" \"does-not-exist\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $c: invalid <new-oid>: does-not-exist\" err &&\n+\ttest_grep \"fatal: create $c: invalid <new-oid>: does-not-exist\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin -z create ref fails with empty new value' '\n \tprintf $F \"create $c\" \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $c: missing <new-oid>\" err &&\n+\ttest_grep \"fatal: create $c: missing <new-oid>\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin -z create ref fails with non commit object' '\n \tprintf $F \"create $c\" \"$(test_oid 001)\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: trying to write ref ${SQ}$c${SQ} with nonexistent object\" err &&\n+\ttest_grep \"fatal: trying to write ref ${SQ}$c${SQ} with nonexistent object\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin -z update ref fails with non commit object' '\n \tprintf $F \"update $b\" \"$(test_oid 001)\" \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: trying to write ref ${SQ}$b${SQ} with nonexistent object\" err &&\n+\ttest_grep \"fatal: trying to write ref ${SQ}$b${SQ} with nonexistent object\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n@@ -1221,7 +1224,7 @@ test_expect_success 'stdin -z update ref works with right old value' '\n test_expect_success 'stdin -z delete ref fails with wrong old value' '\n \tprintf $F \"delete $a\" \"$m~1\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$a'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$a'\"'\"'\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual\n@@ -1230,7 +1233,7 @@ test_expect_success 'stdin -z delete ref fails with wrong old value' '\n test_expect_success 'stdin -z delete ref fails with zero old value' '\n \tprintf $F \"delete $a\" \"$Z\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete $a: zero <old-oid>\" err &&\n+\ttest_grep \"fatal: delete $a: zero <old-oid>\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual\n@@ -1336,7 +1339,7 @@ test_expect_success 'stdin -z update refs fails with wrong old value' '\n \tgit update-ref $c $m &&\n \tprintf $F \"update $a\" \"$m\" \"$m\" \"update $b\" \"$m\" \"$m\" \"update $c\" \"$m\" \"$Z\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual &&\n@@ -1415,13 +1418,13 @@ test_expect_success 'handle per-worktree refs in refs/bisect' '\n \t\tcd worktree &&\n \t\tgit commit --allow-empty -m \"test commit\"  &&\n \t\tgit for-each-ref >for-each-ref.out &&\n-\t\t! grep refs/bisect for-each-ref.out &&\n+\t\ttest_grep ! refs/bisect for-each-ref.out &&\n \t\tgit update-ref refs/bisect/something HEAD &&\n \t\tgit rev-parse refs/bisect/something >../worktree-head &&\n \t\tgit for-each-ref | grep refs/bisect/something\n \t) &&\n \tgit show-ref >actual &&\n-\t! grep 'refs/bisect' actual &&\n+\ttest_grep ! 'refs/bisect' actual &&\n \ttest_must_fail git rev-parse refs/bisect/something &&\n \tgit update-ref refs/bisect/something HEAD &&\n \tgit rev-parse refs/bisect/something >main-head &&\n@@ -1477,7 +1480,7 @@ test_expect_success 'transaction exits on multiple aborts' '\n \ttest_must_fail git update-ref --stdin <stdin >actual 2>err &&\n \tprintf \"%s: ok\\n\" abort >expect &&\n \ttest_cmp expect actual &&\n-\tgrep \"fatal: transaction is closed\" err\n+\ttest_grep \"fatal: transaction is closed\" err\n '\n \n test_expect_success 'transaction exits on start after prepare' '\n@@ -1488,7 +1491,7 @@ test_expect_success 'transaction exits on start after prepare' '\n \ttest_must_fail git update-ref --stdin <stdin 2>err >actual &&\n \tprintf \"%s: ok\\n\" prepare >expect &&\n \ttest_cmp expect actual &&\n-\tgrep \"fatal: prepared transactions can only be closed\" err\n+\ttest_grep \"fatal: prepared transactions can only be closed\" err\n '\n \n test_expect_success 'transaction handles empty abort with missing prepare' '\n@@ -1649,7 +1652,7 @@ test_expect_success PIPE 'transaction flushes status updates' '\n \n \t# This must now fail given that we have locked the ref.\n \ttest_must_fail git update-ref refs/heads/flush $B 2>stderr &&\n-\tgrep \"fatal: update_ref failed for ref ${SQ}refs/heads/flush${SQ}: cannot lock ref\" stderr &&\n+\ttest_grep \"fatal: update_ref failed for ref ${SQ}refs/heads/flush${SQ}: cannot lock ref\" stderr &&\n \n \techo commit >&9 &&\n \techo \"commit: ok\" >expected &&\n@@ -1675,7 +1678,7 @@ do\n \t\tgit symbolic-ref refs/heads/symref $a &&\n \t\tformat_command $type \"symref-verify refs/heads/symref\" \"$a\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: symref-verify: cannot operate with deref mode\" err\n+\t\ttest_grep \"fatal: symref-verify: cannot operate with deref mode\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-verify fails with too many arguments\" '\n@@ -1683,9 +1686,9 @@ do\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err  &&\n \t\tif test \"$type\" = \"-z\"\n \t\tthen\n-\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\t\ttest_grep \"fatal: unknown command: $a\" err\n \t\telse\n-\t\t\tgrep \"fatal: symref-verify refs/heads/symref: extra input:  $a\" err\n+\t\t\ttest_grep \"fatal: symref-verify refs/heads/symref: extra input:  $a\" err\n \t\tfi\n \t'\n \n@@ -1718,7 +1721,7 @@ do\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >before &&\n \t\tformat_command $type \"symref-verify refs/heads/missing\" \"refs/heads/unknown\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/missing${SQ}: unable to resolve reference ${SQ}refs/heads/missing${SQ}\" err &&\n+\t\ttest_grep \"fatal: cannot lock ref ${SQ}refs/heads/missing${SQ}: unable to resolve reference ${SQ}refs/heads/missing${SQ}\" err &&\n \t\ttest_must_fail git rev-parse --verify -q refs/heads/missing &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >after &&\n \t\ttest_cmp before after\n@@ -1744,13 +1747,13 @@ do\n \t\tgit symbolic-ref refs/heads/symref $a &&\n \t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: symref-delete: cannot operate with deref mode\" err\n+\t\ttest_grep \"fatal: symref-delete: cannot operate with deref mode\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-delete fails with no ref\" '\n \t\tformat_command $type \"symref-delete \" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: symref-delete: missing <ref>\" err\n+\t\ttest_grep \"fatal: symref-delete: missing <ref>\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-delete fails deleting regular ref\" '\n@@ -1758,7 +1761,7 @@ do\n \t\tgit update-ref refs/heads/regularref $a &&\n \t\tformat_command $type \"symref-delete refs/heads/regularref\" \"$a\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: expected symref with target ${SQ}$a${SQ}: but is a regular ref\" err\n+\t\ttest_grep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: expected symref with target ${SQ}$a${SQ}: but is a regular ref\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-delete fails with too many arguments\" '\n@@ -1766,16 +1769,16 @@ do\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n \t\tif test \"$type\" = \"-z\"\n \t\tthen\n-\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\t\ttest_grep \"fatal: unknown command: $a\" err\n \t\telse\n-\t\t\tgrep \"fatal: symref-delete refs/heads/symref: extra input:  $a\" err\n+\t\t\ttest_grep \"fatal: symref-delete refs/heads/symref: extra input:  $a\" err\n \t\tfi\n \t'\n \n \ttest_expect_success \"stdin $type symref-delete fails with wrong old value\" '\n \t\tformat_command $type \"symref-delete refs/heads/symref\" \"$m\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main\" err &&\n+\t\ttest_grep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main\" err &&\n \t\tgit symbolic-ref refs/heads/symref >expect &&\n \t\techo $a >actual &&\n \t\ttest_cmp expect actual\n@@ -1813,9 +1816,9 @@ do\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n \t\tif test \"$type\" = \"-z\"\n \t\tthen\n-\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\t\ttest_grep \"fatal: unknown command: $a\" err\n \t\telse\n-\t\t\tgrep \"fatal: symref-create refs/heads/symref: extra input:  $a\" err\n+\t\t\ttest_grep \"fatal: symref-create refs/heads/symref: extra input:  $a\" err\n \t\tfi\n \t'\n \n@@ -1878,16 +1881,16 @@ do\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n \t\tif test \"$type\" = \"-z\"\n \t\tthen\n-\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\t\ttest_grep \"fatal: unknown command: $a\" err\n \t\telse\n-\t\t\tgrep \"fatal: symref-update refs/heads/symref: extra input:  $a\" err\n+\t\t\ttest_grep \"fatal: symref-update refs/heads/symref: extra input:  $a\" err\n \t\tfi\n \t'\n \n \ttest_expect_success \"stdin $type symref-update fails with wrong old value argument\" '\n \t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"foo\" \"$a\" \"$a\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: symref-update refs/heads/symref: invalid arg ${SQ}foo${SQ} for old value\" err\n+\t\ttest_grep \"fatal: symref-update refs/heads/symref: invalid arg ${SQ}foo${SQ} for old value\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-update creates with zero old value\" '\n@@ -1923,7 +1926,7 @@ do\n \t\tgit symbolic-ref refs/heads/symref $a &&\n \t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"$b\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected $b\" err &&\n+\t\ttest_grep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected $b\" err &&\n \t\ttest_must_fail git rev-parse --verify -q $c\n \t'\n \n@@ -1998,7 +2001,7 @@ do\n \t\tgit symbolic-ref --no-recurse refs/heads/symref >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&\n-\t\tgrep \"$Z $(git rev-parse $a)\" actual\n+\t\ttest_grep \"$Z $(git rev-parse $a)\" actual\n \t'\n \n \ttest_expect_success \"stdin $type symref-update regular ref to symref with correct old-oid\" '\n@@ -2010,7 +2013,7 @@ do\n \t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&\n-\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t\ttest_grep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n \t'\n \n \ttest_expect_success \"stdin $type symref-update regular ref to symref fails with wrong old-oid\" '\n@@ -2018,7 +2021,7 @@ do\n \t\tgit update-ref --no-deref refs/heads/regularref $a &&\n \t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"$(git rev-parse refs/heads/target2)\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: is at $(git rev-parse $a) but expected $(git rev-parse refs/heads/target2)\" err &&\n+\t\ttest_grep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: is at $(git rev-parse $a) but expected $(git rev-parse refs/heads/target2)\" err &&\n \t\techo $(git rev-parse $a) >expect &&\n \t\tgit rev-parse refs/heads/regularref >actual &&\n \t\ttest_cmp expect actual\n@@ -2029,7 +2032,7 @@ do\n \t\tgit update-ref --no-deref refs/heads/regularref $a &&\n \t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"not-a-ref-oid\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: symref-update refs/heads/regularref: invalid oid: not-a-ref-oid\" err &&\n+\t\ttest_grep \"fatal: symref-update refs/heads/regularref: invalid oid: not-a-ref-oid\" err &&\n \t\techo $(git rev-parse $a) >expect &&\n \t\tgit rev-parse refs/heads/regularref >actual &&\n \t\ttest_cmp expect actual\n@@ -2040,7 +2043,7 @@ do\n \t\tgit symbolic-ref refs/heads/symref refs/heads/target2 &&\n \t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"oid\" \"$Z\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: reference already exists\" err &&\n+\t\ttest_grep \"fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: reference already exists\" err &&\n \t\techo refs/heads/target2 >expect &&\n \t\tgit symbolic-ref refs/heads/symref >actual &&\n \t\ttest_cmp expect actual\n@@ -2060,7 +2063,7 @@ do\n \t\tgit symbolic-ref --no-recurse refs/heads/symref >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&\n-\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t\ttest_grep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n \t'\n \n \ttest_expect_success \"stdin $type symref-update regular ref to symref\" '\n@@ -2072,7 +2075,7 @@ do\n \t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&\n-\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t\ttest_grep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n \t'\n \n \ttest_expect_success \"stdin $type batch-updates\" '\ndiff --git a/t/t1403-show-ref.sh b/t/t1403-show-ref.sh\nindex 36c903ca19..7ce5af4ab8 100755\n--- a/t/t1403-show-ref.sh\n+++ b/t/t1403-show-ref.sh\n@@ -213,19 +213,19 @@ test_expect_success 'show-ref --verify with dangling ref' '\n \n test_expect_success 'show-ref sub-modes are mutually exclusive' '\n \ttest_must_fail git show-ref --verify --exclude-existing 2>err &&\n-\tgrep \"verify\" err &&\n-\tgrep \"exclude-existing\" err &&\n-\tgrep \"cannot be used together\" err &&\n+\ttest_grep \"verify\" err &&\n+\ttest_grep \"exclude-existing\" err &&\n+\ttest_grep \"cannot be used together\" err &&\n \n \ttest_must_fail git show-ref --verify --exists 2>err &&\n-\tgrep \"verify\" err &&\n-\tgrep \"exists\" err &&\n-\tgrep \"cannot be used together\" err &&\n+\ttest_grep \"verify\" err &&\n+\ttest_grep \"exists\" err &&\n+\ttest_grep \"cannot be used together\" err &&\n \n \ttest_must_fail git show-ref --exclude-existing --exists 2>err &&\n-\tgrep \"exclude-existing\" err &&\n-\tgrep \"exists\" err &&\n-\tgrep \"cannot be used together\" err\n+\ttest_grep \"exclude-existing\" err &&\n+\ttest_grep \"exists\" err &&\n+\ttest_grep \"cannot be used together\" err\n '\n \n test_done\ndiff --git a/t/t1410-reflog.sh b/t/t1410-reflog.sh\nindex ce71f9a30a..81de1d40eb 100755\n--- a/t/t1410-reflog.sh\n+++ b/t/t1410-reflog.sh\n@@ -108,12 +108,12 @@ test_expect_success setup '\n \n test_expect_success 'correct usage on sub-command -h' '\n \ttest_expect_code 129 git reflog expire -h >err &&\n-\tgrep \"git reflog expire\" err\n+\ttest_grep \"git reflog expire\" err\n '\n \n test_expect_success 'correct usage on \"git reflog show -h\"' '\n \ttest_expect_code 129 git reflog show -h >err &&\n-\tgrep -F \"git reflog [show]\" err\n+\ttest_grep -F \"git reflog [show]\" err\n '\n \n test_expect_success 'pass through -- to sub-command' '\ndiff --git a/t/t1415-worktree-refs.sh b/t/t1415-worktree-refs.sh\nindex 51d79bae83..6b2ad04aef 100755\n--- a/t/t1415-worktree-refs.sh\n+++ b/t/t1415-worktree-refs.sh\n@@ -32,7 +32,7 @@ test_expect_success 'ambiguous main-worktree/HEAD' '\n \ttest_when_finished git update-ref -d refs/heads/main-worktree/HEAD &&\n \tgit update-ref refs/heads/main-worktree/HEAD $(git rev-parse HEAD) &&\n \tgit rev-parse main-worktree/HEAD 2>warn &&\n-\tgrep \"main-worktree/HEAD.*ambiguous\" warn\n+\ttest_grep \"main-worktree/HEAD.*ambiguous\" warn\n '\n \n test_expect_success 'resolve worktrees/xx/HEAD' '\n@@ -45,7 +45,7 @@ test_expect_success 'ambiguous worktrees/xx/HEAD' '\n \tgit update-ref refs/heads/worktrees/wt1/HEAD $(git rev-parse HEAD) &&\n \ttest_when_finished git update-ref -d refs/heads/worktrees/wt1/HEAD &&\n \tgit rev-parse worktrees/wt1/HEAD 2>warn &&\n-\tgrep \"worktrees/wt1/HEAD.*ambiguous\" warn\n+\ttest_grep \"worktrees/wt1/HEAD.*ambiguous\" warn\n '\n \n test_expect_success 'reflog of main-worktree/HEAD' '\ndiff --git a/t/t1430-bad-ref-name.sh b/t/t1430-bad-ref-name.sh\nindex 3ab65f72cd..1ed4c7d2c2 100755\n--- a/t/t1430-bad-ref-name.sh\n+++ b/t/t1430-bad-ref-name.sh\n@@ -47,7 +47,7 @@ test_expect_success 'git branch shows badly named ref as warning' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \tgit branch >output 2>error &&\n \ttest_grep -e \"ignoring ref with broken name refs/heads/broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -d can delete badly named ref' '\n@@ -55,8 +55,8 @@ test_expect_success 'branch -d can delete badly named ref' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \tgit branch -d broken...ref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -D can delete badly named ref' '\n@@ -64,8 +64,8 @@ test_expect_success 'branch -D can delete badly named ref' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \tgit branch -D broken...ref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -D cannot delete non-ref in .git dir' '\n@@ -93,8 +93,8 @@ test_expect_success 'git branch cannot create a badly named ref' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \ttest_must_fail git branch broken...ref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -m cannot rename to a bad ref name' '\n@@ -104,8 +104,8 @@ test_expect_success 'branch -m cannot rename to a bad ref name' '\n \ttest_must_fail git branch -m goodref broken...ref &&\n \ttest_cmp_rev main goodref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_failure 'branch -m can rename from a bad ref name' '\n@@ -115,16 +115,16 @@ test_expect_failure 'branch -m can rename from a bad ref name' '\n \tgit branch -m broken...ref renamed &&\n \ttest_cmp_rev main renamed &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'push cannot create a badly named ref' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \ttest_must_fail git push \"file://$(pwd)\" HEAD:refs/heads/broken...ref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_failure 'push --mirror can delete badly named ref' '\n@@ -144,8 +144,8 @@ test_expect_failure 'push --mirror can delete badly named ref' '\n \t) &&\n \tgit -C src push --mirror \"file://$top/dest\" &&\n \tgit -C dest branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'rev-parse skips symref pointing to broken name' '\n@@ -168,9 +168,9 @@ test_expect_success 'for-each-ref emits warnings for broken names' '\n \ttest-tool ref-store main create-symref refs/heads/broken...symref refs/heads/main &&\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...symref\" &&\n \tgit for-each-ref >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output &&\n-\t! grep -e \"badname\" output &&\n-\t! grep -e \"broken\\.\\.\\.symref\" output &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output &&\n+\ttest_grep ! -e \"badname\" output &&\n+\ttest_grep ! -e \"broken\\.\\.\\.symref\" output &&\n \ttest_grep \"ignoring ref with broken name refs/heads/broken\\.\\.\\.ref\" error &&\n \ttest_grep ! \"ignoring broken ref refs/heads/badname\" error &&\n \ttest_grep \"ignoring ref with broken name refs/heads/broken\\.\\.\\.symref\" error\n@@ -183,8 +183,8 @@ test_expect_success 'update-ref -d can delete broken name' '\n \ttest_must_be_empty output &&\n \ttest_must_be_empty error &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -d can delete broken name' '\n@@ -194,8 +194,8 @@ test_expect_success 'branch -d can delete broken name' '\n \ttest_grep \"Deleted branch broken...ref (was broken)\" output &&\n \ttest_must_be_empty error &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'update-ref --no-deref -d can delete symref to broken name' '\n@@ -313,37 +313,37 @@ test_expect_success 'update-ref -d cannot delete absolute path' '\n test_expect_success 'update-ref --stdin fails create with bad ref name' '\n \techo \"create ~a refs/heads/main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'update-ref --stdin fails update with bad ref name' '\n \techo \"update ~a refs/heads/main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'update-ref --stdin fails delete with bad ref name' '\n \techo \"delete ~a refs/heads/main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'update-ref --stdin -z fails create with bad ref name' '\n \tprintf \"%s\\0\" \"create ~a \" refs/heads/main >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a \" err\n+\ttest_grep \"fatal: invalid ref format: ~a \" err\n '\n \n test_expect_success 'update-ref --stdin -z fails update with bad ref name' '\n \tprintf \"%s\\0\" \"update ~a\" refs/heads/main \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'update-ref --stdin -z fails delete with bad ref name' '\n \tprintf \"%s\\0\" \"delete ~a\" refs/heads/main >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'branch rejects HEAD as a branch name' '\ndiff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh\nindex 54e81c2636..47d460a699 100755\n--- a/t/t1450-fsck.sh\n+++ b/t/t1450-fsck.sh\n@@ -68,7 +68,7 @@ test_expect_success 'object with hash mismatch' '\n \t\tgit update-ref refs/heads/bogus $cmt &&\n \n \t\ttest_must_fail git fsck 2>out &&\n-\t\tgrep \"$oldoid: hash-path mismatch, found at: .*$new\" out\n+\t\ttest_grep \"$oldoid: hash-path mismatch, found at: .*$new\" out\n \t)\n '\n \n@@ -172,7 +172,7 @@ test_expect_success 'commit with multiple signatures is okay' '\n \ttest_when_finished \"git update-ref -d refs/heads/bogus\" &&\n \tgit fsck 2>out &&\n \tcat out &&\n-\t! grep \"commit $new\" out\n+\ttest_grep ! \"commit $new\" out\n '\n \n test_expect_success 'email without @ is okay' '\n@@ -183,7 +183,7 @@ test_expect_success 'email without @ is okay' '\n \tgit update-ref refs/heads/bogus \"$new\" &&\n \ttest_when_finished \"git update-ref -d refs/heads/bogus\" &&\n \tgit fsck 2>out &&\n-\t! grep \"commit $new\" out\n+\ttest_grep ! \"commit $new\" out\n '\n \n test_expect_success 'email with embedded > is not okay' '\n@@ -626,7 +626,7 @@ test_expect_success 'fsck notices excessively large tree entry name' '\n \t\tcd large-name &&\n \t\ttest_commit a-long-name &&\n \t\tgit -c fsck.largePathname=warn:10 fsck 2>out &&\n-\t\tgrep \"warning.*large pathname\" out\n+\t\ttest_grep \"warning.*large pathname\" out\n \t)\n '\n \n@@ -849,7 +849,7 @@ test_expect_success 'fsck errors in packed objects' '\n \ttest_must_fail git fsck 2>out &&\n \ttest_grep \"error in commit $one.* - bad name\" out &&\n \ttest_grep \"error in commit $two.* - bad name\" out &&\n-\t! grep corrupt out\n+\ttest_grep ! corrupt out\n '\n \n test_expect_success 'fsck handles multiple packfiles with big blobs' '\n@@ -1027,7 +1027,7 @@ test_expect_success 'bogus head does not fallback to all heads' '\n \ttest_when_finished \"git rm --cached foo\" &&\n \tremove_object $blob &&\n \ttest_must_fail git fsck $ZERO_OID >out 2>&1 &&\n-\t! grep $blob out\n+\ttest_grep ! $blob out\n '\n \n # Corrupt the checksum on the index.\ndiff --git a/t/t1451-fsck-buffer.sh b/t/t1451-fsck-buffer.sh\nindex 3a3d33f405..14151eabbd 100755\n--- a/t/t1451-fsck-buffer.sh\n+++ b/t/t1451-fsck-buffer.sh\n@@ -46,7 +46,7 @@ check () {\n \t\t\techo \"$content\"\n \t\t} >input &&\n \t\ttest_must_fail git hash-object -t \"$type\" input 2>err &&\n-\t\tgrep \"$fsck\" err\n+\t\ttest_grep \"$fsck\" err\n \t'\n }\n \n@@ -125,7 +125,7 @@ ident_checks tag tagger\n test_expect_success 'truncated tree (short hash)' '\n \tprintf \"100644 foo\\0\\1\\1\\1\\1\" >input &&\n \ttest_must_fail git hash-object -t tree input 2>err &&\n-\tgrep badTree err\n+\ttest_grep badTree err\n '\n \n test_expect_success 'truncated tree (missing nul)' '\n@@ -135,7 +135,7 @@ test_expect_success 'truncated tree (missing nul)' '\n \t# parser does not walk past the end of the buffer).\n \tprintf \"100644 a long filename, or a hash with missing nul?\" >input &&\n \ttest_must_fail git hash-object -t tree input 2>err &&\n-\tgrep badTree err\n+\ttest_grep badTree err\n '\n \n test_done\ndiff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh\nindex 5246468024..8f42697143 100755\n--- a/t/t1460-refs-migrate.sh\n+++ b/t/t1460-refs-migrate.sh\n@@ -212,7 +212,7 @@ do\n \t\t\ttest_commit -C repo initial &&\n \t\t\tgit -C repo refs migrate --dry-run \\\n \t\t\t\t--ref-format=$to_format >output &&\n-\t\t\tgrep \"Finished dry-run migration of refs\" output &&\n+\t\t\ttest_grep \"Finished dry-run migration of refs\" output &&\n \t\t\ttest_path_is_dir repo/.git/ref_migration.* &&\n \t\t\techo $from_format >expect &&\n \t\t\tgit -C repo rev-parse --show-ref-format >actual &&\ndiff --git a/t/t1500-rev-parse.sh b/t/t1500-rev-parse.sh\nindex 38067d95f7..4174ca40c3 100755\n--- a/t/t1500-rev-parse.sh\n+++ b/t/t1500-rev-parse.sh\n@@ -204,7 +204,7 @@ test_expect_success 'rev-parse --show-object-format in repo' '\n \tgit rev-parse --show-object-format=output >actual &&\n \ttest_cmp expect actual &&\n \ttest_must_fail git rev-parse --show-object-format=squeamish-ossifrage 2>err &&\n-\tgrep \"unknown mode for --show-object-format: squeamish-ossifrage\" err\n+\ttest_grep \"unknown mode for --show-object-format: squeamish-ossifrage\" err\n '\n \n \n@@ -228,7 +228,7 @@ test_expect_success RUST 'rev-parse --show-object-format in repo with compat mod\n \t\tgit rev-parse --show-object-format=compat >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest_must_fail git rev-parse --show-object-format=squeamish-ossifrage 2>err &&\n-\t\tgrep \"unknown mode for --show-object-format: squeamish-ossifrage\" err\n+\t\ttest_grep \"unknown mode for --show-object-format: squeamish-ossifrage\" err\n \t) &&\n \tmkdir repo2 &&\n \t(\n@@ -254,7 +254,7 @@ test_expect_success 'rev-parse --show-ref-format with invalid storage' '\n \t\tcd repo &&\n \t\tgit config extensions.refstorage broken &&\n \t\ttest_must_fail git rev-parse --show-ref-format 2>err &&\n-\t\tgrep \"error: invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}broken${SQ}\" err\n+\t\ttest_grep \"error: invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}broken${SQ}\" err\n \t)\n '\n \ndiff --git a/t/t1502-rev-parse-parseopt.sh b/t/t1502-rev-parse-parseopt.sh\nindex 3962f1d288..2cab204376 100755\n--- a/t/t1502-rev-parse-parseopt.sh\n+++ b/t/t1502-rev-parse-parseopt.sh\n@@ -331,7 +331,7 @@ test_expect_success 'ambiguous: --no matches both --noble and --no-noble' '\n \tEOF\n \ttest_expect_code 129 env GIT_TEST_DISALLOW_ABBREVIATED_OPTIONS=false \\\n \tgit rev-parse --parseopt -- <spec 2>err --no &&\n-\tgrep \"error: ambiguous option: no (could be --noble or --no-noble)\" err\n+\ttest_grep \"error: ambiguous option: no (could be --noble or --no-noble)\" err\n '\n \n test_done\ndiff --git a/t/t1503-rev-parse-verify.sh b/t/t1503-rev-parse-verify.sh\nindex 75a708f9ba..87638a4a2c 100755\n--- a/t/t1503-rev-parse-verify.sh\n+++ b/t/t1503-rev-parse-verify.sh\n@@ -62,15 +62,15 @@ test_expect_success 'works with one good rev' '\n \n test_expect_success 'fails with any bad rev or many good revs' '\n \ttest_must_fail git rev-parse --verify 2>error &&\n-\tgrep \"single revision\" error &&\n+\ttest_grep \"single revision\" error &&\n \ttest_must_fail git rev-parse --verify foo 2>error &&\n-\tgrep \"single revision\" error &&\n+\ttest_grep \"single revision\" error &&\n \ttest_must_fail git rev-parse --verify HEAD bar 2>error &&\n-\tgrep \"single revision\" error &&\n+\ttest_grep \"single revision\" error &&\n \ttest_must_fail git rev-parse --verify baz HEAD 2>error &&\n-\tgrep \"single revision\" error &&\n+\ttest_grep \"single revision\" error &&\n \ttest_must_fail git rev-parse --verify $HASH2 HEAD 2>error &&\n-\tgrep \"single revision\" error\n+\ttest_grep \"single revision\" error\n '\n \n test_expect_success 'fails silently when using -q' '\ndiff --git a/t/t1510-repo-setup.sh b/t/t1510-repo-setup.sh\nindex bbfe05b8e4..d330b8df47 100755\n--- a/t/t1510-repo-setup.sh\n+++ b/t/t1510-repo-setup.sh\n@@ -604,7 +604,7 @@ test_expect_success '#20b/c: core.worktree and core.bare conflict' '\n \t\tcd 20b/.git &&\n \t\ttest_must_fail git status >/dev/null\n \t) 2>message &&\n-\tgrep \"core.bare and core.worktree\" message\n+\ttest_grep \"core.bare and core.worktree\" message\n '\n \n test_expect_success '#20d: core.worktree and core.bare OK when working tree not needed' '\n@@ -721,8 +721,8 @@ test_expect_success '#22.2: core.worktree and core.bare conflict' '\n \t\texport GIT_DIR &&\n \t\ttest_must_fail git status 2>result\n \t) &&\n-\tgrep \"core.bare and core.worktree\" 22/.git/result &&\n-\tgrep \"core.bare and core.worktree\" 22/result\n+\ttest_grep \"core.bare and core.worktree\" 22/.git/result &&\n+\ttest_grep \"core.bare and core.worktree\" 22/result\n '\n \n # Case #23: GIT_DIR + GIT_WORK_TREE(+core.worktree) suppresses bareness.\n@@ -767,7 +767,7 @@ test_expect_success '#28: core.worktree and core.bare conflict (gitfile case)' '\n \t\tcd 28 &&\n \t\ttest_must_fail git status\n \t) 2>message &&\n-\tgrep \"core.bare and core.worktree\" message\n+\ttest_grep \"core.bare and core.worktree\" message\n '\n \n # Case #29: GIT_WORK_TREE(+core.worktree) overrides core.bare (gitfile case).\n@@ -791,7 +791,7 @@ test_expect_success '#30: core.worktree and core.bare conflict (gitfile version)\n \t\tcd 30 &&\n \t\ttest_must_fail env GIT_DIR=.git git status 2>result\n \t) &&\n-\tgrep \"core.bare and core.worktree\" 30/result\n+\ttest_grep \"core.bare and core.worktree\" 30/result\n '\n \n # Case #31: GIT_DIR + GIT_WORK_TREE(+core.worktree) suppresses\ndiff --git a/t/t1512-rev-parse-disambiguation.sh b/t/t1512-rev-parse-disambiguation.sh\nindex 1a380a4184..0e6b42ec78 100755\n--- a/t/t1512-rev-parse-disambiguation.sh\n+++ b/t/t1512-rev-parse-disambiguation.sh\n@@ -378,7 +378,7 @@ test_expect_success 'ambiguous 40-hex ref' '\n \tVAL=$(git commit-tree $TREE </dev/null) &&\n \tgit update-ref refs/heads/$REF $VAL &&\n \ttest $(git rev-parse $REF 2>err) = $REF &&\n-\tgrep \"refname.*${REF}.*ambiguous\" err\n+\ttest_grep \"refname.*${REF}.*ambiguous\" err\n '\n \n test_expect_success 'ambiguous short sha1 ref' '\n@@ -387,7 +387,7 @@ test_expect_success 'ambiguous short sha1 ref' '\n \tVAL=$(git commit-tree $TREE </dev/null) &&\n \tgit update-ref refs/heads/$REF $VAL &&\n \ttest $(git rev-parse $REF 2>err) = $VAL &&\n-\tgrep \"refname.*${REF}.*ambiguous\" err\n+\ttest_grep \"refname.*${REF}.*ambiguous\" err\n '\n \n test_expect_success 'ambiguity errors are not repeated (raw)' '\ndiff --git a/t/t1515-rev-parse-outside-repo.sh b/t/t1515-rev-parse-outside-repo.sh\nindex 75e89c4b6e..2cb3c7cc9c 100755\n--- a/t/t1515-rev-parse-outside-repo.sh\n+++ b/t/t1515-rev-parse-outside-repo.sh\n@@ -32,7 +32,7 @@ test_expect_success 'rev-parse --local-env-vars' '\n \tgit rev-parse --local-env-vars >actual &&\n \t# we do not want to depend on the complete list here,\n \t# so just look for something plausible\n-\tgrep ^GIT_DIR actual\n+\ttest_grep ^GIT_DIR actual\n '\n \n test_expect_success 'rev-parse --resolve-git-dir' '\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 0132e772e4..9aae3ff074 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -79,7 +79,7 @@ test_expect_success 'git hook usage' '\n \ttest_expect_code 129 git hook run --unknown 2>err &&\n \ttest_expect_code 129 git hook list &&\n \ttest_expect_code 129 git hook list -h &&\n-\tgrep \"unknown option\" err\n+\ttest_grep \"unknown option\" err\n '\n \n test_expect_success 'git hook list: unknown hook name is rejected' '\n@@ -361,9 +361,9 @@ test_expect_success 'hook can be configured for multiple events' '\n \n \t# 'ghi' should be included in both 'pre-commit' and 'test-hook'\n \tgit hook list pre-commit >actual &&\n-\tgrep \"ghi\" actual &&\n+\ttest_grep \"ghi\" actual &&\n \tgit hook list --allow-unknown-hook-name test-hook >actual &&\n-\tgrep \"ghi\" actual\n+\ttest_grep \"ghi\" actual\n '\n \n test_expect_success 'git hook list shows hooks from the hookdir' '\n@@ -569,7 +569,7 @@ test_expect_success 'git hook run a hook with a bad shebang' '\n \t# TODO: We should emit the same (or at least a more similar)\n \t# error on MINGW (essentially Git for Windows) and all other\n \t# platforms.. See the OS-specific code in start_command()\n-\tgrep -E \"^(error|fatal): cannot (exec|spawn) .*bad-hooks/test-hook\" err\n+\ttest_grep -E \"^(error|fatal): cannot (exec|spawn) .*bad-hooks/test-hook\" err\n '\n \n test_expect_success 'stdin to hooks' '\n@@ -826,8 +826,8 @@ test_expect_success 'git hook run -j2 warns for hooks not marked parallel=true'\n \t# neither hook has parallel=true\n \n \tgit hook run --allow-unknown-hook-name -j2 test-hook >out 2>err &&\n-\tgrep \"hook .hook-1. is not marked as parallel=true\" err &&\n-\tgrep \"hook .hook-2. is not marked as parallel=true\" err\n+\ttest_grep \"hook .hook-1. is not marked as parallel=true\" err &&\n+\ttest_grep \"hook .hook-2. is not marked as parallel=true\" err\n '\n \n test_expect_success 'hook.jobs=1 config runs hooks in series' '\n@@ -1068,7 +1068,7 @@ test_expect_success 'hook.jobs=-1 resolves to online_cpus()' '\n \tcpus=$(test-tool online-cpus) &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.txt\" \\\n \t\tgit hook run --allow-unknown-hook-name test-hook >out 2>err &&\n-\tgrep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n+\ttest_grep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n '\n \n test_expect_success 'hook.<event>.jobs=-1 resolves to online_cpus()' '\n@@ -1081,7 +1081,7 @@ test_expect_success 'hook.<event>.jobs=-1 resolves to online_cpus()' '\n \tcpus=$(test-tool online-cpus) &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.txt\" \\\n \t\tgit hook run --allow-unknown-hook-name test-hook >out 2>err &&\n-\tgrep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n+\ttest_grep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n '\n \n test_expect_success 'git hook run -j-1 resolves to online_cpus()' '\n@@ -1092,7 +1092,7 @@ test_expect_success 'git hook run -j-1 resolves to online_cpus()' '\n \tcpus=$(test-tool online-cpus) &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.txt\" \\\n \t\tgit hook run --allow-unknown-hook-name -j-1 test-hook >out 2>err &&\n-\tgrep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n+\ttest_grep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n '\n \n test_expect_success 'hook.jobs rejects values less than -1' '\ndiff --git a/t/t2004-checkout-cache-temp.sh b/t/t2004-checkout-cache-temp.sh\nindex 0afe0ff7ca..db06c321ed 100755\n--- a/t/t2004-checkout-cache-temp.sh\n+++ b/t/t2004-checkout-cache-temp.sh\n@@ -132,8 +132,8 @@ test_expect_success 'overriding --stage=all resets implied --temp' '\n test_expect_success '--stage=all --no-temp is rejected' '\n \trm -f path* .merge_* actual &&\n \ttest_must_fail git checkout-index --stage=all --no-temp -- path1 2>err &&\n-\tgrep -v \"already exists\" err &&\n-\tgrep \"options .--stage=all. and .--no-temp. cannot be used together\" err\n+\ttest_grep -v \"already exists\" err &&\n+\ttest_grep \"options .--stage=all. and .--no-temp. cannot be used together\" err\n '\n \n test_expect_success 'checkout some stages/one file to temporary files' '\ndiff --git a/t/t2019-checkout-ambiguous-ref.sh b/t/t2019-checkout-ambiguous-ref.sh\nindex 1fcef4be95..0ac35798c2 100755\n--- a/t/t2019-checkout-ambiguous-ref.sh\n+++ b/t/t2019-checkout-ambiguous-ref.sh\n@@ -19,7 +19,7 @@ test_expect_success 'checkout ambiguous ref succeeds' '\n '\n \n test_expect_success 'checkout produces ambiguity warning' '\n-\tgrep \"warning.*ambiguous\" stderr\n+\ttest_grep \"warning.*ambiguous\" stderr\n '\n \n test_expect_success 'checkout chooses branch over tag' '\n@@ -41,7 +41,7 @@ test_expect_success 'checkout vague ref succeeds' '\n '\n \n test_expect_success VAGUENESS_SUCCESS 'checkout produces ambiguity warning' '\n-\tgrep \"warning.*ambiguous\" stderr\n+\ttest_grep \"warning.*ambiguous\" stderr\n '\n \n test_expect_success VAGUENESS_SUCCESS 'checkout chooses branch over tag' '\ndiff --git a/t/t2024-checkout-dwim.sh b/t/t2024-checkout-dwim.sh\nindex a3b1449ef1..752e7f0e61 100755\n--- a/t/t2024-checkout-dwim.sh\n+++ b/t/t2024-checkout-dwim.sh\n@@ -311,7 +311,7 @@ test_expect_success 'loosely defined local base branch is reported correctly' '\n \tgit checkout loose >actual.raw 2>&1 &&\n \tsed -e \"s/loose/BRANCHNAME/g\" <actual.raw >actual &&\n \tstatus_uno_is_clean &&\n-\tgrep BRANCHNAME actual &&\n+\ttest_grep BRANCHNAME actual &&\n \n \ttest_cmp expect actual\n '\n@@ -324,7 +324,7 @@ test_expect_success 'reject when arg could be part of dwim branch' '\n \techo bar >dwim-arg &&\n \ttest_must_fail git checkout dwim-arg &&\n \ttest_must_fail git rev-parse refs/heads/dwim-arg -- &&\n-\tgrep bar dwim-arg\n+\ttest_grep bar dwim-arg\n '\n \n test_expect_success 'disambiguate dwim branch and checkout path (1)' '\n@@ -334,7 +334,7 @@ test_expect_success 'disambiguate dwim branch and checkout path (1)' '\n \techo bar >dwim-arg1 &&\n \tgit checkout -- dwim-arg1 &&\n \ttest_must_fail git rev-parse refs/heads/dwim-arg1 -- &&\n-\tgrep foo dwim-arg1\n+\ttest_grep foo dwim-arg1\n '\n \n test_expect_success 'disambiguate dwim branch and checkout path (2)' '\n@@ -344,7 +344,7 @@ test_expect_success 'disambiguate dwim branch and checkout path (2)' '\n \techo bar >dwim-arg2 &&\n \tgit checkout dwim-arg2 -- &&\n \tgit rev-parse refs/heads/dwim-arg2 -- &&\n-\tgrep bar dwim-arg2\n+\ttest_grep bar dwim-arg2\n '\n \n test_done\ndiff --git a/t/t2030-unresolve-info.sh b/t/t2030-unresolve-info.sh\nindex be3fcdde07..9af24b1204 100755\n--- a/t/t2030-unresolve-info.sh\n+++ b/t/t2030-unresolve-info.sh\n@@ -122,7 +122,7 @@ test_expect_success 'add records checkout -m undoes' '\n \tcheck_resolve_undo removed &&\n \techo the index and the work tree is unmerged again &&\n \tgit diff >actual &&\n-\tgrep \"^++<<<<<<<\" actual\n+\ttest_grep \"^++<<<<<<<\" actual\n '\n \n test_expect_success 'unmerge with plumbing' '\n@@ -173,7 +173,7 @@ test_expect_success 'rerere and rerere forget' '\n \ttest -f .git/rr-cache/$rerere_id/postimage &&\n \tgit checkout -m fi/le &&\n \techo resurrect the conflict &&\n-\tgrep \"^=======\" fi/le &&\n+\ttest_grep \"^=======\" fi/le &&\n \techo reresolve the conflict &&\n \tgit rerere &&\n \ttest \"z$(cat fi/le)\" = zdifferent &&\n@@ -199,7 +199,7 @@ test_expect_success 'rerere and rerere forget (subdirectory)' '\n \ttest -f .git/rr-cache/$rerere_id/postimage &&\n \t(cd fi && git checkout -m le) &&\n \techo resurrect the conflict &&\n-\tgrep \"^=======\" fi/le &&\n+\ttest_grep \"^=======\" fi/le &&\n \techo reresolve the conflict &&\n \t(cd fi && git rerere) &&\n \ttest \"z$(cat fi/le)\" = zdifferent &&\ndiff --git a/t/t2060-switch.sh b/t/t2060-switch.sh\nindex c91c4db936..cec2fe6876 100755\n--- a/t/t2060-switch.sh\n+++ b/t/t2060-switch.sh\n@@ -34,13 +34,13 @@ test_expect_success 'switch and detach' '\n \n test_expect_success 'suggestion to detach' '\n \ttest_must_fail git switch main^{commit} 2>stderr &&\n-\tgrep \"try again with the --detach option\" stderr\n+\ttest_grep \"try again with the --detach option\" stderr\n '\n \n test_expect_success 'suggestion to detach is suppressed with advice.suggestDetachingHead=false' '\n \ttest_config advice.suggestDetachingHead false &&\n \ttest_must_fail git switch main^{commit} 2>stderr &&\n-\t! grep \"try again with the --detach option\" stderr\n+\ttest_grep ! \"try again with the --detach option\" stderr\n '\n \n test_expect_success 'switch and detach current branch' '\n@@ -76,7 +76,7 @@ test_expect_success 'new orphan branch from empty' '\n \tgit switch --orphan new-orphan &&\n \ttest_commit orphan &&\n \tgit cat-file commit refs/heads/new-orphan >commit &&\n-\t! grep ^parent commit &&\n+\ttest_grep ! ^parent commit &&\n \tgit ls-files >tracked-files &&\n \techo orphan.t >expected &&\n \ttest_cmp expected tracked-files\ndiff --git a/t/t2070-restore.sh b/t/t2070-restore.sh\nindex 16d6348b69..2c222fb934 100755\n--- a/t/t2070-restore.sh\n+++ b/t/t2070-restore.sh\n@@ -216,7 +216,7 @@ test_expect_success 'restore with merge options are incompatible with certain op\n \t\t\"--staged --worktree --conflict=zdiff3\"\n \tdo\n \t\ttest_must_fail git restore $opts . 2>err &&\n-\t\tgrep \"cannot be used\" err || return\n+\t\ttest_grep \"cannot be used\" err || return\n \tdone\n '\n \ndiff --git a/t/t2080-parallel-checkout-basics.sh b/t/t2080-parallel-checkout-basics.sh\nindex 5ffe1a41e2..88a6ec1826 100755\n--- a/t/t2080-parallel-checkout-basics.sh\n+++ b/t/t2080-parallel-checkout-basics.sh\n@@ -200,13 +200,13 @@ test_expect_success 'parallel checkout respects --[no]-force' '\n \t\t# We expect 0 workers because there is nothing to be done\n \t\ttest_checkout_workers 0 git checkout HEAD &&\n \t\ttest_path_is_file D &&\n-\t\tgrep changed D &&\n-\t\tgrep changed F.t &&\n+\t\ttest_grep changed D &&\n+\t\ttest_grep changed F.t &&\n \n \t\ttest_checkout_workers 2 git checkout --force HEAD &&\n \t\ttest_path_is_dir D &&\n-\t\tgrep D/F D/F.t &&\n-\t\tgrep F F.t\n+\t\ttest_grep D/F D/F.t &&\n+\t\ttest_grep F F.t\n \t)\n '\n \n@@ -224,8 +224,8 @@ test_expect_success SYMLINKS 'parallel checkout checks for symlinks in leading d\n \n \t\ttest_checkout_workers 2 git checkout --force HEAD &&\n \t\t! test -h D &&\n-\t\tgrep D/A D/A.t &&\n-\t\tgrep D/B D/B.t\n+\t\ttest_grep D/A D/A.t &&\n+\t\ttest_grep D/B D/B.t\n \t)\n '\n \n@@ -268,7 +268,7 @@ test_expect_success '\"git checkout .\" report should not include failed entries'\n \t\t#  - missing-delay.a: the delay filter will drop this path\n \t\t#  - parallel-*.a: the blob will be missing\n \t\t#\n-\t\tgrep \"Updated 3 paths from the index\" err &&\n+\t\ttest_grep \"Updated 3 paths from the index\" err &&\n \t\ttest_stdout_line_count = 3 ls *.b &&\n \t\t! ls *.a\n \t)\ndiff --git a/t/t2081-parallel-checkout-collisions.sh b/t/t2081-parallel-checkout-collisions.sh\nindex f6fcfc0c1e..db45f5f30f 100755\n--- a/t/t2081-parallel-checkout-collisions.sh\n+++ b/t/t2081-parallel-checkout-collisions.sh\n@@ -78,8 +78,8 @@ test_expect_success CASE_INSENSITIVE_FS 'worker detects dirname collision' '\n \n \t# Check that it used the right number of workers and detected the collisions\n \ttest_workers_in_event_trace 2 trace &&\n-\tgrep \"category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/B.}\" trace &&\n-\tgrep \"category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/C.}\" trace\n+\ttest_grep \"category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/B.}\" trace &&\n+\ttest_grep \"category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/C.}\" trace\n '\n \n test_expect_success SYMLINKS,CASE_INSENSITIVE_FS 'do not follow symlinks colliding with leading dir' '\n@@ -115,11 +115,11 @@ test_expect_success CASE_INSENSITIVE_FS 'collision report on clone (w/ racy file\n \tset_checkout_config 2 0 &&\n \ttest_checkout_workers 2 git clone . clone-repo 2>stderr &&\n \n-\tgrep FILE_X stderr &&\n-\tgrep FILE_x stderr &&\n-\tgrep file_X stderr &&\n-\tgrep file_x stderr &&\n-\tgrep \"the following paths have collided\" stderr\n+\ttest_grep FILE_X stderr &&\n+\ttest_grep FILE_x stderr &&\n+\ttest_grep file_X stderr &&\n+\ttest_grep file_x stderr &&\n+\ttest_grep \"the following paths have collided\" stderr\n '\n \n # This test ensures that the collision report code is correctly looking for\n@@ -148,11 +148,11 @@ test_expect_success CASE_INSENSITIVE_FS,!MINGW,!CYGWIN \\\n \ttest_checkout_workers 2 \\\n \t\tgit -c core.ignoreCase=false clone . clone-repo 2>stderr &&\n \n-\tgrep FILE_X stderr &&\n-\tgrep FILE_x stderr &&\n-\tgrep file_X stderr &&\n-\tgrep file_x stderr &&\n-\tgrep \"the following paths have collided\" stderr &&\n+\ttest_grep FILE_X stderr &&\n+\ttest_grep FILE_x stderr &&\n+\ttest_grep file_X stderr &&\n+\ttest_grep file_x stderr &&\n+\ttest_grep \"the following paths have collided\" stderr &&\n \n \t# Check that only \"file_x\" was filtered\n \techo file_x >expected.log &&\ndiff --git a/t/t2082-parallel-checkout-attributes.sh b/t/t2082-parallel-checkout-attributes.sh\nindex 79fb11f139..1f43c2a267 100755\n--- a/t/t2082-parallel-checkout-attributes.sh\n+++ b/t/t2082-parallel-checkout-attributes.sh\n@@ -28,8 +28,8 @@ test_expect_success 'parallel-checkout with ident' '\n \t\trm A B &&\n \t\ttest_checkout_workers 2 git reset --hard &&\n \t\thexsz=$(test_oid hexsz) &&\n-\t\tgrep -E \"\\\\\\$Id: [0-9a-f]{$hexsz} \\\\\\$\" A &&\n-\t\tgrep \"\\\\\\$Id\\\\\\$\" B\n+\t\ttest_grep -E \"\\\\\\$Id: [0-9a-f]{$hexsz} \\\\\\$\" A &&\n+\t\ttest_grep \"\\\\\\$Id\\\\\\$\" B\n \t)\n '\n \n@@ -175,15 +175,15 @@ test_expect_success 'parallel-checkout and delayed checkout' '\n \tverify_checkout delayed &&\n \n \t# Check that the *.d files got to the delay queue and were filtered\n-\tgrep \"smudge W.d .* \\[DELAYED\\]\" delayed.log &&\n-\tgrep \"smudge X.d .* \\[DELAYED\\]\" delayed.log &&\n+\ttest_grep \"smudge W.d .* \\[DELAYED\\]\" delayed.log &&\n+\ttest_grep \"smudge X.d .* \\[DELAYED\\]\" delayed.log &&\n \ttest_cmp delayed/W.d original &&\n \ttest_cmp delayed/X.d original &&\n \n \t# Check that the parallel-eligible entries went to the right queue and\n \t# were not filtered\n-\t! grep \"smudge Y .* \\[DELAYED\\]\" delayed.log &&\n-\t! grep \"smudge Z .* \\[DELAYED\\]\" delayed.log &&\n+\ttest_grep ! \"smudge Y .* \\[DELAYED\\]\" delayed.log &&\n+\ttest_grep ! \"smudge Z .* \\[DELAYED\\]\" delayed.log &&\n \ttest_cmp delayed/Y original &&\n \ttest_cmp delayed/Z original\n '\ndiff --git a/t/t2103-update-index-ignore-missing.sh b/t/t2103-update-index-ignore-missing.sh\nindex 6938ecca86..fbfc86b1fa 100755\n--- a/t/t2103-update-index-ignore-missing.sh\n+++ b/t/t2103-update-index-ignore-missing.sh\n@@ -63,9 +63,9 @@ test_expect_success '--unmerged --refresh' '\n \tgit update-index --unmerged --refresh &&\n \techo 2 >two &&\n \ttest_must_fail git update-index --unmerged --refresh >actual &&\n-\tgrep two actual &&\n-\t! grep one actual &&\n-\t! grep three actual\n+\ttest_grep two actual &&\n+\ttest_grep ! one actual &&\n+\ttest_grep ! three actual\n '\n \n test_expect_success '--ignore-submodules --refresh (1)' '\ndiff --git a/t/t2200-add-update.sh b/t/t2200-add-update.sh\nindex 0a96655cfe..bb019d7a7c 100755\n--- a/t/t2200-add-update.sh\n+++ b/t/t2200-add-update.sh\n@@ -241,7 +241,7 @@ test_expect_success 'add -u avoids rename pairing on unmerged paths' '\n test_expect_success '\"add -u non-existent\" should fail' '\n \ttest_must_fail git add -u non-existent &&\n \tgit ls-files >actual &&\n-\t! grep \"non-existent\" actual\n+\ttest_grep ! \"non-existent\" actual\n '\n \n test_expect_success '\"commit -a\" implies \"add -u\" if index becomes empty' '\ndiff --git a/t/t2203-add-intent.sh b/t/t2203-add-intent.sh\nindex 44c1936e4d..69fef7e646 100755\n--- a/t/t2203-add-intent.sh\n+++ b/t/t2203-add-intent.sh\n@@ -57,7 +57,7 @@ test_expect_success 'intent to add does not clobber existing paths' '\n \tgit add -N file elif &&\n \tempty=$(git hash-object --stdin </dev/null) &&\n \tgit ls-files -s >actual &&\n-\t! grep \"$empty\" actual\n+\ttest_grep ! \"$empty\" actual\n '\n \n test_expect_success 'i-t-a entry is simply ignored' '\n@@ -124,7 +124,7 @@ test_expect_success 'cache-tree does not ignore dir that has i-t-a entries' '\n \t\tgit add -N 2/1 &&\n \t\tgit commit -m committed &&\n \t\tgit ls-tree -r HEAD >actual &&\n-\t\tgrep 2/2 actual\n+\t\ttest_grep 2/2 actual\n \t)\n '\n \n@@ -312,7 +312,7 @@ test_expect_success 'apply --intent-to-add' '\n \techo new >new-ita &&\n \tgit add -N new-ita &&\n \tgit diff >expected &&\n-\tgrep \"new file\" expected &&\n+\ttest_grep \"new file\" expected &&\n \tgit reset --hard &&\n \tgit apply --intent-to-add expected &&\n \tgit diff >actual &&\ndiff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh\nindex 58b4445cc4..87b926728a 100755\n--- a/t/t2400-worktree-add.sh\n+++ b/t/t2400-worktree-add.sh\n@@ -122,7 +122,7 @@ test_expect_success 'die the same branch is already checked out' '\n \t(\n \t\tcd here &&\n \t\ttest_must_fail git checkout newmain 2>actual &&\n-\t\tgrep \"already used by worktree at\" actual\n+\t\ttest_grep \"already used by worktree at\" actual\n \t)\n '\n \n@@ -139,7 +139,7 @@ test_expect_success 'refuse to reset a branch in use elsewhere' '\n \t\tgit rev-parse --verify refs/heads/newmain >new.branch &&\n \t\tgit rev-parse --verify HEAD >new.head &&\n \n-\t\tgrep \"already used by worktree at\" error &&\n+\t\ttest_grep \"already used by worktree at\" error &&\n \t\ttest_cmp old.branch new.branch &&\n \t\ttest_cmp old.head new.head &&\n \n@@ -328,7 +328,7 @@ test_wt_add_excl () {\n \tlocal opts=\"$*\" &&\n \ttest_expect_success \"'worktree add' with '$opts' has mutually exclusive options\" '\n \t\ttest_must_fail git worktree add $opts 2>actual &&\n-\t\tgrep -E \"fatal:( options)? .* cannot be used together\" actual\n+\t\ttest_grep -E \"fatal:( options)? .* cannot be used together\" actual\n \t'\n }\n \n@@ -436,13 +436,13 @@ test_wt_add_orphan_hint () {\n \t\t(cd repo && test_commit commit) &&\n \t\tgit -C repo switch --orphan noref &&\n \t\ttest_must_fail git -C repo worktree add $opts foobar/ 2>actual &&\n-\t\t! grep \"error: unknown switch\" actual &&\n-\t\tgrep \"hint: If you meant to create a worktree containing a new unborn branch\" actual &&\n+\t\ttest_grep ! \"error: unknown switch\" actual &&\n+\t\ttest_grep \"hint: If you meant to create a worktree containing a new unborn branch\" actual &&\n \t\tif [ $use_branch -eq 1 ]\n \t\tthen\n-\t\t\tgrep -E \"^hint: +git worktree add --orphan -b [^ ]+ [^ ]+$\" actual\n+\t\t\ttest_grep -E \"^hint: +git worktree add --orphan -b [^ ]+ [^ ]+$\" actual\n \t\telse\n-\t\t\tgrep -E \"^hint: +git worktree add --orphan [^ ]+$\" actual\n+\t\t\ttest_grep -E \"^hint: +git worktree add --orphan [^ ]+$\" actual\n \t\tfi\n \n \t'\n@@ -457,8 +457,8 @@ test_expect_success \"'worktree add' doesn't show orphan hint in bad/orphan HEAD\n \tgit init repo &&\n \t(cd repo && test_commit commit) &&\n \ttest_must_fail git -C repo worktree add --quiet foobar_branch foobar/ 2>actual &&\n-\t! grep \"error: unknown switch\" actual &&\n-\t! grep \"hint: If you meant to create a worktree containing a new unborn branch\" actual\n+\ttest_grep ! \"error: unknown switch\" actual &&\n+\ttest_grep ! \"hint: If you meant to create a worktree containing a new unborn branch\" actual\n '\n \n test_expect_success 'local clone from linked checkout' '\n@@ -469,7 +469,7 @@ test_expect_success 'local clone from linked checkout' '\n test_expect_success 'local clone --shared from linked checkout' '\n \tgit -C bare worktree add --detach ../baretree &&\n \tgit clone --local --shared baretree bare-clone &&\n-\tgrep /bare/ bare-clone/.git/objects/info/alternates\n+\ttest_grep /bare/ bare-clone/.git/objects/info/alternates\n '\n \n test_expect_success '\"add\" worktree with --no-checkout' '\n@@ -491,7 +491,7 @@ test_expect_success 'put a worktree under rebase' '\n \t\tset_fake_editor &&\n \t\tFAKE_LINES=\"edit 1\" git rebase -i HEAD^ &&\n \t\tgit worktree list >actual &&\n-\t\tgrep \"under-rebase.*detached HEAD\" actual\n+\t\ttest_grep \"under-rebase.*detached HEAD\" actual\n \t)\n '\n \n@@ -533,7 +533,7 @@ test_expect_success 'checkout a branch under bisect' '\n \t\tgit bisect bad &&\n \t\tgit bisect good HEAD~2 &&\n \t\tgit worktree list >actual &&\n-\t\tgrep \"under-bisect.*detached HEAD\" actual &&\n+\t\ttest_grep \"under-bisect.*detached HEAD\" actual &&\n \t\ttest_must_fail git worktree add new-bisect under-bisect &&\n \t\ttest_path_is_missing new-bisect\n \t)\ndiff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh\nindex 93f92e854a..eeaf800d74 100755\n--- a/t/t2402-worktree-list.sh\n+++ b/t/t2402-worktree-list.sh\n@@ -101,8 +101,8 @@ test_expect_success '\"list\" all worktrees with locked annotation' '\n \tgit worktree lock locked &&\n \ttest_when_finished \"git worktree unlock locked\" &&\n \tgit worktree list >out &&\n-\tgrep \"/locked  *[0-9a-f].* locked$\" out &&\n-\t! grep \"/unlocked  *[0-9a-f].* locked$\" out\n+\ttest_grep \"/locked  *[0-9a-f].* locked$\" out &&\n+\ttest_grep ! \"/unlocked  *[0-9a-f].* locked$\" out\n '\n \n test_expect_success '\"list\" all worktrees --porcelain with locked' '\n@@ -143,8 +143,8 @@ test_expect_success '\"list\" all worktrees with prunable annotation' '\n \tgit worktree add --detach unprunable &&\n \trm -rf prunable &&\n \tgit worktree list >out &&\n-\tgrep \"/prunable  *[0-9a-f].* prunable$\" out &&\n-\t! grep \"/unprunable  *[0-9a-f].* prunable$\" out\n+\ttest_grep \"/prunable  *[0-9a-f].* prunable$\" out &&\n+\ttest_grep ! \"/unprunable  *[0-9a-f].* prunable$\" out\n '\n \n test_expect_success '\"list\" all worktrees --porcelain with prunable' '\n@@ -162,8 +162,8 @@ test_expect_success '\"list\" all worktrees with prunable consistent with \"prune\"'\n \tgit worktree add --detach unprunable &&\n \trm -rf prunable &&\n \tgit worktree list >out &&\n-\tgrep \"/prunable  *[0-9a-f].* prunable$\" out &&\n-\t! grep \"/unprunable  *[0-9a-f].* unprunable$\" out &&\n+\ttest_grep \"/prunable  *[0-9a-f].* prunable$\" out &&\n+\ttest_grep ! \"/unprunable  *[0-9a-f].* unprunable$\" out &&\n \tgit worktree prune --verbose 2>out &&\n \ttest_grep \"^Removing worktrees/prunable\" out &&\n \ttest_grep ! \"^Removing worktrees/unprunable\" out\n@@ -184,7 +184,7 @@ test_expect_success '\"list\" all worktrees --verbose with locked' '\n \techo \"$(git -C locked2 rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)\" >expect &&\n \tprintf \"\\tlocked: with reason\\n\" >>expect &&\n \tgit worktree list --verbose >out &&\n-\tgrep \"/locked1  *[0-9a-f].* locked$\" out &&\n+\ttest_grep \"/locked1  *[0-9a-f].* locked$\" out &&\n \tsed -n \"s/  */ /g;/\\/locked2  *[0-9a-f].*$/,/locked: .*$/p\" <out >actual &&\n \ttest_cmp actual expect\n '\n@@ -266,7 +266,7 @@ test_expect_success 'broken main worktree still at the top' '\n \t\ttest_cmp ../expected actual &&\n \t\tgit worktree list >out &&\n \t\thead -n 1 out >actual.2 &&\n-\t\tgrep -F \"(error)\" actual.2\n+\t\ttest_grep -F \"(error)\" actual.2\n \t)\n '\n \ndiff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh\nindex 0bb33e8b1b..69768c1207 100755\n--- a/t/t2403-worktree-move.sh\n+++ b/t/t2403-worktree-move.sh\n@@ -75,8 +75,8 @@ test_expect_success 'move worktree' '\n \tgit worktree move source destination &&\n \ttest_path_is_missing source &&\n \tgit worktree list --porcelain >out &&\n-\tgrep \"^worktree.*/destination$\" out &&\n-\t! grep \"^worktree.*/source$\" out &&\n+\ttest_grep \"^worktree.*/destination$\" out &&\n+\ttest_grep ! \"^worktree.*/source$\" out &&\n \tgit -C destination log --format=%s >actual2 &&\n \techo init >expected2 &&\n \ttest_cmp expected2 actual2\n@@ -92,7 +92,7 @@ test_expect_success 'move worktree to another dir' '\n \ttest_when_finished \"git worktree move some-dir/destination destination\" &&\n \ttest_path_is_missing destination &&\n \tgit worktree list --porcelain >out &&\n-\tgrep \"^worktree.*/some-dir/destination$\" out &&\n+\ttest_grep \"^worktree.*/some-dir/destination$\" out &&\n \tgit -C some-dir/destination log --format=%s >actual2 &&\n \techo init >expected2 &&\n \ttest_cmp expected2 actual2\ndiff --git a/t/t2405-worktree-submodule.sh b/t/t2405-worktree-submodule.sh\nindex 11018f37c7..f5c94d490b 100755\n--- a/t/t2405-worktree-submodule.sh\n+++ b/t/t2405-worktree-submodule.sh\n@@ -36,7 +36,7 @@ test_expect_success 'add superproject worktree' '\n \n test_expect_failure 'submodule is checked out just after worktree add' '\n \tgit -C worktree diff --submodule main\"^!\" >out &&\n-\tgrep \"file1 updated\" out\n+\ttest_grep \"file1 updated\" out\n '\n \n test_expect_success 'add superproject worktree and initialize submodules' '\n@@ -46,7 +46,7 @@ test_expect_success 'add superproject worktree and initialize submodules' '\n \n test_expect_success 'submodule is checked out just after submodule update in linked worktree' '\n \tgit -C worktree-submodule-update diff --submodule main\"^!\" >out &&\n-\tgrep \"file1 updated\" out\n+\ttest_grep \"file1 updated\" out\n '\n \n test_expect_success 'add superproject worktree and manually add submodule worktree' '\n@@ -56,7 +56,7 @@ test_expect_success 'add superproject worktree and manually add submodule worktr\n \n test_expect_success 'submodule is checked out after manually adding submodule worktree' '\n \tgit -C linked_submodule diff --submodule main\"^!\" >out &&\n-\tgrep \"file1 updated\" out\n+\ttest_grep \"file1 updated\" out\n '\n \n test_expect_success 'checkout --recurse-submodules uses $GIT_DIR for submodules in a linked worktree' '\ndiff --git a/t/t2407-worktree-heads.sh b/t/t2407-worktree-heads.sh\nindex 57c201869f..a6d7e0f83b 100755\n--- a/t/t2407-worktree-heads.sh\n+++ b/t/t2407-worktree-heads.sh\n@@ -41,10 +41,10 @@ test_expect_success 'refuse to overwrite: checked out in worktree' '\n \tfor i in 1 2 3 4\n \tdo\n \t\ttest_must_fail git branch -f wt-$i HEAD 2>err &&\n-\t\tgrep \"cannot force update the branch\" err &&\n+\t\ttest_grep \"cannot force update the branch\" err &&\n \n \t\ttest_must_fail git branch -D wt-$i 2>err &&\n-\t\tgrep \"cannot delete branch\" err || return 1\n+\t\ttest_grep \"cannot delete branch\" err || return 1\n \tdone\n '\n \n@@ -57,7 +57,7 @@ test_expect_success 'refuse to overwrite: worktree in bisect' '\n \tgit -C wt-4 bisect good wt-1 &&\n \n \ttest_must_fail git branch -f wt-4 HEAD 2>err &&\n-\tgrep \"cannot force update the branch '\\''wt-4'\\'' used by worktree at.*wt-4\" err\n+\ttest_grep \"cannot force update the branch '\\''wt-4'\\'' used by worktree at.*wt-4\" err\n '\n \n test_expect_success 'refuse to overwrite: worktree in rebase (apply)' '\n@@ -67,7 +67,7 @@ test_expect_success 'refuse to overwrite: worktree in rebase (apply)' '\n \ttest_must_fail git -C wt-2 rebase --apply conflict-2 &&\n \n \ttest_must_fail git branch -f wt-2 HEAD 2>err &&\n-\tgrep \"cannot force update the branch '\\''wt-2'\\'' used by worktree at.*wt-2\" err\n+\ttest_grep \"cannot force update the branch '\\''wt-2'\\'' used by worktree at.*wt-2\" err\n '\n \n test_expect_success 'refuse to overwrite: worktree in rebase (merge)' '\n@@ -77,7 +77,7 @@ test_expect_success 'refuse to overwrite: worktree in rebase (merge)' '\n \ttest_must_fail git -C wt-2 rebase conflict-2 &&\n \n \ttest_must_fail git branch -f wt-2 HEAD 2>err &&\n-\tgrep \"cannot force update the branch '\\''wt-2'\\'' used by worktree at.*wt-2\" err\n+\ttest_grep \"cannot force update the branch '\\''wt-2'\\'' used by worktree at.*wt-2\" err\n '\n \n test_expect_success 'refuse to overwrite: worktree in rebase with --update-refs' '\n@@ -89,19 +89,19 @@ test_expect_success 'refuse to overwrite: worktree in rebase with --update-refs'\n \tfor i in 3 4\n \tdo\n \t\ttest_must_fail git branch -f can-be-updated HEAD 2>err &&\n-\t\tgrep \"cannot force update the branch '\\''can-be-updated'\\'' used by worktree at.*wt-3\" err ||\n+\t\ttest_grep \"cannot force update the branch '\\''can-be-updated'\\'' used by worktree at.*wt-3\" err ||\n \t\t\treturn 1\n \tdone\n '\n \n test_expect_success 'refuse to fetch over ref: checked out' '\n \ttest_must_fail git fetch server +refs/heads/wt-3:refs/heads/wt-3 2>err &&\n-\tgrep \"refusing to fetch into branch '\\''refs/heads/wt-3'\\''\" err &&\n+\ttest_grep \"refusing to fetch into branch '\\''refs/heads/wt-3'\\''\" err &&\n \n \t# General fetch into refs/heads/ will fail on first ref,\n \t# so use a generic error message check.\n \ttest_must_fail git fetch server +refs/heads/*:refs/heads/* 2>err &&\n-\tgrep \"refusing to fetch into branch\" err\n+\ttest_grep \"refusing to fetch into branch\" err\n '\n \n test_expect_success 'refuse to fetch over ref: worktree in bisect' '\n@@ -113,7 +113,7 @@ test_expect_success 'refuse to fetch over ref: worktree in bisect' '\n \tgit -C wt-4 bisect good wt-1 &&\n \n \ttest_must_fail git fetch server +refs/heads/wt-4:refs/heads/wt-4 2>err &&\n-\tgrep \"refusing to fetch into branch\" err\n+\ttest_grep \"refusing to fetch into branch\" err\n '\n \n test_expect_success 'refuse to fetch over ref: worktree in rebase' '\n@@ -123,7 +123,7 @@ test_expect_success 'refuse to fetch over ref: worktree in rebase' '\n \ttest_must_fail git -C wt-3 rebase conflict-3 &&\n \n \ttest_must_fail git fetch server +refs/heads/wt-3:refs/heads/wt-3 2>err &&\n-\tgrep \"refusing to fetch into branch\" err\n+\ttest_grep \"refusing to fetch into branch\" err\n '\n \n test_expect_success 'refuse to overwrite when in error states' '\n@@ -149,7 +149,7 @@ test_expect_success 'refuse to overwrite when in error states' '\n \tfor i in 1 2\n \tdo\n \t\ttest_must_fail git branch -f fake-$i HEAD 2>err &&\n-\t\tgrep \"cannot force update the branch '\\''fake-$i'\\'' used by worktree at\" err ||\n+\t\ttest_grep \"cannot force update the branch '\\''fake-$i'\\'' used by worktree at\" err ||\n \t\t\treturn 1\n \tdone\n '\n@@ -161,13 +161,13 @@ test_expect_success 'refuse to overwrite during rebase with --update-refs' '\n \t(\n \t\tset_cat_todo_editor &&\n \t\ttest_must_fail git rebase -i --update-refs HEAD~3 >todo &&\n-\t\t! grep \"update-refs\" todo\n+\t\ttest_grep ! \"update-refs\" todo\n \t) &&\n \tgit branch -f allow-update HEAD~2 &&\n \t(\n \t\tset_cat_todo_editor &&\n \t\ttest_must_fail git rebase -i --update-refs HEAD~3 >todo &&\n-\t\tgrep \"update-ref refs/heads/allow-update\" todo\n+\t\ttest_grep \"update-ref refs/heads/allow-update\" todo\n \t)\n '\n \ndiff --git a/t/t2500-untracked-overwriting.sh b/t/t2500-untracked-overwriting.sh\nindex 5c0bf4d21f..8e84f29e8d 100755\n--- a/t/t2500-untracked-overwriting.sh\n+++ b/t/t2500-untracked-overwriting.sh\n@@ -51,7 +51,7 @@ test_expect_success 'reset --merge will preserve untracked files/dirs' '\n \n \t\ttest_must_fail git reset --merge work 2>error &&\n \t\ttest_cmp expect foo.t/file &&\n-\t\tgrep \"Updating .foo.t. would lose untracked files\" error\n+\t\ttest_grep \"Updating .foo.t. would lose untracked files\" error\n \t)\n '\n \n@@ -66,7 +66,7 @@ test_expect_success 'reset --keep will preserve untracked files/dirs' '\n \n \t\ttest_must_fail git reset --merge work 2>error &&\n \t\ttest_cmp expect foo.t/file &&\n-\t\tgrep \"Updating.*foo.t.*would lose untracked files\" error\n+\t\ttest_grep \"Updating.*foo.t.*would lose untracked files\" error\n \t)\n '\n \n@@ -214,7 +214,7 @@ test_expect_success 'git am --abort and untracked dir vs. unmerged file' '\n \n \t\ttest_must_fail git am --abort 2>errors &&\n \t\ttest_path_is_dir filler &&\n-\t\tgrep \"Updating .filler. would lose untracked files in it\" errors\n+\t\ttest_grep \"Updating .filler. would lose untracked files in it\" errors\n \t)\n '\n \n@@ -237,7 +237,7 @@ test_expect_success 'git am --skip and untracked dir vs deleted file' '\n \t\t# Change our mind about resolutions, just skip this patch\n \t\ttest_must_fail git am --skip 2>errors &&\n \t\ttest_path_is_dir newfile &&\n-\t\tgrep \"Updating .newfile. would lose untracked files in it\" errors\n+\t\ttest_grep \"Updating .newfile. would lose untracked files in it\" errors\n \t)\n '\n \ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex be9140bbaa..eb6451d71d 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -114,7 +114,7 @@ test_expect_success 'merge fails if cwd needs to be removed; recursive friendly'\n \t) &&\n \n \ttest_path_is_dir dirORfile &&\n-\tgrep \"Refusing to remove the current working directory\" error\n+\ttest_grep \"Refusing to remove the current working directory\" error\n '\n \n test_expect_success 'merge fails if cwd needs to be removed' '\n@@ -179,7 +179,7 @@ test_incidental_untracked_dir_removal () {\n test_expect_success 'clean does not remove cwd incidentally' '\n \ttest_incidental_untracked_dir_removal \\\n \t\tgit -C .. clean -fd -e warnings . >warnings &&\n-\tgrep \"Refusing to remove current working directory\" warnings\n+\ttest_grep \"Refusing to remove current working directory\" warnings\n '\n \n test_expect_success 'stash does not remove cwd incidentally' '\ndiff --git a/t/t3001-ls-files-others-exclude.sh b/t/t3001-ls-files-others-exclude.sh\nindex 202fb8d9ea..29a0a25b30 100755\n--- a/t/t3001-ls-files-others-exclude.sh\n+++ b/t/t3001-ls-files-others-exclude.sh\n@@ -161,21 +161,21 @@ test_expect_success 'trailing slash in exclude forces directory match (1)' '\n \n \t>two &&\n \tgit ls-files --others --exclude=two/ >output &&\n-\tgrep \"^two\" output\n+\ttest_grep \"^two\" output\n \n '\n \n test_expect_success 'trailing slash in exclude forces directory match (2)' '\n \n \tgit ls-files --others --exclude=one/a.1/ >output &&\n-\tgrep \"^one/a.1\" output\n+\ttest_grep \"^one/a.1\" output\n \n '\n \n test_expect_success 'negated exclude matches can override previous ones' '\n \n \tgit ls-files --others --exclude=\"a.*\" --exclude=\"!a.1\" >output &&\n-\tgrep \"^a.1\" output\n+\ttest_grep \"^a.1\" output\n '\n \n test_expect_success 'excluded directory overrides content patterns' '\ndiff --git a/t/t3007-ls-files-recurse-submodules.sh b/t/t3007-ls-files-recurse-submodules.sh\nindex 61771eec83..218964edab 100755\n--- a/t/t3007-ls-files-recurse-submodules.sh\n+++ b/t/t3007-ls-files-recurse-submodules.sh\n@@ -302,7 +302,7 @@ test_expect_success '--recurse-submodules does not support --error-unmatch' '\n test_expect_success '--recurse-submodules parses submodule repo config' '\n \ttest_config -C submodule index.sparse \"invalid non-boolean value\" &&\n \ttest_must_fail git ls-files --recurse-submodules 2>err &&\n-\tgrep \"bad boolean config value\" err\n+\ttest_grep \"bad boolean config value\" err\n '\n \n test_expect_success '--recurse-submodules parses submodule worktree config' '\n@@ -310,7 +310,7 @@ test_expect_success '--recurse-submodules parses submodule worktree config' '\n \ttest_config -C submodule --worktree index.sparse \"invalid non-boolean value\" &&\n \n \ttest_must_fail git ls-files --recurse-submodules 2>err &&\n-\tgrep \"bad boolean config value\" err\n+\ttest_grep \"bad boolean config value\" err\n '\n \n test_expect_success '--recurse-submodules submodules ignore super project worktreeConfig extension' '\n@@ -329,7 +329,7 @@ test_expect_success '--recurse-submodules submodules ignore super project worktr\n \t# With extensions.worktreeConfig disabled in the submodule, the invalid\n \t# worktree config is not picked up.\n \tgit ls-files --recurse-submodules 2>err &&\n-\t! grep \"bad boolean config value\" err\n+\ttest_grep ! \"bad boolean config value\" err\n '\n \n test_incompatible_with_recurse_submodules () {\ndiff --git a/t/t3200-branch.sh b/t/t3200-branch.sh\nindex e7829c2c4b..bd839998b1 100755\n--- a/t/t3200-branch.sh\n+++ b/t/t3200-branch.sh\n@@ -204,7 +204,7 @@ test_expect_success 'git branch -M baz bam should succeed when baz is checked ou\n \n test_expect_success 'git branch -M baz bam should add entries to HEAD reflog' '\n \tgit reflog show HEAD >actual &&\n-\tgrep \"HEAD@{0}: Branch: renamed refs/heads/baz to refs/heads/bam\" actual\n+\ttest_grep \"HEAD@{0}: Branch: renamed refs/heads/baz to refs/heads/bam\" actual\n '\n \n test_expect_success 'git branch -M should leave orphaned HEAD alone' '\n@@ -339,7 +339,7 @@ test_expect_success 'git branch -d on orphan HEAD (unmerged)' '\n \ttest_when_finished \"git branch -D to-delete\" &&\n \tgit branch to-delete main &&\n \ttest_must_fail git branch -d to-delete 2>err &&\n-\tgrep \"not fully merged\" err\n+\ttest_grep \"not fully merged\" err\n '\n \n test_expect_success 'git branch -d on orphan HEAD (unmerged, graph)' '\n@@ -350,7 +350,7 @@ test_expect_success 'git branch -d on orphan HEAD (unmerged, graph)' '\n \ttest_when_finished \"rm -rf .git/objects/commit-graph*\" &&\n \tgit commit-graph write --reachable &&\n \ttest_must_fail git branch -d to-delete 2>err &&\n-\tgrep \"not fully merged\" err\n+\ttest_grep \"not fully merged\" err\n '\n \n test_expect_success 'git branch -v -d t should work' '\n@@ -712,7 +712,7 @@ test_expect_success 'git branch -C c1 c2 should succeed when c1 is checked out'\n test_expect_success 'git branch -C c1 c2 should never touch HEAD' '\n \tmsg=\"Branch: copied refs/heads/c1 to refs/heads/c2\" &&\n \tgit reflog HEAD >actual &&\n-\t! grep \"$msg$\" actual\n+\ttest_grep ! \"$msg$\" actual\n '\n \n test_expect_success 'git branch -C main should work when main is checked out' '\n@@ -930,7 +930,7 @@ test_expect_success 'deleting currently checked out branch fails' '\n \tgit worktree add -b my7 my7 &&\n \ttest_must_fail git -C my7 branch -d my7 &&\n \ttest_must_fail git branch -d my7 2>actual &&\n-\tgrep \"^error: cannot delete branch .my7. used by worktree at \" actual &&\n+\ttest_grep \"^error: cannot delete branch .my7. used by worktree at \" actual &&\n \trm -r my7 &&\n \tgit worktree prune\n '\n@@ -941,7 +941,7 @@ test_expect_success 'deleting in-use branch fails' '\n \tgit -C my7 bisect start HEAD HEAD~2 &&\n \ttest_must_fail git -C my7 branch -d my7 &&\n \ttest_must_fail git branch -d my7 2>actual &&\n-\tgrep \"^error: cannot delete branch .my7. used by worktree at \" actual &&\n+\ttest_grep \"^error: cannot delete branch .my7. used by worktree at \" actual &&\n \trm -r my7 &&\n \tgit worktree prune\n '\ndiff --git a/t/t3202-show-branch.sh b/t/t3202-show-branch.sh\nindex a1139f79e2..f402334936 100755\n--- a/t/t3202-show-branch.sh\n+++ b/t/t3202-show-branch.sh\n@@ -123,13 +123,13 @@ test_expect_success 'show-branch --sparse' '\n \tgit commit --allow-empty -m \"another\" &&\n \n \tgit show-branch --sparse >out &&\n-\tgrep \"merge 1 and 10 to make A\" out &&\n+\ttest_grep \"merge 1 and 10 to make A\" out &&\n \n \tgit show-branch >out &&\n-\t! grep \"merge 1 and 10 to make A\" out &&\n+\ttest_grep ! \"merge 1 and 10 to make A\" out &&\n \n \tgit show-branch --no-sparse >out &&\n-\t! grep \"merge 1 and 10 to make A\" out\n+\ttest_grep ! \"merge 1 and 10 to make A\" out\n '\n \n test_expect_success 'setup show branch --list' '\n@@ -189,7 +189,7 @@ while read combo\n do\n \ttest_expect_success \"show-branch $combo (should fail)\" '\n \t\ttest_must_fail git show-branch $combo 2>error &&\n-\t\tgrep -e \"cannot be used together\" -e \"usage:\" error\n+\t\ttest_grep -e \"cannot be used together\" -e \"usage:\" error\n \t'\n done <<\\EOF\n --all --reflog\n@@ -203,7 +203,7 @@ for opt in topo-order date-order reflog\n do\n \ttest_expect_success \"show-branch --no-$opt (should fail)\" '\n \t\ttest_must_fail git show-branch --no-$opt 2>err &&\n-\t\tgrep \"unknown option .no-$opt.\" err\n+\t\ttest_grep \"unknown option .no-$opt.\" err\n \t'\n done\n \ndiff --git a/t/t3203-bran"},{"id":"547144","messageId":"xmqqtsqedxmt.fsf@gitster.g","threadId":"65748","inReplyTo":"pull.2135.v3.git.1783054466.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 0/6] t: add greplint.pl and convert grep to test_grep","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-05T01:38:02Z","receivedAt":"2026-07-05T01:38:05Z","isPatch":true,"body":"\"Michael Montalbo via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> Changes since v2:\n>\n>  * t3420-rebase-autostash: dropped the change to the '! grep dirty file3'\n>    line under 'rebase --quit'. As SZEDER pointed out, file3 never exists in\n>    the conflicted state, so that grep was passing only because it could not\n>    open the file. SZEDER's fix (sg/t3420-do-not-grep-in-missing-file, now in\n>    'next') replaces the line with 'test_path_is_missing file3', which is the\n>    right check; this series simply leaves that line to his fix.\n>\n>  * Audited the remaining '# lint-ok' annotations for the same \"grep a file\n>    that never exists with correctly running Git\" gotcha, as Junio suggested.\n>    The rule the audit applies: 'grep' becomes 'test_grep' only where its\n>    exit code is the assertion; grep that produces data (a filter) or that\n>    reads a file whose presence is conditional stays a plain 'grep', because\n>    test_grep BUGs on a missing file.\n>    \n>    * t5537 (.git/shallow): the file is still present after the repack (the\n>      client stays shallow), so the assertion is converted to 'test_grep !'\n>      like any other; the \"may not exist\" note was wrong.\n>    \n>    * t1400 (.git/packed-refs): the file exists only with the files backend.\n>      Guarded the packed-refs check with a REFFILES prerequisite; the\n>      backend-agnostic 'git show-ref' check that follows still runs under\n>      every backend.\n>    \n>    * t7450 (squatting-clone/d/a/git~2): kept as '! grep' with an improved '#\n>      lint-ok'. 'git~2' is the NTFS 8.3 short name of a planted '..git' decoy\n>      and only exists when 8.3 short-name generation is enabled. Verified on\n>      a Windows VM: with 8.3 disabled (the modern default) the short name is\n>      absent, the '! grep' correctly tolerates it, and a plain test_grep\n>      would BUG. So this one deliberately stays a missing-file-tolerant grep.\n>    \n>    * t5326 and t5702 remain annotated: these are genuine data filters (grep\n>      produces data that is redirected/captured, not an assertion).\n\nGreat.\n\n>      ++   test_grep requires <file> to exist and will BUG otherwise.\n>      ++   When a file's presence is conditional (a backend-specific\n>      ++   file, or a path that only exists on some platforms, such as\n>      ++   an NTFS 8.3 short name), keep a plain guarded 'grep' instead.\n\nIt is not quite clear if I can follow this instruction myself,\nwithout knowing what a \"plain guarded 'grep'\" is, unfortunately.  Is\nit different from bog-standard grep?\n\n>      @@ t/t1400-update-ref.sh: test_expect_success \"move $m (by HEAD)\" '\n>        \ttest_when_finished \"git update-ref -d $m\" &&\n>        \tgit update-ref -d HEAD $B &&\n>       -\t! grep \"$m\" .git/packed-refs &&\n>      -+\t! grep \"$m\" .git/packed-refs && # lint-ok: file may not exist (reftable)\n>      ++\tif test_have_prereq REFFILES\n>      ++\tthen\n>      ++\t\ttest_grep ! \"$m\" .git/packed-refs\n>      ++\tfi &&\n\nThe intent is shown very well in this version (admittedly, the\nlint-ok comment is readable but only by humans and LLMs).  Here, we\nexpect .git/packed-refs only while REFFILES prerequiste is active.\n\nThanks.\n"},{"id":"547145","messageId":"CAC2QwmKvb24Rhx44aey8MuGtV5=3m++5OZwcCrMGwqvAhjdmLQ@mail.gmail.com","threadId":"65748","inReplyTo":"xmqqtsqedxmt.fsf@gitster.g","subject":"Re: [PATCH v3 0/6] t: add greplint.pl and convert grep to test_grep","fromName":"Michael Montalbo","fromEmail":"mmontalbo@gmail.com","sentAt":"2026-07-05T02:49:51Z","receivedAt":"2026-07-05T02:50:04Z","isPatch":true,"body":"On Sat, Jul 4, 2026 at 6:38 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> It is not quite clear if I can follow this instruction myself,\n> without knowing what a \"plain guarded 'grep'\" is, unfortunately.  Is\n> it different from bog-standard grep?\n>\n\nI agree with you that the word \"guarded\" is really confusing here\nafter reading it\nagain. The suggestion should just be to use \"plain `grep`\" with no mention of a\nguard. Will remove it.\n\nMy original intent was something along the lines of \"guarded against BUG'ing\non missing file\", but the wording is confusing and, worse, could encourage the\nanti-pattern of using grep to test for file existence.\n"},{"id":"547177","messageId":"pull.2135.v4.git.1783314119.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v3.git.1783054466.gitgitgadget@gmail.com","subject":"[PATCH v4 0/6] t: add greplint.pl and convert grep to test_grep","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-06T05:01:52Z","receivedAt":"2026-07-06T05:02:02Z","isPatch":true,"body":"test_grep is a wrapper around grep for test assertions that prints the file\ncontents on failure for easier debugging. Bare grep fails silently, making\nit hard to diagnose what went wrong.\n\nThis series converts existing bare grep assertions to test_grep and adds\ngreplint.pl to prevent new ones from being introduced.\n\nPatch 1 documents test_grep in t/README.\n\nPatch 2 fixes three greps missing file arguments (t2402, t7507, t7700). They\nwere reading empty stdin and passing vacuously.\n\nPatch 3 extracts chainlint's Lexer, ShellParser, and ScriptParser into a\nshared module (lib-shell-parser.pl) so greplint.pl can reuse the same\ntokenizer. No functional change to chainlint.\n\nPatch 4 fixes a latent line-counting bug in scan_dqstring where newlines\nfrom $() bodies inside double-quoted strings were counted twice. This does\nnot affect chainlint (which uses byte offsets) but matters for greplint.pl's\nline-number reporting.\n\nPatch 5 converts existing assertion greps to test_grep, including sourced\ntest helpers. Greps used as data filters are left unconverted with lint-ok\nannotations; an assertion on a conditionally-present file is guarded on that\ncondition (a prerequisite, or a 'test -e') and uses test_grep inside the\nguard.\n\nPatch 6 adds greplint.pl with test fixtures (modeled on chainlint/) and\nwires it into the Makefile as test-greplint and check-greplint.\n\nChanges since v3:\n\n * t/README: reworded the guidance to encourage explicit file existence /\n   pre-req checks. Instead of \"keep a plain guarded 'grep'\", it now says to\n   guard the assertion on the condition that governs the file's presence (a\n   prerequisite, or a 'test -e' on the path) and use test_grep inside the\n   guard, with a worked example:\n   \n   if test_have_prereq REFFILES\n   then\n           test_grep ! \"$refname\" .git/packed-refs\n   fi\n   \n\n * t7450 (squatting-clone/d/a/git~2): converted the last\n   missing-file-tolerant '! grep' to that guarded form:\n   \n   if test -f squatting-clone/d/a/git\\~2\n   then\n           test_grep ! gitdir squatting-clone/d/a/git\\~2\n   fi\n   \n   \n   'git~2' is the NTFS 8.3 short name of the planted '..git' file and exists\n   only when 8.3 short-name generation is enabled. Use 'test -f' so the\n   conditional presence is now explicit instead of relying on grep's\n   tolerance for a missing file, and the assertion gains test_grep's\n   diagnostics where git~2 does materialize. This removes the only '#\n   lint-ok' in the series that guarded an assertion rather than a data\n   filter.\n\n * greplint.pl: updated the lint_ok() comment to match. A '# lint-ok'\n   annotation now documents a single case: a grep acting as a data filter\n   whose output is consumed by a redirect or pipe on an enclosing compound\n   command (a subshell or brace group), which the filter heuristic cannot\n   detect locally. The \"file may not exist\" rationale is gone; the two\n   remaining annotated greps (t5326, t5702) are exactly this shape.\n\nNote on ordering: this series leaves the t3420 '! grep dirty file3' line\nuntouched (per the request to omit it), so it depends on\nsg/t3420-do-not-grep-in-missing-file, which replaces that line with\n'test_path_is_missing file3' and is already in 'next'. Applied on top of\nthat topic the series is lint-clean with no edits. On a plain 'master' that\ndoes not yet contain it, the greplint check added by the final patch will\nflag exactly that one line by design (the t3420 test itself still passes;\nonly the static lint fires).\n\nKnown limitation / follow-up:\n\nAssertions like grep pattern file >/dev/null and grep pattern <file are not\nconverted because greplint.pl treats any redirect as a filter, so it does\nnot flag them. The former could be converted, but test_grep prints the\nmatching line on success just as grep does, so the >/dev/null would have to\nbe kept or dropped as a judgment call. The latter requires turning the <file\nredirect into a positional file argument, since test_grep reads a named file\nrather than stdin. Both are left as bare grep with no annotation: unlike the\ngenuine data-filter greps, they carry no '# lint-ok' marker, because\ngreplint silently classifies any redirect as a filter and never flags them.\nA follow-up series can address these once a convention is established.\n\nMichael Montalbo (6):\n  t/README: document test_grep helper\n  t: fix grep assertions missing file arguments\n  t: extract chainlint's parser into shared module\n  t: fix Lexer line count for $() inside double-quoted strings\n  t: convert grep assertions to test_grep\n  t: add greplint to detect bare grep assertions\n\n t/.gitattributes                              |   2 +\n t/Makefile                                    |  29 +-\n t/README                                      |  34 ++\n t/chainlint.pl                                | 529 +----------------\n t/for-each-ref-tests.sh                       |  12 +-\n t/greplint-cat.pl                             |  27 +\n t/greplint.pl                                 | 258 +++++++++\n t/greplint/bare-grep-after-and.expect         |   1 +\n t/greplint/bare-grep-after-and.test           |   4 +\n t/greplint/bare-grep-after-semicolon.expect   |   1 +\n t/greplint/bare-grep-after-semicolon.test     |   4 +\n t/greplint/bare-grep-compound-body.expect     |   3 +\n t/greplint/bare-grep-compound-body.test       |  17 +\n t/greplint/bare-grep-count-mode.expect        |   1 +\n t/greplint/bare-grep-count-mode.test          |   3 +\n t/greplint/bare-grep-explicit-pattern.expect  |   1 +\n t/greplint/bare-grep-explicit-pattern.test    |   3 +\n t/greplint/bare-grep-flags.expect             |   1 +\n t/greplint/bare-grep-flags.test               |   3 +\n t/greplint/bare-grep-lint-ok.expect           |   0\n t/greplint/bare-grep-lint-ok.test             |   4 +\n t/greplint/bare-grep-negated.expect           |   1 +\n t/greplint/bare-grep-negated.test             |   3 +\n t/greplint/bare-grep-pattern-file.expect      |   1 +\n t/greplint/bare-grep-pattern-file.test        |   3 +\n t/greplint/bare-grep-simple.expect            |   1 +\n t/greplint/bare-grep-simple.test              |   3 +\n t/greplint/bare-grep-subshell.expect          |   1 +\n t/greplint/bare-grep-subshell.test            |   5 +\n .../dqstring-continuation-offset.expect       |   1 +\n t/greplint/dqstring-continuation-offset.test  |  11 +\n t/greplint/filter-command-substitution.expect |   0\n t/greplint/filter-command-substitution.test   |   3 +\n t/greplint/filter-pipe-input.expect           |   0\n t/greplint/filter-pipe-input.test             |   3 +\n t/greplint/filter-pipe-output.expect          |   0\n t/greplint/filter-pipe-output.test            |   3 +\n t/greplint/filter-redirect-output.expect      |   0\n t/greplint/filter-redirect-output.test        |   3 +\n t/greplint/filter-stdin-redirect.expect       |   0\n t/greplint/filter-stdin-redirect.test         |   3 +\n t/greplint/grep-as-argument.expect            |   0\n t/greplint/grep-as-argument.test              |   3 +\n t/greplint/grep-as-value.expect               |   0\n t/greplint/grep-as-value.test                 |   6 +\n t/greplint/wrong-negation.expect              |   1 +\n t/greplint/wrong-negation.test                |   3 +\n t/lib-bitmap.sh                               |  12 +-\n t/lib-bundle-uri-protocol.sh                  |  26 +-\n t/lib-httpd.sh                                |   2 +-\n t/lib-shell-parser.pl                         | 534 ++++++++++++++++++\n t/pack-refs-tests.sh                          |   2 +-\n t/show-ref-exists-tests.sh                    |   2 +-\n t/t0000-basic.sh                              |  16 +-\n t/t0001-init.sh                               |  18 +-\n t/t0008-ignores.sh                            |   8 +-\n t/t0009-git-dir-validation.sh                 |   6 +-\n t/t0012-help.sh                               |   4 +-\n t/t0013-sha1dc.sh                             |   2 +-\n t/t0017-env-helper.sh                         |   4 +-\n t/t0021-conversion.sh                         |  18 +-\n t/t0029-core-unsetenvvars.sh                  |   4 +-\n t/t0030-stripspace.sh                         |   4 +-\n t/t0031-lockfile-pid.sh                       |   2 +-\n t/t0040-parse-options.sh                      |  52 +-\n t/t0041-usage.sh                              |   2 +-\n t/t0052-simple-ipc.sh                         |  10 +-\n t/t0061-run-command.sh                        |   2 +-\n t/t0066-dir-iterator.sh                       |   2 +-\n t/t0068-for-each-repo.sh                      |  16 +-\n t/t0070-fundamental.sh                        |   6 +-\n t/t0081-find-pack.sh                          |  12 +-\n t/t0091-bugreport.sh                          |  18 +-\n t/t0092-diagnose.sh                           |  12 +-\n t/t0100-previous.sh                           |   2 +-\n t/t0200-gettext-basic.sh                      |  14 +-\n t/t0203-gettext-setlocale-sanity.sh           |   4 +-\n t/t0204-gettext-reencode-sanity.sh            |   8 +-\n t/t0210-trace2-normal.sh                      |   6 +-\n t/t0211-trace2-perf.sh                        |  80 +--\n t/t0212-trace2-event.sh                       |   8 +-\n t/t0300-credentials.sh                        |   4 +-\n t/t0410-partial-clone.sh                      |  82 +--\n t/t0450-txt-doc-vs-help.sh                    |   2 +-\n t/t0500-progress-display.sh                   |  18 +-\n t/t0610-reftable-basics.sh                    |   8 +-\n t/t1004-read-tree-m-u-wf.sh                   |   8 +-\n t/t1006-cat-file.sh                           |  18 +-\n t/t1007-hash-object.sh                        |   8 +-\n t/t1011-read-tree-sparse-checkout.sh          |  10 +-\n t/t1050-large.sh                              |   6 +-\n t/t1091-sparse-checkout-builtin.sh            |  24 +-\n t/t1092-sparse-checkout-compatibility.sh      |  44 +-\n t/t1300-config.sh                             |  16 +-\n t/t1305-config-include.sh                     |   2 +-\n t/t1308-config-set.sh                         |   6 +-\n t/t1400-update-ref.sh                         | 173 +++---\n t/t1403-show-ref.sh                           |  18 +-\n t/t1410-reflog.sh                             |   4 +-\n t/t1415-worktree-refs.sh                      |   4 +-\n t/t1430-bad-ref-name.sh                       |  56 +-\n t/t1450-fsck.sh                               |  12 +-\n t/t1451-fsck-buffer.sh                        |   6 +-\n t/t1460-refs-migrate.sh                       |   2 +-\n t/t1500-rev-parse.sh                          |   6 +-\n t/t1502-rev-parse-parseopt.sh                 |   2 +-\n t/t1503-rev-parse-verify.sh                   |  10 +-\n t/t1510-repo-setup.sh                         |  10 +-\n t/t1512-rev-parse-disambiguation.sh           |   4 +-\n t/t1515-rev-parse-outside-repo.sh             |   2 +-\n t/t1800-hook.sh                               |  18 +-\n t/t2004-checkout-cache-temp.sh                |   4 +-\n t/t2019-checkout-ambiguous-ref.sh             |   4 +-\n t/t2024-checkout-dwim.sh                      |   8 +-\n t/t2030-unresolve-info.sh                     |   6 +-\n t/t2060-switch.sh                             |   6 +-\n t/t2070-restore.sh                            |   2 +-\n t/t2080-parallel-checkout-basics.sh           |  14 +-\n t/t2081-parallel-checkout-collisions.sh       |  24 +-\n t/t2082-parallel-checkout-attributes.sh       |  12 +-\n t/t2103-update-index-ignore-missing.sh        |   6 +-\n t/t2200-add-update.sh                         |   2 +-\n t/t2203-add-intent.sh                         |   6 +-\n t/t2400-worktree-add.sh                       |  24 +-\n t/t2402-worktree-list.sh                      |  16 +-\n t/t2403-worktree-move.sh                      |   6 +-\n t/t2405-worktree-submodule.sh                 |   6 +-\n t/t2407-worktree-heads.sh                     |  26 +-\n t/t2500-untracked-overwriting.sh              |   8 +-\n t/t2501-cwd-empty.sh                          |   4 +-\n t/t3001-ls-files-others-exclude.sh            |   6 +-\n t/t3007-ls-files-recurse-submodules.sh        |   6 +-\n t/t3200-branch.sh                             |  12 +-\n t/t3202-show-branch.sh                        |  10 +-\n t/t3203-branch-output.sh                      |   4 +-\n t/t3206-range-diff.sh                         |  78 +--\n t/t3207-branch-submodule.sh                   |   4 +-\n t/t3301-notes.sh                              |  32 +-\n t/t3310-notes-merge-manual-resolve.sh         |  16 +-\n t/t3320-notes-merge-worktrees.sh              |   2 +-\n t/t3400-rebase.sh                             |  16 +-\n t/t3402-rebase-merge.sh                       |  16 +-\n t/t3404-rebase-interactive.sh                 |  72 +--\n t/t3406-rebase-message.sh                     |   6 +-\n t/t3415-rebase-autosquash.sh                  |  10 +-\n t/t3416-rebase-onto-threedots.sh              |   4 +-\n t/t3418-rebase-continue.sh                    |  10 +-\n t/t3420-rebase-autostash.sh                   |  24 +-\n t/t3422-rebase-incompatible-options.sh        |   4 +-\n t/t3429-rebase-edit-todo.sh                   |   2 +-\n t/t3430-rebase-merges.sh                      |  32 +-\n t/t3500-cherry.sh                             |   4 +-\n t/t3501-revert-cherry-pick.sh                 |   6 +-\n t/t3504-cherry-pick-rerere.sh                 |   6 +-\n t/t3510-cherry-pick-sequence.sh               |  24 +-\n t/t3602-rm-sparse-checkout.sh                 |   4 +-\n t/t3705-add-sparse-checkout.sh                |  10 +-\n t/t3800-mktag.sh                              |   4 +-\n t/t3901-i18n-patch.sh                         |  16 +-\n t/t3903-stash.sh                              |  28 +-\n t/t3904-stash-patch.sh                        |   4 +-\n t/t3908-stash-in-worktree.sh                  |   2 +-\n t/t4000-diff-format.sh                        |   2 +-\n t/t4001-diff-rename.sh                        |   4 +-\n t/t4011-diff-symlink.sh                       |   2 +-\n t/t4013-diff-various.sh                       |   2 +-\n t/t4014-format-patch.sh                       | 344 +++++------\n t/t4015-diff-whitespace.sh                    |  16 +-\n t/t4017-diff-retval.sh                        |   2 +-\n t/t4018-diff-funcname.sh                      |   2 +-\n t/t4019-diff-wserror.sh                       |   8 +-\n t/t4020-diff-external.sh                      |  18 +-\n t/t4021-format-patch-numbered.sh              |   4 +-\n t/t4022-diff-rewrite.sh                       |  14 +-\n t/t4028-format-patch-mime-headers.sh          |   6 +-\n t/t4031-diff-rewrite-binary.sh                |  18 +-\n t/t4033-diff-patience.sh                      |   2 +-\n t/t4036-format-patch-signer-mime.sh           |   6 +-\n t/t4038-diff-combined.sh                      |   6 +-\n t/t4051-diff-function-context.sh              |  38 +-\n t/t4053-diff-no-index.sh                      |   4 +-\n t/t4063-diff-blobs.sh                         |   2 +-\n t/t4065-diff-anchored.sh                      |  26 +-\n t/t4067-diff-partial-clone.sh                 |  12 +-\n t/t4073-diff-stat-name-width.sh               |  24 +-\n t/t4103-apply-binary.sh                       |   2 +-\n t/t4120-apply-popt.sh                         |   2 +-\n t/t4124-apply-ws-rule.sh                      |  10 +-\n t/t4128-apply-root.sh                         |   2 +-\n t/t4140-apply-ita.sh                          |   4 +-\n t/t4141-apply-too-large.sh                    |   2 +-\n t/t4150-am.sh                                 |  48 +-\n t/t4200-rerere.sh                             |   6 +-\n t/t4201-shortlog.sh                           |   2 +-\n t/t4202-log.sh                                |  84 +--\n t/t4204-patch-id.sh                           |   2 +-\n t/t4205-log-pretty-formats.sh                 |   2 +-\n t/t4209-log-pickaxe.sh                        |  10 +-\n t/t4211-line-log.sh                           |  72 +--\n t/t4216-log-bloom.sh                          |  18 +-\n t/t4252-am-options.sh                         |  22 +-\n t/t4254-am-corrupt.sh                         |   6 +-\n t/t4258-am-quoted-cr.sh                       |   2 +-\n t/t4301-merge-tree-write-tree.sh              |  18 +-\n t/t5000-tar-tree.sh                           |  10 +-\n t/t5004-archive-corner-cases.sh               |   2 +-\n t/t5100-mailinfo.sh                           |   2 +-\n t/t5150-request-pull.sh                       |  18 +-\n t/t5300-pack-object.sh                        |  22 +-\n t/t5302-pack-index.sh                         |   6 +-\n t/t5304-prune.sh                              |   8 +-\n t/t5310-pack-bitmaps.sh                       |  14 +-\n t/t5317-pack-objects-filter-objects.sh        |  12 +-\n t/t5318-commit-graph.sh                       |   8 +-\n t/t5319-multi-pack-index.sh                   |  16 +-\n t/t5324-split-commit-graph.sh                 |  10 +-\n t/t5325-reverse-index.sh                      |   2 +-\n t/t5326-multi-pack-bitmaps.sh                 |  28 +-\n t/t5328-commit-graph-64bit-time.sh            |   2 +-\n t/t5329-pack-objects-cruft.sh                 |   8 +-\n t/t5334-incremental-multi-pack-index.sh       |   2 +-\n t/t5335-compact-multi-pack-index.sh           |   4 +-\n t/t5351-unpack-large-objects.sh               |   2 +-\n t/t5402-post-merge-hook.sh                    |   4 +-\n t/t5403-post-checkout-hook.sh                 |   2 +-\n t/t5404-tracking-branches.sh                  |   2 +-\n t/t5406-remote-rejects.sh                     |   2 +-\n t/t5407-post-rewrite-hook.sh                  |   8 +-\n t/t5409-colorize-remote-messages.sh           |  36 +-\n t/t5500-fetch-pack.sh                         |  38 +-\n t/t5504-fetch-receive-strict.sh               |  14 +-\n t/t5505-remote.sh                             |  20 +-\n t/t5510-fetch.sh                              |  10 +-\n t/t5512-ls-remote.sh                          |   8 +-\n t/t5514-fetch-multiple.sh                     |   2 +-\n t/t5516-fetch-push.sh                         |  20 +-\n t/t5520-pull.sh                               |   4 +-\n t/t5524-pull-msg.sh                           |   6 +-\n t/t5526-fetch-submodules.sh                   |  16 +-\n t/t5529-push-errors.sh                        |   4 +-\n t/t5530-upload-pack-error.sh                  |  18 +-\n t/t5531-deep-submodule-push.sh                |   2 +-\n t/t5532-fetch-proxy.sh                        |   2 +-\n t/t5533-push-cas.sh                           |  12 +-\n t/t5534-push-signed.sh                        |  22 +-\n t/t5537-fetch-shallow.sh                      |   2 +-\n t/t5538-push-shallow.sh                       |   2 +-\n t/t5539-fetch-http-shallow.sh                 |   4 +-\n t/t5541-http-push-smart.sh                    |  32 +-\n t/t5544-pack-objects-hook.sh                  |  12 +-\n t/t5550-http-fetch-dumb.sh                    |   4 +-\n t/t5551-http-fetch-smart.sh                   |  46 +-\n t/t5552-skipping-fetch-negotiator.sh          |   6 +-\n t/t5554-noop-fetch-negotiator.sh              |   4 +-\n t/t5557-http-get.sh                           |   2 +-\n t/t5558-clone-bundle-uri.sh                   |  38 +-\n t/t5562-http-backend-content-length.sh        |   2 +-\n t/t5564-http-proxy.sh                         |  10 +-\n t/t5581-http-curl-verbose.sh                  |   2 +-\n t/t5583-push-branches.sh                      |   8 +-\n t/t5601-clone.sh                              |  28 +-\n t/t5604-clone-reference.sh                    |   8 +-\n t/t5605-clone-local.sh                        |   2 +-\n t/t5606-clone-options.sh                      |   6 +-\n t/t5612-clone-refspec.sh                      |   2 +-\n t/t5616-partial-clone.sh                      |  60 +-\n t/t5619-clone-local-ambiguous-transport.sh    |   2 +-\n t/t5620-backfill.sh                           |  12 +-\n t/t5700-protocol-v1.sh                        |  46 +-\n t/t5701-git-serve.sh                          |  14 +-\n t/t5702-protocol-v2.sh                        | 154 ++---\n t/t5703-upload-pack-ref-in-want.sh            |  22 +-\n t/t5705-session-id-in-capabilities.sh         |  12 +-\n t/t5750-bundle-uri-parse.sh                   |   8 +-\n t/t5801-remote-helpers.sh                     |   4 +-\n t/t5810-proto-disable-local.sh                |   2 +-\n t/t5813-proto-disable-ssh.sh                  |   4 +-\n t/t6000-rev-list-misc.sh                      |  26 +-\n t/t6005-rev-list-count.sh                     |   8 +-\n t/t6006-rev-list-format.sh                    |   4 +-\n t/t6009-rev-list-parent.sh                    |   4 +-\n t/t6020-bundle-misc.sh                        |  12 +-\n t/t6022-rev-list-missing.sh                   |   4 +-\n t/t6030-bisect-porcelain.sh                   | 150 ++---\n t/t6040-tracking-info.sh                      |   2 +-\n t/t6112-rev-list-filters-objects.sh           |  24 +-\n t/t6115-rev-list-du.sh                        |   4 +-\n t/t6120-describe.sh                           |  14 +-\n t/t6200-fmt-merge-msg.sh                      |  82 +--\n t/t6402-merge-rename.sh                       |   4 +-\n t/t6403-merge-file.sh                         |   6 +-\n t/t6404-recursive-merge.sh                    |   2 +-\n t/t6406-merge-attr.sh                         |  20 +-\n t/t6417-merge-ours-theirs.sh                  |  30 +-\n t/t6418-merge-text-auto.sh                    |   2 +-\n t/t6422-merge-rename-corner-cases.sh          |   8 +-\n t/t6423-merge-rename-directories.sh           |  72 +--\n t/t6424-merge-unrelated-index-changes.sh      |   6 +-\n t/t6427-diff3-conflict-markers.sh             |  10 +-\n t/t6432-merge-recursive-space-options.sh      |   4 +-\n t/t6436-merge-overwrite.sh                    |   6 +-\n t/t6437-submodule-merge.sh                    |  10 +-\n t/t6500-gc.sh                                 |   8 +-\n t/t6600-test-reach.sh                         |   4 +-\n t/t7001-mv.sh                                 |  16 +-\n t/t7002-mv-sparse-checkout.sh                 |  38 +-\n t/t7003-filter-branch.sh                      |  16 +-\n t/t7004-tag.sh                                |   2 +-\n t/t7006-pager.sh                              |  16 +-\n t/t7012-skip-worktree-writing.sh              |   6 +-\n t/t7030-verify-tag.sh                         |  52 +-\n t/t7031-verify-tag-signed-ssh.sh              |  46 +-\n t/t7102-reset.sh                              |   2 +-\n t/t7110-reset-merge.sh                        |  40 +-\n t/t7201-co.sh                                 |   6 +-\n t/t7300-clean.sh                              |   2 +-\n t/t7301-clean-interactive.sh                  |   2 +-\n t/t7400-submodule-basic.sh                    |  32 +-\n t/t7402-submodule-rebase.sh                   |   2 +-\n t/t7406-submodule-update.sh                   |  26 +-\n t/t7416-submodule-dash-url.sh                 |  20 +-\n t/t7417-submodule-path-url.sh                 |   2 +-\n t/t7450-bad-git-dotfiles.sh                   |  20 +-\n t/t7501-commit-basic-functionality.sh         |  16 +-\n t/t7502-commit-porcelain.sh                   |   2 +-\n t/t7507-commit-verbose.sh                     |   6 +-\n t/t7508-status.sh                             |   6 +-\n t/t7510-signed-commit.sh                      |  68 +--\n t/t7516-commit-races.sh                       |   4 +-\n t/t7519-status-fsmonitor.sh                   |  14 +-\n t/t7527-builtin-fsmonitor.sh                  |  82 +--\n t/t7528-signed-commit-ssh.sh                  |  68 +--\n t/t7600-merge.sh                              |  10 +-\n t/t7603-merge-reduce-heads.sh                 |  20 +-\n t/t7606-merge-custom.sh                       |   2 +-\n t/t7607-merge-state.sh                        |   4 +-\n t/t7610-mergetool.sh                          |  18 +-\n t/t7700-repack.sh                             |  14 +-\n t/t7703-repack-geometric.sh                   |   4 +-\n t/t7704-repack-cruft.sh                       |  12 +-\n t/t7800-difftool.sh                           |  26 +-\n t/t7810-grep.sh                               |  22 +-\n t/t7814-grep-recurse-submodules.sh            |   2 +-\n t/t7900-maintenance.sh                        |  34 +-\n t/t8008-blame-formats.sh                      |   2 +-\n t/t8010-cat-file-filters.sh                   |   2 +-\n t/t8012-blame-colors.sh                       |   2 +-\n t/t9001-send-email.sh                         | 190 +++----\n t/t9003-help-autocorrect.sh                   |   6 +-\n t/t9106-git-svn-commit-diff-clobber.sh        |   2 +-\n t/t9107-git-svn-migrate.sh                    |  30 +-\n t/t9110-git-svn-use-svm-props.sh              |  20 +-\n t/t9111-git-svn-use-svnsync-props.sh          |  18 +-\n t/t9114-git-svn-dcommit-merge.sh              |   6 +-\n t/t9116-git-svn-log.sh                        |   8 +-\n t/t9117-git-svn-init-clone.sh                 |  12 +-\n t/t9119-git-svn-info.sh                       |  16 +-\n t/t9122-git-svn-author.sh                     |   8 +-\n t/t9130-git-svn-authors-file.sh               |   8 +-\n t/t9138-git-svn-authors-prog.sh               |  14 +-\n t/t9140-git-svn-reset.sh                      |   4 +-\n t/t9153-git-svn-rewrite-uuid.sh               |   4 +-\n t/t9200-git-cvsexportcommit.sh                |   2 +-\n t/t9210-scalar.sh                             |  34 +-\n t/t9211-scalar-clone.sh                       |  16 +-\n t/t9300-fast-import.sh                        |  10 +-\n t/t9350-fast-export.sh                        |  54 +-\n t/t9351-fast-export-anonymize.sh              |  36 +-\n t/t9400-git-cvsserver-server.sh               |   4 +-\n t/t9501-gitweb-standalone-http-status.sh      |  58 +-\n t/t9502-gitweb-standalone-parse-output.sh     |  38 +-\n t/t9800-git-p4-basic.sh                       |  10 +-\n t/t9801-git-p4-branch.sh                      |  48 +-\n t/t9806-git-p4-options.sh                     |  10 +-\n t/t9807-git-p4-submit.sh                      |   2 +-\n t/t9810-git-p4-rcs.sh                         |   8 +-\n t/t9813-git-p4-preserve-users.sh              |   8 +-\n t/t9814-git-p4-rename.sh                      |   8 +-\n t/t9827-git-p4-change-filetype.sh             |   4 +-\n t/t9832-unshelve.sh                           |   6 +-\n t/t9833-errors.sh                             |   4 +-\n t/t9835-git-p4-metadata-encoding-python2.sh   |  36 +-\n t/t9836-git-p4-metadata-encoding-python3.sh   |  38 +-\n t/t9850-shell.sh                              |   2 +-\n t/t9902-completion.sh                         |  26 +-\n 385 files changed, 3810 insertions(+), 3328 deletions(-)\n create mode 100644 t/greplint-cat.pl\n create mode 100644 t/greplint.pl\n create mode 100644 t/greplint/bare-grep-after-and.expect\n create mode 100644 t/greplint/bare-grep-after-and.test\n create mode 100644 t/greplint/bare-grep-after-semicolon.expect\n create mode 100644 t/greplint/bare-grep-after-semicolon.test\n create mode 100644 t/greplint/bare-grep-compound-body.expect\n create mode 100644 t/greplint/bare-grep-compound-body.test\n create mode 100644 t/greplint/bare-grep-count-mode.expect\n create mode 100644 t/greplint/bare-grep-count-mode.test\n create mode 100644 t/greplint/bare-grep-explicit-pattern.expect\n create mode 100644 t/greplint/bare-grep-explicit-pattern.test\n create mode 100644 t/greplint/bare-grep-flags.expect\n create mode 100644 t/greplint/bare-grep-flags.test\n create mode 100644 t/greplint/bare-grep-lint-ok.expect\n create mode 100644 t/greplint/bare-grep-lint-ok.test\n create mode 100644 t/greplint/bare-grep-negated.expect\n create mode 100644 t/greplint/bare-grep-negated.test\n create mode 100644 t/greplint/bare-grep-pattern-file.expect\n create mode 100644 t/greplint/bare-grep-pattern-file.test\n create mode 100644 t/greplint/bare-grep-simple.expect\n create mode 100644 t/greplint/bare-grep-simple.test\n create mode 100644 t/greplint/bare-grep-subshell.expect\n create mode 100644 t/greplint/bare-grep-subshell.test\n create mode 100644 t/greplint/dqstring-continuation-offset.expect\n create mode 100644 t/greplint/dqstring-continuation-offset.test\n create mode 100644 t/greplint/filter-command-substitution.expect\n create mode 100644 t/greplint/filter-command-substitution.test\n create mode 100644 t/greplint/filter-pipe-input.expect\n create mode 100644 t/greplint/filter-pipe-input.test\n create mode 100644 t/greplint/filter-pipe-output.expect\n create mode 100644 t/greplint/filter-pipe-output.test\n create mode 100644 t/greplint/filter-redirect-output.expect\n create mode 100644 t/greplint/filter-redirect-output.test\n create mode 100644 t/greplint/filter-stdin-redirect.expect\n create mode 100644 t/greplint/filter-stdin-redirect.test\n create mode 100644 t/greplint/grep-as-argument.expect\n create mode 100644 t/greplint/grep-as-argument.test\n create mode 100644 t/greplint/grep-as-value.expect\n create mode 100644 t/greplint/grep-as-value.test\n create mode 100644 t/greplint/wrong-negation.expect\n create mode 100644 t/greplint/wrong-negation.test\n create mode 100644 t/lib-shell-parser.pl\n\n\nbase-commit: e9019fcafe0040228b8631c30f97ae1adb61bcdc\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2135%2Fmmontalbo%2Fmm%2Ftest-grep-docs-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2135/mmontalbo/mm/test-grep-docs-v4\nPull-Request: https://github.com/gitgitgadget/git/pull/2135\n\nRange-diff vs v3:\n\n 1:  ec6b478c4c ! 1:  44d5db91bc t/README: document test_grep helper\n     @@ t/README: see test-lib-functions.sh for the full list and their options.\n      +   'grep' is the right choice because the exit code is not the\n      +   assertion itself.\n      +\n     -+   test_grep requires <file> to exist and will BUG otherwise.\n     -+   When a file's presence is conditional (a backend-specific\n     -+   file, or a path that only exists on some platforms, such as\n     -+   an NTFS 8.3 short name), keep a plain guarded 'grep' instead.\n     ++   test_grep requires <file> to exist and will BUG otherwise, so\n     ++   use it only where the file is guaranteed to exist at that point.\n     ++   When a file's presence is conditional (a backend-specific file,\n     ++   or a path that only exists on some platforms, such as an NTFS\n     ++   8.3 short name), guard the assertion on that condition (a\n     ++   prerequisite, or a 'test -e' on the path) and use test_grep\n     ++   inside the guard:\n     ++\n     ++\tif test_have_prereq REFFILES\n     ++\tthen\n     ++\t\ttest_grep ! \"$refname\" .git/packed-refs\n     ++\tfi\n      +\n        - test_path_is_file <path>\n          test_path_is_dir <path>\n 2:  7a837b77cd = 2:  44ba6d1f1a t: fix grep assertions missing file arguments\n 3:  5894ca1724 = 3:  8f3ebe6426 t: extract chainlint's parser into shared module\n 4:  416c95fee5 = 4:  5689d2074a t: fix Lexer line count for $() inside double-quoted strings\n 5:  7518445733 ! 5:  53fedba4d4 t: convert grep assertions to test_grep\n     @@ Commit message\n          exists only with the files backend, so its check is guarded with a\n          REFFILES prerequisite; the backend-agnostic 'git show-ref' check\n          that follows still runs under every backend.  In t7450 'git~2' is\n     -    the NTFS 8.3 short name of a decoy '..git' file and only exists\n     -    when 8.3 short-name generation is enabled; that '! grep' tolerates\n     -    the missing file on purpose, so it is left as-is with a\n     -    \"# lint-ok\" annotation rather than converted (a plain test_grep\n     -    would BUG when the short name is absent).\n     +    the NTFS 8.3 short name of a '..git' file and only exists\n     +    when 8.3 short-name generation is enabled, so its check is guarded\n     +    with a 'test -f' on the path and uses test_grep inside the guard,\n     +    the same shape as t1400 (a plain test_grep would BUG when the\n     +    short name is absent).\n      \n          The conversion was generated using a grep-assertion linter\n          (greplint.pl, added in the following commit) to identify bare\n     @@ Commit message\n              #      test_must_fail git show-ref --verify -q $m\n              #\n              # t7450: git~2 is an NTFS 8.3 short name that exists only when\n     -        # short-name generation is enabled, so this stays a missing-file-\n     -        # tolerant '! grep'; add a comment plus \"# lint-ok\" so the linter\n     -        # skips it.\n     +        # short-name generation is enabled, so guard the check on its\n     +        # presence with 'test -f' and note in a comment why the path can\n     +        # be absent (a plain test_grep would BUG when it is):\n     +        #\n     +        #  -   ! grep gitdir squatting-clone/d/a/git~2\n     +        #  +   if test -f squatting-clone/d/a/git~2\n     +        #  +   then\n     +        #  +           test_grep ! gitdir squatting-clone/d/a/git~2\n     +        #  +   fi\n      \n              # Step 2: reorder pre-existing '! test_grep' to 'test_grep !'\n              # (must come before steps 3-4 so greplint does not see them)\n     @@ t/t7450-bad-git-dotfiles.sh: test_expect_success WINDOWS 'prevent git~1 squattin\n       \t\t\tclone --recurse-submodules squatting squatting-clone 2>err &&\n       \t\ttest_grep -e \"directory not empty\" -e \"not an empty directory\" err &&\n      -\t\t! grep gitdir squatting-clone/d/a/git~2\n     -+\t\t# git~2 is the 8.3 short name of the ..git decoy, present\n     -+\t\t# only when 8.3 name generation is on; a missing git~2 is\n     -+\t\t# fine because the \"directory not empty\" check above is the\n     -+\t\t# real assertion.\n     -+\t\t! grep gitdir squatting-clone/d/a/git~2 # lint-ok: 8.3 short name git~2 may not exist\n     ++\t\t# git~2 is an 8.3 short name, present only when 8.3 name\n     ++\t\t# generation is enabled. The \"directory not empty\" check\n     ++\t\t# above is the primary assertion.\n     ++\t\tif test -f squatting-clone/d/a/git~2\n     ++\t\tthen\n     ++\t\t\ttest_grep ! gitdir squatting-clone/d/a/git~2\n     ++\t\tfi\n       \tfi\n       '\n       \n 6:  3b12a959fd ! 6:  5cda64fc26 t: add greplint to detect bare grep assertions\n     @@ t/greplint.pl (new)\n      +\treturn 1;\n      +}\n      +\n     -+# Some bare greps are intentional (e.g. file may not exist,\n     -+# data filter).  A '# lint-ok' annotation on the source line\n     -+# suppresses the warning.\n     ++# lint_ok() reports whether a bare grep carries a trailing\n     ++# '# lint-ok' comment telling this linter to skip it.\n     ++#\n     ++# In practice this is needed for just one case: a grep acting\n     ++# as a data filter whose output is consumed by a redirect or\n     ++# pipe on an enclosing compound command (such as a subshell or\n     ++# brace group) rather than by grep's own pipeline, e.g.\n     ++#\n     ++#\t( grep ... && # lint-ok\n     ++#\t  sed ... ) >out\n     ++#\n     ++#\t{ grep ... || : # lint-ok\n     ++#\t} >out\n     ++#\n     ++# is_filter() only scans grep's own pipeline: it stops at the\n     ++# separator before the compound command closes and never sees\n     ++# the outer redirect, so it would flag such a grep as an\n     ++# assertion.  A grep that really is an assertion is better\n     ++# written as test_grep (or a guarded test_grep when the file's\n     ++# presence is conditional) than annotated with lint-ok.\n      +sub lint_ok {\n      +\tmy ($raw_lines, $ln) = @_;\n      +\tif ($ln < 1 || $ln > @$raw_lines) {\n\n-- \ngitgitgadget\n"},{"id":"547178","messageId":"44d5db91bc3e951a6d2e34acc545b7093783bdaf.1783314119.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v4.git.1783314119.gitgitgadget@gmail.com","subject":"[PATCH v4 1/6] t/README: document test_grep helper","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-06T05:01:53Z","receivedAt":"2026-07-06T05:02:04Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\ntest_grep is a wrapper around grep for test assertions that prints\nthe file contents on failure for easier debugging.  It also accepts\n'!' as its first argument for negation, which preserves the\ndiagnostic output that '! test_grep' would suppress.\n\nDespite being widely used (and the preferred replacement for bare\ngrep in assertions), test_grep has no entry in t/README alongside\nthe other documented helpers like test_cmp and test_line_count.\nAdd one.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/README | 34 ++++++++++++++++++++++++++++++++++\n 1 file changed, 34 insertions(+)\n\ndiff --git a/t/README b/t/README\nindex 085921be4b..4252774f86 100644\n--- a/t/README\n+++ b/t/README\n@@ -1039,6 +1039,40 @@ see test-lib-functions.sh for the full list and their options.\n \n    Check whether a file has the length it is expected to.\n \n+ - test_grep [!] [<grep-options>] <pattern> <file>\n+\n+   Check whether <file> contains a line matching <pattern>, or\n+   with '!' that no line matches.  Use this instead of bare\n+   'grep <pattern> <file>' in test assertions.  On failure,\n+   test_grep prints the contents of <file> for easier debugging,\n+   whereas a bare 'grep' would fail silently.\n+\n+   For negation, pass '!' as the first argument:\n+\n+\ttest_grep ! \"^diff --git\" actual\n+\n+   Do not negate by writing '! test_grep', as that suppresses the\n+   diagnostic output.\n+\n+   test_grep should only be used as a test assertion.  When grep\n+   is used as a data filter (e.g. 'grep -v \"^index\" actual >filtered')\n+   or inside a command substitution (e.g. '$(grep -c ...)'), plain\n+   'grep' is the right choice because the exit code is not the\n+   assertion itself.\n+\n+   test_grep requires <file> to exist and will BUG otherwise, so\n+   use it only where the file is guaranteed to exist at that point.\n+   When a file's presence is conditional (a backend-specific file,\n+   or a path that only exists on some platforms, such as an NTFS\n+   8.3 short name), guard the assertion on that condition (a\n+   prerequisite, or a 'test -e' on the path) and use test_grep\n+   inside the guard:\n+\n+\tif test_have_prereq REFFILES\n+\tthen\n+\t\ttest_grep ! \"$refname\" .git/packed-refs\n+\tfi\n+\n  - test_path_is_file <path>\n    test_path_is_dir <path>\n    test_path_is_missing <path>\n-- \ngitgitgadget\n\n"},{"id":"547179","messageId":"44ba6d1f1a549f34f73f3863c035837403271f35.1783314119.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v4.git.1783314119.gitgitgadget@gmail.com","subject":"[PATCH v4 2/6] t: fix grep assertions missing file arguments","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-06T05:01:54Z","receivedAt":"2026-07-06T05:02:05Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nThree grep assertions were missing their file arguments, causing\nthem to read from empty stdin instead of the intended file:\n\n- t2402: '! grep ...' should read from 'out', matching the\n  grep on the preceding line.\n- t7507: the closing quote is in the wrong place, making the\n  entire 'diff --git actual' a single pattern with no file\n  argument instead of pattern 'diff --git' and file 'actual'.\n- t7700: '! grep ...' should read from 'packlist', matching\n  the redirect on the preceding line.\n\nWithout file arguments these greps always succeed (empty stdin\nmatches nothing), so the assertions were not actually checking\nanything.  All three tests pass with the corrected file arguments,\nconfirming the intended behavior is sound.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/t2402-worktree-list.sh  | 2 +-\n t/t7507-commit-verbose.sh | 2 +-\n t/t7700-repack.sh         | 2 +-\n 3 files changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh\nindex e0c6abd2f5..93f92e854a 100755\n--- a/t/t2402-worktree-list.sh\n+++ b/t/t2402-worktree-list.sh\n@@ -144,7 +144,7 @@ test_expect_success '\"list\" all worktrees with prunable annotation' '\n \trm -rf prunable &&\n \tgit worktree list >out &&\n \tgrep \"/prunable  *[0-9a-f].* prunable$\" out &&\n-\t! grep \"/unprunable  *[0-9a-f].* prunable$\"\n+\t! grep \"/unprunable  *[0-9a-f].* prunable$\" out\n '\n \n test_expect_success '\"list\" all worktrees --porcelain with prunable' '\ndiff --git a/t/t7507-commit-verbose.sh b/t/t7507-commit-verbose.sh\nindex b53d71c086..acdb6b1455 100755\n--- a/t/t7507-commit-verbose.sh\n+++ b/t/t7507-commit-verbose.sh\n@@ -163,7 +163,7 @@ done\n \n test_expect_success \"status ignores commit.verbose=true\" '\n \tgit -c commit.verbose=true status >actual &&\n-\t! grep \"^diff --git actual\"\n+\t! grep \"^diff --git\" actual\n '\n \n test_done\ndiff --git a/t/t7700-repack.sh b/t/t7700-repack.sh\nindex 63ef63fc50..c6ff3aed30 100755\n--- a/t/t7700-repack.sh\n+++ b/t/t7700-repack.sh\n@@ -194,7 +194,7 @@ test_expect_success 'local packed unreachable obs that exist in alternate ODB ar\n \trm -f .git/objects/pack/* &&\n \tmv pack-* .git/objects/pack/ &&\n \tgit verify-pack -v -- .git/objects/pack/*.idx >packlist &&\n-\t! grep \"^$coid \" &&\n+\t! grep \"^$coid \" packlist &&\n \techo >.git/objects/info/alternates &&\n \ttest_must_fail git show $coid\n '\n-- \ngitgitgadget\n\n"},{"id":"547181","messageId":"8f3ebe6426639e1b7158ea96c30d01a3ca012811.1783314119.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v4.git.1783314119.gitgitgadget@gmail.com","subject":"[PATCH v4 3/6] t: extract chainlint's parser into shared module","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-06T05:01:55Z","receivedAt":"2026-07-06T05:02:08Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nMove chainlint.pl's Lexer, ShellParser, and ScriptParser into a\nshared module (lib-shell-parser.pl) so other lint tools can reuse\nthe same shell parsing infrastructure.  A subsequent commit adds\ngreplint.pl, which needs the same tokenizer to correctly identify\ncommand boundaries.\n\nScriptParser's check_test() becomes a no-op in the shared module.\nchainlint.pl defines ChainlintParser (extending ScriptParser)\nwith the &&-chain check_test() implementation.\n\nNo functional change: chainlint produces the same output and\ncheck-chainlint self-tests pass.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/chainlint.pl        | 529 +----------------------------------------\n t/lib-shell-parser.pl | 531 ++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 543 insertions(+), 517 deletions(-)\n create mode 100644 t/lib-shell-parser.pl\n\ndiff --git a/t/chainlint.pl b/t/chainlint.pl\nindex 2d07a99700..cededf15ee 100755\n--- a/t/chainlint.pl\n+++ b/t/chainlint.pl\n@@ -23,458 +23,9 @@ my $jobs = -1;\n my $show_stats;\n my $emit_all;\n \n-# Lexer tokenizes POSIX shell scripts. It is roughly modeled after section 2.3\n-# \"Token Recognition\" of POSIX chapter 2 \"Shell Command Language\". Although\n-# similar to lexical analyzers for other languages, this one differs in a few\n-# substantial ways due to quirks of the shell command language.\n-#\n-# For instance, in many languages, newline is just whitespace like space or\n-# TAB, but in shell a newline is a command separator, thus a distinct lexical\n-# token. A newline is significant and returned as a distinct token even at the\n-# end of a shell comment.\n-#\n-# In other languages, `1+2` would typically be scanned as three tokens\n-# (`1`, `+`, and `2`), but in shell it is a single token. However, the similar\n-# `1 + 2`, which embeds whitespace, is scanned as three token in shell, as well.\n-# In shell, several characters with special meaning lose that meaning when not\n-# surrounded by whitespace. For instance, the negation operator `!` is special\n-# when standing alone surrounded by whitespace; whereas in `foo!uucp` it is\n-# just a plain character in the longer token \"foo!uucp\". In many other\n-# languages, `\"string\"/foo:'string'` might be scanned as five tokens (\"string\",\n-# `/`, `foo`, `:`, and 'string'), but in shell, it is just a single token.\n-#\n-# The lexical analyzer for the shell command language is also somewhat unusual\n-# in that it recursively invokes the parser to handle the body of `$(...)`\n-# expressions which can contain arbitrary shell code. Such expressions may be\n-# encountered both inside and outside of double-quoted strings.\n-#\n-# The lexical analyzer is responsible for consuming shell here-doc bodies which\n-# extend from the line following a `<<TAG` operator until a line consisting\n-# solely of `TAG`. Here-doc consumption begins when a newline is encountered.\n-# It is legal for multiple here-doc `<<TAG` operators to be present on a single\n-# line, in which case their bodies must be present one following the next, and\n-# are consumed in the (left-to-right) order the `<<TAG` operators appear on the\n-# line. A special complication is that the bodies of all here-docs must be\n-# consumed when the newline is encountered even if the parse context depth has\n-# changed. For instance, in `cat <<A && x=$(cat <<B &&\\n`, bodies of here-docs\n-# \"A\" and \"B\" must be consumed even though \"A\" was introduced outside the\n-# recursive parse context in which \"B\" was introduced and in which the newline\n-# is encountered.\n-package Lexer;\n-\n-sub new {\n-\tmy ($class, $parser, $s) = @_;\n-\tbless {\n-\t\tparser => $parser,\n-\t\tbuff => $s,\n-\t\tlineno => 1,\n-\t\theretags => []\n-\t} => $class;\n-}\n-\n-sub scan_heredoc_tag {\n-\tmy $self = shift @_;\n-\t${$self->{buff}} =~ /\\G(-?)/gc;\n-\tmy $indented = $1;\n-\tmy $token = $self->scan_token();\n-\treturn \"<<$indented\" unless $token;\n-\tmy $tag = $token->[0];\n-\t$tag =~ s/['\"\\\\]//g;\n-\t$$token[0] = $indented ? \"\\t$tag\" : \"$tag\";\n-\tpush(@{$self->{heretags}}, $token);\n-\treturn \"<<$indented$tag\";\n-}\n-\n-sub scan_op {\n-\tmy ($self, $c) = @_;\n-\tmy $b = $self->{buff};\n-\treturn $c unless $$b =~ /\\G(.)/sgc;\n-\tmy $cc = $c . $1;\n-\treturn scan_heredoc_tag($self) if $cc eq '<<';\n-\treturn $cc if $cc =~ /^(?:&&|\\|\\||>>|;;|<&|>&|<>|>\\|)$/;\n-\tpos($$b)--;\n-\treturn $c;\n-}\n-\n-sub scan_sqstring {\n-\tmy $self = shift @_;\n-\t${$self->{buff}} =~ /\\G([^']*'|.*\\z)/sgc;\n-\tmy $s = $1;\n-\t$self->{lineno} += () = $s =~ /\\n/sg;\n-\treturn \"'\" . $s;\n-}\n-\n-sub scan_dqstring {\n-\tmy $self = shift @_;\n-\tmy $b = $self->{buff};\n-\tmy $s = '\"';\n-\twhile (1) {\n-\t\t# slurp up non-special characters\n-\t\t$s .= $1 if $$b =~ /\\G([^\"\\$\\\\]+)/gc;\n-\t\t# handle special characters\n-\t\tlast unless $$b =~ /\\G(.)/sgc;\n-\t\tmy $c = $1;\n-\t\t$s .= '\"', last if $c eq '\"';\n-\t\t$s .= '$' . $self->scan_dollar(), next if $c eq '$';\n-\t\tif ($c eq '\\\\') {\n-\t\t\t$s .= '\\\\', last unless $$b =~ /\\G(.)/sgc;\n-\t\t\t$c = $1;\n-\t\t\t$self->{lineno}++, next if $c eq \"\\n\"; # line splice\n-\t\t\t# backslash escapes only $, `, \", \\ in dq-string\n-\t\t\t$s .= '\\\\' unless $c =~ /^[\\$`\"\\\\]$/;\n-\t\t\t$s .= $c;\n-\t\t\tnext;\n-\t\t}\n-\t\tdie(\"internal error scanning dq-string '$c'\\n\");\n-\t}\n-\t$self->{lineno} += () = $s =~ /\\n/sg;\n-\treturn $s;\n-}\n-\n-sub scan_balanced {\n-\tmy ($self, $c1, $c2) = @_;\n-\tmy $b = $self->{buff};\n-\tmy $depth = 1;\n-\tmy $s = $c1;\n-\twhile ($$b =~ /\\G([^\\Q$c1$c2\\E]*(?:[\\Q$c1$c2\\E]|\\z))/gc) {\n-\t\t$s .= $1;\n-\t\t$depth++, next if $s =~ /\\Q$c1\\E$/;\n-\t\t$depth--;\n-\t\tlast if $depth == 0;\n-\t}\n-\t$self->{lineno} += () = $s =~ /\\n/sg;\n-\treturn $s;\n-}\n-\n-sub scan_subst {\n-\tmy $self = shift @_;\n-\tmy @tokens = $self->{parser}->parse(qr/^\\)$/);\n-\t$self->{parser}->next_token(); # closing \")\"\n-\treturn @tokens;\n-}\n-\n-sub scan_dollar {\n-\tmy $self = shift @_;\n-\tmy $b = $self->{buff};\n-\treturn $self->scan_balanced('(', ')') if $$b =~ /\\G\\((?=\\()/gc; # $((...))\n-\treturn '(' . join(' ', map {$_->[0]} $self->scan_subst()) . ')' if $$b =~ /\\G\\(/gc; # $(...)\n-\treturn $self->scan_balanced('{', '}') if $$b =~ /\\G\\{/gc; # ${...}\n-\treturn $1 if $$b =~ /\\G(\\w+)/gc; # $var\n-\treturn $1 if $$b =~ /\\G([@*#?$!0-9-])/gc; # $*, $1, $$, etc.\n-\treturn '';\n-}\n-\n-sub swallow_heredocs {\n-\tmy $self = shift @_;\n-\tmy $b = $self->{buff};\n-\tmy $tags = $self->{heretags};\n-\twhile (my $tag = shift @$tags) {\n-\t\tmy $start = pos($$b);\n-\t\tmy $indent = $$tag[0] =~ s/^\\t// ? '\\\\s*' : '';\n-\t\t$$b =~ /(?:\\G|\\n)$indent\\Q$$tag[0]\\E(?:\\n|\\z)/gc;\n-\t\tif (pos($$b) > $start) {\n-\t\t\tmy $body = substr($$b, $start, pos($$b) - $start);\n-\t\t\t$self->{parser}->{heredocs}->{$$tag[0]} = {\n-\t\t\t\tcontent => substr($body, 0, length($body) - length($&)),\n-\t\t\t\tstart_line => $self->{lineno},\n-\t\t        };\n-\t\t\t$self->{lineno} += () = $body =~ /\\n/sg;\n-\t\t\tnext;\n-\t\t}\n-\t\tpush(@{$self->{parser}->{problems}}, ['HEREDOC', $tag]);\n-\t\t$$b =~ /(?:\\G|\\n).*\\z/gc; # consume rest of input\n-\t\tmy $body = substr($$b, $start, pos($$b) - $start);\n-\t\t$self->{lineno} += () = $body =~ /\\n/sg;\n-\t\tlast;\n-\t}\n-}\n-\n-sub scan_token {\n-\tmy $self = shift @_;\n-\tmy $b = $self->{buff};\n-\tmy $token = '';\n-\tmy ($start, $startln);\n-RESTART:\n-\t$startln = $self->{lineno};\n-\t$$b =~ /\\G[ \\t]+/gc; # skip whitespace (but not newline)\n-\t$start = pos($$b) || 0;\n-\t$self->{lineno}++, return [\"\\n\", $start, pos($$b), $startln, $startln] if $$b =~ /\\G#[^\\n]*(?:\\n|\\z)/gc; # comment\n-\twhile (1) {\n-\t\t# slurp up non-special characters\n-\t\t$token .= $1 if $$b =~ /\\G([^\\\\;&|<>(){}'\"\\$\\s]+)/gc;\n-\t\t# handle special characters\n-\t\tlast unless $$b =~ /\\G(.)/sgc;\n-\t\tmy $c = $1;\n-\t\tpos($$b)--, last if $c =~ /^[ \\t]$/; # whitespace ends token\n-\t\tpos($$b)--, last if length($token) && $c =~ /^[;&|<>(){}\\n]$/;\n-\t\t$token .= $self->scan_sqstring(), next if $c eq \"'\";\n-\t\t$token .= $self->scan_dqstring(), next if $c eq '\"';\n-\t\t$token .= $c . $self->scan_dollar(), next if $c eq '$';\n-\t\t$self->{lineno}++, $self->swallow_heredocs(), $token = $c, last if $c eq \"\\n\";\n-\t\t$token = $self->scan_op($c), last if $c =~ /^[;&|<>]$/;\n-\t\t$token = $c, last if $c =~ /^[(){}]$/;\n-\t\tif ($c eq '\\\\') {\n-\t\t\t$token .= '\\\\', last unless $$b =~ /\\G(.)/sgc;\n-\t\t\t$c = $1;\n-\t\t\t$self->{lineno}++, next if $c eq \"\\n\" && length($token); # line splice\n-\t\t\t$self->{lineno}++, goto RESTART if $c eq \"\\n\"; # line splice\n-\t\t\t$token .= '\\\\' . $c;\n-\t\t\tnext;\n-\t\t}\n-\t\tdie(\"internal error scanning character '$c'\\n\");\n-\t}\n-\treturn length($token) ? [$token, $start, pos($$b), $startln, $self->{lineno}] : undef;\n-}\n-\n-# ShellParser parses POSIX shell scripts (with minor extensions for Bash). It\n-# is a recursive descent parser very roughly modeled after section 2.10 \"Shell\n-# Grammar\" of POSIX chapter 2 \"Shell Command Language\".\n-package ShellParser;\n-\n-sub new {\n-\tmy ($class, $s) = @_;\n-\tmy $self = bless {\n-\t\tbuff => [],\n-\t\tstop => [],\n-\t\toutput => [],\n-\t\theredocs => {},\n-\t\tinsubshell => 0,\n-\t} => $class;\n-\t$self->{lexer} = Lexer->new($self, $s);\n-\treturn $self;\n-}\n-\n-sub next_token {\n-\tmy $self = shift @_;\n-\treturn pop(@{$self->{buff}}) if @{$self->{buff}};\n-\treturn $self->{lexer}->scan_token();\n-}\n-\n-sub untoken {\n-\tmy $self = shift @_;\n-\tpush(@{$self->{buff}}, @_);\n-}\n-\n-sub peek {\n-\tmy $self = shift @_;\n-\tmy $token = $self->next_token();\n-\treturn undef unless defined($token);\n-\t$self->untoken($token);\n-\treturn $token;\n-}\n-\n-sub stop_at {\n-\tmy ($self, $token) = @_;\n-\treturn 1 unless defined($token);\n-\tmy $stop = ${$self->{stop}}[-1] if @{$self->{stop}};\n-\treturn defined($stop) && $token->[0] =~ $stop;\n-}\n-\n-sub expect {\n-\tmy ($self, $expect) = @_;\n-\tmy $token = $self->next_token();\n-\treturn $token if defined($token) && $token->[0] eq $expect;\n-\tpush(@{$self->{output}}, \"?!ERR?! expected '$expect' but found '\" . (defined($token) ? $token->[0] : \"<end-of-input>\") . \"'\\n\");\n-\t$self->untoken($token) if defined($token);\n-\treturn ();\n-}\n-\n-sub optional_newlines {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\twhile (my $token = $self->peek()) {\n-\t\tlast unless $token->[0] eq \"\\n\";\n-\t\tpush(@tokens, $self->next_token());\n-\t}\n-\treturn @tokens;\n-}\n-\n-sub parse_group {\n-\tmy $self = shift @_;\n-\treturn ($self->parse(qr/^}$/),\n-\t\t$self->expect('}'));\n-}\n-\n-sub parse_subshell {\n-\tmy $self = shift @_;\n-\t$self->{insubshell}++;\n-\tmy @tokens = ($self->parse(qr/^\\)$/),\n-\t\t      $self->expect(')'));\n-\t$self->{insubshell}--;\n-\treturn @tokens;\n-}\n-\n-sub parse_case_pattern {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\twhile (defined(my $token = $self->next_token())) {\n-\t\tpush(@tokens, $token);\n-\t\tlast if $token->[0] eq ')';\n-\t}\n-\treturn @tokens;\n-}\n-\n-sub parse_case {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\tpush(@tokens,\n-\t     $self->next_token(), # subject\n-\t     $self->optional_newlines(),\n-\t     $self->expect('in'),\n-\t     $self->optional_newlines());\n-\twhile (1) {\n-\t\tmy $token = $self->peek();\n-\t\tlast unless defined($token) && $token->[0] ne 'esac';\n-\t\tpush(@tokens,\n-\t\t     $self->parse_case_pattern(),\n-\t\t     $self->optional_newlines(),\n-\t\t     $self->parse(qr/^(?:;;|esac)$/)); # item body\n-\t\t$token = $self->peek();\n-\t\tlast unless defined($token) && $token->[0] ne 'esac';\n-\t\tpush(@tokens,\n-\t\t     $self->expect(';;'),\n-\t\t     $self->optional_newlines());\n-\t}\n-\tpush(@tokens, $self->expect('esac'));\n-\treturn @tokens;\n-}\n-\n-sub parse_for {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\tpush(@tokens,\n-\t     $self->next_token(), # variable\n-\t     $self->optional_newlines());\n-\tmy $token = $self->peek();\n-\tif (defined($token) && $token->[0] eq 'in') {\n-\t\tpush(@tokens,\n-\t\t     $self->expect('in'),\n-\t\t     $self->optional_newlines());\n-\t}\n-\tpush(@tokens,\n-\t     $self->parse(qr/^do$/), # items\n-\t     $self->expect('do'),\n-\t     $self->optional_newlines(),\n-\t     $self->parse_loop_body(),\n-\t     $self->expect('done'));\n-\treturn @tokens;\n-}\n-\n-sub parse_if {\n-\tmy $self = shift @_;\n-\tmy @tokens;\n-\twhile (1) {\n-\t\tpush(@tokens,\n-\t\t     $self->parse(qr/^then$/), # if/elif condition\n-\t\t     $self->expect('then'),\n-\t\t     $self->optional_newlines(),\n-\t\t     $self->parse(qr/^(?:elif|else|fi)$/)); # if/elif body\n-\t\tmy $token = $self->peek();\n-\t\tlast unless defined($token) && $token->[0] eq 'elif';\n-\t\tpush(@tokens, $self->expect('elif'));\n-\t}\n-\tmy $token = $self->peek();\n-\tif (defined($token) && $token->[0] eq 'else') {\n-\t\tpush(@tokens,\n-\t\t     $self->expect('else'),\n-\t\t     $self->optional_newlines(),\n-\t\t     $self->parse(qr/^fi$/)); # else body\n-\t}\n-\tpush(@tokens, $self->expect('fi'));\n-\treturn @tokens;\n-}\n-\n-sub parse_loop_body {\n-\tmy $self = shift @_;\n-\treturn $self->parse(qr/^done$/);\n-}\n-\n-sub parse_loop {\n-\tmy $self = shift @_;\n-\treturn ($self->parse(qr/^do$/), # condition\n-\t\t$self->expect('do'),\n-\t\t$self->optional_newlines(),\n-\t\t$self->parse_loop_body(),\n-\t\t$self->expect('done'));\n-}\n-\n-sub parse_func {\n-\tmy $self = shift @_;\n-\treturn ($self->expect('('),\n-\t\t$self->expect(')'),\n-\t\t$self->optional_newlines(),\n-\t\t$self->parse_cmd()); # body\n-}\n-\n-sub parse_bash_array_assignment {\n-\tmy $self = shift @_;\n-\tmy @tokens = $self->expect('(');\n-\twhile (defined(my $token = $self->next_token())) {\n-\t\tpush(@tokens, $token);\n-\t\tlast if $token->[0] eq ')';\n-\t}\n-\treturn @tokens;\n-}\n-\n-my %compound = (\n-\t'{' => \\&parse_group,\n-\t'(' => \\&parse_subshell,\n-\t'case' => \\&parse_case,\n-\t'for' => \\&parse_for,\n-\t'if' => \\&parse_if,\n-\t'until' => \\&parse_loop,\n-\t'while' => \\&parse_loop);\n-\n-sub parse_cmd {\n-\tmy $self = shift @_;\n-\tmy $cmd = $self->next_token();\n-\treturn () unless defined($cmd);\n-\treturn $cmd if $cmd->[0] eq \"\\n\";\n-\n-\tmy $token;\n-\tmy @tokens = $cmd;\n-\tif ($cmd->[0] eq '!') {\n-\t\tpush(@tokens, $self->parse_cmd());\n-\t\treturn @tokens;\n-\t} elsif (my $f = $compound{$cmd->[0]}) {\n-\t\tpush(@tokens, $self->$f());\n-\t} elsif (defined($token = $self->peek()) && $token->[0] eq '(') {\n-\t\tif ($cmd->[0] !~ /\\w=$/) {\n-\t\t\tpush(@tokens, $self->parse_func());\n-\t\t\treturn @tokens;\n-\t\t}\n-\t\tmy @array = $self->parse_bash_array_assignment();\n-\t\t$tokens[-1]->[0] .= join(' ', map {$_->[0]} @array);\n-\t\t$tokens[-1]->[2] = $array[$#array][2] if @array;\n-\t}\n-\n-\twhile (defined(my $token = $self->next_token())) {\n-\t\t$self->untoken($token), last if $self->stop_at($token);\n-\t\tpush(@tokens, $token);\n-\t\tlast if $token->[0] =~ /^(?:[;&\\n|]|&&|\\|\\|)$/;\n-\t}\n-\tpush(@tokens, $self->next_token()) if $tokens[-1]->[0] ne \"\\n\" && defined($token = $self->peek()) && $token->[0] eq \"\\n\";\n-\treturn @tokens;\n-}\n-\n-sub accumulate {\n-\tmy ($self, $tokens, $cmd) = @_;\n-\tpush(@$tokens, @$cmd);\n-}\n-\n-sub parse {\n-\tmy ($self, $stop) = @_;\n-\tpush(@{$self->{stop}}, $stop);\n-\tgoto DONE if $self->stop_at($self->peek());\n-\tmy @tokens;\n-\twhile (my @cmd = $self->parse_cmd()) {\n-\t\t$self->accumulate(\\@tokens, \\@cmd);\n-\t\tlast if $self->stop_at($self->peek());\n-\t}\n-DONE:\n-\tpop(@{$self->{stop}});\n-\treturn @tokens;\n-}\n+use File::Basename;\n+do(dirname($0) . \"/lib-shell-parser.pl\")\n+\tor die \"$0: failed to load lib-shell-parser.pl: $@$!\\n\";\n \n # TestParser is a subclass of ShellParser which, beyond parsing shell script\n # code, is also imbued with semantic knowledge of test construction, and checks\n@@ -482,9 +33,10 @@ DONE:\n # the tests themselves or in behaviors being exercised by the tests. As such,\n # TestParser is only called upon to parse test bodies, not the top-level\n # scripts in which the tests are defined.\n+\n package TestParser;\n \n-use base 'ShellParser';\n+our @ISA = ('ShellParser');\n \n sub new {\n \tmy $class = shift @_;\n@@ -578,51 +130,10 @@ DONE:\n \t$self->SUPER::accumulate($tokens, $cmd);\n }\n \n-# ScriptParser is a subclass of ShellParser which identifies individual test\n-# definitions within test scripts, and passes each test body through TestParser\n-# to identify possible problems. ShellParser detects test definitions not only\n-# at the top-level of test scripts but also within compound commands such as\n-# loops and function definitions.\n-package ScriptParser;\n-\n-use base 'ShellParser';\n-\n-sub new {\n-\tmy $class = shift @_;\n-\tmy $self = $class->SUPER::new(@_);\n-\t$self->{ntests} = 0;\n-\t$self->{nerrs} = 0;\n-\treturn $self;\n-}\n+# ChainlintParser extends ScriptParser with &&-chain checking\n+package ChainlintParser;\n \n-# extract the raw content of a token, which may be a single string or a\n-# composition of multiple strings and non-string character runs; for instance,\n-# `\"test body\"` unwraps to `test body`; `word\"a b\"42'c d'` to `worda b42c d`\n-sub unwrap {\n-\tmy $token = (@_ ? shift @_ : $_)->[0];\n-\t# simple case: 'sqstring' or \"dqstring\"\n-\treturn $token if $token =~ s/^'([^']*)'$/$1/;\n-\treturn $token if $token =~ s/^\"([^\"]*)\"$/$1/;\n-\n-\t# composite case\n-\tmy ($s, $q, $escaped);\n-\twhile (1) {\n-\t\t# slurp up non-special characters\n-\t\t$s .= $1 if $token =~ /\\G([^\\\\'\"]*)/gc;\n-\t\t# handle special characters\n-\t\tlast unless $token =~ /\\G(.)/sgc;\n-\t\tmy $c = $1;\n-\t\t$q = undef, next if defined($q) && $c eq $q;\n-\t\t$q = $c, next if !defined($q) && $c =~ /^['\"]$/;\n-\t\tif ($c eq '\\\\') {\n-\t\t\tlast unless $token =~ /\\G(.)/sgc;\n-\t\t\t$c = $1;\n-\t\t\t$s .= '\\\\' if $c eq \"\\n\"; # preserve line splice\n-\t\t}\n-\t\t$s .= $c;\n-\t}\n-\treturn $s\n-}\n+our @ISA = ('ScriptParser');\n \n sub format_problem {\n \tlocal $_ = shift;\n@@ -635,10 +146,10 @@ sub format_problem {\n \n sub check_test {\n \tmy $self = shift @_;\n-\tmy $title = unwrap(shift @_);\n+\tmy $title = ScriptParser::unwrap(shift @_);\n \tmy $body = shift @_;\n \tmy $lineno = $body->[3];\n-\t$body = unwrap($body);\n+\t$body = ScriptParser::unwrap($body);\n \tif ($body eq '-') {\n \t\tmy $herebody = shift @_;\n \t\t$body = $herebody->{content};\n@@ -673,24 +184,8 @@ sub check_test {\n \tpush(@{$self->{output}}, \"$c->{blue}# chainlint: $title$c->{reset}\\n$checked\");\n }\n \n-sub parse_cmd {\n-\tmy $self = shift @_;\n-\tmy @tokens = $self->SUPER::parse_cmd();\n-\treturn @tokens unless @tokens && $tokens[0]->[0] =~ /^test_expect_(?:success|failure)$/;\n-\tmy $n = $#tokens;\n-\t$n-- while $n >= 0 && $tokens[$n]->[0] =~ /^(?:[;&\\n|]|&&|\\|\\|)$/;\n-\tmy $herebody;\n-\tif ($n >= 2 && $tokens[$n-1]->[0] eq '-' && $tokens[$n]->[0] =~ /^<<-?(.+)$/) {\n-\t\t$herebody = $self->{heredocs}->{$1};\n-\t\t$n--;\n-\t}\n-\t$self->check_test($tokens[1], $tokens[2], $herebody) if $n == 2; # title body\n-\t$self->check_test($tokens[2], $tokens[3], $herebody) if $n > 2;  # prereq title body\n-\treturn @tokens;\n-}\n-\n # main contains high-level functionality for processing command-line switches,\n-# feeding input test scripts to ScriptParser, and reporting results.\n+# feeding input test scripts to ChainlintParser, and reporting results.\n package main;\n \n my $getnow = sub { return time(); };\n@@ -803,7 +298,7 @@ sub check_script {\n \t\t}\n \t\tmy $s = do { local $/; <$fh> };\n \t\tclose($fh);\n-\t\tmy $parser = ScriptParser->new(\\$s);\n+\t\tmy $parser = ChainlintParser->new(\\$s);\n \t\t1 while $parser->parse_cmd();\n \t\tif (@{$parser->{output}}) {\n \t\t\tmy $c = fd_colors(1);\ndiff --git a/t/lib-shell-parser.pl b/t/lib-shell-parser.pl\nnew file mode 100644\nindex 0000000000..5c435c5d05\n--- /dev/null\n+++ b/t/lib-shell-parser.pl\n@@ -0,0 +1,531 @@\n+# Copyright (c) 2021-2022 Eric Sunshine <sunshine@sunshineco.com>\n+#\n+# Shared shell script parser for test lint tools. Provides Lexer,\n+# ShellParser, and ScriptParser. Subclass ScriptParser and override\n+# check_test() to implement lint checks.\n+\n+use strict;\n+use warnings;\n+\n+# Lexer tokenizes POSIX shell scripts. It is roughly modeled after section 2.3\n+# \"Token Recognition\" of POSIX chapter 2 \"Shell Command Language\". Although\n+# similar to lexical analyzers for other languages, this one differs in a few\n+# substantial ways due to quirks of the shell command language.\n+#\n+# For instance, in many languages, newline is just whitespace like space or\n+# TAB, but in shell a newline is a command separator, thus a distinct lexical\n+# token. A newline is significant and returned as a distinct token even at the\n+# end of a shell comment.\n+#\n+# In other languages, `1+2` would typically be scanned as three tokens\n+# (`1`, `+`, and `2`), but in shell it is a single token. However, the similar\n+# `1 + 2`, which embeds whitespace, is scanned as three token in shell, as well.\n+# In shell, several characters with special meaning lose that meaning when not\n+# surrounded by whitespace. For instance, the negation operator `!` is special\n+# when standing alone surrounded by whitespace; whereas in `foo!uucp` it is\n+# just a plain character in the longer token \"foo!uucp\". In many other\n+# languages, `\"string\"/foo:'string'` might be scanned as five tokens (\"string\",\n+# `/`, `foo`, `:`, and 'string'), but in shell, it is just a single token.\n+#\n+# The lexical analyzer for the shell command language is also somewhat unusual\n+# in that it recursively invokes the parser to handle the body of `$(...)`\n+# expressions which can contain arbitrary shell code. Such expressions may be\n+# encountered both inside and outside of double-quoted strings.\n+#\n+# The lexical analyzer is responsible for consuming shell here-doc bodies which\n+# extend from the line following a `<<TAG` operator until a line consisting\n+# solely of `TAG`. Here-doc consumption begins when a newline is encountered.\n+# It is legal for multiple here-doc `<<TAG` operators to be present on a single\n+# line, in which case their bodies must be present one following the next, and\n+# are consumed in the (left-to-right) order the `<<TAG` operators appear on the\n+# line. A special complication is that the bodies of all here-docs must be\n+# consumed when the newline is encountered even if the parse context depth has\n+# changed. For instance, in `cat <<A && x=$(cat <<B &&\\n`, bodies of here-docs\n+# \"A\" and \"B\" must be consumed even though \"A\" was introduced outside the\n+# recursive parse context in which \"B\" was introduced and in which the newline\n+# is encountered.\n+package Lexer;\n+\n+sub new {\n+\tmy ($class, $parser, $s) = @_;\n+\tbless {\n+\t\tparser => $parser,\n+\t\tbuff => $s,\n+\t\tlineno => 1,\n+\t\theretags => []\n+\t} => $class;\n+}\n+\n+sub scan_heredoc_tag {\n+\tmy $self = shift @_;\n+\t${$self->{buff}} =~ /\\G(-?)/gc;\n+\tmy $indented = $1;\n+\tmy $token = $self->scan_token();\n+\treturn \"<<$indented\" unless $token;\n+\tmy $tag = $token->[0];\n+\t$tag =~ s/['\"\\\\]//g;\n+\t$$token[0] = $indented ? \"\\t$tag\" : \"$tag\";\n+\tpush(@{$self->{heretags}}, $token);\n+\treturn \"<<$indented$tag\";\n+}\n+\n+sub scan_op {\n+\tmy ($self, $c) = @_;\n+\tmy $b = $self->{buff};\n+\treturn $c unless $$b =~ /\\G(.)/sgc;\n+\tmy $cc = $c . $1;\n+\treturn scan_heredoc_tag($self) if $cc eq '<<';\n+\treturn $cc if $cc =~ /^(?:&&|\\|\\||>>|;;|<&|>&|<>|>\\|)$/;\n+\tpos($$b)--;\n+\treturn $c;\n+}\n+\n+sub scan_sqstring {\n+\tmy $self = shift @_;\n+\t${$self->{buff}} =~ /\\G([^']*'|.*\\z)/sgc;\n+\tmy $s = $1;\n+\t$self->{lineno} += () = $s =~ /\\n/sg;\n+\treturn \"'\" . $s;\n+}\n+\n+sub scan_dqstring {\n+\tmy $self = shift @_;\n+\tmy $b = $self->{buff};\n+\tmy $s = '\"';\n+\twhile (1) {\n+\t\t# slurp up non-special characters\n+\t\t$s .= $1 if $$b =~ /\\G([^\"\\$\\\\]+)/gc;\n+\t\t# handle special characters\n+\t\tlast unless $$b =~ /\\G(.)/sgc;\n+\t\tmy $c = $1;\n+\t\t$s .= '\"', last if $c eq '\"';\n+\t\t$s .= '$' . $self->scan_dollar(), next if $c eq '$';\n+\t\tif ($c eq '\\\\') {\n+\t\t\t$s .= '\\\\', last unless $$b =~ /\\G(.)/sgc;\n+\t\t\t$c = $1;\n+\t\t\t$self->{lineno}++, next if $c eq \"\\n\"; # line splice\n+\t\t\t# backslash escapes only $, `, \", \\ in dq-string\n+\t\t\t$s .= '\\\\' unless $c =~ /^[\\$`\"\\\\]$/;\n+\t\t\t$s .= $c;\n+\t\t\tnext;\n+\t\t}\n+\t\tdie(\"internal error scanning dq-string '$c'\\n\");\n+\t}\n+\t$self->{lineno} += () = $s =~ /\\n/sg;\n+\treturn $s;\n+}\n+\n+sub scan_balanced {\n+\tmy ($self, $c1, $c2) = @_;\n+\tmy $b = $self->{buff};\n+\tmy $depth = 1;\n+\tmy $s = $c1;\n+\twhile ($$b =~ /\\G([^\\Q$c1$c2\\E]*(?:[\\Q$c1$c2\\E]|\\z))/gc) {\n+\t\t$s .= $1;\n+\t\t$depth++, next if $s =~ /\\Q$c1\\E$/;\n+\t\t$depth--;\n+\t\tlast if $depth == 0;\n+\t}\n+\t$self->{lineno} += () = $s =~ /\\n/sg;\n+\treturn $s;\n+}\n+\n+sub scan_subst {\n+\tmy $self = shift @_;\n+\tmy @tokens = $self->{parser}->parse(qr/^\\)$/);\n+\t$self->{parser}->next_token(); # closing \")\"\n+\treturn @tokens;\n+}\n+\n+sub scan_dollar {\n+\tmy $self = shift @_;\n+\tmy $b = $self->{buff};\n+\treturn $self->scan_balanced('(', ')') if $$b =~ /\\G\\((?=\\()/gc; # $((...))\n+\treturn '(' . join(' ', map {$_->[0]} $self->scan_subst()) . ')' if $$b =~ /\\G\\(/gc; # $(...)\n+\treturn $self->scan_balanced('{', '}') if $$b =~ /\\G\\{/gc; # ${...}\n+\treturn $1 if $$b =~ /\\G(\\w+)/gc; # $var\n+\treturn $1 if $$b =~ /\\G([@*#?$!0-9-])/gc; # $*, $1, $$, etc.\n+\treturn '';\n+}\n+\n+sub swallow_heredocs {\n+\tmy $self = shift @_;\n+\tmy $b = $self->{buff};\n+\tmy $tags = $self->{heretags};\n+\twhile (my $tag = shift @$tags) {\n+\t\tmy $start = pos($$b);\n+\t\tmy $indent = $$tag[0] =~ s/^\\t// ? '\\\\s*' : '';\n+\t\t$$b =~ /(?:\\G|\\n)$indent\\Q$$tag[0]\\E(?:\\n|\\z)/gc;\n+\t\tif (pos($$b) > $start) {\n+\t\t\tmy $body = substr($$b, $start, pos($$b) - $start);\n+\t\t\t$self->{parser}->{heredocs}->{$$tag[0]} = {\n+\t\t\t\tcontent => substr($body, 0, length($body) - length($&)),\n+\t\t\t\tstart_line => $self->{lineno},\n+\t\t        };\n+\t\t\t$self->{lineno} += () = $body =~ /\\n/sg;\n+\t\t\tnext;\n+\t\t}\n+\t\tpush(@{$self->{parser}->{problems}}, ['HEREDOC', $tag]);\n+\t\t$$b =~ /(?:\\G|\\n).*\\z/gc; # consume rest of input\n+\t\tmy $body = substr($$b, $start, pos($$b) - $start);\n+\t\t$self->{lineno} += () = $body =~ /\\n/sg;\n+\t\tlast;\n+\t}\n+}\n+\n+sub scan_token {\n+\tmy $self = shift @_;\n+\tmy $b = $self->{buff};\n+\tmy $token = '';\n+\tmy ($start, $startln);\n+RESTART:\n+\t$startln = $self->{lineno};\n+\t$$b =~ /\\G[ \\t]+/gc; # skip whitespace (but not newline)\n+\t$start = pos($$b) || 0;\n+\t$self->{lineno}++, return [\"\\n\", $start, pos($$b), $startln, $startln] if $$b =~ /\\G#[^\\n]*(?:\\n|\\z)/gc; # comment\n+\twhile (1) {\n+\t\t# slurp up non-special characters\n+\t\t$token .= $1 if $$b =~ /\\G([^\\\\;&|<>(){}'\"\\$\\s]+)/gc;\n+\t\t# handle special characters\n+\t\tlast unless $$b =~ /\\G(.)/sgc;\n+\t\tmy $c = $1;\n+\t\tpos($$b)--, last if $c =~ /^[ \\t]$/; # whitespace ends token\n+\t\tpos($$b)--, last if length($token) && $c =~ /^[;&|<>(){}\\n]$/;\n+\t\t$token .= $self->scan_sqstring(), next if $c eq \"'\";\n+\t\t$token .= $self->scan_dqstring(), next if $c eq '\"';\n+\t\t$token .= $c . $self->scan_dollar(), next if $c eq '$';\n+\t\t$self->{lineno}++, $self->swallow_heredocs(), $token = $c, last if $c eq \"\\n\";\n+\t\t$token = $self->scan_op($c), last if $c =~ /^[;&|<>]$/;\n+\t\t$token = $c, last if $c =~ /^[(){}]$/;\n+\t\tif ($c eq '\\\\') {\n+\t\t\t$token .= '\\\\', last unless $$b =~ /\\G(.)/sgc;\n+\t\t\t$c = $1;\n+\t\t\t$self->{lineno}++, next if $c eq \"\\n\" && length($token); # line splice\n+\t\t\t$self->{lineno}++, goto RESTART if $c eq \"\\n\"; # line splice\n+\t\t\t$token .= '\\\\' . $c;\n+\t\t\tnext;\n+\t\t}\n+\t\tdie(\"internal error scanning character '$c'\\n\");\n+\t}\n+\treturn length($token) ? [$token, $start, pos($$b), $startln, $self->{lineno}] : undef;\n+}\n+\n+# ShellParser parses POSIX shell scripts (with minor extensions for Bash). It\n+# is a recursive descent parser very roughly modeled after section 2.10 \"Shell\n+# Grammar\" of POSIX chapter 2 \"Shell Command Language\".\n+\n+package ShellParser;\n+\n+sub new {\n+\tmy ($class, $s) = @_;\n+\tmy $self = bless {\n+\t\tbuff => [],\n+\t\tstop => [],\n+\t\toutput => [],\n+\t\theredocs => {},\n+\t\tinsubshell => 0,\n+\t} => $class;\n+\t$self->{lexer} = Lexer->new($self, $s);\n+\treturn $self;\n+}\n+\n+sub next_token {\n+\tmy $self = shift @_;\n+\treturn pop(@{$self->{buff}}) if @{$self->{buff}};\n+\treturn $self->{lexer}->scan_token();\n+}\n+\n+sub untoken {\n+\tmy $self = shift @_;\n+\tpush(@{$self->{buff}}, @_);\n+}\n+\n+sub peek {\n+\tmy $self = shift @_;\n+\tmy $token = $self->next_token();\n+\treturn undef unless defined($token);\n+\t$self->untoken($token);\n+\treturn $token;\n+}\n+\n+sub stop_at {\n+\tmy ($self, $token) = @_;\n+\treturn 1 unless defined($token);\n+\tmy $stop = ${$self->{stop}}[-1] if @{$self->{stop}};\n+\treturn defined($stop) && $token->[0] =~ $stop;\n+}\n+\n+sub expect {\n+\tmy ($self, $expect) = @_;\n+\tmy $token = $self->next_token();\n+\treturn $token if defined($token) && $token->[0] eq $expect;\n+\tpush(@{$self->{output}}, \"?!ERR?! expected '$expect' but found '\" . (defined($token) ? $token->[0] : \"<end-of-input>\") . \"'\\n\");\n+\t$self->untoken($token) if defined($token);\n+\treturn ();\n+}\n+\n+sub optional_newlines {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\twhile (my $token = $self->peek()) {\n+\t\tlast unless $token->[0] eq \"\\n\";\n+\t\tpush(@tokens, $self->next_token());\n+\t}\n+\treturn @tokens;\n+}\n+\n+sub parse_group {\n+\tmy $self = shift @_;\n+\treturn ($self->parse(qr/^}$/),\n+\t\t$self->expect('}'));\n+}\n+\n+sub parse_subshell {\n+\tmy $self = shift @_;\n+\t$self->{insubshell}++;\n+\tmy @tokens = ($self->parse(qr/^\\)$/),\n+\t\t      $self->expect(')'));\n+\t$self->{insubshell}--;\n+\treturn @tokens;\n+}\n+\n+sub parse_case_pattern {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\twhile (defined(my $token = $self->next_token())) {\n+\t\tpush(@tokens, $token);\n+\t\tlast if $token->[0] eq ')';\n+\t}\n+\treturn @tokens;\n+}\n+\n+sub parse_case {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\tpush(@tokens,\n+\t     $self->next_token(), # subject\n+\t     $self->optional_newlines(),\n+\t     $self->expect('in'),\n+\t     $self->optional_newlines());\n+\twhile (1) {\n+\t\tmy $token = $self->peek();\n+\t\tlast unless defined($token) && $token->[0] ne 'esac';\n+\t\tpush(@tokens,\n+\t\t     $self->parse_case_pattern(),\n+\t\t     $self->optional_newlines(),\n+\t\t     $self->parse(qr/^(?:;;|esac)$/)); # item body\n+\t\t$token = $self->peek();\n+\t\tlast unless defined($token) && $token->[0] ne 'esac';\n+\t\tpush(@tokens,\n+\t\t     $self->expect(';;'),\n+\t\t     $self->optional_newlines());\n+\t}\n+\tpush(@tokens, $self->expect('esac'));\n+\treturn @tokens;\n+}\n+\n+sub parse_for {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\tpush(@tokens,\n+\t     $self->next_token(), # variable\n+\t     $self->optional_newlines());\n+\tmy $token = $self->peek();\n+\tif (defined($token) && $token->[0] eq 'in') {\n+\t\tpush(@tokens,\n+\t\t     $self->expect('in'),\n+\t\t     $self->optional_newlines());\n+\t}\n+\tpush(@tokens,\n+\t     $self->parse(qr/^do$/), # items\n+\t     $self->expect('do'),\n+\t     $self->optional_newlines(),\n+\t     $self->parse_loop_body(),\n+\t     $self->expect('done'));\n+\treturn @tokens;\n+}\n+\n+sub parse_if {\n+\tmy $self = shift @_;\n+\tmy @tokens;\n+\twhile (1) {\n+\t\tpush(@tokens,\n+\t\t     $self->parse(qr/^then$/), # if/elif condition\n+\t\t     $self->expect('then'),\n+\t\t     $self->optional_newlines(),\n+\t\t     $self->parse(qr/^(?:elif|else|fi)$/)); # if/elif body\n+\t\tmy $token = $self->peek();\n+\t\tlast unless defined($token) && $token->[0] eq 'elif';\n+\t\tpush(@tokens, $self->expect('elif'));\n+\t}\n+\tmy $token = $self->peek();\n+\tif (defined($token) && $token->[0] eq 'else') {\n+\t\tpush(@tokens,\n+\t\t     $self->expect('else'),\n+\t\t     $self->optional_newlines(),\n+\t\t     $self->parse(qr/^fi$/)); # else body\n+\t}\n+\tpush(@tokens, $self->expect('fi'));\n+\treturn @tokens;\n+}\n+\n+sub parse_loop_body {\n+\tmy $self = shift @_;\n+\treturn $self->parse(qr/^done$/);\n+}\n+\n+sub parse_loop {\n+\tmy $self = shift @_;\n+\treturn ($self->parse(qr/^do$/), # condition\n+\t\t$self->expect('do'),\n+\t\t$self->optional_newlines(),\n+\t\t$self->parse_loop_body(),\n+\t\t$self->expect('done'));\n+}\n+\n+sub parse_func {\n+\tmy $self = shift @_;\n+\treturn ($self->expect('('),\n+\t\t$self->expect(')'),\n+\t\t$self->optional_newlines(),\n+\t\t$self->parse_cmd()); # body\n+}\n+\n+sub parse_bash_array_assignment {\n+\tmy $self = shift @_;\n+\tmy @tokens = $self->expect('(');\n+\twhile (defined(my $token = $self->next_token())) {\n+\t\tpush(@tokens, $token);\n+\t\tlast if $token->[0] eq ')';\n+\t}\n+\treturn @tokens;\n+}\n+\n+my %compound = (\n+\t'{' => \\&parse_group,\n+\t'(' => \\&parse_subshell,\n+\t'case' => \\&parse_case,\n+\t'for' => \\&parse_for,\n+\t'if' => \\&parse_if,\n+\t'until' => \\&parse_loop,\n+\t'while' => \\&parse_loop);\n+\n+sub parse_cmd {\n+\tmy $self = shift @_;\n+\tmy $cmd = $self->next_token();\n+\treturn () unless defined($cmd);\n+\treturn $cmd if $cmd->[0] eq \"\\n\";\n+\n+\tmy $token;\n+\tmy @tokens = $cmd;\n+\tif ($cmd->[0] eq '!') {\n+\t\tpush(@tokens, $self->parse_cmd());\n+\t\treturn @tokens;\n+\t} elsif (my $f = $compound{$cmd->[0]}) {\n+\t\tpush(@tokens, $self->$f());\n+\t} elsif (defined($token = $self->peek()) && $token->[0] eq '(') {\n+\t\tif ($cmd->[0] !~ /\\w=$/) {\n+\t\t\tpush(@tokens, $self->parse_func());\n+\t\t\treturn @tokens;\n+\t\t}\n+\t\tmy @array = $self->parse_bash_array_assignment();\n+\t\t$tokens[-1]->[0] .= join(' ', map {$_->[0]} @array);\n+\t\t$tokens[-1]->[2] = $array[$#array][2] if @array;\n+\t}\n+\n+\twhile (defined(my $token = $self->next_token())) {\n+\t\t$self->untoken($token), last if $self->stop_at($token);\n+\t\tpush(@tokens, $token);\n+\t\tlast if $token->[0] =~ /^(?:[;&\\n|]|&&|\\|\\|)$/;\n+\t}\n+\tpush(@tokens, $self->next_token()) if $tokens[-1]->[0] ne \"\\n\" && defined($token = $self->peek()) && $token->[0] eq \"\\n\";\n+\treturn @tokens;\n+}\n+\n+sub accumulate {\n+\tmy ($self, $tokens, $cmd) = @_;\n+\tpush(@$tokens, @$cmd);\n+}\n+\n+sub parse {\n+\tmy ($self, $stop) = @_;\n+\tpush(@{$self->{stop}}, $stop);\n+\tgoto DONE if $self->stop_at($self->peek());\n+\tmy @tokens;\n+\twhile (my @cmd = $self->parse_cmd()) {\n+\t\t$self->accumulate(\\@tokens, \\@cmd);\n+\t\tlast if $self->stop_at($self->peek());\n+\t}\n+DONE:\n+\tpop(@{$self->{stop}});\n+\treturn @tokens;\n+}\n+\n+# ScriptParser is a subclass of ShellParser which identifies individual test\n+# definitions within test scripts and passes each test body to check_test().\n+# ScriptParser detects test definitions not only at the top-level of test\n+# scripts but also within compound commands such as loops and function\n+# definitions.\n+\n+package ScriptParser;\n+\n+our @ISA = ('ShellParser');\n+\n+sub new {\n+\tmy $class = shift @_;\n+\tmy $self = $class->SUPER::new(@_);\n+\t$self->{ntests} = 0;\n+\t$self->{nerrs} = 0;\n+\treturn $self;\n+}\n+\n+# extract the raw content of a token, which may be a single string or a\n+# composition of multiple strings and non-string character runs; for instance,\n+# `\"test body\"` unwraps to `test body`; `word\"a b\"42'c d'` to `worda b42c d`\n+sub unwrap {\n+\tmy $token = (@_ ? shift @_ : $_)->[0];\n+\t# simple case: 'sqstring' or \"dqstring\"\n+\treturn $token if $token =~ s/^'([^']*)'$/$1/;\n+\treturn $token if $token =~ s/^\"([^\"]*)\"$/$1/;\n+\n+\t# composite case\n+\tmy ($s, $q, $escaped);\n+\twhile (1) {\n+\t\t# slurp up non-special characters\n+\t\t$s .= $1 if $token =~ /\\G([^\\\\'\"]*)/gc;\n+\t\t# handle special characters\n+\t\tlast unless $token =~ /\\G(.)/sgc;\n+\t\tmy $c = $1;\n+\t\t$q = undef, next if defined($q) && $c eq $q;\n+\t\t$q = $c, next if !defined($q) && $c =~ /^['\"]$/;\n+\t\tif ($c eq '\\\\') {\n+\t\t\tlast unless $token =~ /\\G(.)/sgc;\n+\t\t\t$c = $1;\n+\t\t\t$s .= '\\\\' if $c eq \"\\n\"; # preserve line splice\n+\t\t}\n+\t\t$s .= $c;\n+\t}\n+\treturn $s\n+}\n+\n+sub check_test {\n+\t# no-op; subclass and override to implement lint checks\n+}\n+\n+sub parse_cmd {\n+\tmy $self = shift @_;\n+\tmy @tokens = $self->SUPER::parse_cmd();\n+\treturn @tokens unless @tokens && $tokens[0]->[0] =~ /^test_expect_(?:success|failure)$/;\n+\tmy $n = $#tokens;\n+\t$n-- while $n >= 0 && $tokens[$n]->[0] =~ /^(?:[;&\\n|]|&&|\\|\\|)$/;\n+\tmy $herebody;\n+\tif ($n >= 2 && $tokens[$n-1]->[0] eq '-' && $tokens[$n]->[0] =~ /^<<-?(.+)$/) {\n+\t\t$herebody = $self->{heredocs}->{$1};\n+\t\t$n--;\n+\t}\n+\t$self->check_test($tokens[1], $tokens[2], $herebody) if $n == 2; # title body\n+\t$self->check_test($tokens[2], $tokens[3], $herebody) if $n > 2;  # prereq title body\n+\treturn @tokens;\n+}\n+\n+1;\n-- \ngitgitgadget\n\n"},{"id":"547180","messageId":"5689d2074ad81d39502838029831f559365ca776.1783314119.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v4.git.1783314119.gitgitgadget@gmail.com","subject":"[PATCH v4 4/6] t: fix Lexer line count for $() inside double-quoted strings","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-06T05:01:56Z","receivedAt":"2026-07-06T05:02:09Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nscan_dqstring's post-loop newline counter re-counts newlines that\nwere already counted during recursive parsing of $() bodies.  This\nhappens because scan_dollar returns text containing newlines (from\nmulti-line command substitutions), and the catch-all counter at the\nend of scan_dqstring counts all of them again.\n\nFix this by counting newlines inline as non-special characters are\nconsumed, and removing the post-loop catch-all.  Each newline is\nnow counted exactly once: literal newlines at the inline match,\nline splices at the backslash handler, and $() newlines by\nscan_token during the recursive parse.\n\nThis is a latent bug: any consumer that relies on token line\nnumbers rather than byte offsets would get incorrect results for\ntokens following a multi-line $() inside a double-quoted string.\nchainlint is not affected because it annotates the original body\ntext using byte offsets, not token line numbers.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/lib-shell-parser.pl | 9 ++++++---\n 1 file changed, 6 insertions(+), 3 deletions(-)\n\ndiff --git a/t/lib-shell-parser.pl b/t/lib-shell-parser.pl\nindex 5c435c5d05..17fbf461b1 100644\n--- a/t/lib-shell-parser.pl\n+++ b/t/lib-shell-parser.pl\n@@ -93,8 +93,12 @@ sub scan_dqstring {\n \tmy $b = $self->{buff};\n \tmy $s = '\"';\n \twhile (1) {\n-\t\t# slurp up non-special characters\n-\t\t$s .= $1 if $$b =~ /\\G([^\"\\$\\\\]+)/gc;\n+\t\t# Slurp non-special characters; count newlines here because\n+\t\t# newlines inside $() are already counted by the recursive parse.\n+\t\tif ($$b =~ /\\G([^\"\\$\\\\]+)/gc) {\n+\t\t\t$s .= $1;\n+\t\t\t$self->{lineno} += $1 =~ tr/\\n//;\n+\t\t}\n \t\t# handle special characters\n \t\tlast unless $$b =~ /\\G(.)/sgc;\n \t\tmy $c = $1;\n@@ -111,7 +115,6 @@ sub scan_dqstring {\n \t\t}\n \t\tdie(\"internal error scanning dq-string '$c'\\n\");\n \t}\n-\t$self->{lineno} += () = $s =~ /\\n/sg;\n \treturn $s;\n }\n \n-- \ngitgitgadget\n\n"},{"id":"547182","messageId":"5cda64fc260d96b9dcc239d0046d1a0208ce9737.1783314119.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v4.git.1783314119.gitgitgadget@gmail.com","subject":"[PATCH v4 6/6] t: add greplint to detect bare grep assertions","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-06T05:01:58Z","receivedAt":"2026-07-06T05:02:13Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nWithout a lint guard, bare grep assertions will creep back into\ntests over time, defeating the previous commit's conversion.\n\nAdd greplint.pl to catch bare 'grep' used as a test assertion\n(where 'test_grep' should be used) and '! test_grep' (where\n'test_grep !' should be used).\n\ngreplint.pl reuses the shared shell parser from lib-shell-parser.pl\nto tokenize test bodies.  The Lexer collapses heredocs, command\nsubstitutions, and quoted strings into single tokens, so 'grep'\nappearing inside these contexts is not flagged.  A flat walk over\nthe token stream tracks command position and pipeline state to\ndistinguish assertion greps from filter greps.\n\nFor double-quoted test bodies, a source-line walk counts\nbackslash-continuation lines that the Lexer consumes without\nemitting into the body text, adjusting the reported line number\naccordingly.\n\nAdd test fixtures in greplint/ (modeled on chainlint/) covering\ndetection of bare grep assertions, correct skipping of filters,\npipelines, redirects, command substitutions, and lint-ok annotations.\n\nWire into the Makefile as:\n  - test-greplint: runs greplint.pl on $(T) $(THELPERS) $(TPERF)\n  - check-greplint: runs greplint.pl on fixtures, diffs against expected\n  - clean-greplint: removes temp dir\n\nAdd eol=lf entries in t/.gitattributes for greplint fixtures,\nmatching chainlint, so that check-greplint passes on Windows\nwhere core.autocrlf would otherwise cause CRLF mismatches\nbetween expected and actual output.\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/.gitattributes                              |   2 +\n t/Makefile                                    |  29 +-\n t/greplint-cat.pl                             |  27 ++\n t/greplint.pl                                 | 258 ++++++++++++++++++\n t/greplint/bare-grep-after-and.expect         |   1 +\n t/greplint/bare-grep-after-and.test           |   4 +\n t/greplint/bare-grep-after-semicolon.expect   |   1 +\n t/greplint/bare-grep-after-semicolon.test     |   4 +\n t/greplint/bare-grep-compound-body.expect     |   3 +\n t/greplint/bare-grep-compound-body.test       |  17 ++\n t/greplint/bare-grep-count-mode.expect        |   1 +\n t/greplint/bare-grep-count-mode.test          |   3 +\n t/greplint/bare-grep-explicit-pattern.expect  |   1 +\n t/greplint/bare-grep-explicit-pattern.test    |   3 +\n t/greplint/bare-grep-flags.expect             |   1 +\n t/greplint/bare-grep-flags.test               |   3 +\n t/greplint/bare-grep-lint-ok.expect           |   0\n t/greplint/bare-grep-lint-ok.test             |   4 +\n t/greplint/bare-grep-negated.expect           |   1 +\n t/greplint/bare-grep-negated.test             |   3 +\n t/greplint/bare-grep-pattern-file.expect      |   1 +\n t/greplint/bare-grep-pattern-file.test        |   3 +\n t/greplint/bare-grep-simple.expect            |   1 +\n t/greplint/bare-grep-simple.test              |   3 +\n t/greplint/bare-grep-subshell.expect          |   1 +\n t/greplint/bare-grep-subshell.test            |   5 +\n .../dqstring-continuation-offset.expect       |   1 +\n t/greplint/dqstring-continuation-offset.test  |  11 +\n t/greplint/filter-command-substitution.expect |   0\n t/greplint/filter-command-substitution.test   |   3 +\n t/greplint/filter-pipe-input.expect           |   0\n t/greplint/filter-pipe-input.test             |   3 +\n t/greplint/filter-pipe-output.expect          |   0\n t/greplint/filter-pipe-output.test            |   3 +\n t/greplint/filter-redirect-output.expect      |   0\n t/greplint/filter-redirect-output.test        |   3 +\n t/greplint/filter-stdin-redirect.expect       |   0\n t/greplint/filter-stdin-redirect.test         |   3 +\n t/greplint/grep-as-argument.expect            |   0\n t/greplint/grep-as-argument.test              |   3 +\n t/greplint/grep-as-value.expect               |   0\n t/greplint/grep-as-value.test                 |   6 +\n t/greplint/wrong-negation.expect              |   1 +\n t/greplint/wrong-negation.test                |   3 +\n 44 files changed, 415 insertions(+), 5 deletions(-)\n create mode 100644 t/greplint-cat.pl\n create mode 100644 t/greplint.pl\n create mode 100644 t/greplint/bare-grep-after-and.expect\n create mode 100644 t/greplint/bare-grep-after-and.test\n create mode 100644 t/greplint/bare-grep-after-semicolon.expect\n create mode 100644 t/greplint/bare-grep-after-semicolon.test\n create mode 100644 t/greplint/bare-grep-compound-body.expect\n create mode 100644 t/greplint/bare-grep-compound-body.test\n create mode 100644 t/greplint/bare-grep-count-mode.expect\n create mode 100644 t/greplint/bare-grep-count-mode.test\n create mode 100644 t/greplint/bare-grep-explicit-pattern.expect\n create mode 100644 t/greplint/bare-grep-explicit-pattern.test\n create mode 100644 t/greplint/bare-grep-flags.expect\n create mode 100644 t/greplint/bare-grep-flags.test\n create mode 100644 t/greplint/bare-grep-lint-ok.expect\n create mode 100644 t/greplint/bare-grep-lint-ok.test\n create mode 100644 t/greplint/bare-grep-negated.expect\n create mode 100644 t/greplint/bare-grep-negated.test\n create mode 100644 t/greplint/bare-grep-pattern-file.expect\n create mode 100644 t/greplint/bare-grep-pattern-file.test\n create mode 100644 t/greplint/bare-grep-simple.expect\n create mode 100644 t/greplint/bare-grep-simple.test\n create mode 100644 t/greplint/bare-grep-subshell.expect\n create mode 100644 t/greplint/bare-grep-subshell.test\n create mode 100644 t/greplint/dqstring-continuation-offset.expect\n create mode 100644 t/greplint/dqstring-continuation-offset.test\n create mode 100644 t/greplint/filter-command-substitution.expect\n create mode 100644 t/greplint/filter-command-substitution.test\n create mode 100644 t/greplint/filter-pipe-input.expect\n create mode 100644 t/greplint/filter-pipe-input.test\n create mode 100644 t/greplint/filter-pipe-output.expect\n create mode 100644 t/greplint/filter-pipe-output.test\n create mode 100644 t/greplint/filter-redirect-output.expect\n create mode 100644 t/greplint/filter-redirect-output.test\n create mode 100644 t/greplint/filter-stdin-redirect.expect\n create mode 100644 t/greplint/filter-stdin-redirect.test\n create mode 100644 t/greplint/grep-as-argument.expect\n create mode 100644 t/greplint/grep-as-argument.test\n create mode 100644 t/greplint/grep-as-value.expect\n create mode 100644 t/greplint/grep-as-value.test\n create mode 100644 t/greplint/wrong-negation.expect\n create mode 100644 t/greplint/wrong-negation.test\n\ndiff --git a/t/.gitattributes b/t/.gitattributes\nindex 7664c6e027..e867f38c71 100644\n--- a/t/.gitattributes\n+++ b/t/.gitattributes\n@@ -1,5 +1,7 @@\n t[0-9][0-9][0-9][0-9]/* -whitespace\n /chainlint/*.expect eol=lf -whitespace\n+/greplint/*.expect eol=lf -whitespace\n+/greplint/*.test eol=lf -whitespace\n /t0110/url-* binary\n /t3206/* eol=lf\n /t3900/*.txt eol=lf\ndiff --git a/t/Makefile b/t/Makefile\nindex ab8a5b54aa..e3249879c4 100644\n--- a/t/Makefile\n+++ b/t/Makefile\n@@ -27,9 +27,11 @@ TEST_LINT ?= test-lint\n ifdef TEST_OUTPUT_DIRECTORY\n TEST_RESULTS_DIRECTORY = $(TEST_OUTPUT_DIRECTORY)/test-results\n CHAINLINTTMP = $(TEST_OUTPUT_DIRECTORY)/chainlinttmp\n+GREPLINTTMP = $(TEST_OUTPUT_DIRECTORY)/greplinttmp\n else\n TEST_RESULTS_DIRECTORY = test-results\n CHAINLINTTMP = chainlinttmp\n+GREPLINTTMP = greplinttmp\n endif\n \n # Shell quote;\n@@ -38,6 +40,7 @@ TEST_SHELL_PATH_SQ = $(subst ','\\'',$(TEST_SHELL_PATH))\n PERL_PATH_SQ = $(subst ','\\'',$(PERL_PATH))\n TEST_RESULTS_DIRECTORY_SQ = $(subst ','\\'',$(TEST_RESULTS_DIRECTORY))\n CHAINLINTTMP_SQ = $(subst ','\\'',$(CHAINLINTTMP))\n+GREPLINTTMP_SQ = $(subst ','\\'',$(GREPLINTTMP))\n \n T = $(sort $(wildcard t[0-9][0-9][0-9][0-9]-*.sh))\n THELPERS = $(sort $(filter-out $(T),$(wildcard *.sh)))\n@@ -45,6 +48,7 @@ TLIBS = $(sort $(wildcard lib-*.sh)) annotate-tests.sh\n TPERF = $(sort $(wildcard perf/p[0-9][0-9][0-9][0-9]-*.sh))\n TINTEROP = $(sort $(wildcard interop/i[0-9][0-9][0-9][0-9]-*.sh))\n CHAINLINTTESTS = $(sort $(patsubst chainlint/%.test,%,$(wildcard chainlint/*.test)))\n+GREPLINTTESTS = $(sort $(patsubst greplint/%.test,%,$(wildcard greplint/*.test)))\n CHAINLINT = '$(PERL_PATH_SQ)' chainlint.pl\n UNIT_TEST_SOURCES = $(wildcard unit-tests/t-*.c)\n UNIT_TEST_PROGRAMS = $(patsubst unit-tests/%.c,unit-tests/bin/%$(X),$(UNIT_TEST_SOURCES))\n@@ -63,8 +67,8 @@ test: pre-clean check-meson $(TEST_LINT)\n \t$(CHAINLINTSUPPRESS) $(MAKE) aggregate-results-and-cleanup\n \n ifneq ($(PERL_PATH),)\n-test: check-chainlint\n-prove: check-chainlint\n+test: check-chainlint check-greplint\n+prove: check-chainlint check-greplint\n endif\n \n failed:\n@@ -102,7 +106,7 @@ unit-tests-test-tool:\n pre-clean:\n \t$(RM) -r '$(TEST_RESULTS_DIRECTORY_SQ)'\n \n-clean-except-prove-cache: clean-chainlint\n+clean-except-prove-cache: clean-chainlint clean-greplint\n \t$(RM) -r 'trash directory'.*\n \t$(RM) -r valgrind/bin\n \n@@ -120,6 +124,17 @@ check-chainlint:\n \t{ $(CHAINLINT) --emit-all '$(CHAINLINTTMP_SQ)'/tests >'$(CHAINLINTTMP_SQ)'/actual || true; } && \\\n \tdiff -u '$(CHAINLINTTMP_SQ)'/expect '$(CHAINLINTTMP_SQ)'/actual\n \n+clean-greplint:\n+\t$(RM) -r '$(GREPLINTTMP_SQ)'\n+\n+check-greplint:\n+\t@mkdir -p '$(GREPLINTTMP_SQ)' && \\\n+\t'$(PERL_PATH_SQ)' greplint-cat.pl '$(GREPLINTTMP_SQ)' $(GREPLINTTESTS) && \\\n+\t{ '$(PERL_PATH_SQ)' greplint.pl \\\n+\t\t$(patsubst %,greplint/%.test,$(GREPLINTTESTS)) \\\n+\t\t>'$(GREPLINTTMP_SQ)'/actual 2>&1 || true; } && \\\n+\tdiff -u '$(GREPLINTTMP_SQ)'/expect '$(GREPLINTTMP_SQ)'/actual\n+\n check-meson:\n \t@# awk acts up when trying to match single quotes, so we use \\047 instead.\n \t@mkdir -p mesontmp && \\\n@@ -139,7 +154,7 @@ check-meson:\n test-lint: test-lint-duplicates test-lint-executable \\\n \ttest-lint-filenames\n ifneq ($(PERL_PATH),)\n-test-lint: test-lint-shell-syntax\n+test-lint: test-lint-shell-syntax test-greplint\n else\n GIT_TEST_CHAIN_LINT = 0\n endif\n@@ -160,6 +175,9 @@ test-lint-executable:\n test-lint-shell-syntax:\n \t@'$(PERL_PATH_SQ)' check-non-portable-shell.pl $(T) $(THELPERS) $(TPERF)\n \n+test-greplint:\n+\t@'$(PERL_PATH_SQ)' greplint.pl $(T) $(THELPERS) $(TPERF)\n+\n test-lint-filenames:\n \t@# We do *not* pass a glob to ls-files but use grep instead, to catch\n \t@# non-ASCII characters (which are quoted within double-quotes)\n@@ -185,7 +203,8 @@ perf:\n \t$(MAKE) -C perf/ all\n \n .PHONY: pre-clean $(T) aggregate-results clean valgrind perf \\\n-\tcheck-chainlint clean-chainlint test-chainlint $(UNIT_TESTS)\n+\tcheck-chainlint clean-chainlint test-chainlint \\\n+\tcheck-greplint clean-greplint test-greplint $(UNIT_TESTS)\n \n .PHONY: libgit-sys-test libgit-rs-test\n libgit-sys-test:\ndiff --git a/t/greplint-cat.pl b/t/greplint-cat.pl\nnew file mode 100644\nindex 0000000000..fa9c3b8aab\n--- /dev/null\n+++ b/t/greplint-cat.pl\n@@ -0,0 +1,27 @@\n+#!/usr/bin/env perl\n+\n+use strict;\n+use warnings;\n+\n+# Assemble expected output for check-greplint target.\n+# Usage: greplint-cat.pl <outdir> <test-name> ...\n+#\n+# For each <test-name>, reads greplint/<test-name>.expect and\n+# prepends \"greplint/<test-name>.test:\" to every non-empty line,\n+# matching the output format of greplint.pl.  Writes combined\n+# expected output to <outdir>/expect.\n+\n+my $outdir = shift;\n+open(my $expect, '>', \"$outdir/expect\")\n+\tor die \"unable to open $outdir/expect: $!\";\n+\n+for my $name (@ARGV) {\n+\topen(my $fh, '<', \"greplint/$name.expect\")\n+\t\tor die \"unable to open greplint/$name.expect: $!\";\n+\twhile (<$fh>) {\n+\t\tprint $expect \"greplint/$name.test:$_\";\n+\t}\n+\tclose $fh;\n+}\n+\n+close $expect;\ndiff --git a/t/greplint.pl b/t/greplint.pl\nnew file mode 100644\nindex 0000000000..23efe27511\n--- /dev/null\n+++ b/t/greplint.pl\n@@ -0,0 +1,258 @@\n+#!/usr/bin/env perl\n+\n+# Detect bare 'grep' used as a test assertion where 'test_grep'\n+# should be used, and '! test_grep' where 'test_grep !' should\n+# be used.\n+#\n+# The shared shell parser tokenizes test bodies so that 'grep'\n+# inside heredocs, command substitutions like $(grep ...), and\n+# quoted strings is collapsed into a single token and never seen\n+# by our check.  A line-oriented approach would need to track\n+# heredoc delimiters, nested $() depth, and cross-line pipe\n+# state to avoid false positives on patterns like:\n+#\n+#   write_script foo.sh <<-\\EOF\n+#   grep pattern file    # data, not an assertion\n+#   EOF\n+#\n+# The Lexer already handles these.\n+\n+use warnings;\n+use strict;\n+use File::Basename;\n+do(dirname($0) . \"/lib-shell-parser.pl\")\n+\tor die \"$0: failed to load lib-shell-parser.pl: $@$!\\n\";\n+\n+my $exit_code = 0;\n+\n+# GrepLintParser inherits ScriptParser's ability to find\n+# test_expect_success/failure blocks and call check_test()\n+# on each body.  We override check_test() to walk the token\n+# stream looking for bare grep assertions.\n+package GrepLintParser;\n+\n+our @ISA = ('ScriptParser');\n+\n+# After these tokens, the next token is a command word.\n+# For example, in 'echo foo && grep bar file', the 'grep'\n+# after '&&' is at command position and should be flagged.\n+my %cmd_start = map { $_ => 1 } qw(&& || ; ;; do then else elif), \"\\n\", '{', '(';\n+\n+# Tokens indicating grep's output is piped or redirected.\n+my %filter_op = map { $_ => 1 } qw(| > >> <);\n+\n+# A token is at \"command word\" position if the shell would\n+# interpret it as a program name rather than an argument.\n+# Only 'grep' at command position is an assertion we should\n+# flag; 'grep' as an argument ('test_must_fail grep') or\n+# value ('for cmd in grep sed') is not.\n+sub is_command_word {\n+\tmy ($tokens, $pos) = @_;\n+\treturn 1 if $pos == 0;\n+\tfor (my $j = $pos - 1; $j >= 0; $j--) {\n+\t\tmy $t = $tokens->[$j]->[0];\n+\t\t# After a separator or pipe, a new command starts.\n+\t\treturn 1 if $cmd_start{$t} || $t eq '|';\n+\t\t# After '}' or ')', what follows is a separator or\n+\t\t# redirect on the compound command, not a new command.\n+\t\treturn 0 if $t eq '}' || $t eq ')';\n+\t\t# '!' is a prefix that does not consume command\n+\t\t# position; keep scanning to find what precedes it.\n+\t\tnext if $t eq '!';\n+\t\t# Any other word means we are past the command word.\n+\t\treturn 0;\n+\t}\n+\treturn 1;\n+}\n+\n+# lint_ok() reports whether a bare grep carries a trailing\n+# '# lint-ok' comment telling this linter to skip it.\n+#\n+# In practice this is needed for just one case: a grep acting\n+# as a data filter whose output is consumed by a redirect or\n+# pipe on an enclosing compound command (such as a subshell or\n+# brace group) rather than by grep's own pipeline, e.g.\n+#\n+#\t( grep ... && # lint-ok\n+#\t  sed ... ) >out\n+#\n+#\t{ grep ... || : # lint-ok\n+#\t} >out\n+#\n+# is_filter() only scans grep's own pipeline: it stops at the\n+# separator before the compound command closes and never sees\n+# the outer redirect, so it would flag such a grep as an\n+# assertion.  A grep that really is an assertion is better\n+# written as test_grep (or a guarded test_grep when the file's\n+# presence is conditional) than annotated with lint-ok.\n+sub lint_ok {\n+\tmy ($raw_lines, $ln) = @_;\n+\tif ($ln < 1 || $ln > @$raw_lines) {\n+\t\twarn \"lint_ok: line number $ln out of range (1..\" .\n+\t\t    scalar(@$raw_lines) . \")\\n\";\n+\t\treturn 0;\n+\t}\n+\treturn $raw_lines->[$ln - 1] =~ /lint-ok/;\n+}\n+\n+# Grep is a filter (not an assertion) if it receives piped\n+# input or sends its output to a pipe or redirect.  Check\n+# both directions from grep's position in the token stream.\n+sub is_filter {\n+\tmy ($tokens, $pos) = @_;\n+\t# Backward: is grep receiving piped input?\n+\t# Newlines don't break pipes ('cmd |\\n grep' is one\n+\t# pipeline), so skip past them.\n+\tfor (my $j = $pos - 1; $j >= 0; $j--) {\n+\t\tmy $t = $tokens->[$j]->[0];\n+\t\treturn 1 if $t eq '|';\n+\t\tnext if $t eq \"\\n\";\n+\t\tlast if $cmd_start{$t} || $t eq '}' || $t eq ')';\n+\t}\n+\t# Forward: is grep piping or redirecting output?\n+\t# Unlike the backward scan, we do not skip newlines here:\n+\t# a bare newline is a command boundary, and redirects or\n+\t# pipes must appear on the same line as grep (or after a\n+\t# line continuation, which the Lexer consumes).\n+\tfor (my $j = $pos + 1; $j < @$tokens; $j++) {\n+\t\tmy $t = $tokens->[$j]->[0];\n+\t\treturn 0 if $cmd_start{$t};\n+\t\treturn 1 if $filter_op{$t};\n+\t}\n+\treturn 0;\n+}\n+\n+# Map a body-relative line number to a file line number.\n+# For double-quoted bodies, backslash-continuation lines\n+# (\\<newline>) are consumed by the Lexer without appearing\n+# in the body text, so the inner parser sees fewer lines\n+# than the source file has.  We walk the source lines to\n+# count continuations and adjust accordingly.\n+sub body_to_file_line {\n+\tmy ($body_lineno, $body_token, $raw_lines, $body_start) = @_;\n+\tmy $body_text = $body_token->[0];\n+\tmy $body_end_line = $body_token->[4];\n+\tunless ($body_start && $body_start >= 1) {\n+\t\twarn \"body_start is not a positive integer\\n\";\n+\t\treturn $body_lineno;\n+\t}\n+\tmy $file_lineno = $body_lineno + $body_start - 1;\n+\t# Only double-quoted bodies have line splices.\n+\treturn $file_lineno unless $body_text =~ /^\"/;\n+\tmy $adj = 0;\n+\tmy $lines_seen = 0;\n+\tunless ($body_end_line && $body_end_line >= $body_start) {\n+\t\twarn \"body_end_line is not set for double-quoted body\\n\";\n+\t\treturn $file_lineno;\n+\t}\n+\tmy $end = $body_end_line;\n+\tif ($end > @$raw_lines) {\n+\t\twarn \"body_end_line ($end) exceeds file length (\" .\n+\t\t    scalar(@$raw_lines) . \")\\n\";\n+\t\treturn $file_lineno;\n+\t}\n+\tmy $src_ln = $body_start;\n+\twhile ($src_ln <= $end && $lines_seen < $body_lineno) {\n+\t\tmy $line = $raw_lines->[$src_ln - 1];\n+\t\t# Odd trailing backslashes = continuation (\\<nl>).\n+\t\t# Even = escaped backslashes (\\\\), not a continuation.\n+\t\tif ($line =~ /(\\\\*)$/ && length($1) % 2 == 1) {\n+\t\t\t$adj++;\n+\t\t} else {\n+\t\t\t$lines_seen++;\n+\t\t}\n+\t\t$src_ln++;\n+\t}\n+\tif ($lines_seen < $body_lineno) {\n+\t\twarn \"body_lineno ($body_lineno) not found within body range \" .\n+\t\t    \"($body_start..$end)\\n\";\n+\t}\n+\treturn $file_lineno + $adj;\n+}\n+\n+# ScriptParser calls this for each test body found in the script.\n+sub check_test {\n+\tmy $self = shift @_;\n+\tmy $title = ScriptParser::unwrap(shift @_);\n+\tmy $body_token = shift @_;\n+\tmy $body_start = $body_token->[3];\n+\tmy $body = ScriptParser::unwrap($body_token);\n+\t# Handle heredoc-style test bodies:\n+\t#   test_expect_success 'title' - <<\\EOF\n+\t#   grep pattern file\n+\t#   EOF\n+\t# The '-' signals that the body follows as a heredoc.\n+\tif ($body eq '-') {\n+\t\tmy $herebody = shift @_;\n+\t\tif ($herebody) {\n+\t\t\t$body = $herebody->{content};\n+\t\t\t$body_start = $herebody->{start_line};\n+\t\t}\n+\t}\n+\treturn unless $body;\n+\n+\tmy $raw_lines = $self->{raw_lines};\n+\n+\t# The outer parser gives us the body as an opaque string.\n+\t# Parse it to get individual tokens with command boundaries.\n+\tmy $parser = ShellParser->new(\\$body);\n+\tmy @tokens = $parser->parse();\n+\n+\tmy $file = $self->{file};\n+\n+\tfor (my $i = 0; $i < @tokens; $i++) {\n+\t\tmy $text = $tokens[$i]->[0];\n+\t\tnext unless is_command_word(\\@tokens, $i);\n+\n+\t\tmy $token_lineno = $tokens[$i]->[3];\n+\t\tunless (defined($token_lineno) && $token_lineno >= 1) {\n+\t\t\twarn \"token has no line number\\n\";\n+\t\t\tnext;\n+\t\t}\n+\t\tmy $file_lineno = body_to_file_line(\n+\t\t\t$token_lineno,\n+\t\t\t$body_token, $raw_lines, $body_start);\n+\n+\t\t# '!' negates the exit code without consuming command\n+\t\t# position.  '! test_grep' is an anti-pattern because\n+\t\t# test_grep only prints diagnostics on grep failure,\n+\t\t# and '!' inverts after that decision is already made.\n+\t\tif ($text eq '!') {\n+\t\t\tif ($i + 1 < @tokens &&\n+\t\t\t    $tokens[$i + 1]->[0] eq 'test_grep' &&\n+\t\t\t    !lint_ok($raw_lines, $file_lineno)) {\n+\t\t\t\tprint \"$file:$file_lineno: error: \",\n+\t\t\t\t    'use \"test_grep !\" instead of ',\n+\t\t\t\t    '\"! test_grep\"', \"\\n\";\n+\t\t\t\t$exit_code = 1;\n+\t\t\t}\n+\t\t\tnext;\n+\t\t}\n+\n+\t\t# Bare grep as a command (not a filter) is a test\n+\t\t# assertion that should use test_grep for better\n+\t\t# failure diagnostics.\n+\t\tif ($text eq 'grep' &&\n+\t\t    !is_filter(\\@tokens, $i) &&\n+\t\t    !lint_ok($raw_lines, $file_lineno)) {\n+\t\t\tprint \"$file:$file_lineno: error: \",\n+\t\t\t    \"bare grep outside pipeline \",\n+\t\t\t    \"(use test_grep)\\n\";\n+\t\t\t$exit_code = 1;\n+\t\t}\n+\t}\n+}\n+\n+package main;\n+\n+for my $file (@ARGV) {\n+\topen(my $fh, '<:unix:crlf', $file) or die \"$0: $file: $!\\n\";\n+\tmy @raw_lines = <$fh>;\n+\tclose $fh;\n+\tmy $s = join('', @raw_lines);\n+\tmy $parser = GrepLintParser->new(\\$s);\n+\t$parser->{file} = $file;\n+\t$parser->{raw_lines} = \\@raw_lines;\n+\t$parser->parse();\n+}\n+exit $exit_code;\ndiff --git a/t/greplint/bare-grep-after-and.expect b/t/greplint/bare-grep-after-and.expect\nnew file mode 100644\nindex 0000000000..7da1b21aa8\n--- /dev/null\n+++ b/t/greplint/bare-grep-after-and.expect\n@@ -0,0 +1 @@\n+3: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-after-and.test b/t/greplint/bare-grep-after-and.test\nnew file mode 100644\nindex 0000000000..c5b26e4572\n--- /dev/null\n+++ b/t/greplint/bare-grep-after-and.test\n@@ -0,0 +1,4 @@\n+test_expect_success 'grep after && is flagged' '\n+\tcmd &&\n+\tgrep pattern file\n+'\ndiff --git a/t/greplint/bare-grep-after-semicolon.expect b/t/greplint/bare-grep-after-semicolon.expect\nnew file mode 100644\nindex 0000000000..7da1b21aa8\n--- /dev/null\n+++ b/t/greplint/bare-grep-after-semicolon.expect\n@@ -0,0 +1 @@\n+3: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-after-semicolon.test b/t/greplint/bare-grep-after-semicolon.test\nnew file mode 100644\nindex 0000000000..c1e468ddf8\n--- /dev/null\n+++ b/t/greplint/bare-grep-after-semicolon.test\n@@ -0,0 +1,4 @@\n+test_expect_success 'grep after semicolon is flagged' '\n+\techo hello;\n+\tgrep pattern file\n+'\ndiff --git a/t/greplint/bare-grep-compound-body.expect b/t/greplint/bare-grep-compound-body.expect\nnew file mode 100644\nindex 0000000000..eb6210f0c9\n--- /dev/null\n+++ b/t/greplint/bare-grep-compound-body.expect\n@@ -0,0 +1,3 @@\n+4: error: bare grep outside pipeline (use test_grep)\n+8: error: bare grep outside pipeline (use test_grep)\n+15: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-compound-body.test b/t/greplint/bare-grep-compound-body.test\nnew file mode 100644\nindex 0000000000..72472edcf6\n--- /dev/null\n+++ b/t/greplint/bare-grep-compound-body.test\n@@ -0,0 +1,17 @@\n+test_expect_success 'grep after then/do/else is flagged' '\n+\tif true\n+\tthen\n+\t\tgrep pattern file\n+\tfi &&\n+\twhile true\n+\tdo\n+\t\tgrep pattern file &&\n+\t\tbreak\n+\tdone &&\n+\tif true\n+\tthen\n+\t\techo yes\n+\telse\n+\t\tgrep pattern file\n+\tfi\n+'\ndiff --git a/t/greplint/bare-grep-count-mode.expect b/t/greplint/bare-grep-count-mode.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-count-mode.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-count-mode.test b/t/greplint/bare-grep-count-mode.test\nnew file mode 100644\nindex 0000000000..b9c0f18783\n--- /dev/null\n+++ b/t/greplint/bare-grep-count-mode.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep -c is flagged (not special-cased)' '\n+\tgrep -c pattern file\n+'\ndiff --git a/t/greplint/bare-grep-explicit-pattern.expect b/t/greplint/bare-grep-explicit-pattern.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-explicit-pattern.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-explicit-pattern.test b/t/greplint/bare-grep-explicit-pattern.test\nnew file mode 100644\nindex 0000000000..69d8651019\n--- /dev/null\n+++ b/t/greplint/bare-grep-explicit-pattern.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep -e is flagged' '\n+\tgrep -e pattern file\n+'\ndiff --git a/t/greplint/bare-grep-flags.expect b/t/greplint/bare-grep-flags.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-flags.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-flags.test b/t/greplint/bare-grep-flags.test\nnew file mode 100644\nindex 0000000000..9ca0e10233\n--- /dev/null\n+++ b/t/greplint/bare-grep-flags.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep -E is flagged' '\n+\tgrep -E \"pat+ern\" file\n+'\ndiff --git a/t/greplint/bare-grep-lint-ok.expect b/t/greplint/bare-grep-lint-ok.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/bare-grep-lint-ok.test b/t/greplint/bare-grep-lint-ok.test\nnew file mode 100644\nindex 0000000000..335b902a42\n--- /dev/null\n+++ b/t/greplint/bare-grep-lint-ok.test\n@@ -0,0 +1,4 @@\n+test_expect_success 'grep with lint-ok annotation is not flagged' '\n+\tgrep pattern file && # lint-ok\n+\techo done\n+'\ndiff --git a/t/greplint/bare-grep-negated.expect b/t/greplint/bare-grep-negated.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-negated.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-negated.test b/t/greplint/bare-grep-negated.test\nnew file mode 100644\nindex 0000000000..ef789bc8b7\n--- /dev/null\n+++ b/t/greplint/bare-grep-negated.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'negated grep is flagged' '\n+\t! grep pattern file\n+'\ndiff --git a/t/greplint/bare-grep-pattern-file.expect b/t/greplint/bare-grep-pattern-file.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-pattern-file.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-pattern-file.test b/t/greplint/bare-grep-pattern-file.test\nnew file mode 100644\nindex 0000000000..d70035e449\n--- /dev/null\n+++ b/t/greplint/bare-grep-pattern-file.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep -f is flagged' '\n+\tgrep -f patterns.txt file\n+'\ndiff --git a/t/greplint/bare-grep-simple.expect b/t/greplint/bare-grep-simple.expect\nnew file mode 100644\nindex 0000000000..8922d35b42\n--- /dev/null\n+++ b/t/greplint/bare-grep-simple.expect\n@@ -0,0 +1 @@\n+2: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-simple.test b/t/greplint/bare-grep-simple.test\nnew file mode 100644\nindex 0000000000..3a8d5f9bbd\n--- /dev/null\n+++ b/t/greplint/bare-grep-simple.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'bare grep is flagged' '\n+\tgrep pattern file\n+'\ndiff --git a/t/greplint/bare-grep-subshell.expect b/t/greplint/bare-grep-subshell.expect\nnew file mode 100644\nindex 0000000000..7da1b21aa8\n--- /dev/null\n+++ b/t/greplint/bare-grep-subshell.expect\n@@ -0,0 +1 @@\n+3: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/bare-grep-subshell.test b/t/greplint/bare-grep-subshell.test\nnew file mode 100644\nindex 0000000000..1fab2d3363\n--- /dev/null\n+++ b/t/greplint/bare-grep-subshell.test\n@@ -0,0 +1,5 @@\n+test_expect_success 'grep in subshell is flagged' '\n+\t(\n+\t\tgrep pattern file\n+\t)\n+'\ndiff --git a/t/greplint/dqstring-continuation-offset.expect b/t/greplint/dqstring-continuation-offset.expect\nnew file mode 100644\nindex 0000000000..8185bfdca4\n--- /dev/null\n+++ b/t/greplint/dqstring-continuation-offset.expect\n@@ -0,0 +1 @@\n+10: error: bare grep outside pipeline (use test_grep)\ndiff --git a/t/greplint/dqstring-continuation-offset.test b/t/greplint/dqstring-continuation-offset.test\nnew file mode 100644\nindex 0000000000..b332bd8f3b\n--- /dev/null\n+++ b/t/greplint/dqstring-continuation-offset.test\n@@ -0,0 +1,11 @@\n+# Double-quoted test bodies with backslash-continuation lines:\n+# the splice adjustment in check_test compensates for \\<newline>\n+# lines that the lexer consumes without emitting into the body\n+# text, so the reported line number matches the source.\n+test_expect_success 'dqstring continuation offset' \"\n+\tx=\\$(echo \\\n+\t\thello) &&\n+\ty=\\$(echo \\\n+\t\tworld) &&\n+\tgrep pattern file\n+\"\ndiff --git a/t/greplint/filter-command-substitution.expect b/t/greplint/filter-command-substitution.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/filter-command-substitution.test b/t/greplint/filter-command-substitution.test\nnew file mode 100644\nindex 0000000000..bd9a100a5d\n--- /dev/null\n+++ b/t/greplint/filter-command-substitution.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep in command substitution is not flagged' '\n+\tx=$(grep pattern file)\n+'\ndiff --git a/t/greplint/filter-pipe-input.expect b/t/greplint/filter-pipe-input.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/filter-pipe-input.test b/t/greplint/filter-pipe-input.test\nnew file mode 100644\nindex 0000000000..cb5c0d9e23\n--- /dev/null\n+++ b/t/greplint/filter-pipe-input.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep receiving pipe input is not flagged' '\n+\tcmd | grep pattern\n+'\ndiff --git a/t/greplint/filter-pipe-output.expect b/t/greplint/filter-pipe-output.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/filter-pipe-output.test b/t/greplint/filter-pipe-output.test\nnew file mode 100644\nindex 0000000000..01340086d7\n--- /dev/null\n+++ b/t/greplint/filter-pipe-output.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep piping to another command is not flagged' '\n+\tgrep pattern file | wc -l\n+'\ndiff --git a/t/greplint/filter-redirect-output.expect b/t/greplint/filter-redirect-output.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/filter-redirect-output.test b/t/greplint/filter-redirect-output.test\nnew file mode 100644\nindex 0000000000..ee70ba7cde\n--- /dev/null\n+++ b/t/greplint/filter-redirect-output.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep with output redirect is not flagged' '\n+\tgrep pattern file >output\n+'\ndiff --git a/t/greplint/filter-stdin-redirect.expect b/t/greplint/filter-stdin-redirect.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/filter-stdin-redirect.test b/t/greplint/filter-stdin-redirect.test\nnew file mode 100644\nindex 0000000000..292db7766e\n--- /dev/null\n+++ b/t/greplint/filter-stdin-redirect.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep reading from stdin redirect is not flagged' '\n+\tgrep pattern <input\n+'\ndiff --git a/t/greplint/grep-as-argument.expect b/t/greplint/grep-as-argument.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/grep-as-argument.test b/t/greplint/grep-as-argument.test\nnew file mode 100644\nindex 0000000000..7157228b60\n--- /dev/null\n+++ b/t/greplint/grep-as-argument.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'grep as argument to another command is not flagged' '\n+\ttest_must_fail grep pattern file\n+'\ndiff --git a/t/greplint/grep-as-value.expect b/t/greplint/grep-as-value.expect\nnew file mode 100644\nindex 0000000000..e69de29bb2\ndiff --git a/t/greplint/grep-as-value.test b/t/greplint/grep-as-value.test\nnew file mode 100644\nindex 0000000000..92926f6378\n--- /dev/null\n+++ b/t/greplint/grep-as-value.test\n@@ -0,0 +1,6 @@\n+test_expect_success 'grep as value in for-loop is not flagged' '\n+\tfor cmd in grep sed awk\n+\tdo\n+\t\techo $cmd\n+\tdone\n+'\ndiff --git a/t/greplint/wrong-negation.expect b/t/greplint/wrong-negation.expect\nnew file mode 100644\nindex 0000000000..7dc52f65a8\n--- /dev/null\n+++ b/t/greplint/wrong-negation.expect\n@@ -0,0 +1 @@\n+2: error: use \"test_grep !\" instead of \"! test_grep\"\ndiff --git a/t/greplint/wrong-negation.test b/t/greplint/wrong-negation.test\nnew file mode 100644\nindex 0000000000..542fbd9b28\n--- /dev/null\n+++ b/t/greplint/wrong-negation.test\n@@ -0,0 +1,3 @@\n+test_expect_success 'wrong negation of test_grep is flagged' '\n+\t! test_grep pattern file\n+'\n-- \ngitgitgadget\n"},{"id":"547183","messageId":"53fedba4d4f1108393f4f5a36c2958117cf4265d.1783314119.git.gitgitgadget@gmail.com","threadId":"65748","inReplyTo":"pull.2135.v4.git.1783314119.gitgitgadget@gmail.com","subject":"[PATCH v4 5/6] t: convert grep assertions to test_grep","fromName":"Michael Montalbo via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-07-06T05:01:57Z","receivedAt":"2026-07-06T05:02:14Z","isPatch":true,"body":"From: Michael Montalbo <mmontalbo@gmail.com>\n\nReplace bare grep with test_grep in test assertions across the\nsuite, including sourced test helpers (lib-*.sh, *-tests.sh).\ntest_grep prints the contents of the file being searched on\nfailure, making debugging easier than a bare grep which fails\nsilently.\n\nOnly assertion-style greps are converted: grep used as a filter\nin pipelines, command substitutions, conditionals, or with\nredirected I/O is left as-is with a \"# lint-ok\" annotation.\nExisting '! test_grep' calls are rewritten to 'test_grep !' so\nthat the diagnostic output is preserved on failure.\n\ntest_grep requires the file it reads to exist, so '! grep'\nassertions that inspect a file whose presence is conditional need\ncare.  In t5537 the '.git/shallow' file is still present after the\nrepack (the client remains shallow), so the assertion is\nconverted like any other.  In t1400 the '.git/packed-refs' file\nexists only with the files backend, so its check is guarded with a\nREFFILES prerequisite; the backend-agnostic 'git show-ref' check\nthat follows still runs under every backend.  In t7450 'git~2' is\nthe NTFS 8.3 short name of a '..git' file and only exists\nwhen 8.3 short-name generation is enabled, so its check is guarded\nwith a 'test -f' on the path and uses test_grep inside the guard,\nthe same shape as t1400 (a plain test_grep would BUG when the\nshort name is absent).\n\nThe conversion was generated using a grep-assertion linter\n(greplint.pl, added in the following commit) to identify bare\ngrep calls at command position.  To reproduce, from the t/\ndirectory:\n\n    # Step 1: annotate the two data-filter greps (grep produces\n    # data, not a verdict) so the linter skips them.\n    sed -i '/grep -vf before commits\\.raw/s/$/ # lint-ok: data filter/' \\\n        t5326-multi-pack-bitmaps.sh\n    sed -i '/grep -E \"^\\[0-9a-f\\].*|| :/s/$/ # lint-ok: data filter/' \\\n        t5702-protocol-v2.sh\n\n    # Step 1b: two '! grep' assertions need more than a mechanical\n    # conversion; handle them by hand before the linter-driven steps\n    # below so it leaves them alone.\n    #\n    # t1400: '.git/packed-refs' is absent under reftable, so guard the\n    # check with REFFILES (a plain test_grep would BUG on the missing\n    # file):\n    #\n    #      git update-ref -d HEAD $B &&\n    #  -   ! grep \"$m\" .git/packed-refs &&\n    #  +   if test_have_prereq REFFILES\n    #  +   then\n    #  +           test_grep ! \"$m\" .git/packed-refs\n    #  +   fi &&\n    #      test_must_fail git show-ref --verify -q $m\n    #\n    # t7450: git~2 is an NTFS 8.3 short name that exists only when\n    # short-name generation is enabled, so guard the check on its\n    # presence with 'test -f' and note in a comment why the path can\n    # be absent (a plain test_grep would BUG when it is):\n    #\n    #  -   ! grep gitdir squatting-clone/d/a/git~2\n    #  +   if test -f squatting-clone/d/a/git~2\n    #  +   then\n    #  +           test_grep ! gitdir squatting-clone/d/a/git~2\n    #  +   fi\n\n    # Step 2: reorder pre-existing '! test_grep' to 'test_grep !'\n    # (must come before steps 3-4 so greplint does not see them)\n    sed -i 's/! test_grep/test_grep !/' t0031-lockfile-pid.sh\n    sed -i 's/! test_grep/test_grep !/' t5300-pack-object.sh\n    sed -i 's/! test_grep/test_grep !/' t5319-multi-pack-index.sh\n\n    # Step 3: convert '! grep' -> 'test_grep !'\n    perl greplint.pl *.sh 2>&1 | cut -d: -f1,2 |\n    while IFS=: read f l; do\n        sed -i \"${l}s/! *grep/test_grep !/\" \"$f\"\n    done\n\n    # Step 4: convert remaining 'grep' -> 'test_grep'\n    perl greplint.pl *.sh 2>&1 | cut -d: -f1,2 |\n    while IFS=: read f l; do\n        sed -i \"${l}s/grep/test_grep/\" \"$f\"\n    done\n\nTo verify, run: make -C t test-greplint\n\nSigned-off-by: Michael Montalbo <mmontalbo@gmail.com>\n---\n t/for-each-ref-tests.sh                     |  12 +-\n t/lib-bitmap.sh                             |  12 +-\n t/lib-bundle-uri-protocol.sh                |  26 +-\n t/lib-httpd.sh                              |   2 +-\n t/pack-refs-tests.sh                        |   2 +-\n t/show-ref-exists-tests.sh                  |   2 +-\n t/t0000-basic.sh                            |  16 +-\n t/t0001-init.sh                             |  18 +-\n t/t0008-ignores.sh                          |   8 +-\n t/t0009-git-dir-validation.sh               |   6 +-\n t/t0012-help.sh                             |   4 +-\n t/t0013-sha1dc.sh                           |   2 +-\n t/t0017-env-helper.sh                       |   4 +-\n t/t0021-conversion.sh                       |  18 +-\n t/t0029-core-unsetenvvars.sh                |   4 +-\n t/t0030-stripspace.sh                       |   4 +-\n t/t0031-lockfile-pid.sh                     |   2 +-\n t/t0040-parse-options.sh                    |  52 +--\n t/t0041-usage.sh                            |   2 +-\n t/t0052-simple-ipc.sh                       |  10 +-\n t/t0061-run-command.sh                      |   2 +-\n t/t0066-dir-iterator.sh                     |   2 +-\n t/t0068-for-each-repo.sh                    |  16 +-\n t/t0070-fundamental.sh                      |   6 +-\n t/t0081-find-pack.sh                        |  12 +-\n t/t0091-bugreport.sh                        |  18 +-\n t/t0092-diagnose.sh                         |  12 +-\n t/t0100-previous.sh                         |   2 +-\n t/t0200-gettext-basic.sh                    |  14 +-\n t/t0203-gettext-setlocale-sanity.sh         |   4 +-\n t/t0204-gettext-reencode-sanity.sh          |   8 +-\n t/t0210-trace2-normal.sh                    |   6 +-\n t/t0211-trace2-perf.sh                      |  80 ++---\n t/t0212-trace2-event.sh                     |   8 +-\n t/t0300-credentials.sh                      |   4 +-\n t/t0410-partial-clone.sh                    |  82 ++---\n t/t0450-txt-doc-vs-help.sh                  |   2 +-\n t/t0500-progress-display.sh                 |  18 +-\n t/t0610-reftable-basics.sh                  |   8 +-\n t/t1004-read-tree-m-u-wf.sh                 |   8 +-\n t/t1006-cat-file.sh                         |  18 +-\n t/t1007-hash-object.sh                      |   8 +-\n t/t1011-read-tree-sparse-checkout.sh        |  10 +-\n t/t1050-large.sh                            |   6 +-\n t/t1091-sparse-checkout-builtin.sh          |  24 +-\n t/t1092-sparse-checkout-compatibility.sh    |  44 +--\n t/t1300-config.sh                           |  16 +-\n t/t1305-config-include.sh                   |   2 +-\n t/t1308-config-set.sh                       |   6 +-\n t/t1400-update-ref.sh                       | 173 +++++-----\n t/t1403-show-ref.sh                         |  18 +-\n t/t1410-reflog.sh                           |   4 +-\n t/t1415-worktree-refs.sh                    |   4 +-\n t/t1430-bad-ref-name.sh                     |  56 ++--\n t/t1450-fsck.sh                             |  12 +-\n t/t1451-fsck-buffer.sh                      |   6 +-\n t/t1460-refs-migrate.sh                     |   2 +-\n t/t1500-rev-parse.sh                        |   6 +-\n t/t1502-rev-parse-parseopt.sh               |   2 +-\n t/t1503-rev-parse-verify.sh                 |  10 +-\n t/t1510-repo-setup.sh                       |  10 +-\n t/t1512-rev-parse-disambiguation.sh         |   4 +-\n t/t1515-rev-parse-outside-repo.sh           |   2 +-\n t/t1800-hook.sh                             |  18 +-\n t/t2004-checkout-cache-temp.sh              |   4 +-\n t/t2019-checkout-ambiguous-ref.sh           |   4 +-\n t/t2024-checkout-dwim.sh                    |   8 +-\n t/t2030-unresolve-info.sh                   |   6 +-\n t/t2060-switch.sh                           |   6 +-\n t/t2070-restore.sh                          |   2 +-\n t/t2080-parallel-checkout-basics.sh         |  14 +-\n t/t2081-parallel-checkout-collisions.sh     |  24 +-\n t/t2082-parallel-checkout-attributes.sh     |  12 +-\n t/t2103-update-index-ignore-missing.sh      |   6 +-\n t/t2200-add-update.sh                       |   2 +-\n t/t2203-add-intent.sh                       |   6 +-\n t/t2400-worktree-add.sh                     |  24 +-\n t/t2402-worktree-list.sh                    |  16 +-\n t/t2403-worktree-move.sh                    |   6 +-\n t/t2405-worktree-submodule.sh               |   6 +-\n t/t2407-worktree-heads.sh                   |  26 +-\n t/t2500-untracked-overwriting.sh            |   8 +-\n t/t2501-cwd-empty.sh                        |   4 +-\n t/t3001-ls-files-others-exclude.sh          |   6 +-\n t/t3007-ls-files-recurse-submodules.sh      |   6 +-\n t/t3200-branch.sh                           |  12 +-\n t/t3202-show-branch.sh                      |  10 +-\n t/t3203-branch-output.sh                    |   4 +-\n t/t3206-range-diff.sh                       |  78 ++---\n t/t3207-branch-submodule.sh                 |   4 +-\n t/t3301-notes.sh                            |  32 +-\n t/t3310-notes-merge-manual-resolve.sh       |  16 +-\n t/t3320-notes-merge-worktrees.sh            |   2 +-\n t/t3400-rebase.sh                           |  16 +-\n t/t3402-rebase-merge.sh                     |  16 +-\n t/t3404-rebase-interactive.sh               |  72 ++--\n t/t3406-rebase-message.sh                   |   6 +-\n t/t3415-rebase-autosquash.sh                |  10 +-\n t/t3416-rebase-onto-threedots.sh            |   4 +-\n t/t3418-rebase-continue.sh                  |  10 +-\n t/t3420-rebase-autostash.sh                 |  24 +-\n t/t3422-rebase-incompatible-options.sh      |   4 +-\n t/t3429-rebase-edit-todo.sh                 |   2 +-\n t/t3430-rebase-merges.sh                    |  32 +-\n t/t3500-cherry.sh                           |   4 +-\n t/t3501-revert-cherry-pick.sh               |   6 +-\n t/t3504-cherry-pick-rerere.sh               |   6 +-\n t/t3510-cherry-pick-sequence.sh             |  24 +-\n t/t3602-rm-sparse-checkout.sh               |   4 +-\n t/t3705-add-sparse-checkout.sh              |  10 +-\n t/t3800-mktag.sh                            |   4 +-\n t/t3901-i18n-patch.sh                       |  16 +-\n t/t3903-stash.sh                            |  28 +-\n t/t3904-stash-patch.sh                      |   4 +-\n t/t3908-stash-in-worktree.sh                |   2 +-\n t/t4000-diff-format.sh                      |   2 +-\n t/t4001-diff-rename.sh                      |   4 +-\n t/t4011-diff-symlink.sh                     |   2 +-\n t/t4013-diff-various.sh                     |   2 +-\n t/t4014-format-patch.sh                     | 344 ++++++++++----------\n t/t4015-diff-whitespace.sh                  |  16 +-\n t/t4017-diff-retval.sh                      |   2 +-\n t/t4018-diff-funcname.sh                    |   2 +-\n t/t4019-diff-wserror.sh                     |   8 +-\n t/t4020-diff-external.sh                    |  18 +-\n t/t4021-format-patch-numbered.sh            |   4 +-\n t/t4022-diff-rewrite.sh                     |  14 +-\n t/t4028-format-patch-mime-headers.sh        |   6 +-\n t/t4031-diff-rewrite-binary.sh              |  18 +-\n t/t4033-diff-patience.sh                    |   2 +-\n t/t4036-format-patch-signer-mime.sh         |   6 +-\n t/t4038-diff-combined.sh                    |   6 +-\n t/t4051-diff-function-context.sh            |  38 +--\n t/t4053-diff-no-index.sh                    |   4 +-\n t/t4063-diff-blobs.sh                       |   2 +-\n t/t4065-diff-anchored.sh                    |  26 +-\n t/t4067-diff-partial-clone.sh               |  12 +-\n t/t4073-diff-stat-name-width.sh             |  24 +-\n t/t4103-apply-binary.sh                     |   2 +-\n t/t4120-apply-popt.sh                       |   2 +-\n t/t4124-apply-ws-rule.sh                    |  10 +-\n t/t4128-apply-root.sh                       |   2 +-\n t/t4140-apply-ita.sh                        |   4 +-\n t/t4141-apply-too-large.sh                  |   2 +-\n t/t4150-am.sh                               |  48 +--\n t/t4200-rerere.sh                           |   6 +-\n t/t4201-shortlog.sh                         |   2 +-\n t/t4202-log.sh                              |  84 ++---\n t/t4204-patch-id.sh                         |   2 +-\n t/t4205-log-pretty-formats.sh               |   2 +-\n t/t4209-log-pickaxe.sh                      |  10 +-\n t/t4211-line-log.sh                         |  72 ++--\n t/t4216-log-bloom.sh                        |  18 +-\n t/t4252-am-options.sh                       |  22 +-\n t/t4254-am-corrupt.sh                       |   6 +-\n t/t4258-am-quoted-cr.sh                     |   2 +-\n t/t4301-merge-tree-write-tree.sh            |  18 +-\n t/t5000-tar-tree.sh                         |  10 +-\n t/t5004-archive-corner-cases.sh             |   2 +-\n t/t5100-mailinfo.sh                         |   2 +-\n t/t5150-request-pull.sh                     |  18 +-\n t/t5300-pack-object.sh                      |  22 +-\n t/t5302-pack-index.sh                       |   6 +-\n t/t5304-prune.sh                            |   8 +-\n t/t5310-pack-bitmaps.sh                     |  14 +-\n t/t5317-pack-objects-filter-objects.sh      |  12 +-\n t/t5318-commit-graph.sh                     |   8 +-\n t/t5319-multi-pack-index.sh                 |  16 +-\n t/t5324-split-commit-graph.sh               |  10 +-\n t/t5325-reverse-index.sh                    |   2 +-\n t/t5326-multi-pack-bitmaps.sh               |  28 +-\n t/t5328-commit-graph-64bit-time.sh          |   2 +-\n t/t5329-pack-objects-cruft.sh               |   8 +-\n t/t5334-incremental-multi-pack-index.sh     |   2 +-\n t/t5335-compact-multi-pack-index.sh         |   4 +-\n t/t5351-unpack-large-objects.sh             |   2 +-\n t/t5402-post-merge-hook.sh                  |   4 +-\n t/t5403-post-checkout-hook.sh               |   2 +-\n t/t5404-tracking-branches.sh                |   2 +-\n t/t5406-remote-rejects.sh                   |   2 +-\n t/t5407-post-rewrite-hook.sh                |   8 +-\n t/t5409-colorize-remote-messages.sh         |  36 +-\n t/t5500-fetch-pack.sh                       |  38 +--\n t/t5504-fetch-receive-strict.sh             |  14 +-\n t/t5505-remote.sh                           |  20 +-\n t/t5510-fetch.sh                            |  10 +-\n t/t5512-ls-remote.sh                        |   8 +-\n t/t5514-fetch-multiple.sh                   |   2 +-\n t/t5516-fetch-push.sh                       |  20 +-\n t/t5520-pull.sh                             |   4 +-\n t/t5524-pull-msg.sh                         |   6 +-\n t/t5526-fetch-submodules.sh                 |  16 +-\n t/t5529-push-errors.sh                      |   4 +-\n t/t5530-upload-pack-error.sh                |  18 +-\n t/t5531-deep-submodule-push.sh              |   2 +-\n t/t5532-fetch-proxy.sh                      |   2 +-\n t/t5533-push-cas.sh                         |  12 +-\n t/t5534-push-signed.sh                      |  22 +-\n t/t5537-fetch-shallow.sh                    |   2 +-\n t/t5538-push-shallow.sh                     |   2 +-\n t/t5539-fetch-http-shallow.sh               |   4 +-\n t/t5541-http-push-smart.sh                  |  32 +-\n t/t5544-pack-objects-hook.sh                |  12 +-\n t/t5550-http-fetch-dumb.sh                  |   4 +-\n t/t5551-http-fetch-smart.sh                 |  46 +--\n t/t5552-skipping-fetch-negotiator.sh        |   6 +-\n t/t5554-noop-fetch-negotiator.sh            |   4 +-\n t/t5557-http-get.sh                         |   2 +-\n t/t5558-clone-bundle-uri.sh                 |  38 +--\n t/t5562-http-backend-content-length.sh      |   2 +-\n t/t5564-http-proxy.sh                       |  10 +-\n t/t5581-http-curl-verbose.sh                |   2 +-\n t/t5583-push-branches.sh                    |   8 +-\n t/t5601-clone.sh                            |  28 +-\n t/t5604-clone-reference.sh                  |   8 +-\n t/t5605-clone-local.sh                      |   2 +-\n t/t5606-clone-options.sh                    |   6 +-\n t/t5612-clone-refspec.sh                    |   2 +-\n t/t5616-partial-clone.sh                    |  60 ++--\n t/t5619-clone-local-ambiguous-transport.sh  |   2 +-\n t/t5620-backfill.sh                         |  12 +-\n t/t5700-protocol-v1.sh                      |  46 +--\n t/t5701-git-serve.sh                        |  14 +-\n t/t5702-protocol-v2.sh                      | 154 ++++-----\n t/t5703-upload-pack-ref-in-want.sh          |  22 +-\n t/t5705-session-id-in-capabilities.sh       |  12 +-\n t/t5750-bundle-uri-parse.sh                 |   8 +-\n t/t5801-remote-helpers.sh                   |   4 +-\n t/t5810-proto-disable-local.sh              |   2 +-\n t/t5813-proto-disable-ssh.sh                |   4 +-\n t/t6000-rev-list-misc.sh                    |  26 +-\n t/t6005-rev-list-count.sh                   |   8 +-\n t/t6006-rev-list-format.sh                  |   4 +-\n t/t6009-rev-list-parent.sh                  |   4 +-\n t/t6020-bundle-misc.sh                      |  12 +-\n t/t6022-rev-list-missing.sh                 |   4 +-\n t/t6030-bisect-porcelain.sh                 | 150 ++++-----\n t/t6040-tracking-info.sh                    |   2 +-\n t/t6112-rev-list-filters-objects.sh         |  24 +-\n t/t6115-rev-list-du.sh                      |   4 +-\n t/t6120-describe.sh                         |  14 +-\n t/t6200-fmt-merge-msg.sh                    |  82 ++---\n t/t6402-merge-rename.sh                     |   4 +-\n t/t6403-merge-file.sh                       |   6 +-\n t/t6404-recursive-merge.sh                  |   2 +-\n t/t6406-merge-attr.sh                       |  20 +-\n t/t6417-merge-ours-theirs.sh                |  30 +-\n t/t6418-merge-text-auto.sh                  |   2 +-\n t/t6422-merge-rename-corner-cases.sh        |   8 +-\n t/t6423-merge-rename-directories.sh         |  72 ++--\n t/t6424-merge-unrelated-index-changes.sh    |   6 +-\n t/t6427-diff3-conflict-markers.sh           |  10 +-\n t/t6432-merge-recursive-space-options.sh    |   4 +-\n t/t6436-merge-overwrite.sh                  |   6 +-\n t/t6437-submodule-merge.sh                  |  10 +-\n t/t6500-gc.sh                               |   8 +-\n t/t6600-test-reach.sh                       |   4 +-\n t/t7001-mv.sh                               |  16 +-\n t/t7002-mv-sparse-checkout.sh               |  38 +--\n t/t7003-filter-branch.sh                    |  16 +-\n t/t7004-tag.sh                              |   2 +-\n t/t7006-pager.sh                            |  16 +-\n t/t7012-skip-worktree-writing.sh            |   6 +-\n t/t7030-verify-tag.sh                       |  52 +--\n t/t7031-verify-tag-signed-ssh.sh            |  46 +--\n t/t7102-reset.sh                            |   2 +-\n t/t7110-reset-merge.sh                      |  40 +--\n t/t7201-co.sh                               |   6 +-\n t/t7300-clean.sh                            |   2 +-\n t/t7301-clean-interactive.sh                |   2 +-\n t/t7400-submodule-basic.sh                  |  32 +-\n t/t7402-submodule-rebase.sh                 |   2 +-\n t/t7406-submodule-update.sh                 |  26 +-\n t/t7416-submodule-dash-url.sh               |  20 +-\n t/t7417-submodule-path-url.sh               |   2 +-\n t/t7450-bad-git-dotfiles.sh                 |  20 +-\n t/t7501-commit-basic-functionality.sh       |  16 +-\n t/t7502-commit-porcelain.sh                 |   2 +-\n t/t7507-commit-verbose.sh                   |   6 +-\n t/t7508-status.sh                           |   6 +-\n t/t7510-signed-commit.sh                    |  68 ++--\n t/t7516-commit-races.sh                     |   4 +-\n t/t7519-status-fsmonitor.sh                 |  14 +-\n t/t7527-builtin-fsmonitor.sh                |  82 ++---\n t/t7528-signed-commit-ssh.sh                |  68 ++--\n t/t7600-merge.sh                            |  10 +-\n t/t7603-merge-reduce-heads.sh               |  20 +-\n t/t7606-merge-custom.sh                     |   2 +-\n t/t7607-merge-state.sh                      |   4 +-\n t/t7610-mergetool.sh                        |  18 +-\n t/t7700-repack.sh                           |  14 +-\n t/t7703-repack-geometric.sh                 |   4 +-\n t/t7704-repack-cruft.sh                     |  12 +-\n t/t7800-difftool.sh                         |  26 +-\n t/t7810-grep.sh                             |  22 +-\n t/t7814-grep-recurse-submodules.sh          |   2 +-\n t/t7900-maintenance.sh                      |  34 +-\n t/t8008-blame-formats.sh                    |   2 +-\n t/t8010-cat-file-filters.sh                 |   2 +-\n t/t8012-blame-colors.sh                     |   2 +-\n t/t9001-send-email.sh                       | 190 +++++------\n t/t9003-help-autocorrect.sh                 |   6 +-\n t/t9106-git-svn-commit-diff-clobber.sh      |   2 +-\n t/t9107-git-svn-migrate.sh                  |  30 +-\n t/t9110-git-svn-use-svm-props.sh            |  20 +-\n t/t9111-git-svn-use-svnsync-props.sh        |  18 +-\n t/t9114-git-svn-dcommit-merge.sh            |   6 +-\n t/t9116-git-svn-log.sh                      |   8 +-\n t/t9117-git-svn-init-clone.sh               |  12 +-\n t/t9119-git-svn-info.sh                     |  16 +-\n t/t9122-git-svn-author.sh                   |   8 +-\n t/t9130-git-svn-authors-file.sh             |   8 +-\n t/t9138-git-svn-authors-prog.sh             |  14 +-\n t/t9140-git-svn-reset.sh                    |   4 +-\n t/t9153-git-svn-rewrite-uuid.sh             |   4 +-\n t/t9200-git-cvsexportcommit.sh              |   2 +-\n t/t9210-scalar.sh                           |  34 +-\n t/t9211-scalar-clone.sh                     |  16 +-\n t/t9300-fast-import.sh                      |  10 +-\n t/t9350-fast-export.sh                      |  54 +--\n t/t9351-fast-export-anonymize.sh            |  36 +-\n t/t9400-git-cvsserver-server.sh             |   4 +-\n t/t9501-gitweb-standalone-http-status.sh    |  58 ++--\n t/t9502-gitweb-standalone-parse-output.sh   |  38 +--\n t/t9800-git-p4-basic.sh                     |  10 +-\n t/t9801-git-p4-branch.sh                    |  48 +--\n t/t9806-git-p4-options.sh                   |  10 +-\n t/t9807-git-p4-submit.sh                    |   2 +-\n t/t9810-git-p4-rcs.sh                       |   8 +-\n t/t9813-git-p4-preserve-users.sh            |   8 +-\n t/t9814-git-p4-rename.sh                    |   8 +-\n t/t9827-git-p4-change-filetype.sh           |   4 +-\n t/t9832-unshelve.sh                         |   6 +-\n t/t9833-errors.sh                           |   4 +-\n t/t9835-git-p4-metadata-encoding-python2.sh |  36 +-\n t/t9836-git-p4-metadata-encoding-python3.sh |  38 +--\n t/t9850-shell.sh                            |   2 +-\n t/t9902-completion.sh                       |  26 +-\n 338 files changed, 2815 insertions(+), 2806 deletions(-)\n\ndiff --git a/t/for-each-ref-tests.sh b/t/for-each-ref-tests.sh\nindex bd2d45c971..6b359d940e 100644\n--- a/t/for-each-ref-tests.sh\n+++ b/t/for-each-ref-tests.sh\n@@ -523,7 +523,7 @@ test_expect_success 'Verify descending sort' '\n \n test_expect_success 'Give help even with invalid sort atoms' '\n \ttest_expect_code 129 ${git_for_each_ref} --sort=bogus -h >actual 2>&1 &&\n-\tgrep \"^usage: ${git_for_each_ref}\" actual\n+\ttest_grep \"^usage: ${git_for_each_ref}\" actual\n '\n \n cat >expected <<\\EOF\n@@ -622,7 +622,7 @@ test_expect_success 'Quoting style: tcl' '\n for i in \"--perl --shell\" \"-s --python\" \"--python --tcl\" \"--tcl --perl\"; do\n \ttest_expect_success \"more than one quoting style: $i\" \"\n \t\ttest_must_fail ${git_for_each_ref} $i 2>err &&\n-\t\tgrep '^error: more than one quoting style' err\n+\t\ttest_grep '^error: more than one quoting style' err\n \t\"\n done\n \n@@ -1892,7 +1892,7 @@ test_expect_success \"${git_for_each_ref} --stdin: fails if extra args\" '\n \t>in &&\n \ttest_must_fail ${git_for_each_ref} --format=\"%(refname)\" \\\n \t\t--stdin refs/heads/extra <in 2>err &&\n-\tgrep \"unknown arguments supplied with --stdin\" err\n+\ttest_grep \"unknown arguments supplied with --stdin\" err\n '\n \n test_expect_success \"${git_for_each_ref} --stdin: matches\" '\n@@ -1955,11 +1955,11 @@ test_expect_success \"${git_for_each_ref} with nested tags\" '\n \n test_expect_success 'is-base atom with non-commits' '\n \t${git_for_each_ref} --format=\"%(is-base:HEAD) %(refname)\" >out 2>err &&\n-\tgrep \"(HEAD) refs/heads/main\" out &&\n+\ttest_grep \"(HEAD) refs/heads/main\" out &&\n \n \ttest_line_count = 2 err &&\n-\tgrep \"error: object .* is a commit, not a blob\" err &&\n-\tgrep \"error: bad tag pointer to\" err\n+\ttest_grep \"error: object .* is a commit, not a blob\" err &&\n+\ttest_grep \"error: bad tag pointer to\" err\n '\n \n GRADE_FORMAT=\"%(signature:grade)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)\"\ndiff --git a/t/lib-bitmap.sh b/t/lib-bitmap.sh\nindex 62aa6744a6..2de6646128 100644\n--- a/t/lib-bitmap.sh\n+++ b/t/lib-bitmap.sh\n@@ -173,7 +173,7 @@ rev_list_tests_head () {\n \n \ttest_expect_success \"bitmap --objects handles non-commit objects ($state, $branch)\" '\n \t\tgit rev-list --objects --use-bitmap-index $branch tagged-blob >actual &&\n-\t\tgrep $blob actual\n+\t\ttest_grep $blob actual\n \t'\n }\n \n@@ -242,16 +242,16 @@ basic_bitmap_tests () {\n \t\tGIT_PROGRESS_DELAY=0 \\\n \t\t\tgit pack-objects --all --stdout --progress \\\n \t\t\t</dev/null >/dev/null 2>stderr &&\n-\t\tgrep \"Enumerating objects: $count, done\" stderr &&\n-\t\tgrep \"pack-reused $count\" stderr &&\n+\t\ttest_grep \"Enumerating objects: $count, done\" stderr &&\n+\t\ttest_grep \"pack-reused $count\" stderr &&\n \n \t\t# now the same but with one non-reused object\n \t\tgit commit --allow-empty -m \"an extra commit object\" &&\n \t\tGIT_PROGRESS_DELAY=0 \\\n \t\t\tgit pack-objects --all --stdout --progress \\\n \t\t\t</dev/null >/dev/null 2>stderr &&\n-\t\tgrep \"Enumerating objects: $((count+1)), done\" stderr &&\n-\t\tgrep \"pack-reused $count\" stderr\n+\t\ttest_grep \"Enumerating objects: $((count+1)), done\" stderr &&\n+\t\ttest_grep \"pack-reused $count\" stderr\n \t'\n }\n \n@@ -283,7 +283,7 @@ test_rev_exists () {\n \t\tthen\n \t\t\ttest_path_is_file $midx-$(midx_checksum $objdir).rev\n \t\tfi &&\n-\t\tgrep \"\\\"category\\\":\\\"load_midx_revindex\\\",\\\"key\\\":\\\"source\\\",\\\"value\\\":\\\"$kind\\\"\" event.trace\n+\t\ttest_grep \"\\\"category\\\":\\\"load_midx_revindex\\\",\\\"key\\\":\\\"source\\\",\\\"value\\\":\\\"$kind\\\"\" event.trace\n \t'\n }\n \ndiff --git a/t/lib-bundle-uri-protocol.sh b/t/lib-bundle-uri-protocol.sh\nindex de09b6b02e..ecf3284297 100644\n--- a/t/lib-bundle-uri-protocol.sh\n+++ b/t/lib-bundle-uri-protocol.sh\n@@ -63,9 +63,9 @@ test_expect_success \"connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: no\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log &&\n+\ttest_grep \"< version 2\" log &&\n \n-\t! grep bundle-uri log\n+\ttest_grep ! bundle-uri log\n '\n \n test_expect_success \"connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: have bundle-uri\" '\n@@ -78,10 +78,10 @@ test_expect_success \"connect with $BUNDLE_URI_PROTOCOL:// using protocol v2: hav\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log &&\n+\ttest_grep \"< version 2\" log &&\n \n \t# Server advertised bundle-uri capability\n-\tgrep \"< bundle-uri\" log\n+\ttest_grep \"< bundle-uri\" log\n '\n \n test_expect_success \"clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: request bundle-uris\" '\n@@ -95,13 +95,13 @@ test_expect_success \"clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log &&\n+\ttest_grep \"< version 2\" log &&\n \n \t# Server advertised bundle-uri capability\n-\tgrep \"< bundle-uri\" log &&\n+\ttest_grep \"< bundle-uri\" log &&\n \n \t# Client did not issue bundle-uri command\n-\t! grep \"> command=bundle-uri\" log &&\n+\ttest_grep ! \"> command=bundle-uri\" log &&\n \n \tGIT_TRACE_PACKET=\"$PWD/log\" \\\n \tgit \\\n@@ -111,13 +111,13 @@ test_expect_success \"clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log &&\n+\ttest_grep \"< version 2\" log &&\n \n \t# Server advertised bundle-uri capability\n-\tgrep \"< bundle-uri\" log &&\n+\ttest_grep \"< bundle-uri\" log &&\n \n \t# Client issued bundle-uri command\n-\tgrep \"> command=bundle-uri\" log &&\n+\ttest_grep \"> command=bundle-uri\" log &&\n \n \tGIT_TRACE_PACKET=\"$PWD/log3\" \\\n \tgit \\\n@@ -128,13 +128,13 @@ test_expect_success \"clone with $BUNDLE_URI_PROTOCOL:// using protocol v2: reque\n \t\t>actual 2>err &&\n \n \t# Server responded using protocol v2\n-\tgrep \"< version 2\" log3 &&\n+\ttest_grep \"< version 2\" log3 &&\n \n \t# Server advertised bundle-uri capability\n-\tgrep \"< bundle-uri\" log3 &&\n+\ttest_grep \"< bundle-uri\" log3 &&\n \n \t# Client did not issue bundle-uri command (--bundle-uri override)\n-\t! grep \"> command=bundle-uri\" log3\n+\ttest_grep ! \"> command=bundle-uri\" log3\n '\n \n # The remaining tests will all assume transfer.bundleURI=true\ndiff --git a/t/lib-httpd.sh b/t/lib-httpd.sh\nindex fc646447d5..a216e5376f 100644\n--- a/t/lib-httpd.sh\n+++ b/t/lib-httpd.sh\n@@ -275,7 +275,7 @@ test_http_push_nonff () {\n \t'\n \n \ttest_expect_success 'non-fast-forward push show ref status' '\n-\t\tgrep \"^ ! \\[rejected\\][ ]*$BRANCH -> $BRANCH (non-fast-forward)$\" output\n+\t\ttest_grep \"^ ! \\[rejected\\][ ]*$BRANCH -> $BRANCH (non-fast-forward)$\" output\n \t'\n \n \ttest_expect_success 'non-fast-forward push shows help message' '\ndiff --git a/t/pack-refs-tests.sh b/t/pack-refs-tests.sh\nindex d76b087b09..a800177446 100644\n--- a/t/pack-refs-tests.sh\n+++ b/t/pack-refs-tests.sh\n@@ -195,7 +195,7 @@ test_expect_success 'delete ref while another dangling packed ref' '\n test_expect_success 'pack ref directly below refs/' '\n \tgit update-ref refs/top HEAD &&\n \tgit ${pack_refs} --all --prune &&\n-\tgrep refs/top .git/packed-refs &&\n+\ttest_grep refs/top .git/packed-refs &&\n \ttest_path_is_missing .git/refs/top\n '\n \ndiff --git a/t/show-ref-exists-tests.sh b/t/show-ref-exists-tests.sh\nindex 36e8e9df33..1351f690eb 100644\n--- a/t/show-ref-exists-tests.sh\n+++ b/t/show-ref-exists-tests.sh\n@@ -19,7 +19,7 @@ test_expect_success '--exists with missing reference' '\n \n test_expect_success '--exists does not use DWIM' '\n \ttest_expect_code 2 ${git_show_ref_exists} $GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME 2>err &&\n-\tgrep \"reference does not exist\" err\n+\ttest_grep \"reference does not exist\" err\n '\n \n test_expect_success '--exists with HEAD' '\ndiff --git a/t/t0000-basic.sh b/t/t0000-basic.sh\nindex 2b63e1c86c..7bd1f4e7b7 100755\n--- a/t/t0000-basic.sh\n+++ b/t/t0000-basic.sh\n@@ -743,7 +743,7 @@ test_expect_success 'subtest: lazy prereqs do not turn off tracing' '\n \ttest_done\n \tEOF\n \n-\tgrep \"echo trace\" lazy-prereq-and-tracing/err\n+\ttest_grep \"echo trace\" lazy-prereq-and-tracing/err\n '\n \n test_expect_success 'subtest: tests clean up after themselves' '\n@@ -815,7 +815,7 @@ test_expect_success 'subtest: test_atexit is run' '\n \n test_expect_success 'test_oid provides sane info by default' '\n \ttest_oid zero >actual &&\n-\tgrep \"^00*\\$\" actual &&\n+\ttest_grep \"^00*\\$\" actual &&\n \trawsz=\"$(test_oid rawsz)\" &&\n \thexsz=\"$(test_oid hexsz)\" &&\n \t# +1 accounts for the trailing newline\n@@ -827,7 +827,7 @@ test_expect_success 'test_oid can look up data for SHA-1' '\n \ttest_when_finished \"test_detect_hash\" &&\n \ttest_set_hash sha1 &&\n \ttest_oid zero >actual &&\n-\tgrep \"^00*\\$\" actual &&\n+\ttest_grep \"^00*\\$\" actual &&\n \trawsz=\"$(test_oid rawsz)\" &&\n \thexsz=\"$(test_oid hexsz)\" &&\n \ttest $(wc -c <actual) -eq 41 &&\n@@ -839,7 +839,7 @@ test_expect_success 'test_oid can look up data for SHA-256' '\n \ttest_when_finished \"test_detect_hash\" &&\n \ttest_set_hash sha256 &&\n \ttest_oid zero >actual &&\n-\tgrep \"^00*\\$\" actual &&\n+\ttest_grep \"^00*\\$\" actual &&\n \trawsz=\"$(test_oid rawsz)\" &&\n \thexsz=\"$(test_oid hexsz)\" &&\n \ttest $(wc -c <actual) -eq 65 &&\n@@ -884,11 +884,11 @@ test_expect_success 'test_bool_env' '\n \t\t# test script, hence the redirection of fd 7, and aborts\n \t\t# with \"exit 1\", hence the subshell.\n \t\t! ( test_bool_env envvar true ) 7>err &&\n-\t\tgrep \"error: test_bool_env requires bool values\" err &&\n+\t\ttest_grep \"error: test_bool_env requires bool values\" err &&\n \n \t\tenvvar=true &&\n \t\t! ( test_bool_env envvar invalid ) 7>err &&\n-\t\tgrep \"error: test_bool_env requires bool values\" err\n+\t\ttest_grep \"error: test_bool_env requires bool values\" err\n \t)\n '\n \n@@ -1242,12 +1242,12 @@ test_expect_success 'test_must_fail on a failing git command with env' '\n \n test_expect_success 'test_must_fail rejects a non-git command' '\n \t! test_must_fail grep ^$ notafile 2>err &&\n-\tgrep -F \"test_must_fail: only '\"'\"'git'\"'\"' is allowed\" err\n+\ttest_grep -F \"test_must_fail: only '\"'\"'git'\"'\"' is allowed\" err\n '\n \n test_expect_success 'test_must_fail rejects a non-git command with env' '\n \t! test_must_fail env var1=a var2=b grep ^$ notafile 2>err &&\n-\tgrep -F \"test_must_fail: only '\"'\"'git'\"'\"' is allowed\" err\n+\ttest_grep -F \"test_must_fail: only '\"'\"'git'\"'\"' is allowed\" err\n '\n \n test_done\ndiff --git a/t/t0001-init.sh b/t/t0001-init.sh\nindex e89feca544..5cf2e5a35a 100755\n--- a/t/t0001-init.sh\n+++ b/t/t0001-init.sh\n@@ -278,9 +278,9 @@ test_expect_success POSIXPERM 'init creates a new deep directory (umask vs. shar\n \t\tgit init --bare --shared=0660 newdir/a/b/c &&\n \t\ttest_path_is_dir newdir/a/b/c/refs &&\n \t\tls -ld newdir/a newdir/a/b > lsab.out &&\n-\t\t! grep -v \"^drwxrw[sx]r-x\" lsab.out &&\n+\t\ttest_grep ! -v \"^drwxrw[sx]r-x\" lsab.out &&\n \t\tls -ld newdir/a/b/c > lsc.out &&\n-\t\t! grep -v \"^drwxrw[sx]---\" lsc.out\n+\t\ttest_grep ! -v \"^drwxrw[sx]---\" lsc.out\n \t)\n '\n \n@@ -619,7 +619,7 @@ test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage is not allowed wi\n \tgit init refstorage &&\n \tgit -C refstorage config extensions.refStorage files &&\n \ttest_must_fail git -C refstorage rev-parse 2>err &&\n-\tgrep \"repo version is 0, but v1-only extension found\" err\n+\ttest_grep \"repo version is 0, but v1-only extension found\" err\n '\n \n test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage with files backend' '\n@@ -637,7 +637,7 @@ test_expect_success DEFAULT_REPO_FORMAT 'extensions.refStorage with unknown back\n \tgit -C refstorage config core.repositoryformatversion 1 &&\n \tgit -C refstorage config extensions.refStorage garbage &&\n \ttest_must_fail git -C refstorage rev-parse 2>err &&\n-\tgrep \"invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}garbage${SQ}\" err\n+\ttest_grep \"invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}garbage${SQ}\" err\n '\n \n test_expect_success 'init with GIT_DEFAULT_REF_FORMAT=garbage' '\n@@ -848,8 +848,8 @@ test_expect_success MINGW 'redirect std handles' '\n \t\tGIT_REDIRECT_STDOUT=output.txt \\\n \t\tGIT_REDIRECT_STDERR=\"2>&1\" \\\n \t\tgit rev-parse --git-dir --verify refs/invalid &&\n-\tgrep \"^\\\\.git\\$\" output.txt &&\n-\tgrep \"Needed a single revision\" output.txt\n+\ttest_grep \"^\\\\.git\\$\" output.txt &&\n+\ttest_grep \"Needed a single revision\" output.txt\n '\n \n test_expect_success '--initial-branch' '\n@@ -862,14 +862,14 @@ test_expect_success '--initial-branch' '\n \tgit init --initial-branch=ignore initial-branch-option 2>err &&\n \ttest_grep \"ignored --initial-branch\" err &&\n \tgit -C initial-branch-option symbolic-ref HEAD >actual &&\n-\tgrep hello actual\n+\ttest_grep hello actual\n '\n \n test_expect_success 'overridden default initial branch name (config)' '\n \ttest_config_global init.defaultBranch nmb &&\n \tGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME= git init initial-branch-config &&\n \tgit -C initial-branch-config symbolic-ref HEAD >actual &&\n-\tgrep nmb actual\n+\ttest_grep nmb actual\n '\n \n test_expect_success 'advice on unconfigured init.defaultBranch' '\n@@ -907,7 +907,7 @@ test_expect_success 'overridden default main branch name (env)' '\n \ttest_config_global init.defaultBranch nmb &&\n \tGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=env git init main-branch-env &&\n \tgit -C main-branch-env symbolic-ref HEAD >actual &&\n-\tgrep env actual\n+\ttest_grep env actual\n '\n \n test_expect_success 'invalid default branch name' '\ndiff --git a/t/t0008-ignores.sh b/t/t0008-ignores.sh\nindex d77a179bdd..ed95faf327 100755\n--- a/t/t0008-ignores.sh\n+++ b/t/t0008-ignores.sh\n@@ -790,8 +790,8 @@ test_expect_success 'existing file and directory' '\n \t>one &&\n \tmkdir top-level-dir &&\n \tgit check-ignore one top-level-dir >actual &&\n-\tgrep one actual &&\n-\tgrep top-level-dir actual\n+\ttest_grep one actual &&\n+\ttest_grep top-level-dir actual\n '\n \n test_expect_success 'existing directory and file' '\n@@ -800,8 +800,8 @@ test_expect_success 'existing directory and file' '\n \t>one &&\n \tmkdir top-level-dir &&\n \tgit check-ignore top-level-dir one >actual &&\n-\tgrep one actual &&\n-\tgrep top-level-dir actual\n+\ttest_grep one actual &&\n+\ttest_grep top-level-dir actual\n '\n \n test_expect_success 'exact prefix matching (with root)' '\ndiff --git a/t/t0009-git-dir-validation.sh b/t/t0009-git-dir-validation.sh\nindex 33d21ed9ea..4cba478e50 100755\n--- a/t/t0009-git-dir-validation.sh\n+++ b/t/t0009-git-dir-validation.sh\n@@ -35,7 +35,7 @@ test_expect_success PIPE 'setup: .git as a FIFO (named pipe) is rejected' '\n \t\tcd parent/fifo-trap &&\n \t\tmkfifo .git &&\n \t\ttest_must_fail git rev-parse --git-dir 2>stderr &&\n-\t\tgrep \"not a regular file\" stderr\n+\t\ttest_grep \"not a regular file\" stderr\n \t)\n '\n \n@@ -47,7 +47,7 @@ test_expect_success SYMLINKS,PIPE 'setup: .git as a symlink to a FIFO is rejecte\n \t\tmkfifo target-fifo &&\n \t\tln -s target-fifo .git &&\n \t\ttest_must_fail git rev-parse --git-dir 2>stderr &&\n-\t\tgrep \"not a regular file\" stderr\n+\t\ttest_grep \"not a regular file\" stderr\n \t)\n '\n \n@@ -58,7 +58,7 @@ test_expect_success 'setup: .git with garbage content is rejected' '\n \t\tcd parent/garbage-trap &&\n \t\techo \"garbage\" >.git &&\n \t\ttest_must_fail git rev-parse --git-dir 2>stderr &&\n-\t\tgrep \"invalid gitfile format\" stderr\n+\t\ttest_grep \"invalid gitfile format\" stderr\n \t)\n '\n \ndiff --git a/t/t0012-help.sh b/t/t0012-help.sh\nindex c33501bdcd..48d128ad04 100755\n--- a/t/t0012-help.sh\n+++ b/t/t0012-help.sh\n@@ -131,8 +131,8 @@ test_expect_success 'git help succeeds without git.html' '\n \n test_expect_success 'git help --user-interfaces' '\n \tgit help --user-interfaces >help.output &&\n-\tgrep \"^   attributes   \" help.output &&\n-\tgrep \"^   mailmap   \" help.output\n+\ttest_grep \"^   attributes   \" help.output &&\n+\ttest_grep \"^   mailmap   \" help.output\n '\n \n test_expect_success 'git help -c' '\ndiff --git a/t/t0013-sha1dc.sh b/t/t0013-sha1dc.sh\nindex ce3d81227a..3ea3169d92 100755\n--- a/t/t0013-sha1dc.sh\n+++ b/t/t0013-sha1dc.sh\n@@ -16,7 +16,7 @@ fi\n test_expect_success 'test-sha1 detects shattered pdf' '\n \ttest_must_fail test-tool sha1 <\"$TEST_DATA/shattered-1.pdf\" 2>err &&\n \ttest_grep collision err &&\n-\tgrep 38762cf7f55934b34d179ae6a4c80cadccbb7f0a err\n+\ttest_grep 38762cf7f55934b34d179ae6a4c80cadccbb7f0a err\n '\n \n test_done\ndiff --git a/t/t0017-env-helper.sh b/t/t0017-env-helper.sh\nindex 32fe848179..da02bce0ef 100755\n--- a/t/t0017-env-helper.sh\n+++ b/t/t0017-env-helper.sh\n@@ -88,7 +88,7 @@ test_expect_success 'test-tool env-helper reads config thanks to trace2' '\n \ttest_must_fail \\\n \t\tenv HOME=\"$(pwd)/home\" \\\n \t\tgit config -l 2>err &&\n-\tgrep \"exceeded maximum include depth\" err &&\n+\ttest_grep \"exceeded maximum include depth\" err &&\n \n \t# This validates that the assumption that we attempt to\n \t# read the configuration and fail very early in the start-up\n@@ -100,7 +100,7 @@ test_expect_success 'test-tool env-helper reads config thanks to trace2' '\n \t\ttest-tool -C no-such-directory \\\n \t\tenv-helper --type=bool --default=0 \\\n \t\t--exit-code GIT_TEST_ENV_HELPER 2>err &&\n-\tgrep \"exceeded maximum include depth\" err\n+\ttest_grep \"exceeded maximum include depth\" err\n '\n \n test_done\ndiff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh\nindex 033b00a364..4f29d088f3 100755\n--- a/t/t0021-conversion.sh\n+++ b/t/t0021-conversion.sh\n@@ -731,7 +731,7 @@ test_expect_success 'process filter should restart after unexpected write failur\n \t\trm -f debug.log &&\n \t\tgit checkout --quiet --no-progress . 2>git-stderr.log &&\n \n-\t\tgrep \"smudge write error\" git-stderr.log &&\n+\t\ttest_grep \"smudge write error\" git-stderr.log &&\n \t\ttest_grep \"error: external filter\" git-stderr.log &&\n \n \t\tcat >expected.log <<-EOF &&\n@@ -853,7 +853,7 @@ test_expect_success 'invalid process filter must fail (and not hang!)' '\n \n \t\tcp \"$TEST_ROOT/test.o\" test.r &&\n \t\ttest_must_fail git add . 2>git-stderr.log &&\n-\t\tgrep \"expected git-filter-server\" git-stderr.log\n+\t\ttest_grep \"expected git-filter-server\" git-stderr.log\n \t)\n '\n \n@@ -970,7 +970,7 @@ test_expect_success 'missing file in delayed checkout' '\n \n \trm -rf repo-cloned &&\n \ttest_must_fail git clone repo repo-cloned 2>git-stderr.log &&\n-\tgrep \"error: .missing-delay\\.a. was not filtered properly\" git-stderr.log\n+\ttest_grep \"error: .missing-delay\\.a. was not filtered properly\" git-stderr.log\n '\n \n test_expect_success 'invalid file in delayed checkout' '\n@@ -991,7 +991,7 @@ test_expect_success 'invalid file in delayed checkout' '\n \n \trm -rf repo-cloned &&\n \ttest_must_fail git clone repo repo-cloned 2>git-stderr.log &&\n-\tgrep \"error: external filter .* signaled that .unfiltered. is now available although it has not been delayed earlier\" git-stderr.log\n+\ttest_grep \"error: external filter .* signaled that .unfiltered. is now available although it has not been delayed earlier\" git-stderr.log\n '\n \n for mode in 'case' 'utf-8'\n@@ -1032,7 +1032,7 @@ do\n \n \t\tgit clone $mode-collision $mode-collision-cloned &&\n \t\t# Make sure z was really delayed\n-\t\tgrep \"IN: smudge $dir/z .* \\\\[DELAYED\\\\]\" $mode-collision-cloned/delayed.log &&\n+\t\ttest_grep \"IN: smudge $dir/z .* \\\\[DELAYED\\\\]\" $mode-collision-cloned/delayed.log &&\n \n \t\t# Should not create $dir/z at $symlink/z\n \t\ttest_path_is_missing $mode-collision/target-dir/z\n@@ -1070,7 +1070,7 @@ test_expect_success SYMLINKS,CASE_INSENSITIVE_FS \\\n \t\tgit commit -m super &&\n \n \t\tgit checkout --recurse-submodules . &&\n-\t\tgrep \"IN: smudge A/B/y .* \\\\[DELAYED\\\\]\" delayed.log &&\n+\t\ttest_grep \"IN: smudge A/B/y .* \\\\[DELAYED\\\\]\" delayed.log &&\n \t\ttest_path_is_missing target-dir/y\n \t)\n '\n@@ -1161,9 +1161,9 @@ test_expect_success 'delayed checkout correctly reports the number of updated en\n \n \t\trm *.a &&\n \t\tgit checkout . 2>err &&\n-\t\tgrep \"IN: smudge test-delay10.a .* \\\\[DELAYED\\\\]\" delayed.log &&\n-\t\tgrep \"IN: smudge test-delay11.a .* \\\\[DELAYED\\\\]\" delayed.log &&\n-\t\tgrep \"Updated 2 paths from the index\" err\n+\t\ttest_grep \"IN: smudge test-delay10.a .* \\\\[DELAYED\\\\]\" delayed.log &&\n+\t\ttest_grep \"IN: smudge test-delay11.a .* \\\\[DELAYED\\\\]\" delayed.log &&\n+\t\ttest_grep \"Updated 2 paths from the index\" err\n \t)\n '\n \ndiff --git a/t/t0029-core-unsetenvvars.sh b/t/t0029-core-unsetenvvars.sh\nindex baa1b7e85b..975620b389 100755\n--- a/t/t0029-core-unsetenvvars.sh\n+++ b/t/t0029-core-unsetenvvars.sh\n@@ -20,10 +20,10 @@ test_expect_success 'core.unsetenvvars works' '\n \tHOBBES=Calvin &&\n \texport HOBBES &&\n \tgit commit --allow-empty -m with 2>err &&\n-\tgrep Calvin err &&\n+\ttest_grep Calvin err &&\n \tgit -c core.unsetenvvars=FINDUS,HOBBES,CALVIN \\\n \t\tcommit --allow-empty -m without 2>err &&\n-\t! grep Calvin err\n+\ttest_grep ! Calvin err\n '\n \n test_done\ndiff --git a/t/t0030-stripspace.sh b/t/t0030-stripspace.sh\nindex 43155f6bd8..d6e2f63c59 100755\n--- a/t/t0030-stripspace.sh\n+++ b/t/t0030-stripspace.sh\n@@ -407,12 +407,12 @@ test_expect_success 'strip comments with changed comment string' '\n \n test_expect_success 'newline as commentchar is forbidden' '\n \ttest_must_fail git -c core.commentChar=\"$LF\" stripspace -s 2>err &&\n-\tgrep \"core.commentchar cannot contain newline\" err\n+\ttest_grep \"core.commentchar cannot contain newline\" err\n '\n \n test_expect_success 'empty commentchar is forbidden' '\n \ttest_must_fail git -c core.commentchar= stripspace -s 2>err &&\n-\tgrep \"core.commentchar must have at least one character\" err\n+\ttest_grep \"core.commentchar must have at least one character\" err\n '\n \n test_expect_success '-c with single line' '\ndiff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh\nindex 8ef87addf5..e9e2f04049 100755\n--- a/t/t0031-lockfile-pid.sh\n+++ b/t/t0031-lockfile-pid.sh\n@@ -29,7 +29,7 @@ test_expect_success 'PID info not shown by default' '\n \t\ttest_must_fail git add . 2>err &&\n \t\t# Should not crash, just show normal error without PID\n \t\ttest_grep \"Unable to create\" err &&\n-\t\t! test_grep \"is held by process\" err\n+\t\ttest_grep ! \"is held by process\" err\n \t)\n '\n \ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex ca55ea8228..a8348a851c 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -324,13 +324,13 @@ test_expect_success 'non ambiguous option (after two options it abbreviates)' '\n \n test_expect_success 'Alias options do not contribute to abbreviation' '\n \ttest-tool parse-options --alias-source 123 >output &&\n-\tgrep \"^string: 123\" output &&\n+\ttest_grep \"^string: 123\" output &&\n \ttest-tool parse-options --alias-target 123 >output &&\n-\tgrep \"^string: 123\" output &&\n+\ttest_grep \"^string: 123\" output &&\n \ttest_must_fail test-tool parse-options --alias &&\n \tGIT_TEST_DISALLOW_ABBREVIATED_OPTIONS=false \\\n \ttest-tool parse-options --alias 123 >output &&\n-\tgrep \"^string: 123\" output\n+\ttest_grep \"^string: 123\" output\n '\n \n cat >typo.err <<\\EOF\n@@ -582,16 +582,16 @@ test_expect_success 'KEEP_UNKNOWN_OPT works' '\n \n test_expect_success 'NO_INTERNAL_HELP works for -h' '\n \ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd -h 2>err &&\n-\tgrep \"^error: unknown switch \\`h$SQ\" err &&\n-\tgrep \"^usage: \" err\n+\ttest_grep \"^error: unknown switch \\`h$SQ\" err &&\n+\ttest_grep \"^usage: \" err\n '\n \n for help_opt in help help-all\n do\n \ttest_expect_success \"NO_INTERNAL_HELP works for --$help_opt\" \"\n \t\ttest_expect_code 129 test-tool parse-options-flags --no-internal-help cmd --$help_opt 2>err &&\n-\t\tgrep '^error: unknown option \\`'$help_opt\\' err &&\n-\t\tgrep '^usage: ' err\n+\t\ttest_grep '^error: unknown option \\`'$help_opt\\' err &&\n+\t\ttest_grep '^usage: ' err\n \t\"\n done\n \n@@ -608,38 +608,38 @@ test_expect_success 'KEEP_UNKNOWN_OPT | NO_INTERNAL_HELP works' '\n \n test_expect_success 'subcommand - no subcommand shows error and usage' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd 2>err &&\n-\tgrep \"^error: need a subcommand\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: need a subcommand\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - subcommand after -- shows error and usage' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd -- subcmd-one 2>err &&\n-\tgrep \"^error: need a subcommand\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: need a subcommand\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - subcommand after --end-of-options shows error and usage' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd --end-of-options subcmd-one 2>err &&\n-\tgrep \"^error: need a subcommand\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: need a subcommand\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - unknown subcommand shows error and usage' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd nope 2>err &&\n-\tgrep \"^error: unknown subcommand: \\`nope$SQ\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: unknown subcommand: \\`nope$SQ\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - subcommands cannot be abbreviated' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd subcmd-o 2>err &&\n-\tgrep \"^error: unknown subcommand: \\`subcmd-o$SQ$\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: unknown subcommand: \\`subcmd-o$SQ$\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - no negated subcommands' '\n \ttest_expect_code 129 test-tool parse-subcommand cmd no-subcmd-one 2>err &&\n-\tgrep \"^error: unknown subcommand: \\`no-subcmd-one$SQ\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: unknown subcommand: \\`no-subcmd-one$SQ\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - simple' '\n@@ -709,8 +709,8 @@ test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given + u\n \n test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL + subcommand not given + unknown option' '\n \ttest_expect_code 129 test-tool parse-subcommand --subcommand-optional cmd --subcommand-opt 2>err &&\n-\tgrep \"^error: unknown option\" err &&\n-\tgrep ^usage: err\n+\ttest_grep \"^error: unknown option\" err &&\n+\ttest_grep ^usage: err\n '\n \n test_expect_success 'subcommand - SUBCOMMAND_OPTIONAL | KEEP_UNKNOWN_OPT + subcommand not given + unknown option' '\n@@ -778,28 +778,28 @@ test_expect_success 'subcommand - completion helper' '\n \n test_expect_success 'subcommands are incompatible with STOP_AT_NON_OPTION' '\n \ttest_must_fail test-tool parse-subcommand --stop-at-non-option cmd subcmd-one 2>err &&\n-\tgrep ^BUG err\n+\ttest_grep ^BUG err\n '\n \n test_expect_success 'subcommands are incompatible with KEEP_UNKNOWN_OPT unless in combination with SUBCOMMAND_OPTIONAL' '\n \ttest_must_fail test-tool parse-subcommand --keep-unknown-opt cmd subcmd-two 2>err &&\n-\tgrep ^BUG err\n+\ttest_grep ^BUG err\n '\n \n test_expect_success 'subcommands are incompatible with KEEP_DASHDASH unless in combination with SUBCOMMAND_OPTIONAL' '\n \ttest_must_fail test-tool parse-subcommand --keep-dashdash cmd subcmd-two 2>err &&\n-\tgrep ^BUG err\n+\ttest_grep ^BUG err\n '\n \n test_expect_success 'negative unsigned' '\n \ttest_must_fail test-tool parse-options --unsigned -1 >out 2>err &&\n-\tgrep \"non-negative integer\" err &&\n+\ttest_grep \"non-negative integer\" err &&\n \ttest_must_be_empty out\n '\n \n test_expect_success 'unsigned with units but no numbers' '\n \ttest_must_fail test-tool parse-options --unsigned m >out 2>err &&\n-\tgrep \"non-negative integer\" err &&\n+\ttest_grep \"non-negative integer\" err &&\n \ttest_must_be_empty out\n '\n \ndiff --git a/t/t0041-usage.sh b/t/t0041-usage.sh\nindex a0f6f134c7..51af7cc030 100755\n--- a/t/t0041-usage.sh\n+++ b/t/t0041-usage.sh\n@@ -13,7 +13,7 @@ test_expect_success 'setup ' '\n \n test_expect_success 'tag --contains <existent_tag>' '\n \tgit tag --contains \"v1.0\" >actual 2>actual.err &&\n-\tgrep \"v1.0\" actual &&\n+\ttest_grep \"v1.0\" actual &&\n \ttest_line_count = 0 actual.err\n '\n \ndiff --git a/t/t0052-simple-ipc.sh b/t/t0052-simple-ipc.sh\nindex ff98be31a5..14cea84920 100755\n--- a/t/t0052-simple-ipc.sh\n+++ b/t/t0052-simple-ipc.sh\n@@ -33,19 +33,19 @@ test_expect_success 'servers cannot share the same path' '\n test_expect_success 'big response' '\n \ttest-tool simple-ipc send --token=big >actual &&\n \ttest_line_count -ge 10000 actual &&\n-\tgrep -q \"big: [0]*9999\\$\" actual\n+\ttest_grep -q \"big: [0]*9999\\$\" actual\n '\n \n test_expect_success 'chunk response' '\n \ttest-tool simple-ipc send --token=chunk >actual &&\n \ttest_line_count -ge 10000 actual &&\n-\tgrep -q \"big: [0]*9999\\$\" actual\n+\ttest_grep -q \"big: [0]*9999\\$\" actual\n '\n \n test_expect_success 'slow response' '\n \ttest-tool simple-ipc send --token=slow >actual &&\n \ttest_line_count -ge 100 actual &&\n-\tgrep -q \"big: [0]*99\\$\" actual\n+\ttest_grep -q \"big: [0]*99\\$\" actual\n '\n \n # Send an IPC with n=100,000 bytes of ballast.  This should be large enough\n@@ -54,7 +54,7 @@ test_expect_success 'slow response' '\n #\n test_expect_success 'sendbytes' '\n \ttest-tool simple-ipc sendbytes --bytecount=100000 --byte=A >actual &&\n-\tgrep \"sent:A00100000 rcvd:A00100000\" actual\n+\ttest_grep \"sent:A00100000 rcvd:A00100000\" actual\n '\n \n # Start a series of <threads> client threads that each make <batchsize>\n@@ -93,7 +93,7 @@ test_expect_success 'stress test threads' '\n \t\t--batchsize=13 \\\n \t\t>actual &&\n \ttest_line_count = 92 actual &&\n-\tgrep \"good 91\" actual &&\n+\ttest_grep \"good 91\" actual &&\n \tgrep \"sent:A\" <actual >actual_a &&\n \tcat >expect_a <<-EOF &&\n \t\tsent:A00000019 rcvd:A00000019\ndiff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh\nindex 60cfe65979..dc9d77b0e1 100755\n--- a/t/t0061-run-command.sh\n+++ b/t/t0061-run-command.sh\n@@ -97,7 +97,7 @@ test_expect_success POSIXPERM 'run_command reports EACCES' '\n \tchmod -x hello.sh &&\n \ttest_must_fail test-tool run-command run-command ./hello.sh 2>err &&\n \n-\tgrep \"fatal: cannot exec.*hello.sh\" err\n+\ttest_grep \"fatal: cannot exec.*hello.sh\" err\n '\n \n test_expect_success POSIXPERM,SANITY 'unreadable directory in PATH' '\ndiff --git a/t/t0066-dir-iterator.sh b/t/t0066-dir-iterator.sh\nindex df3e9f5fa5..9fbb41bac5 100755\n--- a/t/t0066-dir-iterator.sh\n+++ b/t/t0066-dir-iterator.sh\n@@ -127,7 +127,7 @@ test_expect_success SYMLINKS 'dir-iterator should not follow symlinks by default\n test_expect_success SYMLINKS 'dir-iterator does not resolve top-level symlinks' '\n \ttest_must_fail test-tool dir-iterator ./dir5 >out &&\n \n-\tgrep \"ENOTDIR\" out\n+\ttest_grep \"ENOTDIR\" out\n '\n \n test_done\ndiff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh\nindex 80b163ea99..55b37d895a 100755\n--- a/t/t0068-for-each-repo.sh\n+++ b/t/t0068-for-each-repo.sh\n@@ -21,23 +21,23 @@ test_expect_success 'run based on configured value' '\n \n \tgit for-each-repo --config=run.key commit --allow-empty -m \"ran\" &&\n \tgit -C one log -1 --pretty=format:%s >message &&\n-\tgrep ran message &&\n+\ttest_grep ran message &&\n \tgit -C two log -1 --pretty=format:%s >message &&\n-\t! grep ran message &&\n+\ttest_grep ! ran message &&\n \tgit -C three log -1 --pretty=format:%s >message &&\n-\tgrep ran message &&\n+\ttest_grep ran message &&\n \tgit -C ~/four log -1 --pretty=format:%s >message &&\n-\tgrep ran message &&\n+\ttest_grep ran message &&\n \n \tgit for-each-repo --config=run.key -- commit --allow-empty -m \"ran again\" &&\n \tgit -C one log -1 --pretty=format:%s >message &&\n-\tgrep again message &&\n+\ttest_grep again message &&\n \tgit -C two log -1 --pretty=format:%s >message &&\n-\t! grep again message &&\n+\ttest_grep ! again message &&\n \tgit -C three log -1 --pretty=format:%s >message &&\n-\tgrep again message &&\n+\ttest_grep again message &&\n \tgit -C ~/four log -1 --pretty=format:%s >message &&\n-\tgrep again message &&\n+\ttest_grep again message &&\n \n \tgit -C three for-each-repo --config=run.key -- \\\n \t\tcommit --allow-empty -m \"ran from worktree\" &&\ndiff --git a/t/t0070-fundamental.sh b/t/t0070-fundamental.sh\nindex 6b9dcf984b..8f573c2a0e 100755\n--- a/t/t0070-fundamental.sh\n+++ b/t/t0070-fundamental.sh\n@@ -10,7 +10,7 @@ Verify wrappers and compatibility functions.\n \n test_expect_success 'mktemp to nonexistent directory prints filename' '\n \ttest_must_fail test-tool mktemp doesnotexist/testXXXXXX 2>err &&\n-\tgrep \"doesnotexist/test\" err\n+\ttest_grep \"doesnotexist/test\" err\n '\n \n test_expect_success POSIXPERM,SANITY 'mktemp to unwritable directory prints filename' '\n@@ -18,7 +18,7 @@ test_expect_success POSIXPERM,SANITY 'mktemp to unwritable directory prints file\n \ttest_when_finished \"chmod +w cannotwrite\" &&\n \tchmod -w cannotwrite &&\n \ttest_must_fail test-tool mktemp cannotwrite/testXXXXXX 2>err &&\n-\tgrep \"cannotwrite/test\" err\n+\ttest_grep \"cannotwrite/test\" err\n '\n \n test_expect_success 'git_mkstemps_mode does not fail if fd 0 is not open' '\n@@ -33,7 +33,7 @@ test_expect_success 'check for a bug in the regex routines' '\n test_expect_success 'incomplete sideband messages are reassembled' '\n \ttest-tool pkt-line send-split-sideband >split-sideband &&\n \ttest-tool pkt-line receive-sideband <split-sideband 2>err &&\n-\tgrep \"Hello, world\" err\n+\ttest_grep \"Hello, world\" err\n '\n \n test_expect_success 'eof on sideband message is reported' '\ndiff --git a/t/t0081-find-pack.sh b/t/t0081-find-pack.sh\nindex 26f017422d..ff9d56fbf1 100755\n--- a/t/t0081-find-pack.sh\n+++ b/t/t0081-find-pack.sh\n@@ -52,17 +52,17 @@ test_expect_success 'add more packfiles' '\n \n \t# HEAD^{tree} is in 2 packfiles\n \ttest-tool find-pack HEAD^{tree} >head_tree_packs &&\n-\tgrep \"$head_commit_pack\" head_tree_packs &&\n-\tgrep mypackname1 head_tree_packs &&\n-\t! grep mypackname2 head_tree_packs &&\n+\ttest_grep \"$head_commit_pack\" head_tree_packs &&\n+\ttest_grep mypackname1 head_tree_packs &&\n+\ttest_grep ! mypackname2 head_tree_packs &&\n \ttest-tool find-pack --check-count 2 HEAD^{tree} &&\n \t! test-tool find-pack --check-count 1 HEAD^{tree} &&\n \n \t# HEAD:five.t is also in 2 packfiles\n \ttest-tool find-pack HEAD:five.t >five_packs &&\n-\tgrep \"$head_commit_pack\" five_packs &&\n-\t! grep mypackname1 five_packs &&\n-\tgrep mypackname2 five_packs &&\n+\ttest_grep \"$head_commit_pack\" five_packs &&\n+\ttest_grep ! mypackname1 five_packs &&\n+\ttest_grep mypackname2 five_packs &&\n \ttest-tool find-pack -c 2 HEAD:five.t &&\n \t! test-tool find-pack --check-count=0 HEAD:five.t\n '\ndiff --git a/t/t0091-bugreport.sh b/t/t0091-bugreport.sh\nindex e38ca7a901..81ed41742c 100755\n--- a/t/t0091-bugreport.sh\n+++ b/t/t0091-bugreport.sh\n@@ -40,15 +40,15 @@ test_expect_success 'sanity check \"System Info\" section' '\n \n \t# The beginning should match \"git version --build-options\" verbatim,\n \t# but rather than checking bit-for-bit equality, just test some basics.\n-\tgrep \"git version \" system &&\n-\tgrep \"shell-path: .\" system &&\n+\ttest_grep \"git version \" system &&\n+\ttest_grep \"shell-path: .\" system &&\n \n \t# After the version, there should be some more info.\n \t# This is bound to differ from environment to environment,\n \t# so we just do some rather high-level checks.\n-\tgrep \"uname: .\" system &&\n-\tgrep \"compiler info: .\" system &&\n-\tgrep \"zlib.\" system\n+\ttest_grep \"uname: .\" system &&\n+\ttest_grep \"compiler info: .\" system &&\n+\ttest_grep \"zlib.\" system\n '\n \n test_expect_success 'dies if file with same name as report already exists' '\n@@ -112,7 +112,7 @@ test_expect_success UNZIP '--diagnose creates diagnostics zip archive' '\n \tgit bugreport --diagnose -o report -s test >out &&\n \n \tzip_path=report/git-diagnostics-test.zip &&\n-\tgrep \"Available space\" out &&\n+\ttest_grep \"Available space\" out &&\n \ttest_path_is_file \"$zip_path\" &&\n \n \t# Check zipped archive content\n@@ -120,10 +120,10 @@ test_expect_success UNZIP '--diagnose creates diagnostics zip archive' '\n \ttest_file_not_empty out &&\n \n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t\"$GIT_UNZIP\" -p \"$zip_path\" objects-local.txt >out &&\n-\tgrep \"^Total: [0-9][0-9]*\" out &&\n+\ttest_grep \"^Total: [0-9][0-9]*\" out &&\n \n \t# Should not include .git directory contents by default\n \t! \"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\"\n@@ -136,7 +136,7 @@ test_expect_success UNZIP '--diagnose=stats excludes .git dir contents' '\n \n \t# Includes pack quantity/size info\n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t# Does not include .git directory contents\n \t! \"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\"\ndiff --git a/t/t0092-diagnose.sh b/t/t0092-diagnose.sh\nindex 6cabd6e67b..0bc2b1ed5c 100755\n--- a/t/t0092-diagnose.sh\n+++ b/t/t0092-diagnose.sh\n@@ -8,7 +8,7 @@ test_expect_success UNZIP 'creates diagnostics zip archive' '\n \ttest_when_finished rm -rf report &&\n \n \tgit diagnose -o report -s test >out &&\n-\tgrep \"Available space\" out &&\n+\ttest_grep \"Available space\" out &&\n \n \tzip_path=report/git-diagnostics-test.zip &&\n \ttest_path_is_file \"$zip_path\" &&\n@@ -18,10 +18,10 @@ test_expect_success UNZIP 'creates diagnostics zip archive' '\n \ttest_file_not_empty out &&\n \n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t\"$GIT_UNZIP\" -p \"$zip_path\" objects-local.txt >out &&\n-\tgrep \"^Total: [0-9][0-9]*\" out &&\n+\ttest_grep \"^Total: [0-9][0-9]*\" out &&\n \n \t# Should not include .git directory contents by default\n \t! \"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\"\n@@ -34,7 +34,7 @@ test_expect_success UNZIP 'counts loose objects' '\n \tgit diagnose -o test-count -s 1 >out &&\n \tzip_path=test-count/git-diagnostics-1.zip &&\n \t\"$GIT_UNZIP\" -p \"$zip_path\" objects-local.txt >out &&\n-\tgrep \"^Total: [1-9][0-9]* loose objects\" out\n+\ttest_grep \"^Total: [1-9][0-9]* loose objects\" out\n '\n \n test_expect_success UNZIP '--mode=stats excludes .git dir contents' '\n@@ -45,7 +45,7 @@ test_expect_success UNZIP '--mode=stats excludes .git dir contents' '\n \t# Includes pack quantity/size info\n \tzip_path=report/git-diagnostics-test.zip &&\n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t# Does not include .git directory contents\n \t! \"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\"\n@@ -59,7 +59,7 @@ test_expect_success UNZIP '--mode=all includes .git dir contents' '\n \t# Includes pack quantity/size info\n \tzip_path=report/git-diagnostics-test.zip &&\n \t\"$GIT_UNZIP\" -p \"$zip_path\" packs-local.txt >out &&\n-\tgrep \".git/objects\" out &&\n+\ttest_grep \".git/objects\" out &&\n \n \t# Includes .git directory contents\n \t\"$GIT_UNZIP\" -l \"$zip_path\" | grep \".git/\" &&\ndiff --git a/t/t0100-previous.sh b/t/t0100-previous.sh\nindex dd5d9b4e5e..87a9995ea4 100755\n--- a/t/t0100-previous.sh\n+++ b/t/t0100-previous.sh\n@@ -50,7 +50,7 @@ test_expect_success 'merge @{-1}~1' '\n \tgit checkout main &&\n \tgit merge @{-1}~1 &&\n \tgit cat-file commit HEAD >actual &&\n-\tgrep \"Merge branch '\\''other'\\''\" actual\n+\ttest_grep \"Merge branch '\\''other'\\''\" actual\n '\n \n test_expect_success 'merge @{-100} before checking out that many branches yet' '\ndiff --git a/t/t0200-gettext-basic.sh b/t/t0200-gettext-basic.sh\nindex 8853d8afb9..8db26c1ada 100755\n--- a/t/t0200-gettext-basic.sh\n+++ b/t/t0200-gettext-basic.sh\n@@ -16,7 +16,7 @@ test_expect_success 'sanity: $TEXTDOMAIN is git' '\n '\n \n test_expect_success 'xgettext sanity: Perl _() strings are not extracted' '\n-    ! grep \"A Perl string xgettext will not get\" \"$GIT_PO_PATH\"/is.po\n+    test_grep ! \"A Perl string xgettext will not get\" \"$GIT_PO_PATH\"/is.po\n '\n \n test_expect_success 'xgettext sanity: Comment extraction with --add-comments' '\n@@ -26,8 +26,8 @@ test_expect_success 'xgettext sanity: Comment extraction with --add-comments' '\n '\n \n test_expect_success 'xgettext sanity: Comment extraction with --add-comments stops at statements' '\n-    ! grep \"This is a phony\" \"$GIT_PO_PATH\"/is.po &&\n-    ! grep \"the above comment\" \"$GIT_PO_PATH\"/is.po\n+    test_grep ! \"This is a phony\" \"$GIT_PO_PATH\"/is.po &&\n+    test_grep ! \"the above comment\" \"$GIT_PO_PATH\"/is.po\n '\n \n test_expect_success GETTEXT 'sanity: $TEXTDOMAINDIR exists without NO_GETTEXT=YesPlease' '\n@@ -44,10 +44,10 @@ test_expect_success GETTEXT 'sanity: Icelandic locale was compiled' '\n test_expect_success GETTEXT_LOCALE 'sanity: gettext(\"\") metadata is OK' '\n     # Return value may be non-zero\n     LANGUAGE=is LC_ALL=\"$is_IS_locale\" gettext \"\" >zero-expect &&\n-    grep \"Project-Id-Version: Git\" zero-expect &&\n-    grep \"Git Mailing List <git@vger.kernel.org>\" zero-expect &&\n-    grep \"Content-Type: text/plain; charset=UTF-8\" zero-expect &&\n-    grep \"Content-Transfer-Encoding: 8bit\" zero-expect\n+    test_grep \"Project-Id-Version: Git\" zero-expect &&\n+    test_grep \"Git Mailing List <git@vger.kernel.org>\" zero-expect &&\n+    test_grep \"Content-Type: text/plain; charset=UTF-8\" zero-expect &&\n+    test_grep \"Content-Transfer-Encoding: 8bit\" zero-expect\n '\n \n test_expect_success GETTEXT_LOCALE 'sanity: gettext(unknown) is passed through' '\ndiff --git a/t/t0203-gettext-setlocale-sanity.sh b/t/t0203-gettext-setlocale-sanity.sh\nindex 0ce1f22eff..670082bf09 100755\n--- a/t/t0203-gettext-setlocale-sanity.sh\n+++ b/t/t0203-gettext-setlocale-sanity.sh\n@@ -12,7 +12,7 @@ test_expect_success 'git show a ISO-8859-1 commit under C locale' '\n \ttest_commit \"iso-c-commit\" iso-under-c &&\n \tgit show >out 2>err &&\n \ttest_must_be_empty err &&\n-\tgrep -q \"iso-c-commit\" out\n+\ttest_grep -q \"iso-c-commit\" out\n '\n \n test_expect_success GETTEXT_LOCALE 'git show a ISO-8859-1 commit under a UTF-8 locale' '\n@@ -20,7 +20,7 @@ test_expect_success GETTEXT_LOCALE 'git show a ISO-8859-1 commit under a UTF-8 l\n \ttest_commit \"iso-utf8-commit\" iso-under-utf8 &&\n \tLANGUAGE=is LC_ALL=\"$is_IS_locale\" git show >out 2>err &&\n \ttest_must_be_empty err &&\n-\tgrep -q \"iso-utf8-commit\" out\n+\ttest_grep -q \"iso-utf8-commit\" out\n '\n \n test_done\ndiff --git a/t/t0204-gettext-reencode-sanity.sh b/t/t0204-gettext-reencode-sanity.sh\nindex 28d92bb9b7..48ccddaf7d 100755\n--- a/t/t0204-gettext-reencode-sanity.sh\n+++ b/t/t0204-gettext-reencode-sanity.sh\n@@ -66,22 +66,22 @@ test_expect_success GETTEXT_LOCALE 'gettext: Fetching a UTF-8 msgid -> UTF-8' '\n # eyes.\n test_expect_success GETTEXT_ISO_LOCALE 'gettext: Fetching a UTF-8 msgid -> ISO-8859-1' '\n     LANGUAGE=is LC_ALL=\"$is_IS_iso_locale\" gettext \"TEST: ‘single’ and “double” quotes\" >actual &&\n-    grep \"einfaldar\" actual &&\n-    grep \"$(echo tvöfaldar | iconv -f UTF-8 -t ISO8859-1)\" actual\n+    test_grep \"einfaldar\" actual &&\n+    test_grep \"$(echo tvöfaldar | iconv -f UTF-8 -t ISO8859-1)\" actual\n '\n \n test_expect_success GETTEXT_LOCALE 'gettext.c: git init UTF-8 -> UTF-8' '\n     printf \"Bjó til tóma Git lind\" >expect &&\n     LANGUAGE=is LC_ALL=\"$is_IS_locale\" git init repo >actual &&\n     test_when_finished \"rm -rf repo\" &&\n-    grep \"^$(cat expect) \" actual\n+    test_grep \"^$(cat expect) \" actual\n '\n \n test_expect_success GETTEXT_ISO_LOCALE 'gettext.c: git init UTF-8 -> ISO-8859-1' '\n     printf \"Bjó til tóma Git lind\" >expect &&\n     LANGUAGE=is LC_ALL=\"$is_IS_iso_locale\" git init repo >actual &&\n     test_when_finished \"rm -rf repo\" &&\n-    grep \"^$(iconv -f UTF-8 -t ISO8859-1 <expect) \" actual\n+    test_grep \"^$(iconv -f UTF-8 -t ISO8859-1 <expect) \" actual\n '\n \n test_done\ndiff --git a/t/t0210-trace2-normal.sh b/t/t0210-trace2-normal.sh\nindex 7e1e7af862..beed2b27a5 100755\n--- a/t/t0210-trace2-normal.sh\n+++ b/t/t0210-trace2-normal.sh\n@@ -333,12 +333,12 @@ test_expect_success 'unsafe URLs are redacted by default' '\n \n \tGIT_TRACE2=\"$(pwd)/trace.normal\" \\\n \t\tgit clone https://user:pwd@example.com/ clone &&\n-\t! grep user:pwd trace.normal &&\n+\ttest_grep ! user:pwd trace.normal &&\n \n \tGIT_TRACE2_REDACT=0 GIT_TRACE2=\"$(pwd)/unredacted.normal\" \\\n \t\tgit clone https://user:pwd@example.com/ clone2 &&\n-\tgrep \"start .* clone https://user:pwd@example.com\" unredacted.normal &&\n-\tgrep \"remote.origin.url=https://user:pwd@example.com\" unredacted.normal\n+\ttest_grep \"start .* clone https://user:pwd@example.com\" unredacted.normal &&\n+\ttest_grep \"remote.origin.url=https://user:pwd@example.com\" unredacted.normal\n '\n \n test_done\ndiff --git a/t/t0211-trace2-perf.sh b/t/t0211-trace2-perf.sh\nindex 760cf69087..665c8b3669 100755\n--- a/t/t0211-trace2-perf.sh\n+++ b/t/t0211-trace2-perf.sh\n@@ -283,13 +283,13 @@ test_expect_success 'unsafe URLs are redacted by default' '\n \n \tGIT_TRACE2_PERF=\"$(pwd)/trace.perf\" \\\n \t\tgit clone https://user:pwd@example.com/ clone &&\n-\t! grep user:pwd trace.perf &&\n+\ttest_grep ! user:pwd trace.perf &&\n \n \tGIT_TRACE2_REDACT=0 GIT_TRACE2_PERF=\"$(pwd)/unredacted.perf\" \\\n \t\tgit clone https://user:pwd@example.com/ clone2 &&\n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <unredacted.perf >actual &&\n-\tgrep \"d0|main|start|.* clone https://user:pwd@example.com\" actual &&\n-\tgrep \"d0|main|def_param|.*|remote.origin.url:https://user:pwd@example.com\" actual\n+\ttest_grep \"d0|main|start|.* clone https://user:pwd@example.com\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|remote.origin.url:https://user:pwd@example.com\" actual\n '\n \n # Confirm that the requested command produces a \"cmd_name\" and a\n@@ -358,13 +358,13 @@ test_expect_success LIBCURL \\\n \n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <prop.perf >actual &&\n \n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_\" actual &&\n-\tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n-\tgrep \"d1|main|cmd_name|.*|remote-curl\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|remote-curl\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n # Similarly, `git-http-fetch` is not built from git.c so do a\n@@ -389,13 +389,13 @@ test_expect_success LIBCURL \\\n \n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <prop.perf >actual &&\n \n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_\" actual &&\n-\tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n-\tgrep \"d1|main|cmd_name|.*|http-fetch\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|http-fetch\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n # Historically, alias expansion explicitly emitted the def_param\n@@ -421,22 +421,22 @@ test_expect_success 'expect def_params during git alias expansion' '\n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <prop.perf >actual &&\n \n \t# \"git xxx\" is first mapped to \"git-xxx\" and the child will fail.\n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n \n \t# We unpeel that and substitute \"version\" into \"xxx\" (giving\n \t# \"git version\") and update the cmd_name event.\n-\tgrep \"d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_git_alias_)\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_git_alias_)\" actual &&\n \n \t# These def_param events could be associated with either of the\n \t# above cmd_name events.  It does not matter.\n-\tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n \t# The \"git version\" child sees a different cmd_name hierarchy.\n \t# Also test the def_param (only for completeness).\n-\tgrep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_git_alias_/version)\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_git_alias_/version)\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n test_expect_success 'expect def_params during shell alias expansion' '\n@@ -456,25 +456,25 @@ test_expect_success 'expect def_params during shell alias expansion' '\n \tperl \"$TEST_DIRECTORY/t0211/scrub_perf.perl\" <prop.perf >actual &&\n \n \t# \"git xxx\" is first mapped to \"git-xxx\" and the child will fail.\n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n \n \t# We unpeel that and substitute \"git version\" for \"git xxx\" (as a\n \t# shell command.  Another cmd_name event is emitted as we unpeel.\n-\tgrep \"d0|main|cmd_name|.*|_run_shell_alias_ (_run_dashed_/_run_shell_alias_)\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_shell_alias_ (_run_dashed_/_run_shell_alias_)\" actual &&\n \n \t# These def_param events could be associated with either of the\n \t# above cmd_name events.  It does not matter.\n-\tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n \t# We get the following only because we used a git command for the\n \t# shell command. In general, it could have been a shell script and\n \t# we would see nothing.\n \t#\n \t# The child knows the cmd_name hierarchy so it includes it.\n-\tgrep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_shell_alias_/version)\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_shell_alias_/version)\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n test_expect_success 'expect def_params during nested git alias expansion' '\n@@ -496,33 +496,33 @@ test_expect_success 'expect def_params during nested git alias expansion' '\n \n \t# \"git xxx\" is first mapped to \"git-xxx\" and try to spawn \"git-xxx\"\n \t# and the child will fail.\n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n-\tgrep \"d0|main|child_start|.*|.* class:dashed argv:\\[git-xxx\\]\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_)\" actual &&\n+\ttest_grep \"d0|main|child_start|.*|.* class:dashed argv:\\[git-xxx\\]\" actual &&\n \n \t# We unpeel that and substitute \"yyy\" into \"xxx\" (giving \"git yyy\")\n \t# and spawn \"git-yyy\" and the child will fail.\n-\tgrep \"d0|main|alias|.*|alias:xxx argv:\\[yyy\\]\" actual &&\n-\tgrep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_/_run_dashed_)\" actual &&\n-\tgrep \"d0|main|child_start|.*|.* class:dashed argv:\\[git-yyy\\]\" actual &&\n+\ttest_grep \"d0|main|alias|.*|alias:xxx argv:\\[yyy\\]\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_dashed_ (_run_dashed_/_run_dashed_)\" actual &&\n+\ttest_grep \"d0|main|child_start|.*|.* class:dashed argv:\\[git-yyy\\]\" actual &&\n \n \t# We unpeel that and substitute \"version\" into \"xxx\" (giving\n \t# \"git version\") and update the cmd_name event.\n-\tgrep \"d0|main|alias|.*|alias:yyy argv:\\[version\\]\" actual &&\n-\tgrep \"d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_dashed_/_run_git_alias_)\" actual &&\n+\ttest_grep \"d0|main|alias|.*|alias:yyy argv:\\[version\\]\" actual &&\n+\ttest_grep \"d0|main|cmd_name|.*|_run_git_alias_ (_run_dashed_/_run_dashed_/_run_git_alias_)\" actual &&\n \n \t# These def_param events could be associated with any of the\n \t# above cmd_name events.  It does not matter.\n \tgrep \"d0|main|def_param|.*|cfg.prop.foo:red\" actual >actual.matches &&\n-\tgrep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n+\ttest_grep \"d0|main|def_param|.*|ENV_PROP_FOO:blue\" actual &&\n \n \t# However, we do not want them repeated each time we unpeel.\n \ttest_line_count = 1 actual.matches &&\n \n \t# The \"git version\" child sees a different cmd_name hierarchy.\n \t# Also test the def_param (only for completeness).\n-\tgrep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_dashed_/_run_git_alias_/version)\" actual &&\n-\tgrep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n-\tgrep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n+\ttest_grep \"d1|main|cmd_name|.*|version (_run_dashed_/_run_dashed_/_run_git_alias_/version)\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|cfg.prop.foo:red\" actual &&\n+\ttest_grep \"d1|main|def_param|.*|ENV_PROP_FOO:blue\" actual\n '\n \n test_done\ndiff --git a/t/t0212-trace2-event.sh b/t/t0212-trace2-event.sh\nindex 1211db9f46..f5358a1dd4 100755\n--- a/t/t0212-trace2-event.sh\n+++ b/t/t0212-trace2-event.sh\n@@ -332,7 +332,7 @@ test_expect_success 'unsafe URLs are redacted by default in cmd_start events' '\n \n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.event\" \\\n \t\ttest-tool trace2 300redact_start git clone https://user:pwd@example.com/ clone2 &&\n-\t! grep user:pwd trace.event\n+\ttest_grep ! user:pwd trace.event\n '\n \n test_expect_success 'unsafe URLs are redacted by default in child_start events' '\n@@ -341,7 +341,7 @@ test_expect_success 'unsafe URLs are redacted by default in child_start events'\n \n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.event\" \\\n \t\ttest-tool trace2 301redact_child_start git clone https://user:pwd@example.com/ clone2 &&\n-\t! grep user:pwd trace.event\n+\ttest_grep ! user:pwd trace.event\n '\n \n test_expect_success 'unsafe URLs are redacted by default in exec events' '\n@@ -350,7 +350,7 @@ test_expect_success 'unsafe URLs are redacted by default in exec events' '\n \n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.event\" \\\n \t\ttest-tool trace2 302redact_exec git clone https://user:pwd@example.com/ clone2 &&\n-\t! grep user:pwd trace.event\n+\ttest_grep ! user:pwd trace.event\n '\n \n test_expect_success 'unsafe URLs are redacted by default in def_param events' '\n@@ -359,7 +359,7 @@ test_expect_success 'unsafe URLs are redacted by default in def_param events' '\n \n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.event\" \\\n \t\ttest-tool trace2 303redact_def_param url https://user:pwd@example.com/ &&\n-\t! grep user:pwd trace.event\n+\ttest_grep ! user:pwd trace.event\n '\n \n test_done\ndiff --git a/t/t0300-credentials.sh b/t/t0300-credentials.sh\nindex 64ead1571a..ea11cdb4dc 100755\n--- a/t/t0300-credentials.sh\n+++ b/t/t0300-credentials.sh\n@@ -1014,7 +1014,7 @@ test_expect_success 'credential config with partial URLs' '\n \tdo\n \t\tgit -c credential.$partial.helper=yep \\\n \t\t\tcredential fill <stdin >stdout &&\n-\t\tgrep yep stdout ||\n+\t\ttest_grep yep stdout ||\n \t\treturn 1\n \tdone &&\n \n@@ -1030,7 +1030,7 @@ test_expect_success 'credential config with partial URLs' '\n \tdo\n \t\tgit -c credential.$partial.helper=yep \\\n \t\t\tcredential fill <stdin >stdout &&\n-\t\t! grep yep stdout ||\n+\t\ttest_grep ! yep stdout ||\n \t\treturn 1\n \tdone &&\n \ndiff --git a/t/t0410-partial-clone.sh b/t/t0410-partial-clone.sh\nindex dff442da20..dba4246100 100755\n--- a/t/t0410-partial-clone.sh\n+++ b/t/t0410-partial-clone.sh\n@@ -206,7 +206,7 @@ test_expect_success 'fetching of missing objects' '\n \tgit -C repo cat-file -p \"$HASH\" 2>err &&\n \n \t# Ensure that no spurious FETCH_HEAD messages are written\n-\t! grep FETCH_HEAD err &&\n+\ttest_grep ! FETCH_HEAD err &&\n \n \t# Ensure that the .promisor file is written, and check that its\n \t# associated packfile contains the object\n@@ -214,7 +214,7 @@ test_expect_success 'fetching of missing objects' '\n \ttest_line_count = 1 promisorlist &&\n \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n \tgit verify-pack --verbose \"$IDX\" >out &&\n-\tgrep \"$HASH\" out\n+\ttest_grep \"$HASH\" out\n '\n \n test_expect_success 'fetching of a promised object that promisor remote no longer has' '\n@@ -228,7 +228,7 @@ test_expect_success 'fetching of a promised object that promisor remote no longe\n \n \trm -rf unreliable-server/.git/objects/* &&\n \ttest_must_fail git -C unreliable-client checkout HEAD 2>err &&\n-\tgrep \"could not fetch.*from promisor remote\" err\n+\ttest_grep \"could not fetch.*from promisor remote\" err\n '\n \n test_expect_success 'fetching of missing objects works with ref-in-want enabled' '\n@@ -240,7 +240,7 @@ test_expect_success 'fetching of missing objects works with ref-in-want enabled'\n \trm -rf repo/.git/objects/* &&\n \trm -f trace &&\n \tGIT_TRACE_PACKET=\"$(pwd)/trace\" git -C repo cat-file -p \"$HASH\" &&\n-\tgrep \"fetch< fetch=.*ref-in-want\" trace\n+\ttest_grep \"fetch< fetch=.*ref-in-want\" trace\n '\n \n test_expect_success 'fetching from another promisor remote' '\n@@ -263,7 +263,7 @@ test_expect_success 'fetching from another promisor remote' '\n \ttest_line_count = 1 promisorlist &&\n \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n \tgit verify-pack --verbose \"$IDX\" >out &&\n-\tgrep \"$HASH2\" out\n+\ttest_grep \"$HASH2\" out\n '\n \n test_expect_success 'fetching with --filter configures a promisor remote' '\n@@ -286,7 +286,7 @@ test_expect_success 'fetching with --filter configures a promisor remote' '\n \ttest_line_count = 1 promisorlist &&\n \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n \tgit verify-pack --verbose \"$IDX\" >out &&\n-\tgrep \"$HASH3\" out\n+\ttest_grep \"$HASH3\" out\n '\n \n test_expect_success 'fetching of missing blobs works' '\n@@ -327,8 +327,8 @@ test_expect_success 'fetching of missing trees does not fetch blobs' '\n \n \t# Ensure that the tree, but not the blob, is fetched\n \tgit -C repo rev-list --objects --missing=print $(cat treehash) >objects &&\n-\tgrep \"^$(cat treehash)\" objects &&\n-\tgrep \"^[?]$(cat blobhash)\" objects\n+\ttest_grep \"^$(cat treehash)\" objects &&\n+\ttest_grep \"^[?]$(cat blobhash)\" objects\n '\n \n test_expect_success 'rev-list stops traversal at missing and promised commit' '\n@@ -343,8 +343,8 @@ test_expect_success 'rev-list stops traversal at missing and promised commit' '\n \tgit -C repo config core.repositoryformatversion 1 &&\n \tgit -C repo config extensions.partialclone \"arbitrary string\" &&\n \tgit -C repo rev-list --exclude-promisor-objects --objects bar >out &&\n-\tgrep $(git -C repo rev-parse bar) out &&\n-\t! grep $FOO out\n+\ttest_grep $(git -C repo rev-parse bar) out &&\n+\ttest_grep ! $FOO out\n '\n \n test_expect_success 'missing tree objects with --missing=allow-promisor and --exclude-promisor-objects' '\n@@ -413,10 +413,10 @@ test_expect_success 'rev-list stops traversal at missing and promised tree' '\n \tgit -C repo config core.repositoryformatversion 1 &&\n \tgit -C repo config extensions.partialclone \"arbitrary string\" &&\n \tgit -C repo rev-list --exclude-promisor-objects --objects HEAD >out &&\n-\tgrep $(git -C repo rev-parse foo) out &&\n-\t! grep $TREE out &&\n-\tgrep $(git -C repo rev-parse HEAD) out &&\n-\t! grep $TREE2 out\n+\ttest_grep $(git -C repo rev-parse foo) out &&\n+\ttest_grep ! $TREE out &&\n+\ttest_grep $(git -C repo rev-parse HEAD) out &&\n+\ttest_grep ! $TREE2 out\n '\n \n test_expect_success 'rev-list stops traversal at missing and promised blob' '\n@@ -432,8 +432,8 @@ test_expect_success 'rev-list stops traversal at missing and promised blob' '\n \tgit -C repo config core.repositoryformatversion 1 &&\n \tgit -C repo config extensions.partialclone \"arbitrary string\" &&\n \tgit -C repo rev-list --exclude-promisor-objects --objects HEAD >out &&\n-\tgrep $(git -C repo rev-parse HEAD) out &&\n-\t! grep $BLOB out\n+\ttest_grep $(git -C repo rev-parse HEAD) out &&\n+\ttest_grep ! $BLOB out\n '\n \n test_expect_success 'rev-list stops traversal at promisor commit, tree, and blob' '\n@@ -451,10 +451,10 @@ test_expect_success 'rev-list stops traversal at promisor commit, tree, and blob\n \tgit -C repo config core.repositoryformatversion 1 &&\n \tgit -C repo config extensions.partialclone \"arbitrary string\" &&\n \tgit -C repo rev-list --exclude-promisor-objects --objects HEAD >out &&\n-\t! grep $COMMIT out &&\n-\t! grep $TREE out &&\n-\t! grep $BLOB out &&\n-\tgrep $(git -C repo rev-parse bar) out  # sanity check that some walking was done\n+\ttest_grep ! $COMMIT out &&\n+\ttest_grep ! $TREE out &&\n+\ttest_grep ! $BLOB out &&\n+\ttest_grep $(git -C repo rev-parse bar) out  # sanity check that some walking was done\n '\n \n test_expect_success 'rev-list dies for missing objects on cmd line' '\n@@ -523,10 +523,10 @@ test_expect_success 'gc repacks promisor objects separately from non-promisor ob\n \ttest_line_count = 1 promisorlist &&\n \tPROMISOR_PACKFILE=$(sed \"s/.promisor/.pack/\" <promisorlist) &&\n \tgit verify-pack $PROMISOR_PACKFILE -v >out &&\n-\tgrep \"$TREE_ONE\" out &&\n-\tgrep \"$TREE_TWO\" out &&\n-\t! grep \"$(git -C repo rev-parse one)\" out &&\n-\t! grep \"$(git -C repo rev-parse two)\" out &&\n+\ttest_grep \"$TREE_ONE\" out &&\n+\ttest_grep \"$TREE_TWO\" out &&\n+\ttest_grep ! \"$(git -C repo rev-parse one)\" out &&\n+\ttest_grep ! \"$(git -C repo rev-parse two)\" out &&\n \n \t# Remove the promisor packfile and associated files\n \trm $(sed \"s/.promisor//\" <promisorlist).* &&\n@@ -536,10 +536,10 @@ test_expect_success 'gc repacks promisor objects separately from non-promisor ob\n \tls repo/.git/objects/pack/pack-*.pack >packlist &&\n \ttest_line_count = 1 packlist &&\n \tgit verify-pack repo/.git/objects/pack/pack-*.pack -v >out &&\n-\tgrep \"$(git -C repo rev-parse one)\" out &&\n-\tgrep \"$(git -C repo rev-parse two)\" out &&\n-\t! grep \"$TREE_ONE\" out &&\n-\t! grep \"$TREE_TWO\" out\n+\ttest_grep \"$(git -C repo rev-parse one)\" out &&\n+\ttest_grep \"$(git -C repo rev-parse two)\" out &&\n+\ttest_grep ! \"$TREE_ONE\" out &&\n+\ttest_grep ! \"$TREE_TWO\" out\n '\n \n test_expect_success 'gc does not repack promisor objects if there are none' '\n@@ -616,8 +616,8 @@ test_expect_success 'gc stops traversal when a missing but promised object is re\n \tls repo/.git/objects/pack/pack-*.pack >packlist &&\n \ttest_line_count = 1 packlist &&\n \tgit verify-pack repo/.git/objects/pack/pack-*.pack -v >out &&\n-\tgrep \"$(git -C repo rev-parse HEAD)\" out &&\n-\t! grep \"$TREE_HASH\" out\n+\ttest_grep \"$(git -C repo rev-parse HEAD)\" out &&\n+\ttest_grep ! \"$TREE_HASH\" out\n '\n \n test_expect_success 'do not fetch when checking existence of tree we construct ourselves' '\n@@ -647,10 +647,10 @@ test_expect_success 'exact rename does not need to fetch the blob lazily' '\n \n \tgit clone --filter=blob:none --bare \"file://$(pwd)/repo\" partial.git &&\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\tgrep \"[?]$FILE_HASH\" out &&\n+\ttest_grep \"[?]$FILE_HASH\" out &&\n \tgit -C partial.git log --follow -- new-file.txt &&\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\tgrep \"[?]$FILE_HASH\" out\n+\ttest_grep \"[?]$FILE_HASH\" out\n '\n \n test_expect_success 'lazy-fetch when accessing object not in the_repository' '\n@@ -665,7 +665,7 @@ test_expect_success 'lazy-fetch when accessing object not in the_repository' '\n \n \t# Sanity check that the file is missing\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\tgrep \"[?]$FILE_HASH\" out &&\n+\ttest_grep \"[?]$FILE_HASH\" out &&\n \n \t# The no-lazy-fetch mechanism prevents Git from fetching\n \ttest_must_fail env GIT_NO_LAZY_FETCH=1 \\\n@@ -680,7 +680,7 @@ test_expect_success 'lazy-fetch when accessing object not in the_repository' '\n \n \t# Sanity check that the file is still missing\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\tgrep \"[?]$FILE_HASH\" out &&\n+\ttest_grep \"[?]$FILE_HASH\" out &&\n \n \tgit -C full cat-file -s \"$FILE_HASH\" >expect &&\n \ttest-tool partial-clone object-info partial.git \"$FILE_HASH\" >actual &&\n@@ -688,7 +688,7 @@ test_expect_success 'lazy-fetch when accessing object not in the_repository' '\n \n \t# Sanity check that the file is now present\n \tgit -C partial.git rev-list --objects --missing=print HEAD >out &&\n-\t! grep \"[?]$FILE_HASH\" out\n+\ttest_grep ! \"[?]$FILE_HASH\" out\n '\n \n test_expect_success 'push should not fetch new commit objects' '\n@@ -705,9 +705,9 @@ test_expect_success 'push should not fetch new commit objects' '\n \tCOMMIT=$(git -C server rev-parse server2) &&\n \n \ttest_must_fail git -C client push 2>err &&\n-\tgrep \"fetch first\" err &&\n+\ttest_grep \"fetch first\" err &&\n \tgit -C client rev-list --objects --missing=print \"$COMMIT\" >objects &&\n-\tgrep \"^[?]$COMMIT\" objects\n+\ttest_grep \"^[?]$COMMIT\" objects\n '\n \n test_expect_success 'setup for promisor.quiet tests' '\n@@ -750,7 +750,7 @@ test_expect_success TTY 'promisor.quiet=false shows progress messages' '\n \ttest_terminal git -C repo cat-file -p foo:foo.t 2>err &&\n \n \t# Ensure that progress messages are written\n-\tgrep \"Receiving objects\" err\n+\ttest_grep \"Receiving objects\" err\n '\n \n test_expect_success TTY 'promisor.quiet=true does not show progress messages' '\n@@ -761,7 +761,7 @@ test_expect_success TTY 'promisor.quiet=true does not show progress messages' '\n \ttest_terminal git -C repo cat-file -p foo:foo.t 2>err &&\n \n \t# Ensure that no progress messages are written\n-\t! grep \"Receiving objects\" err\n+\ttest_grep ! \"Receiving objects\" err\n '\n \n test_expect_success TTY 'promisor.quiet=unconfigured shows progress messages' '\n@@ -771,7 +771,7 @@ test_expect_success TTY 'promisor.quiet=unconfigured shows progress messages' '\n \ttest_terminal git -C repo cat-file -p foo:foo.t 2>err &&\n \n \t# Ensure that progress messages are written\n-\tgrep \"Receiving objects\" err\n+\ttest_grep \"Receiving objects\" err\n '\n \n test_expect_success 'promisor.quiet from submodule repo is honored' '\n@@ -819,7 +819,7 @@ test_expect_success 'fetching of missing objects from an HTTP server' '\n \ttest_line_count = 1 promisorlist &&\n \tIDX=$(sed \"s/promisor$/idx/\" promisorlist) &&\n \tgit verify-pack --verbose \"$IDX\" >out &&\n-\tgrep \"$HASH\" out\n+\ttest_grep \"$HASH\" out\n '\n \n # DO NOT add non-httpd-specific tests here, because the last part of this\ndiff --git a/t/t0450-txt-doc-vs-help.sh b/t/t0450-txt-doc-vs-help.sh\nindex 822b0d55a5..e7ba967223 100755\n--- a/t/t0450-txt-doc-vs-help.sh\n+++ b/t/t0450-txt-doc-vs-help.sh\n@@ -87,7 +87,7 @@ do\n \t# -h output assertions\n \ttest_expect_success \"$builtin -h output has no \\t\" '\n \t\th2s=\"$(help_to_synopsis \"$builtin\")\" &&\n-\t\t! grep \"$HT\" \"$h2s\"\n+\t\ttest_grep ! \"$HT\" \"$h2s\"\n \t'\n \n \ttest_expect_success \"$builtin -h output has dashed labels\" '\ndiff --git a/t/t0500-progress-display.sh b/t/t0500-progress-display.sh\nindex d1a498a216..fe2232cce0 100755\n--- a/t/t0500-progress-display.sh\n+++ b/t/t0500-progress-display.sh\n@@ -320,8 +320,8 @@ test_expect_success 'progress generates traces' '\n \n \t# t0212/parse_events.perl intentionally omits regions and data.\n \ttest_region progress \"Working hard\" trace.event &&\n-\tgrep \"\\\"key\\\":\\\"total_objects\\\",\\\"value\\\":\\\"40\\\"\" trace.event &&\n-\tgrep \"\\\"key\\\":\\\"total_bytes\\\",\\\"value\\\":\\\"409600\\\"\" trace.event\n+\ttest_grep \"\\\"key\\\":\\\"total_objects\\\",\\\"value\\\":\\\"40\\\"\" trace.event &&\n+\ttest_grep \"\\\"key\\\":\\\"total_bytes\\\",\\\"value\\\":\\\"409600\\\"\" trace.event\n '\n \n test_expect_success 'progress generates traces: stop / start' '\n@@ -344,8 +344,8 @@ test_expect_success 'progress generates traces: start without stop' '\n \tLSAN_OPTIONS=detect_leaks=0 \\\n \ttest-tool progress \\\n \t\t<in 2>stderr &&\n-\tgrep region_enter.*progress trace-start.event &&\n-\t! grep region_leave.*progress trace-start.event\n+\ttest_grep region_enter.*progress trace-start.event &&\n+\ttest_grep ! region_leave.*progress trace-start.event\n '\n \n test_expect_success 'progress generates traces: stop without start' '\n@@ -355,8 +355,8 @@ test_expect_success 'progress generates traces: stop without start' '\n \n \tGIT_TRACE2_EVENT=\"$PWD/trace-stop.event\" test-tool progress \\\n \t\t<in 2>stderr &&\n-\t! grep region_enter.*progress trace-stop.event &&\n-\t! grep region_leave.*progress trace-stop.event\n+\ttest_grep ! region_enter.*progress trace-stop.event &&\n+\ttest_grep ! region_leave.*progress trace-stop.event\n '\n \n test_expect_success 'progress generates traces: start with active progress bar (no stops)' '\n@@ -369,9 +369,9 @@ test_expect_success 'progress generates traces: start with active progress bar (\n \tLSAN_OPTIONS=detect_leaks=0 \\\n \ttest-tool progress \\\n \t\t<in 2>stderr &&\n-\tgrep region_enter.*progress.*One trace-2start.event &&\n-\tgrep region_enter.*progress.*Two trace-2start.event &&\n-\t! grep region_leave trace-2start.event\n+\ttest_grep region_enter.*progress.*One trace-2start.event &&\n+\ttest_grep region_enter.*progress.*Two trace-2start.event &&\n+\ttest_grep ! region_leave trace-2start.event\n '\n \n test_done\ndiff --git a/t/t0610-reftable-basics.sh b/t/t0610-reftable-basics.sh\nindex e19e036898..2f4c5e2a1b 100755\n--- a/t/t0610-reftable-basics.sh\n+++ b/t/t0610-reftable-basics.sh\n@@ -776,11 +776,11 @@ test_expect_success 'reflog: can delete separate reflog entries' '\n \t\ttest_commit file3 &&\n \t\ttest_commit file4 &&\n \t\tgit reflog >actual &&\n-\t\tgrep file3 actual &&\n+\t\ttest_grep file3 actual &&\n \n \t\tgit reflog delete HEAD@{1} &&\n \t\tgit reflog >actual &&\n-\t\t! grep file3 actual\n+\t\ttest_grep ! file3 actual\n \t)\n '\n \n@@ -902,8 +902,8 @@ test_expect_success 'reflog: garbage collection deletes reflog entries' '\n \t\tdone &&\n \t\tgit reflog refs/heads/main >actual &&\n \t\ttest_line_count = 10 actual &&\n-\t\tgrep \"commit (initial): number 1\" actual &&\n-\t\tgrep \"commit: number 10\" actual &&\n+\t\ttest_grep \"commit (initial): number 1\" actual &&\n+\t\ttest_grep \"commit: number 10\" actual &&\n \n \t\tgit gc &&\n \t\tgit reflog refs/heads/main >actual &&\ndiff --git a/t/t1004-read-tree-m-u-wf.sh b/t/t1004-read-tree-m-u-wf.sh\nindex 11bf10424f..70ba3eea4d 100755\n--- a/t/t1004-read-tree-m-u-wf.sh\n+++ b/t/t1004-read-tree-m-u-wf.sh\n@@ -142,8 +142,8 @@ test_expect_success '3-way not overwriting local changes (our side)' '\n \n \techo >>file1 \"local changes\" &&\n \tread_tree_u_must_succeed -m -u branch-point side-a side-b &&\n-\tgrep \"new line to be kept\" file1 &&\n-\tgrep \"local changes\" file1\n+\ttest_grep \"new line to be kept\" file1 &&\n+\ttest_grep \"local changes\" file1\n \n '\n \n@@ -156,8 +156,8 @@ test_expect_success '3-way not overwriting local changes (their side)' '\n \n \techo >>file2 \"local changes\" &&\n \tread_tree_u_must_fail -m -u branch-point side-a side-b &&\n-\t! grep \"new line to be kept\" file2 &&\n-\tgrep \"local changes\" file2\n+\ttest_grep ! \"new line to be kept\" file2 &&\n+\ttest_grep \"local changes\" file2\n \n '\n \ndiff --git a/t/t1006-cat-file.sh b/t/t1006-cat-file.sh\nindex 8e2c52652c..762c77c351 100755\n--- a/t/t1006-cat-file.sh\n+++ b/t/t1006-cat-file.sh\n@@ -696,8 +696,8 @@ test_expect_success '%(deltabase) reports packed delta bases' '\n \tgit repack -ad &&\n \tgit cat-file --batch-check=\"%(deltabase)\" <blobs >actual &&\n \t{\n-\t\tgrep \"$(git rev-parse HEAD:foo)\" actual ||\n-\t\tgrep \"$(git rev-parse HEAD:foo-plus)\" actual\n+\t\ttest_grep \"$(git rev-parse HEAD:foo)\" actual ||\n+\t\ttest_grep \"$(git rev-parse HEAD:foo-plus)\" actual\n \t}\n '\n \n@@ -826,7 +826,7 @@ test_expect_success 'cat-file -t and -s on corrupt loose object' '\n \t\t# Swap the two to corrupt the repository\n \t\tmv -f \"$other_path\" \"$empty_path\" &&\n \t\ttest_must_fail git fsck 2>err.fsck &&\n-\t\tgrep \"hash-path mismatch\" err.fsck &&\n+\t\ttest_grep \"hash-path mismatch\" err.fsck &&\n \n \t\t# confirm that cat-file is reading the new swapped-in\n \t\t# blob...\n@@ -1318,37 +1318,37 @@ test_expect_success 'cat-file --batch-all-objects --batch-check ignores replace'\n test_expect_success 'batch-command empty command' '\n \techo \"\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*empty command in input.*\" err\n+\ttest_grep \"^fatal:.*empty command in input.*\" err\n '\n \n test_expect_success 'batch-command whitespace before command' '\n \techo \" info deadbeef\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*whitespace before command.*\" err\n+\ttest_grep \"^fatal:.*whitespace before command.*\" err\n '\n \n test_expect_success 'batch-command unknown command' '\n \techo unknown_command >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*unknown command.*\" err\n+\ttest_grep \"^fatal:.*unknown command.*\" err\n '\n \n test_expect_success 'batch-command missing arguments' '\n \techo \"info\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*info requires arguments.*\" err\n+\ttest_grep \"^fatal:.*info requires arguments.*\" err\n '\n \n test_expect_success 'batch-command flush with arguments' '\n \techo \"flush arg\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command --buffer <cmd 2>err &&\n-\tgrep \"^fatal:.*flush takes no arguments.*\" err\n+\ttest_grep \"^fatal:.*flush takes no arguments.*\" err\n '\n \n test_expect_success 'batch-command flush without --buffer' '\n \techo \"flush\" >cmd &&\n \ttest_expect_code 128 git cat-file --batch-command <cmd 2>err &&\n-\tgrep \"^fatal:.*flush is only for --buffer mode.*\" err\n+\ttest_grep \"^fatal:.*flush is only for --buffer mode.*\" err\n '\n \n perl_script='\ndiff --git a/t/t1007-hash-object.sh b/t/t1007-hash-object.sh\nindex de076293b6..4782a6246a 100755\n--- a/t/t1007-hash-object.sh\n+++ b/t/t1007-hash-object.sh\n@@ -202,7 +202,7 @@ done\n test_expect_success 'too-short tree' '\n \techo abc >malformed-tree &&\n \ttest_must_fail git hash-object -t tree malformed-tree 2>err &&\n-\tgrep \"too-short tree object\" err\n+\ttest_grep \"too-short tree object\" err\n '\n \n test_expect_success PERL_TEST_HELPERS 'malformed mode in tree' '\n@@ -210,7 +210,7 @@ test_expect_success PERL_TEST_HELPERS 'malformed mode in tree' '\n \tbin_oid=$(echo $hex_oid | hex2oct) &&\n \tprintf \"9100644 \\0$bin_oid\" >tree-with-malformed-mode &&\n \ttest_must_fail git hash-object -t tree tree-with-malformed-mode 2>err &&\n-\tgrep \"malformed mode in tree entry\" err\n+\ttest_grep \"malformed mode in tree entry\" err\n '\n \n test_expect_success PERL_TEST_HELPERS 'empty filename in tree' '\n@@ -218,7 +218,7 @@ test_expect_success PERL_TEST_HELPERS 'empty filename in tree' '\n \tbin_oid=$(echo $hex_oid | hex2oct) &&\n \tprintf \"100644 \\0$bin_oid\" >tree-with-empty-filename &&\n \ttest_must_fail git hash-object -t tree tree-with-empty-filename 2>err &&\n-\tgrep \"empty filename in tree entry\" err\n+\ttest_grep \"empty filename in tree entry\" err\n '\n \n test_expect_success PERL_TEST_HELPERS 'duplicate filename in tree' '\n@@ -229,7 +229,7 @@ test_expect_success PERL_TEST_HELPERS 'duplicate filename in tree' '\n \t\tprintf \"100644 file\\0$bin_oid\"\n \t} >tree-with-duplicate-filename &&\n \ttest_must_fail git hash-object -t tree tree-with-duplicate-filename 2>err &&\n-\tgrep \"duplicateEntries\" err\n+\ttest_grep \"duplicateEntries\" err\n '\n \n test_expect_success 'corrupt commit' '\ndiff --git a/t/t1011-read-tree-sparse-checkout.sh b/t/t1011-read-tree-sparse-checkout.sh\nindex 742f0fa909..93244ee134 100755\n--- a/t/t1011-read-tree-sparse-checkout.sh\n+++ b/t/t1011-read-tree-sparse-checkout.sh\n@@ -196,7 +196,7 @@ test_expect_success 'read-tree will not throw away dirty changes, non-sparse' '\n \techo dirty >init.t &&\n \tread_tree_u_must_fail -m -u HEAD^ &&\n \ttest_path_is_file init.t &&\n-\tgrep -q dirty init.t\n+\ttest_grep -q dirty init.t\n '\n \n test_expect_success 'read-tree will not throw away dirty changes, sparse' '\n@@ -207,7 +207,7 @@ test_expect_success 'read-tree will not throw away dirty changes, sparse' '\n \techo sub/added >.git/info/sparse-checkout &&\n \tread_tree_u_must_fail -m -u HEAD^ &&\n \ttest_path_is_file init.t &&\n-\tgrep -q dirty init.t\n+\ttest_grep -q dirty init.t\n '\n \n test_expect_success 'read-tree updates worktree, dirty case' '\n@@ -215,7 +215,7 @@ test_expect_success 'read-tree updates worktree, dirty case' '\n \tgit checkout -f top &&\n \techo dirty >init.t &&\n \tread_tree_u_must_fail -m -u HEAD^ &&\n-\tgrep -q dirty init.t &&\n+\ttest_grep -q dirty init.t &&\n \trm init.t\n '\n \n@@ -224,7 +224,7 @@ test_expect_success 'read-tree removes worktree, dirty case' '\n \tgit checkout -f top &&\n \techo dirty >added &&\n \tread_tree_u_must_succeed -m -u HEAD^ &&\n-\tgrep -q dirty added\n+\ttest_grep -q dirty added\n '\n \n test_expect_success 'read-tree adds to worktree, absent case' '\n@@ -240,7 +240,7 @@ test_expect_success 'read-tree adds to worktree, dirty case' '\n \tmkdir sub &&\n \techo dirty >sub/added &&\n \tread_tree_u_must_succeed -u -m HEAD^ &&\n-\tgrep -q dirty sub/added\n+\ttest_grep -q dirty sub/added\n '\n \n test_expect_success 'index removal and worktree narrowing at the same time' '\ndiff --git a/t/t1050-large.sh b/t/t1050-large.sh\nindex 7d40d08521..d295c265c7 100755\n--- a/t/t1050-large.sh\n+++ b/t/t1050-large.sh\n@@ -8,7 +8,7 @@ test_description='adding and checking out large blobs'\n test_expect_success 'core.bigFileThreshold must be non-negative' '\n \t: >input &&\n \ttest_must_fail git -c core.bigFileThreshold=-1 hash-object input >out 2>err &&\n-\tgrep \"bad numeric config value\" err &&\n+\ttest_grep \"bad numeric config value\" err &&\n \ttest_must_be_empty out\n '\n \n@@ -148,12 +148,12 @@ test_expect_success 'diff --stat' '\n \n test_expect_success 'diff' '\n \tgit diff HEAD^ HEAD >actual &&\n-\tgrep \"Binary files.*differ\" actual\n+\ttest_grep \"Binary files.*differ\" actual\n '\n \n test_expect_success 'diff --cached' '\n \tgit diff --cached HEAD^ >actual &&\n-\tgrep \"Binary files.*differ\" actual\n+\ttest_grep \"Binary files.*differ\" actual\n '\n \n test_expect_success 'hash-object' '\ndiff --git a/t/t1091-sparse-checkout-builtin.sh b/t/t1091-sparse-checkout-builtin.sh\nindex cd0aed9975..74b1761e0c 100755\n--- a/t/t1091-sparse-checkout-builtin.sh\n+++ b/t/t1091-sparse-checkout-builtin.sh\n@@ -129,7 +129,7 @@ test_expect_success 'switching to cone mode with non-cone mode patterns' '\n \t\tgit sparse-checkout add dir &&\n \t\tgit config --worktree core.sparseCheckoutCone true &&\n \t\ttest_must_fail git sparse-checkout add dir 2>err &&\n-\t\tgrep \"existing sparse-checkout patterns do not use cone mode\" err\n+\t\ttest_grep \"existing sparse-checkout patterns do not use cone mode\" err\n \t)\n '\n \n@@ -803,7 +803,7 @@ test_expect_success 'cone mode clears ignored subdirectories' '\n \t# When an untracked file is in the way, all untracked files\n \t# (even ignored files) are preserved.\n \tgit -C repo sparse-checkout set folder1 2>err &&\n-\tgrep \"contains untracked files\" err &&\n+\ttest_grep \"contains untracked files\" err &&\n \ttest_path_is_file repo/deep/deeper2/ignored.o &&\n \ttest_path_is_file repo/deep/deeper2/untracked &&\n \n@@ -882,8 +882,8 @@ test_expect_success 'malformed cone-mode patterns' '\n \t# of using the cone-mode translation to a set of directories.\n \tgit -C repo sparse-checkout list >actual 2>err &&\n \ttest_cmp repo/.git/info/sparse-checkout actual &&\n-\tgrep \"warning: your sparse-checkout file may have issues: pattern .* is repeated\" err &&\n-\tgrep \"warning: disabling cone pattern matching\" err\n+\ttest_grep \"warning: your sparse-checkout file may have issues: pattern .* is repeated\" err &&\n+\ttest_grep \"warning: disabling cone pattern matching\" err\n '\n \n test_expect_success 'set from subdir pays attention to prefix' '\n@@ -917,34 +917,34 @@ test_expect_success 'set from subdir in non-cone mode throws an error' '\n \tgit -C repo sparse-checkout disable &&\n \ttest_must_fail git -C repo/deep sparse-checkout set --no-cone deeper2 ../folder1 2>error &&\n \n-\tgrep \"run from the toplevel directory in non-cone mode\" error\n+\ttest_grep \"run from the toplevel directory in non-cone mode\" error\n '\n \n test_expect_success 'set from subdir in non-cone mode throws an error' '\n \tgit -C repo sparse-checkout set --no-cone deep/deeper2 &&\n \ttest_must_fail git -C repo/deep sparse-checkout add deeper1/deepest ../folder1 2>error &&\n \n-\tgrep \"run from the toplevel directory in non-cone mode\" error\n+\ttest_grep \"run from the toplevel directory in non-cone mode\" error\n '\n \n test_expect_success 'by default, cone mode will error out when passed files' '\n \tgit -C repo sparse-checkout reapply --cone &&\n \ttest_must_fail git -C repo sparse-checkout add .gitignore 2>error &&\n \n-\tgrep \".gitignore.*is not a directory\" error\n+\ttest_grep \".gitignore.*is not a directory\" error\n '\n \n test_expect_success 'error on mistyped command line options' '\n \ttest_must_fail git -C repo sparse-checkout add --sikp-checks .gitignore 2>error &&\n \n-\tgrep \"unknown option.*sikp-checks\" error\n+\ttest_grep \"unknown option.*sikp-checks\" error\n '\n \n test_expect_success 'by default, non-cone mode will warn on individual files' '\n \tgit -C repo sparse-checkout reapply --no-cone &&\n \tgit -C repo sparse-checkout add .gitignore 2>warning &&\n \n-\tgrep \"pass a leading slash before paths.*if you want a single file\" warning\n+\ttest_grep \"pass a leading slash before paths.*if you want a single file\" warning\n '\n \n test_expect_success 'setup bare repo' '\n@@ -1108,11 +1108,11 @@ test_expect_success 'clean' '\n \ttouch repo/folder1/extra/inside/file &&\n \n \ttest_must_fail git -C repo sparse-checkout clean 2>err &&\n-\tgrep \"refusing to clean\" err &&\n+\ttest_grep \"refusing to clean\" err &&\n \n \tgit -C repo config clean.requireForce true &&\n \ttest_must_fail git -C repo sparse-checkout clean 2>err &&\n-\tgrep \"refusing to clean\" err &&\n+\ttest_grep \"refusing to clean\" err &&\n \n \tcat >expect <<-\\EOF &&\n \tWould remove deep/deeper2/\n@@ -1255,7 +1255,7 @@ test_expect_success 'sparse-checkout operations with merge conflicts' '\n \t\ttest_must_fail git merge -m \"will-conflict\" right &&\n \n \t\ttest_must_fail git sparse-checkout clean -f 2>err &&\n-\t\tgrep \"failed to convert index to a sparse index\" err &&\n+\t\ttest_grep \"failed to convert index to a sparse index\" err &&\n \n \t\techo merged >folder1/even/more/dirs/file &&\n \t\tgit add --sparse folder1 &&\ndiff --git a/t/t1092-sparse-checkout-compatibility.sh b/t/t1092-sparse-checkout-compatibility.sh\nindex 8186da5c88..9814431cd7 100755\n--- a/t/t1092-sparse-checkout-compatibility.sh\n+++ b/t/t1092-sparse-checkout-compatibility.sh\n@@ -454,10 +454,10 @@ test_expect_success 'add outside sparse cone' '\n \trun_on_sparse ../edit-contents folder1/a &&\n \trun_on_sparse ../edit-contents folder1/newfile &&\n \ttest_sparse_match test_must_fail git add folder1/a &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/a &&\n \ttest_sparse_match test_must_fail git add folder1/newfile &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/newfile\n '\n \n@@ -509,13 +509,13 @@ test_expect_success 'status/add: outside sparse cone' '\n \n \t# Adding the path outside of the sparse-checkout cone should fail.\n \ttest_sparse_match test_must_fail git add folder1/a &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/a &&\n \ttest_all_match git add --refresh folder1/a &&\n \ttest_must_be_empty sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/a &&\n \ttest_sparse_match test_must_fail git add folder1/new &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/new &&\n \ttest_sparse_match git add --sparse folder1/a &&\n \ttest_sparse_match git add --sparse folder1/new &&\n@@ -661,8 +661,8 @@ test_expect_success 'checkout and reset (mixed)' '\n \t# in sparse-checkout or sparse-index.\n \tgit -C full-checkout reset update-folder1 >full-checkout-out &&\n \ttest_sparse_match git reset update-folder1 &&\n-\tgrep \"M\tfolder1/a\" full-checkout-out &&\n-\t! grep \"M\tfolder1/a\" sparse-checkout-out &&\n+\ttest_grep \"M\tfolder1/a\" full-checkout-out &&\n+\ttest_grep ! \"M\tfolder1/a\" sparse-checkout-out &&\n \trun_on_sparse test_path_is_missing folder1\n '\n \n@@ -880,8 +880,8 @@ test_expect_success 'update-index with directories' '\n \t# update-index will exit silently when provided with a directory name\n \t# containing a trailing slash\n \ttest_all_match git update-index deep/ folder1/ &&\n-\tgrep \"Ignoring path deep/\" sparse-checkout-err &&\n-\tgrep \"Ignoring path folder1/\" sparse-checkout-err &&\n+\ttest_grep \"Ignoring path deep/\" sparse-checkout-err &&\n+\ttest_grep \"Ignoring path folder1/\" sparse-checkout-err &&\n \n \t# When update-index is given a directory name WITHOUT a trailing slash, it will\n \t# behave in different ways depending on the status of the directory on disk:\n@@ -1067,7 +1067,7 @@ test_expect_success 'merge with conflict outside cone' '\n \n \t# 2. Add the file with conflict markers\n \ttest_sparse_match test_must_fail git add folder1/a &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder1/a &&\n \ttest_all_match git add --sparse folder1/a &&\n \ttest_all_match git status --porcelain=v2 &&\n@@ -1076,7 +1076,7 @@ test_expect_success 'merge with conflict outside cone' '\n \t#    accept conflict markers as resolved content.\n \trun_on_all mv folder2/a folder2/z &&\n \ttest_sparse_match test_must_fail git add folder2 &&\n-\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \ttest_sparse_unstaged folder2/z &&\n \ttest_all_match git add --sparse folder2 &&\n \ttest_all_match git status --porcelain=v2 &&\n@@ -1107,7 +1107,7 @@ test_expect_success 'cherry-pick/rebase with conflict outside cone' '\n \t\t# SKIP_WORKTREE bit from the index entry for folder1/a, we should\n \t\t# warn that this is a problematic add.\n \t\ttest_sparse_match test_must_fail git add folder1/a &&\n-\t\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\t\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \t\ttest_sparse_unstaged folder1/a &&\n \t\ttest_all_match git add --sparse folder1/a &&\n \t\ttest_all_match git status --porcelain=v2 &&\n@@ -1119,7 +1119,7 @@ test_expect_success 'cherry-pick/rebase with conflict outside cone' '\n \t\t# existing index entry with the SKIP_WORKTREE bit cleared.\n \t\trun_on_all mv folder2/a folder2/z &&\n \t\ttest_sparse_match test_must_fail git add folder2 &&\n-\t\tgrep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n+\t\ttest_grep \"Disable or modify the sparsity rules\" sparse-checkout-err &&\n \t\ttest_sparse_unstaged folder2/z &&\n \t\ttest_all_match git add --sparse folder2 &&\n \t\ttest_all_match git status --porcelain=v2 &&\n@@ -1266,7 +1266,7 @@ test_expect_success 'checkout-index with folders' '\n \trun_on_all test_must_fail git checkout-index -f -- folder1/ &&\n \ttest_cmp full-checkout-err sparse-checkout-err &&\n \t! test_cmp full-checkout-err sparse-index-err &&\n-\tgrep \"is a sparse directory\" sparse-index-err\n+\ttest_grep \"is a sparse directory\" sparse-index-err\n '\n \n test_expect_success 'checkout-index --all' '\n@@ -1374,8 +1374,8 @@ test_expect_success 'submodule handling' '\n \t# having a submodule prevents \"modules\" from collapse\n \ttest_sparse_match git sparse-checkout set deep/deeper1 &&\n \tgit -C sparse-index ls-files --sparse --stage >cache &&\n-\tgrep \"100644 .*\tmodules/a\" cache &&\n-\tgrep \"160000 $(git -C initial-repo rev-parse HEAD) 0\tmodules/sub\" cache\n+\ttest_grep \"100644 .*\tmodules/a\" cache &&\n+\ttest_grep \"160000 $(git -C initial-repo rev-parse HEAD) 0\tmodules/sub\" cache\n '\n \n test_expect_success 'git apply functionality' '\n@@ -1392,7 +1392,7 @@ test_expect_success 'git apply functionality' '\n \n \t# Apply a patch to a file outside the sparse definition\n \ttest_sparse_match test_must_fail git apply ../patch-outside &&\n-\tgrep \"No such file or directory\" sparse-checkout-err &&\n+\ttest_grep \"No such file or directory\" sparse-checkout-err &&\n \n \t# But it works with --index and --cached\n \ttest_all_match git apply --index --stat ../patch-outside &&\n@@ -2013,9 +2013,9 @@ test_expect_success 'mv directory from out-of-cone to in-cone' '\n \ttest_all_match git status --porcelain=v2 &&\n \ttest_sparse_match git ls-files -t &&\n \tgit -C sparse-checkout ls-files -t >actual &&\n-\tgrep -e \"H deep/folder1/0/0/0\" actual &&\n-\tgrep -e \"H deep/folder1/0/1\" actual &&\n-\tgrep -e \"H deep/folder1/a\" actual &&\n+\ttest_grep -e \"H deep/folder1/0/0/0\" actual &&\n+\ttest_grep -e \"H deep/folder1/0/1\" actual &&\n+\ttest_grep -e \"H deep/folder1/a\" actual &&\n \n \ttest_all_match git reset --hard &&\n \n@@ -2025,8 +2025,8 @@ test_expect_success 'mv directory from out-of-cone to in-cone' '\n \ttest_sparse_match git status --porcelain=v2 &&\n \ttest_sparse_match git ls-files -t &&\n \tgit -C sparse-checkout ls-files -t >actual &&\n-\tgrep -e \"H deep/0/0/0\" actual &&\n-\tgrep -e \"H deep/0/1\" actual\n+\ttest_grep -e \"H deep/0/0/0\" actual &&\n+\ttest_grep -e \"H deep/0/1\" actual\n '\n \n test_expect_success 'rm pathspec inside sparse definition' '\n@@ -2517,7 +2517,7 @@ test_expect_success 'advice.sparseIndexExpanded' '\n \tmkdir -p sparse-index/deep/deeper2/deepest &&\n \ttouch sparse-index/deep/deeper2/deepest/bogus &&\n \tgit -C sparse-index status 2>err &&\n-\tgrep \"The sparse index is expanding to a full index\" err &&\n+\ttest_grep \"The sparse index is expanding to a full index\" err &&\n \n \tgit -C sparse-index sparse-checkout disable 2>err &&\n \ttest_line_count = 0 err\ndiff --git a/t/t1300-config.sh b/t/t1300-config.sh\nindex 87ca11a127..b99f782d5d 100755\n--- a/t/t1300-config.sh\n+++ b/t/t1300-config.sh\n@@ -856,7 +856,7 @@ test_expect_success 'renaming a section with an overly-long line' '\n \t\tprintf \"[a] g = h\\\\n\"\n \t} >y &&\n \ttest_must_fail git config ${mode_prefix}rename-section -f y a xyz 2>err &&\n-\tgrep \"refusing to work with overly long line in .y. on line 2\" err\n+\ttest_grep \"refusing to work with overly long line in .y. on line 2\" err\n '\n \n cat >> .git/config << EOF\n@@ -1671,9 +1671,9 @@ test_expect_success 'git --config-env=key=envvar support' '\n \n test_expect_success 'git --config-env with missing value' '\n \ttest_must_fail env ENVVAR=value git --config-env 2>error &&\n-\tgrep \"no config key given for --config-env\" error &&\n+\ttest_grep \"no config key given for --config-env\" error &&\n \ttest_must_fail env ENVVAR=value git --config-env config core.name 2>error &&\n-\tgrep \"invalid config format: config\" error\n+\ttest_grep \"invalid config format: config\" error\n '\n \n test_expect_success 'git --config-env fails with invalid parameters' '\n@@ -2104,7 +2104,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \tkey = true\n \tEOF\n \tgit config ${mode_unset} two.key &&\n-\t! grep two .git/config &&\n+\ttest_grep ! two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n \t[one]\n@@ -2124,7 +2124,7 @@ test_expect_success '--unset last key removes section (except if commented)' '\n \tQkey = true\n \tEOF\n \tgit config ${mode_unset} two.key &&\n-\tgrep two .git/config &&\n+\ttest_grep two .git/config &&\n \n \tq_to_tab >.git/config <<-\\EOF &&\n \t[one]\n@@ -2655,7 +2655,7 @@ test_expect_success '--type rejects unknown specifiers' '\n \n test_expect_success '--type=int requires at least one digit' '\n \ttest_must_fail git config --type int --default m some.key >out 2>error &&\n-\tgrep \"bad numeric config value\" error &&\n+\ttest_grep \"bad numeric config value\" error &&\n \ttest_must_be_empty out\n '\n \n@@ -2967,12 +2967,12 @@ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such\n \n \t# test with any Git command\n \ttest_must_fail git -C hasremoteurlTest status 2>err &&\n-\tgrep \"fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url\" err\n+\ttest_grep \"fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url\" err\n '\n \n test_expect_success 'negated mode causes failure' '\n \ttest_must_fail git config --no-get 2>err &&\n-\tgrep \"unknown option \\`no-get${SQ}\" err\n+\ttest_grep \"unknown option \\`no-get${SQ}\" err\n '\n \n test_expect_success 'specifying multiple modes causes failure' '\ndiff --git a/t/t1305-config-include.sh b/t/t1305-config-include.sh\nindex f3892578e4..f6115269f9 100755\n--- a/t/t1305-config-include.sh\n+++ b/t/t1305-config-include.sh\n@@ -353,7 +353,7 @@ test_expect_success 'include cycles are detected' '\n \tgit -C cycle --git-dir=. config include.path cycle &&\n \tgit config -f cycle/cycle include.path config &&\n \ttest_must_fail git -C cycle --git-dir=. config --get-all test.value 2>stderr &&\n-\tgrep \"exceeded maximum include depth\" stderr\n+\ttest_grep \"exceeded maximum include depth\" stderr\n '\n \n test_expect_success 'onbranch with unborn branch' '\ndiff --git a/t/t1308-config-set.sh b/t/t1308-config-set.sh\nindex e0e49053f0..de95161a83 100755\n--- a/t/t1308-config-set.sh\n+++ b/t/t1308-config-set.sh\n@@ -180,7 +180,7 @@ test_expect_success 'find integer if value is non parse-able' '\n \n test_expect_success 'non parse-able integer value during iteration' '\n \tcheck_config expect_code 128 git_config_int lamb.head 2>result &&\n-\tgrep \"fatal: bad numeric config value .* in file \\.git/config\" result\n+\ttest_grep \"fatal: bad numeric config value .* in file \\.git/config\" result\n '\n \n test_expect_success 'find bool value for the entered key' '\n@@ -302,7 +302,7 @@ test_expect_success 'proper error on directory \"files\"' '\n \techo \"Error (-1) reading configuration file a-directory.\" >expect &&\n \tmkdir a-directory &&\n \ttest_expect_code 2 test-tool config configset_get_value foo.bar a-directory 2>output &&\n-\tgrep \"^warning:\" output &&\n+\ttest_grep \"^warning:\" output &&\n \tgrep \"^Error\" output >actual &&\n \ttest_cmp expect actual\n '\n@@ -312,7 +312,7 @@ test_expect_success POSIXPERM,SANITY 'proper error on non-accessible files' '\n \ttest_when_finished \"chmod +r .git/config\" &&\n \techo \"Error (-1) reading configuration file .git/config.\" >expect &&\n \ttest_expect_code 2 test-tool config configset_get_value foo.bar .git/config 2>output &&\n-\tgrep \"^warning:\" output &&\n+\ttest_grep \"^warning:\" output &&\n \tgrep \"^Error\" output >actual &&\n \ttest_cmp expect actual\n '\ndiff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh\nindex f059d63e38..c3b02ae1d2 100755\n--- a/t/t1400-update-ref.sh\n+++ b/t/t1400-update-ref.sh\n@@ -92,7 +92,7 @@ test_expect_success \"deleting current branch adds message to HEAD's log\" '\n \tgit update-ref -m delete-$m -d $m &&\n \ttest_must_fail git show-ref --verify -q $m &&\n \ttest-tool ref-store main for-each-reflog-ent HEAD >actual &&\n-\tgrep \"delete-$m$\" actual\n+\ttest_grep \"delete-$m$\" actual\n '\n \n test_expect_success \"deleting by HEAD adds message to HEAD's log\" '\n@@ -102,7 +102,7 @@ test_expect_success \"deleting by HEAD adds message to HEAD's log\" '\n \tgit update-ref -m delete-by-head -d HEAD &&\n \ttest_must_fail git show-ref --verify -q $m &&\n \ttest-tool ref-store main for-each-reflog-ent HEAD >actual &&\n-\tgrep \"delete-by-head$\" actual\n+\ttest_grep \"delete-by-head$\" actual\n '\n \n test_expect_success 'update-ref does not create reflogs by default' '\n@@ -192,7 +192,10 @@ test_expect_success \"move $m (by HEAD)\" '\n test_expect_success \"delete $m (by HEAD) should remove both packed and loose $m\" '\n \ttest_when_finished \"git update-ref -d $m\" &&\n \tgit update-ref -d HEAD $B &&\n-\t! grep \"$m\" .git/packed-refs &&\n+\tif test_have_prereq REFFILES\n+\tthen\n+\t\ttest_grep ! \"$m\" .git/packed-refs\n+\tfi &&\n \ttest_must_fail git show-ref --verify -q $m\n '\n \n@@ -575,103 +578,103 @@ test_expect_success 'stdin works with no input' '\n test_expect_success 'stdin fails on empty line' '\n \techo \"\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: empty command in input\" err\n+\ttest_grep \"fatal: empty command in input\" err\n '\n \n test_expect_success 'stdin fails on only whitespace' '\n \techo \" \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command:  \" err\n+\ttest_grep \"fatal: whitespace before command:  \" err\n '\n \n test_expect_success 'stdin fails on leading whitespace' '\n \techo \" create $a $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command:  create $a $m\" err\n+\ttest_grep \"fatal: whitespace before command:  create $a $m\" err\n '\n \n test_expect_success 'stdin fails on unknown command' '\n \techo \"unknown $a\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: unknown $a\" err\n+\ttest_grep \"fatal: unknown command: unknown $a\" err\n '\n \n test_expect_success 'stdin fails on unbalanced quotes' '\n \techo \"create $a \\\"main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: badly quoted argument: \\\\\\\"main\" err\n+\ttest_grep \"fatal: badly quoted argument: \\\\\\\"main\" err\n '\n \n test_expect_success 'stdin fails on invalid escape' '\n \techo \"create $a \\\"ma\\zn\\\"\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: badly quoted argument: \\\\\\\"ma\\\\\\\\zn\\\\\\\"\" err\n+\ttest_grep \"fatal: badly quoted argument: \\\\\\\"ma\\\\\\\\zn\\\\\\\"\" err\n '\n \n test_expect_success 'stdin fails on junk after quoted argument' '\n \techo \"create \\\"$a\\\"main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: unexpected character after quoted argument: \\\\\\\"$a\\\\\\\"main\" err\n+\ttest_grep \"fatal: unexpected character after quoted argument: \\\\\\\"$a\\\\\\\"main\" err\n '\n \n test_expect_success 'stdin fails create with no ref' '\n \techo \"create \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create: missing <ref>\" err\n+\ttest_grep \"fatal: create: missing <ref>\" err\n '\n \n test_expect_success 'stdin fails create with no new value' '\n \techo \"create $a\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $a: missing <new-oid>\" err\n+\ttest_grep \"fatal: create $a: missing <new-oid>\" err\n '\n \n test_expect_success 'stdin fails create with too many arguments' '\n \techo \"create $a $m $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $a: extra input:  $m\" err\n+\ttest_grep \"fatal: create $a: extra input:  $m\" err\n '\n \n test_expect_success 'stdin fails update with no ref' '\n \techo \"update \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: update: missing <ref>\" err\n+\ttest_grep \"fatal: update: missing <ref>\" err\n '\n \n test_expect_success 'stdin fails update with no new value' '\n \techo \"update $a\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: missing <new-oid>\" err\n+\ttest_grep \"fatal: update $a: missing <new-oid>\" err\n '\n \n test_expect_success 'stdin fails update with too many arguments' '\n \techo \"update $a $m $m $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: extra input:  $m\" err\n+\ttest_grep \"fatal: update $a: extra input:  $m\" err\n '\n \n test_expect_success 'stdin fails delete with no ref' '\n \techo \"delete \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete: missing <ref>\" err\n+\ttest_grep \"fatal: delete: missing <ref>\" err\n '\n \n test_expect_success 'stdin fails delete with too many arguments' '\n \techo \"delete $a $m $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete $a: extra input:  $m\" err\n+\ttest_grep \"fatal: delete $a: extra input:  $m\" err\n '\n \n test_expect_success 'stdin fails verify with too many arguments' '\n \techo \"verify $a $m $m\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: verify $a: extra input:  $m\" err\n+\ttest_grep \"fatal: verify $a: extra input:  $m\" err\n '\n \n test_expect_success 'stdin fails option with unknown name' '\n \techo \"option unknown\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: option unknown: unknown\" err\n+\ttest_grep \"fatal: option unknown: unknown\" err\n '\n \n test_expect_success 'stdin fails with duplicate refs' '\n@@ -759,28 +762,28 @@ test_expect_success 'stdin create ref works with path with space to blob' '\n test_expect_success 'stdin update ref fails with wrong old value' '\n \techo \"update $c $m $m~1\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin update ref fails with bad old value' '\n \techo \"update $c $m does-not-exist\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $c: invalid <old-oid>: does-not-exist\" err &&\n+\ttest_grep \"fatal: update $c: invalid <old-oid>: does-not-exist\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin create ref fails with bad new value' '\n \techo \"create $c does-not-exist\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $c: invalid <new-oid>: does-not-exist\" err &&\n+\ttest_grep \"fatal: create $c: invalid <new-oid>: does-not-exist\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin create ref fails with zero new value' '\n \techo \"create $c \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $c: zero <new-oid>\" err &&\n+\ttest_grep \"fatal: create $c: zero <new-oid>\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n@@ -795,7 +798,7 @@ test_expect_success 'stdin update ref works with right old value' '\n test_expect_success 'stdin delete ref fails with wrong old value' '\n \techo \"delete $a $m~1\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$a'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$a'\"'\"'\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual\n@@ -804,7 +807,7 @@ test_expect_success 'stdin delete ref fails with wrong old value' '\n test_expect_success 'stdin delete ref fails with zero old value' '\n \techo \"delete $a \" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete $a: zero <old-oid>\" err &&\n+\ttest_grep \"fatal: delete $a: zero <old-oid>\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual\n@@ -965,7 +968,7 @@ test_expect_success 'stdin update refs fails with wrong old value' '\n \tupdate $c  ''\n \tEOF\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual &&\n@@ -998,123 +1001,123 @@ test_expect_success 'stdin -z works on empty input' '\n test_expect_success 'stdin -z fails on empty line' '\n \techo \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command: \" err\n+\ttest_grep \"fatal: whitespace before command: \" err\n '\n \n test_expect_success 'stdin -z fails on empty command' '\n \tprintf $F \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: empty command in input\" err\n+\ttest_grep \"fatal: empty command in input\" err\n '\n \n test_expect_success 'stdin -z fails on only whitespace' '\n \tprintf $F \" \" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command:  \" err\n+\ttest_grep \"fatal: whitespace before command:  \" err\n '\n \n test_expect_success 'stdin -z fails on leading whitespace' '\n \tprintf $F \" create $a\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: whitespace before command:  create $a\" err\n+\ttest_grep \"fatal: whitespace before command:  create $a\" err\n '\n \n test_expect_success 'stdin -z fails on unknown command' '\n \tprintf $F \"unknown $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: unknown $a\" err\n+\ttest_grep \"fatal: unknown command: unknown $a\" err\n '\n \n test_expect_success 'stdin -z fails create with no ref' '\n \tprintf $F \"create \" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: create: missing <ref>\" err\n+\ttest_grep \"fatal: create: missing <ref>\" err\n '\n \n test_expect_success 'stdin -z fails create with no new value' '\n \tprintf $F \"create $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $a: unexpected end of input when reading <new-oid>\" err\n+\ttest_grep \"fatal: create $a: unexpected end of input when reading <new-oid>\" err\n '\n \n test_expect_success 'stdin -z fails create with too many arguments' '\n \tprintf $F \"create $a\" \"$m\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: $m\" err\n+\ttest_grep \"fatal: unknown command: $m\" err\n '\n \n test_expect_success 'stdin -z fails update with no ref' '\n \tprintf $F \"update \" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update: missing <ref>\" err\n+\ttest_grep \"fatal: update: missing <ref>\" err\n '\n \n test_expect_success 'stdin -z fails update with too few args' '\n \tprintf $F \"update $a\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: unexpected end of input when reading <old-oid>\" err\n+\ttest_grep \"fatal: update $a: unexpected end of input when reading <old-oid>\" err\n '\n \n test_expect_success 'stdin -z emits warning with empty new value' '\n \tgit update-ref $a $m &&\n \tprintf $F \"update $a\" \"\" \"\" >stdin &&\n \tgit update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"warning: update $a: missing <new-oid>, treating as zero\" err &&\n+\ttest_grep \"warning: update $a: missing <new-oid>, treating as zero\" err &&\n \ttest_must_fail git rev-parse --verify -q $a\n '\n \n test_expect_success 'stdin -z fails update with no new value' '\n \tprintf $F \"update $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: unexpected end of input when reading <new-oid>\" err\n+\ttest_grep \"fatal: update $a: unexpected end of input when reading <new-oid>\" err\n '\n \n test_expect_success 'stdin -z fails update with no old value' '\n \tprintf $F \"update $a\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $a: unexpected end of input when reading <old-oid>\" err\n+\ttest_grep \"fatal: update $a: unexpected end of input when reading <old-oid>\" err\n '\n \n test_expect_success 'stdin -z fails update with too many arguments' '\n \tprintf $F \"update $a\" \"$m\" \"$m\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: $m\" err\n+\ttest_grep \"fatal: unknown command: $m\" err\n '\n \n test_expect_success 'stdin -z fails delete with no ref' '\n \tprintf $F \"delete \" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete: missing <ref>\" err\n+\ttest_grep \"fatal: delete: missing <ref>\" err\n '\n \n test_expect_success 'stdin -z fails delete with no old value' '\n \tprintf $F \"delete $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete $a: unexpected end of input when reading <old-oid>\" err\n+\ttest_grep \"fatal: delete $a: unexpected end of input when reading <old-oid>\" err\n '\n \n test_expect_success 'stdin -z fails delete with too many arguments' '\n \tprintf $F \"delete $a\" \"$m\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: $m\" err\n+\ttest_grep \"fatal: unknown command: $m\" err\n '\n \n test_expect_success 'stdin -z fails verify with too many arguments' '\n \tprintf $F \"verify $a\" \"$m\" \"$m\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: unknown command: $m\" err\n+\ttest_grep \"fatal: unknown command: $m\" err\n '\n \n test_expect_success 'stdin -z fails verify with no old value' '\n \tprintf $F \"verify $a\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: verify $a: unexpected end of input when reading <old-oid>\" err\n+\ttest_grep \"fatal: verify $a: unexpected end of input when reading <old-oid>\" err\n '\n \n test_expect_success 'stdin -z fails option with unknown name' '\n \tprintf $F \"option unknown\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: option unknown: unknown\" err\n+\ttest_grep \"fatal: option unknown: unknown\" err\n '\n \n test_expect_success 'stdin -z fails with duplicate refs' '\n@@ -1160,14 +1163,14 @@ test_expect_success 'stdin -z create ref works with path with space to blob' '\n test_expect_success 'stdin -z update ref fails with wrong old value' '\n \tprintf $F \"update $c\" \"$m\" \"$m~1\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin -z update ref fails with bad old value' '\n \tprintf $F \"update $c\" \"$m\" \"does-not-exist\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: update $c: invalid <old-oid>: does-not-exist\" err &&\n+\ttest_grep \"fatal: update $c: invalid <old-oid>: does-not-exist\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n@@ -1176,7 +1179,7 @@ test_expect_success 'stdin -z create ref fails when ref exists' '\n \tgit rev-parse \"$c\" >expect &&\n \tprintf $F \"create $c\" \"$m~1\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \tgit rev-parse \"$c\" >actual &&\n \ttest_cmp expect actual\n '\n@@ -1185,28 +1188,28 @@ test_expect_success 'stdin -z create ref fails with bad new value' '\n \tgit update-ref -d \"$c\" &&\n \tprintf $F \"create $c\" \"does-not-exist\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $c: invalid <new-oid>: does-not-exist\" err &&\n+\ttest_grep \"fatal: create $c: invalid <new-oid>: does-not-exist\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin -z create ref fails with empty new value' '\n \tprintf $F \"create $c\" \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: create $c: missing <new-oid>\" err &&\n+\ttest_grep \"fatal: create $c: missing <new-oid>\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin -z create ref fails with non commit object' '\n \tprintf $F \"create $c\" \"$(test_oid 001)\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: trying to write ref ${SQ}$c${SQ} with nonexistent object\" err &&\n+\ttest_grep \"fatal: trying to write ref ${SQ}$c${SQ} with nonexistent object\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n test_expect_success 'stdin -z update ref fails with non commit object' '\n \tprintf $F \"update $b\" \"$(test_oid 001)\" \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: trying to write ref ${SQ}$b${SQ} with nonexistent object\" err &&\n+\ttest_grep \"fatal: trying to write ref ${SQ}$b${SQ} with nonexistent object\" err &&\n \ttest_must_fail git rev-parse --verify -q $c\n '\n \n@@ -1221,7 +1224,7 @@ test_expect_success 'stdin -z update ref works with right old value' '\n test_expect_success 'stdin -z delete ref fails with wrong old value' '\n \tprintf $F \"delete $a\" \"$m~1\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$a'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$a'\"'\"'\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual\n@@ -1230,7 +1233,7 @@ test_expect_success 'stdin -z delete ref fails with wrong old value' '\n test_expect_success 'stdin -z delete ref fails with zero old value' '\n \tprintf $F \"delete $a\" \"$Z\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: delete $a: zero <old-oid>\" err &&\n+\ttest_grep \"fatal: delete $a: zero <old-oid>\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual\n@@ -1336,7 +1339,7 @@ test_expect_success 'stdin -z update refs fails with wrong old value' '\n \tgit update-ref $c $m &&\n \tprintf $F \"update $a\" \"$m\" \"$m\" \"update $b\" \"$m\" \"$m\" \"update $c\" \"$m\" \"$Z\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n+\ttest_grep \"fatal: cannot lock ref '\"'\"'$c'\"'\"'\" err &&\n \tgit rev-parse $m >expect &&\n \tgit rev-parse $a >actual &&\n \ttest_cmp expect actual &&\n@@ -1415,13 +1418,13 @@ test_expect_success 'handle per-worktree refs in refs/bisect' '\n \t\tcd worktree &&\n \t\tgit commit --allow-empty -m \"test commit\"  &&\n \t\tgit for-each-ref >for-each-ref.out &&\n-\t\t! grep refs/bisect for-each-ref.out &&\n+\t\ttest_grep ! refs/bisect for-each-ref.out &&\n \t\tgit update-ref refs/bisect/something HEAD &&\n \t\tgit rev-parse refs/bisect/something >../worktree-head &&\n \t\tgit for-each-ref | grep refs/bisect/something\n \t) &&\n \tgit show-ref >actual &&\n-\t! grep 'refs/bisect' actual &&\n+\ttest_grep ! 'refs/bisect' actual &&\n \ttest_must_fail git rev-parse refs/bisect/something &&\n \tgit update-ref refs/bisect/something HEAD &&\n \tgit rev-parse refs/bisect/something >main-head &&\n@@ -1477,7 +1480,7 @@ test_expect_success 'transaction exits on multiple aborts' '\n \ttest_must_fail git update-ref --stdin <stdin >actual 2>err &&\n \tprintf \"%s: ok\\n\" abort >expect &&\n \ttest_cmp expect actual &&\n-\tgrep \"fatal: transaction is closed\" err\n+\ttest_grep \"fatal: transaction is closed\" err\n '\n \n test_expect_success 'transaction exits on start after prepare' '\n@@ -1488,7 +1491,7 @@ test_expect_success 'transaction exits on start after prepare' '\n \ttest_must_fail git update-ref --stdin <stdin 2>err >actual &&\n \tprintf \"%s: ok\\n\" prepare >expect &&\n \ttest_cmp expect actual &&\n-\tgrep \"fatal: prepared transactions can only be closed\" err\n+\ttest_grep \"fatal: prepared transactions can only be closed\" err\n '\n \n test_expect_success 'transaction handles empty abort with missing prepare' '\n@@ -1649,7 +1652,7 @@ test_expect_success PIPE 'transaction flushes status updates' '\n \n \t# This must now fail given that we have locked the ref.\n \ttest_must_fail git update-ref refs/heads/flush $B 2>stderr &&\n-\tgrep \"fatal: update_ref failed for ref ${SQ}refs/heads/flush${SQ}: cannot lock ref\" stderr &&\n+\ttest_grep \"fatal: update_ref failed for ref ${SQ}refs/heads/flush${SQ}: cannot lock ref\" stderr &&\n \n \techo commit >&9 &&\n \techo \"commit: ok\" >expected &&\n@@ -1675,7 +1678,7 @@ do\n \t\tgit symbolic-ref refs/heads/symref $a &&\n \t\tformat_command $type \"symref-verify refs/heads/symref\" \"$a\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: symref-verify: cannot operate with deref mode\" err\n+\t\ttest_grep \"fatal: symref-verify: cannot operate with deref mode\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-verify fails with too many arguments\" '\n@@ -1683,9 +1686,9 @@ do\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err  &&\n \t\tif test \"$type\" = \"-z\"\n \t\tthen\n-\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\t\ttest_grep \"fatal: unknown command: $a\" err\n \t\telse\n-\t\t\tgrep \"fatal: symref-verify refs/heads/symref: extra input:  $a\" err\n+\t\t\ttest_grep \"fatal: symref-verify refs/heads/symref: extra input:  $a\" err\n \t\tfi\n \t'\n \n@@ -1718,7 +1721,7 @@ do\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >before &&\n \t\tformat_command $type \"symref-verify refs/heads/missing\" \"refs/heads/unknown\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/missing${SQ}: unable to resolve reference ${SQ}refs/heads/missing${SQ}\" err &&\n+\t\ttest_grep \"fatal: cannot lock ref ${SQ}refs/heads/missing${SQ}: unable to resolve reference ${SQ}refs/heads/missing${SQ}\" err &&\n \t\ttest_must_fail git rev-parse --verify -q refs/heads/missing &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >after &&\n \t\ttest_cmp before after\n@@ -1744,13 +1747,13 @@ do\n \t\tgit symbolic-ref refs/heads/symref $a &&\n \t\tformat_command $type \"symref-delete refs/heads/symref\" \"$a\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: symref-delete: cannot operate with deref mode\" err\n+\t\ttest_grep \"fatal: symref-delete: cannot operate with deref mode\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-delete fails with no ref\" '\n \t\tformat_command $type \"symref-delete \" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: symref-delete: missing <ref>\" err\n+\t\ttest_grep \"fatal: symref-delete: missing <ref>\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-delete fails deleting regular ref\" '\n@@ -1758,7 +1761,7 @@ do\n \t\tgit update-ref refs/heads/regularref $a &&\n \t\tformat_command $type \"symref-delete refs/heads/regularref\" \"$a\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: expected symref with target ${SQ}$a${SQ}: but is a regular ref\" err\n+\t\ttest_grep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: expected symref with target ${SQ}$a${SQ}: but is a regular ref\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-delete fails with too many arguments\" '\n@@ -1766,16 +1769,16 @@ do\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n \t\tif test \"$type\" = \"-z\"\n \t\tthen\n-\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\t\ttest_grep \"fatal: unknown command: $a\" err\n \t\telse\n-\t\t\tgrep \"fatal: symref-delete refs/heads/symref: extra input:  $a\" err\n+\t\t\ttest_grep \"fatal: symref-delete refs/heads/symref: extra input:  $a\" err\n \t\tfi\n \t'\n \n \ttest_expect_success \"stdin $type symref-delete fails with wrong old value\" '\n \t\tformat_command $type \"symref-delete refs/heads/symref\" \"$m\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main\" err &&\n+\t\ttest_grep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main\" err &&\n \t\tgit symbolic-ref refs/heads/symref >expect &&\n \t\techo $a >actual &&\n \t\ttest_cmp expect actual\n@@ -1813,9 +1816,9 @@ do\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n \t\tif test \"$type\" = \"-z\"\n \t\tthen\n-\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\t\ttest_grep \"fatal: unknown command: $a\" err\n \t\telse\n-\t\t\tgrep \"fatal: symref-create refs/heads/symref: extra input:  $a\" err\n+\t\t\ttest_grep \"fatal: symref-create refs/heads/symref: extra input:  $a\" err\n \t\tfi\n \t'\n \n@@ -1878,16 +1881,16 @@ do\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n \t\tif test \"$type\" = \"-z\"\n \t\tthen\n-\t\t\tgrep \"fatal: unknown command: $a\" err\n+\t\t\ttest_grep \"fatal: unknown command: $a\" err\n \t\telse\n-\t\t\tgrep \"fatal: symref-update refs/heads/symref: extra input:  $a\" err\n+\t\t\ttest_grep \"fatal: symref-update refs/heads/symref: extra input:  $a\" err\n \t\tfi\n \t'\n \n \ttest_expect_success \"stdin $type symref-update fails with wrong old value argument\" '\n \t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"foo\" \"$a\" \"$a\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: symref-update refs/heads/symref: invalid arg ${SQ}foo${SQ} for old value\" err\n+\t\ttest_grep \"fatal: symref-update refs/heads/symref: invalid arg ${SQ}foo${SQ} for old value\" err\n \t'\n \n \ttest_expect_success \"stdin $type symref-update creates with zero old value\" '\n@@ -1923,7 +1926,7 @@ do\n \t\tgit symbolic-ref refs/heads/symref $a &&\n \t\tformat_command $type \"symref-update refs/heads/symref\" \"$m\" \"ref\" \"$b\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type --no-deref <stdin 2>err &&\n-\t\tgrep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected $b\" err &&\n+\t\ttest_grep \"fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected $b\" err &&\n \t\ttest_must_fail git rev-parse --verify -q $c\n \t'\n \n@@ -1998,7 +2001,7 @@ do\n \t\tgit symbolic-ref --no-recurse refs/heads/symref >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&\n-\t\tgrep \"$Z $(git rev-parse $a)\" actual\n+\t\ttest_grep \"$Z $(git rev-parse $a)\" actual\n \t'\n \n \ttest_expect_success \"stdin $type symref-update regular ref to symref with correct old-oid\" '\n@@ -2010,7 +2013,7 @@ do\n \t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&\n-\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t\ttest_grep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n \t'\n \n \ttest_expect_success \"stdin $type symref-update regular ref to symref fails with wrong old-oid\" '\n@@ -2018,7 +2021,7 @@ do\n \t\tgit update-ref --no-deref refs/heads/regularref $a &&\n \t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"$(git rev-parse refs/heads/target2)\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: is at $(git rev-parse $a) but expected $(git rev-parse refs/heads/target2)\" err &&\n+\t\ttest_grep \"fatal: cannot lock ref ${SQ}refs/heads/regularref${SQ}: is at $(git rev-parse $a) but expected $(git rev-parse refs/heads/target2)\" err &&\n \t\techo $(git rev-parse $a) >expect &&\n \t\tgit rev-parse refs/heads/regularref >actual &&\n \t\ttest_cmp expect actual\n@@ -2029,7 +2032,7 @@ do\n \t\tgit update-ref --no-deref refs/heads/regularref $a &&\n \t\tformat_command $type \"symref-update refs/heads/regularref\" \"$a\" \"oid\" \"not-a-ref-oid\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: symref-update refs/heads/regularref: invalid oid: not-a-ref-oid\" err &&\n+\t\ttest_grep \"fatal: symref-update refs/heads/regularref: invalid oid: not-a-ref-oid\" err &&\n \t\techo $(git rev-parse $a) >expect &&\n \t\tgit rev-parse refs/heads/regularref >actual &&\n \t\ttest_cmp expect actual\n@@ -2040,7 +2043,7 @@ do\n \t\tgit symbolic-ref refs/heads/symref refs/heads/target2 &&\n \t\tformat_command $type \"symref-update refs/heads/symref\" \"$a\" \"oid\" \"$Z\" >stdin &&\n \t\ttest_must_fail git update-ref --stdin $type <stdin 2>err &&\n-\t\tgrep \"fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: reference already exists\" err &&\n+\t\ttest_grep \"fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: reference already exists\" err &&\n \t\techo refs/heads/target2 >expect &&\n \t\tgit symbolic-ref refs/heads/symref >actual &&\n \t\ttest_cmp expect actual\n@@ -2060,7 +2063,7 @@ do\n \t\tgit symbolic-ref --no-recurse refs/heads/symref >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/symref >actual &&\n-\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t\ttest_grep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n \t'\n \n \ttest_expect_success \"stdin $type symref-update regular ref to symref\" '\n@@ -2072,7 +2075,7 @@ do\n \t\tgit symbolic-ref --no-recurse refs/heads/regularref >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual &&\n-\t\tgrep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n+\t\ttest_grep \"$(git rev-parse $a) $(git rev-parse $a)\" actual\n \t'\n \n \ttest_expect_success \"stdin $type batch-updates\" '\ndiff --git a/t/t1403-show-ref.sh b/t/t1403-show-ref.sh\nindex 36c903ca19..7ce5af4ab8 100755\n--- a/t/t1403-show-ref.sh\n+++ b/t/t1403-show-ref.sh\n@@ -213,19 +213,19 @@ test_expect_success 'show-ref --verify with dangling ref' '\n \n test_expect_success 'show-ref sub-modes are mutually exclusive' '\n \ttest_must_fail git show-ref --verify --exclude-existing 2>err &&\n-\tgrep \"verify\" err &&\n-\tgrep \"exclude-existing\" err &&\n-\tgrep \"cannot be used together\" err &&\n+\ttest_grep \"verify\" err &&\n+\ttest_grep \"exclude-existing\" err &&\n+\ttest_grep \"cannot be used together\" err &&\n \n \ttest_must_fail git show-ref --verify --exists 2>err &&\n-\tgrep \"verify\" err &&\n-\tgrep \"exists\" err &&\n-\tgrep \"cannot be used together\" err &&\n+\ttest_grep \"verify\" err &&\n+\ttest_grep \"exists\" err &&\n+\ttest_grep \"cannot be used together\" err &&\n \n \ttest_must_fail git show-ref --exclude-existing --exists 2>err &&\n-\tgrep \"exclude-existing\" err &&\n-\tgrep \"exists\" err &&\n-\tgrep \"cannot be used together\" err\n+\ttest_grep \"exclude-existing\" err &&\n+\ttest_grep \"exists\" err &&\n+\ttest_grep \"cannot be used together\" err\n '\n \n test_done\ndiff --git a/t/t1410-reflog.sh b/t/t1410-reflog.sh\nindex ce71f9a30a..81de1d40eb 100755\n--- a/t/t1410-reflog.sh\n+++ b/t/t1410-reflog.sh\n@@ -108,12 +108,12 @@ test_expect_success setup '\n \n test_expect_success 'correct usage on sub-command -h' '\n \ttest_expect_code 129 git reflog expire -h >err &&\n-\tgrep \"git reflog expire\" err\n+\ttest_grep \"git reflog expire\" err\n '\n \n test_expect_success 'correct usage on \"git reflog show -h\"' '\n \ttest_expect_code 129 git reflog show -h >err &&\n-\tgrep -F \"git reflog [show]\" err\n+\ttest_grep -F \"git reflog [show]\" err\n '\n \n test_expect_success 'pass through -- to sub-command' '\ndiff --git a/t/t1415-worktree-refs.sh b/t/t1415-worktree-refs.sh\nindex 51d79bae83..6b2ad04aef 100755\n--- a/t/t1415-worktree-refs.sh\n+++ b/t/t1415-worktree-refs.sh\n@@ -32,7 +32,7 @@ test_expect_success 'ambiguous main-worktree/HEAD' '\n \ttest_when_finished git update-ref -d refs/heads/main-worktree/HEAD &&\n \tgit update-ref refs/heads/main-worktree/HEAD $(git rev-parse HEAD) &&\n \tgit rev-parse main-worktree/HEAD 2>warn &&\n-\tgrep \"main-worktree/HEAD.*ambiguous\" warn\n+\ttest_grep \"main-worktree/HEAD.*ambiguous\" warn\n '\n \n test_expect_success 'resolve worktrees/xx/HEAD' '\n@@ -45,7 +45,7 @@ test_expect_success 'ambiguous worktrees/xx/HEAD' '\n \tgit update-ref refs/heads/worktrees/wt1/HEAD $(git rev-parse HEAD) &&\n \ttest_when_finished git update-ref -d refs/heads/worktrees/wt1/HEAD &&\n \tgit rev-parse worktrees/wt1/HEAD 2>warn &&\n-\tgrep \"worktrees/wt1/HEAD.*ambiguous\" warn\n+\ttest_grep \"worktrees/wt1/HEAD.*ambiguous\" warn\n '\n \n test_expect_success 'reflog of main-worktree/HEAD' '\ndiff --git a/t/t1430-bad-ref-name.sh b/t/t1430-bad-ref-name.sh\nindex 3ab65f72cd..1ed4c7d2c2 100755\n--- a/t/t1430-bad-ref-name.sh\n+++ b/t/t1430-bad-ref-name.sh\n@@ -47,7 +47,7 @@ test_expect_success 'git branch shows badly named ref as warning' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \tgit branch >output 2>error &&\n \ttest_grep -e \"ignoring ref with broken name refs/heads/broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -d can delete badly named ref' '\n@@ -55,8 +55,8 @@ test_expect_success 'branch -d can delete badly named ref' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \tgit branch -d broken...ref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -D can delete badly named ref' '\n@@ -64,8 +64,8 @@ test_expect_success 'branch -D can delete badly named ref' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \tgit branch -D broken...ref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -D cannot delete non-ref in .git dir' '\n@@ -93,8 +93,8 @@ test_expect_success 'git branch cannot create a badly named ref' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \ttest_must_fail git branch broken...ref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -m cannot rename to a bad ref name' '\n@@ -104,8 +104,8 @@ test_expect_success 'branch -m cannot rename to a bad ref name' '\n \ttest_must_fail git branch -m goodref broken...ref &&\n \ttest_cmp_rev main goodref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_failure 'branch -m can rename from a bad ref name' '\n@@ -115,16 +115,16 @@ test_expect_failure 'branch -m can rename from a bad ref name' '\n \tgit branch -m broken...ref renamed &&\n \ttest_cmp_rev main renamed &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'push cannot create a badly named ref' '\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...ref\" &&\n \ttest_must_fail git push \"file://$(pwd)\" HEAD:refs/heads/broken...ref &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_failure 'push --mirror can delete badly named ref' '\n@@ -144,8 +144,8 @@ test_expect_failure 'push --mirror can delete badly named ref' '\n \t) &&\n \tgit -C src push --mirror \"file://$top/dest\" &&\n \tgit -C dest branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'rev-parse skips symref pointing to broken name' '\n@@ -168,9 +168,9 @@ test_expect_success 'for-each-ref emits warnings for broken names' '\n \ttest-tool ref-store main create-symref refs/heads/broken...symref refs/heads/main &&\n \ttest_when_finished \"test-tool ref-store main delete-refs REF_NO_DEREF msg refs/heads/broken...symref\" &&\n \tgit for-each-ref >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output &&\n-\t! grep -e \"badname\" output &&\n-\t! grep -e \"broken\\.\\.\\.symref\" output &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output &&\n+\ttest_grep ! -e \"badname\" output &&\n+\ttest_grep ! -e \"broken\\.\\.\\.symref\" output &&\n \ttest_grep \"ignoring ref with broken name refs/heads/broken\\.\\.\\.ref\" error &&\n \ttest_grep ! \"ignoring broken ref refs/heads/badname\" error &&\n \ttest_grep \"ignoring ref with broken name refs/heads/broken\\.\\.\\.symref\" error\n@@ -183,8 +183,8 @@ test_expect_success 'update-ref -d can delete broken name' '\n \ttest_must_be_empty output &&\n \ttest_must_be_empty error &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'branch -d can delete broken name' '\n@@ -194,8 +194,8 @@ test_expect_success 'branch -d can delete broken name' '\n \ttest_grep \"Deleted branch broken...ref (was broken)\" output &&\n \ttest_must_be_empty error &&\n \tgit branch >output 2>error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" error &&\n-\t! grep -e \"broken\\.\\.\\.ref\" output\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" error &&\n+\ttest_grep ! -e \"broken\\.\\.\\.ref\" output\n '\n \n test_expect_success 'update-ref --no-deref -d can delete symref to broken name' '\n@@ -313,37 +313,37 @@ test_expect_success 'update-ref -d cannot delete absolute path' '\n test_expect_success 'update-ref --stdin fails create with bad ref name' '\n \techo \"create ~a refs/heads/main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'update-ref --stdin fails update with bad ref name' '\n \techo \"update ~a refs/heads/main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'update-ref --stdin fails delete with bad ref name' '\n \techo \"delete ~a refs/heads/main\" >stdin &&\n \ttest_must_fail git update-ref --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'update-ref --stdin -z fails create with bad ref name' '\n \tprintf \"%s\\0\" \"create ~a \" refs/heads/main >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a \" err\n+\ttest_grep \"fatal: invalid ref format: ~a \" err\n '\n \n test_expect_success 'update-ref --stdin -z fails update with bad ref name' '\n \tprintf \"%s\\0\" \"update ~a\" refs/heads/main \"\" >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'update-ref --stdin -z fails delete with bad ref name' '\n \tprintf \"%s\\0\" \"delete ~a\" refs/heads/main >stdin &&\n \ttest_must_fail git update-ref -z --stdin <stdin 2>err &&\n-\tgrep \"fatal: invalid ref format: ~a\" err\n+\ttest_grep \"fatal: invalid ref format: ~a\" err\n '\n \n test_expect_success 'branch rejects HEAD as a branch name' '\ndiff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh\nindex 54e81c2636..47d460a699 100755\n--- a/t/t1450-fsck.sh\n+++ b/t/t1450-fsck.sh\n@@ -68,7 +68,7 @@ test_expect_success 'object with hash mismatch' '\n \t\tgit update-ref refs/heads/bogus $cmt &&\n \n \t\ttest_must_fail git fsck 2>out &&\n-\t\tgrep \"$oldoid: hash-path mismatch, found at: .*$new\" out\n+\t\ttest_grep \"$oldoid: hash-path mismatch, found at: .*$new\" out\n \t)\n '\n \n@@ -172,7 +172,7 @@ test_expect_success 'commit with multiple signatures is okay' '\n \ttest_when_finished \"git update-ref -d refs/heads/bogus\" &&\n \tgit fsck 2>out &&\n \tcat out &&\n-\t! grep \"commit $new\" out\n+\ttest_grep ! \"commit $new\" out\n '\n \n test_expect_success 'email without @ is okay' '\n@@ -183,7 +183,7 @@ test_expect_success 'email without @ is okay' '\n \tgit update-ref refs/heads/bogus \"$new\" &&\n \ttest_when_finished \"git update-ref -d refs/heads/bogus\" &&\n \tgit fsck 2>out &&\n-\t! grep \"commit $new\" out\n+\ttest_grep ! \"commit $new\" out\n '\n \n test_expect_success 'email with embedded > is not okay' '\n@@ -626,7 +626,7 @@ test_expect_success 'fsck notices excessively large tree entry name' '\n \t\tcd large-name &&\n \t\ttest_commit a-long-name &&\n \t\tgit -c fsck.largePathname=warn:10 fsck 2>out &&\n-\t\tgrep \"warning.*large pathname\" out\n+\t\ttest_grep \"warning.*large pathname\" out\n \t)\n '\n \n@@ -849,7 +849,7 @@ test_expect_success 'fsck errors in packed objects' '\n \ttest_must_fail git fsck 2>out &&\n \ttest_grep \"error in commit $one.* - bad name\" out &&\n \ttest_grep \"error in commit $two.* - bad name\" out &&\n-\t! grep corrupt out\n+\ttest_grep ! corrupt out\n '\n \n test_expect_success 'fsck handles multiple packfiles with big blobs' '\n@@ -1027,7 +1027,7 @@ test_expect_success 'bogus head does not fallback to all heads' '\n \ttest_when_finished \"git rm --cached foo\" &&\n \tremove_object $blob &&\n \ttest_must_fail git fsck $ZERO_OID >out 2>&1 &&\n-\t! grep $blob out\n+\ttest_grep ! $blob out\n '\n \n # Corrupt the checksum on the index.\ndiff --git a/t/t1451-fsck-buffer.sh b/t/t1451-fsck-buffer.sh\nindex 3a3d33f405..14151eabbd 100755\n--- a/t/t1451-fsck-buffer.sh\n+++ b/t/t1451-fsck-buffer.sh\n@@ -46,7 +46,7 @@ check () {\n \t\t\techo \"$content\"\n \t\t} >input &&\n \t\ttest_must_fail git hash-object -t \"$type\" input 2>err &&\n-\t\tgrep \"$fsck\" err\n+\t\ttest_grep \"$fsck\" err\n \t'\n }\n \n@@ -125,7 +125,7 @@ ident_checks tag tagger\n test_expect_success 'truncated tree (short hash)' '\n \tprintf \"100644 foo\\0\\1\\1\\1\\1\" >input &&\n \ttest_must_fail git hash-object -t tree input 2>err &&\n-\tgrep badTree err\n+\ttest_grep badTree err\n '\n \n test_expect_success 'truncated tree (missing nul)' '\n@@ -135,7 +135,7 @@ test_expect_success 'truncated tree (missing nul)' '\n \t# parser does not walk past the end of the buffer).\n \tprintf \"100644 a long filename, or a hash with missing nul?\" >input &&\n \ttest_must_fail git hash-object -t tree input 2>err &&\n-\tgrep badTree err\n+\ttest_grep badTree err\n '\n \n test_done\ndiff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh\nindex 5246468024..8f42697143 100755\n--- a/t/t1460-refs-migrate.sh\n+++ b/t/t1460-refs-migrate.sh\n@@ -212,7 +212,7 @@ do\n \t\t\ttest_commit -C repo initial &&\n \t\t\tgit -C repo refs migrate --dry-run \\\n \t\t\t\t--ref-format=$to_format >output &&\n-\t\t\tgrep \"Finished dry-run migration of refs\" output &&\n+\t\t\ttest_grep \"Finished dry-run migration of refs\" output &&\n \t\t\ttest_path_is_dir repo/.git/ref_migration.* &&\n \t\t\techo $from_format >expect &&\n \t\t\tgit -C repo rev-parse --show-ref-format >actual &&\ndiff --git a/t/t1500-rev-parse.sh b/t/t1500-rev-parse.sh\nindex 38067d95f7..4174ca40c3 100755\n--- a/t/t1500-rev-parse.sh\n+++ b/t/t1500-rev-parse.sh\n@@ -204,7 +204,7 @@ test_expect_success 'rev-parse --show-object-format in repo' '\n \tgit rev-parse --show-object-format=output >actual &&\n \ttest_cmp expect actual &&\n \ttest_must_fail git rev-parse --show-object-format=squeamish-ossifrage 2>err &&\n-\tgrep \"unknown mode for --show-object-format: squeamish-ossifrage\" err\n+\ttest_grep \"unknown mode for --show-object-format: squeamish-ossifrage\" err\n '\n \n \n@@ -228,7 +228,7 @@ test_expect_success RUST 'rev-parse --show-object-format in repo with compat mod\n \t\tgit rev-parse --show-object-format=compat >actual &&\n \t\ttest_cmp expect actual &&\n \t\ttest_must_fail git rev-parse --show-object-format=squeamish-ossifrage 2>err &&\n-\t\tgrep \"unknown mode for --show-object-format: squeamish-ossifrage\" err\n+\t\ttest_grep \"unknown mode for --show-object-format: squeamish-ossifrage\" err\n \t) &&\n \tmkdir repo2 &&\n \t(\n@@ -254,7 +254,7 @@ test_expect_success 'rev-parse --show-ref-format with invalid storage' '\n \t\tcd repo &&\n \t\tgit config extensions.refstorage broken &&\n \t\ttest_must_fail git rev-parse --show-ref-format 2>err &&\n-\t\tgrep \"error: invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}broken${SQ}\" err\n+\t\ttest_grep \"error: invalid value for ${SQ}extensions.refstorage${SQ}: ${SQ}broken${SQ}\" err\n \t)\n '\n \ndiff --git a/t/t1502-rev-parse-parseopt.sh b/t/t1502-rev-parse-parseopt.sh\nindex 3962f1d288..2cab204376 100755\n--- a/t/t1502-rev-parse-parseopt.sh\n+++ b/t/t1502-rev-parse-parseopt.sh\n@@ -331,7 +331,7 @@ test_expect_success 'ambiguous: --no matches both --noble and --no-noble' '\n \tEOF\n \ttest_expect_code 129 env GIT_TEST_DISALLOW_ABBREVIATED_OPTIONS=false \\\n \tgit rev-parse --parseopt -- <spec 2>err --no &&\n-\tgrep \"error: ambiguous option: no (could be --noble or --no-noble)\" err\n+\ttest_grep \"error: ambiguous option: no (could be --noble or --no-noble)\" err\n '\n \n test_done\ndiff --git a/t/t1503-rev-parse-verify.sh b/t/t1503-rev-parse-verify.sh\nindex 75a708f9ba..87638a4a2c 100755\n--- a/t/t1503-rev-parse-verify.sh\n+++ b/t/t1503-rev-parse-verify.sh\n@@ -62,15 +62,15 @@ test_expect_success 'works with one good rev' '\n \n test_expect_success 'fails with any bad rev or many good revs' '\n \ttest_must_fail git rev-parse --verify 2>error &&\n-\tgrep \"single revision\" error &&\n+\ttest_grep \"single revision\" error &&\n \ttest_must_fail git rev-parse --verify foo 2>error &&\n-\tgrep \"single revision\" error &&\n+\ttest_grep \"single revision\" error &&\n \ttest_must_fail git rev-parse --verify HEAD bar 2>error &&\n-\tgrep \"single revision\" error &&\n+\ttest_grep \"single revision\" error &&\n \ttest_must_fail git rev-parse --verify baz HEAD 2>error &&\n-\tgrep \"single revision\" error &&\n+\ttest_grep \"single revision\" error &&\n \ttest_must_fail git rev-parse --verify $HASH2 HEAD 2>error &&\n-\tgrep \"single revision\" error\n+\ttest_grep \"single revision\" error\n '\n \n test_expect_success 'fails silently when using -q' '\ndiff --git a/t/t1510-repo-setup.sh b/t/t1510-repo-setup.sh\nindex bbfe05b8e4..d330b8df47 100755\n--- a/t/t1510-repo-setup.sh\n+++ b/t/t1510-repo-setup.sh\n@@ -604,7 +604,7 @@ test_expect_success '#20b/c: core.worktree and core.bare conflict' '\n \t\tcd 20b/.git &&\n \t\ttest_must_fail git status >/dev/null\n \t) 2>message &&\n-\tgrep \"core.bare and core.worktree\" message\n+\ttest_grep \"core.bare and core.worktree\" message\n '\n \n test_expect_success '#20d: core.worktree and core.bare OK when working tree not needed' '\n@@ -721,8 +721,8 @@ test_expect_success '#22.2: core.worktree and core.bare conflict' '\n \t\texport GIT_DIR &&\n \t\ttest_must_fail git status 2>result\n \t) &&\n-\tgrep \"core.bare and core.worktree\" 22/.git/result &&\n-\tgrep \"core.bare and core.worktree\" 22/result\n+\ttest_grep \"core.bare and core.worktree\" 22/.git/result &&\n+\ttest_grep \"core.bare and core.worktree\" 22/result\n '\n \n # Case #23: GIT_DIR + GIT_WORK_TREE(+core.worktree) suppresses bareness.\n@@ -767,7 +767,7 @@ test_expect_success '#28: core.worktree and core.bare conflict (gitfile case)' '\n \t\tcd 28 &&\n \t\ttest_must_fail git status\n \t) 2>message &&\n-\tgrep \"core.bare and core.worktree\" message\n+\ttest_grep \"core.bare and core.worktree\" message\n '\n \n # Case #29: GIT_WORK_TREE(+core.worktree) overrides core.bare (gitfile case).\n@@ -791,7 +791,7 @@ test_expect_success '#30: core.worktree and core.bare conflict (gitfile version)\n \t\tcd 30 &&\n \t\ttest_must_fail env GIT_DIR=.git git status 2>result\n \t) &&\n-\tgrep \"core.bare and core.worktree\" 30/result\n+\ttest_grep \"core.bare and core.worktree\" 30/result\n '\n \n # Case #31: GIT_DIR + GIT_WORK_TREE(+core.worktree) suppresses\ndiff --git a/t/t1512-rev-parse-disambiguation.sh b/t/t1512-rev-parse-disambiguation.sh\nindex 1a380a4184..0e6b42ec78 100755\n--- a/t/t1512-rev-parse-disambiguation.sh\n+++ b/t/t1512-rev-parse-disambiguation.sh\n@@ -378,7 +378,7 @@ test_expect_success 'ambiguous 40-hex ref' '\n \tVAL=$(git commit-tree $TREE </dev/null) &&\n \tgit update-ref refs/heads/$REF $VAL &&\n \ttest $(git rev-parse $REF 2>err) = $REF &&\n-\tgrep \"refname.*${REF}.*ambiguous\" err\n+\ttest_grep \"refname.*${REF}.*ambiguous\" err\n '\n \n test_expect_success 'ambiguous short sha1 ref' '\n@@ -387,7 +387,7 @@ test_expect_success 'ambiguous short sha1 ref' '\n \tVAL=$(git commit-tree $TREE </dev/null) &&\n \tgit update-ref refs/heads/$REF $VAL &&\n \ttest $(git rev-parse $REF 2>err) = $VAL &&\n-\tgrep \"refname.*${REF}.*ambiguous\" err\n+\ttest_grep \"refname.*${REF}.*ambiguous\" err\n '\n \n test_expect_success 'ambiguity errors are not repeated (raw)' '\ndiff --git a/t/t1515-rev-parse-outside-repo.sh b/t/t1515-rev-parse-outside-repo.sh\nindex 75e89c4b6e..2cb3c7cc9c 100755\n--- a/t/t1515-rev-parse-outside-repo.sh\n+++ b/t/t1515-rev-parse-outside-repo.sh\n@@ -32,7 +32,7 @@ test_expect_success 'rev-parse --local-env-vars' '\n \tgit rev-parse --local-env-vars >actual &&\n \t# we do not want to depend on the complete list here,\n \t# so just look for something plausible\n-\tgrep ^GIT_DIR actual\n+\ttest_grep ^GIT_DIR actual\n '\n \n test_expect_success 'rev-parse --resolve-git-dir' '\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 0132e772e4..9aae3ff074 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -79,7 +79,7 @@ test_expect_success 'git hook usage' '\n \ttest_expect_code 129 git hook run --unknown 2>err &&\n \ttest_expect_code 129 git hook list &&\n \ttest_expect_code 129 git hook list -h &&\n-\tgrep \"unknown option\" err\n+\ttest_grep \"unknown option\" err\n '\n \n test_expect_success 'git hook list: unknown hook name is rejected' '\n@@ -361,9 +361,9 @@ test_expect_success 'hook can be configured for multiple events' '\n \n \t# 'ghi' should be included in both 'pre-commit' and 'test-hook'\n \tgit hook list pre-commit >actual &&\n-\tgrep \"ghi\" actual &&\n+\ttest_grep \"ghi\" actual &&\n \tgit hook list --allow-unknown-hook-name test-hook >actual &&\n-\tgrep \"ghi\" actual\n+\ttest_grep \"ghi\" actual\n '\n \n test_expect_success 'git hook list shows hooks from the hookdir' '\n@@ -569,7 +569,7 @@ test_expect_success 'git hook run a hook with a bad shebang' '\n \t# TODO: We should emit the same (or at least a more similar)\n \t# error on MINGW (essentially Git for Windows) and all other\n \t# platforms.. See the OS-specific code in start_command()\n-\tgrep -E \"^(error|fatal): cannot (exec|spawn) .*bad-hooks/test-hook\" err\n+\ttest_grep -E \"^(error|fatal): cannot (exec|spawn) .*bad-hooks/test-hook\" err\n '\n \n test_expect_success 'stdin to hooks' '\n@@ -826,8 +826,8 @@ test_expect_success 'git hook run -j2 warns for hooks not marked parallel=true'\n \t# neither hook has parallel=true\n \n \tgit hook run --allow-unknown-hook-name -j2 test-hook >out 2>err &&\n-\tgrep \"hook .hook-1. is not marked as parallel=true\" err &&\n-\tgrep \"hook .hook-2. is not marked as parallel=true\" err\n+\ttest_grep \"hook .hook-1. is not marked as parallel=true\" err &&\n+\ttest_grep \"hook .hook-2. is not marked as parallel=true\" err\n '\n \n test_expect_success 'hook.jobs=1 config runs hooks in series' '\n@@ -1068,7 +1068,7 @@ test_expect_success 'hook.jobs=-1 resolves to online_cpus()' '\n \tcpus=$(test-tool online-cpus) &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.txt\" \\\n \t\tgit hook run --allow-unknown-hook-name test-hook >out 2>err &&\n-\tgrep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n+\ttest_grep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n '\n \n test_expect_success 'hook.<event>.jobs=-1 resolves to online_cpus()' '\n@@ -1081,7 +1081,7 @@ test_expect_success 'hook.<event>.jobs=-1 resolves to online_cpus()' '\n \tcpus=$(test-tool online-cpus) &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.txt\" \\\n \t\tgit hook run --allow-unknown-hook-name test-hook >out 2>err &&\n-\tgrep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n+\ttest_grep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n '\n \n test_expect_success 'git hook run -j-1 resolves to online_cpus()' '\n@@ -1092,7 +1092,7 @@ test_expect_success 'git hook run -j-1 resolves to online_cpus()' '\n \tcpus=$(test-tool online-cpus) &&\n \tGIT_TRACE2_EVENT=\"$(pwd)/trace.txt\" \\\n \t\tgit hook run --allow-unknown-hook-name -j-1 test-hook >out 2>err &&\n-\tgrep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n+\ttest_grep \"\\\"region_enter\\\".*\\\"hook\\\".*\\\"test-hook\\\".*\\\"max:$cpus\\\"\" trace.txt\n '\n \n test_expect_success 'hook.jobs rejects values less than -1' '\ndiff --git a/t/t2004-checkout-cache-temp.sh b/t/t2004-checkout-cache-temp.sh\nindex 0afe0ff7ca..db06c321ed 100755\n--- a/t/t2004-checkout-cache-temp.sh\n+++ b/t/t2004-checkout-cache-temp.sh\n@@ -132,8 +132,8 @@ test_expect_success 'overriding --stage=all resets implied --temp' '\n test_expect_success '--stage=all --no-temp is rejected' '\n \trm -f path* .merge_* actual &&\n \ttest_must_fail git checkout-index --stage=all --no-temp -- path1 2>err &&\n-\tgrep -v \"already exists\" err &&\n-\tgrep \"options .--stage=all. and .--no-temp. cannot be used together\" err\n+\ttest_grep -v \"already exists\" err &&\n+\ttest_grep \"options .--stage=all. and .--no-temp. cannot be used together\" err\n '\n \n test_expect_success 'checkout some stages/one file to temporary files' '\ndiff --git a/t/t2019-checkout-ambiguous-ref.sh b/t/t2019-checkout-ambiguous-ref.sh\nindex 1fcef4be95..0ac35798c2 100755\n--- a/t/t2019-checkout-ambiguous-ref.sh\n+++ b/t/t2019-checkout-ambiguous-ref.sh\n@@ -19,7 +19,7 @@ test_expect_success 'checkout ambiguous ref succeeds' '\n '\n \n test_expect_success 'checkout produces ambiguity warning' '\n-\tgrep \"warning.*ambiguous\" stderr\n+\ttest_grep \"warning.*ambiguous\" stderr\n '\n \n test_expect_success 'checkout chooses branch over tag' '\n@@ -41,7 +41,7 @@ test_expect_success 'checkout vague ref succeeds' '\n '\n \n test_expect_success VAGUENESS_SUCCESS 'checkout produces ambiguity warning' '\n-\tgrep \"warning.*ambiguous\" stderr\n+\ttest_grep \"warning.*ambiguous\" stderr\n '\n \n test_expect_success VAGUENESS_SUCCESS 'checkout chooses branch over tag' '\ndiff --git a/t/t2024-checkout-dwim.sh b/t/t2024-checkout-dwim.sh\nindex a3b1449ef1..752e7f0e61 100755\n--- a/t/t2024-checkout-dwim.sh\n+++ b/t/t2024-checkout-dwim.sh\n@@ -311,7 +311,7 @@ test_expect_success 'loosely defined local base branch is reported correctly' '\n \tgit checkout loose >actual.raw 2>&1 &&\n \tsed -e \"s/loose/BRANCHNAME/g\" <actual.raw >actual &&\n \tstatus_uno_is_clean &&\n-\tgrep BRANCHNAME actual &&\n+\ttest_grep BRANCHNAME actual &&\n \n \ttest_cmp expect actual\n '\n@@ -324,7 +324,7 @@ test_expect_success 'reject when arg could be part of dwim branch' '\n \techo bar >dwim-arg &&\n \ttest_must_fail git checkout dwim-arg &&\n \ttest_must_fail git rev-parse refs/heads/dwim-arg -- &&\n-\tgrep bar dwim-arg\n+\ttest_grep bar dwim-arg\n '\n \n test_expect_success 'disambiguate dwim branch and checkout path (1)' '\n@@ -334,7 +334,7 @@ test_expect_success 'disambiguate dwim branch and checkout path (1)' '\n \techo bar >dwim-arg1 &&\n \tgit checkout -- dwim-arg1 &&\n \ttest_must_fail git rev-parse refs/heads/dwim-arg1 -- &&\n-\tgrep foo dwim-arg1\n+\ttest_grep foo dwim-arg1\n '\n \n test_expect_success 'disambiguate dwim branch and checkout path (2)' '\n@@ -344,7 +344,7 @@ test_expect_success 'disambiguate dwim branch and checkout path (2)' '\n \techo bar >dwim-arg2 &&\n \tgit checkout dwim-arg2 -- &&\n \tgit rev-parse refs/heads/dwim-arg2 -- &&\n-\tgrep bar dwim-arg2\n+\ttest_grep bar dwim-arg2\n '\n \n test_done\ndiff --git a/t/t2030-unresolve-info.sh b/t/t2030-unresolve-info.sh\nindex be3fcdde07..9af24b1204 100755\n--- a/t/t2030-unresolve-info.sh\n+++ b/t/t2030-unresolve-info.sh\n@@ -122,7 +122,7 @@ test_expect_success 'add records checkout -m undoes' '\n \tcheck_resolve_undo removed &&\n \techo the index and the work tree is unmerged again &&\n \tgit diff >actual &&\n-\tgrep \"^++<<<<<<<\" actual\n+\ttest_grep \"^++<<<<<<<\" actual\n '\n \n test_expect_success 'unmerge with plumbing' '\n@@ -173,7 +173,7 @@ test_expect_success 'rerere and rerere forget' '\n \ttest -f .git/rr-cache/$rerere_id/postimage &&\n \tgit checkout -m fi/le &&\n \techo resurrect the conflict &&\n-\tgrep \"^=======\" fi/le &&\n+\ttest_grep \"^=======\" fi/le &&\n \techo reresolve the conflict &&\n \tgit rerere &&\n \ttest \"z$(cat fi/le)\" = zdifferent &&\n@@ -199,7 +199,7 @@ test_expect_success 'rerere and rerere forget (subdirectory)' '\n \ttest -f .git/rr-cache/$rerere_id/postimage &&\n \t(cd fi && git checkout -m le) &&\n \techo resurrect the conflict &&\n-\tgrep \"^=======\" fi/le &&\n+\ttest_grep \"^=======\" fi/le &&\n \techo reresolve the conflict &&\n \t(cd fi && git rerere) &&\n \ttest \"z$(cat fi/le)\" = zdifferent &&\ndiff --git a/t/t2060-switch.sh b/t/t2060-switch.sh\nindex c91c4db936..cec2fe6876 100755\n--- a/t/t2060-switch.sh\n+++ b/t/t2060-switch.sh\n@@ -34,13 +34,13 @@ test_expect_success 'switch and detach' '\n \n test_expect_success 'suggestion to detach' '\n \ttest_must_fail git switch main^{commit} 2>stderr &&\n-\tgrep \"try again with the --detach option\" stderr\n+\ttest_grep \"try again with the --detach option\" stderr\n '\n \n test_expect_success 'suggestion to detach is suppressed with advice.suggestDetachingHead=false' '\n \ttest_config advice.suggestDetachingHead false &&\n \ttest_must_fail git switch main^{commit} 2>stderr &&\n-\t! grep \"try again with the --detach option\" stderr\n+\ttest_grep ! \"try again with the --detach option\" stderr\n '\n \n test_expect_success 'switch and detach current branch' '\n@@ -76,7 +76,7 @@ test_expect_success 'new orphan branch from empty' '\n \tgit switch --orphan new-orphan &&\n \ttest_commit orphan &&\n \tgit cat-file commit refs/heads/new-orphan >commit &&\n-\t! grep ^parent commit &&\n+\ttest_grep ! ^parent commit &&\n \tgit ls-files >tracked-files &&\n \techo orphan.t >expected &&\n \ttest_cmp expected tracked-files\ndiff --git a/t/t2070-restore.sh b/t/t2070-restore.sh\nindex 16d6348b69..2c222fb934 100755\n--- a/t/t2070-restore.sh\n+++ b/t/t2070-restore.sh\n@@ -216,7 +216,7 @@ test_expect_success 'restore with merge options are incompatible with certain op\n \t\t\"--staged --worktree --conflict=zdiff3\"\n \tdo\n \t\ttest_must_fail git restore $opts . 2>err &&\n-\t\tgrep \"cannot be used\" err || return\n+\t\ttest_grep \"cannot be used\" err || return\n \tdone\n '\n \ndiff --git a/t/t2080-parallel-checkout-basics.sh b/t/t2080-parallel-checkout-basics.sh\nindex 5ffe1a41e2..88a6ec1826 100755\n--- a/t/t2080-parallel-checkout-basics.sh\n+++ b/t/t2080-parallel-checkout-basics.sh\n@@ -200,13 +200,13 @@ test_expect_success 'parallel checkout respects --[no]-force' '\n \t\t# We expect 0 workers because there is nothing to be done\n \t\ttest_checkout_workers 0 git checkout HEAD &&\n \t\ttest_path_is_file D &&\n-\t\tgrep changed D &&\n-\t\tgrep changed F.t &&\n+\t\ttest_grep changed D &&\n+\t\ttest_grep changed F.t &&\n \n \t\ttest_checkout_workers 2 git checkout --force HEAD &&\n \t\ttest_path_is_dir D &&\n-\t\tgrep D/F D/F.t &&\n-\t\tgrep F F.t\n+\t\ttest_grep D/F D/F.t &&\n+\t\ttest_grep F F.t\n \t)\n '\n \n@@ -224,8 +224,8 @@ test_expect_success SYMLINKS 'parallel checkout checks for symlinks in leading d\n \n \t\ttest_checkout_workers 2 git checkout --force HEAD &&\n \t\t! test -h D &&\n-\t\tgrep D/A D/A.t &&\n-\t\tgrep D/B D/B.t\n+\t\ttest_grep D/A D/A.t &&\n+\t\ttest_grep D/B D/B.t\n \t)\n '\n \n@@ -268,7 +268,7 @@ test_expect_success '\"git checkout .\" report should not include failed entries'\n \t\t#  - missing-delay.a: the delay filter will drop this path\n \t\t#  - parallel-*.a: the blob will be missing\n \t\t#\n-\t\tgrep \"Updated 3 paths from the index\" err &&\n+\t\ttest_grep \"Updated 3 paths from the index\" err &&\n \t\ttest_stdout_line_count = 3 ls *.b &&\n \t\t! ls *.a\n \t)\ndiff --git a/t/t2081-parallel-checkout-collisions.sh b/t/t2081-parallel-checkout-collisions.sh\nindex f6fcfc0c1e..db45f5f30f 100755\n--- a/t/t2081-parallel-checkout-collisions.sh\n+++ b/t/t2081-parallel-checkout-collisions.sh\n@@ -78,8 +78,8 @@ test_expect_success CASE_INSENSITIVE_FS 'worker detects dirname collision' '\n \n \t# Check that it used the right number of workers and detected the collisions\n \ttest_workers_in_event_trace 2 trace &&\n-\tgrep \"category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/B.}\" trace &&\n-\tgrep \"category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/C.}\" trace\n+\ttest_grep \"category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/B.}\" trace &&\n+\ttest_grep \"category.:.pcheckout.,.key.:.collision/dirname.,.value.:.A/C.}\" trace\n '\n \n test_expect_success SYMLINKS,CASE_INSENSITIVE_FS 'do not follow symlinks colliding with leading dir' '\n@@ -115,11 +115,11 @@ test_expect_success CASE_INSENSITIVE_FS 'collision report on clone (w/ racy file\n \tset_checkout_config 2 0 &&\n \ttest_checkout_workers 2 git clone . clone-repo 2>stderr &&\n \n-\tgrep FILE_X stderr &&\n-\tgrep FILE_x stderr &&\n-\tgrep file_X stderr &&\n-\tgrep file_x stderr &&\n-\tgrep \"the following paths have collided\" stderr\n+\ttest_grep FILE_X stderr &&\n+\ttest_grep FILE_x stderr &&\n+\ttest_grep file_X stderr &&\n+\ttest_grep file_x stderr &&\n+\ttest_grep \"the following paths have collided\" stderr\n '\n \n # This test ensures that the collision report code is correctly looking for\n@@ -148,11 +148,11 @@ test_expect_success CASE_INSENSITIVE_FS,!MINGW,!CYGWIN \\\n \ttest_checkout_workers 2 \\\n \t\tgit -c core.ignoreCase=false clone . clone-repo 2>stderr &&\n \n-\tgrep FILE_X stderr &&\n-\tgrep FILE_x stderr &&\n-\tgrep file_X stderr &&\n-\tgrep file_x stderr &&\n-\tgrep \"the following paths have collided\" stderr &&\n+\ttest_grep FILE_X stderr &&\n+\ttest_grep FILE_x stderr &&\n+\ttest_grep file_X stderr &&\n+\ttest_grep file_x stderr &&\n+\ttest_grep \"the following paths have collided\" stderr &&\n \n \t# Check that only \"file_x\" was filtered\n \techo file_x >expected.log &&\ndiff --git a/t/t2082-parallel-checkout-attributes.sh b/t/t2082-parallel-checkout-attributes.sh\nindex 79fb11f139..1f43c2a267 100755\n--- a/t/t2082-parallel-checkout-attributes.sh\n+++ b/t/t2082-parallel-checkout-attributes.sh\n@@ -28,8 +28,8 @@ test_expect_success 'parallel-checkout with ident' '\n \t\trm A B &&\n \t\ttest_checkout_workers 2 git reset --hard &&\n \t\thexsz=$(test_oid hexsz) &&\n-\t\tgrep -E \"\\\\\\$Id: [0-9a-f]{$hexsz} \\\\\\$\" A &&\n-\t\tgrep \"\\\\\\$Id\\\\\\$\" B\n+\t\ttest_grep -E \"\\\\\\$Id: [0-9a-f]{$hexsz} \\\\\\$\" A &&\n+\t\ttest_grep \"\\\\\\$Id\\\\\\$\" B\n \t)\n '\n \n@@ -175,15 +175,15 @@ test_expect_success 'parallel-checkout and delayed checkout' '\n \tverify_checkout delayed &&\n \n \t# Check that the *.d files got to the delay queue and were filtered\n-\tgrep \"smudge W.d .* \\[DELAYED\\]\" delayed.log &&\n-\tgrep \"smudge X.d .* \\[DELAYED\\]\" delayed.log &&\n+\ttest_grep \"smudge W.d .* \\[DELAYED\\]\" delayed.log &&\n+\ttest_grep \"smudge X.d .* \\[DELAYED\\]\" delayed.log &&\n \ttest_cmp delayed/W.d original &&\n \ttest_cmp delayed/X.d original &&\n \n \t# Check that the parallel-eligible entries went to the right queue and\n \t# were not filtered\n-\t! grep \"smudge Y .* \\[DELAYED\\]\" delayed.log &&\n-\t! grep \"smudge Z .* \\[DELAYED\\]\" delayed.log &&\n+\ttest_grep ! \"smudge Y .* \\[DELAYED\\]\" delayed.log &&\n+\ttest_grep ! \"smudge Z .* \\[DELAYED\\]\" delayed.log &&\n \ttest_cmp delayed/Y original &&\n \ttest_cmp delayed/Z original\n '\ndiff --git a/t/t2103-update-index-ignore-missing.sh b/t/t2103-update-index-ignore-missing.sh\nindex 6938ecca86..fbfc86b1fa 100755\n--- a/t/t2103-update-index-ignore-missing.sh\n+++ b/t/t2103-update-index-ignore-missing.sh\n@@ -63,9 +63,9 @@ test_expect_success '--unmerged --refresh' '\n \tgit update-index --unmerged --refresh &&\n \techo 2 >two &&\n \ttest_must_fail git update-index --unmerged --refresh >actual &&\n-\tgrep two actual &&\n-\t! grep one actual &&\n-\t! grep three actual\n+\ttest_grep two actual &&\n+\ttest_grep ! one actual &&\n+\ttest_grep ! three actual\n '\n \n test_expect_success '--ignore-submodules --refresh (1)' '\ndiff --git a/t/t2200-add-update.sh b/t/t2200-add-update.sh\nindex 0a96655cfe..bb019d7a7c 100755\n--- a/t/t2200-add-update.sh\n+++ b/t/t2200-add-update.sh\n@@ -241,7 +241,7 @@ test_expect_success 'add -u avoids rename pairing on unmerged paths' '\n test_expect_success '\"add -u non-existent\" should fail' '\n \ttest_must_fail git add -u non-existent &&\n \tgit ls-files >actual &&\n-\t! grep \"non-existent\" actual\n+\ttest_grep ! \"non-existent\" actual\n '\n \n test_expect_success '\"commit -a\" implies \"add -u\" if index becomes empty' '\ndiff --git a/t/t2203-add-intent.sh b/t/t2203-add-intent.sh\nindex 44c1936e4d..69fef7e646 100755\n--- a/t/t2203-add-intent.sh\n+++ b/t/t2203-add-intent.sh\n@@ -57,7 +57,7 @@ test_expect_success 'intent to add does not clobber existing paths' '\n \tgit add -N file elif &&\n \tempty=$(git hash-object --stdin </dev/null) &&\n \tgit ls-files -s >actual &&\n-\t! grep \"$empty\" actual\n+\ttest_grep ! \"$empty\" actual\n '\n \n test_expect_success 'i-t-a entry is simply ignored' '\n@@ -124,7 +124,7 @@ test_expect_success 'cache-tree does not ignore dir that has i-t-a entries' '\n \t\tgit add -N 2/1 &&\n \t\tgit commit -m committed &&\n \t\tgit ls-tree -r HEAD >actual &&\n-\t\tgrep 2/2 actual\n+\t\ttest_grep 2/2 actual\n \t)\n '\n \n@@ -312,7 +312,7 @@ test_expect_success 'apply --intent-to-add' '\n \techo new >new-ita &&\n \tgit add -N new-ita &&\n \tgit diff >expected &&\n-\tgrep \"new file\" expected &&\n+\ttest_grep \"new file\" expected &&\n \tgit reset --hard &&\n \tgit apply --intent-to-add expected &&\n \tgit diff >actual &&\ndiff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh\nindex 58b4445cc4..87b926728a 100755\n--- a/t/t2400-worktree-add.sh\n+++ b/t/t2400-worktree-add.sh\n@@ -122,7 +122,7 @@ test_expect_success 'die the same branch is already checked out' '\n \t(\n \t\tcd here &&\n \t\ttest_must_fail git checkout newmain 2>actual &&\n-\t\tgrep \"already used by worktree at\" actual\n+\t\ttest_grep \"already used by worktree at\" actual\n \t)\n '\n \n@@ -139,7 +139,7 @@ test_expect_success 'refuse to reset a branch in use elsewhere' '\n \t\tgit rev-parse --verify refs/heads/newmain >new.branch &&\n \t\tgit rev-parse --verify HEAD >new.head &&\n \n-\t\tgrep \"already used by worktree at\" error &&\n+\t\ttest_grep \"already used by worktree at\" error &&\n \t\ttest_cmp old.branch new.branch &&\n \t\ttest_cmp old.head new.head &&\n \n@@ -328,7 +328,7 @@ test_wt_add_excl () {\n \tlocal opts=\"$*\" &&\n \ttest_expect_success \"'worktree add' with '$opts' has mutually exclusive options\" '\n \t\ttest_must_fail git worktree add $opts 2>actual &&\n-\t\tgrep -E \"fatal:( options)? .* cannot be used together\" actual\n+\t\ttest_grep -E \"fatal:( options)? .* cannot be used together\" actual\n \t'\n }\n \n@@ -436,13 +436,13 @@ test_wt_add_orphan_hint () {\n \t\t(cd repo && test_commit commit) &&\n \t\tgit -C repo switch --orphan noref &&\n \t\ttest_must_fail git -C repo worktree add $opts foobar/ 2>actual &&\n-\t\t! grep \"error: unknown switch\" actual &&\n-\t\tgrep \"hint: If you meant to create a worktree containing a new unborn branch\" actual &&\n+\t\ttest_grep ! \"error: unknown switch\" actual &&\n+\t\ttest_grep \"hint: If you meant to create a worktree containing a new unborn branch\" actual &&\n \t\tif [ $use_branch -eq 1 ]\n \t\tthen\n-\t\t\tgrep -E \"^hint: +git worktree add --orphan -b [^ ]+ [^ ]+$\" actual\n+\t\t\ttest_grep -E \"^hint: +git worktree add --orphan -b [^ ]+ [^ ]+$\" actual\n \t\telse\n-\t\t\tgrep -E \"^hint: +git worktree add --orphan [^ ]+$\" actual\n+\t\t\ttest_grep -E \"^hint: +git worktree add --orphan [^ ]+$\" actual\n \t\tfi\n \n \t'\n@@ -457,8 +457,8 @@ test_expect_success \"'worktree add' doesn't show orphan hint in bad/orphan HEAD\n \tgit init repo &&\n \t(cd repo && test_commit commit) &&\n \ttest_must_fail git -C repo worktree add --quiet foobar_branch foobar/ 2>actual &&\n-\t! grep \"error: unknown switch\" actual &&\n-\t! grep \"hint: If you meant to create a worktree containing a new unborn branch\" actual\n+\ttest_grep ! \"error: unknown switch\" actual &&\n+\ttest_grep ! \"hint: If you meant to create a worktree containing a new unborn branch\" actual\n '\n \n test_expect_success 'local clone from linked checkout' '\n@@ -469,7 +469,7 @@ test_expect_success 'local clone from linked checkout' '\n test_expect_success 'local clone --shared from linked checkout' '\n \tgit -C bare worktree add --detach ../baretree &&\n \tgit clone --local --shared baretree bare-clone &&\n-\tgrep /bare/ bare-clone/.git/objects/info/alternates\n+\ttest_grep /bare/ bare-clone/.git/objects/info/alternates\n '\n \n test_expect_success '\"add\" worktree with --no-checkout' '\n@@ -491,7 +491,7 @@ test_expect_success 'put a worktree under rebase' '\n \t\tset_fake_editor &&\n \t\tFAKE_LINES=\"edit 1\" git rebase -i HEAD^ &&\n \t\tgit worktree list >actual &&\n-\t\tgrep \"under-rebase.*detached HEAD\" actual\n+\t\ttest_grep \"under-rebase.*detached HEAD\" actual\n \t)\n '\n \n@@ -533,7 +533,7 @@ test_expect_success 'checkout a branch under bisect' '\n \t\tgit bisect bad &&\n \t\tgit bisect good HEAD~2 &&\n \t\tgit worktree list >actual &&\n-\t\tgrep \"under-bisect.*detached HEAD\" actual &&\n+\t\ttest_grep \"under-bisect.*detached HEAD\" actual &&\n \t\ttest_must_fail git worktree add new-bisect under-bisect &&\n \t\ttest_path_is_missing new-bisect\n \t)\ndiff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh\nindex 93f92e854a..eeaf800d74 100755\n--- a/t/t2402-worktree-list.sh\n+++ b/t/t2402-worktree-list.sh\n@@ -101,8 +101,8 @@ test_expect_success '\"list\" all worktrees with locked annotation' '\n \tgit worktree lock locked &&\n \ttest_when_finished \"git worktree unlock locked\" &&\n \tgit worktree list >out &&\n-\tgrep \"/locked  *[0-9a-f].* locked$\" out &&\n-\t! grep \"/unlocked  *[0-9a-f].* locked$\" out\n+\ttest_grep \"/locked  *[0-9a-f].* locked$\" out &&\n+\ttest_grep ! \"/unlocked  *[0-9a-f].* locked$\" out\n '\n \n test_expect_success '\"list\" all worktrees --porcelain with locked' '\n@@ -143,8 +143,8 @@ test_expect_success '\"list\" all worktrees with prunable annotation' '\n \tgit worktree add --detach unprunable &&\n \trm -rf prunable &&\n \tgit worktree list >out &&\n-\tgrep \"/prunable  *[0-9a-f].* prunable$\" out &&\n-\t! grep \"/unprunable  *[0-9a-f].* prunable$\" out\n+\ttest_grep \"/prunable  *[0-9a-f].* prunable$\" out &&\n+\ttest_grep ! \"/unprunable  *[0-9a-f].* prunable$\" out\n '\n \n test_expect_success '\"list\" all worktrees --porcelain with prunable' '\n@@ -162,8 +162,8 @@ test_expect_success '\"list\" all worktrees with prunable consistent with \"prune\"'\n \tgit worktree add --detach unprunable &&\n \trm -rf prunable &&\n \tgit worktree list >out &&\n-\tgrep \"/prunable  *[0-9a-f].* prunable$\" out &&\n-\t! grep \"/unprunable  *[0-9a-f].* unprunable$\" out &&\n+\ttest_grep \"/prunable  *[0-9a-f].* prunable$\" out &&\n+\ttest_grep ! \"/unprunable  *[0-9a-f].* unprunable$\" out &&\n \tgit worktree prune --verbose 2>out &&\n \ttest_grep \"^Removing worktrees/prunable\" out &&\n \ttest_grep ! \"^Removing worktrees/unprunable\" out\n@@ -184,7 +184,7 @@ test_expect_success '\"list\" all worktrees --verbose with locked' '\n \techo \"$(git -C locked2 rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)\" >expect &&\n \tprintf \"\\tlocked: with reason\\n\" >>expect &&\n \tgit worktree list --verbose >out &&\n-\tgrep \"/locked1  *[0-9a-f].* locked$\" out &&\n+\ttest_grep \"/locked1  *[0-9a-f].* locked$\" out &&\n \tsed -n \"s/  */ /g;/\\/locked2  *[0-9a-f].*$/,/locked: .*$/p\" <out >actual &&\n \ttest_cmp actual expect\n '\n@@ -266,7 +266,7 @@ test_expect_success 'broken main worktree still at the top' '\n \t\ttest_cmp ../expected actual &&\n \t\tgit worktree list >out &&\n \t\thead -n 1 out >actual.2 &&\n-\t\tgrep -F \"(error)\" actual.2\n+\t\ttest_grep -F \"(error)\" actual.2\n \t)\n '\n \ndiff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh\nindex 0bb33e8b1b..69768c1207 100755\n--- a/t/t2403-worktree-move.sh\n+++ b/t/t2403-worktree-move.sh\n@@ -75,8 +75,8 @@ test_expect_success 'move worktree' '\n \tgit worktree move source destination &&\n \ttest_path_is_missing source &&\n \tgit worktree list --porcelain >out &&\n-\tgrep \"^worktree.*/destination$\" out &&\n-\t! grep \"^worktree.*/source$\" out &&\n+\ttest_grep \"^worktree.*/destination$\" out &&\n+\ttest_grep ! \"^worktree.*/source$\" out &&\n \tgit -C destination log --format=%s >actual2 &&\n \techo init >expected2 &&\n \ttest_cmp expected2 actual2\n@@ -92,7 +92,7 @@ test_expect_success 'move worktree to another dir' '\n \ttest_when_finished \"git worktree move some-dir/destination destination\" &&\n \ttest_path_is_missing destination &&\n \tgit worktree list --porcelain >out &&\n-\tgrep \"^worktree.*/some-dir/destination$\" out &&\n+\ttest_grep \"^worktree.*/some-dir/destination$\" out &&\n \tgit -C some-dir/destination log --format=%s >actual2 &&\n \techo init >expected2 &&\n \ttest_cmp expected2 actual2\ndiff --git a/t/t2405-worktree-submodule.sh b/t/t2405-worktree-submodule.sh\nindex 11018f37c7..f5c94d490b 100755\n--- a/t/t2405-worktree-submodule.sh\n+++ b/t/t2405-worktree-submodule.sh\n@@ -36,7 +36,7 @@ test_expect_success 'add superproject worktree' '\n \n test_expect_failure 'submodule is checked out just after worktree add' '\n \tgit -C worktree diff --submodule main\"^!\" >out &&\n-\tgrep \"file1 updated\" out\n+\ttest_grep \"file1 updated\" out\n '\n \n test_expect_success 'add superproject worktree and initialize submodules' '\n@@ -46,7 +46,7 @@ test_expect_success 'add superproject worktree and initialize submodules' '\n \n test_expect_success 'submodule is checked out just after submodule update in linked worktree' '\n \tgit -C worktree-submodule-update diff --submodule main\"^!\" >out &&\n-\tgrep \"file1 updated\" out\n+\ttest_grep \"file1 updated\" out\n '\n \n test_expect_success 'add superproject worktree and manually add submodule worktree' '\n@@ -56,7 +56,7 @@ test_expect_success 'add superproject worktree and manually add submodule worktr\n \n test_expect_success 'submodule is checked out after manually adding submodule worktree' '\n \tgit -C linked_submodule diff --submodule main\"^!\" >out &&\n-\tgrep \"file1 updated\" out\n+\ttest_grep \"file1 updated\" out\n '\n \n test_expect_success 'checkout --recurse-submodules uses $GIT_DIR for submodules in a linked worktree' '\ndiff --git a/t/t2407-worktree-heads.sh b/t/t2407-worktree-heads.sh\nindex 57c201869f..a6d7e0f83b 100755\n--- a/t/t2407-worktree-heads.sh\n+++ b/t/t2407-worktree-heads.sh\n@@ -41,10 +41,10 @@ test_expect_success 'refuse to overwrite: checked out in worktree' '\n \tfor i in 1 2 3 4\n \tdo\n \t\ttest_must_fail git branch -f wt-$i HEAD 2>err &&\n-\t\tgrep \"cannot force update the branch\" err &&\n+\t\ttest_grep \"cannot force update the branch\" err &&\n \n \t\ttest_must_fail git branch -D wt-$i 2>err &&\n-\t\tgrep \"cannot delete branch\" err || return 1\n+\t\ttest_grep \"cannot delete branch\" err || return 1\n \tdone\n '\n \n@@ -57,7 +57,7 @@ test_expect_success 'refuse to overwrite: worktree in bisect' '\n \tgit -C wt-4 bisect good wt-1 &&\n \n \ttest_must_fail git branch -f wt-4 HEAD 2>err &&\n-\tgrep \"cannot force update the branch '\\''wt-4'\\'' used by worktree at.*wt-4\" err\n+\ttest_grep \"cannot force update the branch '\\''wt-4'\\'' used by worktree at.*wt-4\" err\n '\n \n test_expect_success 'refuse to overwrite: worktree in rebase (apply)' '\n@@ -67,7 +67,7 @@ test_expect_success 'refuse to overwrite: worktree in rebase (apply)' '\n \ttest_must_fail git -C wt-2 rebase --apply conflict-2 &&\n \n \ttest_must_fail git branch -f wt-2 HEAD 2>err &&\n-\tgrep \"cannot force update the branch '\\''wt-2'\\'' used by worktree at.*wt-2\" err\n+\ttest_grep \"cannot force update the branch '\\''wt-2'\\'' used by worktree at.*wt-2\" err\n '\n \n test_expect_success 'refuse to overwrite: worktree in rebase (merge)' '\n@@ -77,7 +77,7 @@ test_expect_success 'refuse to overwrite: worktree in rebase (merge)' '\n \ttest_must_fail git -C wt-2 rebase conflict-2 &&\n \n \ttest_must_fail git branch -f wt-2 HEAD 2>err &&\n-\tgrep \"cannot force update the branch '\\''wt-2'\\'' used by worktree at.*wt-2\" err\n+\ttest_grep \"cannot force update the branch '\\''wt-2'\\'' used by worktree at.*wt-2\" err\n '\n \n test_expect_success 'refuse to overwrite: worktree in rebase with --update-refs' '\n@@ -89,19 +89,19 @@ test_expect_success 'refuse to overwrite: worktree in rebase with --update-refs'\n \tfor i in 3 4\n \tdo\n \t\ttest_must_fail git branch -f can-be-updated HEAD 2>err &&\n-\t\tgrep \"cannot force update the branch '\\''can-be-updated'\\'' used by worktree at.*wt-3\" err ||\n+\t\ttest_grep \"cannot force update the branch '\\''can-be-updated'\\'' used by worktree at.*wt-3\" err ||\n \t\t\treturn 1\n \tdone\n '\n \n test_expect_success 'refuse to fetch over ref: checked out' '\n \ttest_must_fail git fetch server +refs/heads/wt-3:refs/heads/wt-3 2>err &&\n-\tgrep \"refusing to fetch into branch '\\''refs/heads/wt-3'\\''\" err &&\n+\ttest_grep \"refusing to fetch into branch '\\''refs/heads/wt-3'\\''\" err &&\n \n \t# General fetch into refs/heads/ will fail on first ref,\n \t# so use a generic error message check.\n \ttest_must_fail git fetch server +refs/heads/*:refs/heads/* 2>err &&\n-\tgrep \"refusing to fetch into branch\" err\n+\ttest_grep \"refusing to fetch into branch\" err\n '\n \n test_expect_success 'refuse to fetch over ref: worktree in bisect' '\n@@ -113,7 +113,7 @@ test_expect_success 'refuse to fetch over ref: worktree in bisect' '\n \tgit -C wt-4 bisect good wt-1 &&\n \n \ttest_must_fail git fetch server +refs/heads/wt-4:refs/heads/wt-4 2>err &&\n-\tgrep \"refusing to fetch into branch\" err\n+\ttest_grep \"refusing to fetch into branch\" err\n '\n \n test_expect_success 'refuse to fetch over ref: worktree in rebase' '\n@@ -123,7 +123,7 @@ test_expect_success 'refuse to fetch over ref: worktree in rebase' '\n \ttest_must_fail git -C wt-3 rebase conflict-3 &&\n \n \ttest_must_fail git fetch server +refs/heads/wt-3:refs/heads/wt-3 2>err &&\n-\tgrep \"refusing to fetch into branch\" err\n+\ttest_grep \"refusing to fetch into branch\" err\n '\n \n test_expect_success 'refuse to overwrite when in error states' '\n@@ -149,7 +149,7 @@ test_expect_success 'refuse to overwrite when in error states' '\n \tfor i in 1 2\n \tdo\n \t\ttest_must_fail git branch -f fake-$i HEAD 2>err &&\n-\t\tgrep \"cannot force update the branch '\\''fake-$i'\\'' used by worktree at\" err ||\n+\t\ttest_grep \"cannot force update the branch '\\''fake-$i'\\'' used by worktree at\" err ||\n \t\t\treturn 1\n \tdone\n '\n@@ -161,13 +161,13 @@ test_expect_success 'refuse to overwrite during rebase with --update-refs' '\n \t(\n \t\tset_cat_todo_editor &&\n \t\ttest_must_fail git rebase -i --update-refs HEAD~3 >todo &&\n-\t\t! grep \"update-refs\" todo\n+\t\ttest_grep ! \"update-refs\" todo\n \t) &&\n \tgit branch -f allow-update HEAD~2 &&\n \t(\n \t\tset_cat_todo_editor &&\n \t\ttest_must_fail git rebase -i --update-refs HEAD~3 >todo &&\n-\t\tgrep \"update-ref refs/heads/allow-update\" todo\n+\t\ttest_grep \"update-ref refs/heads/allow-update\" todo\n \t)\n '\n \ndiff --git a/t/t2500-untracked-overwriting.sh b/t/t2500-untracked-overwriting.sh\nindex 5c0bf4d21f..8e84f29e8d 100755\n--- a/t/t2500-untracked-overwriting.sh\n+++ b/t/t2500-untracked-overwriting.sh\n@@ -51,7 +51,7 @@ test_expect_success 'reset --merge will preserve untracked files/dirs' '\n \n \t\ttest_must_fail git reset --merge work 2>error &&\n \t\ttest_cmp expect foo.t/file &&\n-\t\tgrep \"Updating .foo.t. would lose untracked files\" error\n+\t\ttest_grep \"Updating .foo.t. would lose untracked files\" error\n \t)\n '\n \n@@ -66,7 +66,7 @@ test_expect_success 'reset --keep will preserve untracked files/dirs' '\n \n \t\ttest_must_fail git reset --merge work 2>error &&\n \t\ttest_cmp expect foo.t/file &&\n-\t\tgrep \"Updating.*foo.t.*would lose untracked files\" error\n+\t\ttest_grep \"Updating.*foo.t.*would lose untracked files\" error\n \t)\n '\n \n@@ -214,7 +214,7 @@ test_expect_success 'git am --abort and untracked dir vs. unmerged file' '\n \n \t\ttest_must_fail git am --abort 2>errors &&\n \t\ttest_path_is_dir filler &&\n-\t\tgrep \"Updating .filler. would lose untracked files in it\" errors\n+\t\ttest_grep \"Updating .filler. would lose untracked files in it\" errors\n \t)\n '\n \n@@ -237,7 +237,7 @@ test_expect_success 'git am --skip and untracked dir vs deleted file' '\n \t\t# Change our mind about resolutions, just skip this patch\n \t\ttest_must_fail git am --skip 2>errors &&\n \t\ttest_path_is_dir newfile &&\n-\t\tgrep \"Updating .newfile. would lose untracked files in it\" errors\n+\t\ttest_grep \"Updating .newfile. would lose untracked files in it\" errors\n \t)\n '\n \ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex be9140bbaa..eb6451d71d 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -114,7 +114,7 @@ test_expect_success 'merge fails if cwd needs to be removed; recursive friendly'\n \t) &&\n \n \ttest_path_is_dir dirORfile &&\n-\tgrep \"Refusing to remove the current working directory\" error\n+\ttest_grep \"Refusing to remove the current working directory\" error\n '\n \n test_expect_success 'merge fails if cwd needs to be removed' '\n@@ -179,7 +179,7 @@ test_incidental_untracked_dir_removal () {\n test_expect_success 'clean does not remove cwd incidentally' '\n \ttest_incidental_untracked_dir_removal \\\n \t\tgit -C .. clean -fd -e warnings . >warnings &&\n-\tgrep \"Refusing to remove current working directory\" warnings\n+\ttest_grep \"Refusing to remove current working directory\" warnings\n '\n \n test_expect_success 'stash does not remove cwd incidentally' '\ndiff --git a/t/t3001-ls-files-others-exclude.sh b/t/t3001-ls-files-others-exclude.sh\nindex 202fb8d9ea..29a0a25b30 100755\n--- a/t/t3001-ls-files-others-exclude.sh\n+++ b/t/t3001-ls-files-others-exclude.sh\n@@ -161,21 +161,21 @@ test_expect_success 'trailing slash in exclude forces directory match (1)' '\n \n \t>two &&\n \tgit ls-files --others --exclude=two/ >output &&\n-\tgrep \"^two\" output\n+\ttest_grep \"^two\" output\n \n '\n \n test_expect_success 'trailing slash in exclude forces directory match (2)' '\n \n \tgit ls-files --others --exclude=one/a.1/ >output &&\n-\tgrep \"^one/a.1\" output\n+\ttest_grep \"^one/a.1\" output\n \n '\n \n test_expect_success 'negated exclude matches can override previous ones' '\n \n \tgit ls-files --others --exclude=\"a.*\" --exclude=\"!a.1\" >output &&\n-\tgrep \"^a.1\" output\n+\ttest_grep \"^a.1\" output\n '\n \n test_expect_success 'excluded directory overrides content patterns' '\ndiff --git a/t/t3007-ls-files-recurse-submodules.sh b/t/t3007-ls-files-recurse-submodules.sh\nindex 61771eec83..218964edab 100755\n--- a/t/t3007-ls-files-recurse-submodules.sh\n+++ b/t/t3007-ls-files-recurse-submodules.sh\n@@ -302,7 +302,7 @@ test_expect_success '--recurse-submodules does not support --error-unmatch' '\n test_expect_success '--recurse-submodules parses submodule repo config' '\n \ttest_config -C submodule index.sparse \"invalid non-boolean value\" &&\n \ttest_must_fail git ls-files --recurse-submodules 2>err &&\n-\tgrep \"bad boolean config value\" err\n+\ttest_grep \"bad boolean config value\" err\n '\n \n test_expect_success '--recurse-submodules parses submodule worktree config' '\n@@ -310,7 +310,7 @@ test_expect_success '--recurse-submodules parses submodule worktree config' '\n \ttest_config -C submodule --worktree index.sparse \"invalid non-boolean value\" &&\n \n \ttest_must_fail git ls-files --recurse-submodules 2>err &&\n-\tgrep \"bad boolean config value\" err\n+\ttest_grep \"bad boolean config value\" err\n '\n \n test_expect_success '--recurse-submodules submodules ignore super project worktreeConfig extension' '\n@@ -329,7 +329,7 @@ test_expect_success '--recurse-submodules submodules ignore super project worktr\n \t# With extensions.worktreeConfig disabled in the submodule, the invalid\n \t# worktree config is not picked up.\n \tgit ls-files --recurse-submodules 2>err &&\n-\t! grep \"bad boolean config value\" err\n+\ttest_grep ! \"bad boolean config value\" err\n '\n \n test_incompatible_with_recurse_submodules () {\ndiff --git a/t/t3200-branch.sh b/t/t3200-branch.sh\nindex e7829c2c4b..bd839998b1 100755\n--- a/t/t3200-branch.sh\n+++ b/t/t3200-branch.sh\n@@ -204,7 +204,7 @@ test_expect_success 'git branch -M baz bam should succeed when baz is checked ou\n \n test_expect_success 'git branch -M baz bam should add entries to HEAD reflog' '\n \tgit reflog show HEAD >actual &&\n-\tgrep \"HEAD@{0}: Branch: renamed refs/heads/baz to refs/heads/bam\" actual\n+\ttest_grep \"HEAD@{0}: Branch: renamed refs/heads/baz to refs/heads/bam\" actual\n '\n \n test_expect_success 'git branch -M should leave orphaned HEAD alone' '\n@@ -339,7 +339,7 @@ test_expect_success 'git branch -d on orphan HEAD (unmerged)' '\n \ttest_when_finished \"git branch -D to-delete\" &&\n \tgit branch to-delete main &&\n \ttest_must_fail git branch -d to-delete 2>err &&\n-\tgrep \"not fully merged\" err\n+\ttest_grep \"not fully merged\" err\n '\n \n test_expect_success 'git branch -d on orphan HEAD (unmerged, graph)' '\n@@ -350,7 +350,7 @@ test_expect_success 'git branch -d on orphan HEAD (unmerged, graph)' '\n \ttest_when_finished \"rm -rf .git/objects/commit-graph*\" &&\n \tgit commit-graph write --reachable &&\n \ttest_must_fail git branch -d to-delete 2>err &&\n-\tgrep \"not fully merged\" err\n+\ttest_grep \"not fully merged\" err\n '\n \n test_expect_success 'git branch -v -d t should work' '\n@@ -712,7 +712,7 @@ test_expect_success 'git branch -C c1 c2 should succeed when c1 is checked out'\n test_expect_success 'git branch -C c1 c2 should never touch HEAD' '\n \tmsg=\"Branch: copied refs/heads/c1 to refs/heads/c2\" &&\n \tgit reflog HEAD >actual &&\n-\t! grep \"$msg$\" actual\n+\ttest_grep ! \"$msg$\" actual\n '\n \n test_expect_success 'git branch -C main should work when main is checked out' '\n@@ -930,7 +930,7 @@ test_expect_success 'deleting currently checked out branch fails' '\n \tgit worktree add -b my7 my7 &&\n \ttest_must_fail git -C my7 branch -d my7 &&\n \ttest_must_fail git branch -d my7 2>actual &&\n-\tgrep \"^error: cannot delete branch .my7. used by worktree at \" actual &&\n+\ttest_grep \"^error: cannot delete branch .my7. used by worktree at \" actual &&\n \trm -r my7 &&\n \tgit worktree prune\n '\n@@ -941,7 +941,7 @@ test_expect_success 'deleting in-use branch fails' '\n \tgit -C my7 bisect start HEAD HEAD~2 &&\n \ttest_must_fail git -C my7 branch -d my7 &&\n \ttest_must_fail git branch -d my7 2>actual &&\n-\tgrep \"^error: cannot delete branch .my7. used by worktree at \" actual &&\n+\ttest_grep \"^error: cannot delete branch .my7. used by worktree at \" actual &&\n \trm -r my7 &&\n \tgit worktree prune\n '\ndiff --git a/t/t3202-show-branch.sh b/t/t3202-show-branch.sh\nindex a1139f79e2..f402334936 100755\n--- a/t/t3202-show-branch.sh\n+++ b/t/t3202-show-branch.sh\n@@ -123,13 +123,13 @@ test_expect_success 'show-branch --sparse' '\n \tgit commit --allow-empty -m \"another\" &&\n \n \tgit show-branch --sparse >out &&\n-\tgrep \"merge 1 and 10 to make A\" out &&\n+\ttest_grep \"merge 1 and 10 to make A\" out &&\n \n \tgit show-branch >out &&\n-\t! grep \"merge 1 and 10 to make A\" out &&\n+\ttest_grep ! \"merge 1 and 10 to make A\" out &&\n \n \tgit show-branch --no-sparse >out &&\n-\t! grep \"merge 1 and 10 to make A\" out\n+\ttest_grep ! \"merge 1 and 10 to make A\" out\n '\n \n test_expect_success 'setup show branch --list' '\n@@ -189,7 +189,7 @@ while read combo\n do\n \ttest_expect_success \"show-branch $combo (should fail)\" '\n \t\ttest_must_fail git show-branch $combo 2>error &&\n-\t\tgrep -e \"cannot be used together\" -e \"usage:\" error\n+\t\ttest_grep -e \"cannot be used together\" -e \"usage:\" error\n \t'\n done <<\\EOF\n --all --reflog\n@@ -203,7 +203,7 @@ for opt in topo-order date-order reflog\n do\n \ttest_expect_su"}]}