sod 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +2 -0
- data/LICENSE.adoc +134 -0
- data/README.adoc +952 -0
- data/lib/sod/action.rb +84 -0
- data/lib/sod/command.rb +74 -0
- data/lib/sod/container.rb +18 -0
- data/lib/sod/context.rb +33 -0
- data/lib/sod/error.rb +7 -0
- data/lib/sod/graph/loader.rb +40 -0
- data/lib/sod/graph/node.rb +107 -0
- data/lib/sod/graph/runner.rb +71 -0
- data/lib/sod/import.rb +7 -0
- data/lib/sod/models/action.rb +38 -0
- data/lib/sod/models/command.rb +11 -0
- data/lib/sod/prefabs/actions/config/create.rb +76 -0
- data/lib/sod/prefabs/actions/config/delete.rb +60 -0
- data/lib/sod/prefabs/actions/config/edit.rb +47 -0
- data/lib/sod/prefabs/actions/config/view.rb +47 -0
- data/lib/sod/prefabs/actions/help.rb +34 -0
- data/lib/sod/prefabs/actions/version.rb +27 -0
- data/lib/sod/prefabs/commands/config.rb +21 -0
- data/lib/sod/presenters/action.rb +54 -0
- data/lib/sod/presenters/node.rb +95 -0
- data/lib/sod/refines/option_parsers.rb +23 -0
- data/lib/sod/shell.rb +33 -0
- data/lib/sod/types/pathname.rb +6 -0
- data/lib/sod.rb +13 -0
- data/sod.gemspec +35 -0
- data.tar.gz.sig +4 -0
- metadata +190 -0
- metadata.gz.sig +0 -0
data/README.adoc
ADDED
@@ -0,0 +1,952 @@
|
|
1
|
+
:toc: macro
|
2
|
+
:toclevels: 5
|
3
|
+
:figure-caption!:
|
4
|
+
|
5
|
+
:cogger_link: link:https://alchemists.io/projects/cogger[Cogger]
|
6
|
+
:dry_container_link: link:https://dry-rb.org/gems/dry-container[Dry Container]
|
7
|
+
:etcher_link: link:https://alchemists.io/projects/etcher[Etcher]
|
8
|
+
:gemsmith_link: link:https://alchemists.io/projects/gemsmith[Gemsmith]
|
9
|
+
:git-lint_link: link:https://alchemists.io/projects/git-lint[Git Lint]
|
10
|
+
:hanamismith_link: link:https://alchemists.io/projects/hanamismith[Hanamismith]
|
11
|
+
:infusible_link: link:https://alchemists.io/projects/infusible[Infusible]
|
12
|
+
:milestoner_link: link:https://alchemists.io/projects/milestoner[Milestoner]
|
13
|
+
:option_parser_link: link:https://rubyapi.org/o/s?q=OptionParser[Option Parser]
|
14
|
+
:pennyworth_link: link:https://alchemists.io/projects/pennyworth[Pennyworth]
|
15
|
+
:pragmater_link: link:https://alchemists.io/projects/pragmater[Pragmater]
|
16
|
+
:rake_link: link:https://github.com/ruby/rake[Rake]
|
17
|
+
:rubysmith_link: link:https://alchemists.io/projects/rubysmith[Rubysmith]
|
18
|
+
:runcom_link: link:https://alchemists.io/projects/runcom[Runcom]
|
19
|
+
:spek_link: link:https://alchemists.io/projects/spek[Spek]
|
20
|
+
:sublime_text_kit_link: link:https://alchemists.io/projects/sublime_text_kit[Sublime Text Kit]
|
21
|
+
:tocer_link: link:https://alchemists.io/projects/tocer[Tocer]
|
22
|
+
:tone_link: link:https://alchemists.io/projects/tone[Tone]
|
23
|
+
:versionaire_link: link:https://alchemists.io/projects/versionaire[Versionaire]
|
24
|
+
:xdg_link: link:https://alchemists.io/projects/xdg[XDG]
|
25
|
+
|
26
|
+
= Sod
|
27
|
+
|
28
|
+
Sod -- as in the ground upon which you stand -- provides a Domain Specific Language (DSL) for creating reusable Command Line Interfaces (CLIs). This gem builds upon and enhances native {option_parser_link} behavior by smoothing out the rough edges you wish {option_parser_link} didn't have.
|
29
|
+
|
30
|
+
toc::[]
|
31
|
+
|
32
|
+
== Features
|
33
|
+
|
34
|
+
- Builds upon and enhances native {option_parser_link} functionality.
|
35
|
+
- Provides a simple DSL for composing reusable CLI commands and actions.
|
36
|
+
- Provides a blank slate that is fully customizable to your needs.
|
37
|
+
- Provides prefabricated commands and actions for quick setup and experimentation.
|
38
|
+
- Uses {infusible_link} for function composition.
|
39
|
+
- Uses {tone_link} for colorized documentation.
|
40
|
+
- Uses {cogger_link} for colorized logging.
|
41
|
+
|
42
|
+
== Screenshots
|
43
|
+
|
44
|
+
*DSL*
|
45
|
+
|
46
|
+
image::https://alchemists.io/images/projects/sod/screenshots/dsl.png[A screenshot of the DSL syntax,width=597,height=512,role=focal_point]
|
47
|
+
|
48
|
+
*Output*
|
49
|
+
|
50
|
+
image::https://alchemists.io/images/projects/sod/screenshots/output.png[A screenshot of the generated help documentation,width=473,height=500,role=focal_point]
|
51
|
+
|
52
|
+
== Requirements
|
53
|
+
|
54
|
+
. link:https://www.ruby-lang.org[Ruby].
|
55
|
+
. Familiarity with {option_parser_link} syntax and behavior.
|
56
|
+
|
57
|
+
== Setup
|
58
|
+
|
59
|
+
To install _with_ security, run:
|
60
|
+
|
61
|
+
[source,bash]
|
62
|
+
----
|
63
|
+
# 💡 Skip this line if you already have the public certificate installed.
|
64
|
+
gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem)
|
65
|
+
gem install sod --trust-policy HighSecurity
|
66
|
+
----
|
67
|
+
|
68
|
+
To install _without_ security, run:
|
69
|
+
|
70
|
+
[source,bash]
|
71
|
+
----
|
72
|
+
gem install sod
|
73
|
+
----
|
74
|
+
|
75
|
+
You can also add the gem directly to your project:
|
76
|
+
|
77
|
+
[source,bash]
|
78
|
+
----
|
79
|
+
bundle add sod
|
80
|
+
----
|
81
|
+
|
82
|
+
Once the gem is installed, you only need to require it:
|
83
|
+
|
84
|
+
[source,ruby]
|
85
|
+
----
|
86
|
+
require "sod"
|
87
|
+
----
|
88
|
+
|
89
|
+
== Usage
|
90
|
+
|
91
|
+
Creating and calling a CLI is as simple as:
|
92
|
+
|
93
|
+
[source,ruby]
|
94
|
+
----
|
95
|
+
Sod.new.call
|
96
|
+
# nil
|
97
|
+
----
|
98
|
+
|
99
|
+
Granted, the above isn't terribly exciting -- in terms of initial behavior -- but illustrates how default behavior provides a _blank slate_ from which to mold custom behavior as you like. To provide minimum functionality, you'll want to give your CLI a name, banner, and throw in the prefabricated help action:
|
100
|
+
|
101
|
+
[source,ruby]
|
102
|
+
----
|
103
|
+
cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
|
104
|
+
on Sod::Prefabs::Actions::Help, self
|
105
|
+
end
|
106
|
+
|
107
|
+
cli.call
|
108
|
+
|
109
|
+
# Demo 0.0.0: A demonstration.
|
110
|
+
#
|
111
|
+
# USAGE
|
112
|
+
# demo [OPTIONS]
|
113
|
+
#
|
114
|
+
# OPTIONS
|
115
|
+
# -h, --help [COMMAND] Show this message.
|
116
|
+
----
|
117
|
+
|
118
|
+
Notice, with only a few extra lines of code, you can build upon the initial _blank slate_ provided for you and start to see your custom CLI take form. You can even take this a step further and outline the structure of your CLI with _inline commands_:
|
119
|
+
|
120
|
+
[source,ruby]
|
121
|
+
----
|
122
|
+
cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
|
123
|
+
on Sod::Prefabs::Actions::Help, self
|
124
|
+
|
125
|
+
on "generate", "Generate project templates."
|
126
|
+
on "db", "Manage database."
|
127
|
+
end
|
128
|
+
|
129
|
+
cli.call
|
130
|
+
|
131
|
+
# Demo 0.0.0: A demonstration.
|
132
|
+
#
|
133
|
+
# USAGE
|
134
|
+
# demo [OPTIONS]
|
135
|
+
# demo COMMAND [OPTIONS]
|
136
|
+
#
|
137
|
+
# OPTIONS
|
138
|
+
# -h, --help [COMMAND] Show this message.
|
139
|
+
#
|
140
|
+
# COMMANDS
|
141
|
+
# generate Generate project templates.
|
142
|
+
# db Manage database.
|
143
|
+
----
|
144
|
+
|
145
|
+
We'll dive into the defaults, prefabrications, and custom commands/actions soon but knowing a _help_ action is provided for you is a good first step in learning how to build your own custom CLI.
|
146
|
+
|
147
|
+
=== Name
|
148
|
+
|
149
|
+
A good CLI needs a name and, by default, this is the name of file, script, or IRB session you are currently creating your CLI instance in. For example, when using this project's `bin/console` script, my CLI name is:
|
150
|
+
|
151
|
+
[source,ruby]
|
152
|
+
----
|
153
|
+
Sod.new.name # "console"
|
154
|
+
----
|
155
|
+
|
156
|
+
The default name is automatically acquired via the `$PROGRAM_NAME` global variable. Any file extension is immediately trimmed which means creating your CLI instance within a `demo.rb` file will have a name of `"demo"`. Should this not be desired, you can customize further by providing your own name:
|
157
|
+
|
158
|
+
[source,ruby]
|
159
|
+
----
|
160
|
+
# With a symbol.
|
161
|
+
Sod.new(:demo).name # "demo"
|
162
|
+
|
163
|
+
# With a string.
|
164
|
+
Sod.new("demo").name # "demo"
|
165
|
+
----
|
166
|
+
|
167
|
+
When using the prefabricated help action, the name of your CLI will also show up in the usage documentation:
|
168
|
+
|
169
|
+
[source,ruby]
|
170
|
+
----
|
171
|
+
Sod.new(:demo) { on Sod::Prefabs::Actions::Help, self }
|
172
|
+
.call
|
173
|
+
|
174
|
+
# USAGE
|
175
|
+
# demo [OPTIONS]
|
176
|
+
#
|
177
|
+
# OPTIONS
|
178
|
+
# -h, --help [COMMAND] Show this message.
|
179
|
+
----
|
180
|
+
|
181
|
+
=== Banner
|
182
|
+
|
183
|
+
The banner is optional but strongly encouraged because it allows you to give your CLI a label and short description. Example:
|
184
|
+
|
185
|
+
[source,ruby]
|
186
|
+
----
|
187
|
+
cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
|
188
|
+
on Sod::Prefabs::Actions::Help, self
|
189
|
+
end
|
190
|
+
|
191
|
+
cli.call
|
192
|
+
|
193
|
+
# Demo 0.0.0: A demonstration.
|
194
|
+
#
|
195
|
+
# USAGE
|
196
|
+
# demo [OPTIONS]
|
197
|
+
#
|
198
|
+
# OPTIONS
|
199
|
+
# -h, --help [COMMAND] Show this message.
|
200
|
+
----
|
201
|
+
|
202
|
+
As you can see, when a banner is present, you are able to describe your CLI while providing relevant information such as current version with minimal effort.
|
203
|
+
|
204
|
+
=== DSL
|
205
|
+
|
206
|
+
You've already seen some of the DSL syntax, via the earlier examples, but now we can zoom in on the building blocks: commands and actions. Only a single method is required to add them: `on`. For example, here's what nesting looks like:
|
207
|
+
|
208
|
+
[source,ruby]
|
209
|
+
----
|
210
|
+
Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
|
211
|
+
on "db", "Manage database." do
|
212
|
+
on Start
|
213
|
+
on Stop
|
214
|
+
|
215
|
+
on "structure", "Manage database structure." do
|
216
|
+
on Dump
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
on Sod::Prefabs::Actions::Version, "Demo 0.0.0"
|
221
|
+
on Sod::Prefabs::Actions::Help, self
|
222
|
+
end
|
223
|
+
----
|
224
|
+
|
225
|
+
Despite the `Start`, `Stop`, and `Dump` actions not being implemented yet -- because you'll get a `NameError` if you try -- this does mean you'd have the following functionality available to you from the command line:
|
226
|
+
|
227
|
+
[source,bash]
|
228
|
+
----
|
229
|
+
demo db --start
|
230
|
+
demo db --stop
|
231
|
+
demo db structure --dump
|
232
|
+
demo --version
|
233
|
+
demo --help
|
234
|
+
----
|
235
|
+
|
236
|
+
The `on` method is the primary method of the DSL. Short and sweet. You'll also see `on` used when implementing custom commands and actions too. The `on` method can take any number of positional and/or keyword arguments. Here's an example where you might want to customize your database action by injecting a new dependencies:
|
237
|
+
|
238
|
+
[source,ruby]
|
239
|
+
----
|
240
|
+
Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
|
241
|
+
on DB, "MyDatabase", host: localhost, port: 5432
|
242
|
+
end
|
243
|
+
----
|
244
|
+
|
245
|
+
The first _positional_ argument (i.e. `DB`) is _always_ your action (don't worry, this'll be explained shortly), the second _positional_ argument is the first positional argument to the `DB.new` method followed by the `host` and `port` _keyword_ arguments. In other words, here's what's happening:
|
246
|
+
|
247
|
+
[source,ruby]
|
248
|
+
----
|
249
|
+
# Pattern
|
250
|
+
on DB, *, **
|
251
|
+
|
252
|
+
# DSL
|
253
|
+
on DB, "MyDatabase", host: localhost, port: 5432
|
254
|
+
|
255
|
+
# Actual
|
256
|
+
DB.new "MyDatabase", host: localhost, port: 5432
|
257
|
+
----
|
258
|
+
|
259
|
+
This also means you get the following benefits:
|
260
|
+
|
261
|
+
* Lazy initialization of your commands/actions.
|
262
|
+
* Any positional and/or keyword arguments will be forwarded to your command/action. Blocks are excluded since they are used by the `on` method for nesting purposes.
|
263
|
+
|
264
|
+
To further understand the DSL, commands, and actions you'll need to start with actions since they are the building blocks.
|
265
|
+
|
266
|
+
==== Actions
|
267
|
+
|
268
|
+
Actions are the lowest building blocks of the DSL which allow you to quickly implement, test, reuse, and compose more complex architectures. They provide a nice wrapper around native {option_parser_link} functionality so if you are familiar with how `OptionParser#on` works, then you'll feel at home with actions.
|
269
|
+
|
270
|
+
There are two kinds of actions: custom and prefabricated. We'll start with custom actions and explore prefabricated actions later. Custom actions allow you to define your own functionality by inheriting from `Sod::Action` and leveraging the DSL that comes with it. Here's a high level breakdown of the macros you can use:
|
271
|
+
|
272
|
+
* `description`: Optional (but strongly encouraged). Allows you to describe your action and appears within help documentation. If the description is not defined, then your action will be runnable while hidden from help documentation (this is similar to how {rake_link} task descriptions work).
|
273
|
+
* `ancillary`: Optional. Allows you to provide supplemental text in addition to your description that might be helpful to know about when displaying help documentation. This can accept single or multiple arguments. Order matters since each argument will appear on a separate line in the order listed.
|
274
|
+
* `on`: Required. Allows you to define the behavior of your action through keyword arguments. Otherwise, if not defined, the action will be ignored. This macro mimics {option_parser_link} `#on` behavior via the following positional and keyword arguments:
|
275
|
+
** `aliases`: Required. This is a positional argument and defines the short and long form aliases of your action. Your aliases can be a single string (i.e. `on "--version"`) or an array of short and long form aliases. For example, using `on %w[-v --version]` would allow you to use `-v` or `--version` from the command line to call your action. You can also use boolean aliases such as `--build` or `--[no-]build` which the option parser will supply to your `#call` method as a boolean value.
|
276
|
+
** `argument`: Optional. Serves as documentation, must be a string value, and allows the {option_parser_link} to determine if the argument is required or optional. As per the {option_parser_link} documentation, you could use the following values for example:
|
277
|
+
*** `TEXT`: Required text.
|
278
|
+
*** `[TEXT]`: Optional text.
|
279
|
+
*** `a,b,c`: Required list.
|
280
|
+
*** `[a,b,c]`: Optional list.
|
281
|
+
** `type`: Optional. The type is inferred from your argument but, if you need to be explicit or want to use a custom type not supported by the option parser by default, you can specify the type by providing a primitive. Example: `String`, `Array`, `Hash`, `Date`, etc. You can also use custom types, as provided by this gem, or your own custom type. See {option_parser_link} documentation for details.
|
282
|
+
** `allow`: Optional. Allows you to define what values are allowed as defined via the `argument` or `type` keywords. This can be a string, array, hash, etc. as long as it's compatible with what is defined via the `argument` and/or `type` keyword. This information will also show up in the help documentation as well.
|
283
|
+
** `default`: Optional. Allows you to supply a default value and is a handy for simple values which don't require lazy evaluation via the corresponding default macro. ⚠️ This is ignored if the corresponding macro is used so ensure you use one or the other but not both.
|
284
|
+
** `description`: Optional. Allows you to define a description. Handy for short descriptions that can fit on a single line. Otherwise, for longer descriptions, use the macro. ⚠️ This is ignored if the corresponding macro is used so ensure you use one or the other but not both.
|
285
|
+
** `ancillary`: Optional. Allows you to define ancillary text to supplement your description. It can accept a string or an array. Handy for short, supplementary, text that can fit on a single line. Otherwise, for more verbose details, use the macro. ⚠️ This is ignored if the corresponding macro is used so ensure you use one or the other but not both.
|
286
|
+
* `default`: Optional. Uses a block, which is lazy evaluated, so you can define a default value. This is most helpful when used in combination with an _optional_ `argument` and/or `type` which can fallback to a safe default. This information shows up in the help text too. If your default value is a boolean then it will be color coded green for `true` and red for `false`.
|
287
|
+
|
288
|
+
At a minimum, you want to define the `description` and `on` macros while implementing the `#call` message. Here's an example of implementing an action that echoes input as output:
|
289
|
+
|
290
|
+
[source,ruby]
|
291
|
+
----
|
292
|
+
class Echo < Sod::Action
|
293
|
+
description "Echo input as output."
|
294
|
+
|
295
|
+
on %w[-e --echo], argument: "TEXT"
|
296
|
+
|
297
|
+
def call(text) = puts text
|
298
|
+
end
|
299
|
+
|
300
|
+
cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration" do
|
301
|
+
on Echo
|
302
|
+
on Sod::Prefabs::Actions::Help, self
|
303
|
+
end
|
304
|
+
----
|
305
|
+
|
306
|
+
If we run the above implementation with different inputs, we'll get multiple outputs:
|
307
|
+
|
308
|
+
[source,ruby]
|
309
|
+
----
|
310
|
+
cli.call
|
311
|
+
|
312
|
+
# Demo 0.0.0: A demonstration
|
313
|
+
#
|
314
|
+
# USAGE
|
315
|
+
# demo [OPTIONS]
|
316
|
+
#
|
317
|
+
# OPTIONS
|
318
|
+
# -e, --echo TEXT Echo input as output.
|
319
|
+
# -h, --help [COMMAND] Show this message.
|
320
|
+
#
|
321
|
+
# cli.call %w[--echo hello]
|
322
|
+
|
323
|
+
cli.call %w[--echo hello]
|
324
|
+
|
325
|
+
# hello
|
326
|
+
|
327
|
+
cli.call %s[--e hello]
|
328
|
+
|
329
|
+
# hello
|
330
|
+
|
331
|
+
cli.call ["--echo"]
|
332
|
+
|
333
|
+
# 🛑 Missing argument for: --echo.
|
334
|
+
----
|
335
|
+
|
336
|
+
As you can see, the description shows up in the help text while the `-e` and `--echo` aliases can be used interchangeably. We only get the missing argument error when the argument (i.e. `"TEXT"`) isn't supplied. Finally, when the action is called, we see that the `"hello"` text is outputted to the console for use. Here's an updated version of the above implementation which leverages all features:
|
337
|
+
|
338
|
+
[source,ruby]
|
339
|
+
----
|
340
|
+
class Echo < Sod::Action
|
341
|
+
description "Echo input as output."
|
342
|
+
|
343
|
+
ancillary "Supplementary text.", "Additional text."
|
344
|
+
|
345
|
+
on %w[-e --echo], argument: "[TEXT]", type: String, allow: %w[hello goodbye]
|
346
|
+
|
347
|
+
default { "hello" }
|
348
|
+
|
349
|
+
def call(text = nil) = puts(text || default)
|
350
|
+
end
|
351
|
+
|
352
|
+
cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration" do
|
353
|
+
on Echo
|
354
|
+
on Sod::Prefabs::Actions::Help, self
|
355
|
+
end
|
356
|
+
----
|
357
|
+
|
358
|
+
This time, when we run the above implementation, we have additional details:
|
359
|
+
|
360
|
+
[source,ruby]
|
361
|
+
----
|
362
|
+
cli.call
|
363
|
+
|
364
|
+
# Demo 0.0.0: A demonstration
|
365
|
+
#
|
366
|
+
# USAGE
|
367
|
+
# demo [OPTIONS]
|
368
|
+
#
|
369
|
+
# OPTIONS
|
370
|
+
# -e, --echo [TEXT] Echo input as output.
|
371
|
+
# Supplementary text.
|
372
|
+
# Additional text.
|
373
|
+
# Use: hello or goodbye.
|
374
|
+
# Default: hello.
|
375
|
+
# -h, --help [COMMAND] Show this message.
|
376
|
+
|
377
|
+
cli.call ["--echo"]
|
378
|
+
|
379
|
+
# hello
|
380
|
+
|
381
|
+
cli.call %w[--echo goodbye]
|
382
|
+
|
383
|
+
# goodbye
|
384
|
+
|
385
|
+
cli.call %w[--echo hi]
|
386
|
+
|
387
|
+
# 🛑 Invalid argument: --echo hi
|
388
|
+
----
|
389
|
+
|
390
|
+
Notice how the help text is more verbose. Not only do you see the description for the `--echo` action printed but you also see the two ancillary lines, documentation on what is allowed (i.e. you can only use "hello" or "goodbye"), and what the default will be (i.e. "hello") when `--echo` doesn't get an argument since it's optional. This is why you can see `--echo` can be called with nothing, an allowed value, or an value that isn't allowed which causes an _invalid argument_ error to show up.
|
391
|
+
|
392
|
+
Lastly, your action's `#call` method _must_ be implemented. Otherwise, you'll get an exception as show here:
|
393
|
+
|
394
|
+
[source,ruby]
|
395
|
+
----
|
396
|
+
class Echo < Sod::Action
|
397
|
+
description "Echo input as output."
|
398
|
+
on %w[-e --echo]
|
399
|
+
end
|
400
|
+
|
401
|
+
cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration" do
|
402
|
+
on Echo
|
403
|
+
on Sod::Prefabs::Actions::Help, self
|
404
|
+
end
|
405
|
+
|
406
|
+
cli.call ["--echo"]
|
407
|
+
|
408
|
+
# `Echo#call [[:rest, :*]]` must be implemented. (NotImplementedError)
|
409
|
+
----
|
410
|
+
|
411
|
+
At a minimum, as shown from the error above, your `#call` method needs to allow the forwarding of positional arguments which means you can use `def call(*)` if you want to ignore arguments or define which arguments you care about and ignore the rest. Up to you. Also, _all_ of the information defined within your action is available to you within the instance. Here's an example action which inspects itself:
|
412
|
+
|
413
|
+
[source,ruby]
|
414
|
+
----
|
415
|
+
class Echo < Sod::Action
|
416
|
+
description "Echo input as output."
|
417
|
+
|
418
|
+
ancillary "Supplementary."
|
419
|
+
|
420
|
+
on "--inspect", argument: "[TEXT]", type: String, allow: %w[one two], default: "A default."
|
421
|
+
|
422
|
+
def call(*)
|
423
|
+
puts handle:, aliases:, argument:, type:, allow:, default:, description:, ancillary:
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration" do
|
428
|
+
on Echo
|
429
|
+
on Sod::Prefabs::Actions::Help, self
|
430
|
+
end
|
431
|
+
|
432
|
+
cli.call ["--inspect"]
|
433
|
+
|
434
|
+
# {
|
435
|
+
# :handle => "--inspect [TEXT]",
|
436
|
+
# :aliases => ["--inspect"],
|
437
|
+
# :argument => "[TEXT]",
|
438
|
+
# :type => String,
|
439
|
+
# :allow => ["one", "two"],
|
440
|
+
# :default => "A default.",
|
441
|
+
# :description => "Echo input as output.",
|
442
|
+
# :ancillary => ["Supplementary."]
|
443
|
+
# }
|
444
|
+
----
|
445
|
+
|
446
|
+
Although, not shown in the above, the `#to_a` and `#to_h` methods are available as well.
|
447
|
+
|
448
|
+
==== Commands
|
449
|
+
|
450
|
+
Commands are a step up from actions in that they allow you to organize and group your actions while giving you the ability to process the data parsed by your actions. If it helps, a command mimics {option_parser_link} behavior when you initialize and define multiple, actionable, blocks. Here's an example which maps the terminology of this gem with that of {option_parser_link}:
|
451
|
+
|
452
|
+
[source,ruby]
|
453
|
+
----
|
454
|
+
options = {}
|
455
|
+
|
456
|
+
# Command
|
457
|
+
OptionParser.new do |parser|
|
458
|
+
# Actions
|
459
|
+
parser.on("--one", "One.") { |value| options[:one] = value }
|
460
|
+
parser.on("--two", "Two.") { |value| options[:two] = value }
|
461
|
+
parser.on("--three", "Three.") { |value| options[:three] = value }
|
462
|
+
end
|
463
|
+
----
|
464
|
+
|
465
|
+
The equivalent of the above, as provided by this gem, is:
|
466
|
+
|
467
|
+
[source,ruby]
|
468
|
+
----
|
469
|
+
require "dry/container"
|
470
|
+
require "infusible"
|
471
|
+
require "refinements/structs"
|
472
|
+
require "sod"
|
473
|
+
|
474
|
+
module Container
|
475
|
+
extend Dry::Container::Mixin
|
476
|
+
|
477
|
+
register(:input, memoize: true) { Hash.new }
|
478
|
+
end
|
479
|
+
|
480
|
+
Import = Infusible.with Container
|
481
|
+
|
482
|
+
class One < Sod::Action
|
483
|
+
include Import[:input]
|
484
|
+
|
485
|
+
on "--[no-]one", description: "One."
|
486
|
+
|
487
|
+
def call(value) = input[:one] = value
|
488
|
+
end
|
489
|
+
|
490
|
+
class Two < Sod::Action
|
491
|
+
include Import[:input]
|
492
|
+
|
493
|
+
on "--[no-]two", description: "Two."
|
494
|
+
|
495
|
+
def call(value) = input[:two] = value
|
496
|
+
end
|
497
|
+
|
498
|
+
class Demo < Sod::Command
|
499
|
+
include Import[:input]
|
500
|
+
|
501
|
+
handle "demo"
|
502
|
+
|
503
|
+
description "A demonstration command."
|
504
|
+
|
505
|
+
on One
|
506
|
+
on Two
|
507
|
+
|
508
|
+
def call = puts input
|
509
|
+
end
|
510
|
+
|
511
|
+
cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration" do
|
512
|
+
on Demo
|
513
|
+
on Sod::Prefabs::Actions::Help, self
|
514
|
+
end
|
515
|
+
|
516
|
+
cli.call ["demo", "--one", "--no-two"]
|
517
|
+
|
518
|
+
# {:one => true, :two => false}
|
519
|
+
----
|
520
|
+
|
521
|
+
You might be thinking: "Hey, that's more lines of code!" True but -- more importantly -- you get the benefit of composible and reusable architectures -- because each command/action is encapsulated -- which you don't get with {option_parser_link}.
|
522
|
+
|
523
|
+
By the way, the above example uses the {dry_container_link} gem for defining dependencies and the {infusible_link} gem for injecting those dependencies. You'll also notice that the `input` hash is memoized within the container to allow for mutation. The fact that you have to mutate input is a bummer and you should strive to avoid mutation whenever you can. In this case, mutation is necessary because the underlining architecture of the {option_parser_link} doesn't provide any other way to share state amongst your commands and actions. So this is one example of how you can do that.
|
524
|
+
|
525
|
+
You'll also notice, as mentioned with actions earlier, that commands share, roughly, the same DSL as actions with a few differences in terms of macros:
|
526
|
+
|
527
|
+
* `handle`: Required. The name of your command or the _namespace_ for which you group multiple actions. Otherwise, if not defined, then your command won't be runnable.
|
528
|
+
* `description`: Optional (but strongly recommended). Defines what your command is about and shows up in the help documentation. Otherwise, if not provided, your command's description will be blank.
|
529
|
+
* `ancillary`: Optional. Allows you to provide supplemental text for your description that might be helpful to know about when displaying help documentation. This can accept single or multiple arguments. Order matters since each argument will appear on a separate line in the order listed.
|
530
|
+
* `on`: Required. The syntax for this is identical to the CLI DSL where you define your action (constant) as the first positional argument followed by any number of positional and/or keyword arguments that you want to feed into your action when the `.new` method is called.
|
531
|
+
|
532
|
+
If we reuse the above example and print the help documentation, you'll see the following output:
|
533
|
+
|
534
|
+
[source,ruby]
|
535
|
+
----
|
536
|
+
cli.call
|
537
|
+
|
538
|
+
# Demo 0.0.0: A demonstration
|
539
|
+
#
|
540
|
+
# USAGE
|
541
|
+
# demo [OPTIONS]
|
542
|
+
# demo COMMAND [OPTIONS]
|
543
|
+
#
|
544
|
+
# OPTIONS
|
545
|
+
# -h, --help [COMMAND] Show this message.
|
546
|
+
#
|
547
|
+
# COMMANDS
|
548
|
+
# demo A demonstration command.
|
549
|
+
----
|
550
|
+
|
551
|
+
...and if we display help on the `demo` command itself, we'll see all of it's capabilities:
|
552
|
+
|
553
|
+
[source,ruby]
|
554
|
+
----
|
555
|
+
cli.call ["demo"]
|
556
|
+
|
557
|
+
# A demonstration command.
|
558
|
+
#
|
559
|
+
# USAGE
|
560
|
+
# demo [OPTIONS]
|
561
|
+
#
|
562
|
+
# OPTIONS
|
563
|
+
# --[no-]one
|
564
|
+
# --[no-]two
|
565
|
+
----
|
566
|
+
|
567
|
+
Commands come in two forms: inline and reusable. You've already seen how reusable commands work but the next sections will go into more detail.
|
568
|
+
|
569
|
+
===== Inline
|
570
|
+
|
571
|
+
Inline commands provide a lightweight way to namespace your actions when you don't need, or want, to implement a _reusable_ command. If we refactor the earlier example to use inline commands, here's what it would look like:
|
572
|
+
|
573
|
+
[source,ruby]
|
574
|
+
----
|
575
|
+
cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration" do
|
576
|
+
on "demo", "A demonstration command." do
|
577
|
+
on One
|
578
|
+
on Two
|
579
|
+
end
|
580
|
+
|
581
|
+
on Sod::Prefabs::Actions::Help, self
|
582
|
+
end
|
583
|
+
----
|
584
|
+
|
585
|
+
Inline commands can have ancillary text by passing in additional arguments _after_ the description. Example:
|
586
|
+
|
587
|
+
[source,ruby]
|
588
|
+
----
|
589
|
+
cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration" do
|
590
|
+
on "demo", "A demonstration command.", "Some text.", "Some more text."
|
591
|
+
end
|
592
|
+
----
|
593
|
+
|
594
|
+
While the above is convenient, it can get out of control quickly. If this happens, please consider taking your _inline_ command and turning it into a _reusable_ command so your implementation remains organized and readable.
|
595
|
+
|
596
|
+
There is no limit on how deep you can go with nesting but if you are using anything beyond one or two levels of nesting then you should reconsider your design as your CLI is getting too complicated.
|
597
|
+
|
598
|
+
===== Reusable
|
599
|
+
|
600
|
+
A _reusable_ command is what you saw earlier where you can subclass from `Sod::Command` to implement your custom command. Here's the code again:
|
601
|
+
|
602
|
+
[source,ruby]
|
603
|
+
----
|
604
|
+
class Demo < Sod::Command
|
605
|
+
handle "demo"
|
606
|
+
|
607
|
+
description "A demonstration command."
|
608
|
+
|
609
|
+
ancillary "Some text.", "Some more text."
|
610
|
+
|
611
|
+
on One
|
612
|
+
on Two
|
613
|
+
|
614
|
+
def call = puts "Your implementation goes here."
|
615
|
+
end
|
616
|
+
----
|
617
|
+
|
618
|
+
One major difference between _reusable_ and _inline_ commands is that _reusable_ commands allow you implement a `#call` method. This method is optional, so if you don't need it, you don't have to implement it. However, if you do, this means you can process the input from your actions. This method is called _after_ the option parser has parsed all command line input for your actions which gives you a handy way to process all collected input via a single command. 💡 This is how the {rubysmith_link}, {gemsmith_link}, and {hanamismith_link} gems all build new Ruby projects for you based on the actions passed to them via the CLI.
|
619
|
+
|
620
|
+
==== Initialization
|
621
|
+
|
622
|
+
In all the action and command examples, thus far, we've not used an initializer. You can always customize how your command or action is initialized by defining one and forwarding all keyword arguments to `super`. Here's an example for both an action and a command:
|
623
|
+
|
624
|
+
[source,ruby]
|
625
|
+
----
|
626
|
+
class MyAction < Sod::Action
|
627
|
+
def initialize(processor: Processor.new, **)
|
628
|
+
super(**)
|
629
|
+
@processor = processor
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
class MyCommand < Sod::Command
|
634
|
+
def initialize(handler: Handler.new, **)
|
635
|
+
super(**)
|
636
|
+
@handler = handler
|
637
|
+
end
|
638
|
+
end
|
639
|
+
----
|
640
|
+
|
641
|
+
The reason you need to forward keyword arguments to `super` is so that injected dependencies from the super class are always available to you. Especially, contexts, which are explained next.
|
642
|
+
|
643
|
+
==== Contexts
|
644
|
+
|
645
|
+
Contexts are a mechanism for passing common data between your commands and actions with override capability if desired. They are a hybrid between a `Hash` and a `Struct`. They can be constructed two ways depending on your preference:
|
646
|
+
|
647
|
+
[source,ruby]
|
648
|
+
----
|
649
|
+
# Traditional
|
650
|
+
context = Sod::Context.new defaults_path: "path/to/defaults.yml", version_label: "Demo 0.0.0"
|
651
|
+
|
652
|
+
# Short (like Struct or Data)
|
653
|
+
context = Sod::Context[defaults_path: "path/to/defaults.yml", version_label: "Demo 0.0.0"]
|
654
|
+
----
|
655
|
+
|
656
|
+
Once you have an instance, you can use it as follows:
|
657
|
+
|
658
|
+
[source,ruby]
|
659
|
+
----
|
660
|
+
# Direct
|
661
|
+
context.defaults_path # "path/to/defaults.yml"
|
662
|
+
|
663
|
+
# With override.
|
664
|
+
context["my/path", :defaults_path] # "my/path"
|
665
|
+
----
|
666
|
+
|
667
|
+
The override is handy for situations where you have a default value that you would prefer to use (i.e. first argument) but want to fallback to the `:defaults_path` if the override is `nil`. When you put all of this together, this means you can build a single context and use it within your commands and actions by injecting it:
|
668
|
+
|
669
|
+
[source,ruby]
|
670
|
+
----
|
671
|
+
context = Sod::Context[defaults_path: "path/to/defaults.yml" version_label: "Demo 0.0.0"]
|
672
|
+
|
673
|
+
Sod.new :demo, banner: "A demonstration." do
|
674
|
+
on(Sod::Prefabs::Commands::Config, context:)
|
675
|
+
on(Sod::Prefabs::Actions::Version, context:)
|
676
|
+
on Sod::Prefabs::Actions::Help, self
|
677
|
+
end
|
678
|
+
----
|
679
|
+
|
680
|
+
💡 When passing a context to a command, it'll automatically be passed to all actions defined within that command. Each action can then choose to use the context or not.
|
681
|
+
|
682
|
+
==== Types
|
683
|
+
|
684
|
+
Types are a way to extend default {option_parser_link} functionality. Two types, not provided by {option_parser_link}, that are worth being aware of are:
|
685
|
+
|
686
|
+
**Pathname**
|
687
|
+
|
688
|
+
Provided by this gem and must be manually required since it's disabled by default. Example:
|
689
|
+
|
690
|
+
[source,ruby]
|
691
|
+
----
|
692
|
+
require "sod"
|
693
|
+
require "sod/types/pathname"
|
694
|
+
|
695
|
+
class Demo < Sod::Action
|
696
|
+
on "--path", argument: "PATH", type: Pathname
|
697
|
+
end
|
698
|
+
----
|
699
|
+
|
700
|
+
With the above, you'll always get a link:https://rubyapi.org/o/s?q=Pathname[Pathname] instance as input to your action.
|
701
|
+
|
702
|
+
**Version**
|
703
|
+
|
704
|
+
Provided via the {versionaire_link} gem which gives you a `Version` type when dealing with link:https://semver.org[semantic versions]. Here's how to leverage it:
|
705
|
+
|
706
|
+
[source,ruby]
|
707
|
+
----
|
708
|
+
require "versionaire"
|
709
|
+
require "versionaire/extensions/option_parser"
|
710
|
+
|
711
|
+
class Demo < Sod::Action
|
712
|
+
on "--version", argument: "VERSION", type: Versionaire::Version
|
713
|
+
end
|
714
|
+
----
|
715
|
+
|
716
|
+
==== Prefabrications
|
717
|
+
|
718
|
+
Several pre-built commands and actions are provided for you as foundational tooling to get you up and running quickly. You can use and customize them as desired.
|
719
|
+
|
720
|
+
===== Configure
|
721
|
+
|
722
|
+
The configure command -- and associated actions -- allows you to interact with CLI configurations such as those managed by the {xdg_link}, {runcom_link}, and/or {etcher_link} gems which adhere to the XDG Directory Specification. Example:
|
723
|
+
|
724
|
+
[source,ruby]
|
725
|
+
----
|
726
|
+
require "runcom"
|
727
|
+
|
728
|
+
context = Sod::Context[
|
729
|
+
defaults_path: "defaults.yml",
|
730
|
+
xdg_config: Runcom::Config.new("demo/configuration.yml")
|
731
|
+
]
|
732
|
+
|
733
|
+
cli = Sod.new :rubysmith, banner: "Demo 0.0.0: A demonstration." do
|
734
|
+
on(Sod::Prefabs::Commands::Config, context:)
|
735
|
+
on Sod::Prefabs::Actions::Help, self
|
736
|
+
end
|
737
|
+
|
738
|
+
cli.call ["config"]
|
739
|
+
|
740
|
+
# Manage configuration.
|
741
|
+
#
|
742
|
+
# USAGE
|
743
|
+
# config [OPTIONS]
|
744
|
+
#
|
745
|
+
# OPTIONS
|
746
|
+
# -c, --create Create default configuration.
|
747
|
+
# Prompts for local or global path.
|
748
|
+
# -e, --edit Edit project configuration.
|
749
|
+
# -v, --view View project configuration.
|
750
|
+
# -d, --delete Delete project configuration.
|
751
|
+
# Prompts for confirmation.
|
752
|
+
----
|
753
|
+
|
754
|
+
This action is most useful when building customizable CLIs where you want users of your CLI to have the flexibility of customizing their preferences.
|
755
|
+
|
756
|
+
===== Help
|
757
|
+
|
758
|
+
By now you should be familiar with the help action which allows you to print CLI documentation for users of your CLI. This action consumes the entire graph (i.e. `self`) of information in order to render documentation. You'll want to add this by default or customize with your own help action should you not like the default functionality. Anything is possible. Here's how to use:
|
759
|
+
|
760
|
+
[source,ruby]
|
761
|
+
----
|
762
|
+
cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
|
763
|
+
on Sod::Prefabs::Actions::Help, self
|
764
|
+
end
|
765
|
+
|
766
|
+
cli.call
|
767
|
+
cli.call ["-h"]
|
768
|
+
cli.call ["--help"]
|
769
|
+
cli.call ["--help", "some_command"]
|
770
|
+
----
|
771
|
+
|
772
|
+
💡 Passing `-h` or `--help` is optional since the CLI will default to printing help if only given a command.
|
773
|
+
|
774
|
+
===== Version
|
775
|
+
|
776
|
+
The version action allows users to check which version of your CLI they are using and only requires supplying version information when creating the action:
|
777
|
+
|
778
|
+
[source,ruby]
|
779
|
+
----
|
780
|
+
cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
|
781
|
+
on Sod::Prefabs::Actions::Version.new("Demo 0.0.0")
|
782
|
+
end
|
783
|
+
|
784
|
+
cli.call ["-v"] # Demo 0.0.0
|
785
|
+
cli.call ["--version"] # Demo 0.0.0
|
786
|
+
----
|
787
|
+
|
788
|
+
💡 This pairs well with the {spek_link} gem which pulls this information straight from your `gemspec`.
|
789
|
+
|
790
|
+
=== Examples
|
791
|
+
|
792
|
+
Hopefully the above is plenty of information to get you started but here are a few more examples in case it helps:
|
793
|
+
|
794
|
+
==== Inline Script
|
795
|
+
|
796
|
+
The following demonstrates an link:https://alchemists.io/articles/ruby_bundler_inline[inline script] using commands and actions.
|
797
|
+
|
798
|
+
[source,ruby]
|
799
|
+
----
|
800
|
+
#! /usr/bin/env ruby
|
801
|
+
# frozen_string_literal: true
|
802
|
+
|
803
|
+
# Save as `demo`, then `chmod 755 demo`, and run as `./demo`.
|
804
|
+
|
805
|
+
require "bundler/inline"
|
806
|
+
|
807
|
+
gemfile true do
|
808
|
+
source "https://rubygems.org"
|
809
|
+
|
810
|
+
gem "amazing_print"
|
811
|
+
gem "debug"
|
812
|
+
gem "sod"
|
813
|
+
end
|
814
|
+
|
815
|
+
class Start < Sod::Action
|
816
|
+
include Sod::Import[:logger]
|
817
|
+
|
818
|
+
description "Start database."
|
819
|
+
|
820
|
+
on "--start"
|
821
|
+
|
822
|
+
def call(*) = logger.info { "Starting database..." }
|
823
|
+
end
|
824
|
+
|
825
|
+
class Stop < Sod::Action
|
826
|
+
include Sod::Import[:logger]
|
827
|
+
|
828
|
+
description "Stop database."
|
829
|
+
|
830
|
+
on "--stop"
|
831
|
+
|
832
|
+
def call(*) = logger.info { "Stopping database..." }
|
833
|
+
end
|
834
|
+
|
835
|
+
class Echo < Sod::Action
|
836
|
+
include Sod::Import[:kernel]
|
837
|
+
|
838
|
+
description "Echo input as output."
|
839
|
+
|
840
|
+
on %w[-e --echo], argument: "TEXT"
|
841
|
+
|
842
|
+
def call(text) = kernel.puts text
|
843
|
+
end
|
844
|
+
|
845
|
+
cli = Sod.new :demo, banner: "Demo 0.0.0: A demonstration." do
|
846
|
+
on "db", "Manage database." do
|
847
|
+
on Start
|
848
|
+
on Stop
|
849
|
+
end
|
850
|
+
|
851
|
+
on Sod::Prefabs::Actions::Version, "Demo 0.0.0"
|
852
|
+
on Sod::Prefabs::Actions::Help, self
|
853
|
+
end
|
854
|
+
----
|
855
|
+
|
856
|
+
Once you've saved the above to your local disk, you can experiment with it by passing different command line arguments to it:
|
857
|
+
|
858
|
+
[source,bash]
|
859
|
+
----
|
860
|
+
./demo
|
861
|
+
|
862
|
+
# Demo 0.0.0: A demonstration.
|
863
|
+
#
|
864
|
+
# USAGE
|
865
|
+
# demo [OPTIONS]
|
866
|
+
# demo COMMAND [OPTIONS]
|
867
|
+
#
|
868
|
+
# OPTIONS
|
869
|
+
# -v, --version Show version.
|
870
|
+
# -h, --help [COMMAND] Show this message.
|
871
|
+
#
|
872
|
+
# COMMANDS
|
873
|
+
# db Manage database.
|
874
|
+
|
875
|
+
./demo db
|
876
|
+
|
877
|
+
# Manage database.
|
878
|
+
#
|
879
|
+
# USAGE
|
880
|
+
# db [OPTIONS]
|
881
|
+
#
|
882
|
+
# OPTIONS
|
883
|
+
# --start Start database.
|
884
|
+
# --stop Stop database.
|
885
|
+
|
886
|
+
./demo db --start
|
887
|
+
# 🟢 Starting database...
|
888
|
+
|
889
|
+
./demo db --stop
|
890
|
+
# 🟢 Stopping database...
|
891
|
+
|
892
|
+
./demo --version
|
893
|
+
# Demo 0.0.0
|
894
|
+
----
|
895
|
+
|
896
|
+
==== Gems
|
897
|
+
|
898
|
+
The following gems are built atop Sod and you can study the `CLI` namespace each or use the {gemsmith_link} gem to generate a CLI template project with all of this baked in for you. Here's the list:
|
899
|
+
|
900
|
+
* {gemsmith_link}
|
901
|
+
* {git-lint_link}
|
902
|
+
* {hanamismith_link}
|
903
|
+
* {milestoner_link}
|
904
|
+
* {pennyworth_link}
|
905
|
+
* {pragmater_link}
|
906
|
+
* {rubysmith_link}
|
907
|
+
* {sublime_text_kit_link}
|
908
|
+
* {tocer_link}
|
909
|
+
|
910
|
+
== Development
|
911
|
+
|
912
|
+
To contribute, run:
|
913
|
+
|
914
|
+
[source,bash]
|
915
|
+
----
|
916
|
+
git clone https://github.com/bkuhlmann/sod
|
917
|
+
cd sod
|
918
|
+
bin/setup
|
919
|
+
----
|
920
|
+
|
921
|
+
You can also use the IRB console for direct access to all objects:
|
922
|
+
|
923
|
+
[source,bash]
|
924
|
+
----
|
925
|
+
bin/console
|
926
|
+
----
|
927
|
+
|
928
|
+
== Tests
|
929
|
+
|
930
|
+
To test, run:
|
931
|
+
|
932
|
+
[source,bash]
|
933
|
+
----
|
934
|
+
bin/rake
|
935
|
+
----
|
936
|
+
|
937
|
+
== link:https://alchemists.io/policies/license[License]
|
938
|
+
|
939
|
+
== link:https://alchemists.io/policies/security[Security]
|
940
|
+
|
941
|
+
== link:https://alchemists.io/policies/code_of_conduct[Code of Conduct]
|
942
|
+
|
943
|
+
== link:https://alchemists.io/policies/contributions[Contributions]
|
944
|
+
|
945
|
+
== link:https://alchemists.io/projects/sod/versions[Versions]
|
946
|
+
|
947
|
+
== link:https://alchemists.io/community[Community]
|
948
|
+
|
949
|
+
== Credits
|
950
|
+
|
951
|
+
* Built with link:https://alchemists.io/projects/gemsmith[Gemsmith].
|
952
|
+
* Engineered by link:https://alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann].
|