shell_helpers 0.6.0 → 0.7.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.
@@ -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