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.
@@ -2,35 +2,94 @@
2
2
  require 'open3'
3
3
  require 'shellwords'
4
4
 
5
- module SH
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
- def run_output(*command)
14
- stdout,_stderr,_status = run_command(*command)
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 run_status(*command)
19
- _stdout,_stderr,status = run_command(*command)
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
- #a simple wrapper for %x//
24
- def run_simple(*command, quiet: false, fail_mode: :error, chomp: false)
25
- if command.length > 1
26
- launch=command.shelljoin
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=command.first
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
- out = %x/#{launch}/
33
- status=$?.success?
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
- out.chomp! if chomp
44
- return out, status
45
- end
46
-
47
- #capture stdout and status, silence stderr
48
- def run(*args)
49
- begin
50
- if Open3.respond_to?(:capture3) then
51
- out, _error, status=Open3.capture3(*args)
52
- return out, status.success?
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 = `#{args.shelljoin} 2>/dev/null`
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
- DR::Run.run_command(*args)
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 DR::Run.run(*args)
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? Fixnum
215
+ if status.kind_of? Integer
130
216
  status
131
217
  elsif status
132
218
  0
@@ -1,14 +1,25 @@
1
1
  # vim: foldmethod=marker
2
- #from methadone (error.rb, exit_now.rb, process_status.rb, run.rb; last
3
- #import v1.3.1-2-g9be3b5a)
4
- require_relative 'logger'
5
- require_relative 'run'
6
- require 'simplecolor'
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
- module SH
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 DR::ExitNow.exit_now! over using this
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: false, capture: false, on_success: nil, on_failure: nil, expected:0, dryrun: false, escape: false,
106
- log_level_execute: :debug, log_level_error: :error,
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 DR::ProcessStatus
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 if 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
- sh_logger.send(curopts[:log_level_execute], SimpleColor.color("Executing '#{command_name}'",:bold)) if log
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
- case command
149
- when Array
150
- stdout,stderr,status = DR::Run.run_command(*command,**opts)
151
- else
152
- stdout,stderr,status = DR::Run.run_command(command.to_s,**opts)
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
- case command
156
- when Array
157
- system(*command,**opts)
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
- puts command.to_s
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 DR::FailedCommandError if the command exited nonzero.
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 DR::FailedCommandError.new(process_status.exitstatus,command_name(args),failure_msg: failure_msg)
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
- return command.first.to_s
283
+ command.first.to_s
219
284
  else
220
- return command.to_s
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 DR::CLILogging
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 but with login enabled even when
235
- #command succeed
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