shell_helpers 0.1.0 → 0.6.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.
@@ -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