defmodule Nexus do @moduledoc """ Nexus can be used to define simple to complex CLI applications. The main component of Nexus is the macro `defcommand/2`, used to register CLI commands. Notice that the module that uses `Nexus` is defined as a complete CLI, with own commands and logic. To define a command you need to name it and pass some options: - `:type`: the argument type to be parsed to. The absense of this option will define a command without arguments, which can be used to define a subcommand group. See more on the [Types](#types) section. - `:required`: defines if the presence of the command is required or not. All commands are required by default. If you define a command as not required, you also need to define a default value. - `:default`: defines a default value for the command. It can be any term, but it must be of the same type as the `:type` option. ## Usage defmodule MyCLI do use Nexus defcommand :foo, type: :string, required?: true @impl true def handle_input(:foo, _args) do IO.puts("Hello :foo command!") end Nexus.parse() __MODULE__.run(System.argv()) end ## Types Nexus supports the following types: - `:string`: parses the argument as a string. This is the default type. - `:integer`: parses the argument as an integer. - `:float`: parses the argument as a float. - `:null`: parses the argument as a null value. This is useful to define subcommands. - `{:enum, values_list}`: parses the argument as a literal, but only if it is included into the `values_list` list. Note that current it only support string or atom values. """ @type command :: Nexus.Command.t() 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, defcommand: 3] require Nexus @behaviour Nexus.CLI end end @doc """ Like `def/2`, but registers a command that can be invoked from the command line. The `@doc` module attribute and the arguments metadata are used to generate the CLI options. Each defined command produces events that can be handled using the `Nexus.CLI` behaviour, where the event is the command name as an atom and the second argument is a list of arguments. """ @spec defcommand(atom, keyword) :: Macro.t() defmacro defcommand(cmd, opts) 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 description. You can also define your own `help` command, copying the `quote/2` block of this macro. """ defmacro help do quote do Nexus.defcommand(:help, type: :null, doc: "Prints this help message.") @impl Nexus.CLI def handle_input(:help, _args) do __MODULE__ |> Nexus.help() |> IO.puts() end end end @doc """ Generates three functions that can be used to manage and run your CLI. ### `__commands__/0` Return all commands that were defined into your CLI module. ### `run/1` Run your CLI against argv content. Notice that this function only runs a single command and returns `:ok`. It can be used to easily define mix tasks. Also this function expects that the `handle_input/2` callback from `Nexus.CLI` would have some implementation for the a comand `N` that would be parsed. ### `parse/1` Build a CLI based on argv content. It can be used if you want to manage your CLI or decide how you want to execute functions. It builds a map where given commands and options parsed will be keys and those values. #### Example {:ok, cli} = MyCLI.parse(System.argv) cli.mycommand # `arg` to `mycommand` """ defmacro parse do quote do defstruct Enum.map(@commands, &{&1.name, nil}) def __commands__, do: @commands def run(args) do raw = Enum.join(args, " ") Nexus.CommandDispatcher.dispatch!(__MODULE__, raw) end @spec parse(list(binary)) :: {:ok, Nexus.CLI.t()} | {:error, atom} def parse(args \\ System.argv()) do if is_list(args) do args |> Enum.join(" ") |> Nexus.CLI.build(__MODULE__) else Nexus.CLI.build(args, __MODULE__) end end end end @doc """ Given a module which defines a CLI with `Nexus`, builds a default help string that can be printed safelly. This function is used when you use the `help/0` macro. """ def help(cli_module) do cmds = cli_module.__commands__() banner = if function_exported?(cli_module, :banner, 0) do "#{cli_module.banner()}\n\n" end """ #{banner} COMMANDS: #{Enum.map_join(cmds, "\n", fn cmd -> " #{cmd.name} - #{cmd.doc}" end)} """ end def __make_command__!(module, cmd_name, opts) do opts |> Keyword.put(:name, cmd_name) |> Keyword.put(:module, module) |> Keyword.put_new(:required, false) |> Keyword.put_new(:type, :string) |> Nexus.Command.parse!() end end