squared 0.6.10 → 0.7.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.
@@ -33,7 +33,8 @@ module Squared
33
33
  d|detach force-recreate menu no-build no-color no-deps no-log-prefix no-recreate no-start quiet-build
34
34
  quiet-pull remove-orphans V|renew-anon-volumes timestamps wait w|watch y|yes attach=b
35
35
  exit-code-from=b no-attach=b pull=b scale=i t|timeout=i wait-timeout=i].freeze,
36
- down: %w[remove-orphans v|volumes rmi=b t|timeout=i].freeze
36
+ down: %w[remove-orphans v|volumes rmi=b t|timeout=i].freeze,
37
+ publish: %w[app resolve-image-digests with-env y|yes oci-version=b].freeze
37
38
  }.freeze,
38
39
  container: {
39
40
  create: %w[init i|interactive no-healthcheck oom-kill-disable privileged P|publish-all q|quiet read-only
@@ -64,6 +65,7 @@ module Squared
64
65
  }.freeze,
65
66
  image: {
66
67
  ls: %w[a|all digests no-trunc q|quiet tree f|filter=q format=q].freeze,
68
+ pull: %w[a|all-tags platform=q q|quiet].freeze,
67
69
  push: %w[a|all-tags platform=q q|quiet].freeze,
68
70
  rm: %w[f|force no-prune platform=q].freeze,
69
71
  save: %w[o|output=p platform=q].freeze
@@ -107,9 +109,9 @@ module Squared
107
109
 
108
110
  subtasks({
109
111
  'build' => %i[tag context].freeze,
110
- 'compose' => %i[build create run exec up down service].freeze,
111
- 'bake' => %i[build check].freeze,
112
- 'image' => %i[ls rm push tag save].freeze,
112
+ 'compose' => %i[build create publish run exec up down service].freeze,
113
+ 'bake' => %i[build compose check].freeze,
114
+ 'image' => %i[ls rm pull push tag save].freeze,
113
115
  'container' => %i[run create exec update commit inspect diff start stop restart pause unpause top stats kill
114
116
  rm].freeze,
115
117
  'network' => %i[connect disconnect].freeze,
@@ -119,17 +121,19 @@ module Squared
119
121
  attr_reader :context
120
122
  attr_accessor :tag
121
123
 
122
- def initialize(*, file: nil, context: nil, tag: nil, secrets: nil, mounts: [], registry: nil, **kwargs)
124
+ def initialize(*, mounts: [], **kwargs)
123
125
  super
124
- return unless dockerfile(file).exist?
126
+ self.global = nil
127
+ return unless dockerfile(kwargs[:file]).exist?
125
128
 
126
- @context = context
127
- self.tag = tag || tagname("#{@project}:#{@version || 'latest'}")
129
+ self.tag = kwargs[:tag] || tagname("#{@project}:#{@version || 'latest'}")
130
+ @context = kwargs[:context]
128
131
  @mounts = mounts
129
- @secrets = secrets
130
- @registry = tagjoin registry, kwargs[:username]
132
+ @secrets = kwargs[:secrets]
133
+ @registry = tagjoin kwargs[:registry], kwargs[:username]
134
+ @oci = tagjoin kwargs[:oci], kwargs[:username]
131
135
  initialize_ref Docker.ref
132
- initialize_logger(**kwargs)
136
+ initialize_logger kwargs[:log]
133
137
  initialize_env(**kwargs)
134
138
  @output[4] = merge_opts(kwargs[:args], @output[4]) if kwargs[:args]
135
139
  end
@@ -163,25 +167,21 @@ module Squared
163
167
  end
164
168
  cmd << '-a' if has_value!(args, 'a', 'all') && command != 'network'
165
169
  data = VAL_DOCKER[:ls][command.to_sym]
166
- cols = if has_value!(args, 's', 'standard')
167
- data.first(data.index('CreatedAt'))
168
- else
169
- [].tap do |out|
170
- args.each do |val|
171
- if val =~ /^(\d+)$/
172
- out << data[$1.to_i.pred]
173
- elsif val =~ /^(\d+)(-|\.{2,3})(\d+)$/
174
- j = $1.to_i.pred
175
- k = $3.to_i - ($2 == '..' ? 2 : 1)
176
- out.concat(data[j..k]) if k > j
177
- end
178
- end
179
- next unless out.empty?
180
-
181
- out.replace(choice_index('Select a column', data, multiple: true, attempts: 1))
182
- end
183
- end
184
- cmd << quote_option('format', "table #{cols.map! { |val| "{{.#{val}}}" }.join("\t")}")
170
+ if has_value!(args, 's', 'standard')
171
+ cols = data.first(data.index('CreatedAt'))
172
+ else
173
+ cols = args.each_with_object([]) do |val, out|
174
+ if val =~ /^(\d+)$/
175
+ out << data[$1.to_i.pred]
176
+ elsif val =~ /^(\d+)(-|\.{2,3})(\d+)$/
177
+ j = $1.to_i.pred
178
+ k = $3.to_i - ($2 == '..' ? 2 : 1)
179
+ out.concat(data[j..k]) if k > j
180
+ end
181
+ end
182
+ cols = choice_index('Select a column', data, multiple: true, attempts: 1) if cols.empty?
183
+ end
184
+ cmd << quote_option('format', "table #{cols.map { |val| "{{.#{val}}}" }.join("\t")}")
185
185
  run(cmd, banner: false, from: :ls)
186
186
  end
187
187
  end
@@ -199,14 +199,14 @@ module Squared
199
199
  break unless bake?
200
200
 
201
201
  case flag
202
- when :build
203
- format_desc action, flag, 'opts*,target*,context|:'
202
+ when :build, :compose
203
+ format_desc action, flag, 'opts*,target*,context?|:'
204
204
  task flag do |_, args|
205
205
  args = args.to_a
206
206
  if args.first == ':'
207
207
  choice_command :bake
208
208
  else
209
- buildx :bake, args
209
+ buildx(:bake, args, from: (:'buildx:bake' if flag == :compose))
210
210
  end
211
211
  end
212
212
  when :check
@@ -239,6 +239,13 @@ module Squared
239
239
  compose!(flag, [command], service: service.empty? || service)
240
240
  end
241
241
  end
242
+ when :publish
243
+ next unless @oci
244
+
245
+ format_desc action, flag, 'tag?,repository?,opts*'
246
+ task flag, [:tag] do |_, args|
247
+ compose! flag, args.to_a
248
+ end
242
249
  else
243
250
  format_desc action, flag, 'opts*,service*|:'
244
251
  task flag do |_, args|
@@ -275,6 +282,13 @@ module Squared
275
282
  when 'image'
276
283
  case flag
277
284
  when :push
285
+ next unless @registry
286
+
287
+ format_desc action, flag, 'tag?,registry/username?,opts*'
288
+ task flag, [:tag] do |_, args|
289
+ image flag, args.to_a
290
+ end
291
+ when :pull
278
292
  format_desc action, flag, 'tag,registry/username?,opts*'
279
293
  task flag, [:tag] do |_, args|
280
294
  id = param_guard(action, flag, args: args, key: :tag)
@@ -282,18 +296,14 @@ module Squared
282
296
  end
283
297
  else
284
298
  format_desc(action, flag, case flag
285
- when :rm, :save then 'id,opts*'
286
- when :tag then 'version'
299
+ when :rm, :save then 'id*,opts*'
300
+ when :tag then 'version?'
287
301
  else 'opts*,args*'
288
- end, before: 'pattern?')
302
+ end)
289
303
  task flag do |_, args|
290
304
  args = args.to_a
291
- n = args.size
292
- if (n > 1 || (flag == :ls && n > 0)) && OptionPartition.pattern?(args.first)
293
- filter = args.shift
294
- end
295
305
  if !args.empty? || flag == :ls
296
- image(flag, args, filter: filter)
306
+ image flag, args
297
307
  else
298
308
  choice_command flag
299
309
  end
@@ -319,8 +329,8 @@ module Squared
319
329
  def clean(*, sync: invoked_sync?('clean'), **)
320
330
  if runnable?(@clean)
321
331
  super
322
- elsif sync || option('y', prefix: 'docker')
323
- image :rm
332
+ else
333
+ image(:rm, sync: sync)
324
334
  end
325
335
  end
326
336
 
@@ -353,7 +363,7 @@ module Squared
353
363
  [args, flags].each_with_index do |item, i|
354
364
  next unless item && (data = append_any(item, target: []))
355
365
 
356
- ret.merge(data.map! { |arg| i == 0 ? fill_option(arg) : quote_option('build-arg', arg) })
366
+ ret.merge(data.map { |arg| i == 0 ? fill_option(arg) : quote_option('build-arg', arg) })
357
367
  end
358
368
  case from
359
369
  when :run
@@ -361,13 +371,13 @@ module Squared
361
371
  when String
362
372
  ret << quote_option('secret', @secrets, double: true)
363
373
  when Hash
364
- append = lambda do |type|
365
- Array(@secrets[type]).each { |arg| ret << quote_option('secret', "type=#{type},#{arg}", double: true) }
374
+ append = lambda do |key|
375
+ ret.merge(Array(@secrets[key]).map { |arg| quote_option('secret', "type=#{key},#{arg}", double: true) })
366
376
  end
367
377
  append.call(:file)
368
378
  append.call(:env)
369
379
  else
370
- Array(@secrets).each { |arg| ret << quote_option('secret', arg) }
380
+ ret.merge(Array(@secrets).map { |arg| quote_option('secret', arg) })
371
381
  end
372
382
  if (val = option('tag', ignore: false))
373
383
  append_tag val
@@ -381,18 +391,18 @@ module Squared
381
391
  ret
382
392
  end
383
393
 
384
- def buildx(flag, opts = [], tag: nil, context: nil)
394
+ def buildx(flag, opts = [], tag: nil, context: nil, from: nil)
385
395
  cmd, opts = docker_session('buildx', opts: opts)
386
- op = OPT_DOCKER[:buildx].yield_self do |data|
387
- OptionPartition.new(opts, data[:common], cmd, project: self)
388
- .append(flag, quote: false)
389
- .parse(data[flag == :bake ? :bake : :build] + data[:shared])
390
- end
396
+ data = OPT_DOCKER[:buildx]
397
+ op = OptionPartition.new(opts, data[:common], cmd, project: self)
398
+ op.append(flag, quote: false)
399
+ .parse(data[flag == :bake ? :bake : :build] + data[:shared])
391
400
  case flag
392
401
  when :build, :context
393
402
  append_tag(tag || option('tag', ignore: false) || self.tag)
394
403
  append_context context
395
404
  when :bake
405
+ append_file(0, index: 3) unless from || op.arg?('f', 'file') || !anypath?(*COMPOSEFILE)
396
406
  unless op.empty?
397
407
  args = op.dup
398
408
  op.reset
@@ -408,11 +418,11 @@ module Squared
408
418
  end
409
419
  end
410
420
  op.clear(pass: false)
411
- run(from: :"buildx:#{flag}")
421
+ run(from: from || symjoin('buildx', flag))
412
422
  end
413
423
 
414
- def compose!(flag, opts = [], service: nil, multiple: false)
415
- from = :"compose:#{flag}"
424
+ def compose!(flag, opts = [], id: nil, service: nil, multiple: false)
425
+ from = symjoin 'compose', flag
416
426
  if flag == :service
417
427
  command = opts.first
418
428
  if service == true
@@ -426,23 +436,31 @@ module Squared
426
436
  else
427
437
  cmd, opts = docker_session('compose', opts: opts)
428
438
  op = OptionPartition.new(opts, OPT_DOCKER[:compose][:common], cmd, project: self)
429
- append_file filetype unless op.arg?('f', 'file')
439
+ append_file(filetype, force: flag == :publish) unless op.arg?('f', 'file')
430
440
  op << flag
431
441
  op.parse(OPT_DOCKER[:compose].fetch(flag, []))
432
- if op.remove(':') || service == ':'
433
- keys = Set.new
434
- read_composefile('services', target: op.values_of('f', 'file')) { |data| keys.merge(data.keys) }
435
- service = unless keys.empty?
436
- choice_index('Add services', keys, multiple: multiple, force: !multiple,
437
- attempts: multiple ? 1 : 3)
438
- end
439
- end
440
- if multiple
441
- op.concat(service) if service
442
- op.append(delim: true, escape: true, strip: /^:/)
442
+ if flag == :publish
443
+ id ||= option('tag', ignore: false) || op.shift || tagmain
444
+ registry ||= option('registry') || op.shift || @oci
445
+ emptyargs op, flag
446
+ op << shell_quote(tagjoin(registry, id))
447
+ return unless confirm_command(op.to_s, title: from, target: id)
443
448
  else
444
- raise_error ArgumentError, 'no service was selected', hint: flag unless service
445
- append_command(flag, service, op.extras, prompt: '::')
449
+ if op.remove(':') || service == ':'
450
+ keys = Set.new
451
+ read_composefile('services', target: op.values_of('f', 'file')) { |data| keys.merge(data.keys) }
452
+ service = unless keys.empty?
453
+ choice_index('Add services', keys, multiple: multiple, force: !multiple,
454
+ attempts: multiple ? 1 : 3)
455
+ end
456
+ end
457
+ if multiple
458
+ op.concat(service) if service
459
+ op.append(delim: true, escape: true, strip: /^:/)
460
+ else
461
+ raise_error ArgumentError, 'no service was selected', hint: flag unless service
462
+ append_command(flag, service, op.extras, prompt: '::')
463
+ end
446
464
  end
447
465
  end
448
466
  run(from: from)
@@ -450,35 +468,32 @@ module Squared
450
468
 
451
469
  def container(flag, opts = [], id: nil)
452
470
  cmd, opts = docker_session('container', flag, opts: opts)
453
- rc = flag == :run || flag == :create
454
- op = OPT_DOCKER[:container].yield_self do |data|
455
- list = data.fetch(flag, [])
456
- list += data[:create] if flag == :run
457
- list += data[:update] if rc
458
- OptionPartition.new(opts, list, cmd, project: self, args: rc || flag == :exec)
459
- end
460
- from = :"container:#{flag}"
471
+ data = OPT_DOCKER[:container]
472
+ list = data.fetch(flag, [])
473
+ list += data[:create] if (rc = flag == :run)
474
+ list += data[:update] if rc ||= flag == :create
475
+ op = OptionPartition.new(opts, list, cmd, project: self, args: rc || flag == :exec)
476
+ from = symjoin 'container', flag
461
477
  case flag
462
478
  when :run, :create, :exec
463
479
  if rc && !op.arg?('mount')
464
480
  all = collect_hash VAL_DOCKER[:run]
465
481
  delim = Regexp.new(",\\s*(?=#{all.join('|')})")
466
482
  Array(@mounts).each do |val|
467
- args = []
468
483
  type = nil
469
- val.split(delim).each do |opt|
470
- k, v, q = split_option opt
484
+ args = val.split(delim).each_with_object([]) do |opt, out|
485
+ k, v, q = OptionPartition.parse_option(opt)
471
486
  if k == 'type'
472
487
  case v
473
488
  when 'bind', 'volume', 'image', 'tmpfs'
474
489
  type = v
475
490
  else
476
- raise_error TypeError, "unknown: #{v}", hint: flag
491
+ raise_error TypeError, "unknown: #{v || "''"}", hint: flag
477
492
  end
478
493
  elsif all.include?(k)
479
494
  unless type
480
- VAL_DOCKER[:run].each_pair do |key, a|
481
- next unless a.include?(k)
495
+ VAL_DOCKER[:run].each_pair do |key, items|
496
+ next unless items.include?(k)
482
497
 
483
498
  type = key.to_s unless key == :common
484
499
  break
@@ -486,13 +501,14 @@ module Squared
486
501
  end
487
502
  case k
488
503
  when 'readonly', 'ro'
489
- args << k
504
+ out << k
490
505
  next
491
506
  when 'source', 'src', 'destination', 'dst', 'target', 'volume-subpath', 'image-path'
507
+ raise_error ArgumentError, "#{k}: no path value", hint: flag unless v
492
508
  v = basepath v
493
509
  v = shell_quote(v, option: false, force: false) if q == ''
494
510
  end
495
- args << "#{k}=#{q + v + q}"
511
+ out << "#{k}=#{q}#{v}#{q}"
496
512
  elsif !silent?
497
513
  log_message('unrecognized option', subject: from, hint: k)
498
514
  end
@@ -518,7 +534,7 @@ module Squared
518
534
  opts = []
519
535
  append_option('platform', target: opts, equals: true)
520
536
  opts << case option('disable-content-trust', ignore: false)
521
- when 'false', '0'
537
+ when '0', 'false'
522
538
  '--disable-content-trust=false'
523
539
  else
524
540
  '--disable-content-trust'
@@ -537,12 +553,12 @@ module Squared
537
553
  run(from: from)
538
554
  end
539
555
 
540
- def image(flag, opts = [], sync: true, id: nil, registry: nil, filter: nil)
556
+ def image(flag, opts = [], sync: true, id: nil, registry: nil)
541
557
  cmd, opts = docker_session('image', flag, opts: opts)
542
558
  op = OptionPartition.new(opts, OPT_DOCKER[:image].fetch(flag, []), cmd, project: self)
543
- exception = self.exception
559
+ exception = exception?
544
560
  banner = true
545
- from = :"image:#{flag}"
561
+ from = symjoin 'image', flag
546
562
  case flag
547
563
  when :ls
548
564
  if opts.size == op.size
@@ -555,7 +571,7 @@ module Squared
555
571
  opts.delete(val)
556
572
  break
557
573
  end
558
- list_image(:run, filter: filter, from: from) do |val|
574
+ list_image(:run, from: from) do |val|
559
575
  container(:run, if name
560
576
  opts.dup << "name=#{index == 0 ? name : "#{name}-#{index}"}"
561
577
  else
@@ -569,7 +585,7 @@ module Squared
569
585
  when :rm
570
586
  unless id
571
587
  if op.empty?
572
- list_image(:rm, filter: filter, from: from) { |val| image(:rm, opts, sync: sync, id: val) }
588
+ list_image(:rm, from: from) { |val| image(:rm, opts, sync: sync, id: val) }
573
589
  else
574
590
  op.each { |val| run(cmd.temp(val), sync: sync, from: from) }
575
591
  end
@@ -581,51 +597,48 @@ module Squared
581
597
  banner = false
582
598
  end
583
599
  when :tag, :save
584
- found = false
585
- list_image(flag, filter: filter, from: from) do |val|
600
+ list_image(flag, from: from) do |val|
586
601
  op << val
587
- found = true
588
- if flag == :tag
589
- op << tagname("#{project}:#{op.first}")
590
- break
591
- end
602
+ next unless flag == :tag
603
+
604
+ op << tagname("#{project}:#{op.first}")
605
+ break
592
606
  end
593
- raise_error ArgumentError, 'target not specified', hint: flag unless found
594
- when :push
595
- id ||= option('tag', ignore: false) || tagmain
596
- registry ||= op.shift || option('registry') || @registry
597
- unless id && op.empty?
598
- if id
599
- raise_error ArgumentError, "unrecognized args: #{op.join(', ')}", hint: flag
600
- else
601
- raise_error 'no id/tag', hint: flag
602
- end
607
+ when :pull
608
+ if !id
609
+ id = tagmain
610
+ elsif !op.arg?('a', 'all-tags') && !id.include?(':')
611
+ id = "#{project}:#{id}"
612
+ end
613
+ unless registry
614
+ registry = op.shift
615
+ registry ||= option('registry') || @registry unless id.include?('/')
603
616
  end
617
+ cmd << shell_quote(tagjoin(registry, id))
618
+ when :push
619
+ id ||= option('tag', ignore: false) || op.shift || tagmain
620
+ registry ||= option('registry') || op.shift || @registry
621
+ emptyargs op, flag
604
622
  raise_error ArgumentError, 'username/registry not specified', hint: flag unless registry
605
- registry.chomp!('/')
606
- uri = shell_quote "#{registry}/#{id}"
623
+ uri = shell_quote tagjoin(registry, id)
607
624
  op << uri
608
625
  img = docker_output 'image', 'tag', id, uri
609
626
  return unless confirm_command(img.to_s, cmd.to_s, target: id, as: registry, title: from)
610
627
 
611
- cmd = img
628
+ @session = img
612
629
  sync = false
613
- exception = true
630
+ exception ||= true
614
631
  banner = false
615
632
  end
616
- run(cmd, sync: sync, exception: exception, banner: banner, from: from).tap do |ret|
617
- success?(ret, flag == :tag || flag == :save)
618
- end
633
+ success?(run(sync: sync, exception: exception, banner: banner, from: from), flag == :tag || flag == :save)
619
634
  end
620
635
 
621
636
  def network(flag, opts = [], target: nil)
622
637
  cmd, opts = docker_session('network', flag, opts: opts)
623
638
  OptionPartition.new(opts, OPT_DOCKER[:network].fetch(flag, []), cmd, project: self)
624
639
  .clear
625
- from = :"network:#{flag}"
626
- list_image(flag, docker_output('ps -a'), from: from) do |img|
627
- success?(run(cmd.temp(target, img), from: from))
628
- end
640
+ from = symjoin 'network', flag
641
+ list_image(flag, docker_output('ps -a'), from: from) { |id| success?(run(cmd.temp(target, id), from: from)) }
629
642
  end
630
643
 
631
644
  def build?
@@ -674,7 +687,7 @@ module Squared
674
687
  elsif (data = doc.dig(*keys))
675
688
  yield data
676
689
  end
677
- rescue StandardError => e
690
+ rescue => e
678
691
  log.debug e
679
692
  end
680
693
  end
@@ -691,11 +704,11 @@ module Squared
691
704
  end
692
705
 
693
706
  def append_command(flag, val, list, target: @session, prompt: ':')
694
- if list.delete(prompt)
695
- list << readline('Enter command [args]', force: flag == :exec)
696
- else
697
- env('DOCKER_ARGS') { |args| list << args }
698
- end
707
+ list << if list.delete(prompt)
708
+ readline('Enter command [args]', force: flag == :exec)
709
+ else
710
+ env('DOCKER_ARGS')
711
+ end
699
712
  case flag
700
713
  when :run
701
714
  unless session_arg?('name', target: target)
@@ -708,10 +721,10 @@ module Squared
708
721
  target << list.join(' && ') unless list.empty?
709
722
  end
710
723
 
711
- def append_file(type, target: @session, index: 2)
712
- return if !@file || (ENV['COMPOSE_FILE'] && compose?(type))
724
+ def append_file(type, target: @session, index: 2, force: false)
725
+ return unless @file && !(ENV['COMPOSE_FILE'] && compose?(type))
713
726
 
714
- unless @file.is_a?(Array)
727
+ unless @file.is_a?(Array) || force
715
728
  case type
716
729
  when 2, 4
717
730
  return
@@ -719,14 +732,7 @@ module Squared
719
732
  return unless COMPOSEFILE.select { |val| basepath!(val) }.size > 1
720
733
  end
721
734
  end
722
- files = Array(@file).map { |val| quote_option('file', basepath(val)) }
723
- if target.is_a?(Set)
724
- opts = target.to_a.insert(index, *files)
725
- target.clear
726
- .merge(opts)
727
- else
728
- target.insert(index, *files)
729
- end
735
+ target.insert(index, *Array(@file).map { |val| quote_option('file', basepath(val)) })
730
736
  end
731
737
 
732
738
  def append_context(ctx = nil, target: @session)
@@ -740,7 +746,7 @@ module Squared
740
746
  ver = option('version', target: target, ignore: false)
741
747
  case val
742
748
  when String
743
- split_escape val
749
+ val.split(',')
744
750
  else
745
751
  Array(val)
746
752
  end.each do |s|
@@ -779,20 +785,12 @@ module Squared
779
785
  [cmd, status, no]
780
786
  end
781
787
 
782
- def list_image(flag, cmd = docker_output('image ls -a'), filter: nil, hint: nil, no: true, from: nil)
788
+ def list_image(flag, cmd = docker_output('image ls -a'), hint: nil, no: true, from: nil)
783
789
  pwd_set(from: from) do
784
790
  index = 1
785
791
  all = option('all', prefix: 'docker')
786
792
  y = from == :'image:rm' && option('y', prefix: 'docker')
787
- filter = env('DOCKER_FILTER', filter).to_s
788
- pat = if OptionPartition.pattern?(filter)
789
- Regexp.new(filter)
790
- elsif filter.match?(/[:_-]$/)
791
- /\b#{Regexp.escape(filter)}/
792
- else
793
- filter = filter.empty? ? '(?:[:_-]|$)' : "[:_-]#{filter}"
794
- /\b(?:#{dnsname(name)}|#{tagname(project)}|#{tagmain.split(':', 2).first})#{filter}/
795
- end
793
+ pat = /\b(?:#{dnsname(name)}|#{tagname(project)}|#{tagmain.split(':', 2).first})\b/
796
794
  IO.popen(cmd.temp('--format=json')).each do |line|
797
795
  data = JSON.parse(line)
798
796
  id = data['ID']
@@ -841,7 +839,7 @@ module Squared
841
839
  end
842
840
  list_empty(hint: hint || from) if index == 1 && !y
843
841
  end
844
- rescue StandardError => e
842
+ rescue => e
845
843
  on_error e, from
846
844
  end
847
845
 
@@ -913,18 +911,18 @@ module Squared
913
911
  when :tag
914
912
  args = tagjoin @registry, tag
915
913
  when :save
916
- opts = "#{opts}.tar" unless opts.end_with?('.tar')
914
+ opts = opts.sub_ext('.tar')
917
915
  cmd << quote_option('output', File.expand_path(opts))
918
916
  if args
919
- cmd << basic_option('platform', args)
917
+ cmd << quote_option('platform', args)
920
918
  args = nil
921
919
  end
922
920
  else
923
921
  cmd << opts << '--'
924
922
  end
925
- cmd.merge(Array(out).map! { |val| val.split(/\s+/, 2).first })
923
+ cmd.merge(Array(out).map { |val| val.split(/\s+/, 2).first })
926
924
  cmd << args
927
- success?(run(cmd), ctx.start_with?(/network|tag|save/))
925
+ success?(run(cmd), ctx.start_with?('network', 'tag', 'save'))
928
926
  end
929
927
 
930
928
  def filetype(val = dockerfile)
@@ -947,11 +945,12 @@ module Squared
947
945
  end
948
946
 
949
947
  def tagjoin(*args, char: '/')
950
- args.join(char) unless (args = args.compact).empty?
948
+ args.compact!
949
+ args.map { |val| val.chomp(char) }.join(char) unless args.empty?
951
950
  end
952
951
 
953
952
  def tagname(val)
954
- val = val.split(':').map! { |s| charname(s.sub(/^\W+/, '')) }
953
+ val = val.split(':').map { |s| charname(s.sub(/^\W+/, '')) }
955
954
  ret = val.join(':')
956
955
  ret = val.first if val.size > 1 && ret.size > 128
957
956
  ret[0..127]
@@ -968,6 +967,14 @@ module Squared
968
967
  def tagmain
969
968
  tag.is_a?(Array) ? tag.first : tag
970
969
  end
970
+
971
+ def emptyargs(list, hint = nil)
972
+ raise_error ArgumentError, "unrecognized args: #{list.join(', ')}", hint: hint unless list.empty?
973
+ end
974
+
975
+ def anypath?(*args)
976
+ args.any? { |val| basepath!(val) }
977
+ end
971
978
  end
972
979
 
973
980
  Application.implement Docker