From c760a5a4af71a46a1400b345c3e55a2c18625104 Mon Sep 17 00:00:00 2001 From: Leandro Pereira Date: Tue, 28 Mar 2023 12:12:33 -0400 Subject: Add `:return_separator` to `OptionParser` (#12499) --- lib/elixir/lib/option_parser.ex | 31 +++++++++++++-- lib/elixir/test/elixir/option_parser_test.exs | 17 ++++++++ lib/mix/lib/mix/tasks/run.ex | 4 +- lib/mix/test/mix/tasks/run_test.exs | 57 +++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/option_parser.ex b/lib/elixir/lib/option_parser.ex index 89a76f057..cce5cd63c 100644 --- a/lib/elixir/lib/option_parser.ex +++ b/lib/elixir/lib/option_parser.ex @@ -33,7 +33,8 @@ defmodule OptionParser do switches: keyword, strict: keyword, aliases: keyword, - allow_nonexistent_atoms: boolean + allow_nonexistent_atoms: boolean, + return_separator: boolean ] defmodule ParseError do @@ -83,6 +84,7 @@ defmodule OptionParser do * `:switches` or `:strict` - see the "Switch definitions" section below * `:allow_nonexistent_atoms` - see the "Parsing unknown switches" section below * `:aliases` - see the "Aliases" section below + * `:return_separator` - see the "Return separator" section below ## Switch definitions @@ -230,6 +232,21 @@ defmodule OptionParser do ...> ) {[unlock: "path/to/file", unlock: "path/to/another/file"], [], []} + ## Return separator + + The separator `--` implies options should no longer be processed. + By default, the separator is not returned as parts of the arguments, + but that can be changed via the `:return_separator` option: + + iex> OptionParser.parse(["--", "lib"], return_separator: true, strict: []) + {[], ["--", "lib"], []} + + iex> OptionParser.parse(["--no-halt", "--", "lib"], return_separator: true, switches: [halt: :boolean]) + {[halt: false], ["--", "lib"], []} + + iex> OptionParser.parse(["script.exs", "--no-halt", "--", "foo"], return_separator: true, switches: [halt: :boolean]) + {[{:halt, false}], ["script.exs", "--", "foo"], []} + """ @spec parse(argv, options) :: {parsed, argv, errors} def parse(argv, opts \\ []) when is_list(argv) and is_list(opts) do @@ -363,8 +380,15 @@ defmodule OptionParser do invalid = if config.strict?, do: [{option, nil} | invalid], else: invalid do_parse(rest, config, opts, args, invalid, all?) - {:error, ["--" | rest]} -> - {Enum.reverse(opts), Enum.reverse(args, rest), Enum.reverse(invalid)} + {:error, ["--" | rest] = remaining_args} -> + args = + if config.return_separator? do + Enum.reverse(args, remaining_args) + else + Enum.reverse(args, rest) + end + + {Enum.reverse(opts), args, Enum.reverse(invalid)} {:error, [arg | rest] = remaining_args} -> # there is no option @@ -616,6 +640,7 @@ defmodule OptionParser do %{ aliases: opts[:aliases] || [], allow_nonexistent_atoms?: opts[:allow_nonexistent_atoms] || false, + return_separator?: opts[:return_separator] || false, strict?: strict?, switches: switches } diff --git a/lib/elixir/test/elixir/option_parser_test.exs b/lib/elixir/test/elixir/option_parser_test.exs index 7f38e4264..c589a1165 100644 --- a/lib/elixir/test/elixir/option_parser_test.exs +++ b/lib/elixir/test/elixir/option_parser_test.exs @@ -148,6 +148,23 @@ defmodule OptionParserTest do ) == {[source: "foo"], ["bar", "--", "-x"], []} end + test "return separators" do + assert OptionParser.parse_head(["--", "foo"], + switches: [], + return_separator: true + ) == {[], ["--", "foo"], []} + + assert OptionParser.parse_head(["--no-halt", "--", "foo"], + switches: [halt: :boolean], + return_separator: true + ) == {[halt: false], ["--", "foo"], []} + + assert OptionParser.parse_head(["foo.exs", "--no-halt", "--", "foo"], + switches: [halt: :boolean], + return_separator: true + ) == {[], ["foo.exs", "--no-halt", "--", "foo"], []} + end + test "parses - as argument" do argv = ["--foo", "-", "-b", "-"] opts = [strict: [foo: :boolean, boo: :string], aliases: [b: :boo]] diff --git a/lib/mix/lib/mix/tasks/run.ex b/lib/mix/lib/mix/tasks/run.ex index 3eb30e66d..3eab6eb42 100644 --- a/lib/mix/lib/mix/tasks/run.ex +++ b/lib/mix/lib/mix/tasks/run.ex @@ -78,7 +78,8 @@ defmodule Mix.Tasks.Run do elixir_version_check: :boolean, parallel_require: :keep, preload_modules: :boolean - ] + ], + return_separator: true ) run(args, opts, head, &Code.eval_string/1, &Code.require_file/1) @@ -112,6 +113,7 @@ defmodule Mix.Tasks.Run do {file, argv} = case {Keyword.has_key?(opts, :eval), head} do + {_, ["--" | rest]} -> {nil, rest} {true, _} -> {nil, head} {_, [head | tail]} -> {head, tail} {_, []} -> {nil, []} diff --git a/lib/mix/test/mix/tasks/run_test.exs b/lib/mix/test/mix/tasks/run_test.exs index 9f25d6a1d..6845fdfef 100644 --- a/lib/mix/test/mix/tasks/run_test.exs +++ b/lib/mix/test/mix/tasks/run_test.exs @@ -80,6 +80,14 @@ defmodule Mix.Tasks.RunTest do Mix.Tasks.Run.run([file, "foo", "-e", "bar"]) assert_received {:argv, ["foo", "-e", "bar"]} + unload_file.() + Mix.Tasks.Run.run([file, "foo", "--", "bar"]) + assert_received {:argv, ["foo", "--", "bar"]} + + unload_file.() + Mix.Tasks.Run.run([file, "--custom-opt", "foo", "--", "bar"]) + assert_received {:argv, ["--custom-opt", "foo", "--", "bar"]} + unload_file.() Mix.Tasks.Run.run(["-e", expr, file, "foo", "-x", "bar"]) assert_received {:argv, [^file, "foo", "-x", "bar"]} @@ -93,4 +101,53 @@ defmodule Mix.Tasks.RunTest do assert_received {:argv, [^file, "-x", "bar"]} end) end + + defmodule AppArgvSample do + def project do + [app: :app_argv_sample, version: "0.1.0"] + end + + def application do + send(self(), {:argv, System.argv()}) + [extra_applications: [:logger]] + end + end + + describe "rewrite System.argv without file arg" do + setup do + Mix.Project.pop() + Mix.Project.push(AppArgvSample) + :ok + end + + test "no args" do + in_fixture("no_mixfile", fn -> + Mix.Tasks.Run.run(["--"]) + assert_received {:argv, []} + end) + end + + test "multiple args" do + in_fixture("no_mixfile", fn -> + Mix.Tasks.Run.run(["--", "foo", "bar"]) + assert_received {:argv, ["foo", "bar"]} + end) + end + + test "with opts" do + in_fixture("no_mixfile", fn -> + Mix.Tasks.Run.run(["--no-start", "--", "foo", "bar"]) + assert_received {:argv, ["foo", "bar"]} + end) + end + + test "with eval" do + in_fixture("no_mixfile", fn -> + expr = "send self(), {:test, :argv}" + Mix.Tasks.Run.run(["-e", expr, "--", "foo"]) + assert_received {:argv, ["foo"]} + assert_received {:test, :argv} + end) + end + end end -- cgit v1.2.1