squared 0.5.26 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ unless defined?(Readline)
4
+ if RUBY_ENGINE == 'ruby' && RUBY_VERSION < '2.7'
5
+ require 'readline'
6
+ else
7
+ begin
8
+ require 'reline'
9
+ Object.send(:remove_const, :Readline) if Object.const_defined?(:Readline)
10
+ Readline = Reline
11
+ rescue LoadError
12
+ require 'readline'
13
+ end
14
+ end
15
+ end
16
+
3
17
  module Squared
4
18
  module Common
5
19
  module Prompt
6
20
  module_function
7
21
 
8
- def confirm(msg, default = nil, agree: 'Y', cancel: 'N', attempts: 5, timeout: 30)
9
- require 'readline'
22
+ def confirm(msg, default = nil, agree: 'Y', cancel: 'N', attempts: 3, timeout: 60)
10
23
  require 'timeout'
11
24
  if agree == 'Y' && cancel == 'N' && !msg.match?(%r{\[(?:Yn|nY|Y/n|y/N)\]})
12
25
  case default
@@ -38,9 +51,8 @@ module Squared
38
51
  end
39
52
  end
40
53
 
41
- def choice(msg, list = nil, min: 1, max: 1, multiple: false, force: true, grep: nil, auto: true,
42
- attempts: 5, timeout: 0)
43
- require 'readline'
54
+ def choice(msg, list = nil, min: 1, max: 1, multiple: false, index: false, grep: nil, border: nil, auto: true,
55
+ force: true, attempts: 3, timeout: 0)
44
56
  require 'timeout'
45
57
  if list
46
58
  grep &&= Array(grep).map { |val| Regexp.new(val) }
@@ -52,40 +64,51 @@ module Squared
52
64
  puts '%2d. %s' % [items.size, val]
53
65
  end
54
66
  max = items.size
55
- raise_error 'empty selection list' if max == 0
67
+ raise ArgumentError, 'empty selection list' if max == 0
68
+
56
69
  min = grep ? 1 : [min, max].min
57
70
  if auto
58
- msg = "#{msg}: [#{min}-#{max}#{if multiple
59
- "|,#{multiple.is_a?(::Numeric) ? "{#{multiple}}" : ''}"
60
- end}] "
71
+ auto.times { puts } if auto.is_a?(::Numeric)
72
+ if border == true
73
+ puts print_footer
74
+ elsif border
75
+ puts print_footer(border: border)
76
+ end
77
+ msg = "#{msg + (force ? ':' : '?')} [#{min}-#{max}#{if (n = multiple)
78
+ "|,#{n.is_a?(::Numeric) ? "{#{n}}" : '*'}"
79
+ end}] "
61
80
  end
62
81
  end
63
- valid = ->(s) { s.match?(/^-?\d+$/) && s.to_i.between?(min, max <= min ? Float::INFINITY : max) }
82
+ between = ->(s) { s.match?(/^\d+$/) && s.to_i.between?(min, max) }
64
83
  Timeout.timeout(timeout) do
65
84
  while (ch = Readline.readline(msg))
66
85
  unless (ch = ch.strip).empty?
67
86
  if multiple
68
- a = ch.split(',')
69
- b = a.map do |s|
70
- s.strip!
71
- if s =~ /^(\d+)-(\d+)$/
72
- next unless valid.call($1) && valid.call($2)
87
+ k = if ch == '*'
88
+ (min..max).to_a
89
+ else
90
+ ch.split(/\s*,\s*/).map! do |s|
91
+ if s =~ /^(\d+)-(\d+)$/
92
+ next unless between.call($1) && between.call($2)
73
93
 
74
- c = $1.to_i
75
- d = $2.to_i
76
- next (c..d).to_a if c < d
77
- elsif valid.call(s)
78
- s.to_i
94
+ i = $1.to_i
95
+ j = $2.to_i
96
+ next (i..j).to_a if i < j
97
+ elsif between.call(s)
98
+ s.to_i
99
+ end
100
+ end
101
+ end
102
+ unless k.include?(nil)
103
+ k.flatten!
104
+ k.uniq!
105
+ k.sort!
106
+ unless multiple.is_a?(::Numeric) && multiple != k.size
107
+ return index || !items ? k : k.map! { |i| items[i.pred] }
79
108
  end
80
109
  end
81
- unless b.include?(nil)
82
- b.flatten!
83
- b.uniq!
84
- b.sort!
85
- return items ? b.map! { |i| items[i - 1] } : b unless multiple.is_a?(::Numeric) && multiple != b.size
86
- end
87
- elsif valid.call(ch)
88
- return items ? items[ch.to_i - 1] : ch.to_i
110
+ elsif between.call(ch)
111
+ return index || !items ? ch.to_i : items[ch.to_i.pred]
89
112
  end
90
113
  end
91
114
  attempts -= 1
@@ -98,12 +121,11 @@ module Squared
98
121
  puts
99
122
  exit 0
100
123
  else
101
- multiple ? [] : nil
124
+ [] if multiple
102
125
  end
103
126
  end
104
127
 
105
128
  def readline(msg, history = false, force: nil, multiline: nil, &blk)
106
- require 'readline'
107
129
  multiline = if multiline && Readline.respond_to?(:readmultiline)
108
130
  multiline.is_a?(::Enumerable) || block_given? ? multiline : [multiline.to_s]
109
131
  end
@@ -113,20 +135,16 @@ module Squared
113
135
  elsif block_given?
114
136
  Readline.readmultiline(msg, history, &blk)
115
137
  else
116
- Readline.readmultiline(msg, history) do |line|
117
- next if line.strip.empty?
118
-
119
- multiline.any? { |val| line.split.last.end_with?(val.to_s) }
120
- end
138
+ Readline.readmultiline(msg, history) { |line| multiline.any? { |val| line.split.last.end_with?(val.to_s) } }
121
139
  end
122
140
  end
123
141
  case force
124
142
  when ::TrueClass, ::FalseClass
125
- msg = "#{msg} %s " % if multiline
126
- multiline.is_a?(::Enumerable) ? "{#{multiline.to_a.join('|')}}" : multiline
127
- else
128
- "(#{force ? 'required' : 'optional'}):"
129
- end
143
+ msg = "#{msg}%s%s " % if multiline
144
+ [' ', multiline.is_a?(::Enumerable) ? "{#{multiline.to_a.join('|')}}" : multiline]
145
+ else
146
+ [force ? ':' : '?', '']
147
+ end
130
148
  ret = (prompt.call || '').strip
131
149
  multiline.each { |val| break if ret.delete_suffix!(val.to_s) } if multiline.is_a?(::Enumerable)
132
150
  exit 1 if force && ret.empty?
@@ -6,14 +6,11 @@ require 'shellwords'
6
6
  module Squared
7
7
  module Common
8
8
  module Shell
9
- QUOTE_VALUE = /\A(["'])(.*)\1\z/m.freeze
10
- private_constant :QUOTE_VALUE
11
-
12
9
  module_function
13
10
 
14
11
  def shell_escape(val, quote: false, force: false, double: false, option: false, override: false)
15
- if (r = /\A(--?)([^=\s]+)((=|\s+)(["'])?(?(5)(.*)\5|(.*)))?\z/m.match(val = val.to_s))
16
- if (data = r[2].match(QUOTE_VALUE))
12
+ if (r = /\A(--?)([^= ]+)((=|\s+)(["'])?(?(5)(.*)\5|(.*)))?\z/m.match(val = val.to_s))
13
+ if (data = r[2].match(/\A(["'])(.+)\1\z/))
17
14
  double = data[1] == '"'
18
15
  override = true
19
16
  elsif !r[3] || r[6]
@@ -28,9 +25,9 @@ module Squared
28
25
 
29
26
  r[7]
30
27
  end
31
- r[1] + (data ? data[2] : r[2]) + r[4] + shell_quote(opt, force: force, double: double, override: override)
32
- elsif option && val =~ /\A(-{0,2}[^=\s-][^=\s]*)=(.+)\z/m
33
- return val if $2.match?(QUOTE_VALUE)
28
+ r[1] + (data ? data[2] : r[2]) + r[4] + shell_quote(opt, double: double, force: force, override: override)
29
+ elsif option && val =~ /\A([^=]+)=(.+)\z/m
30
+ return val if $2.match?(/\A(["']).+\1\z/m)
34
31
 
35
32
  "#{$1}=%s" % if $2.include?(' ')
36
33
  shell_quote($2, option: false)
@@ -40,7 +37,7 @@ module Squared
40
37
  Shellwords.escape($2)
41
38
  end
42
39
  elsif Rake::Win32.windows?
43
- quote ? shell_quote(val, force: force, double: double) : val
40
+ quote ? shell_quote(val, double: double, force: force) : val
44
41
  elsif val.empty?
45
42
  ''
46
43
  else
@@ -48,67 +45,46 @@ module Squared
48
45
  end
49
46
  end
50
47
 
51
- def shell_quote(val, option: true, force: true, double: false, preserve: true, pass: false, override: false)
48
+ def shell_quote(val, option: true, force: true, double: false, override: false)
52
49
  val = val.to_s
53
50
  return val if (!force && !val.include?(' ')) || val.empty?
54
- return val if option && val.match?(/\A(?:-[^=\s-](?:=|\s+)?|(--)?[^=\s-][^=\s]*(?(1)(?:=|\s+)|=))(["']).+\2\z/m)
55
51
 
56
- if val =~ QUOTE_VALUE
57
- if pass == '"' || pass == "'"
58
- return val if pass == $1
59
- elsif pass || ($1 == '"' && Rake::Win32.windows? && val.match?(/(?:[#{File::SEPARATOR} ]|\\")/o))
60
- return val
61
- end
62
- case preserve
63
- when false
64
- base = $2
65
- when '"'
66
- base = $2 if $1 == "'"
67
- when "'"
68
- base = $2 if $1 == '"'
69
- end
70
- end
71
- q = -> { (base || val).gsub("'\\\\''", "'") }
72
- if double || Rake::Win32.windows? || (ARG[:QUOTE] == '"' && !override)
73
- "\"#{q.call.gsub(/(?<!\\)"/, '\\"')}\""
52
+ if option && val.match?(/(?:\A|\A[^=\s]+(?:=|\s+)|#{Rake::Win32.windows? ? '[\\\/]' : '\/'})(["']).+\1\z/mo)
53
+ val
54
+ elsif double || Rake::Win32.windows? || (ARG[:QUOTE] == '"' && !override)
55
+ "\"#{val.gsub(/(?<!\\)"/, '\\"')}\""
74
56
  else
75
- "'#{q.call.gsub("'", "'\\\\''")}'"
57
+ "'#{val.gsub("'", "'\\\\''")}'"
76
58
  end
77
59
  end
78
60
 
79
- def shell_option(flag, val = nil, escape: true, quote: true, option: true, force: true, double: false,
80
- merge: false, override: false)
61
+ def shell_option(flag, val = nil, sep: '=', escape: true, quote: true, force: true, double: false, merge: false,
62
+ override: false)
81
63
  flag = flag.to_s
82
- if flag =~ QUOTE_VALUE
64
+ if flag =~ /\A(["'])(.+)\1\z/
83
65
  double = $1 == '"'
84
66
  flag = $2
85
67
  escape = false
86
68
  override = true
87
69
  end
88
- sep = unless flag.empty?
89
- if flag[0] == '-'
90
- if flag[1] == '-'
91
- '='
92
- else
93
- merge ? '' : ' '
94
- end
95
- elsif flag.size == 1
96
- pre = '-'
97
- merge ? '' : ' '
98
- else
99
- pre = '--'
100
- '='
101
- end
102
- end
103
- "#{pre}#{flag}#{unless val.nil?
104
- "#{sep}#{if escape
105
- shell_escape(val, quote: quote, double: double, override: override)
106
- elsif quote
107
- shell_quote(val, option: option, force: force, double: double, override: override)
108
- else
109
- val
110
- end}"
111
- end}"
70
+ b = if flag[0] == '-'
71
+ flag[1] == '-' ? sep : ' '
72
+ elsif flag.size == 1
73
+ a = '-'
74
+ merge ? '' : ' '
75
+ else
76
+ a = '--'
77
+ sep
78
+ end
79
+ "#{a}#{flag}#{unless val.nil?
80
+ "#{b}#{if escape
81
+ shell_escape(val, quote: quote, double: double, override: override)
82
+ elsif quote
83
+ shell_quote(val, option: false, force: force, double: double, override: override)
84
+ else
85
+ val
86
+ end}"
87
+ end}"
112
88
  end
113
89
 
114
90
  def shell_split(val, join: nil, **kwargs)
@@ -118,12 +94,45 @@ module Squared
118
94
  ret.join(join.is_a?(::String) ? join : ' ')
119
95
  end
120
96
 
97
+ def shell_parse(val, escape: false, force: true, **kwargs)
98
+ a = []
99
+ b = []
100
+ c = []
101
+ d = []
102
+ e = [a, b]
103
+ j = -1
104
+ val.shellsplit.each_with_index do |opt, i|
105
+ if opt == '--'
106
+ e = [c, d]
107
+ elsif opt =~ /\A--?[^=]+(=|\z)/
108
+ j = $1 == '=' ? -1 : i
109
+ e[0] << [opt]
110
+ elsif j >= 0
111
+ e[0][j] << opt
112
+ else
113
+ e[1] << shell_quote(opt, option: false, force: force)
114
+ end
115
+ end
116
+ ret = [[a, b], [], [c, d]].flat_map do |e, f|
117
+ next '--' unless e
118
+
119
+ e.flat_map do |item|
120
+ if item.size == 1
121
+ fill_option(item.first)
122
+ else
123
+ flag = item.shift
124
+ item.map! { |s| shell_option(flag, s, escape: escape, force: force, **kwargs) }
125
+ end
126
+ end.concat(f)
127
+ end
128
+ ret.pop if ret.last == '--'
129
+ ret
130
+ end
131
+
121
132
  def shell_bin(name, env: true)
122
- require_relative 'base'
123
- key = name.to_s.upcase
124
- key = File.basename(key, '.*') if Rake::Win32.windows?
125
- shell_quote((env && ENV["PATH_#{key}"]) || PATH[key] || PATH[key.to_sym] || name,
126
- option: false, force: false, double: true)
133
+ key = name.upcase
134
+ shell_quote((env && ENV["PATH_#{key}"]) || PATH[key] || PATH[key.to_sym] || name, option: false, force: false,
135
+ double: true)
127
136
  end
128
137
 
129
138
  def line_width(lines)
@@ -132,7 +141,6 @@ module Squared
132
141
  end
133
142
 
134
143
  def fill_option(val, **kwargs)
135
- return val unless val.is_a?(::String)
136
144
  return "-#{val}" if val.match?(/\A(?:[a-z]\d*|\d)\z/i)
137
145
 
138
146
  shell_escape(val.start_with?('-') ? val : "--#{val}", **kwargs)
@@ -6,10 +6,28 @@ require 'rake'
6
6
  module Squared
7
7
  module Common
8
8
  module System
9
+ class << self
10
+ private
11
+
12
+ def parse_link(val)
13
+ case val
14
+ when ::TrueClass, 's'
15
+ 1
16
+ when 'r'
17
+ 2
18
+ when 'h'
19
+ 3
20
+ else
21
+ raise ArgumentError, "unrecognized 'link' flag: #{val}" if val
22
+
23
+ 0
24
+ end
25
+ end
26
+ end
27
+
9
28
  module_function
10
29
 
11
30
  def shell(*args, name: :system, **kwargs)
12
- kwargs.delete(:exception) unless name == :system
13
31
  if RUBY_ENGINE == 'jruby' && Rake::Win32.windows?
14
32
  e = kwargs[:exception]
15
33
  if (dir = kwargs[:chdir]) && ((pwd = Dir.pwd) != dir)
@@ -25,16 +43,16 @@ module Squared
25
43
  else
26
44
  return Kernel.send(name, *args, **kwargs)
27
45
  end
28
- return ret unless e && !ret
46
+ return ret unless e && !ret && name == :system
29
47
 
30
48
  raise $?.to_s
31
49
  end
32
50
 
33
- def copy_dir(src, dest, glob = ['**/*'], create: false, link: nil, force: false, pass: nil, hidden: false,
34
- verbose: true)
51
+ def copy_dir(src, dest, glob = ['**/*'], create: false, link: nil, preserve: nil, force: false, verbose: true,
52
+ pass: nil, hidden: false)
35
53
  base = Pathname.new(src)
36
54
  target = Pathname.new(dest)
37
- raise "#{target.cleanpath} (not found)" if !create && !target.parent.exist?
55
+ raise Errno::ENOENT, dest.cleanpath.to_s unless create || target.parent.exist?
38
56
 
39
57
  subdir = {}
40
58
  target.mkpath if create
@@ -58,53 +76,67 @@ module Squared
58
76
  end
59
77
  count = 0
60
78
  soft = 0
79
+ type = System.send :parse_link, link
61
80
  subdir.each do |dir, files|
62
- if link
63
- files.dup.tap do |items|
64
- files.clear
65
- items.each do |file|
66
- if file.exist?
67
- if !file.symlink?
68
- files << file
69
- elsif !force
70
- next
71
- end
72
- end
73
- if link == 'hard'
74
- FileUtils.ln(file, dir, force: force, verbose: false)
75
- else
76
- FileUtils.ln_s(file, dir, force: force, verbose: false)
81
+ unless type == 0
82
+ items = files.dup
83
+ files.clear
84
+ items.each do |file|
85
+ if file.exist?
86
+ if !file.symlink?
87
+ files << file
88
+ elsif !force
89
+ next
77
90
  end
78
- soft += 1
79
91
  end
92
+ case type
93
+ when 1
94
+ FileUtils.ln_s(file, dir, force: force, verbose: false)
95
+ when 2
96
+ FileUtils.ln_s(file.relative_path_from(dir), dir, force: force, verbose: false)
97
+ else
98
+ FileUtils.ln(file, dir, force: force, verbose: false)
99
+ end
100
+ soft += 1
80
101
  end
81
102
  end
82
103
  next if files.empty?
83
104
 
84
- out = FileUtils.cp(files, dir, verbose: false)
105
+ out = FileUtils.cp(files, dir, preserve: preserve, verbose: false)
85
106
  count += out.size
86
107
  end
87
108
  puts [target.realpath, subdir.size, soft > 0 ? "#{count}+#{soft}" : count].join(' => ') if verbose
88
109
  end
89
110
 
90
- def copy_guard(src, dest, link: nil, force: false, verbose: true)
91
- unless force
92
- target = Pathname.new(dest)
93
- if target.directory?
94
- src = Array(src).reject { |val| target.join(File.basename(val)).exist? }
95
- return if src.empty?
96
- elsif target.exist?
97
- return
111
+ def copy_guard(*src, dest, base: '.', create: false, link: nil, preserve: nil, force: false, verbose: true)
112
+ src = src.compact.flatten
113
+ dest = Pathname.new(dest).realdirpath
114
+ base = Pathname.new(base).realpath
115
+ dir = if dest.directory?
116
+ true
117
+ elsif src.size > 1
118
+ raise Errno::ENOENT, dest.cleanpath.to_s unless create && !dest.exist?
119
+
120
+ dest.mkpath
121
+ true
122
+ end
123
+ targets = src.map! { |file| [base + file, dir ? dest + File.basename(file) : dest] }
124
+ return if !force && (targets = targets.reject { |to| to[1].exist? }).empty?
125
+
126
+ type = System.send :parse_link, link
127
+ targets.each do |file, to|
128
+ case type
129
+ when 0
130
+ FileUtils.cp(file, to, preserve: preserve, verbose: verbose)
131
+ when 1
132
+ FileUtils.ln_s(file, to, force: force, verbose: verbose)
133
+ when 2
134
+ FileUtils.ln_s(file.relative_path_from(dir ? to.dirname : to), to, force: force, verbose: verbose)
135
+ else
136
+ FileUtils.ln(file, to, force: force, verbose: verbose)
98
137
  end
99
138
  end
100
- case link
101
- when 'hard', 1
102
- FileUtils.ln(src, dest, force: force, verbose: verbose)
103
- when ::TrueClass, 'soft', 0
104
- FileUtils.ln_s(src, dest, force: force, verbose: verbose)
105
- else
106
- FileUtils.cp(src, dest, verbose: verbose)
107
- end
139
+ nil
108
140
  end
109
141
  end
110
142
  end
@@ -9,21 +9,44 @@ module Squared
9
9
  module Utils
10
10
  module_function
11
11
 
12
- def split_escape(val, char: ',')
13
- val.split(/\s*(?<!\\)#{char}\s*/)
12
+ def as_a(obj, *meth, flat: nil, compact: false, &blk)
13
+ return [] if obj.nil?
14
+
15
+ unless obj.is_a?(::Array)
16
+ obj = if obj.respond_to?(:to_ary)
17
+ obj.to_ary
18
+ elsif obj.respond_to?(:to_a) && !obj.is_a?(::Hash) && (val = obj.to_a).is_a?(::Array)
19
+ val
20
+ else
21
+ [obj]
22
+ end
23
+ end
24
+ obj = flat.is_a?(::Numeric) ? obj.flatten(flat) : obj.flatten if flat
25
+ obj = obj.compact if compact
26
+ obj = obj.map(&meth.shift) until meth.empty?
27
+ return obj unless block_given?
28
+
29
+ obj.select(&blk)
30
+ end
31
+
32
+ def split_escape(val, char: ',', &blk)
33
+ ret = val.split(/\s*(?<!\\)#{char}\s*/)
34
+ return ret unless block_given?
35
+
36
+ ret.each(&blk)
14
37
  end
15
38
 
16
39
  def split_option(val)
17
40
  val = val.strip
18
41
  return [val, '', ''] unless (i = val.index('='))
19
42
 
20
- last = val[(i + 1)..-1].strip
43
+ last = val[i.succ..-1].strip
21
44
  quote = ''
22
45
  if last =~ /\A(["'])(.+)\1\z/
23
46
  last = $2
24
47
  quote = $1
25
48
  end
26
- [val[0..(i - 1)], last, quote]
49
+ [val[0..i.pred], last, quote]
27
50
  end
28
51
 
29
52
  def task_invoke(*cmd, args: [], exception: true, warning: true)
@@ -105,15 +128,15 @@ module Squared
105
128
  end
106
129
  end
107
130
 
108
- def env(key, default = nil, suffix: nil, strict: false, equals: nil, ignore: nil)
131
+ def env(key, default = nil, suffix: nil, strict: false, equals: nil, ignore: nil, **)
109
132
  ret = env_value(key, suffix: suffix, strict: strict)
110
- return ret == equals.to_s unless equals.nil?
133
+ return Array(equals).any? { |val| val.to_s == ret } unless equals.nil?
111
134
 
112
135
  ret.empty? || (ignore && Array(ignore).any? { |val| val.to_s == ret }) ? default : ret
113
136
  end
114
137
 
115
138
  def env_key(*val)
116
- val.join('_').sub(/\A\d+\W*/, '__').gsub(/\W+/, '_').upcase
139
+ val.join('_').gsub(/\W+/, '_').upcase
117
140
  end
118
141
 
119
142
  def env_value(key, default = '', suffix: nil, strict: false)
@@ -150,19 +173,11 @@ module Squared
150
173
  end
151
174
 
152
175
  def env_pipe(key, default = 1, suffix: nil, strict: false, root: nil)
153
- out = 1
154
176
  case key
155
177
  when ::String
156
178
  case (ret = env_value(key, suffix: suffix, strict: strict))
157
179
  when '0', '1', '2'
158
180
  return ret.to_i
159
- else
160
- out = default if default.is_a?(::Numeric)
161
- if ret.include?(File::SEPARATOR)
162
- default = ret
163
- elsif key.include?(File::SEPARATOR)
164
- default = key
165
- end
166
181
  end
167
182
  when ::Numeric
168
183
  return key if key.between?(0, 2)
@@ -173,7 +188,7 @@ module Squared
173
188
  (root ? Pathname.new(root) + default : Pathname.new(default)).realdirpath
174
189
  rescue StandardError => e
175
190
  warn e
176
- out
191
+ 1
177
192
  end
178
193
  end
179
194