diff --git a/CHANGELOG.md b/CHANGELOG.md index 166a6a7..ee50585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `${v//pat/rep}` / `${v/pat/rep}` refuse before allocating a result that would exceed `:max_value_bytes` ([#86](https://github.com/elixir-ai-tools/just_bash/issues/86)) * empty-string path operands are `ENOENT`, not the current directory ([#79](https://github.com/elixir-ai-tools/just_bash/issues/79)) * honour `--` as end-of-options for file-operand commands ([#83](https://github.com/elixir-ai-tools/just_bash/issues/83)) +* find and jq honour `--` as end-of-options: `find -- -foo` is a path, `jq -- .` is the filter ([#90](https://github.com/elixir-ai-tools/just_bash/issues/90)) * head default line count no longer emits a trailing blank line ([#80](https://github.com/elixir-ai-tools/just_bash/issues/80)) * tac no longer invents a newline on an unterminated last record ([#82](https://github.com/elixir-ai-tools/just_bash/issues/82)) * `/dev/null` exists as a filesystem node, so operands agree with redirects ([#78](https://github.com/elixir-ai-tools/just_bash/issues/78)) diff --git a/lib/just_bash/commands/find.ex b/lib/just_bash/commands/find.ex index e2d115f..fa9bea9 100644 --- a/lib/just_bash/commands/find.ex +++ b/lib/just_bash/commands/find.ex @@ -3,6 +3,7 @@ defmodule JustBash.Commands.Find do @behaviour JustBash.Commands.Command alias JustBash.Commands.Command + alias JustBash.Commands.StdinOperand alias JustBash.FS alias JustBash.Limit @@ -47,24 +48,27 @@ defmodule JustBash.Commands.Find do end defp parse_args(args) do - parse_args(args, %{ - paths: [], - name: nil, - iname: nil, - type: nil, - maxdepth: nil, - mindepth: nil, - empty: false, - print0: false, - exec_cmd: nil, - deadline: nil - }) + {option_args, extra} = StdinOperand.split_end_of_options(args) + + with {:ok, opts} <- + parse_args(option_args, %{ + paths: [], + name: nil, + iname: nil, + type: nil, + maxdepth: nil, + mindepth: nil, + empty: false, + print0: false, + exec_cmd: nil, + deadline: nil + }) do + {:ok, %{opts | paths: opts.paths ++ extra}} + end end defp parse_args([], opts), do: {:ok, opts} - defp parse_args(["--" | rest], opts), do: parse_args(rest, opts) - defp parse_args(["-name", pattern | rest], opts) do parse_args(rest, %{opts | name: pattern}) end diff --git a/lib/just_bash/commands/jq.ex b/lib/just_bash/commands/jq.ex index 670eca6..e16f250 100644 --- a/lib/just_bash/commands/jq.ex +++ b/lib/just_bash/commands/jq.ex @@ -196,15 +196,17 @@ defmodule JustBash.Commands.Jq do defp parse_args_with_end_of_options(args) do {option_args, extra} = StdinOperand.split_end_of_options(args) - with {:ok, opts} <- parse_args(option_args, default_opts()) do - attach_jq_operands(opts, extra) + with {:ok, opts, leftover} <- parse_args(option_args, default_opts()) do + # `--` only ends options. Leftover tokens from before `--` plus everything + # after it are the same positionals GNU jq uses: filter, then file. + attach_jq_operands(opts, leftover ++ extra) end end defp attach_jq_operands(opts, []), do: {:ok, opts} - defp attach_jq_operands(%{file: nil} = opts, [file]), do: {:ok, %{opts | file: file}} + defp attach_jq_operands(opts, [filter]), do: {:ok, %{opts | filter: filter}} - defp attach_jq_operands(%{filter: "."} = opts, [filter, file]), + defp attach_jq_operands(opts, [filter, file]), do: {:ok, %{opts | filter: filter, file: file}} defp attach_jq_operands(_opts, _), do: {:error, "jq: too many arguments\n"} @@ -226,9 +228,9 @@ defmodule JustBash.Commands.Jq do } end - defp parse_args([], opts), do: {:ok, opts} + defp parse_args([], opts), do: {:ok, opts, []} - defp parse_args(["--help" | _], opts), do: {:ok, %{opts | help: true}} + defp parse_args(["--help" | _], opts), do: {:ok, %{opts | help: true}, []} defp parse_args(["-r" | rest], opts), do: parse_args(rest, %{opts | raw_output: true}) defp parse_args(["--raw-output" | rest], opts), do: parse_args(rest, %{opts | raw_output: true}) @@ -284,17 +286,7 @@ defmodule JustBash.Commands.Jq do {:error, "jq: Unknown option: #{flag}\n"} end - defp parse_args([filter | rest], opts) do - if opts.filter == "." do - case rest do - [file] -> {:ok, %{opts | filter: filter, file: file}} - [] -> {:ok, %{opts | filter: filter}} - _ -> {:error, "jq: too many arguments\n"} - end - else - {:ok, %{opts | file: filter}} - end - end + defp parse_args(positionals, opts), do: {:ok, opts, positionals} defp help_text do """ diff --git a/test/commands/end_of_options_test.exs b/test/commands/end_of_options_test.exs index 7578532..811ecf2 100644 --- a/test/commands/end_of_options_test.exs +++ b/test/commands/end_of_options_test.exs @@ -240,6 +240,93 @@ defmodule JustBash.Commands.EndOfOptionsTest do end end + # Issue #90: find kept parsing predicates after `--`, and jq treated the + # first extra token as a file. `--` ends options; what follows is operands. + describe "find -- treats the remainder as paths" do + test "find -- -foo searches a path named -foo, not a predicate" do + bash = JustBash.new(files: %{"/-foo/x" => "x\n"}, cwd: "/") + {result, _} = JustBash.exec(bash, "find -- -foo") + + assert result.exit_code == 0 + assert result.stderr == "" + refute result.stderr =~ "unknown predicate" + assert result.stdout =~ "-foo" + assert result.stdout =~ "x" + end + + test "find -- -foo does not raise from exec/2 when the path is missing" do + {result, _} = JustBash.exec(JustBash.new(), "find -- -foo") + + assert result.exit_code != 0 + assert result.stderr =~ "No such file" + refute result.stderr =~ "unknown predicate" + refute result.stderr =~ "crashed" + end + end + + describe "jq -- keeps the filter as a positional, not a file" do + test "jq -- . applies the identity filter to stdin" do + {result, _} = JustBash.exec(JustBash.new(), ~S[printf '{"k":1}\n' | jq -- .]) + + assert result.exit_code == 0 + assert result.stderr == "" + refute result.stderr =~ "No such file" + assert result.stdout =~ "\"k\"" + end + + test "jq -- . does not raise from exec/2" do + {result, _} = JustBash.exec(JustBash.new(), "jq -- .") + + # Empty stdin is valid JSON-enough for the identity filter; the contract + # is only that the host gets a result back. + assert is_integer(result.exit_code) + refute result.stderr =~ "crashed" + end + + test "jq . -- FILE still reads the file after an explicit filter" do + {result, _} = JustBash.exec(sandbox(), "jq . -- /j.json") + + assert result.exit_code == 0 + assert result.stderr == "" + assert result.stdout =~ "\"k\"" + end + end + + describe "additional -- placements" do + test "-- as the only argument is not opened as a file" do + {cat, _} = JustBash.exec(sandbox(), "printf hello | cat --") + assert cat.exit_code == 0 + assert cat.stdout == "hello" + refute cat.stderr =~ "--" + + {find, _} = JustBash.exec(sandbox(), "cd /d; find --") + assert find.exit_code == 0 + assert find.stdout =~ "." + refute find.stderr =~ "unknown predicate" + + {jq, _} = JustBash.exec(sandbox(), ~S[printf '{"k":1}\n' | jq --]) + assert jq.exit_code == 0 + assert jq.stdout =~ "\"k\"" + end + + test "a file named -- is readable after --" do + bash = JustBash.new(files: %{"/--" => "dashdash\n"}, cwd: "/") + {result, _} = JustBash.exec(bash, "cat -- --") + + assert result.exit_code == 0 + assert result.stderr == "" + assert result.stdout == "dashdash\n" + end + + test "-- between two operands does not consume either operand" do + {result, _} = JustBash.exec(sandbox(), "cat /f -- /g") + + assert result.exit_code == 0 + assert result.stderr == "" + assert result.stdout == @content <> @other + end + end + defp sandbox do JustBash.new( files: %{