shell_helpers 0.1.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|