shell_helpers 0.1.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +6 -2
- data/.travis.yml +10 -0
- data/.yardopts +6 -1
- data/Gemfile +8 -0
- data/LICENSE.txt +1 -1
- data/README.md +27 -3
- data/Rakefile +7 -12
- data/TODO +2 -0
- data/bin/abs_to_rel.rb +42 -0
- data/bin/mv_and_ln.rb +54 -0
- data/gemspec.yml +5 -4
- data/lib/shell_helpers.rb +38 -11
- data/lib/shell_helpers/export.rb +169 -0
- data/lib/shell_helpers/logger.rb +83 -28
- data/lib/shell_helpers/options.rb +28 -0
- data/lib/shell_helpers/pathname.rb +583 -110
- data/lib/shell_helpers/run.rb +115 -29
- data/lib/shell_helpers/sh.rb +188 -39
- data/lib/shell_helpers/sysutils.rb +427 -0
- data/lib/shell_helpers/utils.rb +216 -119
- data/lib/shell_helpers/version.rb +1 -1
- data/shell_helpers.gemspec +13 -1
- data/test/helper.rb +12 -1
- data/test/test_export.rb +77 -0
- metadata +33 -8
- data/.document +0 -3
data/lib/shell_helpers/run.rb
CHANGED
@@ -2,35 +2,94 @@
|
|
2
2
|
require 'open3'
|
3
3
|
require 'shellwords'
|
4
4
|
|
5
|
-
module
|
5
|
+
module ShellHelpers
|
6
6
|
module Run #{{{
|
7
7
|
extend(self)
|
8
|
+
RunError=Class.new(StandardError)
|
9
|
+
|
10
|
+
def sudo_args(sudoarg)
|
11
|
+
return "" unless sudoarg
|
12
|
+
return sudoarg.shellsplit if sudoarg.is_a?(String)
|
13
|
+
["sudo"]
|
14
|
+
end
|
15
|
+
|
8
16
|
#the run_* commands here capture all the output
|
9
17
|
def run_command(*command)
|
18
|
+
#stdout, stderr, status
|
10
19
|
return Open3.capture3(*command)
|
11
20
|
end
|
12
21
|
|
13
|
-
|
14
|
-
|
22
|
+
#Only capture output
|
23
|
+
def output_of(*command)
|
24
|
+
stdout,status = Open3.capture2(*command)
|
25
|
+
yield stdout, status if block_given?
|
15
26
|
return stdout
|
16
27
|
end
|
17
28
|
|
18
|
-
def
|
19
|
-
|
29
|
+
def status_of(*command)
|
30
|
+
stdout,stderr,status = run_command(*command)
|
31
|
+
yield stdout, stderr, status if block_given?
|
20
32
|
return status.success?
|
33
|
+
#system(*command)
|
34
|
+
#return $?.dup
|
21
35
|
end
|
22
36
|
|
23
|
-
#
|
24
|
-
|
25
|
-
|
26
|
-
|
37
|
+
#wrap the output of the command in an enumerator
|
38
|
+
#allows to lazily parse the result
|
39
|
+
def run_lazy(*command)
|
40
|
+
r=nil
|
41
|
+
IO.popen(command) do |f|
|
42
|
+
r=f.each_line.lazy
|
43
|
+
end
|
44
|
+
r
|
45
|
+
end
|
46
|
+
|
47
|
+
def process_command(*args, **opts)
|
48
|
+
spawn_opts={}
|
49
|
+
if args.last.kind_of?(Hash)
|
50
|
+
#we may have no symbol keywords
|
51
|
+
*args,spawn_opts=*args
|
52
|
+
end
|
53
|
+
sudo=opts.delete(:sudo)
|
54
|
+
env={}
|
55
|
+
if args.first.kind_of?(Hash)
|
56
|
+
env,*args=*args
|
57
|
+
end
|
58
|
+
env.merge!(opts.delete(:env)||{})
|
59
|
+
args=args.map {|arg| arg.to_s} if args.length > 1
|
60
|
+
spawn_opts.merge!(opts)
|
61
|
+
if sudo
|
62
|
+
if args.length > 1
|
63
|
+
args.unshift(*Run.sudo_args(sudo))
|
64
|
+
else
|
65
|
+
args="#{Run.sudo_args(sudo).shelljoin} #{args.first}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
return env, args, spawn_opts
|
69
|
+
end
|
70
|
+
|
71
|
+
#by default capture stdout and status
|
72
|
+
def run(*args, output: :capture, error: nil, fail_mode: :error, chomp: false, sudo: false, error_mode: nil, expected: nil, on_success: nil, quiet: nil, **opts)
|
73
|
+
env, args, spawn_opts=Run.process_command(*args, **opts)
|
74
|
+
|
75
|
+
if args.length > 1
|
76
|
+
launch=args.shelljoin
|
27
77
|
else
|
28
|
-
launch=
|
78
|
+
launch=args.first #assume it has already been escaped
|
29
79
|
end
|
30
|
-
launch+=" 2>/dev/null" if quiet
|
80
|
+
launch+=" 2>/dev/null" if error==:quiet or quiet
|
81
|
+
launch+=" >/dev/null" if output==:quiet
|
82
|
+
out=error=nil
|
83
|
+
|
31
84
|
begin
|
32
|
-
|
33
|
-
|
85
|
+
if error==:capture
|
86
|
+
out, error, status=Open3.capture3(env, launch, spawn_opts)
|
87
|
+
elsif output==:capture
|
88
|
+
out, status=Open3.capture2(env, launch, spawn_opts)
|
89
|
+
else
|
90
|
+
system(env, launch, spawn_opts)
|
91
|
+
status=$?
|
92
|
+
end
|
34
93
|
rescue => e
|
35
94
|
status=false
|
36
95
|
case fail_mode
|
@@ -38,24 +97,49 @@ module SH
|
|
38
97
|
raise e
|
39
98
|
when :empty
|
40
99
|
out=""
|
100
|
+
when :nil
|
101
|
+
out=nil
|
102
|
+
when Proc
|
103
|
+
fail_mode.call(e)
|
41
104
|
end
|
42
105
|
end
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
out
|
52
|
-
|
106
|
+
status=ProcessStatus.new(status, expected) if expected
|
107
|
+
yield status.success?, out, err, status if block_given?
|
108
|
+
if status.success?
|
109
|
+
# this block is called in case of success
|
110
|
+
on_success.call(status, out, err) if on_success.is_a?(Proc)
|
111
|
+
else # the command failed
|
112
|
+
case error_mode
|
113
|
+
when :nil
|
114
|
+
out=nil
|
115
|
+
when :empty
|
116
|
+
out=""
|
117
|
+
when :error
|
118
|
+
raise RunError.new("Error running command '#{launch}': #{status}")
|
119
|
+
when Proc
|
120
|
+
error_mode.call(status, out, error)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
if chomp and out
|
124
|
+
case chomp
|
125
|
+
when :line, :lines
|
126
|
+
#out is now an array
|
127
|
+
out=out.each_line.map {|l| l.chomp}
|
53
128
|
else
|
54
|
-
out
|
55
|
-
status=$?
|
56
|
-
return out, status.success?
|
129
|
+
out.chomp!
|
57
130
|
end
|
58
131
|
end
|
132
|
+
|
133
|
+
return out, error, status if error
|
134
|
+
return out, status
|
135
|
+
end
|
136
|
+
|
137
|
+
#a simple wrapper for %x//
|
138
|
+
def run_simple(*command, **opts, &b)
|
139
|
+
# here the block is called in case of failure
|
140
|
+
opts[:error_mode]=b if b
|
141
|
+
out, *_rest = run(*command, **opts)
|
142
|
+
return out
|
59
143
|
end
|
60
144
|
|
61
145
|
#same as Run, but if we get interrupted once, we don't want to launch any more commands
|
@@ -65,7 +149,7 @@ module SH
|
|
65
149
|
def run_command(*args)
|
66
150
|
if !@interrupted
|
67
151
|
begin
|
68
|
-
|
152
|
+
Run.run_command(*args)
|
69
153
|
rescue Interrupt #interruption
|
70
154
|
@interrupted=true
|
71
155
|
return "", "", false
|
@@ -75,10 +159,12 @@ module SH
|
|
75
159
|
end
|
76
160
|
end
|
77
161
|
|
162
|
+
#TODO: handle non default options, 'error: :capture' would imply we
|
163
|
+
#need to return "", "", false
|
78
164
|
def run(*command)
|
79
165
|
if !@interrupted
|
80
166
|
begin
|
81
|
-
return
|
167
|
+
return Run.run(*command)
|
82
168
|
rescue Interrupt #interruption
|
83
169
|
@interrupted=true
|
84
170
|
return "", false
|
@@ -126,7 +212,7 @@ module SH
|
|
126
212
|
else
|
127
213
|
status
|
128
214
|
end
|
129
|
-
if status.kind_of?
|
215
|
+
if status.kind_of? Integer
|
130
216
|
status
|
131
217
|
elsif status
|
132
218
|
0
|
data/lib/shell_helpers/sh.rb
CHANGED
@@ -1,14 +1,25 @@
|
|
1
1
|
# vim: foldmethod=marker
|
2
|
-
#from methadone (error.rb, exit_now.rb, process_status.rb, run.rb;
|
3
|
-
#import
|
4
|
-
|
5
|
-
|
6
|
-
require '
|
2
|
+
#from methadone (error.rb, exit_now.rb, process_status.rb, run.rb;
|
3
|
+
#last import 4626a2bca9b6e54077a06a0f8e11a04fadc6e7ae, 2017-01-19
|
4
|
+
require 'shell_helpers/logger'
|
5
|
+
require 'shell_helpers/run'
|
6
|
+
require 'forwardable'
|
7
7
|
|
8
|
-
|
8
|
+
begin
|
9
|
+
require 'simplecolor'
|
10
|
+
rescue LoadError
|
11
|
+
#fallback, don't colorize
|
12
|
+
module SimpleColor
|
13
|
+
def self.color(s,**opts)
|
14
|
+
puts s
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module ShellHelpers
|
9
20
|
# ExitNow {{{
|
10
21
|
# Standard exception you can throw to exit with a given status code.
|
11
|
-
# Generally, you should prefer
|
22
|
+
# Generally, you should prefer SH::ExitNow.exit_now! over using this
|
12
23
|
# directly, however you may wish to create a rich hierarchy of exceptions
|
13
24
|
# that extend from this in your app, so this is provided if you wish to
|
14
25
|
# do so.
|
@@ -102,10 +113,50 @@ module SH
|
|
102
113
|
extend self
|
103
114
|
attr_writer :default_sh_options
|
104
115
|
def default_sh_options
|
105
|
-
@default_sh_options||={log:
|
106
|
-
|
116
|
+
@default_sh_options||={log: true, capture: false, on_success: nil, on_failure: nil, expected:0, dryrun: false, escape: false,
|
117
|
+
log_level_execute_debug: :debug,
|
118
|
+
log_level_execute: :info, log_level_error: :error,
|
107
119
|
log_level_stderr: :error, log_level_stdout_success: :info,
|
108
|
-
log_level_stdout_fail: :warn}
|
120
|
+
log_level_stdout_fail: :warn, detach: false}
|
121
|
+
end
|
122
|
+
|
123
|
+
attr_writer :spawned
|
124
|
+
def spawned
|
125
|
+
@spawned||=[]
|
126
|
+
end
|
127
|
+
def wait_spawned
|
128
|
+
spawned.each {|c| Process.waitpid(c)}
|
129
|
+
end
|
130
|
+
|
131
|
+
# callback called by sh to select the exec mode
|
132
|
+
# mode: :system,:spawn,:exec,:capture
|
133
|
+
# opts: sudo, env
|
134
|
+
def shrun(*args,mode: :system, **opts)
|
135
|
+
env, args, spawn_opts=Run.process_command(*args, **opts)
|
136
|
+
# p env, args, spawn_opts
|
137
|
+
case mode
|
138
|
+
when :system
|
139
|
+
system(env,*args,spawn_opts)
|
140
|
+
when :spawn, :detach
|
141
|
+
pid=spawn(env,*args,spawn_opts)
|
142
|
+
if mode==:detach
|
143
|
+
Process.detach(pid)
|
144
|
+
else
|
145
|
+
spawned << pid
|
146
|
+
if block_given?
|
147
|
+
yield pid
|
148
|
+
Process.wait(pid)
|
149
|
+
else
|
150
|
+
pid
|
151
|
+
end
|
152
|
+
end
|
153
|
+
when :exec
|
154
|
+
exec(env,*args,spawn_opts)
|
155
|
+
when :capture
|
156
|
+
Run.run_command(env,*args,spawn_opts)
|
157
|
+
when :run
|
158
|
+
Run.run(env,*args,spawn_opts)
|
159
|
+
end
|
109
160
|
end
|
110
161
|
|
111
162
|
# Run a shell command, capturing and logging its output.
|
@@ -117,7 +168,7 @@ module SH
|
|
117
168
|
# name: pretty name of command
|
118
169
|
# on_success,on_failure: blocks to call on success/failure
|
119
170
|
# block:: if provided, will be called if the command exited nonzero. The block may take 0, 1, 2, or 3 arguments.
|
120
|
-
# The arguments provided are the standard output as a string, standard error as a string, and the processstatus as
|
171
|
+
# The arguments provided are the standard output as a string, standard error as a string, and the processstatus as SH::ProcessStatus
|
121
172
|
# You should be safe to pass in a lambda instead of a block, as long as your lambda doesn't take more than three arguments
|
122
173
|
#
|
123
174
|
# Example
|
@@ -130,41 +181,48 @@ module SH
|
|
130
181
|
# end
|
131
182
|
#
|
132
183
|
# Returns the exit status of the command. Note that if the command doesn't exist, this returns 127.
|
184
|
+
|
133
185
|
def sh(*command, **opts, &block)
|
134
186
|
defaults=default_sh_options
|
135
187
|
curopts=defaults.dup
|
136
188
|
defaults.keys.each do |k|
|
137
189
|
v=opts.delete(k)
|
138
|
-
curopts[k]=v
|
190
|
+
curopts[k]=v unless v.nil?
|
139
191
|
end
|
192
|
+
|
140
193
|
log=curopts[:log]
|
141
|
-
command=command.first if command.length==1 and command.first.kind_of?(Array)
|
142
|
-
command_name = curopts[:name] || command_name(command)
|
194
|
+
# command=command.first if command.length==1 and command.first.kind_of?(Array) #so that sh(["ls", "-a"]) works
|
195
|
+
command_name = curopts[:name] || command_name(command) #this keep the options
|
143
196
|
command=command.shelljoin if curopts[:escape]
|
144
|
-
|
197
|
+
if log
|
198
|
+
sh_logger.send(curopts[:log_level_execute], SimpleColor.color("Executing '#{command_name}'",:bold))
|
199
|
+
p_env, p_args, p_opts= Run.process_command(*command, **opts)
|
200
|
+
sh_logger.send(curopts[:log_level_execute_debug], SimpleColor.color("Debug execute: '#{[p_env, *p_args, p_opts]}'", :bold))
|
201
|
+
end
|
145
202
|
|
146
203
|
if !curopts[:dryrun]
|
147
|
-
if curopts[:capture]
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
204
|
+
if curopts[:capture] || curopts[:mode]==:capture
|
205
|
+
stdout,stderr,status = shrun(*command,**opts,mode: :capture)
|
206
|
+
elsif curopts[:detach] || curopts[:mode]==:spawn || curopts[:mode]==:detach
|
207
|
+
mode = curopts[:detach] ? :detach : curops[:mode]
|
208
|
+
_pid = shrun(*command,**opts, mode: mode)
|
209
|
+
status=0; stdout=nil; stderr=nil
|
210
|
+
elsif curopts[:mode]==:run
|
211
|
+
*res=shrun(*command,**opts,mode: :capture)
|
212
|
+
case res.length
|
213
|
+
when 2
|
214
|
+
stdout, status=res; stderr=nil
|
215
|
+
when 3
|
216
|
+
stdout, stderr, status=res
|
153
217
|
end
|
154
218
|
else
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
else
|
159
|
-
system(command.to_s,**opts)
|
160
|
-
end
|
161
|
-
status=$?
|
162
|
-
stdout=nil; stderr=nil
|
219
|
+
mode=curopts[:mode]||:system
|
220
|
+
shrun(*command,mode: mode, **opts)
|
221
|
+
status=$?; stdout=nil; stderr=nil
|
163
222
|
end
|
164
223
|
else
|
165
|
-
|
166
|
-
status=0
|
167
|
-
stdout=nil; stderr=nil
|
224
|
+
sh_logger.info command.to_s
|
225
|
+
status=0; stdout=nil; stderr=nil
|
168
226
|
end
|
169
227
|
process_status = ProcessStatus.new(status,curopts[:expected])
|
170
228
|
|
@@ -187,7 +245,7 @@ module SH
|
|
187
245
|
|
188
246
|
# Run a command, throwing an exception if the command exited nonzero.
|
189
247
|
# Otherwise, behaves exactly like #sh.
|
190
|
-
# Raises
|
248
|
+
# Raises SH::FailedCommandError if the command exited nonzero.
|
191
249
|
# Examples:
|
192
250
|
#
|
193
251
|
# sh!("rsync foo bar")
|
@@ -197,7 +255,7 @@ module SH
|
|
197
255
|
def sh!(*args,failure_msg: nil,**opts, &block)
|
198
256
|
on_failure=Proc.new do |*blockargs|
|
199
257
|
process_status=blockargs.last
|
200
|
-
raise
|
258
|
+
raise FailedCommandError.new(process_status.exitstatus,command_name(args),failure_msg: failure_msg)
|
201
259
|
end
|
202
260
|
sh(*args,**opts,on_failure: on_failure,&block)
|
203
261
|
end
|
@@ -212,18 +270,27 @@ module SH
|
|
212
270
|
@sh_logger = logger
|
213
271
|
end
|
214
272
|
|
273
|
+
#split commands on newlines and run sh on each line
|
274
|
+
def sh_commands(com, **opts)
|
275
|
+
com.each_line do |line|
|
276
|
+
sh(line.chomp,**opts)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
215
280
|
private
|
216
281
|
def command_name(command)
|
217
282
|
if command.size == 1
|
218
|
-
|
283
|
+
command.first.to_s
|
219
284
|
else
|
220
|
-
|
285
|
+
#command.to_s
|
286
|
+
#command.map {|i| i.to_s}.to_s
|
287
|
+
command.shelljoin
|
221
288
|
end
|
222
289
|
end
|
223
290
|
|
224
291
|
def sh_logger
|
225
292
|
@sh_logger ||= begin
|
226
|
-
raise StandardError, "No logger set! Please include
|
293
|
+
raise StandardError, "No logger set! Please include SH::CLILogging
|
227
294
|
ng or provide your own via #change_sh_logger." unless self.respond_to?(:logger)
|
228
295
|
self.logger
|
229
296
|
end
|
@@ -231,8 +298,8 @@ ng or provide your own via #change_sh_logger." unless self.respond_to?(:logger)
|
|
231
298
|
|
232
299
|
end
|
233
300
|
|
234
|
-
#SH::ShLog.sh is like SH::Sh.sh
|
235
|
-
#
|
301
|
+
#SH::ShLog.sh is by default like SH::Sh.sh.
|
302
|
+
# It is easy to change it to be more verbose though
|
236
303
|
module ShLog
|
237
304
|
include Sh
|
238
305
|
extend self
|
@@ -240,5 +307,87 @@ ng or provide your own via #change_sh_logger." unless self.respond_to?(:logger)
|
|
240
307
|
@default_sh_options[:log]=true
|
241
308
|
@default_sh_options[:log_level_execute]=:info
|
242
309
|
end
|
310
|
+
|
311
|
+
# Do not log execution
|
312
|
+
module ShQuiet
|
313
|
+
include Sh
|
314
|
+
extend self
|
315
|
+
@default_sh_options=default_sh_options
|
316
|
+
@default_sh_options[:log]=true
|
317
|
+
@default_sh_options[:log_level_execute]=:debug
|
318
|
+
end
|
319
|
+
|
320
|
+
# Completely silent
|
321
|
+
module ShSilent
|
322
|
+
include Sh
|
323
|
+
extend self
|
324
|
+
@default_sh_options=default_sh_options
|
325
|
+
@default_sh_options[:log]=false
|
326
|
+
end
|
327
|
+
|
328
|
+
module ShDryRun
|
329
|
+
include Sh
|
330
|
+
extend self
|
331
|
+
@default_sh_options=default_sh_options
|
332
|
+
@default_sh_options[:log]=true
|
333
|
+
@default_sh_options[:log_level_execute]=:info
|
334
|
+
@default_sh_options[:dryrun]=true
|
335
|
+
end
|
336
|
+
|
337
|
+
# this modules deal with default options, paths and arg handling for
|
338
|
+
# different programs, while letting the user control
|
339
|
+
# Exemples:
|
340
|
+
# ShConfig.launch(:ls, "/", config: {ls: {default_opts: ["-l"]}})
|
341
|
+
# => ["ls", "-l", "/", {}]
|
342
|
+
# ShConfig.launch(:ls, "/", config: {ls: {default_opts: ["-l"]}}, method: :sh)
|
343
|
+
# ShConfig.launch(:ls, "/", config: {ls: {default_opts: ["-l"]}}) { |*args| SH.sh(*args) }
|
344
|
+
# ShConfig.launch(:ls, "/", config: {ls: {wrap: ->(cmd,*args, &b) { b.call(cmd, '-l', *args) } }}, method: :sh)
|
345
|
+
|
346
|
+
module ShConfig
|
347
|
+
extend self
|
348
|
+
def launch(*args, opts: [], cmd_prepend: [], cmd_postpone: [], config: self.sh_config, default_opts: true, method: nil, **keywords, &b)
|
349
|
+
if args.length == 1 and (arg=args.first).is_a?(String)
|
350
|
+
args=arg.shellsplit
|
351
|
+
args[0]=args[0][1..-1].to_sym if args[0][0]==':'
|
352
|
+
end
|
353
|
+
opts=opts.shellsplit if opts.is_a?(String)
|
354
|
+
default_opts=default_opts.shellsplit if default_opts.is_a?(String)
|
355
|
+
cmd_prepend=cmd_prepend.shellsplit if cmd_prepend.is_a?(String)
|
356
|
+
cmd_postpone=cmd_postpone.shellsplit if cmd_postpone.is_a?(String)
|
357
|
+
dopts = default_opts.is_a?(Array) ? default_opts : []
|
358
|
+
cmd, *args=args
|
359
|
+
if cmd.is_a?(Symbol)
|
360
|
+
if config.key?(cmd)
|
361
|
+
c=config[cmd]
|
362
|
+
cmd=c[:bin] || cmd.to_s
|
363
|
+
dopts += (Array(c[:default_opts])||[]) if default_opts
|
364
|
+
wrap=c[:wrap]
|
365
|
+
else
|
366
|
+
cmd=cmd.to_s
|
367
|
+
end
|
368
|
+
end
|
369
|
+
cargs=Array(cmd_prepend) + [cmd] + dopts + Array(opts) + args + Array(cmd_postpone)
|
370
|
+
if !b
|
371
|
+
if method
|
372
|
+
b=lambda do |*args|
|
373
|
+
SH.public_send(method, *args)
|
374
|
+
end
|
375
|
+
else
|
376
|
+
b=lambda do |*args|
|
377
|
+
return *args
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
if wrap
|
382
|
+
wrap.call(*cargs, **keywords, &b)
|
383
|
+
else
|
384
|
+
b.call(*cargs, **keywords)
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
def sh_config
|
389
|
+
{}
|
390
|
+
end
|
391
|
+
end
|
243
392
|
# }}}
|
244
393
|
end
|