tap 0.19.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History +100 -45
- data/MIT-LICENSE +1 -1
- data/README +95 -51
- data/bin/tap +11 -57
- data/bin/tapexe +84 -0
- data/doc/API +91 -139
- data/doc/Configuration +93 -0
- data/doc/Examples/Command Line +10 -42
- data/doc/Examples/Tapfile +124 -0
- data/doc/Ruby to Ruby +87 -0
- data/doc/Workflow Syntax +185 -0
- data/lib/tap.rb +74 -5
- data/lib/tap/app.rb +217 -310
- data/lib/tap/app/api.rb +44 -23
- data/lib/tap/app/queue.rb +11 -12
- data/lib/tap/app/stack.rb +4 -4
- data/lib/tap/declarations.rb +200 -0
- data/lib/tap/declarations/context.rb +31 -0
- data/lib/tap/declarations/description.rb +33 -0
- data/lib/tap/env.rb +133 -779
- data/lib/tap/env/cache.rb +87 -0
- data/lib/tap/env/constant.rb +94 -39
- data/lib/tap/env/path.rb +71 -0
- data/lib/tap/join.rb +42 -78
- data/lib/tap/joins/gate.rb +85 -0
- data/lib/tap/joins/switch.rb +4 -2
- data/lib/tap/joins/sync.rb +3 -3
- data/lib/tap/middleware.rb +5 -5
- data/lib/tap/middlewares/debugger.rb +18 -58
- data/lib/tap/parser.rb +115 -183
- data/lib/tap/root.rb +162 -239
- data/lib/tap/signal.rb +72 -0
- data/lib/tap/signals.rb +20 -2
- data/lib/tap/signals/class_methods.rb +38 -43
- data/lib/tap/signals/configure.rb +19 -0
- data/lib/tap/signals/help.rb +5 -7
- data/lib/tap/signals/load.rb +49 -0
- data/lib/tap/signals/module_methods.rb +1 -0
- data/lib/tap/task.rb +46 -275
- data/lib/tap/tasks/dump.rb +21 -16
- data/lib/tap/tasks/list.rb +184 -0
- data/lib/tap/tasks/load.rb +4 -4
- data/lib/tap/tasks/prompt.rb +128 -0
- data/lib/tap/tasks/signal.rb +42 -0
- data/lib/tap/tasks/singleton.rb +35 -0
- data/lib/tap/tasks/stream.rb +64 -0
- data/lib/tap/utils.rb +83 -0
- data/lib/tap/version.rb +2 -2
- data/lib/tap/workflow.rb +124 -0
- data/tap.yml +0 -0
- metadata +59 -24
- data/cmd/console.rb +0 -43
- data/cmd/manifest.rb +0 -118
- data/cmd/run.rb +0 -145
- data/doc/Examples/Workflow +0 -40
- data/lib/tap/app/node.rb +0 -29
- data/lib/tap/env/context.rb +0 -61
- data/lib/tap/env/gems.rb +0 -63
- data/lib/tap/env/manifest.rb +0 -179
- data/lib/tap/env/minimap.rb +0 -308
- data/lib/tap/intern.rb +0 -50
- data/lib/tap/joins.rb +0 -9
- data/lib/tap/prompt.rb +0 -36
- data/lib/tap/root/utils.rb +0 -220
- data/lib/tap/root/versions.rb +0 -138
- data/lib/tap/signals/signal.rb +0 -68
data/bin/tapexe
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# usage: tap [workflow] [--- tapfile] [-d-]
|
4
|
+
#
|
5
|
+
# workflow:
|
6
|
+
# [break] [left:]right [ARGS...] Constants are identified by matching the
|
7
|
+
# left and right paths; all subsequent args
|
8
|
+
# example: are passed to the constant. Breaks delimit
|
9
|
+
# tap:load -> Tap::Tasks::Load and the syntax repeats.
|
10
|
+
#
|
11
|
+
# breaks:
|
12
|
+
# - Delimiter, separates object argvs
|
13
|
+
# -- Delimits argvs and enques the next object
|
14
|
+
# -:[qai][.class] Sequence - joins previous and next objects
|
15
|
+
# -/obj[/sig] Signal - enques signal with argv
|
16
|
+
# --/obj[/sig] Signal - executes signal with argv
|
17
|
+
# -@ obj Enque - enques obj with argv
|
18
|
+
# -! obj Execute - executes obj with argv
|
19
|
+
# -. .- Escape begin/end
|
20
|
+
# --- End workflow
|
21
|
+
#
|
22
|
+
# env:
|
23
|
+
# TAP_GEMS Gem patterns to match and register
|
24
|
+
# TAP_PATH Directory paths to register
|
25
|
+
# TAPENV Signal files for env
|
26
|
+
# TAPRC Signal files for app
|
27
|
+
# TAPFILE Declaration files
|
28
|
+
#
|
29
|
+
# examples:
|
30
|
+
# tap load str -: dump A simple sequence workflow
|
31
|
+
# tap load str - dump - join 0 1 Manually build the join
|
32
|
+
# tap --/use debugger -d- Debugging utilities
|
33
|
+
# tap prompt Open a prompt
|
34
|
+
# tap list List resources
|
35
|
+
#
|
36
|
+
|
37
|
+
require 'tap'
|
38
|
+
require 'tap/parser'
|
39
|
+
|
40
|
+
begin
|
41
|
+
options = Tap.options
|
42
|
+
|
43
|
+
if ARGV[-1] == '-d-'
|
44
|
+
ARGV.pop
|
45
|
+
options[:debug] = 'true'
|
46
|
+
end
|
47
|
+
|
48
|
+
if ARGV == ['--help']
|
49
|
+
puts Lazydoc.usage(__FILE__)
|
50
|
+
puts "\nversion #{Tap::VERSION} -- #{Tap::WEBSITE}"
|
51
|
+
exit(0)
|
52
|
+
end
|
53
|
+
|
54
|
+
app = Tap.setup(options)
|
55
|
+
parser = Tap::Parser.new
|
56
|
+
|
57
|
+
loop do
|
58
|
+
break if ARGV.empty?
|
59
|
+
parser.parse!(ARGV)
|
60
|
+
parser.build_to(app)
|
61
|
+
|
62
|
+
break if ARGV.empty?
|
63
|
+
config_parser = ConfigParser.new(app.config,
|
64
|
+
:option_break => Tap::Parser::BREAK,
|
65
|
+
:keep_break => true,
|
66
|
+
:clear_config => false,
|
67
|
+
:add_defaults => false)
|
68
|
+
config_parser.add(app.class.configurations)
|
69
|
+
|
70
|
+
config_parser.scan(ARGV) do |file|
|
71
|
+
app.call('sig' => 'load', 'args' => [file])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
app.run
|
76
|
+
rescue
|
77
|
+
if $DEBUG || options[:debug]
|
78
|
+
raise $!
|
79
|
+
end
|
80
|
+
$stderr.puts $!.message
|
81
|
+
exit(1)
|
82
|
+
end
|
83
|
+
|
84
|
+
exit(0)
|
data/doc/API
CHANGED
@@ -1,70 +1,71 @@
|
|
1
|
-
= Application Programming
|
1
|
+
= Application Programming Interfaces
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
All of the objects used by tap have two APIs -- one regarding the specific
|
4
|
+
type of object (ex a task, a join, or middleware), and another regarding how
|
5
|
+
applications find and use the objects. These APIs allow users to make custom
|
6
|
+
objects.
|
5
7
|
|
6
|
-
|
8
|
+
== Object Interface
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
+
Applications require the following methods for tasks, joins, and middleware.
|
11
|
+
Tap provides base classes that implement these APIs. Objects satisfying the
|
12
|
+
task, join, or middleware API are referred to as workflow objects.
|
10
13
|
|
11
|
-
|
12
|
-
or passed to the node via a join. All signature constructs are allowed
|
13
|
-
including multiple arguments, default arguments and splats (blocks are
|
14
|
-
technically allowed but will never receive a value).
|
14
|
+
=== {Task}[link:classes/Tap/Task.html]
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
16
|
+
call(input) # any return is allowed
|
17
|
+
joins() # returns an array of joins, or nil (optional)
|
18
|
+
|
19
|
+
The call method takes an object and returns an object. There are no formal
|
20
|
+
constraints for the input/output objects. The optional joins method returns an
|
21
|
+
array of joins to be called when the task completes, or nil if no joins are to
|
22
|
+
be called. Each join is called in order.
|
21
23
|
|
22
24
|
=== {Join}[link:classes/Tap/Join.html]
|
23
25
|
|
24
|
-
call(
|
26
|
+
call(output) # any return is allowed
|
25
27
|
|
26
|
-
The call method receives the
|
27
|
-
|
28
|
+
The call method receives the output of a task. The result of call is not used;
|
29
|
+
call internally performs the join actions.
|
28
30
|
|
29
31
|
=== {Middleware}[link:classes/Tap/Middleware.html]
|
30
32
|
|
31
33
|
Middleware.new(stack, *args) # returns an instance of middleware
|
32
|
-
call(node,
|
34
|
+
call(node, input) # return is the task output
|
33
35
|
stack() # returns the original stack
|
34
36
|
|
35
|
-
Middleware wraps the execution of
|
36
|
-
middleware during execution
|
37
|
-
|
38
|
-
|
39
|
-
|
37
|
+
Middleware wraps the execution of tasks. Tasks and inputs are passed to the
|
38
|
+
middleware during execution. Joins are performed with the call output.
|
39
|
+
|
40
|
+
== {Application Interface}[link:classes/Tap/App/Api.html]
|
41
|
+
|
42
|
+
Tap defines an interface allowing applications to create and manage objects
|
43
|
+
using signals. The application interface is distinct from the workflow APIs,
|
44
|
+
although typically one will be implemented on top of the other. Objects
|
45
|
+
satisfying the application interface are referred to as application objects.
|
40
46
|
|
41
|
-
|
47
|
+
The application interface allows instantiation of a class from a hash and
|
48
|
+
serialization of an instance back into a hash. The hash is referred to as a
|
49
|
+
specification and must be serializable as {JSON}[http://json.org/] (meaning
|
50
|
+
the hash must consist of simple object types: numbers, strings, hashes, and
|
51
|
+
arrays).
|
42
52
|
|
43
|
-
|
44
|
-
application interface allowing objects to be created and modified using
|
45
|
-
signals sent to an application (think HTTP to a web app). Signals are simple
|
46
|
-
hash constructs and typically form the basis for user interfaces. The
|
47
|
-
application interface is distinct from the object APIs, although it is typical
|
48
|
-
to implement the application interface on top of an object API.
|
53
|
+
The application interface consists of two methods:
|
49
54
|
|
50
|
-
|
51
|
-
|
52
|
-
allow instantiation of a class from a hash and serialization of an instance
|
53
|
-
back into a hash. The hash is referred to as a specification and must be
|
54
|
-
serializable as {JSON}[http://json.org/], basically meaning the hash must
|
55
|
-
consist of simple object types: numbers, strings, hashes, and arrays.
|
55
|
+
Const.build(spec, app) # returns an instance of self
|
56
|
+
to_spec # returns a spec
|
56
57
|
|
57
|
-
|
58
|
+
As an example:
|
58
59
|
|
59
|
-
class
|
60
|
+
class Example
|
60
61
|
class << self
|
61
|
-
# Build takes a specification
|
62
|
+
# Build takes a specification and returns an instance of self.
|
62
63
|
# The spec must be serializable as JSON.
|
63
|
-
def build(spec={}, app=Tap::App.
|
64
|
+
def build(spec={}, app=Tap::App.current)
|
64
65
|
end
|
65
66
|
end
|
66
67
|
|
67
|
-
# Takes no inputs and returns a specification
|
68
|
+
# Takes no inputs and returns a specification that, when built,
|
68
69
|
# returns an object like self.
|
69
70
|
#
|
70
71
|
# obj.class.build(obj.to_spec) # => returns an object like obj
|
@@ -74,22 +75,29 @@ The application interface consists of two methods, build and to_spec:
|
|
74
75
|
end
|
75
76
|
end
|
76
77
|
|
77
|
-
The application
|
78
|
-
implemented but add functionality for specific, common use cases.
|
79
|
-
|
78
|
+
The application interface reserves several additional methods that do not need
|
79
|
+
to be implemented but add functionality for specific, common use cases. These
|
80
|
+
are:
|
81
|
+
|
82
|
+
Const.parse(argv, app) # returns an instance of self, cannot modify ARGV
|
83
|
+
Const.parse!(argv, app) # same as parse but can modify ARGV
|
84
|
+
signal(sig) # returns an object responding to call
|
85
|
+
associations # returns an array like [refs, brefs]
|
86
|
+
|
87
|
+
If present they must do the following:
|
80
88
|
|
81
|
-
|
82
|
-
class Stub
|
89
|
+
class Example
|
83
90
|
class << self
|
84
|
-
#
|
85
|
-
#
|
86
|
-
#
|
87
|
-
#
|
88
|
-
|
91
|
+
# Takes an argument vector (an array, usually from the command line)
|
92
|
+
# and returns an instance of self. If a block is given, parse
|
93
|
+
# yields the instance and remaining args and returns block result.
|
94
|
+
#
|
95
|
+
# Parse should not modify argv.
|
96
|
+
def parse(argv=ARGV, app=Tap::App.current)
|
89
97
|
end
|
90
98
|
|
91
99
|
# Same as parse, but able to modify argv.
|
92
|
-
def parse!(argv=ARGV, app=Tap::App.
|
100
|
+
def parse!(argv=ARGV, app=Tap::App.current)
|
93
101
|
end
|
94
102
|
end
|
95
103
|
|
@@ -98,18 +106,17 @@ present they must adhere to these specifications.
|
|
98
106
|
def signal(sig)
|
99
107
|
end
|
100
108
|
|
101
|
-
# Returns a nested array of
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
105
|
-
# AFTER self.
|
109
|
+
# Returns a nested array of application objects associated with self.
|
110
|
+
# The array should be structured like [refs, brefs], where refs are
|
111
|
+
# references to objects that must be built BEFORE self and brefs are
|
112
|
+
# back-references to objects that must be built AFTER self.
|
106
113
|
#
|
107
|
-
# For example,
|
108
|
-
# method for a
|
109
|
-
# must be built after
|
110
|
-
# returns refs to their input and output
|
114
|
+
# For example, tasks must be built before joins. As such, the associations
|
115
|
+
# method for a task returns a brefs for each of its joins. Similarly, joins
|
116
|
+
# must be built after tasks and hence the associations method for a join
|
117
|
+
# returns refs to their input and output tasks:
|
111
118
|
#
|
112
|
-
#
|
119
|
+
# task.associations # => [nil, join]
|
113
120
|
# join.associations # => [inputs + outputs, nil]
|
114
121
|
#
|
115
122
|
# Nil is a valid return for associations, indicating no associations.
|
@@ -118,7 +125,7 @@ present they must adhere to these specifications.
|
|
118
125
|
end
|
119
126
|
|
120
127
|
The parse methods are used for building objects from interfaces that provide
|
121
|
-
an array of inputs (ex the command line)
|
128
|
+
an array of inputs rather a hash (ex the command line); without them objects
|
122
129
|
are effectively excluded from use within these interfaces.
|
123
130
|
|
124
131
|
Signals can be used to interact with specific objects from a user interface
|
@@ -128,96 +135,41 @@ cannot receive signals.
|
|
128
135
|
The associations method is used to order complex builds and is described in
|
129
136
|
more detail below.
|
130
137
|
|
131
|
-
===
|
138
|
+
=== Object References
|
132
139
|
|
133
|
-
Specifications often require references to other
|
134
|
-
refers to input and output
|
135
|
-
variables that, unlike the
|
136
|
-
|
137
|
-
the +obj+ and +var+ methods.
|
140
|
+
Specifications often require references to other application objects, as when
|
141
|
+
a join refers to input and output tasks. These references are normally
|
142
|
+
specified as variables that, unlike the object itself, are serializable as
|
143
|
+
JSON. Apps manage variables via the +obj+ and +var+ methods.
|
138
144
|
|
139
|
-
As an example
|
140
|
-
application object:
|
145
|
+
As an example:
|
141
146
|
|
142
|
-
class
|
143
|
-
def initialize(
|
144
|
-
@
|
147
|
+
class A
|
148
|
+
def initialize(b)
|
149
|
+
@b = b
|
145
150
|
end
|
146
151
|
|
147
152
|
def to_spec
|
148
|
-
{'
|
153
|
+
{'b' => app.var(@b)} # store a variable into the spec
|
149
154
|
end
|
150
155
|
|
151
156
|
def associations
|
152
|
-
[[@
|
157
|
+
[[@b], nil] # establish a build order
|
153
158
|
end
|
154
159
|
|
155
160
|
class << self
|
156
|
-
def build(spec={}, app=Tap::App.
|
157
|
-
|
158
|
-
new(
|
161
|
+
def build(spec={}, app=Tap::App.current)
|
162
|
+
b = app.obj(spec['b']) # retrieve an object referenced by the spec
|
163
|
+
new(b)
|
159
164
|
end
|
160
165
|
end
|
161
166
|
end
|
162
167
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
reference
|
168
|
-
associations method returns @object in the 'ref' array.
|
168
|
+
The to_spec method records a variable identifying @b, rather than @b itself,
|
169
|
+
which allows the spec to be properly serialized. Likewise the build method
|
170
|
+
de-references the variable to retrieve b when initializing a new instance; the
|
171
|
+
associations array is used to ensure that b is built by the time A.build tries
|
172
|
+
to de-reference the variable.
|
169
173
|
|
170
174
|
Note that only references to objects implementing the application interface
|
171
|
-
|
172
|
-
application interface must be serialized and deserialized by the build/to_spec
|
173
|
-
methods internally.
|
174
|
-
|
175
|
-
=== Resource Identifiers
|
176
|
-
|
177
|
-
Tap discovers application resources using resource identifiers (ie constant
|
178
|
-
attributes, see {Lazydoc}[http://tap.rubyforge.org/lazydoc]). Resources
|
179
|
-
identified in this way can be automatically loaded by the Tap::Env. If no
|
180
|
-
identifiers are specified for a resource, the user must manually load the
|
181
|
-
resource files.
|
182
|
-
|
183
|
-
As an example, this identifies the Sample constant as an 'example' resource.
|
184
|
-
|
185
|
-
[lib/file.rb]
|
186
|
-
|
187
|
-
# Sample::example summary
|
188
|
-
class Sample
|
189
|
-
end
|
190
|
-
|
191
|
-
A resource can be identified by zero or more identifiers. Typically all
|
192
|
-
identifiers will be put in the same file as the class, but this does not have
|
193
|
-
to be the case; applications automatically require all files that identify a
|
194
|
-
resource. The order in which the files are required is indeterminate and it is
|
195
|
-
up to the user to ensure consistency. For example:
|
196
|
-
|
197
|
-
[a.rb]
|
198
|
-
# Sample::a
|
199
|
-
class Sample
|
200
|
-
end
|
201
|
-
|
202
|
-
[b.rb]
|
203
|
-
require 'a'
|
204
|
-
|
205
|
-
# Sample::b
|
206
|
-
class Sample
|
207
|
-
end
|
208
|
-
|
209
|
-
Here the require statement ensures a.rb is always required before b.rb. Note
|
210
|
-
that consistency is automatic when all identifiers are in the same file (and
|
211
|
-
thus only one file is required).
|
212
|
-
|
213
|
-
The constant name will be inferred from file path if no constant name is
|
214
|
-
specified. This is the most compact form for identifying a resource:
|
215
|
-
|
216
|
-
[lib/sample.rb]
|
217
|
-
|
218
|
-
# ::example summary
|
219
|
-
class Sample
|
220
|
-
end
|
221
|
-
|
222
|
-
In this case no constant name is specified, so 'Sample' is inferred from
|
223
|
-
'sample.rb'. Constant names are determined from the path using camelization.
|
175
|
+
should be stored this way.
|
data/doc/Configuration
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
= Configuration
|
2
|
+
|
3
|
+
Configuring tap is a matter of setting ENV variables that tell the executable
|
4
|
+
what to make available to workflows. Each of the ENV variables can be treated
|
5
|
+
like PATH, where multiple paths may be joined by a colon. The config files
|
6
|
+
specified in the ENV are handled in order as listed, before the command line
|
7
|
+
workflow is parsed.
|
8
|
+
|
9
|
+
=== TAP_GEMS (default '.')
|
10
|
+
|
11
|
+
Specifies globs of gems to automatically load into the environment. Versions
|
12
|
+
can be specified as normal, separated by a comma. Use an empty string to
|
13
|
+
specify no gems.
|
14
|
+
|
15
|
+
% gem install tap-tasks
|
16
|
+
% TAP_GEMS=. tap inspect a b c
|
17
|
+
["a", "b", "c"]
|
18
|
+
% TAP_GEMS=tap-ta* tap inspect a b c
|
19
|
+
["a", "b", "c"]
|
20
|
+
% TAP_GEMS='tap-tasks, >= 0.6.0' tap inspect a b c
|
21
|
+
["a", "b", "c"]
|
22
|
+
% TAP_GEMS=nomatch tap inspect a b c
|
23
|
+
unresolvable constant: "inspect"
|
24
|
+
% TAP_GEMS= tap inspect a b c
|
25
|
+
unresolvable constant: "inspect"
|
26
|
+
|
27
|
+
Note that all matching gems will be activated when tap launches.
|
28
|
+
|
29
|
+
=== TAP_PATH (default '.')
|
30
|
+
|
31
|
+
Specifies directories to be scanned and registered with the tap env. All files
|
32
|
+
matching TAP_PATH/lib/**/*.rb will be scanned for constants; TAP_PATH itself
|
33
|
+
will be registered as a path in env. If TAP_PATH/tap.yml exists, it will be
|
34
|
+
loaded as a map of paths.
|
35
|
+
|
36
|
+
As a shorthand, just know that any constants under the lib directory of
|
37
|
+
TAP_PATH will be discovered.
|
38
|
+
|
39
|
+
[dir/lib/goodnight.rb]
|
40
|
+
require 'tap/task'
|
41
|
+
|
42
|
+
# ::task
|
43
|
+
class Goodnight < Tap::Task
|
44
|
+
def process(input)
|
45
|
+
puts "goodnight #{input}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
% tap goodnight moon
|
50
|
+
unresolvable constant: "goodnight"
|
51
|
+
% TAP_PATH=dir tap goodnight moon
|
52
|
+
goodnight moon
|
53
|
+
|
54
|
+
=== TAPENV (default 'tapenv')
|
55
|
+
|
56
|
+
Specifies signal files to be loaded in the env signaling context. These files
|
57
|
+
can be used to manually adjust an environment by setting/unsetting constants
|
58
|
+
and resource paths.
|
59
|
+
|
60
|
+
[tapenv]
|
61
|
+
unset Tap::Tasks::Dump
|
62
|
+
|
63
|
+
% tap load a -: dump
|
64
|
+
unresolvable constant: "dump"
|
65
|
+
|
66
|
+
=== TAPRC (default '~/.taprc:taprc')
|
67
|
+
|
68
|
+
Specifies signal files to be loaded in the app signaling context. These files
|
69
|
+
can be used to manually build workflows, or configure the app.
|
70
|
+
|
71
|
+
[taprc]
|
72
|
+
set loader load
|
73
|
+
set dumper dump
|
74
|
+
|
75
|
+
% tap - join loader dumper -/enq loader 'goodnight moon'
|
76
|
+
goodnight moon
|
77
|
+
|
78
|
+
=== TAPFILE (default 'tapfile')
|
79
|
+
|
80
|
+
Specifies ruby files that will be executed in the app context (ie using the
|
81
|
+
binding of the app instance). Tapfiles can be used to declare tasks, typically
|
82
|
+
using the Tap::Declarations module, or to manually setup workflows.
|
83
|
+
|
84
|
+
[tapfile]
|
85
|
+
require 'tap/declarations'
|
86
|
+
extend Tap::Declarations
|
87
|
+
|
88
|
+
task :goodnight do |config, args|
|
89
|
+
"Goodnight #{args}!"
|
90
|
+
end
|
91
|
+
|
92
|
+
% tap goodnight Moon -: dump
|
93
|
+
Goodnight Moon!
|