shell_helpers 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,11 +7,7 @@ module ShellHelpers
7
7
  extend(self)
8
8
  RunError=Class.new(StandardError)
9
9
 
10
- def sudo_args(sudoarg)
11
- return "" unless sudoarg
12
- return sudoarg.shellsplit if sudoarg.is_a?(String)
13
- ["sudo"]
14
- end
10
+ ## Trivial wrappers around Open3
15
11
 
16
12
  #the run_* commands here capture all the output
17
13
  def run_command(*command)
@@ -44,6 +40,19 @@ module ShellHelpers
44
40
  r
45
41
  end
46
42
 
43
+ ## Launching a command
44
+ #handle sudo arguments
45
+
46
+ def sudo_args(sudoarg)
47
+ if sudoarg.respond_to?(:sudo_loop)
48
+ sudoarg.sudo_loop
49
+ end
50
+ return [] unless sudoarg
51
+ return sudoarg.shellsplit if sudoarg.is_a?(String)
52
+ ["sudo"]
53
+ end
54
+
55
+ # get the args, environment and spawning options
47
56
  def process_command(*args, **opts)
48
57
  spawn_opts={}
49
58
  if args.last.kind_of?(Hash)
@@ -56,7 +65,7 @@ module ShellHelpers
56
65
  env,*args=*args
57
66
  end
58
67
  env.merge!(opts.delete(:env)||{})
59
- args=args.map {|arg| arg.to_s} if args.length > 1
68
+ args=args.map.with_index {|arg, i| i == 0 && arg.is_a?(Array) ? arg : arg.to_s} if args.length > 1
60
69
  spawn_opts.merge!(opts)
61
70
  if sudo
62
71
  if args.length > 1
@@ -68,22 +77,32 @@ module ShellHelpers
68
77
  return env, args, spawn_opts
69
78
  end
70
79
 
80
+ ## Run a command
71
81
  #by default capture stdout and status
82
+ # callbacks: yield status.success?, out, err, status if block_given?
83
+ # if status.success? => on_success.call(status, out, err)
84
+ # else error_mode.call(status, out, error)
85
+ # rescue ... => fail_mode.call(e)
86
+ # return status, out, error
72
87
  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)
88
+ env, args, spawn_opts=Run.process_command(*args, sudo: sudo, **opts)
74
89
 
75
- if args.length > 1
76
- launch=args.shelljoin
90
+ if args.is_a?(Array)
91
+ if args.length > 1
92
+ launch=args.shelljoin
93
+ else
94
+ launch=args.first #assume it has already been escaped
95
+ end
77
96
  else
78
- launch=args.first #assume it has already been escaped
97
+ launch=args.to_s
79
98
  end
80
99
  launch+=" 2>/dev/null" if error==:quiet or quiet
81
100
  launch+=" >/dev/null" if output==:quiet
82
- out=error=nil
101
+ out=err=nil
83
102
 
84
103
  begin
85
104
  if error==:capture
86
- out, error, status=Open3.capture3(env, launch, spawn_opts)
105
+ out, err, status=Open3.capture3(env, launch, spawn_opts)
87
106
  elsif output==:capture
88
107
  out, status=Open3.capture2(env, launch, spawn_opts)
89
108
  else
@@ -104,8 +123,9 @@ module ShellHelpers
104
123
  end
105
124
  end
106
125
  status=ProcessStatus.new(status, expected) if expected
107
- yield status.success?, out, err, status if block_given?
108
- if status.success?
126
+ status_success=status.success? if status.respond_to? :success?
127
+ yield status_success, out, err, status if block_given?
128
+ if status_success
109
129
  # this block is called in case of success
110
130
  on_success.call(status, out, err) if on_success.is_a?(Proc)
111
131
  else # the command failed
@@ -117,7 +137,7 @@ module ShellHelpers
117
137
  when :error
118
138
  raise RunError.new("Error running command '#{launch}': #{status}")
119
139
  when Proc
120
- error_mode.call(status, out, error)
140
+ error_mode.call(status, out, err)
121
141
  end
122
142
  end
123
143
  if chomp and out
@@ -130,18 +150,27 @@ module ShellHelpers
130
150
  end
131
151
  end
132
152
 
133
- return out, error, status if error
134
- return out, status
153
+ # return out, error, status if error
154
+ return status, out, err
135
155
  end
136
156
 
137
157
  #a simple wrapper for %x//
158
+ #return the output; the block is called in case of an error
159
+ #eg SH.run_simple("echo foo; false") do p "error" end
160
+ #=> returns "foo\n" and prints error
138
161
  def run_simple(*command, **opts, &b)
139
- # here the block is called in case of failure
162
+ # here the block is called in case of error
140
163
  opts[:error_mode]=b if b
141
- out, *_rest = run(*command, **opts)
164
+ _status, out, _error = run(*command, **opts)
142
165
  return out
143
166
  end
144
167
 
168
+ # like run, but only returns status.success?
169
+ def run_success(*command, **opts, &b)
170
+ status, _out, _error = run(*command, **opts, &b)
171
+ status.success?
172
+ end
173
+
145
174
  #same as Run, but if we get interrupted once, we don't want to launch any more commands
146
175
  module Interrupt #{{{
147
176
  extend(self)
@@ -57,6 +57,42 @@ module ShellHelpers
57
57
  end
58
58
  end
59
59
  # }}}
60
+
61
+ # SudoLoop {{{
62
+ # extend SudoLoop.configure to run a sudo loop
63
+ # SH.sh("ls /", sudo: "sudo".extend(SH::SudoLoop.configure(**opts))
64
+ module SudoLoop
65
+ extend self
66
+ def configure(**opts)
67
+ return Module.new do |m|
68
+ m.define_method(:sudo_loop) do
69
+ SudoLoop.instance_method(:run_sudo_loop).bind(self).call(**opts)
70
+ end
71
+ m.define_method(:stop_sudo_loop) do
72
+ SudoLoop.instance_method(:stop_sudo_loop).bind(self).call
73
+ end
74
+ end
75
+ end
76
+
77
+ def run_sudo_loop(command: "sudo -v", interval: 30, init_command: command)
78
+ if @sudo_loop_thread.nil? or !@sudo_loop_thread.alive?
79
+ Sh.sh_or_proc(init_command, log: false)
80
+ #require 'pry'; binding.pry
81
+ @sudo_loop_thread = Thread.new do
82
+ loop do
83
+ Sh.sh_or_proc(command, log: false)
84
+ sleep(interval)
85
+ end
86
+ end
87
+ @sudo_loop_thread.run
88
+ end
89
+ end
90
+
91
+ def stop_sudo_loop
92
+ @sudo_loop_thread&.kill
93
+ end
94
+ end #}}}
95
+
60
96
  # Sh {{{
61
97
  # Module with various helper methods for executing external commands.
62
98
  # In most cases, you can use #sh to run commands and have decent logging
@@ -82,10 +118,10 @@ module ShellHelpers
82
118
  # # This block isn't called, since the command failed
83
119
  # end
84
120
  #
85
- # sh 'ls -l /tmp/' do |stdout|
121
+ # sh 'ls -l /tmp/', capture: true do |status, stdout|
86
122
  # # stdout contains the output of the command
87
123
  # end
88
- # sh 'ls -l /tmp/ /non_existent_dir' do |stdout,stderr|
124
+ # sh 'ls -l /tmp/ /non_existent_dir', capture: true do |status, stdout,stderr|
89
125
  # # stdout contains the output of the command,
90
126
  # # stderr contains the standard error output.
91
127
  # end
@@ -108,16 +144,18 @@ module ShellHelpers
108
144
  end
109
145
  # }}}
110
146
 
147
+ ShError=Class.new(StandardError)
111
148
  module Sh
112
149
  include CLILogging
113
150
  extend self
114
151
  attr_writer :default_sh_options
115
152
  def default_sh_options
116
- @default_sh_options||={log: true, capture: false, on_success: nil, on_failure: nil, expected:0, dryrun: false, escape: false,
153
+ @default_sh_options||={log: true, capture: false, on_success: nil, on_error: nil, expected:0, dryrun: false, escape: false,
117
154
  log_level_execute_debug: :debug,
118
155
  log_level_execute: :info, log_level_error: :error,
119
156
  log_level_stderr: :error, log_level_stdout_success: :info,
120
- log_level_stdout_fail: :warn, detach: false}
157
+ log_level_stdout_fail: :warn, detach: false,
158
+ mode: :system}
121
159
  end
122
160
 
123
161
  attr_writer :spawned
@@ -131,14 +169,14 @@ module ShellHelpers
131
169
  # callback called by sh to select the exec mode
132
170
  # mode: :system,:spawn,:exec,:capture
133
171
  # opts: sudo, env
134
- def shrun(*args,mode: :system, **opts)
172
+ def shrun(*args,mode: :system, block: nil, **opts)
135
173
  env, args, spawn_opts=Run.process_command(*args, **opts)
136
174
  # p env, args, spawn_opts
137
175
  case mode
138
176
  when :system
139
177
  system(env,*args,spawn_opts)
140
178
  when :spawn, :detach
141
- pid=spawn(env,*args,spawn_opts)
179
+ pid=spawn(env,*args,spawn_opts, &block)
142
180
  if mode==:detach
143
181
  Process.detach(pid)
144
182
  else
@@ -151,11 +189,13 @@ module ShellHelpers
151
189
  end
152
190
  end
153
191
  when :exec
154
- exec(env,*args,spawn_opts)
192
+ exec(env,*args,spawn_opts, &block)
155
193
  when :capture
156
- Run.run_command(env,*args,spawn_opts)
194
+ Run.run_command(env,*args,spawn_opts, &block)
157
195
  when :run
158
- Run.run(env,*args,spawn_opts)
196
+ Run.run(env,*args,spawn_opts, &block)
197
+ else
198
+ raise ShError.new("In shrun, mode #{mode} not understood")
159
199
  end
160
200
  end
161
201
 
@@ -166,7 +206,7 @@ module ShellHelpers
166
206
  # error output is logged at WARN.
167
207
  # +:expected+:: an Int or Array of Int representing error codes, <b>in addition to 0</b>, that are expected and therefore constitute success. Useful for commands that don't use exit codes the way you'd like
168
208
  # name: pretty name of command
169
- # on_success,on_failure: blocks to call on success/failure
209
+ # on_success,on_error: blocks to call on success/failure
170
210
  # block:: if provided, will be called if the command exited nonzero. The block may take 0, 1, 2, or 3 arguments.
171
211
  # The arguments provided are the standard output as a string, standard error as a string, and the processstatus as SH::ProcessStatus
172
212
  # 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
@@ -180,9 +220,13 @@ module ShellHelpers
180
220
  # # ...
181
221
  # end
182
222
  #
183
- # Returns the exit status of the command. Note that if the command doesn't exist, this returns 127.
223
+ # Returns the exit status of the command (Note that if the command doesn't exist, this returns 127.), stdout, stderr and the full status of the command
184
224
 
185
- def sh(*command, **opts, &block)
225
+ # callbacks: on_success, on_error
226
+ # yield process_status.success?,stdout,stderr,process_status if block_given?
227
+ # returns success; except in capture mode where it returns success,
228
+ # stdout, stderr, process_status
229
+ def sh(*command, argv0: nil, **opts)
186
230
  defaults=default_sh_options
187
231
  curopts=defaults.dup
188
232
  defaults.keys.each do |k|
@@ -191,13 +235,19 @@ module ShellHelpers
191
235
  end
192
236
 
193
237
  log=curopts[:log]
194
- # command=command.first if command.length==1 and command.first.kind_of?(Array) #so that sh(["ls", "-a"]) works
238
+ command=[[command.first, argv0], *command[1..-1]] if argv0 and command.length > 1 and !curopts[:escape]
239
+
240
+ if command.length==1 and command.first.kind_of?(Array) #so that sh(["ls", "-a"]) works
241
+ command=command.first
242
+ command=[[command.first, argv0], *command[1..-1]] if argv0 and !curopts[:escape]
243
+ end
195
244
  command_name = curopts[:name] || command_name(command) #this keep the options
245
+ # this should not be needed
196
246
  command=command.shelljoin if curopts[:escape]
197
247
  if log
198
248
  sh_logger.send(curopts[:log_level_execute], SimpleColor.color("Executing '#{command_name}'",:bold))
199
249
  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))
250
+ sh_logger.send(curopts[:log_level_execute_debug], SimpleColor.color("Debug execute: #{[p_env, *p_args, p_opts]}", :bold))
201
251
  end
202
252
 
203
253
  if !curopts[:dryrun]
@@ -208,15 +258,9 @@ module ShellHelpers
208
258
  _pid = shrun(*command,**opts, mode: mode)
209
259
  status=0; stdout=nil; stderr=nil
210
260
  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
217
- end
261
+ status, stdout, stderr=shrun(*command,mode: curopts[:mode], **opts)
218
262
  else
219
- mode=curopts[:mode]||:system
263
+ mode = curopts[:mode] || :system
220
264
  shrun(*command,mode: mode, **opts)
221
265
  status=$?; stdout=nil; stderr=nil
222
266
  end
@@ -230,17 +274,26 @@ module ShellHelpers
230
274
  if process_status.success?
231
275
  sh_logger.send(curopts[:log_level_stdout_success], SimpleColor.color("stdout output of '#{command_name}':\n",:bold,:green)+stdout) unless stdout.nil? or stdout.strip.length == 0 or !log
232
276
  curopts[:on_success].call(stdout,stderr,process_status) unless curopts[:on_success].nil?
233
- block.call(stdout,stderr,process_status) unless block.nil?
277
+ # block.call(stdout,stderr,process_status) unless block.nil?
234
278
  else
235
279
  sh_logger.send(curopts[:log_level_stdout_fail], SimpleColor.color("stdout output of '#{command_name}':\n",:bold,:yellow)+stdout) unless stdout.nil? or stdout.strip.length == 0 or !log
236
280
  sh_logger.send(curopts[:log_level_error], SimpleColor.color("Error running '#{command_name}': #{process_status.status}",:red,:bold)) if log
237
- curopts[:on_failure].call(stdout,stderr,process_status) unless curopts[:on_failure].nil?
281
+ curopts[:on_error].call(stdout,stderr,process_status) unless curopts[:on_error].nil?
282
+ end
283
+ yield process_status.success?,stdout,stderr,process_status if block_given?
284
+ if curopts[:capture] || curopts[:mode]==:capture || curopts[:mode]==:run
285
+ return process_status.success?,stdout,stderr,process_status
286
+ else
287
+ return process_status.success?
238
288
  end
239
- return process_status.success?,stdout,stderr,process_status
240
289
 
241
290
  rescue SystemCallError => ex
242
291
  sh_logger.send(curopts[:log_level_error], SimpleColor.color("Error running '#{command_name}': #{ex.message}",:red,:bold)) if log
243
- return 127
292
+ if block_given?
293
+ yield 127, nil, nil, nil
294
+ else
295
+ return 127, nil, nil, nil
296
+ end
244
297
  end
245
298
 
246
299
  # Run a command, throwing an exception if the command exited nonzero.
@@ -253,11 +306,11 @@ module ShellHelpers
253
306
  # sh!("rsync foo bar", :failure_msg => "Couldn't rsync, check log for details")
254
307
  # # => if command fails, app exits and user sees: "error: Couldn't rsync, check log for details
255
308
  def sh!(*args,failure_msg: nil,**opts, &block)
256
- on_failure=Proc.new do |*blockargs|
309
+ on_error=Proc.new do |*blockargs|
257
310
  process_status=blockargs.last
258
311
  raise FailedCommandError.new(process_status.exitstatus,command_name(args),failure_msg: failure_msg)
259
312
  end
260
- sh(*args,**opts,on_failure: on_failure,&block)
313
+ sh(*args,**opts,on_error: on_error,&block)
261
314
  end
262
315
 
263
316
  # Override the default logger (which is the one provided by CLILogging).
@@ -277,6 +330,20 @@ module ShellHelpers
277
330
  end
278
331
  end
279
332
 
333
+ # returns only the success or failure
334
+ def sh_or_proc(cmd, *args, **opts, &b)
335
+ case cmd
336
+ when Proc
337
+ cmd.call(*args, **opts, &b)
338
+ when Array
339
+ suc, _r=SH.sh(*cmd, *args, **opts, &b)
340
+ suc
341
+ when String
342
+ suc, _r=SH.sh(cmd + " #{args.shelljoin}", **opts, &b)
343
+ suc
344
+ end
345
+ end
346
+
280
347
  private
281
348
  def command_name(command)
282
349
  if command.size == 1
@@ -369,8 +436,8 @@ ng or provide your own via #change_sh_logger." unless self.respond_to?(:logger)
369
436
  cargs=Array(cmd_prepend) + [cmd] + dopts + Array(opts) + args + Array(cmd_postpone)
370
437
  if !b
371
438
  if method
372
- b=lambda do |*args|
373
- SH.public_send(method, *args)
439
+ b=lambda do |*args, **kw|
440
+ SH.public_send(method, *args, **kw)
374
441
  end
375
442
  else
376
443
  b=lambda do |*args|
@@ -10,6 +10,8 @@ module ShellHelpers
10
10
  SysError=Class.new(StandardError)
11
11
 
12
12
  # wrap 'stat'
13
+ # @example
14
+ # SH.stat_file("mine") => {:access=>"700", :blocknumber=>0,...}
13
15
  def stat_file(file)
14
16
  require 'time'
15
17
  opts=%w(a b B f F g G h i m n N o s u U w x y z)
@@ -37,9 +39,14 @@ module ShellHelpers
37
39
  r[:statustime] = begin Time.parse(stats[19]) rescue nil end
38
40
  r
39
41
  end
42
+
40
43
  # wrap stat --file-system
44
+ # @example
45
+ # SH.stat_filesystem("mine") => {:userfreeblocks=>..., fstype=>"btrfs"}
41
46
  def stat_filesystem(file, up: true)
42
- if up
47
+ if up #output the fs info of the first ascending path that exist
48
+ # usefull to get infos of the filesystem of a file we want to create
49
+ # but does not yet exist
43
50
  file=Pathname.new(file)
44
51
  file.ascend.each do |f|
45
52
  return stat_filesystem(f, up: false) if f.exist?
@@ -47,7 +54,7 @@ module ShellHelpers
47
54
  end
48
55
  opts=%w(a b c d f i l n s S T)
49
56
  stats=Run.run_simple("stat --file-system --format='#{opts.map{|o| "%#{o}\n"}.join}' #{file.shellescape}", chomp: :lines)
50
- stats=stats.each_line.map {|l| l.chomp}
57
+ #stats=stats.each_line.map {|l| l.chomp}
51
58
  r={}
52
59
  r[:userfreeblocks]=stats[0].to_i
53
60
  r[:totalblocks]=stats[1].to_i
@@ -93,6 +100,8 @@ module ShellHelpers
93
100
  # :type=>"swap",
94
101
  # :partlabel=>"swap",
95
102
  # :partuuid=>"f4eef373-0803-4701-bd47-b968c44065a6"}
103
+ # @exemple
104
+ # SH.blkid => {"/dev/sda1"=> {:devname=>"/dev/sda1", :sec_type=>"msdos", :label_fatboot=>"boot", :label=>"boot", :uuid=>"D906-BEB0", :partlabel=>"boot", :partuuid=>"...",:fstype=>"vfat"}, ...}
96
105
  def blkid(*args, sudo: false)
97
106
  # get devname, (part)label/uuid, fstype
98
107
  fsoptions=Run.run_simple("blkid -o export #{args.shelljoin}", fail_mode: :empty, chomp: true, sudo: sudo)
@@ -100,6 +109,8 @@ module ShellHelpers
100
109
  end
101
110
 
102
111
  # use lsblk to get infos about devices
112
+ # @exemple
113
+ # SH.lsblk => {"/dev/sda"=>{:devname=>"/dev/sda", :devtype=>"disk"}, "/dev/sda1"=> {:devname=>"/dev/sda1", :label=>"boot", :uuid=>"D906-BEB0", :partlabel=>"boot", :partuuid=>"00000000-0000-0000-0000-000000000000", :parttype=>"c12a7328-f81f-11d2-ba4b-00a0c93ec93b", :devtype=>"part", :fstype=>"vfat"},...}
103
114
  def lsblk(sudo: false)
104
115
  # get devname, mountpoint, (part)label/uuid, (part/dev/fs)type
105
116
  fsoptions=Run.run_simple("lsblk -l -J -o NAME,MOUNTPOINT,LABEL,UUID,PARTLABEL,PARTUUID,PARTTYPE,TYPE,FSTYPE", fail_mode: :empty, chomp: true, sudo: sudo)
@@ -123,6 +134,8 @@ module ShellHelpers
123
134
  end
124
135
 
125
136
  # use findmnt to get infos about mount points
137
+ # @exemple
138
+ # SH.findmnt => {"/dev/bcache0[/slash]"=> {:mountpoint=>"/", :devname=>"/dev/bcache0[/slash]", :fstype=>"btrfs", :mountoptions=> ["rw", "noatime", "compress=lzo", "ssd", "space_cache", "autodefrag", "subvolid=257", "subvol=/slash"], :label=>"rootleaf", :uuid=>"1db5b600-df3e-4d1e-9eef-6a0a7fda491d", :partlabel=>"", :partuuid=>"", :fsroot=>"/slash"}, ...}
126
139
  def findmnt(sudo: false)
127
140
  # get devname, mountpoint, mountoptions, (part)label/uuid, fsroot
128
141
  # only looks at mounted devices (but in comparison to lsblk also show
@@ -139,6 +152,8 @@ module ShellHelpers
139
152
  fs
140
153
  end
141
154
 
155
+ # we default to lsblk
156
+ # findmnt adds the subvolumes, the fsroot and the mountoptions
142
157
  def fs_infos(mode: :devices)
143
158
  return findmnt if mode == :mount
144
159
  return lsblk.merge(findmnt) if mode == :all
@@ -151,6 +166,9 @@ module ShellHelpers
151
166
  end
152
167
 
153
168
  # find devices matching props
169
+ # @exemple
170
+ # SH.find_devices({label: "boot"})
171
+ # => [{:devname=>"/dev/sda1", :label=>"boot", :uuid=>"D906-BEB0", :partlabel=>"boot", :partuuid=>"...", :parttype=>"c12a7328-f81f-11d2-ba4b-00a0c93ec93b", :devtype=>"part", :fstype=>"vfat"}]
154
172
  def find_devices(props, method: :all)
155
173
  props=props.clone
156
174
  return [{devname: props[:devname]}] unless props[:devname].nil?
@@ -161,6 +179,10 @@ module ShellHelpers
161
179
  end
162
180
 
163
181
  if method==:blkid
182
+ # try with UUID, then LABEL, then PARTUUID
183
+ # as soon as we have a non empty label, we return the result of
184
+ # blkid on it.
185
+ #
164
186
  # Warning, since 'blkid' can only test one label, we cannot check
165
187
  # that all parameters are valid
166
188
  # search from most discriminant to less discriminant
@@ -174,21 +196,27 @@ module ShellHelpers
174
196
  if props[:parttype]
175
197
  find_devices(props, method: :all)
176
198
  end
177
- else
199
+ else #method=:all
178
200
  fs=fs_infos
179
- # here we check all parameters
201
+ # here we check all parameters (ie all defined labels are correct)
180
202
  # however, if none are defined, this return true, so we check that at least one is defined
181
203
  return [] unless %i(uuid label partuuid partlabel parttype).any? {|k| props[k]}
182
204
  return fs.keys.select do |k|
183
205
  fsprops=fs[k]
184
- # the fsinfos should have one of this parameters defined
185
- next false unless %i(uuid label partuuid partlabel parttype).any? {|k| fsprops[k]}
186
206
  next false if (disk=props[:disk]) && !fsprops[:devname].start_with?(disk.to_s)
187
- %i(uuid label partuuid partlabel parttype).all? do |key|
207
+ # all defined labels should match
208
+ next false unless %i(uuid label partuuid partlabel parttype).all? do |key|
188
209
  ptype=props[key]
189
210
  ptype=partition_type(ptype) if key==:parttype and ptype.is_a?(Symbol)
190
211
  !ptype or !fsprops[key] or ptype==fsprops[key]
191
212
  end
213
+ # their should at least be one matching label
214
+ next false unless %i(uuid label partuuid partlabel parttype).select do |key|
215
+ ptype=props[key]
216
+ ptype=partition_type(ptype) if key==:parttype and ptype.is_a?(Symbol)
217
+ ptype and fsprops[key] and ptype==fsprops[key]
218
+ end.length > 0
219
+ true
192
220
  end.map {|k| fs[k]}
193
221
  end
194
222
  return []
@@ -204,6 +232,19 @@ module ShellHelpers
204
232
  return devs.first&.fetch(:devname)
205
233
  end
206
234
 
235
+ # Mount devices on paths
236
+ # @argument
237
+ # - paths: Array (or Hash) of path
238
+ # => path: {mountpoint: "/mnt", mountoptions: [], fstype: "ext4",
239
+ # subvol: ..., device_info}
240
+ # where device_info is used to find the device via find_device
241
+ # so can be set via :devname, or uuid label partuuid partlabel parttype
242
+ # @opts
243
+ # - sort: sort the mountpoints
244
+ # - abort_on_error: fail if a mount failt
245
+ # - mkpath: mkpath the mountpoints
246
+ # @return
247
+ # the paths and a lambda to unmount
207
248
  def mount(paths, mkpath: true, abort_on_error: true, sort: true)
208
249
  paths=paths.values if paths.is_a?(Hash)
209
250
  paths=paths.select {|p| p[:mountpoint]}
@@ -244,6 +285,9 @@ module ShellHelpers
244
285
  end
245
286
  end
246
287
 
288
+ # by default give symbol => guid
289
+ # can also give symbol => hexa (mode: :hexa)
290
+ # or hexa/guid => symbol (mode: :symbol)
247
291
  def partition_type(type, mode: :guid)
248
292
  if mode==:symbol
249
293
  %i(boot swap home x86_root x86-64_root arm64_root arm32_root linux).each do |symb|
@@ -272,6 +316,8 @@ module ShellHelpers
272
316
  end
273
317
  end
274
318
 
319
+ # @exemple
320
+ # SH.partition_infos("/dev/sda", sudo: true) => [{:partlabel=>"boot", :partattributes=>"0000000000000004", :partuuid=>"00000000-0000-0000-0000-000000000000", :parttype=>"c12a7328-f81f-11d2-ba4b-00a0c93ec93b"}, {:partlabel=>"swap", :partattributes=>"0000000000000000", :partuuid=>"f4eef373-0803-4701-bd47-b968c44065a6", :parttype=>"0fc63daf-8483-4772-8e79-3d69d8477de4"}, {:partlabel=>"slash", :partattributes=>"0000000000000000", :partuuid=>"31b4cd66-39ab-4c5b-a229-d5d2010d53dd", :parttype=>"0fc63daf-8483-4772-8e79-3d69d8477de4"}]
275
321
  def partition_infos(device, sudo: false)
276
322
  parts = Run.run_simple("partx -o NR --show #{device.shellescape}", sudo: sudo) { return nil }
277
323
  infos=[]
@@ -295,7 +341,16 @@ module ShellHelpers
295
341
  infos
296
342
  end
297
343
 
298
- #options: check => check that no partitions exist first
344
+ #@argument
345
+ # A list of partitions
346
+ # Note that here the device has to be specified with :disk; looking at
347
+ # PARTLABEL or things like that don't make sense if the partitions
348
+ # don't exist
349
+ #@options:
350
+ # - check => check that no partitions exist first
351
+ # - partprobe: run partprobe if we made partitions
352
+ # @exemple
353
+ # SH.make_partitions( {:boot=> {:parttype=>:boot, :partlength=>"+100M", :fstype=>"vfat", :rel_mountpoint=>"boot", :mountoptions=>["fmask=133"]}, :slash=> {:parttype=>:"x86-64_root", :fstype=>"ext4", :rel_mountpoint=>"."}, ...})
299
354
  def make_partitions(partitions, check: true, partprobe: true)
300
355
  partitions=partitions.values if partitions.is_a?(Hash)
301
356
  done=[]
@@ -351,6 +406,9 @@ module ShellHelpers
351
406
  Sh.sh("wipefs -a #{disk.shellescape}", sudo: true)
352
407
  end
353
408
 
409
+ # make filesystems
410
+ # takes a list of fs infos (where the device is specified like before,
411
+ # via devname, label, ...)
354
412
  def make_fs(fs, check: true)
355
413
  fs=fs.values if fs.is_a?(Hash)
356
414
  fs.each do |partfs|
@@ -378,6 +436,7 @@ module ShellHelpers
378
436
  end
379
437
  end
380
438
 
439
+ # use fallocate to make a raw image
381
440
  def make_raw_image(name, size="1G")
382
441
  raw=Pathname.new(name)
383
442
  raw.touch
@@ -387,6 +446,7 @@ module ShellHelpers
387
446
  raw
388
447
  end
389
448
 
449
+ # makes a btrfs subvolume
390
450
  def make_btrfs_subvolume(dir, check: true)
391
451
  if check and dir.directory?
392
452
  raise SysError("Subvolume already exists at #{dir}") if check==:raise
@@ -396,6 +456,8 @@ module ShellHelpers
396
456
  dir
397
457
  end
398
458
  end
459
+
460
+ # try to make a subvolume, else fallsback to a dir
399
461
  def make_dir_or_subvolume(dir)
400
462
  dir=Pathname.new(dir)
401
463
  return :directory if dir.directory?
@@ -409,6 +471,7 @@ module ShellHelpers
409
471
  end
410
472
  end
411
473
 
474
+ # runs losetup, and returns the created disk, and a lambda to close
412
475
  def losetup(img)
413
476
  disk = Run.run_simple("losetup -f --show #{img.shellescape}", sudo: true, chomp: true, error_mode: :nil)
414
477
  close=lambda do