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.
@@ -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