appear 1.1.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.doc-coverage +1 -1
- data/CHANGELOG.md +11 -0
- data/README.md +1 -1
- data/bin/appear +6 -0
- data/lib/appear/command.rb +28 -0
- data/lib/appear/config.rb +7 -0
- data/lib/appear/constants.rb +1 -4
- data/lib/appear/editor/nvim.rb +251 -0
- data/lib/appear/editor.rb +262 -0
- data/lib/appear/instance.rb +4 -0
- data/lib/appear/lsof.rb +84 -51
- data/lib/appear/mac_os.rb +12 -2
- data/lib/appear/output.rb +16 -0
- data/lib/appear/processes.rb +4 -6
- data/lib/appear/revealers.rb +65 -62
- data/lib/appear/runner.rb +25 -5
- data/lib/appear/terminal.rb +127 -0
- data/lib/appear/tmux.rb +285 -39
- data/lib/appear/util/command_builder.rb +148 -0
- data/lib/appear/util/join.rb +144 -0
- data/lib/appear/util/memoizer.rb +83 -0
- data/lib/appear/util/value_class.rb +57 -0
- data/lib/appear/util.rb +6 -0
- data/lib/appear.rb +55 -1
- data/scripts/console +9 -1
- data/tools/macOS-helper.js +24 -16
- data/tools/unix-dropper.applescript +167 -0
- metadata +11 -3
- data/lib/appear/join.rb +0 -134
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'appear/service'
|
2
|
+
require 'appear/constants'
|
3
|
+
|
4
|
+
module Appear
|
5
|
+
# A base service that retrieves a usable Terminal instance, suitable for
|
6
|
+
# spawning command-line programs.
|
7
|
+
class Terminals
|
8
|
+
def initialize(terminals)
|
9
|
+
@terminals = terminals
|
10
|
+
end
|
11
|
+
|
12
|
+
# Get the Terminal instance
|
13
|
+
#
|
14
|
+
# @return [Terminal::MacTerminal]
|
15
|
+
def get
|
16
|
+
current || default
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def current
|
22
|
+
@terminals.find { |t| t.running? }
|
23
|
+
end
|
24
|
+
|
25
|
+
def default
|
26
|
+
# TODO: select terminals based on OS support or should that happen
|
27
|
+
# upstream? in the creator of the Terminals instance? No. It should
|
28
|
+
# happen here.
|
29
|
+
@terminals.find { |t| t.class == TerminalApp }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# This module contains Terminal service implementations. So far, we're only
|
34
|
+
# concerned with Mac usrs, so you can look at {MacTerminal} to read the
|
35
|
+
# interface.
|
36
|
+
module Terminal
|
37
|
+
# Base class for mac terminal support
|
38
|
+
class MacTerminal < Service
|
39
|
+
require_service :mac_os
|
40
|
+
require_service :processes
|
41
|
+
|
42
|
+
# @abstract subclasses must implement this method.
|
43
|
+
# @return [String] "app name" on OS X. Both the process name of processes
|
44
|
+
# of this app, and the name used by Applescript to send events to this
|
45
|
+
# app.
|
46
|
+
def app_name
|
47
|
+
raise NotImplemented
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Boolean] true if the app has a running process, false
|
51
|
+
# otherwise.
|
52
|
+
def running?
|
53
|
+
services.processes.pgrep(app_name).length > 0
|
54
|
+
end
|
55
|
+
|
56
|
+
# Enumerate the panes (seperate interactive sessions) that this terminal
|
57
|
+
# program has.
|
58
|
+
#
|
59
|
+
# @abstract subclasses must implement this method.
|
60
|
+
# @return [Array<#tty>] any objects with a tty field
|
61
|
+
def panes
|
62
|
+
raise NotImplemented
|
63
|
+
end
|
64
|
+
|
65
|
+
# Reveal a pane. Subclasses must implement this method.
|
66
|
+
#
|
67
|
+
# @abstract subclasses must implement this method.
|
68
|
+
# @param pane [#tty] any object with a tty field
|
69
|
+
def reveal_pane(pane)
|
70
|
+
raise NotImplemented
|
71
|
+
end
|
72
|
+
end # MacTerminal
|
73
|
+
|
74
|
+
# TerminalApp support service.
|
75
|
+
class TerminalApp < MacTerminal
|
76
|
+
# @see MacTerminal#app_name
|
77
|
+
def app_name
|
78
|
+
'Terminal'
|
79
|
+
end
|
80
|
+
|
81
|
+
# @see MacTerminal#panes
|
82
|
+
def panes
|
83
|
+
pids = services.processes.pgrep(app_name)
|
84
|
+
services.mac_os.call_method('terminal_panes').map do |hash|
|
85
|
+
hash[:pids] = pids
|
86
|
+
OpenStruct.new(hash)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# @see MacTerminal#reveal_pane
|
91
|
+
def reveal_pane(pane)
|
92
|
+
services.mac_os.call_method('terminal_reveal_tty', pane.tty)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Iterm2 support service.
|
97
|
+
class Iterm2 < MacTerminal
|
98
|
+
# @see MacTerminal#app_name
|
99
|
+
def app_name
|
100
|
+
'iTerm2'
|
101
|
+
end
|
102
|
+
|
103
|
+
# @see MacTerminal#panes
|
104
|
+
def panes
|
105
|
+
pids = services.processes.pgrep(app_name)
|
106
|
+
services.mac_os.call_method('iterm2_panes').map do |hash|
|
107
|
+
hash[:pids] = pids
|
108
|
+
OpenStruct.new(hash)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# @see MacTerminal#reveal_pane
|
113
|
+
def reveal_pane(pane)
|
114
|
+
services.mac_os.call_method('iterm2_reveal_tty', pane.tty)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Create a new window running the given command.
|
118
|
+
#
|
119
|
+
# @param command_str [String] command to run
|
120
|
+
# @return [#tty] pane
|
121
|
+
def new_window(command_str)
|
122
|
+
res = services.mac_os.call_method('iterm2_new_window', command_str)
|
123
|
+
OpenStruct.new(res)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/appear/tmux.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
require 'ostruct'
|
2
1
|
require 'appear/service'
|
2
|
+
require 'appear/util/command_builder'
|
3
|
+
require 'appear/util/value_class'
|
3
4
|
|
4
5
|
module Appear
|
5
6
|
# The Tmux service is in charge of interacting with `tmux` processes. It is
|
@@ -11,67 +12,312 @@ module Appear
|
|
11
12
|
class Tmux < Service
|
12
13
|
delegate :run, :runner
|
13
14
|
|
15
|
+
# Base value class for Tmux values. This class works in concert with the
|
16
|
+
# Tmux service to make a fluent Tmux API easy.
|
17
|
+
#
|
18
|
+
# TmuxValues all have a reference to the Tmux service that created them, so
|
19
|
+
# that they can implement methods that proxy Tmux service interaction.
|
20
|
+
class TmuxValue < ::Appear::Util::ValueClass
|
21
|
+
# @return [Tmux] the tmux service that created this pane
|
22
|
+
property :tmux
|
23
|
+
|
24
|
+
# @option opts [Symbol] :tmux tmux format string name of this attribute
|
25
|
+
# @option opts [#to_proc] :parse proc taking a String (read from tmux) and
|
26
|
+
# returns the type-coerced version of this field. A symbol can be used,
|
27
|
+
# just like with usual block syntax.
|
28
|
+
def self.property(name, opts = {})
|
29
|
+
var = super(name, opts)
|
30
|
+
@tmux_attrs ||= {}
|
31
|
+
@tmux_attrs[var] = opts if opts[:tmux]
|
32
|
+
end
|
33
|
+
|
34
|
+
# The format string we pass to Tmux when we expect a result of this
|
35
|
+
# class's type. This format string should cause Tmux to return a value we
|
36
|
+
# can hand to {self.parse}
|
37
|
+
#
|
38
|
+
# @return [String]
|
39
|
+
def self.format_string
|
40
|
+
result = ""
|
41
|
+
@tmux_attrs.each do |var, opts|
|
42
|
+
next unless opts[:tmux]
|
43
|
+
part = ' ' + var.to_s + ':#{' + opts[:tmux].to_s + '}'
|
44
|
+
result += part
|
45
|
+
end
|
46
|
+
result
|
47
|
+
end
|
48
|
+
|
49
|
+
# Parse a raw data has as returned by the {Tmux} service into an instance
|
50
|
+
# of this class.
|
51
|
+
#
|
52
|
+
# @param tmux_hash [Hash]
|
53
|
+
# @param tmux [Tmux] the tmux service
|
54
|
+
def self.parse(tmux_hash, tmux)
|
55
|
+
result = { :tmux => tmux }
|
56
|
+
tmux_hash.each do |var, tmux_val|
|
57
|
+
parser = @tmux_attrs[var][:parse]
|
58
|
+
if parser
|
59
|
+
result[var] = parser.to_proc.call(tmux_val)
|
60
|
+
else
|
61
|
+
result[var] = tmux_val
|
62
|
+
end
|
63
|
+
end
|
64
|
+
self.new(result)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# A tmux pane.
|
69
|
+
class Pane < TmuxValue
|
70
|
+
# @return [Fixnum] pid of the process running in the pane
|
71
|
+
property :pid, tmux: :pane_pid, parse: :to_i
|
72
|
+
|
73
|
+
# @return [String] session name
|
74
|
+
property :session, tmux: :session_name
|
75
|
+
|
76
|
+
# @return [Fixnum] window index
|
77
|
+
property :window, tmux: :window_index, parse: :to_i
|
78
|
+
|
79
|
+
# @return [Fixnum] pane index
|
80
|
+
property :pane, tmux: :pane_index, parse: :to_i
|
81
|
+
|
82
|
+
# @return [Boolean] is this pane the active pane in this session
|
83
|
+
property :active?, var: :active, tmux: :pane_active, parse: proc {|a| a.to_i != 0 }
|
84
|
+
|
85
|
+
# @return [String] command running in this pane
|
86
|
+
property :command_name, tmux: :pane_current_command
|
87
|
+
|
88
|
+
# @return [String] pane current path
|
89
|
+
property :current_path, tmux: :pane_current_path
|
90
|
+
|
91
|
+
# @return [String] window id
|
92
|
+
property :id, :tmux => :pane_id
|
93
|
+
|
94
|
+
# String suitable for use as the "target" specifier for a Tmux command
|
95
|
+
#
|
96
|
+
# @return [String]
|
97
|
+
def target
|
98
|
+
# "#{session}:#{window}.#{pane}"
|
99
|
+
id
|
100
|
+
end
|
101
|
+
|
102
|
+
# Split this pane
|
103
|
+
#
|
104
|
+
# @param opts [Hash]
|
105
|
+
def split(opts = {})
|
106
|
+
tmux.split_window(opts.merge(:t => target))
|
107
|
+
end
|
108
|
+
|
109
|
+
# Reveal this pane
|
110
|
+
def reveal
|
111
|
+
tmux.reveal_pane(self)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Send keys to this pane
|
115
|
+
#
|
116
|
+
# @param keys [String]
|
117
|
+
# @param opts [Hash]
|
118
|
+
def send_keys(keys, opts = {})
|
119
|
+
tmux.send_keys(self, keys, opts)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# A tmux session.
|
124
|
+
# Has many windows.
|
125
|
+
class Session < TmuxValue
|
126
|
+
# @return [String] session name
|
127
|
+
property :session, tmux: :session_name
|
128
|
+
|
129
|
+
# @return [String] tmux id of this session
|
130
|
+
property :id, :tmux => :session_id
|
131
|
+
|
132
|
+
# @return [Fixnum] number of clients attached to this session
|
133
|
+
property :attached, :tmux => :session_attached, :parse => :to_i
|
134
|
+
|
135
|
+
# @return [Fixnum] width, in text columns
|
136
|
+
property :width, :tmux => :session_width, :parse => :to_i
|
137
|
+
|
138
|
+
# @return [Fixnum] height, in text rows
|
139
|
+
property :height, :tmux => :session_height, :parse => :to_i
|
140
|
+
|
141
|
+
# String suitable for use as the "target" specifier for a Tmux command
|
142
|
+
#
|
143
|
+
# @return [String]
|
144
|
+
def target
|
145
|
+
# session
|
146
|
+
id
|
147
|
+
end
|
148
|
+
|
149
|
+
# @return [Array<Window>] the windows in this session
|
150
|
+
def windows
|
151
|
+
tmux.windows.select { |w| w.session == session }
|
152
|
+
end
|
153
|
+
|
154
|
+
# @return [Array<Client>] all clients attached to this session
|
155
|
+
def clients
|
156
|
+
tmux.clients.select { |c| c.session == session }
|
157
|
+
end
|
158
|
+
|
159
|
+
# Create a new window in this session. By default, the window will be
|
160
|
+
# created at the end of the session.
|
161
|
+
#
|
162
|
+
# @param opts [Hash]
|
163
|
+
def new_window(opts = {})
|
164
|
+
win = windows.last.window || -1
|
165
|
+
tmux.new_window(opts.merge(:t => "#{target}:#{win + 1}"))
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# A tmux window.
|
170
|
+
# Has many panes.
|
171
|
+
class Window < TmuxValue
|
172
|
+
# @return [String] session name
|
173
|
+
property :session, :tmux => :session_name
|
174
|
+
|
175
|
+
# @return [Fixnum] window index
|
176
|
+
property :window, :tmux => :window_index, :parse => :to_i
|
177
|
+
|
178
|
+
# @return [String] window id
|
179
|
+
property :id, :tmux => :window_id
|
180
|
+
|
181
|
+
# @return [Boolean] is the window active?
|
182
|
+
property :active?,
|
183
|
+
:tmux => :window_active,
|
184
|
+
:var => :active,
|
185
|
+
:parse => proc {|b| b.to_i != 0}
|
186
|
+
|
187
|
+
# @return [Array<Pane>]
|
188
|
+
def panes
|
189
|
+
tmux.panes.select { |p| p.session == session && p.window == window }
|
190
|
+
end
|
191
|
+
|
192
|
+
# String suitable for use as the "target" specifier for a Tmux command
|
193
|
+
#
|
194
|
+
# @return [String]
|
195
|
+
def target
|
196
|
+
# "#{session}:#{window}"
|
197
|
+
id
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# A tmux client.
|
202
|
+
class Client < TmuxValue
|
203
|
+
# @return [String] path to the TTY device of this client
|
204
|
+
property :tty, :tmux => :client_tty
|
205
|
+
|
206
|
+
# @return [String] term name
|
207
|
+
property :term, :tmux => :client_termname
|
208
|
+
|
209
|
+
# @return [String] session name
|
210
|
+
property :session, :tmux => :client_session
|
211
|
+
|
212
|
+
# String suitable for use as the "target" specifier for a Tmux command
|
213
|
+
#
|
214
|
+
# @return [String]
|
215
|
+
def target
|
216
|
+
tty
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def initialize(svcs = {})
|
221
|
+
super(svcs)
|
222
|
+
@memo = ::Appear::Util::Memoizer.new
|
223
|
+
end
|
224
|
+
|
225
|
+
# List all the tmux clients on the system
|
226
|
+
#
|
227
|
+
# @return [Array<Client>]
|
14
228
|
def clients
|
15
|
-
|
16
|
-
'list-clients',
|
17
|
-
'-F',
|
18
|
-
format_string(
|
19
|
-
:tty => :client_tty,
|
20
|
-
:term => :client_termname,
|
21
|
-
:session => :client_session
|
22
|
-
),
|
23
|
-
])
|
229
|
+
ipc_returning(command('list-clients'), Client)
|
24
230
|
end
|
25
231
|
|
232
|
+
# List all the tmux panes on the system
|
233
|
+
#
|
234
|
+
# @return [Array<Pane>]
|
26
235
|
def panes
|
27
|
-
panes
|
28
|
-
|
29
|
-
'-a',
|
30
|
-
'-F',
|
31
|
-
format_string(
|
32
|
-
:pid => :pane_pid,
|
33
|
-
:session => :session_name,
|
34
|
-
:window => :window_index,
|
35
|
-
:pane => :pane_index,
|
36
|
-
:command_name => :pane_current_command,
|
37
|
-
:active => :pane_active)
|
38
|
-
])
|
236
|
+
ipc_returning(command('list-panes').flags(:a => true), Pane)
|
237
|
+
end
|
39
238
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
239
|
+
# List all the tmux sessions on the system
|
240
|
+
#
|
241
|
+
# @return [Array<Session>]
|
242
|
+
def sessions
|
243
|
+
ipc_returning(command('list-sessions'), Session)
|
244
|
+
end
|
45
245
|
|
46
|
-
|
246
|
+
# List all the tmux windows in any session on the system
|
247
|
+
#
|
248
|
+
# @return [Array<Window>]
|
249
|
+
def windows
|
250
|
+
ipc_returning(command('list-windows').flags(:a => true), Window)
|
47
251
|
end
|
48
252
|
|
253
|
+
# Reveal a pane in tmux.
|
254
|
+
#
|
255
|
+
# @param pane [Pane] a pane
|
49
256
|
def reveal_pane(pane)
|
50
|
-
ipc(
|
51
|
-
|
257
|
+
ipc(command('select-pane').flags(:t => pane.target))
|
258
|
+
# TODO: how do we use a real target for this?
|
259
|
+
ipc(command('select-window').flags(:t => "#{pane.session}:#{pane.window}"))
|
260
|
+
pane
|
261
|
+
end
|
262
|
+
|
263
|
+
# Create a new window
|
264
|
+
def new_window(opts = {})
|
265
|
+
ipc_returning_one(command('new-window').flags(opts), Window)
|
266
|
+
end
|
267
|
+
|
268
|
+
# Split a window
|
269
|
+
def split_window(opts = {})
|
270
|
+
ipc_returning_one(command('split-window').flags(opts), Pane)
|
271
|
+
end
|
272
|
+
|
273
|
+
# Create a new session
|
274
|
+
def new_session(opts = {})
|
275
|
+
ipc_returning_one(command('new-session').flags(opts), Session)
|
276
|
+
end
|
277
|
+
|
278
|
+
# Send keys to a pane
|
279
|
+
def send_keys(pane, keys, opts = {})
|
280
|
+
ipc(command('send-keys').flags(opts.merge(:t => pane.target)).args(*keys))
|
281
|
+
end
|
282
|
+
|
283
|
+
# Construct a command that will attach the given session when run
|
284
|
+
#
|
285
|
+
# @param session [String] use Session#target
|
286
|
+
# @return [Appear::Util::CommandBuilder]
|
287
|
+
def attach_session_command(session)
|
288
|
+
command('attach-session').flags(:t => session)
|
52
289
|
end
|
53
290
|
|
54
291
|
private
|
55
292
|
|
56
|
-
def
|
57
|
-
|
293
|
+
def command(subcommand)
|
294
|
+
Appear::Util::CommandBuilder.new(['tmux', subcommand])
|
295
|
+
end
|
296
|
+
|
297
|
+
def ipc(cmd)
|
298
|
+
res = run(cmd.to_a)
|
58
299
|
res.lines.map do |line|
|
59
300
|
info = {}
|
60
301
|
line.strip.split(' ').each do |pair|
|
61
302
|
key, *value = pair.split(':')
|
62
303
|
info[key.to_sym] = value.join(':')
|
63
304
|
end
|
64
|
-
|
305
|
+
info
|
65
306
|
end
|
66
307
|
end
|
67
308
|
|
68
|
-
def
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
309
|
+
def ipc_returning(cmd, klass)
|
310
|
+
@memo.call(cmd, klass) do
|
311
|
+
cmd.flags(:F => klass.format_string)
|
312
|
+
ipc(cmd).map do |row|
|
313
|
+
klass.parse(row, self)
|
314
|
+
end
|
73
315
|
end
|
74
|
-
|
316
|
+
end
|
317
|
+
|
318
|
+
def ipc_returning_one(cmd, klass)
|
319
|
+
# -P in tmux is usually required to print information about newly created objects
|
320
|
+
ipc_returning(cmd.flags(:P => true), klass).first
|
75
321
|
end
|
76
322
|
end
|
77
323
|
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module Appear
|
2
|
+
module Util
|
3
|
+
# Builds command strings.
|
4
|
+
#
|
5
|
+
# @example A tmux query command
|
6
|
+
# tmux_panes = CommandBuilder.new(%w(tmux list-panes)).
|
7
|
+
# flags(:a => true, :F => '#{session_name} #{pane_index}')
|
8
|
+
# output, status = Open3.capture2e(*tmux_panes.to_a)
|
9
|
+
class CommandBuilder
|
10
|
+
# @param command [#to_s, Array<#to_s>] the command. Use an array if you
|
11
|
+
# need multiple words before we start listing arguments, eg `%w(vagrant
|
12
|
+
# up)`
|
13
|
+
#
|
14
|
+
# @param opts [Hash] options hash
|
15
|
+
# @option opts [Boolean] :single_dash_long_flags When true, flags like
|
16
|
+
# :foo will be printed like "-foo" instead of the default "--foo"
|
17
|
+
# @option opts [Boolean] :dashdash_after_flags When true, a "--" argument
|
18
|
+
# will be inserted after the flags but before the arguments.
|
19
|
+
#
|
20
|
+
# @example dashdash_after_flags
|
21
|
+
# c = CommandBuilder.new('ssh', :dashdash_after_flags => true)
|
22
|
+
# .flags(:foo => 1, :b => true).args('a', 'b').to_s
|
23
|
+
# "ssh --foo 1 -b -- a b"
|
24
|
+
def initialize(command, opts = {})
|
25
|
+
@command = command
|
26
|
+
@flags = opts.delete(:flags) || Hash.new { |h, k| h[k] = [] }
|
27
|
+
@argv = opts.delete(:argv) || []
|
28
|
+
@options = {
|
29
|
+
:single_dash_long_flags => false,
|
30
|
+
:dashdash_after_flags => false,
|
31
|
+
}.merge(opts)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Add a flag to this command
|
35
|
+
#
|
36
|
+
# @param name [#to_s] flag name, eg 'cached' for --cached
|
37
|
+
# @param val [Boolean, #to_s] flag value, eg '3fdb21'. Can pass "true"
|
38
|
+
# for boolean, set-only flags.
|
39
|
+
# @return [self]
|
40
|
+
def flag(name, val)
|
41
|
+
@flags[name] << val
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
# Add a bunch of flags at once, using a map of flag => argument.
|
46
|
+
#
|
47
|
+
# @param flag_map [Hash<#to_s, [TrueClass, #to_s, Array<#to_s>]>]
|
48
|
+
# @return [self]
|
49
|
+
#
|
50
|
+
# @example multiple duplicate args
|
51
|
+
# CommandBuilder.new('foo').flags(:o => ['Val1', 'Val2]).to_s
|
52
|
+
# # "foo -o Val1 -o Val2"
|
53
|
+
def flags(flag_map)
|
54
|
+
flag_map.each do |f, v|
|
55
|
+
if v.is_a?(Array)
|
56
|
+
v.each do |v_prime|
|
57
|
+
flag(f, v_prime)
|
58
|
+
end
|
59
|
+
else
|
60
|
+
flag(f, v)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
# Add arguments to this command. Arguments always come after flags, and
|
67
|
+
# may be seperated from flags with -- if you pass :dashdash_after_flags
|
68
|
+
# option in the constructor.
|
69
|
+
#
|
70
|
+
# @param args [Array<#to_s>] args to add
|
71
|
+
# @return [self]
|
72
|
+
def args(*args)
|
73
|
+
@argv.concat(args)
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
# Add a subcommand, with its own flags arguments, after the current
|
78
|
+
# command. This is useful for eg building calls to nested commands.
|
79
|
+
#
|
80
|
+
# @param name [#to_s, Array<#to_s>] the subcommand, see {#initialize}
|
81
|
+
# @param opts [Hash] see {#initialize}
|
82
|
+
# @yield [subc] Add flags and arguments to the subcommand
|
83
|
+
# @yieldparam [CommandBuilder] subc the subcommand
|
84
|
+
# @return [self]
|
85
|
+
#
|
86
|
+
# @example eg, vagrant
|
87
|
+
# v_up = CommandBuilder.new('vagrant').flags(:root => pwd).subcommand('up') do |up|
|
88
|
+
# up.flags(:provider => :virtualbox', 'no-provision' => true).args('apollo')
|
89
|
+
# end
|
90
|
+
def subcommand(name, opts = {})
|
91
|
+
# use our options as the defaults
|
92
|
+
# then use the given options as the overrides
|
93
|
+
subc = CommandBuilder.new(name, @options.merge(opts))
|
94
|
+
yield(subc)
|
95
|
+
args(*subc.to_a)
|
96
|
+
self
|
97
|
+
end
|
98
|
+
|
99
|
+
# Render this command to an array of strings, suitable for execution with
|
100
|
+
# `system` or other methods that take an ARGV array.
|
101
|
+
#
|
102
|
+
# @return [Array<String>] the command
|
103
|
+
def to_a
|
104
|
+
res = [@command].flatten
|
105
|
+
@flags.each do |name, params|
|
106
|
+
flag = flag_name_to_arg(name)
|
107
|
+
params.each do |param|
|
108
|
+
res << flag
|
109
|
+
res << param.to_s unless param == true
|
110
|
+
end
|
111
|
+
end
|
112
|
+
res << '--' if @options[:dashdash_after_flags]
|
113
|
+
res.concat(@argv)
|
114
|
+
res.map { |v| v.to_s }
|
115
|
+
end
|
116
|
+
|
117
|
+
# Render this command as a string, suitable for execution with `sh` or
|
118
|
+
# `system` or other methods that take a command string.
|
119
|
+
#
|
120
|
+
# @return [String] the command
|
121
|
+
def to_s
|
122
|
+
to_a.shelljoin
|
123
|
+
end
|
124
|
+
|
125
|
+
# Duplicate this {CommandBuilder} instance.
|
126
|
+
#
|
127
|
+
# @return [CommandBuilder]
|
128
|
+
def dup
|
129
|
+
opts = @options.dup
|
130
|
+
opts[:argv] = @argv.dup
|
131
|
+
opts[:flags] = @flags.dup
|
132
|
+
self.class.new(@command.dup, opts)
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
# @param flag [#to_s]
|
138
|
+
# @return [String]
|
139
|
+
def flag_name_to_arg(flag)
|
140
|
+
if flag.to_s.length == 1 || @options[:single_dash_long_flags]
|
141
|
+
"-#{flag}"
|
142
|
+
else
|
143
|
+
"--#{flag}"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|