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.
- checksums.yaml +4 -4
- data/ChangeLog.md +236 -2
- data/LICENSE.txt +1 -1
- data/Rakefile +7 -10
- data/gemspec.yml +1 -0
- data/lib/shell_helpers.rb +61 -23
- data/lib/shell_helpers/export.rb +4 -4
- data/lib/shell_helpers/logger.bak.rb +481 -0
- data/lib/shell_helpers/logger.rb +274 -56
- data/lib/shell_helpers/pathname.rb +16 -3
- data/lib/shell_helpers/run.rb +48 -19
- data/lib/shell_helpers/sh.rb +97 -30
- data/lib/shell_helpers/sysutils.rb +71 -8
- data/lib/shell_helpers/utils.rb +12 -8
- data/lib/shell_helpers/version.rb +1 -1
- data/test/test_logger.rb +48 -0
- metadata +20 -5
data/lib/shell_helpers/run.rb
CHANGED
@@ -7,11 +7,7 @@ module ShellHelpers
|
|
7
7
|
extend(self)
|
8
8
|
RunError=Class.new(StandardError)
|
9
9
|
|
10
|
-
|
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.
|
76
|
-
|
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.
|
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=
|
101
|
+
out=err=nil
|
83
102
|
|
84
103
|
begin
|
85
104
|
if error==:capture
|
86
|
-
out,
|
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
|
-
|
108
|
-
if
|
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,
|
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,
|
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
|
162
|
+
# here the block is called in case of error
|
140
163
|
opts[:error_mode]=b if b
|
141
|
-
out,
|
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)
|
data/lib/shell_helpers/sh.rb
CHANGED
@@ -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,
|
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,
|
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
|
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
|
-
|
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
|
-
|
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:
|
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
|
-
|
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]
|
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[:
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
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
|
-
|
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
|