• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rzcastilho / do_it / 977056ba9b908bcaee19f038a4f7cd37a699a52b

16 Oct 2023 06:29PM UTC coverage: 88.855% (+11.5%) from 77.397%
977056ba9b908bcaee19f038a4f7cd37a699a52b

push

github

web-flow
Feature/sub commands (#14)

* feat: :sparkles: subcommands support

* feat: :children_crossing: fix and improve help commands

* chore: :heavy_plus_sign: add castore

* test: :white_check_mark: add tests

* chore: :coffin: remove unused code

* docs: :memo: update documentation

* chore: :green_heart: adjust plublish CI hook

* chore: :bookmark: update version number

41 of 41 new or added lines in 4 files covered. (100.0%)

295 of 332 relevant lines covered (88.86%)

43.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/lib/do_it/command.ex
1
defmodule DoIt.Command do
2
  @moduledoc false
3

4
  alias DoIt.{Argument, Option}
5

6
  defmacro __using__(opts) do
7
    quote do
8
      case List.keyfind(unquote(opts), :command, 0) do
9
        {:command, command} ->
10
          Module.put_attribute(__MODULE__, :command, command)
11

12
        _ ->
13
          Module.put_attribute(
14
            __MODULE__,
15
            :command,
16
            __ENV__.module
17
            |> Atom.to_string()
18
            |> String.split(".")
19
            |> List.last()
20
            |> Macro.underscore()
21
          )
22
      end
23

24
      case List.keyfind(unquote(opts), :description, 0) do
25
        {:description, description} ->
26
          Module.put_attribute(__MODULE__, :description, description)
27

28
        _ ->
29
          raise(DoIt.CommandDefinitionError, "description is required for command definition")
30
      end
31

32
      Module.register_attribute(__MODULE__, :subcommands, accumulate: true, persist: false)
33

34
      Module.register_attribute(__MODULE__, :subcommand_descriptions,
35
        accumulate: true,
36
        persist: false
37
      )
38

39
      Module.register_attribute(__MODULE__, :arguments, accumulate: true)
40
      Module.register_attribute(__MODULE__, :options, accumulate: true)
41
      Module.register_attribute(__MODULE__, :strict, accumulate: true)
42
      Module.register_attribute(__MODULE__, :aliases, accumulate: true)
43

44
      Module.put_attribute(__MODULE__, :options, %Option{
45
        name: :help,
46
        type: :boolean,
47
        description: "Print this help"
48
      })
49

50
      Module.put_attribute(__MODULE__, :strict, {:help, :boolean})
51

52
      import unquote(__MODULE__),
53
        only: [subcommand: 1, argument: 3, argument: 4, option: 3, option: 4]
54

55
      @before_compile unquote(__MODULE__)
56
    end
57
  end
58

59
  defmacro __before_compile__(env) do
×
60
    compile(Module.get_attribute(env.module, :subcommands), __CALLER__)
×
61
  end
62

63
  def compile([], caller) do
64
    caller_module = caller.module
×
65

66
    quote do
67
      @behaviour DoIt.Runner
68
      alias DoIt.Commfig
69

70
      def command(), do: {@command, @description}
71

72
      def help(%{breadcrumb: breadcrumb}) do
73
        DoIt.Output.print_help(
74
          commands: breadcrumb ++ [unquote(caller_module)],
75
          description: @description,
76
          arguments: @arguments,
77
          options: @options
78
        )
79
      end
80

81
      def do_it(args, context) do
82
        case OptionParser.parse(args, strict: @strict, aliases: @aliases) do
83
          {options, arguments, []} ->
84
            if {:help, true} in options do
85
              help(context)
86
            else
87
              with {:ok, parsed_arguments} <- Argument.parse_input(@arguments, arguments),
88
                   {:ok, parsed_options} <- Option.parse_input(@options, options),
89
                   {:ok, validated_arguments} <-
90
                     Argument.validate_input(@arguments, parsed_arguments),
91
                   {:ok, validated_options} <- Option.validate_input(@options, parsed_options) do
92
                run(
93
                  Enum.into(validated_arguments, %{}),
94
                  Enum.into(validated_options, %{}),
95
                  context
96
                )
97
              else
98
                {:error, message} ->
99
                  DoIt.Output.print_errors(message)
100
                  help(context)
101
              end
102
            end
103

104
          {_, _, invalid_options} ->
105
            DoIt.Output.print_invalid_options(@command, invalid_options)
106

107
            help(context)
108
        end
109
      end
110
    end
111
  end
112

113
  def compile(subcommands, caller) do
114
    caller_module = caller.module
×
115

116
    subcommands_ast =
×
117
      for module <- subcommands do
×
118
        with {subcommand, _} <- module.command() do
×
119
          quote do
120
            def do_it([unquote(subcommand) | args], context) do
121
              %{breadcrumb: breadcrumb} = context
122

123
              apply(String.to_existing_atom("#{unquote(module)}"), :do_it, [
124
                args,
125
                %{context | breadcrumb: breadcrumb ++ [unquote(caller.module)]}
×
126
              ])
127
            end
128
          end
129
        end
130
      end
131

132
    quote do
133
      def command(), do: {@command, @description}
134

135
      def help(%{breadcrumb: breadcrumb}) do
136
        DoIt.Output.print_help(
137
          commands: breadcrumb ++ [unquote(caller_module)],
138
          description: @description,
139
          subcommands: @subcommand_descriptions
140
        )
141
      end
142

143
      def do_it(["--help" | _], context), do: help(context)
144

145
      unquote(subcommands_ast)
146

147
      def do_it([unknown | _], context) do
148
        IO.puts("invalid subcommand #{unknown}")
149
        help(context)
150
      end
151

152
      def do_it([], context) do
153
        IO.puts("no subcommand provided")
154
        help(context)
155
      end
156
    end
157
  end
158

159
  defmacro subcommand(module) do
160
    quote do
161
      @subcommands unquote(module)
162
      with {name, description} <- unquote(module).command() do
163
        @subcommand_descriptions %{name: name, description: description}
164
      end
165
    end
166
  end
167

168
  defmacro argument(name, type, description, opts \\ []) do
169
    quote do
170
      argument =
171
        struct(
172
          %Argument{name: unquote(name), type: unquote(type), description: unquote(description)},
173
          unquote(opts)
174
        )
175
        |> Argument.validate_definition()
176

177
      Module.put_attribute(__MODULE__, :arguments, argument)
178
    end
179
  end
180

181
  defmacro option(name, type, description, opts \\ []) do
182
    quote do
183
      option =
184
        struct(
185
          %Option{name: unquote(name), type: unquote(type), description: unquote(description)},
186
          unquote(opts)
187
        )
188
        |> Option.validate_definition()
189

190
      Module.put_attribute(__MODULE__, :options, option)
191

192
      case List.keyfind(unquote(opts), :keep, 0) do
193
        {:keep, true} ->
194
          Module.put_attribute(__MODULE__, :strict, {unquote(name), [unquote(type), :keep]})
195

196
        _ ->
197
          Module.put_attribute(__MODULE__, :strict, {unquote(name), unquote(type)})
198
      end
199

200
      case List.keymember?(unquote(opts), :alias, 0) do
201
        true ->
202
          Module.put_attribute(__MODULE__, :aliases, {unquote(opts[:alias]), unquote(name)})
203

204
        _ ->
205
          nil
206
      end
207
    end
208
  end
209
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc