strands-shell 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. {strands_shell-0.4.0 → strands_shell-0.5.0}/Cargo.lock +25 -25
  2. {strands_shell-0.4.0 → strands_shell-0.5.0}/Cargo.toml +1 -1
  3. {strands_shell-0.4.0 → strands_shell-0.5.0}/PKG-INFO +1 -1
  4. {strands_shell-0.4.0 → strands_shell-0.5.0}/package.json +1 -1
  5. {strands_shell-0.4.0 → strands_shell-0.5.0}/pyproject.toml +1 -1
  6. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/grep.rs +186 -67
  7. {strands_shell-0.4.0 → strands_shell-0.5.0}/tests/shell_integration.rs +341 -0
  8. {strands_shell-0.4.0 → strands_shell-0.5.0}/.cargo/config.toml +0 -0
  9. {strands_shell-0.4.0 → strands_shell-0.5.0}/.github/CODEOWNERS +0 -0
  10. {strands_shell-0.4.0 → strands_shell-0.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  11. {strands_shell-0.4.0 → strands_shell-0.5.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  12. {strands_shell-0.4.0 → strands_shell-0.5.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  13. {strands_shell-0.4.0 → strands_shell-0.5.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  14. {strands_shell-0.4.0 → strands_shell-0.5.0}/.github/actions/stamp-version/action.yml +0 -0
  15. {strands_shell-0.4.0 → strands_shell-0.5.0}/.github/dependabot.yml +0 -0
  16. {strands_shell-0.4.0 → strands_shell-0.5.0}/.github/scripts/group-release-notes.py +0 -0
  17. {strands_shell-0.4.0 → strands_shell-0.5.0}/.github/workflows/ci.yml +0 -0
  18. {strands_shell-0.4.0 → strands_shell-0.5.0}/.github/workflows/pr-title.yml +0 -0
  19. {strands_shell-0.4.0 → strands_shell-0.5.0}/.github/workflows/release.yml +0 -0
  20. {strands_shell-0.4.0 → strands_shell-0.5.0}/.gitignore +0 -0
  21. {strands_shell-0.4.0 → strands_shell-0.5.0}/AGENTS.md +0 -0
  22. {strands_shell-0.4.0 → strands_shell-0.5.0}/CODE_OF_CONDUCT.md +0 -0
  23. {strands_shell-0.4.0 → strands_shell-0.5.0}/CONTRIBUTING.md +0 -0
  24. {strands_shell-0.4.0 → strands_shell-0.5.0}/LICENSE +0 -0
  25. {strands_shell-0.4.0 → strands_shell-0.5.0}/NOTICE +0 -0
  26. {strands_shell-0.4.0 → strands_shell-0.5.0}/README.md +0 -0
  27. {strands_shell-0.4.0 → strands_shell-0.5.0}/SECURITY.md +0 -0
  28. {strands_shell-0.4.0 → strands_shell-0.5.0}/build.rs +0 -0
  29. {strands_shell-0.4.0 → strands_shell-0.5.0}/index.d.ts +0 -0
  30. {strands_shell-0.4.0 → strands_shell-0.5.0}/index.js +0 -0
  31. {strands_shell-0.4.0 → strands_shell-0.5.0}/python/strands_shell/__init__.py +0 -0
  32. {strands_shell-0.4.0 → strands_shell-0.5.0}/scripts/build-wasm.sh +0 -0
  33. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/alias.rs +0 -0
  34. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/cd.rs +0 -0
  35. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/colon.rs +0 -0
  36. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/echo.rs +0 -0
  37. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/export.rs +0 -0
  38. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/find.rs +0 -0
  39. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/getopts.rs +0 -0
  40. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/hash.rs +0 -0
  41. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/local.rs +0 -0
  42. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/lua.rs +0 -0
  43. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/mod.rs +0 -0
  44. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/printf.rs +0 -0
  45. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/pwd.rs +0 -0
  46. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/read.rs +0 -0
  47. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/set.rs +0 -0
  48. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/shift.rs +0 -0
  49. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/test.rs +0 -0
  50. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/trap.rs +0 -0
  51. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/type_cmd.rs +0 -0
  52. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/umask.rs +0 -0
  53. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/unset.rs +0 -0
  54. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/wait.rs +0 -0
  55. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/builtins/xargs.rs +0 -0
  56. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/cli.rs +0 -0
  57. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/basename.rs +0 -0
  58. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/cat.rs +0 -0
  59. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/chmod.rs +0 -0
  60. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/cp.rs +0 -0
  61. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/curl.rs +0 -0
  62. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/cut.rs +0 -0
  63. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/date.rs +0 -0
  64. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/dirname.rs +0 -0
  65. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/echo.rs +0 -0
  66. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/env.rs +0 -0
  67. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/false.rs +0 -0
  68. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/head.rs +0 -0
  69. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/jq.rs +0 -0
  70. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/ln.rs +0 -0
  71. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/ls.rs +0 -0
  72. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/mkdir.rs +0 -0
  73. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/mktemp.rs +0 -0
  74. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/mod.rs +0 -0
  75. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/mv.rs +0 -0
  76. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/pwd.rs +0 -0
  77. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/readlink.rs +0 -0
  78. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/rm.rs +0 -0
  79. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/rmdir.rs +0 -0
  80. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/sed.rs +0 -0
  81. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/sleep.rs +0 -0
  82. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/sort.rs +0 -0
  83. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/tail.rs +0 -0
  84. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/tee.rs +0 -0
  85. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/touch.rs +0 -0
  86. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/tr.rs +0 -0
  87. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/true.rs +0 -0
  88. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/uniq.rs +0 -0
  89. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/commands/wc.rs +0 -0
  90. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/exec.rs +0 -0
  91. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/io.rs +0 -0
  92. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/js.rs +0 -0
  93. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/lib.rs +0 -0
  94. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/main.rs +0 -0
  95. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/mcp.rs +0 -0
  96. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/mcp_client.rs +0 -0
  97. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/os.rs +0 -0
  98. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/parser.rs +0 -0
  99. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/prelude.rs +0 -0
  100. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/python.rs +0 -0
  101. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/shell.rs +0 -0
  102. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/vfs.rs +0 -0
  103. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/vfs_config.rs +0 -0
  104. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/vfs_kernel.rs +0 -0
  105. {strands_shell-0.4.0 → strands_shell-0.5.0}/src/wasm_main.rs +0 -0
  106. {strands_shell-0.4.0 → strands_shell-0.5.0}/strands-shell-macros/Cargo.toml +0 -0
  107. {strands_shell-0.4.0 → strands_shell-0.5.0}/strands-shell-macros/src/lib.rs +0 -0
  108. {strands_shell-0.4.0 → strands_shell-0.5.0}/tests/config.rs +0 -0
  109. {strands_shell-0.4.0 → strands_shell-0.5.0}/tests/curl_integration.rs +0 -0
  110. {strands_shell-0.4.0 → strands_shell-0.5.0}/tests/js/test_bindings.mjs +0 -0
  111. {strands_shell-0.4.0 → strands_shell-0.5.0}/tests/js/test_builder.mjs +0 -0
  112. {strands_shell-0.4.0 → strands_shell-0.5.0}/tests/js/test_config.mjs +0 -0
  113. {strands_shell-0.4.0 → strands_shell-0.5.0}/tests/lua_integration.rs +0 -0
  114. {strands_shell-0.4.0 → strands_shell-0.5.0}/tests/mcp_integration.rs +0 -0
  115. {strands_shell-0.4.0 → strands_shell-0.5.0}/tests/python/test_bindings.py +0 -0
  116. {strands_shell-0.4.0 → strands_shell-0.5.0}/tests/python/test_builder.py +0 -0
  117. {strands_shell-0.4.0 → strands_shell-0.5.0}/tests/python/test_config.py +0 -0
  118. {strands_shell-0.4.0 → strands_shell-0.5.0}/tests/ts/api.types.ts +0 -0
  119. {strands_shell-0.4.0 → strands_shell-0.5.0}/tests/vfs_unit.rs +0 -0
  120. {strands_shell-0.4.0 → strands_shell-0.5.0}/tsconfig.json +0 -0
@@ -214,9 +214,9 @@ dependencies = [
214
214
 
215
215
  [[package]]
216
216
  name = "clap"
217
- version = "4.6.6"
217
+ version = "4.6.7"
218
218
  source = "registry+https://github.com/rust-lang/crates.io-index"
219
- checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
219
+ checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
220
220
  dependencies = [
221
221
  "clap_builder",
222
222
  "clap_derive",
@@ -224,9 +224,9 @@ dependencies = [
224
224
 
225
225
  [[package]]
226
226
  name = "clap_builder"
227
- version = "4.6.6"
227
+ version = "4.6.7"
228
228
  source = "registry+https://github.com/rust-lang/crates.io-index"
229
- checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
229
+ checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
230
230
  dependencies = [
231
231
  "anstream",
232
232
  "anstyle",
@@ -236,9 +236,9 @@ dependencies = [
236
236
 
237
237
  [[package]]
238
238
  name = "clap_derive"
239
- version = "4.6.4"
239
+ version = "4.6.7"
240
240
  source = "registry+https://github.com/rust-lang/crates.io-index"
241
- checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
241
+ checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
242
242
  dependencies = [
243
243
  "heck",
244
244
  "proc-macro2",
@@ -880,9 +880,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
880
880
 
881
881
  [[package]]
882
882
  name = "lua-src"
883
- version = "551.0.1"
883
+ version = "551.0.2"
884
884
  source = "registry+https://github.com/rust-lang/crates.io-index"
885
- checksum = "087097f9936a7d819bda525b32d6e96f9c54f3d35ff23ff72fc0c1697d2127db"
885
+ checksum = "d400ffef0e3d4d29287092bdc5a276d0bf468b2c69709d5bab0d469312d9f947"
886
886
  dependencies = [
887
887
  "cc",
888
888
  ]
@@ -928,9 +928,9 @@ dependencies = [
928
928
 
929
929
  [[package]]
930
930
  name = "mlua"
931
- version = "0.12.1"
931
+ version = "0.12.2"
932
932
  source = "registry+https://github.com/rust-lang/crates.io-index"
933
- checksum = "d96e5d00f19d8c46c71ceaced99593b90c31c57aa1fe2cb3e93a8b1698eedba9"
933
+ checksum = "7a7ae6b65bf54b7fdd4cf5b6f44f60862350ebf3fe3779b3a9cc005e7fa14f81"
934
934
  dependencies = [
935
935
  "bstr",
936
936
  "either",
@@ -944,9 +944,9 @@ dependencies = [
944
944
 
945
945
  [[package]]
946
946
  name = "mlua-sys"
947
- version = "0.12.0"
947
+ version = "0.13.0"
948
948
  source = "registry+https://github.com/rust-lang/crates.io-index"
949
- checksum = "b806d7ade031f5d6607eae3e283fb034cb795a76247dd0d1ba753c8c42debccf"
949
+ checksum = "1631cda17bd6486410ab751d91f5fd4484f441589278cdad879eb3afc721a285"
950
950
  dependencies = [
951
951
  "cc",
952
952
  "cfg-if",
@@ -958,9 +958,9 @@ dependencies = [
958
958
 
959
959
  [[package]]
960
960
  name = "napi"
961
- version = "3.12.2"
961
+ version = "3.14.2"
962
962
  source = "registry+https://github.com/rust-lang/crates.io-index"
963
- checksum = "58c5f4d5375213fdb7be2655e152386e82f026f9a5ba36a75556e11359aafe09"
963
+ checksum = "a4e2cf85c02d9b057898c485b824c3c416b65193689e2b7f517efc94b86ed24c"
964
964
  dependencies = [
965
965
  "bitflags",
966
966
  "ctor",
@@ -975,42 +975,42 @@ dependencies = [
975
975
 
976
976
  [[package]]
977
977
  name = "napi-build"
978
- version = "2.4.1"
978
+ version = "2.6.0"
979
979
  source = "registry+https://github.com/rust-lang/crates.io-index"
980
- checksum = "60fdf9b392c50e7c4170fa633bd909490ed7835cea4c046776d1a4dd8d2ae0ab"
980
+ checksum = "b899b545d3aa6dca985939059f258c5488d34e4ecf39c274e20009748f4b846d"
981
981
 
982
982
  [[package]]
983
983
  name = "napi-derive"
984
- version = "3.6.9"
984
+ version = "3.6.11"
985
985
  source = "registry+https://github.com/rust-lang/crates.io-index"
986
- checksum = "6b1d232c24dc6754979b27efdf2047a5849efc7c92c1245de87e6bedbb9f5c3c"
986
+ checksum = "903796e3a3f79eb7e52b1548c11f1817cddec62b0e347a3e97af88b1bd76fd53"
987
987
  dependencies = [
988
988
  "convert_case",
989
989
  "ctor",
990
990
  "napi-derive-backend",
991
991
  "proc-macro2",
992
992
  "quote",
993
- "syn 2.0.119",
993
+ "syn 3.0.2",
994
994
  ]
995
995
 
996
996
  [[package]]
997
997
  name = "napi-derive-backend"
998
- version = "6.1.4"
998
+ version = "6.1.5"
999
999
  source = "registry+https://github.com/rust-lang/crates.io-index"
1000
- checksum = "fd6856fe1686867da90ed0dc292d7c0c87a695eb7c578b913360f33619ee01e3"
1000
+ checksum = "fae2444f4ba7333754bb73414ff8059dec2cb23b83a95bd1a0bf08169eed1be0"
1001
1001
  dependencies = [
1002
1002
  "convert_case",
1003
1003
  "proc-macro2",
1004
1004
  "quote",
1005
1005
  "semver",
1006
- "syn 2.0.119",
1006
+ "syn 3.0.2",
1007
1007
  ]
1008
1008
 
1009
1009
  [[package]]
1010
1010
  name = "napi-sys"
1011
- version = "3.3.0"
1011
+ version = "3.4.0"
1012
1012
  source = "registry+https://github.com/rust-lang/crates.io-index"
1013
- checksum = "85fbf1fa9f1babfe396d74bbbf52b3643770243e8f5b0b46715d4caf7f0dfc9a"
1013
+ checksum = "e22a4f25c16a5c5411d987cd6fbd48313522ae1789ade2d6dd3efdc6d40a0fc8"
1014
1014
  dependencies = [
1015
1015
  "libloading",
1016
1016
  ]
@@ -1606,7 +1606,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
1606
1606
 
1607
1607
  [[package]]
1608
1608
  name = "strands-shell"
1609
- version = "0.4.0"
1609
+ version = "0.5.0"
1610
1610
  dependencies = [
1611
1611
  "async-trait",
1612
1612
  "axum",
@@ -2,7 +2,7 @@
2
2
  name = "strands-shell"
3
3
  # Placeholder — the real version comes from the release git tag, stamped at
4
4
  # build time by .github/actions/stamp-version. Do not bump this by hand.
5
- version = "0.4.0"
5
+ version = "0.5.0"
6
6
  edition = "2024"
7
7
  description = "A virtual shell sandbox for AI agents"
8
8
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strands-shell
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strands-agents/shell",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Strands Shell — a virtual shell sandbox for AI agents (Node.js bindings)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -6,7 +6,7 @@ build-backend = "maturin"
6
6
  name = "strands-shell"
7
7
  # Placeholder — the real version comes from the release git tag, stamped at
8
8
  # build time by .github/actions/stamp-version. Do not bump this by hand.
9
- version = "0.4.0"
9
+ version = "0.5.0"
10
10
  description = "A virtual shell for AI agents"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.10"
@@ -21,6 +21,9 @@ Options:
21
21
  -H, --with-filename print filename with matches
22
22
  -h, --no-filename suppress filename prefix
23
23
  -o, --only-matching show only the matching part
24
+ -a, --text process a binary file as if it were text
25
+ -I ignore binary files (treat as non-matching)
26
+ --binary-files=TYPE binary, text, or without-match
24
27
  -m, --max-count NUM stop after NUM matches per file
25
28
  -A, --after-context NUM print NUM lines after match
26
29
  -B, --before-context NUM print NUM lines before match
@@ -29,6 +32,17 @@ Options:
29
32
  --exclude=GLOB skip files matching GLOB
30
33
  --exclude-dir=DIR skip directories matching DIR";
31
34
 
35
+ /// How to treat input that looks binary (NUL byte or invalid UTF-8).
36
+ #[derive(Clone, Copy, PartialEq, Eq)]
37
+ enum BinaryFiles {
38
+ /// Report "binary file matches" instead of printing the lines.
39
+ Binary,
40
+ /// Print every line, decoding invalid bytes lossily (captured output must be UTF-8).
41
+ Text,
42
+ /// Treat files containing NUL as if they had no matches.
43
+ WithoutMatch,
44
+ }
45
+
32
46
  struct Opts {
33
47
  patterns: Vec<String>,
34
48
  files: Vec<String>,
@@ -46,6 +60,7 @@ struct Opts {
46
60
  with_filename: Option<bool>,
47
61
  only_matching: bool,
48
62
  max_count: Option<u64>,
63
+ binary_files: BinaryFiles,
49
64
  after_context: usize,
50
65
  before_context: usize,
51
66
  include: Vec<String>,
@@ -71,6 +86,7 @@ fn parse_args(args: &[String]) -> Result<Option<Opts>, Box<dyn std::error::Error
71
86
  with_filename: None,
72
87
  only_matching: false,
73
88
  max_count: None,
89
+ binary_files: BinaryFiles::Binary,
74
90
  after_context: 0,
75
91
  before_context: 0,
76
92
  include: Vec::new(),
@@ -97,6 +113,20 @@ fn parse_args(args: &[String]) -> Result<Option<Opts>, Box<dyn std::error::Error
97
113
  Short('h') | Long("no-filename") => opts.with_filename = Some(false),
98
114
  Short('o') | Long("only-matching") => opts.only_matching = true,
99
115
  Short('m') | Long("max-count") => opts.max_count = Some(parser.value()?.parse()?),
116
+ Short('a') | Long("text") => opts.binary_files = BinaryFiles::Text,
117
+ Short('I') => opts.binary_files = BinaryFiles::WithoutMatch,
118
+ Long("binary-files") => {
119
+ opts.binary_files = match parser.value()?.string()?.as_str() {
120
+ "binary" => BinaryFiles::Binary,
121
+ "text" => BinaryFiles::Text,
122
+ "without-match" => BinaryFiles::WithoutMatch,
123
+ other => {
124
+ return Err(
125
+ format!("invalid argument '{other}' for '--binary-files'").into()
126
+ );
127
+ }
128
+ };
129
+ }
100
130
  Short('A') | Long("after-context") => opts.after_context = parser.value()?.parse()?,
101
131
  Short('B') | Long("before-context") => opts.before_context = parser.value()?.parse()?,
102
132
  Short('C') | Long("context") => {
@@ -125,7 +155,9 @@ fn parse_args(args: &[String]) -> Result<Option<Opts>, Box<dyn std::error::Error
125
155
  Ok(Some(opts))
126
156
  }
127
157
 
128
- fn build_regex(opts: &Opts) -> Result<regex::Regex, Box<dyn std::error::Error + Send + Sync>> {
158
+ fn build_regex(
159
+ opts: &Opts,
160
+ ) -> Result<regex::bytes::Regex, Box<dyn std::error::Error + Send + Sync>> {
129
161
  let combined = if opts.fixed {
130
162
  opts.patterns
131
163
  .iter()
@@ -142,7 +174,7 @@ fn build_regex(opts: &Opts) -> Result<regex::Regex, Box<dyn std::error::Error +
142
174
  if opts.line_regexp {
143
175
  pat = format!("^(?:{})$", pat);
144
176
  }
145
- let re = regex::RegexBuilder::new(&pat)
177
+ let re = regex::bytes::RegexBuilder::new(&pat)
146
178
  .case_insensitive(opts.ignore_case)
147
179
  .build()?;
148
180
  Ok(re)
@@ -211,87 +243,160 @@ async fn collect_files_recursive(os: &dyn Kernel, path: &str, opts: &Opts, out:
211
243
  }
212
244
  }
213
245
 
246
+ async fn write_context(
247
+ w: &mut os::FdWriter,
248
+ prefix: &str,
249
+ line_number: bool,
250
+ lineno: u64,
251
+ text: &[u8],
252
+ ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
253
+ if !prefix.is_empty() {
254
+ wprint!(w, "{}-", prefix)?;
255
+ }
256
+ if line_number {
257
+ wprint!(w, "{}-", lineno)?;
258
+ }
259
+ wprintln!(w, "{}", String::from_utf8_lossy(text))?;
260
+ Ok(())
261
+ }
262
+
214
263
  async fn grep_reader<R: tokio::io::AsyncRead + Unpin + Send>(
215
264
  reader: R,
216
- re: &regex::Regex,
265
+ re: &regex::bytes::Regex,
217
266
  opts: &Opts,
218
267
  prefix: &str,
268
+ name: &str,
219
269
  w: &mut os::FdWriter,
270
+ ew: &mut os::FdWriter,
220
271
  ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
221
- let mut buf_reader = BufReader::new(reader);
222
- let mut line = String::new();
272
+ let mut buf_reader = BufReader::with_capacity(96 * 1024, reader);
273
+ let mut line: Vec<u8> = Vec::new();
223
274
  let mut lineno: u64 = 0;
224
275
  let mut match_count: u64 = 0;
225
276
  let mut found = false;
277
+ let detect_binary = opts.binary_files != BinaryFiles::Text;
278
+ // A NUL in the first 96 KiB (GNU's first buffer) marks the whole input binary; a later NUL
279
+ // does so from its line on (GNU decides per buffer there).
280
+ let mut file_binary = detect_binary && buf_reader.fill_buf().await?.contains(&0);
281
+ // A matched or context line was hidden as binary, so report it on stderr.
282
+ let mut binary_matched = false;
283
+ let summary_only = opts.quiet || opts.list || opts.list_non_matching || opts.count;
226
284
 
227
285
  let use_context = opts.before_context > 0 || opts.after_context > 0;
228
- // Ring buffer for before-context
229
- let mut before_buf: std::collections::VecDeque<(u64, String)> =
286
+ // Last B lines not yet written (hidden ones included), as (lineno, bytes, hidden).
287
+ let mut before_buf: std::collections::VecDeque<(u64, Vec<u8>, bool)> =
230
288
  std::collections::VecDeque::new();
231
- // How many more after-context lines to print
232
- let mut after_remaining: usize = 0;
233
- // Whether we need a "--" separator before the next context group
234
- let mut need_sep = false;
289
+ // Line number of the last line written (0 = none); GNU's lastout.
290
+ let mut last_printed: u64 = 0;
291
+ let mut used = false;
292
+ // After-context lines still owed.
293
+ let mut pending: usize = 0;
294
+
295
+ // A binary file is treated as non-matching under -I.
296
+ let skip_file = file_binary && opts.binary_files == BinaryFiles::WithoutMatch;
235
297
 
236
298
  loop {
299
+ if skip_file {
300
+ break;
301
+ }
237
302
  line.clear();
238
- if buf_reader.read_line(&mut line).await? == 0 {
303
+ if buf_reader.read_until(b'\n', &mut line).await? == 0 {
239
304
  break;
240
305
  }
241
306
  lineno += 1;
242
- let text = line.trim_end_matches('\n').trim_end_matches('\r');
243
- let matched = re.is_match(text) ^ opts.invert;
307
+ let mut raw = line.as_slice();
308
+ raw = raw.strip_suffix(b"\n").unwrap_or(raw);
309
+ raw = raw.strip_suffix(b"\r").unwrap_or(raw);
310
+ if detect_binary && !file_binary && raw.contains(&0) {
311
+ file_binary = true;
312
+ if opts.binary_files == BinaryFiles::WithoutMatch {
313
+ // GNU counts the whole file as non-matching; printed lines stay printed.
314
+ match_count = 0;
315
+ found = false;
316
+ break;
317
+ }
318
+ }
319
+ // Invalid UTF-8 is matched on raw bytes but never printed unless -a.
320
+ let invalid = detect_binary && std::str::from_utf8(raw).is_err();
321
+ let is_binary = file_binary || invalid;
322
+ // -o hides only matches that are themselves invalid UTF-8.
323
+ let per_match = opts.only_matching && !opts.invert && !file_binary;
324
+ let matched = re.is_match(raw) ^ opts.invert;
325
+ // Hidden lines are never written but otherwise behave like normal lines.
326
+ let hidden = is_binary && !per_match;
244
327
 
245
328
  if matched {
246
329
  found = true;
247
330
  match_count += 1;
248
331
 
249
- if opts.quiet || opts.list || opts.list_non_matching || opts.count {
250
- if let Some(max) = opts.max_count
251
- && match_count >= max
252
- {
332
+ if summary_only {
333
+ // -q/-l/-L stop at the first match (GNU done_on_match).
334
+ let first_match_done = opts.quiet || opts.list || opts.list_non_matching;
335
+ if first_match_done || opts.max_count.is_some_and(|max| match_count >= max) {
253
336
  break;
254
337
  }
255
338
  continue;
256
339
  }
257
340
 
258
- if use_context {
259
- // Print separator between context groups
260
- if (opts.before_context == 0 || !before_buf.is_empty()) && need_sep {
261
- wprintln!(w, "--")?;
341
+ if hidden && file_binary {
342
+ binary_matched = true;
343
+ break;
344
+ }
345
+
346
+ // Region start; a gap from the last written line starts a new group.
347
+ let rs = (last_printed + 1).max(lineno.saturating_sub(opts.before_context as u64));
348
+ // GNU's `used` is set by any selected line, hidden or not. With nothing written
349
+ // and no -A, its lastout is still unset and never matches, so the group gets `--`.
350
+ let lastout_unset = last_printed == 0 && opts.after_context == 0;
351
+ if use_context && used && (lastout_unset || rs != last_printed + 1) {
352
+ wprintln!(w, "--")?;
353
+ }
354
+ used = true;
355
+ for (bno, btext, bhidden) in before_buf.drain(..) {
356
+ if bno < rs {
357
+ continue;
262
358
  }
263
- need_sep = false;
264
- // Flush before-context buffer
265
- for (bno, btext) in before_buf.drain(..) {
266
- if !prefix.is_empty() {
267
- wprint!(w, "{}-", prefix)?;
268
- }
269
- if opts.line_number {
270
- wprint!(w, "{}-", bno)?;
359
+ if bhidden {
360
+ binary_matched |= !opts.only_matching;
361
+ } else {
362
+ if !opts.only_matching {
363
+ write_context(w, prefix, opts.line_number, bno, &btext).await?;
271
364
  }
272
- wprintln!(w, "{}", btext)?;
365
+ last_printed = bno;
273
366
  }
274
- after_remaining = opts.after_context;
275
367
  }
276
368
 
277
- if opts.only_matching && !opts.invert {
278
- for m in re.find_iter(text) {
369
+ if hidden {
370
+ // A hidden match starts no after-context (GNU lastout does not advance).
371
+ binary_matched = true;
372
+ pending = 0;
373
+ } else {
374
+ if opts.only_matching && !opts.invert {
375
+ for m in re.find_iter(raw) {
376
+ // GNU drops the rest of the line once a match is suppressed.
377
+ if invalid && std::str::from_utf8(m.as_bytes()).is_err() {
378
+ binary_matched = true;
379
+ break;
380
+ }
381
+ if !prefix.is_empty() {
382
+ wprint!(w, "{}:", prefix)?;
383
+ }
384
+ if opts.line_number {
385
+ wprint!(w, "{}:", lineno)?;
386
+ }
387
+ wprintln!(w, "{}", String::from_utf8_lossy(m.as_bytes()))?;
388
+ }
389
+ } else {
279
390
  if !prefix.is_empty() {
280
391
  wprint!(w, "{}:", prefix)?;
281
392
  }
282
393
  if opts.line_number {
283
394
  wprint!(w, "{}:", lineno)?;
284
395
  }
285
- wprintln!(w, "{}", m.as_str())?;
286
- }
287
- } else {
288
- if !prefix.is_empty() {
289
- wprint!(w, "{}:", prefix)?;
396
+ wprintln!(w, "{}", String::from_utf8_lossy(raw))?;
290
397
  }
291
- if opts.line_number {
292
- wprint!(w, "{}:", lineno)?;
293
- }
294
- wprintln!(w, "{}", text)?;
398
+ last_printed = lineno;
399
+ pending = opts.after_context;
295
400
  }
296
401
 
297
402
  if let Some(max) = opts.max_count
@@ -299,25 +404,21 @@ async fn grep_reader<R: tokio::io::AsyncRead + Unpin + Send>(
299
404
  {
300
405
  break;
301
406
  }
302
- } else if use_context && found && after_remaining > 0 {
303
- // Print after-context line
304
- after_remaining -= 1;
305
- if !prefix.is_empty() {
306
- wprint!(w, "{}-", prefix)?;
307
- }
308
- if opts.line_number {
309
- wprint!(w, "{}-", lineno)?;
310
- }
311
- wprintln!(w, "{}", text)?;
312
- if after_remaining == 0 {
313
- need_sep = true;
314
- }
315
- } else if use_context {
316
- // Buffer for before-context
317
- if after_remaining == 0 && found && !need_sep {
318
- need_sep = true;
407
+ } else if pending > 0 {
408
+ if hidden {
409
+ // GNU lastout never passes a hidden line, so it spends the rest of the context.
410
+ pending = 0;
411
+ binary_matched |= !opts.only_matching;
412
+ } else {
413
+ pending -= 1;
414
+ // -o prints no context, but the line still counts as written.
415
+ if !opts.only_matching {
416
+ write_context(w, prefix, opts.line_number, lineno, raw).await?;
417
+ }
418
+ last_printed = lineno;
319
419
  }
320
- before_buf.push_back((lineno, text.to_string()));
420
+ } else if opts.before_context > 0 {
421
+ before_buf.push_back((lineno, raw.to_vec(), hidden));
321
422
  while before_buf.len() > opts.before_context {
322
423
  before_buf.pop_front();
323
424
  }
@@ -336,6 +437,9 @@ async fn grep_reader<R: tokio::io::AsyncRead + Unpin + Send>(
336
437
  if opts.list_non_matching && !found {
337
438
  wprintln!(w, "{}", prefix)?;
338
439
  }
440
+ if binary_matched && !summary_only && opts.binary_files == BinaryFiles::Binary {
441
+ wprintln!(ew, "grep: {}: binary file matches", name)?;
442
+ }
339
443
 
340
444
  Ok(found)
341
445
  }
@@ -377,12 +481,14 @@ async fn cmd_grep(os: &dyn Kernel, args: &[String]) -> CommandResult {
377
481
  let show_name = opts.with_filename.unwrap_or(multi);
378
482
 
379
483
  let mut w = io::stdout()?;
484
+ // stderr can only be taken once per process, so share one writer across files.
485
+ let mut ew = io::stderr()?;
380
486
  let mut any_match = false;
381
487
 
382
488
  if files.is_empty() {
383
489
  // Read from stdin
384
490
  let reader = io::stdin()?;
385
- if grep_reader(reader, &re, &opts, "", &mut w).await? {
491
+ if grep_reader(reader, &re, &opts, "", "(standard input)", &mut w, &mut ew).await? {
386
492
  any_match = true;
387
493
  }
388
494
  } else {
@@ -391,7 +497,6 @@ async fn cmd_grep(os: &dyn Kernel, args: &[String]) -> CommandResult {
391
497
  Ok(fd) => fd,
392
498
  Err(e) => {
393
499
  if !opts.quiet {
394
- let mut ew = io::stderr()?;
395
500
  wprintln!(ew, "grep: {}: {}", path, e)?;
396
501
  }
397
502
  continue;
@@ -399,10 +504,24 @@ async fn cmd_grep(os: &dyn Kernel, args: &[String]) -> CommandResult {
399
504
  };
400
505
  let reader = io::take_reader(fd)?;
401
506
  let prefix = if show_name { path.as_str() } else { "" };
402
- if grep_reader(reader, &re, &opts, prefix, &mut w).await? {
403
- any_match = true;
404
- if opts.quiet {
405
- return Ok(0);
507
+ match grep_reader(reader, &re, &opts, prefix, path, &mut w, &mut ew).await {
508
+ Ok(true) => {
509
+ any_match = true;
510
+ if opts.quiet {
511
+ return Ok(0);
512
+ }
513
+ }
514
+ Ok(false) => {}
515
+ Err(e) => {
516
+ // A closed stdout must stop grep, not repeat per file.
517
+ if e.downcast_ref::<std::io::Error>()
518
+ .is_some_and(|io| io.kind() == std::io::ErrorKind::BrokenPipe)
519
+ {
520
+ return Err(e);
521
+ }
522
+ if !opts.quiet {
523
+ wprintln!(ew, "grep: {}: {}", path, e)?;
524
+ }
406
525
  }
407
526
  }
408
527
  }