diff --git a/examples/escript/example.ex b/examples/escript/example.ex index 62f41df..f0bd61d 100644 --- a/examples/escript/example.ex +++ b/examples/escript/example.ex @@ -18,6 +18,10 @@ defmodule Escript.Example do defcommand :foo, required: true, type: :string, doc: "Command that receives a string as argument and prints it." defcommand :fizzbuzz, type: {:enum, ~w(fizz buzz)a}, doc: "Fizz bUZZ", required: true + defcommand :foo_bar, type: :null, doc: "Teste" do + defcommand :foo, default: "hello", doc: "Hello" + defcommand :bar, default: "hello", doc: "Hello" + end @impl true def version, do: "0.1.0" @@ -35,6 +39,16 @@ defmodule Escript.Example do IO.puts("fizz") end + def handle_input(:foo_bar, %{value: _, subcommand: :foo}) do + # do something wth "foo" value + :ok + end + + def handle_input(:foo_bar, %{value: _, subcommand: :bar}) do + # do something wth "bar" value + :ok + end + Nexus.help() Nexus.parse() diff --git a/lib/nexus.ex b/lib/nexus.ex index 359c892..2ff5b8e 100644 --- a/lib/nexus.ex +++ b/lib/nexus.ex @@ -46,8 +46,9 @@ defmodule Nexus do defmacro __using__(_opts) do quote do Module.register_attribute(__MODULE__, :commands, accumulate: true) + Module.register_attribute(__MODULE__, :subcommands, accumulate: true) - import Nexus, only: [defcommand: 2] + import Nexus, only: [defcommand: 2, defcommand: 3] require Nexus @behaviour Nexus.CLI @@ -65,11 +66,33 @@ defmodule Nexus do """ @spec defcommand(atom, keyword) :: Macro.t() defmacro defcommand(cmd, opts) do - quote do + quote location: :keep do @commands Nexus.__make_command__!(__MODULE__, unquote(cmd), unquote(opts)) end end + defmacro defcommand(cmd, opts, do: ast) do + subcommands = build_subcommands(ast) + + quote location: :keep do + @commands Nexus.__make_command__!( + __MODULE__, + unquote(cmd), + Keyword.put(unquote(opts), :subcommands, unquote(subcommands)) + ) + end + end + + defp build_subcommands({:__block__, _, subs}) do + Enum.map(subs, &build_subcommands/1) + end + + defp build_subcommands({:defcommand, _, [cmd, opts]}) do + quote do + Nexus.__make_command__!(__MODULE__, unquote(cmd), unquote(opts)) + end + end + @doc """ Generates a default `help` command for your CLI. It uses the optional `banner/0` callback from `Nexus.CLI` to complement diff --git a/lib/nexus/command.ex b/lib/nexus/command.ex index 22f3cc8..0940fba 100644 --- a/lib/nexus/command.ex +++ b/lib/nexus/command.ex @@ -12,11 +12,18 @@ defmodule Nexus.Command do required: boolean, name: atom, default: term, - doc: String.t() + doc: String.t(), + subcommands: [Nexus.Command.t()] } @enforce_keys ~w(module type name)a - defstruct module: nil, required: true, type: :string, name: nil, default: nil, doc: "" + defstruct module: nil, + required: true, + type: :string, + name: nil, + default: nil, + doc: "", + subcommands: [] @spec parse!(keyword) :: Nexus.Command.t() def parse!(attrs) do diff --git a/lib/nexus/command/input.ex b/lib/nexus/command/input.ex index 17b3ef5..054c4af 100644 --- a/lib/nexus/command/input.ex +++ b/lib/nexus/command/input.ex @@ -4,10 +4,10 @@ defmodule Nexus.Command.Input do on commands dispatched """ - @type t :: %__MODULE__{value: term, raw: binary} + @type t :: %__MODULE__{value: term, raw: binary, subcommand: atom} @enforce_keys ~w(value raw)a - defstruct value: nil, raw: nil + defstruct value: nil, raw: nil, subcommand: nil @spec parse!(term, binary) :: Nexus.Command.Input.t() def parse!(value, raw) do diff --git a/lib/nexus/parser.ex b/lib/nexus/parser.ex index 470173d..7879e78 100644 --- a/lib/nexus/parser.ex +++ b/lib/nexus/parser.ex @@ -14,8 +14,28 @@ defmodule Nexus.Parser do |> String.trim_leading() |> parse_command(cmd) |> case do - {:ok, input} -> input - {:error, _} -> raise Error, "Failed to parse command #{inspect(cmd)}" + {:ok, input} -> + input + + {:error, _} -> + raise Error, "Failed to parse command #{inspect(cmd)}" + + :error -> + raise Error, + "Failed to parse command #{inspect(cmd)} with subcommands #{inspect(cmd.subcommands)}" + end + end + + defp parse_subcommand(input, cmd) do + case parse_command(input, cmd) do + {:ok, input} -> {:halt, {:ok, Map.put(input, :subcommand, cmd.name)}} + {:error, _} -> {:cont, :error} + end + end + + defp parse_command(input, %Command{type: :null, subcommands: [_ | _] = subs} = cmd) do + with {:ok, {_, rest}} <- literal(input, cmd.name) do + Enum.reduce_while(subs, :error, fn sub, _acc -> parse_subcommand(rest, sub) end) end end diff --git a/mix.exs b/mix.exs index a99f71f..74e291b 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Nexus.MixProject do use Mix.Project - @version "0.3.1" + @version "0.4.0" @source_url "https://github.com/zoedsoupe/nexus" def project do @@ -40,7 +40,7 @@ defmodule Nexus.MixProject do licenses: ["WTFPL"], contributors: ["zoedsoupe"], links: %{"GitHub" => @source_url}, - files: ~w(lib/nexus lib/nexus.ex LICENSE README.md mix.*) + files: ~w(lib/nexus lib/nexus.ex LICENSE README.md mix.* examples) } end