squared 0.4.6 → 0.4.8

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'json'
4
4
  require 'date'
5
+ require 'time'
5
6
  require 'logger'
6
7
 
7
8
  module Squared
@@ -13,13 +14,15 @@ module Squared
13
14
  include Shell
14
15
  include Prompt
15
16
  include Utils
17
+ include Support
16
18
  include Rake::DSL
17
19
 
18
20
  VAR_SET = %i[parent global envname dependfile dependindex theme run script env pass].freeze
19
21
  BLK_SET = %i[run depend doc lint test copy clean].freeze
20
22
  SEM_VER = /\b(\d+)(?:(\.)(\d+))?(?:(\.)(\d+)(\S+)?)?\b/.freeze
21
23
  URI_SCHEME = %r{^([a-z][a-z\d+-.]*)://[^@:\[\]\\^<>|\s]}i.freeze
22
- private_constant :VAR_SET, :BLK_SET, :SEM_VER, :URI_SCHEME
24
+ TASK_METADATA = Rake::TaskManager.record_task_metadata
25
+ private_constant :VAR_SET, :BLK_SET, :SEM_VER, :URI_SCHEME, :TASK_METADATA
23
26
 
24
27
  class << self
25
28
  def populate(*); end
@@ -44,7 +47,9 @@ module Squared
44
47
  @ref ||= to_s.downcase.to_sym
45
48
  end
46
49
 
47
- def subtasks(val)
50
+ def subtasks(val = nil, &blk)
51
+ return @@tasks[val || ref].each(&blk) if block_given?
52
+
48
53
  @@tasks[ref] = val.freeze
49
54
  end
50
55
 
@@ -58,12 +63,11 @@ module Squared
58
63
  end
59
64
 
60
65
  @@tasks = {}
61
- @@task_desc = Rake::TaskManager.record_task_metadata
62
66
  @@print_order = 0
63
67
 
64
68
  subtasks({
65
69
  'graph' => %i[run print].freeze,
66
- 'unpack' => %i[zip tar ext].freeze
70
+ 'unpack' => %i[zip tar gem ext].freeze
67
71
  })
68
72
 
69
73
  attr_reader :name, :project, :workspace, :path, :theme, :exception, :pipe, :verbose,
@@ -94,7 +98,7 @@ module Squared
94
98
  @exception = env_bool(kwargs[:exception], workspace.exception, strict: true)
95
99
  @pipe = env_pipe(kwargs[:pipe], workspace.pipe, strict: true)
96
100
  @verbose = case verbose
97
- when nil
101
+ when NilClass
98
102
  workspace.verbose
99
103
  when String
100
104
  env_bool(verbose, workspace.verbose, strict: true, index: true)
@@ -192,19 +196,24 @@ module Squared
192
196
  return if @log
193
197
 
194
198
  log = log.is_a?(Hash) ? log.dup : { file: log }
195
- unless (file = env('LOG_FILE'))
196
- file = case env('LOG_AUTO')
197
- when 'y', 'year'
198
- "#{@name}-#{Date.today.year}.log"
199
- when 'm', 'month'
200
- "#{@name}-#{Date.today.strftime('%Y-%m')}.log"
201
- when 'd', 'day', '1'
202
- "#{@name}-#{Date.today}.log"
203
- end
204
- end
205
- if file ||= log[:file]
206
- file = Date.today.strftime(file)
207
- file = (dir = env('LOG_DIR')) ? @workspace.home.join(dir, file) : @workspace.home.join(file)
199
+ if (file = env('LOG_FILE'))
200
+ file = Time.now.strftime(file)
201
+ elsif (val = env('LOG_AUTO'))
202
+ file = "#{@name}-%s.log" % [case val
203
+ when 'y', 'year'
204
+ Date.today.year
205
+ when 'm', 'month'
206
+ Date.today.strftime('%Y-%m')
207
+ when 'd', 'day', '1'
208
+ Date.today
209
+ else
210
+ val.include?('%') ? Time.now.strftime(val) : Time.now.strftime('%FT%T%:z')
211
+ end]
212
+ elsif (val = log[:file])
213
+ file = val.is_a?(String) ? Time.now.strftime(val) : "#{@name}-#{Date.today}.log"
214
+ end
215
+ if file
216
+ file = (val = env('LOG_DIR')) ? @workspace.home.join(val, file) : @workspace.home.join(file)
208
217
  begin
209
218
  file = file.realdirpath
210
219
  rescue StandardError => e
@@ -259,7 +268,7 @@ module Squared
259
268
  return unless ref?(Base.ref)
260
269
 
261
270
  namespace name do
262
- @@tasks[Base.ref].each do |action, flags|
271
+ Base.subtasks do |action, flags|
263
272
  next if @pass.include?(action)
264
273
 
265
274
  namespace action do
@@ -292,17 +301,21 @@ module Squared
292
301
  end
293
302
  end
294
303
  when 'unpack'
295
- format_desc(action, flag, 'tag/url,dir,digest?,f/force?', before: flag == :ext ? 'ext' : nil)
304
+ format_desc(action, flag, 'tag/url,dir,digest?,f|force?', before: flag == :ext ? 'ext' : nil)
296
305
  params = %i[tag dir digest force]
297
306
  params.unshift(:ext) if flag == :ext
298
307
  task flag, params do |_, args|
299
- ext = param_guard(action, flag, args: args, key: :ext).downcase if flag == :ext
308
+ ext = flag == :ext ? param_guard(action, flag, args: args, key: :ext) : flag.to_s
300
309
  tag = param_guard(action, flag, args: args, key: :tag)
301
310
  dir = param_guard(action, flag, args: args, key: :dir)
302
311
  unless tag.match?(URI_SCHEME)
303
- raise_error 'no base uri' unless @release
304
-
305
- tag = "#{@release.include?('??') ? @release.sub('??', tag) : @release + tag}.#{flag}"
312
+ if flag == :gem
313
+ tag = "https://rubygems.org/downloads/#{File.basename(tag, '.gem')}.gem"
314
+ elsif @release
315
+ tag = "%s.#{ext}" % [@release.include?('??') ? @release.sub('??', tag) : @release + tag]
316
+ else
317
+ raise_error("no base uri: #{tag}", hint: ext)
318
+ end
306
319
  end
307
320
  case (digest = args.digest)
308
321
  when 'f', 'force'
@@ -311,7 +324,7 @@ module Squared
311
324
  else
312
325
  force = args.fetch(:force, false)
313
326
  end
314
- unpack(basepath(dir), uri: tag, digest: digest, ext: ext || flag.to_s, force: force)
327
+ unpack(basepath(dir), uri: tag, digest: digest, ext: ext, force: force)
315
328
  end
316
329
  end
317
330
  end
@@ -343,15 +356,15 @@ module Squared
343
356
  end
344
357
  end
345
358
  if path.is_a?(String) && (seg = path[%r{^(.+)[\\/]\*+$}, 1])
346
- return self unless checkdir.(path = basepath(seg))
359
+ return self unless checkdir.call(path = basepath(seg))
347
360
 
348
- path = path.children.select { |val| checkdir.(val) }
361
+ path = path.children.select { |val| checkdir.call(val) }
349
362
  end
350
363
  if path.is_a?(Array)
351
364
  name = @name if name == true
352
365
  path.each { |val| add(val, name && task_join(name, File.basename(val)), **kwargs, &blk) }
353
366
  return self
354
- elsif !projectpath?(path = basepath(path)) || !checkdir.(path)
367
+ elsif !projectpath?(path = basepath(path)) || !checkdir.call(path)
355
368
  return self
356
369
  elsif name.is_a?(Symbol)
357
370
  name = name.to_s
@@ -395,12 +408,19 @@ module Squared
395
408
 
396
409
  run_b(@run, sync: sync, from: from) if series?(@run)
397
410
  args = @output
398
- banner = verbosity > 0 if task_invoked?('build', 'build:sync', 'default')
411
+ banner = verbosetype > 1 if task_invoked?('build', 'build:sync', 'default')
412
+ end
413
+ if args.first.is_a?(Struct)
414
+ f, blk = args.first.to_a
415
+ args[0] = instance_eval(&blk) || f
416
+ return unless args[0]
399
417
  end
400
418
  if args.all? { |val| val.is_a?(Array) }
401
419
  cmd = []
402
420
  var = {}
403
421
  args.each do |val|
422
+ next instance_exec(*val[1..-1], &val[0]) if val.first.is_a?(Proc)
423
+
404
424
  a, b, c, d, e = val
405
425
  case b
406
426
  when Hash
@@ -450,7 +470,7 @@ module Squared
450
470
  end
451
471
 
452
472
  def archive(*, sync: invoked_sync?('archive'), **)
453
- return unless @archive.is_a?(Array)
473
+ return unless @archive.is_a?(Hash)
454
474
 
455
475
  unpack(path, **@archive, sync: sync, from: :archive)
456
476
  end
@@ -471,11 +491,18 @@ module Squared
471
491
  run_b(@copy, sync: sync, from: :copy)
472
492
  end
473
493
 
474
- def clean(*, sync: invoked_sync?('clean'), **)
494
+ def clean(*args, sync: invoked_sync?('clean'), pass: false, **kwargs)
475
495
  return unless @clean
476
496
 
477
- on :first, :clean
497
+ on :first, :clean unless pass
478
498
  case @clean
499
+ when Struct
500
+ if (any = instance_eval(&@clean.block) || @clean.run)
501
+ temp = @clean
502
+ @clean = any
503
+ clean(*args, sync: sync, pass: true, **kwargs)
504
+ @clean = temp
505
+ end
479
506
  when String
480
507
  run_s(@clean, sync: sync)
481
508
  when Hash
@@ -488,12 +515,11 @@ module Squared
488
515
  end
489
516
  else
490
517
  if @clean.is_a?(Enumerable) && !series?(@clean)
491
- as_a(@clean).each do |val|
492
- val = val.to_s
493
- path = basepath(val)
518
+ @clean.each do |val|
519
+ path = basepath(val = val.to_s)
494
520
  if path.directory? && val.match?(%r{[\\/]$})
495
521
  log&.warn "rm -rf #{path}"
496
- FileUtils.rm_rf(path, verbose: verbose)
522
+ rm_rf(path, verbose: verbose)
497
523
  else
498
524
  log&.warn "rm #{path}"
499
525
  (val.include?('*') ? Dir[path] : [path]).each do |file|
@@ -511,7 +537,7 @@ module Squared
511
537
  run_b(@clean, sync: sync)
512
538
  end
513
539
  end
514
- on :last, :clean
540
+ on :last, :clean unless pass
515
541
  end
516
542
 
517
543
  def graph(start = [], tasks = nil, sync: invoked_sync?('graph'), pass: [], out: nil)
@@ -519,13 +545,13 @@ module Squared
519
545
  tasks ||= []
520
546
  split_escape(val).each do |task|
521
547
  if ref?(task.to_sym) && (script = workspace.script_get(:graph, ref: task.to_sym))
522
- tasks += script[:graph]
548
+ tasks.concat(script[:graph])
523
549
  else
524
550
  tasks << task
525
551
  end
526
552
  end
527
553
  end
528
- pass += split_escape(val) if (val = env('GRAPH', suffix: 'PASS'))
554
+ pass.concat(split_escape(val)) if (val = env('GRAPH', suffix: 'PASS'))
529
555
  start, neg = start.partition { |name| !name.start_with?('-') }
530
556
  data = graph_collect(self, start, pass: neg.map! { |name| name[1..-1] })
531
557
  unless out
@@ -554,8 +580,11 @@ module Squared
554
580
  elsif !target.empty?
555
581
  raise_error('directory not empty', hint: target) unless force || env('UNPACK_FORCE')
556
582
  create = true
583
+ elsif !uri
584
+ raise_error('no download uri', hint: target)
557
585
  end
558
586
  if digest
587
+ require 'digest'
559
588
  if (n = digest.index(':').to_i) > 0
560
589
  size = digest[0, n].downcase
561
590
  digest = digest[n + 1..-1]
@@ -582,11 +611,10 @@ module Squared
582
611
  if (val = env('HEADERS')) && (val = parse_json(val, hint: "HEADERS_#{@envname}"))
583
612
  headers = val
584
613
  end
585
- require 'open-uri'
586
614
  data = nil
587
615
  (uri = as_a(uri)).each_with_index do |url, index|
588
616
  last = index == uri.size - 1
589
- URI.open(url, headers) do |f|
617
+ fetch_uri(url, headers) do |f|
590
618
  data = f.read
591
619
  if algo && algo.hexdigest(data) != digest
592
620
  data = nil
@@ -610,13 +638,19 @@ module Squared
610
638
  raise_error('no content', hint: url)
611
639
  end
612
640
  end
613
- ext ||= URI.parse(uri).path[/^.+?\.((?:tar\.)?\w+)$/i, 1]
614
- if (n = env("#{ext == 'zip' || ext == '7z' ? ext.upcase : 'TAR'}_DEPTH", ignore: false))
615
- depth = n.to_i
641
+ raise_error('no content type', hint: uri) unless ext ||= URI.parse(uri).path[/\.(\w+)(\?|$)/i, 1]
642
+ ext = ext.downcase
643
+ if (val = env("#{%w[zip 7z gem].include?(ext) ? ext.upcase : 'TAR'}_DEPTH", ignore: false))
644
+ depth = val.to_i
616
645
  end
617
646
  begin
618
- require 'tempfile'
619
- file = Tempfile.new("#{name}-")
647
+ if ext == 'gem'
648
+ dir = Dir.mktmpdir
649
+ file = File.new(File.join(dir, File.basename(uri)), 'w')
650
+ else
651
+ require 'tempfile'
652
+ file = Tempfile.new("#{name}-")
653
+ end
620
654
  file.write(data)
621
655
  file.close
622
656
  if create
@@ -639,6 +673,9 @@ module Squared
639
673
  depth = 0
640
674
  when '7z'
641
675
  session '7z', 'x', shell_quote(file.path), "-o#{shell_quote(target)}"
676
+ when 'gem'
677
+ session 'gem', 'unpack', shell_quote(file.path), quote_option('target', target)
678
+ depth = 0 unless val
642
679
  else
643
680
  raise_error("unsupported format: #{ext}", hint: uri)
644
681
  end
@@ -647,7 +684,10 @@ module Squared
647
684
  entry = target.children.first
648
685
  break unless entry.directory?
649
686
 
650
- dest = target.join(File.basename(file.path))
687
+ i = 0
688
+ while (dest = target.join("#{File.basename(file.path)}-#{i}")).exist?
689
+ i += 1
690
+ end
651
691
  FileUtils.mv(entry, dest)
652
692
  dest.children.each { |child| FileUtils.mv(child, target) }
653
693
  dest.rmdir
@@ -655,7 +695,11 @@ module Squared
655
695
  depth -= 1
656
696
  end
657
697
  ensure
658
- file&.unlink
698
+ if dir
699
+ remove_entry dir
700
+ elsif file
701
+ file.unlink
702
+ end
659
703
  end
660
704
  end
661
705
 
@@ -715,7 +759,7 @@ module Squared
715
759
  @parent = val if (val = val.first).is_a?(Project::Base)
716
760
  when :graph
717
761
  @graph = case val.first
718
- when nil, false
762
+ when NilClass, FalseClass
719
763
  nil
720
764
  else
721
765
  val.flatten.map!(&:to_s).freeze
@@ -918,7 +962,13 @@ module Squared
918
962
 
919
963
  def run_b(obj, from: nil, sync: true)
920
964
  case obj
921
- when Proc, Method
965
+ when Struct
966
+ if (any = instance_eval(&obj.block) || obj.run)
967
+ run_b(any, from: from, sync: sync)
968
+ end
969
+ when Proc
970
+ instance_eval(&obj)
971
+ when Method
922
972
  obj.call
923
973
  else
924
974
  if series?(obj)
@@ -947,14 +997,14 @@ module Squared
947
997
  end
948
998
  start = target.name
949
999
  if depth == 0
950
- items = check.(dedupe.(start))
1000
+ items = check.call(dedupe.call(start))
951
1001
  single = items.size == 1
952
1002
  else
953
- items = check.(data[start])
1003
+ items = check.call(data[start])
954
1004
  end
955
1005
  if out
956
1006
  a, b, c, d, e = ARG[:GRAPH]
957
- f = tag.(target)
1007
+ f = tag.call(target)
958
1008
  out << case depth
959
1009
  when 0
960
1010
  f
@@ -971,15 +1021,15 @@ module Squared
971
1021
  items.each_with_index do |proj, i|
972
1022
  next if done.include?(proj)
973
1023
 
974
- t = dedupe.(proj.name)
1024
+ t = dedupe.call(proj.name)
975
1025
  j = if out
976
- if i == items.size - 1 || check.(post = items[i + 1..-1]).empty?
1026
+ if i == items.size - 1 || check.call(post = items[i + 1..-1]).empty?
977
1027
  true
978
1028
  elsif !t.empty? && depth > 0
979
1029
  post.reject { |pr| t.include?(pr) }.empty?
980
1030
  end
981
1031
  end
982
- unless start == proj.name || (none = check.(t).empty?)
1032
+ unless start == proj.name || (none = check.call(t).empty?)
983
1033
  graph_branch(proj, data, tasks, out, sync: sync, pass: pass, done: done, depth: depth.succ,
984
1034
  single: single, last: j == true, context: target)
985
1035
  end
@@ -1005,7 +1055,7 @@ module Squared
1005
1055
  elsif none
1006
1056
  a, b, c, d = ARG[:GRAPH]
1007
1057
  out << if depth == 0
1008
- "#{i == items.size - 1 ? d : c}#{b * 4} #{tag.(proj)}"
1058
+ "#{i == items.size - 1 ? d : c}#{b * 4} #{tag.call(proj)}"
1009
1059
  else
1010
1060
  s = ''.dup
1011
1061
  k = 0
@@ -1015,7 +1065,7 @@ module Squared
1015
1065
  s += "#{indent || (last && data[final].last == context) ? ' ' : a} "
1016
1066
  k += 1
1017
1067
  end
1018
- s + "#{j ? d : c}#{b * 3} #{tag.(proj)}"
1068
+ s + "#{j ? d : c}#{b * 3} #{tag.call(proj)}"
1019
1069
  end
1020
1070
  end
1021
1071
  done << proj
@@ -1043,7 +1093,7 @@ module Squared
1043
1093
  next if (objs = data.fetch(proj.name, [])).include?(target)
1044
1094
 
1045
1095
  deps << proj
1046
- deps += objs
1096
+ deps.concat(objs)
1047
1097
  end
1048
1098
  end
1049
1099
  data[target.name] = deps.uniq.reject { |proj| proj == target }
@@ -1066,7 +1116,7 @@ module Squared
1066
1116
  end
1067
1117
 
1068
1118
  def session(*cmd, prefix: cmd.first, main: true, path: true, options: true)
1069
- prefix = prefix.to_s.upcase
1119
+ prefix = stripext(prefix.to_s).upcase
1070
1120
  if path && (val = ENV["PATH_#{prefix}"] || PATH[prefix] || PATH[prefix.to_sym])
1071
1121
  cmd[0] = shell_quote(val, force: false)
1072
1122
  end
@@ -1077,9 +1127,9 @@ module Squared
1077
1127
  main ? @session = ret : ret
1078
1128
  end
1079
1129
 
1080
- def session_delete(*list, target: @session)
1130
+ def session_delete(*args, target: @session)
1081
1131
  ret = []
1082
- list.each do |val|
1132
+ args.each do |val|
1083
1133
  pat = /^#{Regexp.escape(shell_option(val))}(?: |=|$)/
1084
1134
  if (key = target.find { |opt| opt.match?(pat) })
1085
1135
  target.delete(key)
@@ -1101,122 +1151,25 @@ module Squared
1101
1151
  cmd.done
1102
1152
  end
1103
1153
 
1104
- def option(*args, prefix: @session&.first, **kwargs)
1154
+ def option(*args, target: @session, prefix: target&.first, **kwargs)
1105
1155
  if prefix
1106
- prefix = File.basename(prefix, File.extname(prefix))
1107
1156
  args.each do |val|
1108
- ret = env("#{prefix}_#{val.gsub(/\W/, '_')}".upcase, **kwargs)
1157
+ ret = env("#{stripext(prefix)}_#{val.gsub(/\W/, '_')}".upcase, **kwargs)
1109
1158
  return ret if ret
1110
1159
  end
1111
1160
  end
1112
1161
  nil
1113
1162
  end
1114
1163
 
1115
- def option_sanitize(opts, list, target: @session, no: nil, single: nil, args: false, first: false, pass: ['='])
1116
- ret = []
1117
- reg = []
1118
- bare = []
1119
- e = []
1120
- b = []
1121
- m = []
1122
- p = []
1123
- q = []
1124
- qq = []
1125
- i = []
1126
- f = []
1127
- si = []
1128
- list.map do |val|
1129
- x, y = val.split('|')
1130
- if y
1131
- if (n = val.index('='))
1132
- x += val[n..-1]
1133
- end
1134
- [x, y]
1135
- else
1136
- x
1137
- end
1138
- end
1139
- .flatten
1140
- .each do |val|
1141
- if (n = val.index('='))
1142
- flag = val[0, n]
1143
- case val[n + 1]
1144
- when 'e'
1145
- e << flag
1146
- when 'b'
1147
- b << flag
1148
- when 'm'
1149
- m << flag
1150
- when 'q'
1151
- qq << flag if val[n + 2] == 'q'
1152
- q << flag
1153
- when 'p'
1154
- p << flag
1155
- when 'i'
1156
- i << flag
1157
- when 'f'
1158
- f << flag
1159
- when 'n'
1160
- si << flag
1161
- else
1162
- reg << Regexp.escape(flag)
1163
- end
1164
- bare << flag if val.end_with?('?')
1165
- else
1166
- bare << val
1167
- end
1168
- end
1169
- no = (no || []).map { |val| (n = val.index('=')) ? val[0, n] : val }
1170
- bare += no
1171
- found = false
1172
- opts.each do |opt|
1173
- next ret << opt if found
1174
-
1175
- if single&.match?(opt)
1176
- target << "-#{opt}"
1177
- elsif bare.include?(opt)
1178
- target << (opt.size == 1 ? "-#{opt}" : "--#{opt}")
1179
- elsif opt.start_with?('no-') && no.include?(name = opt[3..-1])
1180
- target << "--no-#{name}"
1181
- else
1182
- if opt =~ /\A([^=]+)=(.+)\z/
1183
- key = $1
1184
- val = $2
1185
- match = ->(flag, pat) { flag.include?(key) && pat.match?(val) }
1186
- if e.include?(key)
1187
- target << shell_option(key, val)
1188
- elsif q.include?(key)
1189
- target << quote_option(key, val, double: qq.include?(key))
1190
- elsif p.include?(key)
1191
- target << quote_option(key, basepath(val))
1192
- elsif m.include?(key)
1193
- target << basic_option(key, val, merge: true)
1194
- elsif b.include?(key) || match.(i, /^\d+$/) || match.(f, /^\d*(?:\.\d+)?$/) || match.(si, /^-?\d+$/)
1195
- target << basic_option(key, val)
1196
- else
1197
- ret << opt
1198
- end
1199
- opt = key
1200
- else
1201
- ret << opt
1202
- found = true if args
1203
- end
1204
- found = true if first && pass.none? { |s| opt.include?(s) }
1205
- end
1206
- end
1207
- [ret, reg.empty? ? /\A\s+\z/ : /^(#{reg.join('|')})=(.+)$/]
1164
+ def option_sanitize(opts, list, target: @session, **kwargs)
1165
+ op = OptionPartition.new(opts, list, target, project: self, **kwargs)
1166
+ [op.extras, op.values]
1208
1167
  end
1209
1168
 
1210
- def option_clear(opts, target: @session, pass: true, append: false, **kwargs)
1211
- return if opts.empty?
1212
-
1213
- kwargs[:subject] ||= target&.first
1214
- kwargs[:hint] ||= 'unrecognized'
1215
- warn log_message(Logger::WARN, opts.join(', '), pass: true, **kwargs)
1216
- append_value(opts, delim: true) if append
1217
- return if pass || confirm("Run? [#{sub_style(target, styles: theme[:inline])}] [y/N] ", 'N', timeout: 30)
1169
+ def option_clear(opts, target: @session, **kwargs)
1170
+ return unless target
1218
1171
 
1219
- raise_error 'user cancelled'
1172
+ OptionPartition.clear(target, opts, styles: theme[:inline], **kwargs)
1220
1173
  end
1221
1174
 
1222
1175
  def print_item(*val)
@@ -1263,7 +1216,7 @@ module Squared
1263
1216
  end
1264
1217
 
1265
1218
  def format_desc(action, flag, opts = nil, **kwargs)
1266
- return unless @@task_desc
1219
+ return unless TASK_METADATA
1267
1220
 
1268
1221
  ret = [@desc, action]
1269
1222
  ret << flag if flag
@@ -1323,7 +1276,7 @@ module Squared
1323
1276
  items.each_with_index do |val, i|
1324
1277
  next unless reg.empty? || reg.any? { |pat| val[0].match?(pat) }
1325
1278
 
1326
- out << "#{i.succ.to_s.rjust(pad)}. #{each ? each.(val) : val[0]}"
1279
+ out << "#{i.succ.to_s.rjust(pad)}. #{each ? each.call(val) : val[0]}"
1327
1280
  end
1328
1281
  end
1329
1282
  sub = [headerstyle]
@@ -1331,7 +1284,7 @@ module Squared
1331
1284
  out = ["No #{type} were found:", '']
1332
1285
  unless grep.empty?
1333
1286
  i = 0
1334
- out += grep.map { |s| "#{i += 1}. #{s}" }
1287
+ out.concat(grep.map { |s| "#{i += 1}. #{s}" })
1335
1288
  out << ''
1336
1289
  end
1337
1290
  if from
@@ -1358,7 +1311,6 @@ module Squared
1358
1311
  end
1359
1312
 
1360
1313
  def append_hash(data, target: @session, build: false)
1361
- target ||= []
1362
1314
  if build && (type = env('BUILD', suffix: 'TYPE') || ENV['BUILD_TYPE'])
1363
1315
  type = "__#{type}__"
1364
1316
  if (extra = data[type] || data[type.to_sym]).is_a?(Hash)
@@ -1379,7 +1331,7 @@ module Squared
1379
1331
  append_repeat(key, val, target: target)
1380
1332
  when Numeric
1381
1333
  target << basic_option(key, val)
1382
- when false
1334
+ when FalseClass
1383
1335
  target << shell_option(key).sub(/^--(?!no-)/, '--no-')
1384
1336
  else
1385
1337
  target << shell_option(key, val.is_a?(String) ? val : nil)
@@ -1411,22 +1363,17 @@ module Squared
1411
1363
  end
1412
1364
  end
1413
1365
 
1414
- def append_value(*list, target: @session, delim: false, escape: false, quote: true)
1415
- return if (list = list.flatten).empty?
1366
+ def append_value(*list, target: @session, **kwargs)
1367
+ return unless target
1416
1368
 
1417
- target << '--' if delim && !target.include?('--')
1418
- list.map! do |val|
1419
- ret = escape ? shell_escape(val, quote: quote) : shell_quote(val)
1420
- target << ret
1421
- ret
1422
- end
1369
+ OptionPartition.append(target, *list, **kwargs)
1423
1370
  end
1424
1371
 
1425
1372
  def append_first(*list, target: @session, flag: true, equals: false, escape: true, quote: true, force: true,
1426
1373
  **kwargs)
1427
- return if (list = list.flatten).empty?
1374
+ return if list.empty?
1428
1375
 
1429
- list.each do |opt|
1376
+ list.flatten.each do |opt|
1430
1377
  next unless (val = option(opt, **kwargs))
1431
1378
 
1432
1379
  return target << (if flag
@@ -1440,11 +1387,11 @@ module Squared
1440
1387
 
1441
1388
  def append_option(*list, target: @session, no: false, equals: false, escape: true, quote: true, force: true,
1442
1389
  **kwargs)
1443
- return if (list = list.flatten).empty?
1390
+ return if list.empty?
1444
1391
 
1445
1392
  ret = []
1446
- list.each do |flag|
1447
- next unless (val = option(flag, **kwargs))
1393
+ list.flatten.each do |flag|
1394
+ next unless (val = option(flag, target: target, **kwargs))
1448
1395
 
1449
1396
  if val == '0' && no
1450
1397
  flag = "no-#{flag}"
@@ -1456,50 +1403,52 @@ module Squared
1456
1403
  end
1457
1404
 
1458
1405
  def append_nocolor(target: @session)
1459
- target << '--no-color' if !ARG[:COLOR] || stdin? || option('no-color', ignore: false)
1406
+ target << '--no-color' if !ARG[:COLOR] || stdin? || option('color', target: target, equals: '0')
1460
1407
  end
1461
1408
 
1462
1409
  def merge_opts(base, data)
1463
- return data unless base
1464
- return base unless data
1465
-
1466
- ret = case data
1467
- when String
1468
- case base
1469
- when String
1470
- "#{base} #{data}"
1471
- when Hash
1472
- "#{append_hash(base).join(' ')} #{data}"
1473
- when Enumerable
1474
- "#{base.to_a.join(' ')} #{data}"
1475
- end
1476
- when Hash
1477
- case base
1478
- when String
1479
- "#{base} #{append_hash(data).join(' ')}"
1480
- when Hash
1481
- base.merge(data)
1482
- when Enumerable
1483
- base.to_a + append_hash(data)
1484
- end
1485
- when Enumerable
1486
- case base
1487
- when String
1488
- "#{base} #{data.to_a.join(' ')}"
1489
- when Hash
1490
- "#{append_hash(base).join(' ')} #{data.to_a.join(' ')}"
1491
- when Enumerable
1492
- base.to_a + data.to_a
1493
- end
1494
- else
1495
- base
1496
- end
1497
- ret || data
1410
+ case data
1411
+ when String
1412
+ case base
1413
+ when String
1414
+ "#{base} #{data}"
1415
+ when Hash
1416
+ "#{append_hash(base).join(' ')} #{data}"
1417
+ when Enumerable
1418
+ "#{base.to_a.join(' ')} #{data}"
1419
+ else
1420
+ data
1421
+ end
1422
+ when Hash
1423
+ case base
1424
+ when String
1425
+ "#{base} #{append_hash(data).join(' ')}"
1426
+ when Hash
1427
+ base.merge(data)
1428
+ when Enumerable
1429
+ Set.new(base.to_a + append_hash(data)).to_a
1430
+ else
1431
+ data
1432
+ end
1433
+ when Enumerable
1434
+ case base
1435
+ when String
1436
+ "#{base} #{data.to_a.join(' ')}"
1437
+ when Hash
1438
+ "#{append_hash(base).join(' ')} #{data.to_a.join(' ')}"
1439
+ when Enumerable
1440
+ Set.new(base.to_a + data.to_a).to_a
1441
+ else
1442
+ data
1443
+ end
1444
+ else
1445
+ base
1446
+ end
1498
1447
  end
1499
1448
 
1500
1449
  def collect_hash(data, pass: [])
1501
1450
  ret = []
1502
- data.each { |key, val| ret += val unless pass.include?(key) }
1451
+ data.each { |key, val| ret.concat(val) unless pass.include?(key) }
1503
1452
  ret
1504
1453
  end
1505
1454
 
@@ -1513,6 +1462,15 @@ module Squared
1513
1462
  ret
1514
1463
  end
1515
1464
 
1465
+ def fetch_uri(*args, **kwargs, &blk)
1466
+ require 'open-uri'
1467
+ if RUBY_VERSION < '2.5'
1468
+ open(*args, **kwargs, &blk)
1469
+ else
1470
+ URI.open(*args, **kwargs, &blk)
1471
+ end
1472
+ end
1473
+
1516
1474
  def param_guard(action, flag, args: nil, key: nil, pat: nil, values: nil)
1517
1475
  if args && key
1518
1476
  val = args.fetch(key, nil)
@@ -1527,6 +1485,39 @@ module Squared
1527
1485
  args
1528
1486
  end
1529
1487
 
1488
+ def confirm_outdated(pkg, ver, rev, lock: false)
1489
+ a = sub_style(case rev
1490
+ when 1
1491
+ 'MAJOR'
1492
+ when 2
1493
+ 'MINOR'
1494
+ else
1495
+ 'PATCH'
1496
+ end, styles: theme[:header])
1497
+ b = sub_style("#{pkg} #{ver}", styles: theme[:inline])
1498
+ c, d = rev == 1 || lock ? ['y/N', 'N'] : ['Y/n', 'Y']
1499
+ e = lock ? " #{sub_style('(locked)', styles: color(:red))}" : ''
1500
+ confirm("Upgrade to #{a}? #{b + e} [#{c}] ", d, timeout: 60)
1501
+ end
1502
+
1503
+ def choice_index(msg, list, values: nil, multiple: false, accept: nil, trim: nil)
1504
+ puts if @@print_order > 0
1505
+ raise_error 'user cancelled' unless (ret = choice(msg, list, multiple: multiple))
1506
+ ret = multiple ? ret.map! { |val| val.sub(trim, '') } : ret.sub(trim, '') if trim
1507
+ exit 1 if accept && !confirm("#{accept} [#{ret.join(', ')}] [y/N] ", 'N', timeout: 60)
1508
+ if values
1509
+ ret = [ret]
1510
+ values.each do |val|
1511
+ val, req = val if val.is_a?(Array)
1512
+ val = Readline.readline("#{val} (#{req ? 'required' : 'optional'}): ", true).strip
1513
+ raise_error 'user cancelled' if req && val.empty?
1514
+ ret << (val.empty? ? nil : val)
1515
+ end
1516
+ end
1517
+ @@print_order += 1
1518
+ ret
1519
+ end
1520
+
1530
1521
  def runenv
1531
1522
  nil
1532
1523
  end
@@ -1539,12 +1530,11 @@ module Squared
1539
1530
  end
1540
1531
  end
1541
1532
 
1542
- def relativepath(*files, all: false)
1543
- return [] if files.empty?
1533
+ def relativepath(*list, all: false)
1534
+ return [] if list.empty?
1544
1535
 
1545
- pat = /^#{Regexp.escape(File.join(path, ''))}/
1546
- files.flatten.map! { |val| Pathname.new(val) }.select { |val| projectpath?(val) }.map! do |val|
1547
- val = val.absolute? ? val.to_s.sub(pat, '') : val.to_s
1536
+ list.flatten.map! { |val| Pathname.new(val) }.select { |val| projectpath?(val) }.map! do |val|
1537
+ val = val.absolute? ? val.relative_path_from(path).to_s : val.to_s
1548
1538
  val = val[2..-1] if val.start_with?('./')
1549
1539
  val = "#{val}*" if all && val.end_with?('/')
1550
1540
  val
@@ -1553,11 +1543,11 @@ module Squared
1553
1543
 
1554
1544
  def projectmap(files, parent: false, pass: true)
1555
1545
  unless parent
1556
- project = files.select { |val| projectpath?(val) }
1557
- raise_error 'pathspec not within worktree' unless pass || files.size == project.size
1558
- files = project
1546
+ proj = files.select { |val| projectpath?(val) }
1547
+ raise_error 'pathspec not within worktree' unless pass || files.size == proj.size
1548
+ files = proj
1559
1549
  end
1560
- files.map { |val| val == '.' ? '.' : shell_quote(basepath(val.strip)) }
1550
+ files.map { |val| val == '.' ? '.' : shell_quote(basepath(val)) }
1561
1551
  end
1562
1552
 
1563
1553
  def semver(val)
@@ -1597,7 +1587,18 @@ module Squared
1597
1587
  end
1598
1588
 
1599
1589
  def epochtime
1600
- DateTime.now.strftime('%Q').to_i
1590
+ Time.now.strftime('%s%L').to_i
1591
+ end
1592
+
1593
+ def verbosetype
1594
+ case verbose
1595
+ when TrueClasss
1596
+ 1
1597
+ when Numeric
1598
+ verbose.succ
1599
+ else
1600
+ 0
1601
+ end
1601
1602
  end
1602
1603
 
1603
1604
  def on(event, from, *args, **kwargs)
@@ -1622,15 +1623,15 @@ module Squared
1622
1623
  end
1623
1624
  end
1624
1625
 
1625
- def pwd_set(done = nil, pass: false, from: nil, dryrun: false, &blk)
1626
+ def pwd_set(done = nil, pass: false, from: nil, dryrun: false)
1626
1627
  pwd = Pathname.pwd
1627
1628
  if block_given?
1628
1629
  begin
1629
1630
  if path == pwd || pass == true || (pass.is_a?(String) && semscan(pass).join >= RUBY_VERSION)
1630
- ret = instance_eval(&blk)
1631
+ ret = yield
1631
1632
  else
1632
1633
  Dir.chdir(path)
1633
- ret = instance_eval(&blk)
1634
+ ret = yield
1634
1635
  Dir.chdir(pwd)
1635
1636
  end
1636
1637
  rescue StandardError => e
@@ -1681,13 +1682,13 @@ module Squared
1681
1682
  @output = if cmd.all? { |data| data.is_a?(Hash) }
1682
1683
  diso = false
1683
1684
  dise = false
1684
- cmd.map { |data| parse.(data) }
1685
+ cmd.map { |data| parse.call(data) }
1685
1686
  else
1686
1687
  cmd.dup
1687
1688
  end
1688
1689
  return
1689
1690
  when Hash
1690
- @output = parse.(data)
1691
+ @output = parse.call(data)
1691
1692
  else
1692
1693
  @output[0] = cmd
1693
1694
  end
@@ -1747,7 +1748,8 @@ module Squared
1747
1748
  end
1748
1749
 
1749
1750
  def projectpath?(val)
1750
- Pathname.new(val).absolute? ? val.to_s.start_with?(File.join(path, '')) : !val.to_s.start_with?('..')
1751
+ val = Pathname.new(val).cleanpath
1752
+ val.absolute? ? val.to_s.start_with?(File.join(path, '')) : !val.to_s.start_with?(File.join('..', ''))
1751
1753
  end
1752
1754
 
1753
1755
  def semmajor?(cur, want)
@@ -1767,11 +1769,8 @@ module Squared
1767
1769
  val.is_a?(Array) && val.all? { |p| p.is_a?(Proc) || p.is_a?(Method) }
1768
1770
  end
1769
1771
 
1770
- def session_arg?(*list, target: @session, value: false)
1771
- list.any? do |val|
1772
- pat = /^#{Regexp.escape(shell_option(val))}#{value ? '[ =].' : '(?:[ =]|$)'}/
1773
- target.any? { |opt| opt.match?(pat) }
1774
- end
1772
+ def session_arg?(*args, target: @session, **kwargs)
1773
+ !!target && OptionPartition.arg?(target, *args, **kwargs)
1775
1774
  end
1776
1775
 
1777
1776
  def from_sync?(*val)