squared 0.3.5 → 0.4.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.
@@ -15,8 +15,10 @@ module Squared
15
15
  include Rake::DSL
16
16
 
17
17
  VAR_SET = %i[parent global envname dependfile theme run script env pass].freeze
18
+ BLK_SET = %i[run depend doc lint test copy clean].freeze
18
19
  SEM_VER = /\b(\d+)(?:(\.)(\d+))?(?:(\.)(\d+)(\S+)?)?\b/.freeze
19
- private_constant :VAR_SET, :SEM_VER
20
+ URI_SCHEME = %r{^([a-z][a-z\d+-.]*)://[^@:\[\]\\^<>|\s]}i.freeze
21
+ private_constant :VAR_SET, :BLK_SET, :SEM_VER, :URI_SCHEME
20
22
 
21
23
  class << self
22
24
  def populate(*); end
@@ -25,7 +27,7 @@ module Squared
25
27
  def bannerargs(*); end
26
28
 
27
29
  def tasks
28
- %i[build depend graph doc lint test copy clean].freeze
30
+ (%i[build archive graph] + BLK_SET).freeze
29
31
  end
30
32
 
31
33
  def as_path(val)
@@ -51,7 +53,8 @@ module Squared
51
53
  end
52
54
 
53
55
  (@@tasks = {})[ref] = {
54
- 'graph' => %i[run print].freeze
56
+ 'graph' => %i[run print].freeze,
57
+ 'unpack' => %i[zip tar tgz tar.gz txz tar.xz].freeze
55
58
  }.freeze
56
59
  @@task_desc = Rake::TaskManager.record_task_metadata
57
60
  @@print_order = 0
@@ -59,8 +62,8 @@ module Squared
59
62
  attr_reader :name, :project, :workspace, :path, :theme, :exception, :pipe, :verbose,
60
63
  :group, :parent, :dependfile
61
64
 
62
- def initialize(workspace, path, name, *, group: nil, graph: nil, pass: nil, exclude: nil, verbose: nil,
63
- first: {}, last: {}, error: {}, common: ARG[:COMMON], **kwargs)
65
+ def initialize(workspace, path, name, *, group: nil, graph: nil, pass: nil, exclude: nil, release: nil,
66
+ archive: nil, first: {}, last: {}, error: {}, common: ARG[:COMMON], **kwargs)
64
67
  @path = path
65
68
  @workspace = workspace
66
69
  @name = name.to_s.freeze
@@ -73,6 +76,13 @@ module Squared
73
76
  @copy = kwargs[:copy]
74
77
  @clean = kwargs[:clean]
75
78
  @version = kwargs[:version]
79
+ @archive = case archive
80
+ when String, Array
81
+ { uri: archive }
82
+ when Hash
83
+ archive
84
+ end
85
+ @release = release
76
86
  @envname = @name.gsub(/[^\w]+/, '_').upcase.freeze
77
87
  @exception = env_bool(kwargs[:exception], workspace.exception, strict: true)
78
88
  @pipe = env_pipe(kwargs[:pipe], workspace.pipe, strict: true)
@@ -108,7 +118,7 @@ module Squared
108
118
  @parent = nil
109
119
  @global = false
110
120
  run_set(kwargs[:run], kwargs[:env], opts: kwargs.fetch(:opts, true))
111
- initialize_ref(Base.ref)
121
+ initialize_ref Base.ref
112
122
  end
113
123
 
114
124
  def initialize_ref(ref)
@@ -116,7 +126,7 @@ module Squared
116
126
  end
117
127
 
118
128
  def initialize_build(ref, **kwargs)
119
- initialize_ref(ref)
129
+ initialize_ref ref
120
130
  if (@script = @workspace.script_get(group: @group, ref: ref))
121
131
  if @script[:log] && !kwargs.key?(:log)
122
132
  kwargs[:log] = @script[:log]
@@ -166,7 +176,9 @@ module Squared
166
176
  def initialize_events(ref, **)
167
177
  return unless (events = @workspace.events_get(group: @group, ref: ref))
168
178
 
169
- events.each { |task, data| data.each { |ev, blk| (@events[ev] ||= {})[task] ||= blk } }
179
+ events.each do |task, data|
180
+ data.each { |ev, blk| (@events[ev] ||= {})[task] ||= [blk] }
181
+ end
170
182
  end
171
183
 
172
184
  def initialize_logger(log: nil, **)
@@ -192,12 +204,12 @@ module Squared
192
204
  raise if @exception
193
205
 
194
206
  file = nil
195
- warn log_message(Logger::WARN, e) if warning?
207
+ warn log_message(Logger::WARN, e, pass: true) if warning?
196
208
  end
197
209
  end
198
210
  log[:progname] ||= @name
199
211
  if (val = env('LOG_LEVEL', ignore: false))
200
- log[:level] = val
212
+ log[:level] = val.match?(/^\d$/) ? log_sym(val.to_i) : val
201
213
  end
202
214
  log.delete(:file)
203
215
  @log = [file, log]
@@ -206,20 +218,18 @@ module Squared
206
218
  def initialize_env(dev: nil, prod: nil, **)
207
219
  @dev = env_match('BUILD', dev, suffix: 'DEV', strict: true)
208
220
  @prod = env_match('BUILD', prod, suffix: 'PROD', strict: true)
209
- cmd = @output[0]
210
- if @output[2] != false && (val = env('BUILD', suffix: 'ENV'))
211
- begin
212
- data = JSON.parse(val)
213
- raise_error('invalid JSON object', val, hint: "BUILD_#{@envname}_ENV") unless data.is_a?(Hash)
214
- rescue StandardError => e
215
- log&.warn e
216
- else
217
- @output[2] = data
221
+ unless @output[2] == false || !(val = env('BUILD', suffix: 'ENV'))
222
+ data = parse_json(val, hint: "BUILD_#{@envname}_ENV")
223
+ @output[2] = data if data
224
+ end
225
+ unless @output[0] == false || @output[0].is_a?(Array)
226
+ if (val = env('BUILD', suffix: 'OPTS'))
227
+ n = @output[0] ? 1 : 3
228
+ @output[n] = merge_opts(@output[n], shell_split(val, escape: false))
229
+ end
230
+ if (val = env(ref.to_s.upcase, suffix: 'OPTS'))
231
+ @output[4] = merge_opts(@output[4], shell_split(val, escape: false))
218
232
  end
219
- end
220
- if cmd != false && !cmd.is_a?(Array)
221
- @output[cmd ? 1 : 3] = shell_split(val, escape: false, join: true) if (val = env('BUILD', suffix: 'OPTS'))
222
- @output[4] = shell_split(val, escape: false, join: true) if !cmd && (val = env('SCRIPT', suffix: 'OPTS'))
223
233
  end
224
234
  @version = val if (val = env('BUILD', suffix: 'VERSION'))
225
235
  return unless (val = env('BUILD', strict: true))
@@ -261,7 +271,7 @@ module Squared
261
271
  out, done = graph(args, out: [])
262
272
  out.map! do |val|
263
273
  done.each_with_index do |proj, i|
264
- next unless val =~ / #{Regexp.escape(proj.name)}(?:@\d|\z)/
274
+ next unless val.match?(/ #{Regexp.escape(proj.name)}(?:@\d|\z)/)
265
275
 
266
276
  val += " (#{i.succ})"
267
277
  break
@@ -275,6 +285,25 @@ module Squared
275
285
  ])
276
286
  end
277
287
  end
288
+ when 'unpack'
289
+ format_desc action, flag, 'tag/url,dir,digest?,f/force?'
290
+ task flag, [:tag, :dir, :digest, :force] do |_, args|
291
+ tag = param_guard(action, flag, args: args, key: :tag)
292
+ dir = param_guard(action, flag, args: args, key: :dir)
293
+ unless tag.match?(URI_SCHEME)
294
+ raise_error 'no base uri' unless @release
295
+
296
+ tag = "#{@release.include?('??') ? @release.sub('??', tag) : @release + tag}.#{flag}"
297
+ end
298
+ case (digest = args.digest)
299
+ when 'f', 'force'
300
+ digest = nil
301
+ force = true
302
+ else
303
+ force = args.fetch(:force, false)
304
+ end
305
+ unpack(basepath(dir), uri: tag, digest: digest, ext: flag.to_s, force: force)
306
+ end
278
307
  end
279
308
  end
280
309
  end
@@ -343,18 +372,19 @@ module Squared
343
372
 
344
373
  out = obj.link(self, *args, **kwargs, &blk) if obj.respond_to?(:link)
345
374
  if !out
346
- warn log_message(Logger::WARN, 'link not compatible', subject: obj.to_s, hint: name)
375
+ warn log_message(Logger::WARN, 'link not compatible', subject: obj.to_s, hint: name, pass: true)
347
376
  elsif out.respond_to?(:build)
348
377
  out.build
349
378
  end
350
379
  self
351
380
  end
352
381
 
353
- def build(*args, sync: invoked_sync?('build'), from: :run, **)
382
+ def build(*args, sync: invoked_sync?('build'), from: @buildtype || :build, **)
354
383
  banner = verbose
355
384
  if args.empty?
356
- return unless from == :run
385
+ return unless from == (@buildtype || :build)
357
386
 
387
+ run_b(@run, sync: sync, from: from) if series?(@run)
358
388
  args = @output
359
389
  banner = verbose == 1 if task_invoked?('build', 'build:sync')
360
390
  end
@@ -365,7 +395,7 @@ module Squared
365
395
  a, b, c, d, e = val
366
396
  case b
367
397
  when Hash
368
- b = append_hash(b).join(' ')
398
+ b = append_hash(b, build: true).join(' ')
369
399
  when Enumerable
370
400
  b = b.to_a.join(' ')
371
401
  end
@@ -381,7 +411,7 @@ module Squared
381
411
  end
382
412
  cmd = cmd.join(' && ')
383
413
  else
384
- cmd, opts, var, flags, scr = args
414
+ cmd, opts, var, flags, extra = args
385
415
  end
386
416
  if cmd
387
417
  cmd = as_get(cmd)
@@ -389,7 +419,7 @@ module Squared
389
419
  flags = append_hash(flags).join(' ') if flags.is_a?(Hash)
390
420
  case opts
391
421
  when Hash
392
- opts = append_hash(opts)
422
+ opts = append_hash(opts, build: true)
393
423
  cmd = as_a(cmd).push(flags).concat(opts).compact.join(' ')
394
424
  when Enumerable
395
425
  cmd = as_a(cmd).concat(opts.to_a)
@@ -399,9 +429,9 @@ module Squared
399
429
  cmd = [cmd, flags, opts].compact.join(' ') if opts || flags
400
430
  end
401
431
  else
402
- return unless respond_to?(:compose)
432
+ return unless (opts || extra) && respond_to?(:compose)
403
433
 
404
- cmd = compose(as_get(opts), flags, script: true, args: scr, from: from)
434
+ cmd = compose(as_get(opts), flags, script: true, args: extra, from: from)
405
435
  end
406
436
  run(cmd, var, from: from, banner: banner, sync: sync)
407
437
  end
@@ -410,6 +440,12 @@ module Squared
410
440
  run_b(@depend, sync: sync, from: :depend)
411
441
  end
412
442
 
443
+ def archive(*, sync: invoked_sync?('archive'), **)
444
+ return unless @archive.is_a?(Array)
445
+
446
+ unpack(path, **@archive, sync: sync, from: :archive)
447
+ end
448
+
413
449
  def doc(*, sync: invoked_sync?('doc'), **)
414
450
  run_b(@doc, sync: sync, from: :doc)
415
451
  end
@@ -439,27 +475,31 @@ module Squared
439
475
  rescue StandardError => e
440
476
  log&.error e
441
477
  ret = on(:error, from, e)
442
- raise if @exception && ret != true
478
+ raise if exception && ret != true
443
479
  end
444
- when Enumerable
445
- as_a(@clean).each do |val|
446
- val = val.to_s
447
- path = basepath(val)
448
- if path.directory? && val =~ %r{[\\/]\z}
449
- log&.warn "rm -rf #{path}"
450
- path.rmtree(verbose: verbose)
451
- else
452
- log&.warn "rm #{path}"
453
- (val.include?('*') ? Dir[path] : [path]).each do |file|
454
- next unless File.file?(file)
455
-
456
- begin
457
- File.delete(file)
458
- rescue StandardError => e
459
- log&.error e
480
+ else
481
+ if @clean.is_a?(Enumerable) && !series?(@clean)
482
+ as_a(@clean).each do |val|
483
+ val = val.to_s
484
+ path = basepath(val)
485
+ if path.directory? && val.match?(%r{[\\/]$})
486
+ log&.warn "rm -rf #{path}"
487
+ path.rmtree(verbose: verbose)
488
+ else
489
+ log&.warn "rm #{path}"
490
+ (val.include?('*') ? Dir[path] : [path]).each do |file|
491
+ next unless File.file?(file)
492
+
493
+ begin
494
+ File.delete(file)
495
+ rescue StandardError => e
496
+ log&.error e
497
+ end
460
498
  end
461
499
  end
462
500
  end
501
+ else
502
+ run_b(@clean, sync: sync)
463
503
  end
464
504
  end
465
505
  on :last, :clean
@@ -496,6 +536,119 @@ module Squared
496
536
  end
497
537
  end
498
538
 
539
+ def unpack(target, sync: true, uri: nil, digest: nil, ext: nil, force: false, depth: 1, headers: {},
540
+ from: :unpack)
541
+ if !target.exist?
542
+ target.mkpath
543
+ elsif !target.directory?
544
+ raise_error('invalid location', hint: target)
545
+ elsif !target.empty?
546
+ raise_error('directory not empty', hint: target) unless force || env('UNPACK_FORCE')
547
+ create = true
548
+ end
549
+ if digest
550
+ if (n = digest.index(':').to_i) > 0
551
+ size = digest[0, n].downcase
552
+ digest = digest[n + 1..-1]
553
+ else
554
+ size = digest.size
555
+ end
556
+ algo = case size
557
+ when 32, 'md5'
558
+ Digest::MD5
559
+ when 'rmd160'
560
+ Digest::RMD160
561
+ when 40, 'sha1'
562
+ Digest::SHA1
563
+ when 64, 'sha256'
564
+ Digest::SHA256
565
+ when 96, 'sha384'
566
+ Digest::SHA384
567
+ when 128, 'sha512'
568
+ Digest::SHA512
569
+ else
570
+ raise_error("invalid checksum: #{digest}", hint: name)
571
+ end
572
+ end
573
+ if (val = env('HEADERS')) && (out = parse_json(val, hint: "HEADERS_#{@envname}"))
574
+ headers = out
575
+ end
576
+ require 'open-uri'
577
+ data = nil
578
+ (uri = as_a(uri)).each_with_index do |url, index|
579
+ last = index == uri.size - 1
580
+ URI.open(url, headers) do |f|
581
+ data = f.read
582
+ if algo && algo.hexdigest(data) != digest
583
+ data = nil
584
+ raise_error("checksum failed: #{digest}", hint: url) if last
585
+ end
586
+ next if ext && index == 0
587
+
588
+ case f.content_type
589
+ when 'application/zip'
590
+ ext = 'zip'
591
+ when 'application/x-gzip'
592
+ ext = 'tgz'
593
+ when 'application/x-xz'
594
+ ext = 'txz'
595
+ end
596
+ end
597
+ if data
598
+ uri = url
599
+ break
600
+ elsif last
601
+ raise_error('no content', hint: url)
602
+ end
603
+ end
604
+ ext ||= URI.parse(uri).path[/^.+?\.((?:tar\.)?\w+)$/i, 1]
605
+ if (n = env("#{ext == 'zip' ? 'ZIP' : 'TAR'}_DEPTH", ignore: false))
606
+ depth = n.to_i
607
+ end
608
+ begin
609
+ require 'tempfile'
610
+ file = Tempfile.new("#{name}-")
611
+ file.write(data)
612
+ file.close
613
+ if create
614
+ warn log_message(Logger::WARN, name, 'force remove', hint: target, pass: true)
615
+ target.rmtree(verbose: true)
616
+ target.mkpath
617
+ end
618
+ case ext
619
+ when 'zip', 'aar'
620
+ session 'unzip', shell_quote(file.path), quote_option('d', target)
621
+ when 'tar', 'tgz', 'tar.gz', 'tar.xz'
622
+ flags = +(verbose ? 'v' : '')
623
+ case ext
624
+ when 'tgz', 'tar.gz'
625
+ flags += 'z'
626
+ when 'txz', 'tar.xz'
627
+ flags += 'J'
628
+ end
629
+ session 'tar', "-x#{flags}", basic_option('strip-components', depth), quote_option('f', file.path),
630
+ quote_option('C', target)
631
+ depth = 0
632
+ else
633
+ raise_error("unsupported format: #{ext}", hint: uri)
634
+ end
635
+ run(sync: sync, from: from)
636
+ while depth > 0 && target.children.size == 1
637
+ entry = target.children.first
638
+ break unless entry.directory?
639
+
640
+ dest = target.join(File.basename(file.path))
641
+ FileUtils.mv(entry, dest)
642
+ dest.children.each { |file| FileUtils.mv(file, target) }
643
+ dest.rmdir
644
+ target = entry
645
+ depth -= 1
646
+ end
647
+ ensure
648
+ file&.unlink
649
+ end
650
+ end
651
+
499
652
  def first(key, *args, **kwargs, &blk)
500
653
  event(:first, key, *args, **kwargs, &blk)
501
654
  end
@@ -508,17 +661,38 @@ module Squared
508
661
  event(:error, key, *args, **kwargs, &blk)
509
662
  end
510
663
 
511
- def event(name, key, *args, **kwargs, &blk)
512
- (@events[name.to_sym] ||= {})[key.to_sym] = [block_given? ? [blk] + args : args, kwargs]
664
+ def event(name, key, *args, override: false, **kwargs, &blk)
665
+ data = @events[name.to_sym] ||= {}
666
+ items = if override
667
+ data[key.to_sym] = []
668
+ else
669
+ data[key.to_sym] ||= []
670
+ end
671
+ items << [block_given? ? [blk] + args : args, kwargs]
672
+ self
513
673
  end
514
674
 
515
675
  def as(cmd, script, to = nil)
516
676
  script = { "#{script}": to } if to
517
677
  data = (@as ||= {})[cmd.to_sym] ||= {}
518
678
  script.each { |key, val| data[key.to_s] = val }
679
+ self
519
680
  end
520
681
 
521
- def variable_set(key, *val, **kwargs)
682
+ def series(key, override: false, &blk)
683
+ if blocks.include?(key.to_sym) && block_given?
684
+ if !override && series?(target = instance_variable_get(:"@#{key}"))
685
+ target << blk
686
+ else
687
+ instance_variable_set :"@#{key}", [blk]
688
+ end
689
+ else
690
+ log&.warn "series: @#{key} (invalid)"
691
+ end
692
+ self
693
+ end
694
+
695
+ def variable_set(key, *val, **kwargs, &blk)
522
696
  if variables.include?(key)
523
697
  case key
524
698
  when :build, :run
@@ -539,17 +713,22 @@ module Squared
539
713
  when :dependfile
540
714
  @dependfile = basepath(*val)
541
715
  else
542
- instance_variable_set :"@#{key}", val.first
716
+ if blocks.include?(key) && block_given?
717
+ series key, &blk
718
+ else
719
+ instance_variable_set(:"@#{key}", block_given? && val.empty? ? blk : val.first)
720
+ end
543
721
  end
544
722
  else
545
723
  log&.warn "variable_set: @#{key} (private)"
546
724
  end
725
+ self
547
726
  end
548
727
 
549
728
  def enabled?(ref = nil, **)
550
729
  return false if ref && !ref?(ref)
551
730
 
552
- path.directory? && !path.empty?
731
+ (path.directory? && !path.empty?) || archive?
553
732
  end
554
733
 
555
734
  def has?(meth, ref = nil)
@@ -563,7 +742,7 @@ module Squared
563
742
  end
564
743
 
565
744
  def build?
566
- !!@output[0] || script?
745
+ !!@output[0] || script? || series?(@run)
567
746
  end
568
747
 
569
748
  def script?
@@ -574,6 +753,10 @@ module Squared
574
753
  !!@depend
575
754
  end
576
755
 
756
+ def archive?
757
+ @archive.is_a?(Hash) && (!path.exist? || path.empty?)
758
+ end
759
+
577
760
  def graph?
578
761
  @graph.is_a?(Array) && !@graph.empty?
579
762
  end
@@ -634,8 +817,12 @@ module Squared
634
817
  @ref.reverse_each
635
818
  end
636
819
 
637
- def basepath(*args, ascend: nil)
638
- ret = path.join(*args)
820
+ def basepath(*args)
821
+ path.join(*args)
822
+ end
823
+
824
+ def rootpath(*args, ascend: nil)
825
+ ret = basepath(*args)
639
826
  return ret unless ascend && !ret.exist?
640
827
 
641
828
  path.parent.ascend.each do |dir|
@@ -672,7 +859,7 @@ module Squared
672
859
  unless cmd
673
860
  if warning?
674
861
  from &&= from.to_s
675
- warn log_message(Logger::WARN, from || 'unknown', subject: project, hint: 'no command given')
862
+ warn log_message(Logger::WARN, from || 'unknown', subject: project, hint: 'no command given', pass: true)
676
863
  end
677
864
  return
678
865
  end
@@ -709,18 +896,23 @@ module Squared
709
896
  end
710
897
 
711
898
  def run_b(obj, from: nil, sync: true)
712
- return unless obj
713
-
714
- if obj.is_a?(Array) && obj.any? { |val| !val.is_a?(String) }
715
- build(*obj, from: from, sync: sync)
899
+ case obj
900
+ when Proc, Method
901
+ obj.call
716
902
  else
717
- run_s(obj.is_a?(Enumerable) ? obj.to_a : obj, from: from, sync: sync)
903
+ if series?(obj)
904
+ obj.each(&:call)
905
+ elsif obj.is_a?(Array) && obj.any? { |val| !val.is_a?(String) }
906
+ build(*obj, from: from, sync: sync)
907
+ elsif obj
908
+ run_s(obj.is_a?(Enumerable) ? obj.to_a : obj, from: from, sync: sync)
909
+ end
718
910
  end
719
911
  end
720
912
 
721
913
  def graph_branch(target, data, tasks = nil, out = nil, sync: true, pass: [], done: [], depth: 0,
722
914
  single: false, last: false, context: nil)
723
- tag = ->(proj) { "#{proj.name}#{SEM_VER =~ proj.version ? "@#{proj.version}" : ''}" }
915
+ tag = ->(proj) { "#{proj.name}#{SEM_VER.match?(proj.version) ? "@#{proj.version}" : ''}" }
724
916
  check = ->(deps) { deps.reject { |val| done.include?(val) } }
725
917
  dedupe = lambda do |name|
726
918
  next [] unless (ret = data[name])
@@ -861,8 +1053,7 @@ module Squared
861
1053
  def session_delete(*list, target: @session)
862
1054
  ret = []
863
1055
  list.each do |val|
864
- pat = /^#{Regexp.escape(shell_option(val))}(?: |=|$)/
865
- if (key = target.find { |opt| opt =~ pat })
1056
+ if (key = target.find { |opt| opt.match?(/^#{Regexp.escape(shell_option(val))}(?: |=|$)/o) })
866
1057
  target.delete(key)
867
1058
  ret << key
868
1059
  end
@@ -877,7 +1068,7 @@ module Squared
877
1068
  def session_done(cmd)
878
1069
  return cmd unless cmd.respond_to?(:done)
879
1070
 
880
- raise_error('no args were added', hint: cmd.first || name) unless cmd.size > 1
1071
+ raise_error('no args added', hint: cmd.first || name) unless cmd.size > 1
881
1072
  @session = nil if cmd == @session
882
1073
  cmd.done
883
1074
  end
@@ -898,9 +1089,12 @@ module Squared
898
1089
  bare = []
899
1090
  e = []
900
1091
  b = []
1092
+ m = []
901
1093
  p = []
902
1094
  q = []
1095
+ qq = []
903
1096
  i = []
1097
+ f = []
904
1098
  list = list.map do |val|
905
1099
  x, y = val.split('|')
906
1100
  if y
@@ -920,12 +1114,17 @@ module Squared
920
1114
  e << flag
921
1115
  when 'b'
922
1116
  b << flag
1117
+ when 'm'
1118
+ m << flag
923
1119
  when 'q'
1120
+ qq << flag if val[n + 2] == 'q'
924
1121
  q << flag
925
1122
  when 'p'
926
1123
  p << flag
927
1124
  when 'i'
928
1125
  i << flag
1126
+ when 'f'
1127
+ f << flag
929
1128
  else
930
1129
  reg << Regexp.escape(flag)
931
1130
  end
@@ -946,23 +1145,27 @@ module Squared
946
1145
  target << "--no-#{name}"
947
1146
  else
948
1147
  if opt =~ /^([^=]+)=(.+)$/
949
- a = $1
950
- if e.include?(a)
951
- target << shell_option(a, $2)
952
- elsif q.include?(a)
953
- target << quote_option(a, $2)
954
- elsif p.include?(a)
955
- target << quote_option(a, basepath($2))
956
- elsif b.include?(a) || (i.include?(a) && /^\d+$/.match?($2))
957
- target << basic_option(a, $2)
1148
+ key = $1
1149
+ val = $2
1150
+ match = ->(flag, pat) { flag.include?(key) && pat.match?(val) }
1151
+ if e.include?(key)
1152
+ target << shell_option(key, val)
1153
+ elsif q.include?(key)
1154
+ target << quote_option(key, val, double: qq.include?(key))
1155
+ elsif p.include?(key)
1156
+ target << quote_option(key, basepath(val))
1157
+ elsif m.include?(key)
1158
+ target << basic_option(key, val, merge: true)
1159
+ elsif b.include?(key) || match.(i, /^\d+$/) || match.(f, /^\d*(?:\.\d+)?$/)
1160
+ target << basic_option(key, val)
958
1161
  else
959
1162
  ret << opt
960
1163
  end
961
- opt = a
1164
+ opt = key
962
1165
  else
963
1166
  ret << opt
964
1167
  end
965
- found = true if first && pass.none? { |val| opt.include?(val) }
1168
+ found = true if first && pass.none? { |s| opt.include?(s) }
966
1169
  end
967
1170
  end
968
1171
  [ret, reg.empty? ? /\A\s+\z/ : /^(#{reg.join('|')})=(.+)$/]
@@ -971,7 +1174,7 @@ module Squared
971
1174
  def option_clear(opts, target: @session, **kwargs)
972
1175
  kwargs[:subject] ||= target&.first
973
1176
  kwargs[:hint] ||= 'not used'
974
- warn log_message(Logger::WARN, opts.join(', '), **kwargs) unless opts.empty?
1177
+ warn log_message(Logger::WARN, opts.join(', '), pass: true, **kwargs) unless opts.empty?
975
1178
  end
976
1179
 
977
1180
  def print_item(*val)
@@ -1001,7 +1204,7 @@ module Squared
1001
1204
  val
1002
1205
  end
1003
1206
  end
1004
- out << sub_style('-' * n, styles: border)
1207
+ out << sub_style(ARG[:BORDER][1] * n, styles: border)
1005
1208
  out.join("\n")
1006
1209
  end
1007
1210
 
@@ -1013,7 +1216,7 @@ module Squared
1013
1216
  sub.each { |h| s = sub_style(s, **h) }
1014
1217
  s
1015
1218
  end
1016
- ret = [sub_style('-' * n, styles: kwargs.key?(:border) ? kwargs[:border] : borderstyle), *lines]
1219
+ ret = [sub_style(ARG[:BORDER][1] * n, styles: kwargs.key?(:border) ? kwargs[:border] : borderstyle), *lines]
1017
1220
  ret.reverse! if reverse
1018
1221
  ret.join("\n")
1019
1222
  end
@@ -1039,7 +1242,7 @@ module Squared
1039
1242
  if verbose
1040
1243
  out = []
1041
1244
  if data[:command]
1042
- if /\A(?:"((?:[^"]|(?<=\\)")+)"|'((?:[^']|(?<=\\)')+)'|(\S+)) /.match(cmd)
1245
+ if cmd =~ /\A(?:"((?:[^"]|(?<=\\)")+)"|'((?:[^']|(?<=\\)')+)'|(\S+)) /
1043
1246
  path = $3 || $2 || $1
1044
1247
  cmd = cmd.sub(path, File.basename(path).upcase)
1045
1248
  end
@@ -1077,9 +1280,9 @@ module Squared
1077
1280
  unless items.empty?
1078
1281
  pad = items.size.to_s.size
1079
1282
  items.each_with_index do |val, i|
1080
- next unless reg.empty? || reg.any? { |pat| val[0] =~ pat }
1283
+ next unless reg.empty? || reg.any? { |pat| val[0].match?(pat) }
1081
1284
 
1082
- out << "#{(i + 1).to_s.rjust(pad)}. #{each ? each.(val) : val[0]}"
1285
+ out << "#{i.succ.to_s.rjust(pad)}. #{each ? each.(val) : val[0]}"
1083
1286
  end
1084
1287
  end
1085
1288
  sub = [headerstyle]
@@ -1113,9 +1316,9 @@ module Squared
1113
1316
  opts.each { |val| target << shell_option(flag, val) }
1114
1317
  end
1115
1318
 
1116
- def append_hash(data, target: @session)
1319
+ def append_hash(data, target: @session, build: false)
1117
1320
  target ||= []
1118
- if (type = env('BUILD', suffix: 'TYPE') || ENV['BUILD_TYPE'])
1321
+ if build && (type = env('BUILD', suffix: 'TYPE') || ENV['BUILD_TYPE'])
1119
1322
  type = "__#{type}__"
1120
1323
  if (extra = data[type] || data[type.to_sym]).is_a?(Hash)
1121
1324
  data = data.merge(extra)
@@ -1144,7 +1347,7 @@ module Squared
1144
1347
  target
1145
1348
  end
1146
1349
 
1147
- def append_any(val, target: @session, delim: false)
1350
+ def append_any(val, target: @session, build: false, delim: false)
1148
1351
  if delim && !target.include?('--')
1149
1352
  target << '--'
1150
1353
  else
@@ -1154,7 +1357,7 @@ module Squared
1154
1357
  when String
1155
1358
  target << val
1156
1359
  when Hash
1157
- append_hash(val, target: target)
1360
+ append_hash(val, target: target, build: build)
1158
1361
  when Enumerable
1159
1362
  if target.is_a?(Array)
1160
1363
  target.concat(val.to_a)
@@ -1163,6 +1366,7 @@ module Squared
1163
1366
  end
1164
1367
  else
1165
1368
  target.delete('--') if delim
1369
+ nil
1166
1370
  end
1167
1371
  end
1168
1372
 
@@ -1171,8 +1375,9 @@ module Squared
1171
1375
 
1172
1376
  target << '--' if delim && !target.include?('--')
1173
1377
  list.map do |val|
1174
- target << (val = escape ? shell_escape(val, quote: quote) : shell_quote(val))
1175
- val
1378
+ item = escape ? shell_escape(val, quote: quote) : shell_quote(val)
1379
+ target << item
1380
+ item
1176
1381
  end
1177
1382
  end
1178
1383
 
@@ -1180,7 +1385,7 @@ module Squared
1180
1385
  **kwargs)
1181
1386
  return if (list = list.flatten).empty?
1182
1387
 
1183
- list.flatten.each do |opt|
1388
+ list.each do |opt|
1184
1389
  next unless (val = option(opt, **kwargs))
1185
1390
 
1186
1391
  return target << (if flag
@@ -1197,7 +1402,7 @@ module Squared
1197
1402
  return if (list = list.flatten).empty?
1198
1403
 
1199
1404
  ret = []
1200
- list.flatten.each do |flag|
1405
+ list.each do |flag|
1201
1406
  next unless (val = option(flag, **kwargs))
1202
1407
 
1203
1408
  if val == '0' && no
@@ -1213,16 +1418,63 @@ module Squared
1213
1418
  target << '--no-color' if !ARG[:COLOR] || stdin? || option('no-color', ignore: false)
1214
1419
  end
1215
1420
 
1421
+ def merge_opts(base, data)
1422
+ return data unless base
1423
+ return base unless data
1424
+
1425
+ ret = case data
1426
+ when String
1427
+ case base
1428
+ when String
1429
+ "#{base} #{data}"
1430
+ when Hash
1431
+ "#{append_hash(base).join(' ')} #{data}"
1432
+ when Enumerable
1433
+ "#{base.to_a.join(' ')} #{data}"
1434
+ end
1435
+ when Hash
1436
+ case base
1437
+ when String
1438
+ "#{base} #{append_hash(data).join(' ')}"
1439
+ when Hash
1440
+ base.merge(data)
1441
+ when Enumerable
1442
+ base.to_a + append_hash(data)
1443
+ end
1444
+ when Enumerable
1445
+ case base
1446
+ when String
1447
+ "#{base} #{data.to_a.join(' ')}"
1448
+ when Hash
1449
+ "#{append_hash(base).join(' ')} #{data.to_a.join(' ')}"
1450
+ when Enumerable
1451
+ base.to_a + data.to_a
1452
+ end
1453
+ else
1454
+ base
1455
+ end
1456
+ ret || data
1457
+ end
1458
+
1216
1459
  def collect_hash(data, pass: [])
1217
1460
  ret = []
1218
1461
  data.each { |key, val| ret += val unless pass.include?(key) }
1219
1462
  ret
1220
1463
  end
1221
1464
 
1222
- def param_guard(action, flag, args: nil, key: nil, pat: nil)
1465
+ def parse_json(val, hint: nil)
1466
+ ret = JSON.parse(val)
1467
+ raise_error('invalid JSON object', val, hint: hint) unless ret.is_a?(Hash)
1468
+ rescue StandardError => e
1469
+ log&.warn e
1470
+ else
1471
+ ret
1472
+ end
1473
+
1474
+ def param_guard(action, flag, args: nil, key: nil, pat: nil, values: nil)
1223
1475
  if args && key
1224
1476
  val = args[key]
1225
- return val unless val.nil? || (pat && !val.match?(pat))
1477
+ return val unless val.nil? || (pat && !val.match?(pat)) || (values && !values.include?(val))
1226
1478
 
1227
1479
  @session = nil
1228
1480
  raise_error(action, "#{flag}[#{key}]", hint: val.nil? ? 'missing' : 'invalid')
@@ -1233,6 +1485,17 @@ module Squared
1233
1485
  args
1234
1486
  end
1235
1487
 
1488
+ def relativepath(*files, all: false)
1489
+ return [] if files.empty?
1490
+
1491
+ files.flatten.map { |val| Pathname.new(val) }.select { |val| projectpath?(val) }.map do |val|
1492
+ val = val.absolute? ? val.to_s.sub(/^#{Regexp.escape(File.join(path, ''))}/o, '') : val.to_s
1493
+ val = val[2..-1] if val.start_with?('./')
1494
+ val = "#{val}*" if all && val.end_with?('/')
1495
+ val
1496
+ end
1497
+ end
1498
+
1236
1499
  def projectmap(files, parent: false)
1237
1500
  files = files.select { |val| projectpath?(val) } unless parent
1238
1501
  files.map { |val| val == '.' ? '.' : shell_quote(basepath(val.strip)) }
@@ -1274,22 +1537,28 @@ module Squared
1274
1537
  val.compact.map { |s| color(s) }.flatten
1275
1538
  end
1276
1539
 
1540
+ def epochtime
1541
+ DateTime.now.strftime('%Q').to_i
1542
+ end
1543
+
1277
1544
  def on(event, from, *args, **kwargs)
1278
- return unless from && (obj = @events[event][from])
1545
+ return unless from
1279
1546
 
1280
- if obj.is_a?(Array) && obj[1].is_a?(Hash)
1281
- opts = kwargs.empty? ? obj[1] : obj[1].merge(kwargs)
1282
- target = obj[0]
1283
- else
1284
- opts = kwargs
1285
- target = obj
1286
- end
1287
- as_a(target, flat: true).each do |cmd|
1288
- case cmd
1289
- when Proc, Method
1290
- cmd.call(*args, **opts)
1291
- when String
1292
- run(cmd, **opts)
1547
+ @events[event][from]&.each do |obj|
1548
+ if obj.is_a?(Array) && obj[1].is_a?(Hash)
1549
+ opts = kwargs.empty? ? obj[1] : obj[1].merge(kwargs)
1550
+ target = obj[0]
1551
+ else
1552
+ opts = kwargs
1553
+ target = obj
1554
+ end
1555
+ as_a(target, flat: true).each do |cmd|
1556
+ case cmd
1557
+ when Proc, Method
1558
+ cmd.call(*args, **opts)
1559
+ when String
1560
+ run(cmd, **opts)
1561
+ end
1293
1562
  end
1294
1563
  end
1295
1564
  end
@@ -1330,23 +1599,53 @@ module Squared
1330
1599
  end
1331
1600
 
1332
1601
  def run_set(cmd, val = nil, opts: nil, **)
1333
- return @output = cmd.dup if cmd.is_a?(Array)
1334
-
1335
- unless @output[1] == false && !@output[0].nil?
1602
+ diso = @output[1] == false && !@output[0].nil?
1603
+ dise = @output[2] == false
1604
+ parse = lambda do |data|
1605
+ ret = []
1606
+ if data[:command]
1607
+ ret[0] = data[:command]
1608
+ ret[1] = data[:opts] unless diso
1609
+ ret[3] = data[:args]
1610
+ elsif data[:script]
1611
+ ret[1] = data[:script]
1612
+ ret[3] = data[:opts]
1613
+ ret[4] = data[:args]
1614
+ else
1615
+ ret[0] = false
1616
+ end
1617
+ ret[2] = data[:env] unless dise
1618
+ ret
1619
+ end
1620
+ case cmd
1621
+ when Array
1622
+ @output = if cmd.all? { |data| data.is_a?(Hash) }
1623
+ diso = false
1624
+ dise = false
1625
+ cmd.map { |data| parse.(data) }
1626
+ else
1627
+ cmd.dup
1628
+ end
1629
+ return
1630
+ when Hash
1631
+ @output = parse.(data)
1632
+ else
1633
+ @output[0] = cmd
1634
+ end
1635
+ unless diso
1336
1636
  if opts == false
1337
1637
  @output[1] = false
1338
1638
  elsif opts && opts != true
1339
1639
  @output[1] = opts
1340
1640
  end
1341
1641
  end
1342
- unless @output[2] == false
1343
- if val.is_a?(Hash)
1344
- @output[2] = val
1345
- elsif val == false
1346
- @output[2] = false
1347
- end
1642
+ return if dise
1643
+
1644
+ if val.is_a?(Hash)
1645
+ @output[2] = val
1646
+ elsif val == false
1647
+ @output[2] = false
1348
1648
  end
1349
- @output[0] = cmd
1350
1649
  end
1351
1650
 
1352
1651
  def script_set(cmd, prod: nil, args: nil, **)
@@ -1362,6 +1661,8 @@ module Squared
1362
1661
  end
1363
1662
 
1364
1663
  def as_get(val)
1664
+ return unless val
1665
+
1365
1666
  @global && (ret = @as && @as[from] && @as[from][val]) ? ret : val
1366
1667
  end
1367
1668
 
@@ -1396,19 +1697,20 @@ module Squared
1396
1697
 
1397
1698
  def runnable?(val)
1398
1699
  case val
1399
- when String
1700
+ when String, Enumerable, Proc, Method
1400
1701
  true
1401
- when Enumerable
1402
- !val.is_a?(Hash)
1403
1702
  else
1404
1703
  false
1405
1704
  end
1406
1705
  end
1407
1706
 
1707
+ def series?(val)
1708
+ val.is_a?(Array) && val.all? { |p| p.is_a?(Proc) || p.is_a?(Method) }
1709
+ end
1710
+
1408
1711
  def session_arg?(*list, target: @session, value: false)
1409
1712
  list.any? do |val|
1410
- pat = /^#{Regexp.escape(shell_option(val))}#{value ? '[ =].' : '(?:[ =]|$)'}/
1411
- target.any? { |opt| opt =~ pat }
1713
+ target.any? { |opt| opt.match?(/^#{Regexp.escape(shell_option(val))}#{value ? '[ =].' : '(?:[ =]|$)'}/o) }
1412
1714
  end
1413
1715
  end
1414
1716
 
@@ -1445,6 +1747,10 @@ module Squared
1445
1747
  Base.tasks + VAR_SET
1446
1748
  end
1447
1749
 
1750
+ def blocks
1751
+ BLK_SET
1752
+ end
1753
+
1448
1754
  def borderstyle
1449
1755
  ((data = workspace.banner_get(*@ref, group: group)) && data[:border]) || theme[:border]
1450
1756
  end