squared 0.8.8 → 0.9.0.pre1

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.
data/exe/sqd ADDED
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $:.unshift(File.expand_path('../lib', __dir__)) if File.exist?(File.expand_path('../squared.gemspec', __dir__))
5
+
6
+ require 'squared'
7
+ require 'rake'
8
+ require 'optparse'
9
+
10
+ class SqdParser < OptionParser
11
+ include Squared::Common::Format
12
+ include Utils
13
+
14
+ attr_reader :options, :global
15
+
16
+ def initialize(*)
17
+ super
18
+ @options = { 'project-args': [], 'project-env': [] }
19
+ @global = []
20
+ @program_name = 'squared'
21
+ @raise_unknown = false
22
+ @require_exact = true
23
+ end
24
+
25
+ def parse_global(list, assert:)
26
+ raise ArgumentError, "cannot be used with --#{assert}" if options.key?(assert)
27
+
28
+ ARGV.each_with_index do |val, index|
29
+ if val == '--'
30
+ separator!(index: index)
31
+ break
32
+ end
33
+ next if val.start_with?('-') || val.include?(':') || val.empty?
34
+
35
+ global.concat(list.map { |name| "#{val}:#{name}" })
36
+ ARGV[index] = ''
37
+ end
38
+ end
39
+
40
+ def parse_common(list, session:, arg:)
41
+ if list && !list.include?('*')
42
+ session[arg].merge!(list.to_h { |key| [key, 'true'] })
43
+ else
44
+ session[arg] = Hash.new('true')
45
+ end
46
+ end
47
+
48
+ def banner_set(val, session:)
49
+ session[:BANNER] = val
50
+ ENV['BANNER'] = val ? 'true' : 'false'
51
+ end
52
+
53
+ def color_set(val, session:)
54
+ if (session[:COLOR] = val)
55
+ enable_aixterm
56
+ enable_drawing
57
+ else
58
+ enable_drawing false
59
+ Squared::Common.const_get(:VAR)[:colors].freeze
60
+ end
61
+ ENV['NO_COLOR'] = val ? nil : '1'
62
+ ENV['FORCE_COLOR'] = val ? '2' : '0'
63
+ end
64
+
65
+ def run
66
+ parse!(into: options)
67
+ if (project = options[:project])
68
+ pattern, project = project.partition do |val|
69
+ next true if val == '*'
70
+
71
+ val = ".#{val}" if val.start_with?('*')
72
+ val.match?(/[*+\[(]|(?<!\\)\.\.|\{\d+\}/) && Regexp.new(val)
73
+ rescue RegexpError
74
+ false
75
+ end
76
+ pattern.each do |val|
77
+ val = if val == '*'
78
+ '[^:]+'
79
+ else
80
+ val.sub(/^\*/, '.*')
81
+ .sub(/(?<!\.)\*$/, '.*')
82
+ .gsub(/\.([*+])/, '[^:]\1?')
83
+ end
84
+ IO.popen("rake -T \"^#{val}:\"").each { |line| project << $1 if line =~ /\Arake (#{val}):/o }
85
+ end
86
+ exit 1 if project.empty? && !pattern.empty?
87
+ project.uniq!
88
+ options[:'project-env'].each do |val|
89
+ n = val.index('=')
90
+ key = val[...n]
91
+ val = val[n.succ..].stripquote
92
+ project.each { |name| ENV["#{key}_#{env_key(name.split(':').first)}"] = val }
93
+ end
94
+ ARGV.each_with_index do |val, index|
95
+ if val == '--'
96
+ separator!(index: index)
97
+ break
98
+ end
99
+ next unless val.start_with?(':')
100
+
101
+ args = options[:'project-args'].shift
102
+ global.concat(project.map { |name| "#{name}#{val}#{"[#{args}]" if args}" })
103
+ ARGV[index] = ''
104
+ end
105
+ end
106
+ ARGV.reject!(&:empty?)
107
+ Rake.application.run(global.concat(ARGV))
108
+ end
109
+
110
+ def separator!(index:)
111
+ ARGV[index] = '' if ARGV.grep('--').size > 1
112
+ end
113
+
114
+ def error_exit(*val, hint)
115
+ raise OptionParser::InvalidArgument, message(*val, hint: hint)
116
+ end
117
+ end
118
+
119
+ SqdParser.new do |sqd|
120
+ session = Squared::Common::ARG
121
+ sqd.banner = 'Usage: sqd [options] [rake options] [global...] [:targets...] -- [rake targets...]'
122
+ sqd.separator ''
123
+ sqd.on('-R', '--ref STR,STR', Array, 'Run global commands by class reference') do |val|
124
+ sqd.parse_global(val, assert: :group)
125
+ session[:APP][:series_ref_min] = 1
126
+ end
127
+ sqd.on('-G', '--group STR,STR', Array, 'Run global commands by project group') do |val|
128
+ sqd.parse_global(val, assert: :ref)
129
+ end
130
+ sqd.separator ''
131
+ sqd.on('-P', '--project STR,PAT', Array, 'Run project commands with common arguments')
132
+ sqd.on('-a', '--project-args STR,STR', 'Provide command arguments for :targets (multiple)') do |val|
133
+ sqd.options[:'project-args'] << val
134
+ end
135
+ sqd.on('-e', '--project-env KEY=STR', 'Provide command ENV variables for :targets (multiple)') do |val|
136
+ sqd.error_exit('missing value', "#{val}=") unless val.include?('=')
137
+
138
+ sqd.options[:'project-env'] << val
139
+ end
140
+ sqd.separator ''
141
+ sqd.on('-s', '--[no-]strict', 'Do not run commands with invalid arguments') do |val|
142
+ session[:STRICT] = val
143
+ ENV['STRICT'] = val ? 'true' : 'false'
144
+ end
145
+ sqd.on('--update', 'Use update strategy for applicable commands') do
146
+ ENV['UPDATE'] = 'true'
147
+ end
148
+ sqd.on('-F', '--force [STR,STR]', Array, 'Enable force through ENV by application') do |val|
149
+ sqd.parse_common(val, session: session, arg: :FORCE)
150
+ end
151
+ sqd.on('-y', '--yes [STR,STR]', Array, 'Enable auto-approve through ENV by application') do |val|
152
+ sqd.parse_common(val, session: session, arg: :YES)
153
+ end
154
+ sqd.on('-d', '--dry-run', 'Run commands without any modifying actions') do
155
+ ENV['DRY_RUN'] = 'true'
156
+ end
157
+ sqd.separator ''
158
+ sqd.on('-S', '--select PAGE:CHOICE', 'Prompt menu selection pagination limits') do |val|
159
+ page, choice = val.split(/\D/, 2).map(&:to_i)
160
+ sqd.error_exit('not an integer', val) if choice.nil? ? page == 0 : choice == 0
161
+
162
+ session[:PAGE] = page if page > 0
163
+ session[:CHOICE] = choice if choice.to_i > 0
164
+ ENV['SELECT'] = "#{page}:#{choice}"
165
+ end
166
+ sqd.separator ''
167
+ sqd.on('-p', '--pipe 0|1|2', Integer, 'Set file descriptor for communication') do |val|
168
+ sqd.error_exit('out of range', val) unless val.between?(0, 2)
169
+
170
+ session[:PIPE] = val
171
+ end
172
+ sqd.on('-o', '--pipe-out FILE', 'Write command data stream to a file') do |val|
173
+ sqd.error_exit("missing an explicit \".#{File::SEPARATOR}\"", val) unless val.include?(File::SEPARATOR)
174
+
175
+ session[:OUT] = val
176
+ end
177
+ sqd.on('--fail 0..4', Integer, 'Set application exception level') do |val|
178
+ sqd.error_exit('out of range', val) unless val.between?(0, 4)
179
+
180
+ session[:FAIL] = val
181
+ end
182
+ %i[debug info warn error fatal].then do |state|
183
+ values = state + (0..4).map(&:to_s)
184
+ sqd.on('--log-level SEVERITY', values, "Set logger and exception state (#{state.join(', ')})") do |val|
185
+ require 'logger'
186
+ severity = case val
187
+ when :debug then Logger::DEBUG
188
+ when :info then Logger::INFO
189
+ when :warn then Logger::WARN
190
+ when :error then Logger::ERROR
191
+ when :fatal then Logger::FATAL
192
+ else
193
+ val.to_i
194
+ end
195
+ session[:LEVEL] = severity
196
+ ENV['LOG_LEVEL'] = severity.to_s
197
+ end
198
+ end
199
+ sqd.separator ''
200
+ sqd.on('--[no-]banner', 'Display executing command attributes') do |val|
201
+ sqd.banner_set(val, session: session)
202
+ end
203
+ sqd.on('-x', '--[no-]color', 'Use enhanced styling for terminal output') do |val|
204
+ sqd.color_set(val, session: session)
205
+ end
206
+ sqd.on('-X', '--plain', 'Disable banner and styling for all commands') do
207
+ sqd.banner_set(false, session: session)
208
+ sqd.color_set(false, session: session)
209
+ end
210
+ sqd.on('-v', '--verbose', 'Increase output verbosity (multiple)') do
211
+ unless session[:VERBOSE]
212
+ session[:VERBOSE] = $VERBOSE ? 1 : 0
213
+ sqd.global.unshift('--verbose')
214
+ end
215
+ ENV['VERBOSE'] = (session[:VERBOSE] += 1).to_s
216
+ end
217
+ sqd.on('--version', 'Print the current version') do
218
+ puts Squared::VERSION
219
+ exit 0
220
+ end
221
+ sqd.on('-h', '--help', 'Print the command line options') do
222
+ sqd.help_exit
223
+ end
224
+ end.run
@@ -11,14 +11,17 @@ module Squared
11
11
  COMMON: true,
12
12
  VERBOSE: nil,
13
13
  STRICT: false,
14
+ FORCE: {},
15
+ YES: {},
16
+ APP: {},
14
17
  BANNER: true,
15
- CHOICE: 25,
16
- TAB: 4,
18
+ PAGE: 20,
19
+ CHOICE: 100,
17
20
  QUOTE: "'",
18
21
  SPACE: ' => ',
19
22
  SUCCESS: 'Success',
20
- GRAPH: %w[| - | \\ -].freeze,
21
- BORDER: %w[| - - - - - | | - -].freeze,
23
+ GRAPH: %w[| - | \\ -],
24
+ BORDER: %w[| - - - - - | | - -],
22
25
  VIEW: 'view',
23
26
  LEVEL: ENV.fetch('LOG_LEVEL', 0).to_i,
24
27
  COLOR: ENV.fetch('NO_COLOR', '').empty?
@@ -102,7 +105,6 @@ module Squared
102
105
 
103
106
  def __freeze__
104
107
  PATH.freeze
105
- ARG.each_value { |val| val.freeze if val.is_a?(::Hash) || val.is_a?(::Array) }
106
108
  ARG.freeze
107
109
  VAR.each_value(&:freeze)
108
110
  VAR[:theme].each_value { |val| val.freeze.each_value(&:freeze) }
@@ -50,9 +50,14 @@ module Squared
50
50
  self
51
51
  end
52
52
 
53
- def enable_drawing
54
- ARG[:GRAPH] = BOX_GRAPH
55
- ARG[:BORDER] = BOX_BORDER
53
+ def enable_drawing(state = true)
54
+ if state
55
+ ARG[:GRAPH] = BOX_GRAPH unless ARG[:GRAPH].frozen?
56
+ ARG[:BORDER] = BOX_BORDER unless ARG[:BORDER].frozen?
57
+ else
58
+ ARG[:GRAPH].freeze
59
+ ARG[:BORDER].freeze
60
+ end
56
61
  self
57
62
  end
58
63
 
@@ -296,7 +301,7 @@ module Squared
296
301
  if footer
297
302
  unless sub.empty? && !right
298
303
  footer.map! do |s|
299
- s = s.rjust(n + ARG[:TAB]) if right
304
+ s = s.rjust(n + 4) if right
300
305
  sub.each { |h| sub_style!(s, **h) }
301
306
  s
302
307
  end
@@ -1,27 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'timeout'
4
+
3
5
  module Squared
4
6
  module Common
5
7
  module Prompt
6
8
  def self.reline
7
- if RUBY_ENGINE == 'ruby' && RUBY_VERSION < '2.7'
8
- require 'readline'
9
- Readline
10
- else
11
- begin
12
- require 'reline'
13
- Reline
14
- rescue LoadError
15
- require 'readline'
16
- Readline
17
- end
18
- end
9
+ require 'reline'
10
+ Reline
11
+ rescue LoadError
12
+ require 'readline'
13
+ Readline
19
14
  end
20
15
 
21
16
  module_function
22
17
 
23
18
  def confirm(msg, accept = nil, agree: 'Y', cancel: 'N', force: false, attempts: 3, timeout: 60)
24
- require 'timeout'
25
19
  if agree == 'Y' && cancel == 'N' && !msg.match?(%r{\[(Yn|nY|Y/n|y/N)\]})
26
20
  case accept
27
21
  when 'Y'
@@ -34,7 +28,7 @@ module Squared
34
28
  cancel = /^#{Regexp.escape(cancel)}$/i if cancel.is_a?(::String)
35
29
  Timeout.timeout(timeout) do
36
30
  while (ch = Prompt.reline.readline(msg))
37
- ch = ch.chomp
31
+ ch.chomp!
38
32
  case (ch.empty? ? accept : ch)
39
33
  when agree
40
34
  return true
@@ -54,9 +48,9 @@ module Squared
54
48
  false
55
49
  end
56
50
 
57
- def choice(msg, list = nil, min: 1, max: 1, multiple: false, index: false, grep: nil, border: nil, auto: true,
58
- force: true, attempts: 3, timeout: 0, empty: nil)
59
- require 'timeout'
51
+ def choice(msg, list = nil, min: 1, max: nil, multiple: false, index: false, grep: nil, border: nil, force: true,
52
+ attempts: 3, timeout: 0, pipe: 1, scroll: true, empty: nil, page: 25)
53
+ std = pipe == 2 ? $stderr : $stdout
60
54
  multiple = Array(multiple) if multiple.is_a?(::Numeric)
61
55
  if list
62
56
  grep &&= Array(grep).map { |val| Regexp.new(val) }
@@ -64,36 +58,71 @@ module Squared
64
58
  next if grep&.none? { |pat| pat.match?(val) }
65
59
 
66
60
  out << val.to_s.chomp
67
- puts '%2d. %s' % [out.size, val]
68
61
  end
69
- max = items.size
62
+ max = max ? [max, items.size].min : items.size
70
63
  if max == 0
71
64
  raise ArgumentError, 'empty selection list' unless empty
72
65
 
73
66
  puts empty
74
67
  exit 1
75
68
  end
69
+ tmpl = "%#{[items.size.to_s.size, 2].max}d. %s"
70
+ items = items.map.with_index { |item, i| [item, tmpl % [i.succ, item]] }
76
71
  min = grep ? 1 : [min, max].min
77
- if auto
78
- auto.times { puts } if auto.is_a?(::Numeric)
72
+ require 'io/console'
73
+ if min > 1 || max < items.size
74
+ items = items[min.pred...max]
75
+ min = 1
76
+ end
77
+ page = page ? [page, max].min : max
78
+ cur = 0
79
+ line = 0
80
+ last = (max / page.to_f).ceil
81
+ pre = msg
82
+ scroll = true if border
83
+ clear = -> { std.print "\r\e[K" }
84
+ write = lambda do |s, buf = true|
85
+ line += [s.lines.size, 1].max if buf
86
+ clear.call unless scroll && !border
87
+ std.puts s
88
+ end
89
+ render = lambda do |index, init = false|
90
+ fr = index * page
91
+ to = [fr + page, max].min
92
+ unless init
93
+ std.cursor_up((scroll ? 0 : page) + line.succ)
94
+ clear.call
95
+ end
96
+ line = 0
97
+ items[fr...to].each { |item| write.call(item.last, false) }
79
98
  if border == true
80
- puts print_footer
99
+ write.call(print_footer)
81
100
  elsif border
82
- puts print_footer(border: border)
101
+ write.call(print_footer(border: border))
102
+ end
103
+ fr = scroll ? 1 : fr.succ
104
+ suf = "|,#{multiple.is_a?(::Array) ? "{#{multiple.join(',')}}" : '*'}" if multiple
105
+ msg = "#{pre + (force ? ':' : '?')} [#{fr}-#{to}#{"/#{max}" if scroll && to < max}#{suf}] "
106
+ if cur == last.pred
107
+ cur = -1
108
+ last = 0 if scroll || (attempts -= 1) == 0
109
+ end
110
+ if init && last > 1 && !scroll
111
+ write.call('', false)
112
+ std.cursor_up(1)
83
113
  end
84
- msg = "#{msg + (force ? ':' : '?')} [#{min}-#{max}#{if (n = multiple)
85
- "|,#{n.is_a?(::Array) ? "{#{n.join(',')}}" : '*'}"
86
- end}] "
87
114
  end
115
+ render.call(0, true)
116
+ else
117
+ max ||= 1
118
+ last = 0
88
119
  end
89
120
  between = ->(s) { s.match?(/^\d+$/) && s.to_i.between?(min, max <= min ? Float::INFINITY : max) }
90
121
  Timeout.timeout(timeout) do
91
122
  while (ch = Prompt.reline.readline(msg))
92
123
  ch.strip!
93
- unless ch.empty?
94
- if ch == '-' && !force
95
- break
96
- elsif multiple
124
+ if !ch.empty?
125
+ if multiple
97
126
  k = if ch == '*'
98
127
  (min..max).to_a
99
128
  else
@@ -114,12 +143,15 @@ module Squared
114
143
  k.uniq!
115
144
  k.sort!
116
145
  unless multiple.is_a?(::Array) && !multiple.include?(k.size)
117
- return index || !items ? k : k.map { |i| items[i.pred] }
146
+ return index || !items ? k : k.map { |i| items[i.pred].first }
118
147
  end
119
148
  end
120
149
  elsif between.call(ch)
121
- return index || !items ? ch.to_i : items[ch.to_i.pred]
150
+ return index || !items ? ch.to_i : items[ch.to_i.pred].first
122
151
  end
152
+ elsif last > 0
153
+ render.call(cur += 1)
154
+ next
123
155
  end
124
156
  attempts -= 1
125
157
  next if attempts > 0
@@ -128,7 +160,7 @@ module Squared
128
160
  break
129
161
  end
130
162
  rescue Interrupt
131
- puts
163
+ std.puts
132
164
  exit 0
133
165
  else
134
166
  [] if multiple
@@ -167,7 +167,9 @@ module Squared
167
167
  end
168
168
 
169
169
  def line_width(lines)
170
- [lines.empty? ? 80 : [lines.max_by(&:size).size, 80].max, Rake.application.terminal_width].min
170
+ require 'io/console/size'
171
+ cols = IO.default_console_size.last
172
+ [lines.empty? ? cols : [lines.max_by(&:size).size, cols].max, Rake.application.terminal_width].min
171
173
  end
172
174
 
173
175
  def fill_option(val, **kwargs)
@@ -28,15 +28,8 @@ module Squared
28
28
  module_function
29
29
 
30
30
  def shell(*args, name: :system, **kwargs)
31
- if name != :system
32
- kwargs.delete(:exception)
33
- elsif RUBY_VERSION < '2.6'
34
- ex = kwargs.delete(:exception)
35
- end
36
- ret = Kernel.send(name, *args, **kwargs)
37
- raise $?.to_s if ex && !ret
38
-
39
- ret
31
+ kwargs.delete(:exception) unless name == :system
32
+ Kernel.send(name, *args, **kwargs)
40
33
  end
41
34
 
42
35
  def shell_t(*args, timeout: 0, **kwargs)
@@ -65,7 +65,7 @@ module Squared
65
65
  command = File.basename(@main)
66
66
  command = ARG[:VIEW] if command == @name
67
67
  end
68
- ext = @ext[1..-1].downcase
68
+ ext = @ext[1..].downcase
69
69
  if (data = @@mime_obj[ext])
70
70
  add(data[1].first, command: command, opts: opts, ext: data[1])
71
71
  else
@@ -98,7 +98,7 @@ module Squared
98
98
  namespace task_name(name) do |ns|
99
99
  @mime.each do |type, items|
100
100
  items.each do |command, file, opts|
101
- next if Rake::Task.task_defined?("#{ns.scope.path}:#{command}:#{type}")
101
+ next if Rake::Task.task_defined?(task_join(ns.scope.path, command, type))
102
102
 
103
103
  namespace command do
104
104
  unless (data = @@mime_obj[type])
@@ -288,7 +288,7 @@ module Squared
288
288
  if project
289
289
  project.workspace.task_name(val)
290
290
  else
291
- @prefix ? "#{@prefix}:#{val}" : val.to_s
291
+ @prefix ? task_join(@prefix, val) : val.to_s
292
292
  end
293
293
  end
294
294
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Squared
4
- VERSION = '0.8.8'
4
+ VERSION = '0.9.0.pre1'
5
5
  end