Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
32 changes: 18 additions & 14 deletions lib/just_bash/commands/find.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
26 changes: 9 additions & 17 deletions lib/just_bash/commands/jq.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand All @@ -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})
Expand Down Expand Up @@ -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
"""
Expand Down
87 changes: 87 additions & 0 deletions test/commands/end_of_options_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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: %{
Expand Down
Loading