cmds 0.0.3 → 0.0.4
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.
- checksums.yaml +4 -4
- data/Rakefile +152 -0
- data/ansible/dev.yml +5 -0
- data/cmds.gemspec +1 -0
- data/lib/cmds/capture.rb +47 -0
- data/lib/cmds/debug.rb +101 -0
- data/lib/cmds/erb_context.rb +30 -0
- data/lib/cmds/io_handler.rb +76 -0
- data/lib/cmds/pipe.rb +13 -0
- data/lib/cmds/result.rb +34 -0
- data/lib/cmds/shell_eruby.rb +11 -0
- data/lib/cmds/stream.rb +239 -0
- data/lib/cmds/sugar.rb +76 -0
- data/lib/cmds/util.rb +254 -0
- data/lib/cmds/version.rb +1 -1
- data/lib/cmds.rb +19 -376
- data/scratch/popen3.rb +33 -0
- data/scratch/proxy.rb +5 -0
- data/spec/cmds/assert_spec.rb +16 -0
- data/spec/cmds/capture_spec.rb +108 -0
- data/spec/cmds/curry_spec.rb +4 -4
- data/spec/cmds/error_spec.rb +7 -2
- data/spec/cmds/ok_spec.rb +11 -1
- data/spec/cmds/replace_shortcuts_spec.rb +105 -65
- data/spec/cmds/stream_spec.rb +58 -0
- data/spec/debug_helper.rb +3 -0
- data/spec/spec_helper.rb +64 -5
- data/test/answers.txt +3 -0
- data/test/bin/dspec +1 -0
- data/test/echo_cmd.rb +1 -0
- data/test/lines.txt +4 -0
- data/test/questions.rb +15 -0
- data/test/tick.rb +6 -0
- metadata +46 -9
- data/scratch/blah.rb +0 -6
- data/spec/cmds/call_spec.rb +0 -27
- data/spec/cmds/raise_on_error_spec.rb +0 -11
- data/spec/cmds/run_spec.rb +0 -49
data/lib/cmds/stream.rb
ADDED
@@ -0,0 +1,239 @@
|
|
1
|
+
class Cmds
|
2
|
+
# stream inputs and/or outputs
|
3
|
+
#
|
4
|
+
# originally inspired by
|
5
|
+
#
|
6
|
+
# https://nickcharlton.net/posts/ruby-subprocesses-with-stdout-stderr-streams.html
|
7
|
+
#
|
8
|
+
# with major modifications from looking at Ruby's open3 module.
|
9
|
+
#
|
10
|
+
def stream *subs, &input_block
|
11
|
+
Cmds.debug "entering Cmds#stream",
|
12
|
+
subs: subs,
|
13
|
+
input_block: input_block
|
14
|
+
|
15
|
+
# use `merge_options` to get the args and kwds (we will take custom
|
16
|
+
# care of input in _stream)
|
17
|
+
options = merge_options subs, nil
|
18
|
+
|
19
|
+
# build the command string
|
20
|
+
cmd = Cmds.sub @template, options[:args], options[:kwds]
|
21
|
+
|
22
|
+
# call the internal function
|
23
|
+
really_stream cmd, options, &input_block
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# do the actual work...
|
29
|
+
def really_stream cmd, options, &input_block
|
30
|
+
Cmds.debug "entering Cmds#really_stream",
|
31
|
+
cmd: cmd,
|
32
|
+
options: options,
|
33
|
+
input_block: input_block
|
34
|
+
|
35
|
+
# create the handler that will be yielded to the input block
|
36
|
+
handler = IOHandler.new
|
37
|
+
|
38
|
+
# handle input
|
39
|
+
|
40
|
+
# default to the instance variable
|
41
|
+
input = @input
|
42
|
+
|
43
|
+
# if a block was provided, it might provide overriding input
|
44
|
+
if input_block
|
45
|
+
case input_block.arity
|
46
|
+
when 0
|
47
|
+
# when the input block takes no arguments it returns the input
|
48
|
+
input = input_block.call
|
49
|
+
when 1
|
50
|
+
# when the input block takes one argument, give it the handler and
|
51
|
+
# ignore the return value
|
52
|
+
input_block.call handler
|
53
|
+
|
54
|
+
# if input was assigned to the handler in the block, use it as input
|
55
|
+
input = handler.in unless handler.in.nil?
|
56
|
+
else
|
57
|
+
# bad block provided
|
58
|
+
raise ArgumentError.new NRSER.squish <<-BLOCK
|
59
|
+
provided input block must have arity 0 or 1
|
60
|
+
BLOCK
|
61
|
+
end # case input.arity
|
62
|
+
end # if input_block
|
63
|
+
|
64
|
+
# hash of options that will be passed to `spawn`
|
65
|
+
spawn_opts = {}
|
66
|
+
|
67
|
+
Cmds.debug "looking at input...",
|
68
|
+
input: input
|
69
|
+
|
70
|
+
# (possibly) create the input pipe... this will be nil if the provided
|
71
|
+
# input is io-like. in this case it will be used directly in the
|
72
|
+
# `spawn` options.
|
73
|
+
in_pipe = case input
|
74
|
+
when nil, String
|
75
|
+
Cmds.debug "input is a String or nil, creating pipe..."
|
76
|
+
|
77
|
+
in_pipe = Cmds::Pipe.new "INPUT", :in
|
78
|
+
spawn_opts[:in] = in_pipe.r
|
79
|
+
|
80
|
+
# don't buffer input
|
81
|
+
in_pipe.w.sync = true
|
82
|
+
in_pipe
|
83
|
+
|
84
|
+
else
|
85
|
+
Cmds.debug "input should be io-like, setting spawn opt.",
|
86
|
+
input: input
|
87
|
+
if input == $stdin
|
88
|
+
Cmds.debug "input is $stdin."
|
89
|
+
end
|
90
|
+
spawn_opts[:in] = input
|
91
|
+
nil
|
92
|
+
|
93
|
+
end # case input
|
94
|
+
|
95
|
+
# (possibly) create the output pipes.
|
96
|
+
#
|
97
|
+
# `stream` can be told to send it's output to either:
|
98
|
+
#
|
99
|
+
# 1. a Proc that will invoked with each line.
|
100
|
+
# 2. an io-like object that can be provided as `spawn`'s `:out` or
|
101
|
+
# `:err` options.
|
102
|
+
#
|
103
|
+
# in case (1) a `Cmds::Pipe` wrapping read and write piped `IO` instances
|
104
|
+
# will be created and assigned to the relevant of `out_pipe` or `err_pipe`.
|
105
|
+
#
|
106
|
+
# in case (2) the io-like object will be sent directly to `spawn` and
|
107
|
+
# the relevant `out_pipe` or `err_pipe` will be `nil`.
|
108
|
+
#
|
109
|
+
out_pipe, err_pipe = [
|
110
|
+
["ERROR", :err],
|
111
|
+
["OUTPUT", :out],
|
112
|
+
].map do |name, sym|
|
113
|
+
Cmds.debug "looking at #{ name }..."
|
114
|
+
# see if hanlder.out or hanlder.err is a Proc
|
115
|
+
if handler.send(sym).is_a? Proc
|
116
|
+
Cmds.debug "#{ name } is a Proc, creating pipe..."
|
117
|
+
pipe = Cmds::Pipe.new name, sym
|
118
|
+
# the corresponding :out or :err option for spawn needs to be
|
119
|
+
# the pipe's write handle
|
120
|
+
spawn_opts[sym] = pipe.w
|
121
|
+
# return the pipe
|
122
|
+
pipe
|
123
|
+
|
124
|
+
else
|
125
|
+
Cmds.debug "#{ name } should be io-like, setting spawn opt.",
|
126
|
+
output: handler.send(sym)
|
127
|
+
spawn_opts[sym] = handler.send(sym)
|
128
|
+
# the pipe is nil!
|
129
|
+
nil
|
130
|
+
end
|
131
|
+
end # map outputs
|
132
|
+
|
133
|
+
Cmds.debug "spawning...",
|
134
|
+
cmd: cmd,
|
135
|
+
opts: spawn_opts
|
136
|
+
|
137
|
+
pid = spawn cmd, spawn_opts
|
138
|
+
|
139
|
+
Cmds.debug "spawned.",
|
140
|
+
pid: pid
|
141
|
+
|
142
|
+
wait_thread = Process.detach pid
|
143
|
+
wait_thread[:name] = "WAIT"
|
144
|
+
|
145
|
+
Cmds.debug "wait thread created.",
|
146
|
+
thread: wait_thread
|
147
|
+
|
148
|
+
# close child ios if created
|
149
|
+
# the spawned process will read from in_pipe.r so we don't need it
|
150
|
+
in_pipe.r.close if in_pipe
|
151
|
+
# and we don't need to write to the output pipes, that will also happen
|
152
|
+
# in the spawned process
|
153
|
+
[out_pipe, err_pipe].each {|pipe| pipe.w.close if pipe}
|
154
|
+
|
155
|
+
# create threads to handle any pipes that were created
|
156
|
+
|
157
|
+
in_thread = if in_pipe
|
158
|
+
Thread.new do
|
159
|
+
Thread.current[:name] = in_pipe.name
|
160
|
+
Cmds.debug "thread started, writing input..."
|
161
|
+
|
162
|
+
in_pipe.w.write input unless input.nil?
|
163
|
+
|
164
|
+
Cmds.debug "write done, closing in_pipe.w..."
|
165
|
+
in_pipe.w.close
|
166
|
+
|
167
|
+
Cmds.debug "thread done."
|
168
|
+
end # Thread
|
169
|
+
end
|
170
|
+
|
171
|
+
out_thread, err_thread = [out_pipe, err_pipe].map do |pipe|
|
172
|
+
if pipe
|
173
|
+
Thread.new do
|
174
|
+
Thread.current[:name] = pipe.name
|
175
|
+
Cmds.debug "thread started"
|
176
|
+
|
177
|
+
loop do
|
178
|
+
Cmds.debug "blocking on gets..."
|
179
|
+
line = pipe.r.gets
|
180
|
+
if line.nil?
|
181
|
+
Cmds.debug "received nil, output done."
|
182
|
+
else
|
183
|
+
Cmds.debug NRSER.squish <<-BLOCK
|
184
|
+
received #{ line.bytesize } bytes, passing to handler.
|
185
|
+
BLOCK
|
186
|
+
end
|
187
|
+
handler.thread_send_line pipe.sym, line
|
188
|
+
break if line.nil?
|
189
|
+
end
|
190
|
+
|
191
|
+
Cmds.debug "reading done, closing pipe.r (unless already closed)..."
|
192
|
+
pipe.r.close unless pipe.r.closed?
|
193
|
+
|
194
|
+
Cmds.debug "thread done."
|
195
|
+
end # thread
|
196
|
+
end # if pipe
|
197
|
+
end # map threads
|
198
|
+
|
199
|
+
Cmds.debug "handing off main thread control to the handler..."
|
200
|
+
begin
|
201
|
+
handler.start
|
202
|
+
|
203
|
+
Cmds.debug "handler done."
|
204
|
+
|
205
|
+
ensure
|
206
|
+
# wait for the threads to complete
|
207
|
+
Cmds.debug "joining threads..."
|
208
|
+
|
209
|
+
[in_thread, out_thread, err_thread, wait_thread].each do |thread|
|
210
|
+
if thread
|
211
|
+
Cmds.debug "joining #{ thread[:name] } thread..."
|
212
|
+
thread.join
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
Cmds.debug "all threads done."
|
217
|
+
end
|
218
|
+
|
219
|
+
status = wait_thread.value.exitstatus
|
220
|
+
Cmds.debug "exit status: #{ status.inspect }"
|
221
|
+
|
222
|
+
Cmds.debug "checking @assert and exit status..."
|
223
|
+
if @assert && status != 0
|
224
|
+
# we don't necessarily have the err output, so we can't include it
|
225
|
+
# in the error message
|
226
|
+
msg = NRSER.squish <<-BLOCK
|
227
|
+
streamed command `#{ cmd }` exited with status #{ status }
|
228
|
+
BLOCK
|
229
|
+
|
230
|
+
raise SystemCallError.new msg, status
|
231
|
+
end
|
232
|
+
|
233
|
+
Cmds.debug "streaming completed."
|
234
|
+
|
235
|
+
return status
|
236
|
+
end #really_stream
|
237
|
+
|
238
|
+
# end private
|
239
|
+
end
|
data/lib/cmds/sugar.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# convenience methods
|
2
|
+
|
3
|
+
# global methods
|
4
|
+
# ==============
|
5
|
+
|
6
|
+
# proxies to `Cmds::capture`
|
7
|
+
def Cmds *args, &block
|
8
|
+
Cmds.capture *args, &block
|
9
|
+
end
|
10
|
+
|
11
|
+
# proxies to `Cmds::ok?`
|
12
|
+
def Cmds? *args, &block
|
13
|
+
Cmds.ok? *args, &block
|
14
|
+
end
|
15
|
+
|
16
|
+
# proxies to `Cmds::assert`
|
17
|
+
def Cmds! *args, &block
|
18
|
+
Cmds.assert *args, &block
|
19
|
+
end
|
20
|
+
|
21
|
+
class Cmds
|
22
|
+
# class methods
|
23
|
+
# =============
|
24
|
+
|
25
|
+
# create a new Cmd from template and subs and call it
|
26
|
+
def self.capture template, *subs, &input_block
|
27
|
+
new(template, options(subs, input_block)).capture
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.ok? template, *subs, &input_block
|
31
|
+
new(template, options(subs, input_block)).ok?
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.error? template, *subs, &input_block
|
35
|
+
new(template, options(subs, input_block)).error?
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.assert template, *subs, &input_block
|
39
|
+
new(
|
40
|
+
template,
|
41
|
+
options(subs, input_block).merge!(assert: true)
|
42
|
+
).capture
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.stream template, *subs, &input_block
|
46
|
+
Cmds.new(template).stream *subs, &input_block
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.stream! template, *subs, &input_block
|
50
|
+
Cmds.new(template, assert: true).stream *subs, &input_block
|
51
|
+
end # ::stream!
|
52
|
+
|
53
|
+
# instance methods
|
54
|
+
# ================
|
55
|
+
|
56
|
+
alias_method :call, :capture
|
57
|
+
|
58
|
+
def ok?
|
59
|
+
stream == 0
|
60
|
+
end
|
61
|
+
|
62
|
+
def error?
|
63
|
+
stream != 0
|
64
|
+
end
|
65
|
+
|
66
|
+
# def assert
|
67
|
+
# capture.raise_error
|
68
|
+
# end
|
69
|
+
|
70
|
+
def proxy
|
71
|
+
stream do |io|
|
72
|
+
io.in = $stdin
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end # class Cmds
|
data/lib/cmds/util.rb
ADDED
@@ -0,0 +1,254 @@
|
|
1
|
+
# util functions
|
2
|
+
class Cmds
|
3
|
+
# class methods
|
4
|
+
# =============
|
5
|
+
|
6
|
+
# shortcut for Shellwords.escape
|
7
|
+
#
|
8
|
+
# also makes it easier to change or customize or whatever
|
9
|
+
def self.esc str
|
10
|
+
Shellwords.escape str
|
11
|
+
end
|
12
|
+
|
13
|
+
# escape option hash.
|
14
|
+
#
|
15
|
+
# this is only useful for the two common option styles:
|
16
|
+
#
|
17
|
+
# - single character keys become `-<char> <value>`
|
18
|
+
#
|
19
|
+
# {x: 1} => "-x 1"
|
20
|
+
#
|
21
|
+
# - longer keys become `--<key>=<value>` options
|
22
|
+
#
|
23
|
+
# {blah: 2} => "--blah=2"
|
24
|
+
#
|
25
|
+
# if you have something else, you're going to have to just put it in
|
26
|
+
# the cmd itself, like:
|
27
|
+
#
|
28
|
+
# Cmds "blah -assholeOptionOn:%{s}", "ok"
|
29
|
+
#
|
30
|
+
# or whatever similar shit said command requires.
|
31
|
+
#
|
32
|
+
# however, if the value is an Array, it will repeat the option for each
|
33
|
+
# value:
|
34
|
+
#
|
35
|
+
# {x: [1, 2, 3]} => "-x 1 -x 2 -x 3"
|
36
|
+
# {blah: [1, 2, 3]} => "--blah=1 --blah=2 --blah=3"
|
37
|
+
#
|
38
|
+
# i can't think of any right now, but i swear i've seen commands that take
|
39
|
+
# opts that way.
|
40
|
+
#
|
41
|
+
def self.expand_option_hash hash
|
42
|
+
hash.map {|key, values|
|
43
|
+
# keys need to be strings
|
44
|
+
key = key.to_s unless key.is_a? String
|
45
|
+
|
46
|
+
[key, values]
|
47
|
+
|
48
|
+
}.sort {|(key_a, values_a), (key_b, values_b)|
|
49
|
+
# sort by the (now string) keys
|
50
|
+
key_a <=> key_b
|
51
|
+
|
52
|
+
}.map {|key, values|
|
53
|
+
# for simplicity's sake, treat all values like an array
|
54
|
+
values = [values] unless values.is_a? Array
|
55
|
+
|
56
|
+
# keys of length 1 expand to `-x v` form
|
57
|
+
expanded = if key.length == 1
|
58
|
+
values.map {|value|
|
59
|
+
if value.nil?
|
60
|
+
"-#{ esc key }"
|
61
|
+
else
|
62
|
+
"-#{ esc key } #{ esc value}"
|
63
|
+
end
|
64
|
+
}
|
65
|
+
|
66
|
+
# longer keys expand to `--key=value` form
|
67
|
+
else
|
68
|
+
values.map {|value|
|
69
|
+
if value.nil?
|
70
|
+
"--#{ esc key }"
|
71
|
+
else
|
72
|
+
"--#{ esc key }=#{ esc value }"
|
73
|
+
end
|
74
|
+
}
|
75
|
+
end
|
76
|
+
}.flatten.join ' '
|
77
|
+
end # ::expand_option_hash
|
78
|
+
|
79
|
+
# expand one of the substitutions
|
80
|
+
def self.expand_sub sub
|
81
|
+
case sub
|
82
|
+
when nil
|
83
|
+
# nil is just an empty string, NOT an empty string bash token
|
84
|
+
''
|
85
|
+
when Hash
|
86
|
+
expand_option_hash sub
|
87
|
+
else
|
88
|
+
esc sub.to_s
|
89
|
+
end
|
90
|
+
end # ::expand_sub
|
91
|
+
|
92
|
+
# substitute values into a command, escaping them for the shell and
|
93
|
+
# offering convenient expansions for some structures.
|
94
|
+
#
|
95
|
+
# `cmd` is a string that can be substituted via ruby's `%` operator, like
|
96
|
+
#
|
97
|
+
# "git diff %s"
|
98
|
+
#
|
99
|
+
# for positional substitution, or
|
100
|
+
#
|
101
|
+
# "git diff %{path}"
|
102
|
+
#
|
103
|
+
# for keyword substitution.
|
104
|
+
#
|
105
|
+
# `subs` is either:
|
106
|
+
#
|
107
|
+
# - an Array when `cmd` has positional placeholders
|
108
|
+
# - a Hash when `cmd` has keyword placeholders.
|
109
|
+
#
|
110
|
+
# the elements of the `subs` array or values of the `subs` hash are:
|
111
|
+
#
|
112
|
+
# - strings that are substituted into `cmd` after being escaped:
|
113
|
+
#
|
114
|
+
# sub "git diff %{path}", path: "some path/to somewhere"
|
115
|
+
# # => 'git diff some\ path/to\ somewhere'
|
116
|
+
#
|
117
|
+
# - hashes that are expanded into options:
|
118
|
+
#
|
119
|
+
# sub "psql %{opts} %{database} < %{filepath}",
|
120
|
+
# database: "blah",
|
121
|
+
# filepath: "/where ever/it/is.psql",
|
122
|
+
# opts: {
|
123
|
+
# username: "bingo bob",
|
124
|
+
# host: "localhost",
|
125
|
+
# port: 12345,
|
126
|
+
# }
|
127
|
+
# # => 'psql --host=localhost --port=12345 --username=bingo\ bob blah < /where\ ever/it/is.psql'
|
128
|
+
#
|
129
|
+
def self.sub cmd, args = [], kwds = {}
|
130
|
+
raise TypeError.new("args must be an Array") unless args.is_a? Array
|
131
|
+
raise TypeError.new("kwds must be an Hash") unless kwds.is_a? Hash
|
132
|
+
|
133
|
+
context = ERBContext.new(args, kwds)
|
134
|
+
erb = ShellEruby.new(replace_shortcuts cmd)
|
135
|
+
|
136
|
+
NRSER.squish erb.result(context.get_binding)
|
137
|
+
end # ::sub
|
138
|
+
|
139
|
+
def self.options subs, input_block
|
140
|
+
args = []
|
141
|
+
kwds = {}
|
142
|
+
input = input_block.nil? ? nil : input_block.call
|
143
|
+
|
144
|
+
case subs.length
|
145
|
+
when 0
|
146
|
+
# nothing to do
|
147
|
+
when 1
|
148
|
+
# can either be a hash, which is interpreted as a keywords,
|
149
|
+
# or an array, which is interpreted as positional arguments
|
150
|
+
case subs[0]
|
151
|
+
when Hash
|
152
|
+
kwds = subs[0]
|
153
|
+
|
154
|
+
when Array
|
155
|
+
args = subs[0]
|
156
|
+
|
157
|
+
else
|
158
|
+
raise TypeError.new NRSER.squish <<-BLOCK
|
159
|
+
first *subs arg must be Array or Hash, not #{ subs[0].inspect }
|
160
|
+
BLOCK
|
161
|
+
end
|
162
|
+
|
163
|
+
when 2
|
164
|
+
# first arg needs to be an array, second a hash
|
165
|
+
unless subs[0].is_a? Array
|
166
|
+
raise TypeError.new NRSER.squish <<-BLOCK
|
167
|
+
first *subs arg needs to be an array, not #{ subs[0].inspect }
|
168
|
+
BLOCK
|
169
|
+
end
|
170
|
+
|
171
|
+
unless subs[1].is_a? Hash
|
172
|
+
raise TypeError.new NRSER.squish <<-BLOCK
|
173
|
+
second *subs arg needs to be a Hash, not #{ subs[1].inspect }
|
174
|
+
BLOCK
|
175
|
+
end
|
176
|
+
|
177
|
+
args, kwds = subs
|
178
|
+
else
|
179
|
+
raise ArgumentError.new NRSER.squish <<-BLOCK
|
180
|
+
must provide one or two *subs arguments, received #{ 1 + subs.length }
|
181
|
+
BLOCK
|
182
|
+
end
|
183
|
+
|
184
|
+
return {
|
185
|
+
args: args,
|
186
|
+
kwds: kwds,
|
187
|
+
input: input,
|
188
|
+
}
|
189
|
+
end # ::options
|
190
|
+
|
191
|
+
def self.replace_shortcuts template
|
192
|
+
template
|
193
|
+
.gsub(
|
194
|
+
# %s => <%= arg %>
|
195
|
+
/(?<=\A|[[:space:]])\%s(?=\Z|[[:space:]])/,
|
196
|
+
'<%= arg %>'
|
197
|
+
)
|
198
|
+
.gsub(
|
199
|
+
# %%s => %s (escpaing)
|
200
|
+
/(?<=\A|[[:space:]])(\%+)\%s(?=\Z|[[:space:]])/,
|
201
|
+
'\1s'
|
202
|
+
)
|
203
|
+
.gsub(
|
204
|
+
# %{key} => <%= key %>, %{key?} => <%= key? %>
|
205
|
+
/(?<=\A|[[:space:]])\%\{([a-zA-Z_]+\??)\}(?=\Z|[[:space:]])/,
|
206
|
+
'<%= \1 %>'
|
207
|
+
)
|
208
|
+
.gsub(
|
209
|
+
# %%{key} => %{key}, %%{key?} => %{key?} (escpaing)
|
210
|
+
/(?<=\A|[[:space:]])(\%+)\%\{([a-zA-Z_]+\??)\}(?=\Z|[[:space:]])/,
|
211
|
+
'\1{\2}\3'
|
212
|
+
)
|
213
|
+
.gsub(
|
214
|
+
# %<key>s => <%= key %>, %<key?>s => <%= key? %>
|
215
|
+
/(?<=\A|[[:space:]])\%\<([a-zA-Z_]+\??)\>s(?=\Z|[[:space:]])/,
|
216
|
+
'<%= \1 %>'
|
217
|
+
)
|
218
|
+
.gsub(
|
219
|
+
# %%<key>s => %<key>s, %%<key?>s => %<key?>s (escaping)
|
220
|
+
/(?<=\A|[[:space:]])(\%+)\%\<([a-zA-Z_]+\??)\>s(?=\Z|[[:space:]])/,
|
221
|
+
'\1<\2>s'
|
222
|
+
)
|
223
|
+
end # ::replace_shortcuts
|
224
|
+
|
225
|
+
# instance methods
|
226
|
+
# ================
|
227
|
+
|
228
|
+
# returns a new `Cmds` with the subs and input block merged in
|
229
|
+
def curry *subs, &input_block
|
230
|
+
self.class.new @template, merge_options(subs, input_block)
|
231
|
+
end
|
232
|
+
|
233
|
+
private
|
234
|
+
|
235
|
+
# merges options already present on the object with options
|
236
|
+
# provided via subs and input_block and returns a new options
|
237
|
+
# Hash
|
238
|
+
def merge_options subs, input_block
|
239
|
+
# get the options present in the arguments
|
240
|
+
options = Cmds.options subs, input_block
|
241
|
+
# the new args are created by appending the provided args to the
|
242
|
+
# existing ones
|
243
|
+
options[:args] = @args + options[:args]
|
244
|
+
# the new kwds are created by merging the provided kwds into the
|
245
|
+
# exising ones (new values override previous)
|
246
|
+
options[:kwds] = @kwds.merge options[:kwds]
|
247
|
+
# if there is input present via the provided block, it is used.
|
248
|
+
# otherwise, previous input is used, which may be `nil`
|
249
|
+
options[:input] ||= @input
|
250
|
+
return options
|
251
|
+
end # #merge_options
|
252
|
+
|
253
|
+
# end private
|
254
|
+
end # class Cmds
|
data/lib/cmds/version.rb
CHANGED