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.
@@ -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
@@ -1,95 +1,38 @@
1
1
  require 'shellwords'
2
- require 'dr/ruby_ext/core_ext'
3
- require_relative 'pathname'
4
- require_relative 'parser'
2
+ require 'shell_helpers/pathname'
3
+ require 'dr/base/uri'
5
4
 
6
- module SH
7
- module ShellExport
8
- extend self
5
+ module ShellHelpers
9
6
 
10
- #export a value for SHELL consumption
11
- def export_value(v)
12
- case v
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
- #from {ploum: plim} return something like
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
- #export_variable("ploum","plam") yields ploum="plam"
42
- def export_variable(name, value, local: false, export: false)
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
- #export_parse(hash,"name:value")
53
- #will output name=$(hash[value])
54
- #special cases: when value = '/' we return the full hash
55
- # when value ends by /, we return the splitted hash (and name serves
56
- # as a prefix)
57
- #Ex: Numenor ~ $ ./mine/00COMPUTERS.rb --export=//
58
- # HOSTNAME=Numenor;
59
- # HOSTTYPE=perso;
60
- # HOMEPATH=/home/dams;...
61
- # Numenor ~ $ ./mine/00COMPUTERS.rb --export=syst/
62
- # LAPTOP=true;
63
- # ARCH=i686;...
64
- #Remark: in name:value, we don't put name in uppercase
65
- #But in split hash mode, we put the keys in uppercase (to prevent collisions)
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
- def find(*paths, filter: nil, follow_symlink: false, depth: nil)
103
- block_given? or return enum_for(__method__, *paths, filter: filter, follow_symlink: follow_symlink, depth: depth)
104
-
105
- paths.collect!{|d| raise Errno::ENOENT unless File.exist?(d); Pathname.new(d.dup)}
106
- paths.collect!{|d| [ d, Pathname.new('.'), d ]}
107
- while filedata = paths.shift
108
- file, filerel, fileabs = *filedata
109
- catch(:prune) do #use throw(:prune) to skip a path
110
- Dir.chdir(fileabs) do
111
- #if the filter is true, we don't yield the file
112
- case filter
113
- when Proc
114
- filter.call(file,filerel,fileabs)
115
- when Array
116
- filter.all? do |test|
117
- case test
118
- when :directory? #special case
119
- file.directory? && !file.symlink?
120
- else
121
- file.send(test)
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 or yield file.dup.taint, filerel.dup.taint, fileabs.dup.taint
125
- if file.directory? and (depth.nil? or filerel.each_filename.size <= depth) then
126
- next if !follow_symlink && file.symlink?
127
- file.children(false).sort.reverse_each do |f|
128
- fj = file + f
129
- f = filerel + f
130
- paths.unshift [fj.untaint,f.untaint,fileabs]
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(opt=nil)
140
- return unless $stdout.tty? and opt != :never
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
- lessenv=ENV['LESS']
158
- lessenv="-FRX" if lessenv.empty?
159
- lessenv+="F" unless lessenv.match(/F/) or opt == :always
160
- lessenv+="R" unless lessenv.match(/R/)
161
- lessenv+="X" unless lessenv.match(/X/)
162
- ENV['LESS']=lessenv
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
- exec pager rescue exec "/bin/sh", "-c", pager
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