appear 1.1.1 → 1.2.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.
- 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
|