squared 0.3.6 → 0.4.1

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])
@@ -867,8 +1059,7 @@ module Squared
867
1059
  def session_delete(*list, target: @session)
868
1060
  ret = []
869
1061
  list.each do |val|
870
- pat = /^#{Regexp.escape(shell_option(val))}(?: |=|$)/
871
- if (key = target.find { |opt| opt =~ pat })
1062
+ if (key = target.find { |opt| opt.match?(/^#{Regexp.escape(shell_option(val))}(?: |=|$)/o) })
872
1063
  target.delete(key)
873
1064
  ret << key
874
1065
  end
@@ -883,7 +1074,7 @@ module Squared
883
1074
  def session_done(cmd)
884
1075
  return cmd unless cmd.respond_to?(:done)
885
1076
 
886
- raise_error('no args were added', hint: cmd.first || name) unless cmd.size > 1
1077
+ raise_error('no args added', hint: cmd.first || name) unless cmd.size > 1
887
1078
  @session = nil if cmd == @session
888
1079
  cmd.done
889
1080
  end
@@ -904,9 +1095,12 @@ module Squared
904
1095
  bare = []
905
1096
  e = []
906
1097
  b = []
1098
+ m = []
907
1099
  p = []
908
1100
  q = []
1101
+ qq = []
909
1102
  i = []
1103
+ f = []
910
1104
  list = list.map do |val|
911
1105
  x, y = val.split('|')
912
1106
  if y
@@ -926,12 +1120,17 @@ module Squared
926
1120
  e << flag
927
1121
  when 'b'
928
1122
  b << flag
1123
+ when 'm'
1124
+ m << flag
929
1125
  when 'q'
1126
+ qq << flag if val[n + 2] == 'q'
930
1127
  q << flag
931
1128
  when 'p'
932
1129
  p << flag
933
1130
  when 'i'
934
1131
  i << flag
1132
+ when 'f'
1133
+ f << flag
935
1134
  else
936
1135
  reg << Regexp.escape(flag)
937
1136
  end
@@ -952,23 +1151,27 @@ module Squared
952
1151
  target << "--no-#{name}"
953
1152
  else
954
1153
  if opt =~ /^([^=]+)=(.+)$/
955
- a = $1
956
- if e.include?(a)
957
- target << shell_option(a, $2)
958
- elsif q.include?(a)
959
- target << quote_option(a, $2)
960
- elsif p.include?(a)
961
- target << quote_option(a, basepath($2))
962
- elsif b.include?(a) || (i.include?(a) && /^\d+$/.match?($2))
963
- target << basic_option(a, $2)
1154
+ key = $1
1155
+ val = $2
1156
+ match = ->(flag, pat) { flag.include?(key) && pat.match?(val) }
1157
+ if e.include?(key)
1158
+ target << shell_option(key, val)
1159
+ elsif q.include?(key)
1160
+ target << quote_option(key, val, double: qq.include?(key))
1161
+ elsif p.include?(key)
1162
+ target << quote_option(key, basepath(val))
1163
+ elsif m.include?(key)
1164
+ target << basic_option(key, val, merge: true)
1165
+ elsif b.include?(key) || match.(i, /^\d+$/) || match.(f, /^\d*(?:\.\d+)?$/)
1166
+ target << basic_option(key, val)
964
1167
  else
965
1168
  ret << opt
966
1169
  end
967
- opt = a
1170
+ opt = key
968
1171
  else
969
1172
  ret << opt
970
1173
  end
971
- found = true if first && pass.none? { |val| opt.include?(val) }
1174
+ found = true if first && pass.none? { |s| opt.include?(s) }
972
1175
  end
973
1176
  end
974
1177
  [ret, reg.empty? ? /\A\s+\z/ : /^(#{reg.join('|')})=(.+)$/]
@@ -977,7 +1180,7 @@ module Squared
977
1180
  def option_clear(opts, target: @session, **kwargs)
978
1181
  kwargs[:subject] ||= target&.first
979
1182
  kwargs[:hint] ||= 'not used'
980
- warn log_message(Logger::WARN, opts.join(', '), **kwargs) unless opts.empty?
1183
+ warn log_message(Logger::WARN, opts.join(', '), pass: true, **kwargs) unless opts.empty?
981
1184
  end
982
1185
 
983
1186
  def print_item(*val)
@@ -1007,7 +1210,7 @@ module Squared
1007
1210
  val
1008
1211
  end
1009
1212
  end
1010
- out << sub_style('-' * n, styles: border)
1213
+ out << sub_style(ARG[:BORDER][1] * n, styles: border)
1011
1214
  out.join("\n")
1012
1215
  end
1013
1216
 
@@ -1019,7 +1222,7 @@ module Squared
1019
1222
  sub.each { |h| s = sub_style(s, **h) }
1020
1223
  s
1021
1224
  end
1022
- ret = [sub_style('-' * n, styles: kwargs.key?(:border) ? kwargs[:border] : borderstyle), *lines]
1225
+ ret = [sub_style(ARG[:BORDER][1] * n, styles: kwargs.key?(:border) ? kwargs[:border] : borderstyle), *lines]
1023
1226
  ret.reverse! if reverse
1024
1227
  ret.join("\n")
1025
1228
  end
@@ -1045,7 +1248,7 @@ module Squared
1045
1248
  if verbose
1046
1249
  out = []
1047
1250
  if data[:command]
1048
- if /\A(?:"((?:[^"]|(?<=\\)")+)"|'((?:[^']|(?<=\\)')+)'|(\S+)) /.match(cmd)
1251
+ if cmd =~ /\A(?:"((?:[^"]|(?<=\\)")+)"|'((?:[^']|(?<=\\)')+)'|(\S+)) /
1049
1252
  path = $3 || $2 || $1
1050
1253
  cmd = cmd.sub(path, File.basename(path).upcase)
1051
1254
  end
@@ -1083,9 +1286,9 @@ module Squared
1083
1286
  unless items.empty?
1084
1287
  pad = items.size.to_s.size
1085
1288
  items.each_with_index do |val, i|
1086
- next unless reg.empty? || reg.any? { |pat| val[0] =~ pat }
1289
+ next unless reg.empty? || reg.any? { |pat| val[0].match?(pat) }
1087
1290
 
1088
- out << "#{(i + 1).to_s.rjust(pad)}. #{each ? each.(val) : val[0]}"
1291
+ out << "#{i.succ.to_s.rjust(pad)}. #{each ? each.(val) : val[0]}"
1089
1292
  end
1090
1293
  end
1091
1294
  sub = [headerstyle]
@@ -1119,9 +1322,9 @@ module Squared
1119
1322
  opts.each { |val| target << shell_option(flag, val) }
1120
1323
  end
1121
1324
 
1122
- def append_hash(data, target: @session)
1325
+ def append_hash(data, target: @session, build: false)
1123
1326
  target ||= []
1124
- if (type = env('BUILD', suffix: 'TYPE') || ENV['BUILD_TYPE'])
1327
+ if build && (type = env('BUILD', suffix: 'TYPE') || ENV['BUILD_TYPE'])
1125
1328
  type = "__#{type}__"
1126
1329
  if (extra = data[type] || data[type.to_sym]).is_a?(Hash)
1127
1330
  data = data.merge(extra)
@@ -1150,7 +1353,7 @@ module Squared
1150
1353
  target
1151
1354
  end
1152
1355
 
1153
- def append_any(val, target: @session, delim: false)
1356
+ def append_any(val, target: @session, build: false, delim: false)
1154
1357
  if delim && !target.include?('--')
1155
1358
  target << '--'
1156
1359
  else
@@ -1160,7 +1363,7 @@ module Squared
1160
1363
  when String
1161
1364
  target << val
1162
1365
  when Hash
1163
- append_hash(val, target: target)
1366
+ append_hash(val, target: target, build: build)
1164
1367
  when Enumerable
1165
1368
  if target.is_a?(Array)
1166
1369
  target.concat(val.to_a)
@@ -1169,6 +1372,7 @@ module Squared
1169
1372
  end
1170
1373
  else
1171
1374
  target.delete('--') if delim
1375
+ nil
1172
1376
  end
1173
1377
  end
1174
1378
 
@@ -1177,8 +1381,9 @@ module Squared
1177
1381
 
1178
1382
  target << '--' if delim && !target.include?('--')
1179
1383
  list.map do |val|
1180
- target << (val = escape ? shell_escape(val, quote: quote) : shell_quote(val))
1181
- val
1384
+ item = escape ? shell_escape(val, quote: quote) : shell_quote(val)
1385
+ target << item
1386
+ item
1182
1387
  end
1183
1388
  end
1184
1389
 
@@ -1186,7 +1391,7 @@ module Squared
1186
1391
  **kwargs)
1187
1392
  return if (list = list.flatten).empty?
1188
1393
 
1189
- list.flatten.each do |opt|
1394
+ list.each do |opt|
1190
1395
  next unless (val = option(opt, **kwargs))
1191
1396
 
1192
1397
  return target << (if flag
@@ -1203,7 +1408,7 @@ module Squared
1203
1408
  return if (list = list.flatten).empty?
1204
1409
 
1205
1410
  ret = []
1206
- list.flatten.each do |flag|
1411
+ list.each do |flag|
1207
1412
  next unless (val = option(flag, **kwargs))
1208
1413
 
1209
1414
  if val == '0' && no
@@ -1219,16 +1424,63 @@ module Squared
1219
1424
  target << '--no-color' if !ARG[:COLOR] || stdin? || option('no-color', ignore: false)
1220
1425
  end
1221
1426
 
1427
+ def merge_opts(base, data)
1428
+ return data unless base
1429
+ return base unless data
1430
+
1431
+ ret = case data
1432
+ when String
1433
+ case base
1434
+ when String
1435
+ "#{base} #{data}"
1436
+ when Hash
1437
+ "#{append_hash(base).join(' ')} #{data}"
1438
+ when Enumerable
1439
+ "#{base.to_a.join(' ')} #{data}"
1440
+ end
1441
+ when Hash
1442
+ case base
1443
+ when String
1444
+ "#{base} #{append_hash(data).join(' ')}"
1445
+ when Hash
1446
+ base.merge(data)
1447
+ when Enumerable
1448
+ base.to_a + append_hash(data)
1449
+ end
1450
+ when Enumerable
1451
+ case base
1452
+ when String
1453
+ "#{base} #{data.to_a.join(' ')}"
1454
+ when Hash
1455
+ "#{append_hash(base).join(' ')} #{data.to_a.join(' ')}"
1456
+ when Enumerable
1457
+ base.to_a + data.to_a
1458
+ end
1459
+ else
1460
+ base
1461
+ end
1462
+ ret || data
1463
+ end
1464
+
1222
1465
  def collect_hash(data, pass: [])
1223
1466
  ret = []
1224
1467
  data.each { |key, val| ret += val unless pass.include?(key) }
1225
1468
  ret
1226
1469
  end
1227
1470
 
1228
- def param_guard(action, flag, args: nil, key: nil, pat: nil)
1471
+ def parse_json(val, hint: nil)
1472
+ ret = JSON.parse(val)
1473
+ raise_error('invalid JSON object', val, hint: hint) unless ret.is_a?(Hash)
1474
+ rescue StandardError => e
1475
+ log&.warn e
1476
+ else
1477
+ ret
1478
+ end
1479
+
1480
+ def param_guard(action, flag, args: nil, key: nil, pat: nil, values: nil)
1229
1481
  if args && key
1230
1482
  val = args[key]
1231
- return val unless val.nil? || (pat && !val.match?(pat))
1483
+ return val unless val.nil? || (pat && !val.match?(pat)) || (values && !values.include?(val))
1232
1484
 
1233
1485
  @session = nil
1234
1486
  raise_error(action, "#{flag}[#{key}]", hint: val.nil? ? 'missing' : 'invalid')
@@ -1239,6 +1491,17 @@ module Squared
1239
1491
  args
1240
1492
  end
1241
1493
 
1494
+ def relativepath(*files, all: false)
1495
+ return [] if files.empty?
1496
+
1497
+ files.flatten.map { |val| Pathname.new(val) }.select { |val| projectpath?(val) }.map do |val|
1498
+ val = val.absolute? ? val.to_s.sub(/^#{Regexp.escape(File.join(path, ''))}/o, '') : val.to_s
1499
+ val = val[2..-1] if val.start_with?('./')
1500
+ val = "#{val}*" if all && val.end_with?('/')
1501
+ val
1502
+ end
1503
+ end
1504
+
1242
1505
  def projectmap(files, parent: false)
1243
1506
  files = files.select { |val| projectpath?(val) } unless parent
1244
1507
  files.map { |val| val == '.' ? '.' : shell_quote(basepath(val.strip)) }
@@ -1280,22 +1543,28 @@ module Squared
1280
1543
  val.compact.map { |s| color(s) }.flatten
1281
1544
  end
1282
1545
 
1546
+ def epochtime
1547
+ DateTime.now.strftime('%Q').to_i
1548
+ end
1549
+
1283
1550
  def on(event, from, *args, **kwargs)
1284
- return unless from && (obj = @events[event][from])
1551
+ return unless from
1285
1552
 
1286
- if obj.is_a?(Array) && obj[1].is_a?(Hash)
1287
- opts = kwargs.empty? ? obj[1] : obj[1].merge(kwargs)
1288
- target = obj[0]
1289
- else
1290
- opts = kwargs
1291
- target = obj
1292
- end
1293
- as_a(target, flat: true).each do |cmd|
1294
- case cmd
1295
- when Proc, Method
1296
- cmd.call(*args, **opts)
1297
- when String
1298
- run(cmd, **opts)
1553
+ @events[event][from]&.each do |obj|
1554
+ if obj.is_a?(Array) && obj[1].is_a?(Hash)
1555
+ opts = kwargs.empty? ? obj[1] : obj[1].merge(kwargs)
1556
+ target = obj[0]
1557
+ else
1558
+ opts = kwargs
1559
+ target = obj
1560
+ end
1561
+ as_a(target, flat: true).each do |cmd|
1562
+ case cmd
1563
+ when Proc, Method
1564
+ cmd.call(*args, **opts)
1565
+ when String
1566
+ run(cmd, **opts)
1567
+ end
1299
1568
  end
1300
1569
  end
1301
1570
  end
@@ -1336,23 +1605,53 @@ module Squared
1336
1605
  end
1337
1606
 
1338
1607
  def run_set(cmd, val = nil, opts: nil, **)
1339
- return @output = cmd.dup if cmd.is_a?(Array)
1340
-
1341
- unless @output[1] == false && !@output[0].nil?
1608
+ diso = @output[1] == false && !@output[0].nil?
1609
+ dise = @output[2] == false
1610
+ parse = lambda do |data|
1611
+ ret = []
1612
+ if data[:command]
1613
+ ret[0] = data[:command]
1614
+ ret[1] = data[:opts] unless diso
1615
+ ret[3] = data[:args]
1616
+ elsif data[:script]
1617
+ ret[1] = data[:script]
1618
+ ret[3] = data[:opts]
1619
+ ret[4] = data[:args]
1620
+ else
1621
+ ret[0] = false
1622
+ end
1623
+ ret[2] = data[:env] unless dise
1624
+ ret
1625
+ end
1626
+ case cmd
1627
+ when Array
1628
+ @output = if cmd.all? { |data| data.is_a?(Hash) }
1629
+ diso = false
1630
+ dise = false
1631
+ cmd.map { |data| parse.(data) }
1632
+ else
1633
+ cmd.dup
1634
+ end
1635
+ return
1636
+ when Hash
1637
+ @output = parse.(data)
1638
+ else
1639
+ @output[0] = cmd
1640
+ end
1641
+ unless diso
1342
1642
  if opts == false
1343
1643
  @output[1] = false
1344
1644
  elsif opts && opts != true
1345
1645
  @output[1] = opts
1346
1646
  end
1347
1647
  end
1348
- unless @output[2] == false
1349
- if val.is_a?(Hash)
1350
- @output[2] = val
1351
- elsif val == false
1352
- @output[2] = false
1353
- end
1648
+ return if dise
1649
+
1650
+ if val.is_a?(Hash)
1651
+ @output[2] = val
1652
+ elsif val == false
1653
+ @output[2] = false
1354
1654
  end
1355
- @output[0] = cmd
1356
1655
  end
1357
1656
 
1358
1657
  def script_set(cmd, prod: nil, args: nil, **)
@@ -1368,6 +1667,8 @@ module Squared
1368
1667
  end
1369
1668
 
1370
1669
  def as_get(val)
1670
+ return unless val
1671
+
1371
1672
  @global && (ret = @as && @as[from] && @as[from][val]) ? ret : val
1372
1673
  end
1373
1674
 
@@ -1402,19 +1703,20 @@ module Squared
1402
1703
 
1403
1704
  def runnable?(val)
1404
1705
  case val
1405
- when String
1706
+ when String, Enumerable, Proc, Method
1406
1707
  true
1407
- when Enumerable
1408
- !val.is_a?(Hash)
1409
1708
  else
1410
1709
  false
1411
1710
  end
1412
1711
  end
1413
1712
 
1713
+ def series?(val)
1714
+ val.is_a?(Array) && val.all? { |p| p.is_a?(Proc) || p.is_a?(Method) }
1715
+ end
1716
+
1414
1717
  def session_arg?(*list, target: @session, value: false)
1415
1718
  list.any? do |val|
1416
- pat = /^#{Regexp.escape(shell_option(val))}#{value ? '[ =].' : '(?:[ =]|$)'}/
1417
- target.any? { |opt| opt =~ pat }
1719
+ target.any? { |opt| opt.match?(/^#{Regexp.escape(shell_option(val))}#{value ? '[ =].' : '(?:[ =]|$)'}/o) }
1418
1720
  end
1419
1721
  end
1420
1722
 
@@ -1451,6 +1753,10 @@ module Squared
1451
1753
  Base.tasks + VAR_SET
1452
1754
  end
1453
1755
 
1756
+ def blocks
1757
+ BLK_SET
1758
+ end
1759
+
1454
1760
  def borderstyle
1455
1761
  ((data = workspace.banner_get(*@ref, group: group)) && data[:border]) || theme[:border]
1456
1762
  end