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.
- checksums.yaml +5 -5
- data/.gitignore +6 -2
- data/.travis.yml +10 -0
- data/.yardopts +6 -1
- data/Gemfile +8 -0
- data/LICENSE.txt +1 -1
- data/README.md +27 -3
- data/Rakefile +7 -12
- data/TODO +2 -0
- data/bin/abs_to_rel.rb +42 -0
- data/bin/mv_and_ln.rb +54 -0
- data/gemspec.yml +5 -4
- data/lib/shell_helpers.rb +38 -11
- data/lib/shell_helpers/export.rb +169 -0
- data/lib/shell_helpers/logger.rb +83 -28
- data/lib/shell_helpers/options.rb +28 -0
- data/lib/shell_helpers/pathname.rb +583 -110
- data/lib/shell_helpers/run.rb +115 -29
- data/lib/shell_helpers/sh.rb +188 -39
- data/lib/shell_helpers/sysutils.rb +427 -0
- data/lib/shell_helpers/utils.rb +216 -119
- data/lib/shell_helpers/version.rb +1 -1
- data/shell_helpers.gemspec +13 -1
- data/test/helper.rb +12 -1
- data/test/test_export.rb +77 -0
- metadata +33 -8
- data/.document +0 -3
@@ -0,0 +1,427 @@
|
|
1
|
+
require 'shell_helpers/export'
|
2
|
+
require 'shell_helpers/sh'
|
3
|
+
require 'shell_helpers/pathname'
|
4
|
+
require 'shellwords'
|
5
|
+
|
6
|
+
module ShellHelpers
|
7
|
+
|
8
|
+
module SysUtils
|
9
|
+
extend self
|
10
|
+
SysError=Class.new(StandardError)
|
11
|
+
|
12
|
+
# wrap 'stat'
|
13
|
+
def stat_file(file)
|
14
|
+
require 'time'
|
15
|
+
opts=%w(a b B f F g G h i m n N o s u U w x y z)
|
16
|
+
stats=Run.run_simple("stat --format='#{opts.map{|o| "%#{o}\n"}.join}' #{file.shellescape}", chomp: :lines)
|
17
|
+
r={}
|
18
|
+
r[:access]=stats[0]
|
19
|
+
r[:blocknumber]=stats[1].to_i
|
20
|
+
r[:blocksize]=stats[2].to_i
|
21
|
+
r[:rawmode]=stats[3]
|
22
|
+
r[:filetype]=stats[4]
|
23
|
+
r[:gid]=stats[5].to_i
|
24
|
+
r[:group]=stats[6]
|
25
|
+
r[:hardlinks]=stats[7].to_i
|
26
|
+
r[:inode]=stats[8].to_i
|
27
|
+
r[:mountpoint]=stats[9]
|
28
|
+
r[:filename]=stats[10]
|
29
|
+
r[:quotedfilename]=stats[11]
|
30
|
+
r[:optimalsize]=stats[12]
|
31
|
+
r[:size]=stats[13].to_i
|
32
|
+
r[:uid]=stats[14].to_i
|
33
|
+
r[:user]=stats[15]
|
34
|
+
r[:birthtime] = begin Time.parse(stats[16]) rescue nil end
|
35
|
+
r[:accesstime] = begin Time.parse(stats[17]) rescue nil end
|
36
|
+
r[:changedtime]= begin Time.parse(stats[18]) rescue nil end
|
37
|
+
r[:statustime] = begin Time.parse(stats[19]) rescue nil end
|
38
|
+
r
|
39
|
+
end
|
40
|
+
# wrap stat --file-system
|
41
|
+
def stat_filesystem(file, up: true)
|
42
|
+
if up
|
43
|
+
file=Pathname.new(file)
|
44
|
+
file.ascend.each do |f|
|
45
|
+
return stat_filesystem(f, up: false) if f.exist?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
opts=%w(a b c d f i l n s S T)
|
49
|
+
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}
|
51
|
+
r={}
|
52
|
+
r[:userfreeblocks]=stats[0].to_i
|
53
|
+
r[:totalblocks]=stats[1].to_i
|
54
|
+
r[:totalnodes]=stats[2].to_i
|
55
|
+
r[:freenodes]=stats[3].to_i
|
56
|
+
r[:freeblocks]=stats[4].to_i
|
57
|
+
r[:fsid]=stats[5]
|
58
|
+
r[:maxlength]=stats[6].to_i
|
59
|
+
r[:name]=stats[7]
|
60
|
+
r[:blocksize]=stats[8].to_i
|
61
|
+
r[:innerblocksize]=stats[9].to_i
|
62
|
+
r[:fstype]=stats[10]
|
63
|
+
r
|
64
|
+
end
|
65
|
+
|
66
|
+
def parse_blkid(output)
|
67
|
+
devs={}
|
68
|
+
r=[]
|
69
|
+
convert=lambda do |h|
|
70
|
+
h[:type] && h[:fstype]=h.delete(:type)
|
71
|
+
name=h[:devname]
|
72
|
+
devs[name]=h
|
73
|
+
end
|
74
|
+
output=output.each_line if output.is_a?(String)
|
75
|
+
output.each do |l|
|
76
|
+
l=l.chomp
|
77
|
+
if l.empty?
|
78
|
+
convert.(Export.import_parse(r))
|
79
|
+
r=[]
|
80
|
+
else
|
81
|
+
r<<l
|
82
|
+
end
|
83
|
+
end
|
84
|
+
convert.(Export.import_parse(r)) unless r.empty?
|
85
|
+
devs
|
86
|
+
end
|
87
|
+
|
88
|
+
# output should be the result of `blkid -o export ...`
|
89
|
+
# return a list of things like
|
90
|
+
# {:devname=>"/dev/sda2",
|
91
|
+
# :label=>"swap",
|
92
|
+
# :uuid=>"82af0d2f-5ef6-418a-8656-bdfe843f19e1",
|
93
|
+
# :type=>"swap",
|
94
|
+
# :partlabel=>"swap",
|
95
|
+
# :partuuid=>"f4eef373-0803-4701-bd47-b968c44065a6"}
|
96
|
+
def blkid(*args, sudo: false)
|
97
|
+
# get devname, (part)label/uuid, fstype
|
98
|
+
fsoptions=Run.run_simple("blkid -o export #{args.shelljoin}", fail_mode: :empty, chomp: true, sudo: sudo)
|
99
|
+
parse_blkid(fsoptions)
|
100
|
+
end
|
101
|
+
|
102
|
+
# use lsblk to get infos about devices
|
103
|
+
def lsblk(sudo: false)
|
104
|
+
# get devname, mountpoint, (part)label/uuid, (part/dev/fs)type
|
105
|
+
fsoptions=Run.run_simple("lsblk -l -J -o NAME,MOUNTPOINT,LABEL,UUID,PARTLABEL,PARTUUID,PARTTYPE,TYPE,FSTYPE", fail_mode: :empty, chomp: true, sudo: sudo)
|
106
|
+
require 'json'
|
107
|
+
json=JSON.parse(fsoptions)
|
108
|
+
fs={}
|
109
|
+
json["blockdevices"]&.each do |props|
|
110
|
+
r={}
|
111
|
+
props.each do |k,v|
|
112
|
+
k=k.to_sym
|
113
|
+
k=:devtype if k==:type
|
114
|
+
if k==:name
|
115
|
+
k=:devname
|
116
|
+
v="/dev/#{v}"
|
117
|
+
end
|
118
|
+
r[k]=v unless v.nil?
|
119
|
+
end
|
120
|
+
fs[r[:devname]]=r
|
121
|
+
end
|
122
|
+
fs
|
123
|
+
end
|
124
|
+
|
125
|
+
# use findmnt to get infos about mount points
|
126
|
+
def findmnt(sudo: false)
|
127
|
+
# get devname, mountpoint, mountoptions, (part)label/uuid, fsroot
|
128
|
+
# only looks at mounted devices (but in comparison to lsblk also show
|
129
|
+
# virtual mounts and bind mounts)
|
130
|
+
fsoptions=SH::Run.run_simple("findmnt --raw -o SOURCE,TARGET,FSTYPE,OPTIONS,LABEL,UUID,PARTLABEL,PARTUUID,FSROOT", fail_mode: :empty, chomp: true, sudo: sudo)
|
131
|
+
fs={}
|
132
|
+
fsoptions.each_line.to_a[1..-1]&.each do |l|
|
133
|
+
#two ' ' means a missing option, so we want to split on / /, not on ' '
|
134
|
+
source,target,fstype,options,label,uuid,partlabel,partuuid,fsroot=l.chomp.split(/ /)
|
135
|
+
next unless source=~%r(^/dev/) #skip non dev mountpoints
|
136
|
+
options=options.split(',')
|
137
|
+
fs[source]={mountpoint: target, devname: source, fstype: fstype, mountoptions: options, label: label, uuid: uuid, partlabel: partlabel, partuuid: partuuid, fsroot: fsroot}
|
138
|
+
end
|
139
|
+
fs
|
140
|
+
end
|
141
|
+
|
142
|
+
def fs_infos(mode: :devices)
|
143
|
+
return findmnt if mode == :mount
|
144
|
+
return lsblk.merge(findmnt) if mode == :all
|
145
|
+
# :devname, :devtype, :mountpoint, [:mountoptions], :label, :uuid, :partlabel, :partuuid, :parttype, :fstype, [:fsroot]
|
146
|
+
lsblk
|
147
|
+
end
|
148
|
+
|
149
|
+
def refresh_blkid_cache
|
150
|
+
Sh.sh("blkid", sudo: true)
|
151
|
+
end
|
152
|
+
|
153
|
+
# find devices matching props
|
154
|
+
def find_devices(props, method: :all)
|
155
|
+
props=props.clone
|
156
|
+
return [{devname: props[:devname]}] unless props[:devname].nil?
|
157
|
+
# name is both for label and partlabel
|
158
|
+
if props.key?(:name)
|
159
|
+
props[:label] = props[:name] unless props.key?(:label)
|
160
|
+
props[:partlabel] = props[:name] unless props.key?(:partlabel)
|
161
|
+
end
|
162
|
+
|
163
|
+
if method==:blkid
|
164
|
+
# Warning, since 'blkid' can only test one label, we cannot check
|
165
|
+
# that all parameters are valid
|
166
|
+
# search from most discriminant to less discriminant
|
167
|
+
%i(uuid label partuuid partlabel).each do |key|
|
168
|
+
if (label=props[key])
|
169
|
+
return parse_blkid(%x/blkid -o export -t #{key.to_s.upcase}=#{label.shellescape}/).values
|
170
|
+
end
|
171
|
+
end
|
172
|
+
# unfortunately `blkid PARTTYPE=...` does not work, so we need to parse
|
173
|
+
# ourselves
|
174
|
+
if props[:parttype]
|
175
|
+
find_devices(props, method: :all)
|
176
|
+
end
|
177
|
+
else
|
178
|
+
fs=fs_infos
|
179
|
+
# here we check all parameters
|
180
|
+
# however, if none are defined, this return true, so we check that at least one is defined
|
181
|
+
return [] unless %i(uuid label partuuid partlabel parttype).any? {|k| props[k]}
|
182
|
+
return fs.keys.select do |k|
|
183
|
+
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
|
+
next false if (disk=props[:disk]) && !fsprops[:devname].start_with?(disk.to_s)
|
187
|
+
%i(uuid label partuuid partlabel parttype).all? do |key|
|
188
|
+
ptype=props[key]
|
189
|
+
ptype=partition_type(ptype) if key==:parttype and ptype.is_a?(Symbol)
|
190
|
+
!ptype or !fsprops[key] or ptype==fsprops[key]
|
191
|
+
end
|
192
|
+
end.map {|k| fs[k]}
|
193
|
+
end
|
194
|
+
return []
|
195
|
+
end
|
196
|
+
|
197
|
+
# like find_devices but warn out if the result is of length > 1
|
198
|
+
def find_device(props)
|
199
|
+
devs=find_devices(props)
|
200
|
+
devs=yield(devs) if block_given?
|
201
|
+
devs=[devs].flatten
|
202
|
+
warn "Device #{props} not found" if devs.empty?
|
203
|
+
warn "Several devices for #{props} found: #{devs.map {|d| d&.fetch(:devname)}}" if devs.length >1
|
204
|
+
return devs.first&.fetch(:devname)
|
205
|
+
end
|
206
|
+
|
207
|
+
def mount(paths, mkpath: true, abort_on_error: true, sort: true)
|
208
|
+
paths=paths.values if paths.is_a?(Hash)
|
209
|
+
paths=paths.select {|p| p[:mountpoint]}
|
210
|
+
# sort so that the mounts are in correct order
|
211
|
+
paths=paths.sort { |p1, p2| Pathname.new(p1[:mountpoint]) <=> Pathname.new(p2[:mountpoint]) } if sort
|
212
|
+
close=lambda do
|
213
|
+
umount(paths, sort: sort)
|
214
|
+
end
|
215
|
+
paths.each do |path|
|
216
|
+
dev=find_device(path)
|
217
|
+
raise SysError.new("Device #{path} not found") unless dev
|
218
|
+
options=path[:mountoptions]||[]
|
219
|
+
options=options.split(',') if options.is_a?(String)
|
220
|
+
options<<"subvol=#{path[:subvol].shellescape}" if path[:subvol]
|
221
|
+
#options=options.join(',') if options.is_a?(Array)
|
222
|
+
mntpoint=Pathname.new(path[:mountpoint])
|
223
|
+
mntpoint.sudo_mkpath if mkpath
|
224
|
+
cmd="mount #{(fs=path[:fstype]) && "-t #{fs.shellescape}"} #{options.empty? ? "" : "-o #{options.join(',').shellescape}"} #{dev.shellescape} #{mntpoint.shellescape}"
|
225
|
+
abort_on_error ? Sh.sh!(cmd, sudo: true) : Sh.sh(cmd, sudo: true)
|
226
|
+
end
|
227
|
+
if block_given?
|
228
|
+
begin
|
229
|
+
yield paths
|
230
|
+
ensure
|
231
|
+
close.call
|
232
|
+
end
|
233
|
+
end
|
234
|
+
return paths, close
|
235
|
+
end
|
236
|
+
|
237
|
+
def umount(paths, sort: true)
|
238
|
+
paths=paths.values if paths.is_a?(Hash)
|
239
|
+
paths=paths.select {|p| p[:mountpoint]}
|
240
|
+
paths=paths.sort { |p1, p2| Pathname.new(p1[:mountpoint]) <=> Pathname.new(p2[:mountpoint]) } if sort
|
241
|
+
paths.reverse.each do |path|
|
242
|
+
mntpoint=path[:mountpoint]
|
243
|
+
Sh.sh("umount #{mntpoint.shellescape}", sudo: true)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def partition_type(type, mode: :guid)
|
248
|
+
if mode==:symbol
|
249
|
+
%i(boot swap home x86_root x86-64_root arm64_root arm32_root linux).each do |symb|
|
250
|
+
%i(hexa guid).each do |mode|
|
251
|
+
partition_type(symb, mode: mode) == type.downcase and return symb
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
case type
|
256
|
+
when :boot
|
257
|
+
mode == :hexa ? "ef00" : "c12a7328-f81f-11d2-ba4b-00a0c93ec93b"
|
258
|
+
when :swap
|
259
|
+
mode == :hexa ? "8200" : "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f"
|
260
|
+
when :home
|
261
|
+
mode == :hexa ? "8302" : "933ac7e1-2eb4-4f13-b844-0e14e2aef915"
|
262
|
+
when :x86_root
|
263
|
+
mode == :hexa ? "8303" : "44479540-f297-41b2-9af7-d131d5f0458a"
|
264
|
+
when :"x86-64_root"
|
265
|
+
mode == :hexa ? "8304" : "4f68bce3-e8cd-4db1-96e7-fbcaf984b709"
|
266
|
+
when :arm64_root
|
267
|
+
mode == :hexa ? "8305" : "b921b045-1df0-41c3-af44-4c6f280d3fae"
|
268
|
+
when :arm32_root
|
269
|
+
mode == :hexa ? "8307" : "69dad710-2ce4-4e3c-b16c-21a1d49abed3"
|
270
|
+
when :linux
|
271
|
+
mode == :hexa ? "8300" : "0fc63daf-8483-4772-8e79-3d69d8477de4"
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def partition_infos(device, sudo: false)
|
276
|
+
parts = Run.run_simple("partx -o NR --show #{device.shellescape}", sudo: sudo) { return nil }
|
277
|
+
infos=[]
|
278
|
+
nums=parts.each_line.count - 1
|
279
|
+
(1..nums).each do |i|
|
280
|
+
infos[i-1]={}
|
281
|
+
part_options=Run.run_simple("sgdisk -i#{i} #{device.shellescape}", chomp: true, sudo: sudo)
|
282
|
+
part_options.match(/^Partition name: '(.*)'/) do |m|
|
283
|
+
infos[i-1][:partlabel]=m[1]
|
284
|
+
end
|
285
|
+
part_options.match(/^Attribute flags: (.*)/) do |m|
|
286
|
+
infos[i-1][:partattributes]=m[1]
|
287
|
+
end
|
288
|
+
part_options.match(/^Partition unique GUID: (.*)/) do |m|
|
289
|
+
infos[i-1][:partuuid]=m[1].downcase
|
290
|
+
end
|
291
|
+
part_options.match(/^Partition GUID code: (\S*)/) do |m|
|
292
|
+
infos[i-1][:parttype]=m[1].downcase
|
293
|
+
end
|
294
|
+
end
|
295
|
+
infos
|
296
|
+
end
|
297
|
+
|
298
|
+
#options: check => check that no partitions exist first
|
299
|
+
def make_partitions(partitions, check: true, partprobe: true)
|
300
|
+
partitions=partitions.values if partitions.is_a?(Hash)
|
301
|
+
done=[]
|
302
|
+
disk_partitions=partitions.group_by {|p| p[:disk]}
|
303
|
+
disk_partitions.each do |disk, dpartitions|
|
304
|
+
next if disk.nil?
|
305
|
+
if check
|
306
|
+
partinfos=blkid(disk, sudo: true)
|
307
|
+
# gpt partitions: PTUUID="652121ab-7935-403c-8b87-65a149a415ac" PTTYPE="gpt"
|
308
|
+
# dos partitions: PTUUID="17a4a006" PTTYPE="dos"
|
309
|
+
# others: PTTYPE="PMBR"
|
310
|
+
unless partinfos.empty?
|
311
|
+
raise SysError("Disk #{disk} is not empty: #{partinfos}") if check==:raise
|
312
|
+
warn "Disk #{disk} is not empty: #{partinfos}, skipping..."
|
313
|
+
next
|
314
|
+
end
|
315
|
+
end
|
316
|
+
opts=[]
|
317
|
+
dpartitions.each do |partition|
|
318
|
+
next unless %i(partnum partstart partlength partlabel partattributes parttype).any? {|k| partition.key?(k)}
|
319
|
+
num=partition[:partnum]&.to_i || 0
|
320
|
+
start=partition[:partstart] || 0
|
321
|
+
length=partition[:partlength] || 0
|
322
|
+
name=partition[:partlabel] || partition[:name]
|
323
|
+
attributes=partition[:partattributes]
|
324
|
+
type=partition[:parttype]
|
325
|
+
attributes=2 if type==:boot
|
326
|
+
type=partition_type(type, mode: :hexa) if type.is_a?(Symbol)
|
327
|
+
uuid=partition[:partuuid]
|
328
|
+
alignment=partition[:partalignment]
|
329
|
+
opts += ["-n", "#{num}:#{start}:#{length}"]
|
330
|
+
opts += ["-c", "#{num}:#{name}"] if name
|
331
|
+
opts += ["-t", "#{num}:#{type}"] if type
|
332
|
+
opts += ["-u", "#{num}:#{uuid}"] if uuid
|
333
|
+
opts << "--attributes=#{num}:set:#{attributes}" if attributes
|
334
|
+
opts << ["--set-alignment=#{alignment}"] if alignment
|
335
|
+
end
|
336
|
+
unless opts.empty?
|
337
|
+
Sh.sh!("sgdisk #{opts.shelljoin} #{disk.shellescape}", sudo: true)
|
338
|
+
done << disk
|
339
|
+
end
|
340
|
+
end
|
341
|
+
SH.sh("partprobe #{done.shelljoin}", sudo: true) unless done.empty? or !partprobe
|
342
|
+
done
|
343
|
+
end
|
344
|
+
|
345
|
+
def zap_partitions(disk)
|
346
|
+
# Zap (destroy) the GPT and MBR data structures and then exit.
|
347
|
+
Sh.sh("sgdisk --zap-all #{disk.shellescape}", sudo: true)
|
348
|
+
end
|
349
|
+
def wipefs(disk)
|
350
|
+
# wipe all signatures
|
351
|
+
Sh.sh("wipefs -a #{disk.shellescape}", sudo: true)
|
352
|
+
end
|
353
|
+
|
354
|
+
def make_fs(fs, check: true)
|
355
|
+
fs=fs.values if fs.is_a?(Hash)
|
356
|
+
fs.each do |partfs|
|
357
|
+
dev=SH.find_device(partfs)
|
358
|
+
if dev and (fstype=partfs[:fstype])
|
359
|
+
opts=partfs[:fsoptions]||[]
|
360
|
+
bin="mkfs.#{fstype.to_s.shellescape}"
|
361
|
+
bin="mkswap" if fstype.to_s=="swap"
|
362
|
+
label=partfs[:label]||partfs[:name]
|
363
|
+
if label
|
364
|
+
labelkey="-L"
|
365
|
+
labelkey="-n" if fstype.to_s=="vfat"
|
366
|
+
opts+=[labelkey, label]
|
367
|
+
end
|
368
|
+
if check
|
369
|
+
diskinfos=blkid(dev, sudo: true)
|
370
|
+
unless diskinfos.dig(dev,:fstype).nil?
|
371
|
+
raise SysError("Device #{dev} already has a filesystem: #{diskinfos[dev]}") if check==:raise
|
372
|
+
warn "Device #{dev} already has a filesystem: #{diskinfos[dev]}"
|
373
|
+
next
|
374
|
+
end
|
375
|
+
end
|
376
|
+
SH.sh("#{bin} #{opts.shelljoin} #{dev.shellescape}", sudo: true)
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
def make_raw_image(name, size="1G")
|
382
|
+
raw=Pathname.new(name)
|
383
|
+
raw.touch
|
384
|
+
rawfs=stat_filesystem(raw)
|
385
|
+
raw.chattr("+C") if rawfs[:fstype]=="btrfs"
|
386
|
+
Sh.sh("fallocate -l #{size} #{raw.shellescape}")
|
387
|
+
raw
|
388
|
+
end
|
389
|
+
|
390
|
+
def make_btrfs_subvolume(dir, check: true)
|
391
|
+
if check and dir.directory?
|
392
|
+
raise SysError("Subvolume already exists at #{dir}") if check==:raise
|
393
|
+
warn "Subvolume already exists at #{dir}, skipping..."
|
394
|
+
else
|
395
|
+
SH.sh("btrfs subvolume create #{dir.shellescape}", sudo: true)
|
396
|
+
dir
|
397
|
+
end
|
398
|
+
end
|
399
|
+
def make_dir_or_subvolume(dir)
|
400
|
+
dir=Pathname.new(dir)
|
401
|
+
return :directory if dir.directory?
|
402
|
+
fstype=stat_filesystem(dir, up: true)
|
403
|
+
if fstype[:fstype]=="btrfs"
|
404
|
+
make_btrfs_subvolume(dir)
|
405
|
+
return :subvol
|
406
|
+
else
|
407
|
+
dir.sudo_mkpath
|
408
|
+
return :directory
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
def losetup(img)
|
413
|
+
disk = Run.run_simple("losetup -f --show #{img.shellescape}", sudo: true, chomp: true, error_mode: :nil)
|
414
|
+
close=lambda do
|
415
|
+
SH.sh("losetup -d #{disk.shellescape}", sudo: true) if disk
|
416
|
+
end
|
417
|
+
if block_given?
|
418
|
+
begin
|
419
|
+
yield disk
|
420
|
+
ensure
|
421
|
+
close.call
|
422
|
+
end
|
423
|
+
end
|
424
|
+
return disk, close
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
data/lib/shell_helpers/utils.rb
CHANGED
@@ -1,95 +1,38 @@
|
|
1
1
|
require 'shellwords'
|
2
|
-
require '
|
3
|
-
|
4
|
-
require_relative 'parser'
|
2
|
+
require 'shell_helpers/pathname'
|
3
|
+
require 'dr/base/uri'
|
5
4
|
|
6
|
-
module
|
7
|
-
module ShellExport
|
8
|
-
extend self
|
5
|
+
module ShellHelpers
|
9
6
|
|
10
|
-
|
11
|
-
def
|
12
|
-
|
13
|
-
when String
|
14
|
-
return v.shellescape
|
15
|
-
when Array
|
16
|
-
return "(#{v.map {|i| i.to_s.shellescape}.join(' ')})"
|
17
|
-
when Hash
|
18
|
-
return "(#{v.map {|k,v| k.to_s.shellescape+" "+v.to_s.shellescape}.join(' ')})"
|
19
|
-
when nil
|
20
|
-
return ""
|
21
|
-
else
|
22
|
-
return v.to_s.shellescape
|
23
|
-
end
|
7
|
+
module ExtendSSHKit
|
8
|
+
def backend(&b)
|
9
|
+
local? ? SSHKit::Backend::Local.new(&b) : SSHKit.config.backend.new(self, &b)
|
24
10
|
end
|
25
|
-
|
26
|
-
|
27
|
-
#ploum=plim
|
28
|
-
#that can be evaluated by the shell
|
29
|
-
def export_hash(hash, local: false, export: false, prefix:"")
|
30
|
-
r=""
|
31
|
-
r+="local #{hash.keys.map {|s| s.to_s.upcase}.join(" ")}\n" if local
|
32
|
-
hash.each do |k,v|
|
33
|
-
name=prefix+k.to_s.upcase
|
34
|
-
r+="typeset -A #{name};\n" if Hash === v
|
35
|
-
r+=name+"="+export_value(v)+";\n"
|
36
|
-
end
|
37
|
-
r+="export #{hash.keys.map {|k| prefix+k.to_s.upcase}.join(" ")}\n" if export
|
38
|
-
return r
|
11
|
+
def connect(&b)
|
12
|
+
backend(&b).run
|
39
13
|
end
|
14
|
+
end
|
40
15
|
|
41
|
-
|
42
|
-
|
43
|
-
r=""
|
44
|
-
#name=name.upcase
|
45
|
-
r+="local #{name}\n" if local
|
46
|
-
r+="typeset -A #{name};\n" if Hash === value
|
47
|
-
r+=name+"="+export_value(value)+";\n"
|
48
|
-
r+="export #{name}\n" if export
|
49
|
-
return r
|
50
|
-
end
|
16
|
+
module Utils
|
17
|
+
extend self
|
51
18
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
def export_parse(hash,s)
|
67
|
-
r=""
|
68
|
-
args=Parser.parse_string(s)
|
69
|
-
args[:values].each do |k,v|
|
70
|
-
name=k
|
71
|
-
if !v
|
72
|
-
v=name
|
73
|
-
#in split mode, don't use prefix if name gave the value
|
74
|
-
name= name.size==1? "all" : "" if name[name.size-1]=="/"
|
75
|
-
end
|
76
|
-
if v.size>1 && v[v.size-1]=='/'
|
77
|
-
all=true
|
78
|
-
v=v[0...v.size-1]
|
79
|
-
end
|
80
|
-
value=hash.keyed_value(v)
|
81
|
-
if all
|
82
|
-
r+=export_hash(value, local: args[:opts][k]["local"], export: args[:opts][k]["export"], prefix: name)
|
83
|
-
else
|
84
|
-
r+=export_variable(name,value, local: args[:opts][k]["local"], export: args[:opts][k]["export"])
|
85
|
-
end
|
19
|
+
def eval_shell(r, shell: :puts)
|
20
|
+
return r if r.nil? or r.empty?
|
21
|
+
case (shell||"").to_sym
|
22
|
+
when :puts
|
23
|
+
puts r
|
24
|
+
when :eval
|
25
|
+
r+=";" if r && !r.end_with?(';')
|
26
|
+
print r
|
27
|
+
when :exec
|
28
|
+
require 'shell_helpers/sh'
|
29
|
+
return ShLog.sh(r)
|
30
|
+
when :exec_quiet
|
31
|
+
require 'shell_helpers/sh'
|
32
|
+
return Sh.sh(r)
|
86
33
|
end
|
87
34
|
return r
|
88
35
|
end
|
89
|
-
end
|
90
|
-
|
91
|
-
module ShellUtils
|
92
|
-
extend self
|
93
36
|
|
94
37
|
class << self
|
95
38
|
attr_accessor :orig_stdin, :orig_stdout, :orig_stderr
|
@@ -99,45 +42,68 @@ module SH
|
|
99
42
|
@orig_stderr=$stderr
|
100
43
|
|
101
44
|
#An improved find from Find::find that takes in the block the absolute and relative name of the files (+the directory where the relative file is from), and has filter options
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
end
|
45
|
+
#Returns ::Pathname, except when the value is a SH::Pathname where it
|
46
|
+
#returns a SH::Pathname
|
47
|
+
def find(*bases, filter: nil, prune: nil, follow_symlink: false, depth: false, max_depth: nil, chdir: false)
|
48
|
+
block_given? or return enum_for(__method__, *bases, filter: filter, follow_symlink: follow_symlink, depth: depth, max_depth: max_depth, chdir: chdir)
|
49
|
+
bases.collect!{|d| raise Errno::ENOENT unless File.exist?(d); d.dup}.each do |base|
|
50
|
+
klass=base.is_a?(::Pathname) ? base.class : ::Pathname
|
51
|
+
base=klass.new(base)
|
52
|
+
|
53
|
+
test_filter=lambda do |filter,*files|
|
54
|
+
case filter
|
55
|
+
when Proc
|
56
|
+
filter.call(*files)
|
57
|
+
when Array
|
58
|
+
file=files.first
|
59
|
+
filter.any? do |test|
|
60
|
+
case test
|
61
|
+
when :directory? #special case
|
62
|
+
file.directory? && !file.symlink?
|
63
|
+
else
|
64
|
+
file.send(test)
|
123
65
|
end
|
124
|
-
end
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
yield_files=lambda do |*files|
|
71
|
+
unless test_filter.(filter,*files)
|
72
|
+
files.map! {|f| f.dup.taint}
|
73
|
+
if chdir
|
74
|
+
Dir.chdir(base) do
|
75
|
+
yield *files, base
|
76
|
+
end
|
77
|
+
else
|
78
|
+
yield *files, base
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
do_find=lambda do |*files|
|
84
|
+
file,filerel=*files
|
85
|
+
catch(:prune) do #use throw(:prune) to skip a path (recursively)
|
86
|
+
unless test_filter.(prune,*files)
|
87
|
+
yield_files.(*files) unless depth
|
88
|
+
if file.directory? and (max_depth.nil? or (filerel.to_s=="." and max_depth>0) or filerel.each_filename.to_a.size < max_depth)
|
89
|
+
next if !follow_symlink && file.symlink?
|
90
|
+
file.children(false).sort.reverse_each do |f|
|
91
|
+
fj = file + f
|
92
|
+
f = filerel + f
|
93
|
+
do_find.(fj.untaint,f.untaint)
|
94
|
+
end
|
95
|
+
yield_files.(*files) if depth
|
131
96
|
end
|
132
97
|
end
|
133
98
|
end
|
134
99
|
end
|
100
|
+
do_find.call(base, klass.new('.'))
|
135
101
|
end
|
136
102
|
end
|
137
103
|
|
138
104
|
#all output is sent to the pager
|
139
|
-
def run_pager(
|
140
|
-
return unless $stdout.tty? and
|
105
|
+
def run_pager(*args, launch: :tty, default_less_env: "-FRX")
|
106
|
+
return unless $stdout.tty? and launch != :never
|
141
107
|
read, write = IO.pipe
|
142
108
|
|
143
109
|
unless Kernel.fork # Child process
|
@@ -154,16 +120,17 @@ module SH
|
|
154
120
|
write.close
|
155
121
|
|
156
122
|
#ENV['LESS'] = 'FSRX' # Don't page if the input is short enough
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
ENV['LESS']=
|
123
|
+
less_env=ENV['LESS']
|
124
|
+
less_env=default_less_env if less_env.empty?
|
125
|
+
less_env+="F" unless less_env.match(/F/) or launch == :always
|
126
|
+
less_env+="R" unless less_env.match(/R/)
|
127
|
+
less_env+="X" unless less_env.match(/X/)
|
128
|
+
ENV['LESS']=less_env
|
163
129
|
|
164
130
|
Kernel.select [$stdin] # Wait until we have input before we start the pager
|
165
131
|
pager = ENV['PAGER'] || 'less'
|
166
|
-
|
132
|
+
run=args.unshift(pager)
|
133
|
+
exec *run rescue exec "/bin/sh", "-c", *run
|
167
134
|
end
|
168
135
|
|
169
136
|
#inside run_pager, escape from the pager
|
@@ -222,5 +189,135 @@ module SH
|
|
222
189
|
nil
|
223
190
|
end
|
224
191
|
|
192
|
+
def find_file(file,path)
|
193
|
+
path.each do |dir|
|
194
|
+
dir=Pathname.new(dir)
|
195
|
+
path=dir+file
|
196
|
+
return path if path.file?
|
197
|
+
end
|
198
|
+
return nil
|
199
|
+
end
|
200
|
+
|
201
|
+
def find_files(pattern,path)
|
202
|
+
path.map { |dir| Pathname.glob(dir+pattern) }.flatten
|
203
|
+
end
|
204
|
+
|
205
|
+
def rsync(*files, out, default_opts: "-vcz", preserve: true, partial: true, keep_dirlinks: false, sudo: false, backup: false, relative: false, delete: false, clean_out: false, expected: 23, chown: nil, sshcommand: nil, exclude: [], **opts)
|
206
|
+
require 'shell_helpers/sh'
|
207
|
+
rsync_opts=[*opts.delete(:rsync_opts)] || []
|
208
|
+
rsync_opts << default_opts
|
209
|
+
rsync_opts << "-a" if preserve
|
210
|
+
rsync_opts << "-P" if partial #--partial --progress
|
211
|
+
rsync_opts+=%w(--no-owner --no-group) if preserve==:nochown
|
212
|
+
rsync_opts+=["--chown", chown] if chown
|
213
|
+
#on dest: do not replace a symlink to a directory with the real directory
|
214
|
+
#use --copy-dirlinks for the same usage on source
|
215
|
+
rsync_opts << "--keep-dirlinks" if keep_dirlinks
|
216
|
+
exclude.each do |ex|
|
217
|
+
rsync_opts += ["--exclude", ex.shellescape]
|
218
|
+
end
|
219
|
+
if relative
|
220
|
+
rsync_opts << "--relative"
|
221
|
+
rsync_opts << "--no-implied-dirs"
|
222
|
+
end
|
223
|
+
rsync_opts << "--delete" if delete
|
224
|
+
if clean_out
|
225
|
+
out=Pathname.new(out)
|
226
|
+
out.rmtree
|
227
|
+
out.mkpath
|
228
|
+
end
|
229
|
+
opts[:log]||=true
|
230
|
+
opts[:log_level_execute]||=:info
|
231
|
+
if backup
|
232
|
+
rsync_opts << "--backup"
|
233
|
+
rsync_opts << (backup.to_s[-1]=="/" ? "--backup-dir=#{backup}" : "--suffix=#{backup}") unless backup==true
|
234
|
+
end
|
235
|
+
if sshcommand
|
236
|
+
rsync_opts << "-e"
|
237
|
+
rsync_opts << sshcommand.shellescape
|
238
|
+
end
|
239
|
+
rsync_opts+=opts.delete(:rsync_late_opts)||[]
|
240
|
+
Sh.sh( (sudo ? ["sudo"] : [])+["rsync"]+rsync_opts+files.map(&:to_s)+[out.to_s], expected: expected, **opts)
|
241
|
+
#expected: rsync error code 23 is some files/attrs were not transferred
|
242
|
+
end
|
243
|
+
|
244
|
+
# host can be of the form user@host:port
|
245
|
+
# warning this is different from standard ssh syntax of user@host:path
|
246
|
+
def ssh(host, *commands, mode: :exec, ssh_command: 'ssh',
|
247
|
+
ssh_options: [], ssh_Ooptions: [],
|
248
|
+
port: nil, forward: nil, x11: nil, user: nil, path: nil, parse: true,
|
249
|
+
pty: nil,
|
250
|
+
**opts)
|
251
|
+
|
252
|
+
#sshkit has a special setting for :local
|
253
|
+
host=host.to_s unless mode==:sshkit and host.is_a?(Symbol)
|
254
|
+
parse and host.is_a?(String) and host.match(/^(?:(.*)@)?(.*?)(?::(\d*))?$/) do |m|
|
255
|
+
user||=m[1]
|
256
|
+
host=m[2]
|
257
|
+
port||=m[3]
|
258
|
+
end
|
259
|
+
unless mode==:net_ssh or mode==:sshkit
|
260
|
+
ssh_command, *command_options= ssh_command.shellsplit
|
261
|
+
ssh_options=command_options+ssh_options
|
262
|
+
ssh_options += ["-p", port.to_s] if port
|
263
|
+
ssh_options += ["-W", forward] if forward
|
264
|
+
if x11 == :trusted
|
265
|
+
ssh_options << "-Y"
|
266
|
+
elsif x11
|
267
|
+
ssh_options << "-X"
|
268
|
+
end
|
269
|
+
ssh_options << "-T" if pty==false
|
270
|
+
ssh_options << "-t" if pty==true
|
271
|
+
ssh_options += ssh_Ooptions.map {|o| ["-o", o]}.flatten
|
272
|
+
else #net_ssh options needs to be a hash
|
273
|
+
ssh_options={} if ssh_options.is_a?(Array)
|
274
|
+
ssh_options[:port]=port if port
|
275
|
+
end
|
276
|
+
case mode
|
277
|
+
when :system,:spawn,:capture,:exec
|
278
|
+
host="#{user}@#{host}" if user
|
279
|
+
Sh.sh([ssh_command]+ssh_options+[host]+commands, mode: mode, **opts)
|
280
|
+
when :net_ssh
|
281
|
+
require 'net/ssh'
|
282
|
+
user=nil;
|
283
|
+
Net::SSH.start(host, user, ssh_options)
|
284
|
+
when :sshkit
|
285
|
+
require 'sshkit'
|
286
|
+
host=SSHKit::Host.new(host)
|
287
|
+
host.extend(ExtendSSHKit)
|
288
|
+
host.port=port if port
|
289
|
+
host.user=user if user
|
290
|
+
host.ssh_options=ssh_options
|
291
|
+
host
|
292
|
+
when :uri
|
293
|
+
URI::Ssh::Generic.build(scheme: 'ssh', userinfo: user, host: host, path: path, port: port) #, query: ssh_options.join('&'))
|
294
|
+
else
|
295
|
+
# return options
|
296
|
+
{ ssh_command: ssh_command,
|
297
|
+
ssh_options: ssh_options,
|
298
|
+
ssh_command_options: ([ssh_command]+ssh_options).shelljoin,
|
299
|
+
user: user,
|
300
|
+
host: host,
|
301
|
+
hostssh: user ? "#{user}@#{host}" : host,
|
302
|
+
command: commands }
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def capture_stdout
|
307
|
+
old_stdout = $stdout
|
308
|
+
$stdout = StringIO.new('','w')
|
309
|
+
if block_given?
|
310
|
+
begin
|
311
|
+
yield
|
312
|
+
output=$stdout.string
|
313
|
+
ensure
|
314
|
+
$stdout = old_stdout
|
315
|
+
end
|
316
|
+
return output
|
317
|
+
else
|
318
|
+
return old_stdout
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
225
322
|
end
|
226
323
|
end
|