clee 0.4.2

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/lib/clee.rb ADDED
@@ -0,0 +1,623 @@
1
+ # -*- encoding : utf-8 -*-
2
+ #
3
+ require_relative 'clee/_lib.rb'
4
+
5
+ Clee.load_dependencies!
6
+
7
+ #
8
+ class Clee
9
+ class Error < ::StandardError; end
10
+
11
+ attr_accessor :env
12
+ attr_accessor :argv
13
+ attr_accessor :options
14
+ attr_accessor :params
15
+ attr_accessor :stdin
16
+ attr_accessor :stdout
17
+ attr_accessor :stderr
18
+ attr_accessor :help
19
+
20
+ def _run!
21
+ _setup!
22
+ _parse_command_line!
23
+ _set_mode!
24
+ _run_mode!
25
+ end
26
+
27
+ def _setup!
28
+ @klass = self.class
29
+
30
+ @env = Hash.new
31
+ @options = Hash.new
32
+
33
+ @argv = ARGV.map(&:dup)
34
+
35
+ @stdin = $stdin.dup
36
+ @stdout = $stdout.dup
37
+ @stderr = $stderr.dup
38
+
39
+ @name = @klass.name.dup
40
+ @help = @klass.help.dup
41
+ @tldr = @klass.tldr.dup
42
+ end
43
+
44
+ def _parse_command_line!
45
+ _parse_options!
46
+ _parse_env!
47
+ _parse_params!
48
+ end
49
+
50
+ def _parse_options!
51
+ @options = Hash.new
52
+
53
+ o = OptionParser.new
54
+
55
+ klass.options.each do |spec|
56
+ spec => long:, short:, value:
57
+ args = []
58
+
59
+ case value
60
+ when :required
61
+ args.push "-#{ short }" if short
62
+ args.push "--#{ long } value" if long
63
+ when :optional
64
+ args.push "-#{ short }" if short
65
+ args.push "--#{ long } [value]" if long
66
+ when :none
67
+ args.push "-#{ short }" if short
68
+ args.push "--[no-]#{ long }" if long
69
+ end
70
+
71
+ o.on(*args) do |val|
72
+ if @options.has_key?(long)
73
+ @options[long] = [@options[long], val].flatten
74
+ else
75
+ @options[long] = val
76
+ end
77
+ end
78
+ end
79
+
80
+ begin
81
+ o.parse!(@argv)
82
+ rescue OptionParser::MissingArgument => e
83
+ warn(e.message)
84
+ exit 1
85
+ rescue OptionParser::InvalidOption => e
86
+ warn(e.message)
87
+ exit 1
88
+ end
89
+
90
+ Clee.symbolize_keys!(@options)
91
+ end
92
+
93
+ def _parse_env!
94
+ #
95
+ envs = Hash.new
96
+
97
+ @argv.each_with_index do |arg, index|
98
+ if arg =~ /[^=]+\s*=/
99
+ k, v = arg.split(/\s*=\s*/, 3)
100
+
101
+ key = k.to_s.strip.to_sym
102
+ val = v.to_s.strip == '' ? nil : v
103
+
104
+ envs[index] = {key:, val:}
105
+ end
106
+ end
107
+
108
+ #
109
+ validate = proc do |long:, value:, val:|
110
+ case value
111
+ when :required
112
+ unless val
113
+ warn("#{ long }=:missing")
114
+ exit 1
115
+ end
116
+
117
+ when :none
118
+ unless val.nil?
119
+ warn("#{ long }=#{ val }")
120
+ exit 1
121
+ end
122
+
123
+ when :optional
124
+ :noop
125
+ end
126
+ end
127
+
128
+ #
129
+ add_env = proc do |long, val|
130
+ if @env.has_key?(long)
131
+ @env[long] = [@env[long], val].flatten
132
+ else
133
+ @env[long] = val
134
+ end
135
+ end
136
+
137
+ klass.envs.each do |spec|
138
+ spec => long:, short:, keys:, value:
139
+
140
+ keys.reverse.each do |key|
141
+ if ENV.has_key?(key.to_s)
142
+ v = ENV[key.to_s]
143
+ val = v.to_s.strip == '' ? nil : v
144
+ validate[long:, value:, val:]
145
+ add_env[long, val]
146
+ end
147
+ end
148
+
149
+ envs.each do |index, env|
150
+ env => key:, val:
151
+
152
+ if([long, short].include?(key))
153
+ validate[long:, value:, val:]
154
+ add_env[long, val]
155
+ @argv[index] = nil
156
+ end
157
+ end
158
+ end
159
+
160
+ #
161
+ @argv.compact!
162
+
163
+ Clee.symbolize_keys!(@env)
164
+ end
165
+
166
+ def _parse_params!
167
+ @params = @env.merge(@options)
168
+ end
169
+
170
+ def _set_mode!
171
+ @mode = nil
172
+
173
+ first = 0
174
+ last = @argv.size
175
+
176
+ while last > 0
177
+ args = @argv[first ... last]
178
+ mode = klass.mode?(*args)
179
+
180
+ if mode
181
+ @mode = mode
182
+ @argv.replace(@argv[last..])
183
+ break
184
+ end
185
+
186
+ last = last - 1
187
+ end
188
+ end
189
+
190
+ def _run_mode!
191
+ if @mode
192
+ mode_method = klass.mode_method_for(@mode)
193
+ send(mode_method)
194
+ exit 0
195
+ end
196
+
197
+ if argv.first == 'help' || params[:help]
198
+ help!(exit: 0)
199
+ end
200
+
201
+ send(:run)
202
+ exit 0
203
+ end
204
+
205
+ def _default_run!
206
+ help!(exit: 1)
207
+ end
208
+
209
+ def run
210
+ _default_run!
211
+ end
212
+
213
+ def progname
214
+ File.basename($0)
215
+ end
216
+
217
+ def help!(**kws)
218
+ help = (@help || _default_help)
219
+ puts help
220
+ status = kws.fetch(:exit){ kws.fetch('exit'){ 1 } }
221
+ exit(status.to_i) if status
222
+ end
223
+
224
+ def _default_help
225
+ [].tap do |l|
226
+ p = proc do |string|
227
+ l.push string.to_s.rstrip
228
+ l.push "\n"
229
+ end
230
+
231
+ h = proc do |header|
232
+ l.push "\n"
233
+ p[header]
234
+ p['_' * header.to_s.size]
235
+ end
236
+
237
+ h[:NAME]
238
+ p[" #{ progname }"]
239
+
240
+ if @tldr
241
+ h[:TLDR]
242
+ p[" #{ @tldr }"]
243
+ end
244
+
245
+ unless klass.envs.empty?
246
+ h[:ENVIRONMENT]
247
+ klass.envs.each do |spec|
248
+ spec => value:, keys:
249
+
250
+ if value == :none
251
+ p[" #{ keys.join(' | ') }"]
252
+ else
253
+ p[" #{ keys.join(' | ') } : value=#{ value }"]
254
+ end
255
+ end
256
+ end
257
+
258
+ unless klass.options.empty?
259
+ h[:OPTIONS]
260
+ klass.options.each do |spec|
261
+ spec => value:, opts:
262
+
263
+ if value == :none
264
+ p[" #{ opts.join(' | ') }"]
265
+ else
266
+ p[" #{ opts.join(' | ') } : value=#{ value }"]
267
+ end
268
+ end
269
+ end
270
+
271
+
272
+ h[:MODES]
273
+ p[" ~> #{ progname }"]
274
+ modes.each do |mode|
275
+ p[" ~> #{ progname } #{ mode.join ' ' }"]
276
+ end
277
+ p[" ~> #{ progname } help"]
278
+
279
+ end.join.strip
280
+ end
281
+ #
282
+ def Clee.deep_copy(obj)
283
+ Marshal.load(Marshal.dump(obj))
284
+ end
285
+
286
+ def Clee.symbolize_keys!(hash)
287
+ hash.transform_keys!(&:to_sym)
288
+
289
+ hash.each do |key, val|
290
+ if val.is_a?(Hash)
291
+ symbolize_keys!(val)
292
+ end
293
+ end
294
+
295
+ hash
296
+ end
297
+
298
+ def Clee.stringify_keys(hash)
299
+ stringify_keys!(deep_copy(hash))
300
+ end
301
+
302
+ def Clee.stringify_keys!(hash)
303
+ hash.transform_keys!(&:to_sym)
304
+
305
+ hash.each do |key, val|
306
+ if val.is_a?(Hash)
307
+ stringify_keys!(val)
308
+ end
309
+ end
310
+
311
+ hash
312
+ end
313
+
314
+ def Clee.stringify_keys(hash)
315
+ stringify_keys!(deep_copy(hash))
316
+ end
317
+
318
+ #
319
+ def klass
320
+ self.class
321
+ end
322
+
323
+ def Clee.klass
324
+ self
325
+ end
326
+
327
+ #
328
+ ANSI = {
329
+ :clear => "\e[0m",
330
+ :reset => "\e[0m",
331
+ :erase_line => "\e[K",
332
+ :erase_char => "\e[P",
333
+ :bold => "\e[1m",
334
+ :dark => "\e[2m",
335
+ :underline => "\e[4m",
336
+ :underscore => "\e[4m",
337
+ :blink => "\e[5m",
338
+ :reverse => "\e[7m",
339
+ :concealed => "\e[8m",
340
+ :black => "\e[30m",
341
+ :red => "\e[31m",
342
+ :green => "\e[32m",
343
+ :yellow => "\e[33m",
344
+ :blue => "\e[34m",
345
+ :magenta => "\e[35m",
346
+ :cyan => "\e[36m",
347
+ :white => "\e[37m",
348
+ :on_black => "\e[40m",
349
+ :on_red => "\e[41m",
350
+ :on_green => "\e[42m",
351
+ :on_yellow => "\e[43m",
352
+ :on_blue => "\e[44m",
353
+ :on_magenta => "\e[45m",
354
+ :on_cyan => "\e[46m",
355
+ :on_white => "\e[47m"
356
+ }
357
+
358
+ Ansi = Object.new
359
+
360
+ ANSI.each do |key, value|
361
+ Ansi.singleton_class.define_method(key){ value }
362
+ end
363
+
364
+ def Clee.ansi
365
+ @ansi ||= Ansi
366
+ end
367
+
368
+ def ansi
369
+ klass.ansi
370
+ end
371
+
372
+ #
373
+ class Logger
374
+ Levels = [
375
+ :success,
376
+ :failure,
377
+ :message,
378
+ :warning,
379
+ :special,
380
+ ]
381
+
382
+ Colors = {
383
+ success: Ansi.green,
384
+ failure: Ansi.red,
385
+ message: Ansi.cyan,
386
+ default: Ansi.blue,
387
+ warning: Ansi.yellow,
388
+ special: Ansi.magenta,
389
+ clear: Ansi.clear,
390
+ }
391
+
392
+ def initialize(io = $stderr)
393
+ @io = io
394
+ end
395
+
396
+ def log(arg, *args, **kws)
397
+ level = kws.fetch(:level){ :message }
398
+ color = kws[:color] ? Ansi.public_send(kws[:color]) : color_for(level)
399
+ clear = color_for(:clear)
400
+
401
+ [arg, *args].each do |arg|
402
+ ts = Time.now.utc.iso8601(2)
403
+ msg = msg_for(arg)
404
+
405
+ prefix = "### [#{ level.to_s.upcase } @ #{ ts }]"
406
+
407
+ if @io.tty?
408
+ @io.write(color)
409
+ @io.write(prefix)
410
+ @io.write(clear)
411
+ else
412
+ @io.write(prefix)
413
+ end
414
+
415
+ @io.write("\n#{ msg }\n")
416
+ @io.flush
417
+ end
418
+ end
419
+
420
+ Levels.each do |level|
421
+ define_method(level){|arg, *args| log(arg, *args, level:)}
422
+ end
423
+
424
+ def color_for(level)
425
+ Colors.fetch(level.to_s.to_sym){ Colors.fetch(:default) }
426
+ end
427
+
428
+ def msg_for(arg)
429
+ case
430
+ when arg.is_a?(String)
431
+ arg.strip
432
+ when arg.is_a?(Exception)
433
+ "#{ e.message } (#{ e.class.name })\n#{ Array(e.backtrace).join(10.chr) }"
434
+ else
435
+ arg.pretty_inspect
436
+ end
437
+ end
438
+ end
439
+
440
+ def Clee.logger
441
+ @@logger ||= Logger.new
442
+ end
443
+
444
+ def logger
445
+ klass.logger
446
+ end
447
+
448
+ def log(*args, **kws, &block)
449
+ return logger if args.empty? && kws.empty? && block.nil?
450
+
451
+ logger.message(*args, **kws, &block)
452
+ end
453
+
454
+ def emsg(e)
455
+ if e.is_a?(Exception)
456
+ "#{ e.message } (#{ e.class.name })\n#{ Array(e.backtrace).join(10.chr) }"
457
+ else
458
+ e.to_s
459
+ end
460
+ end
461
+
462
+ #
463
+ def Clee.parse_spec(list, *args, **kws, &block)
464
+ return list if args.empty? && kws.empty? && block.nil?
465
+
466
+ argv = []
467
+
468
+ args.each do |arg|
469
+ if arg.is_a?(Hash)
470
+ kws.update(Clee.symbolize_keys(arg))
471
+ else
472
+ argv.push(arg)
473
+ end
474
+ end
475
+
476
+ long = kws.fetch(:long){ argv.shift }
477
+ raise ArgumentError.new('long=nil') unless long
478
+ long = long.to_s.to_sym
479
+
480
+ short = kws.fetch(:short){ argv.shift }
481
+
482
+ value = kws.fetch(:value){ :none }.to_s.to_sym
483
+
484
+ values = [:none, :required, :optional]
485
+
486
+ raise ArgumentError.new("value=#{ value }") unless values.include?(value)
487
+
488
+ keys = [long, short].compact
489
+
490
+ opts = [(long && "--#{ long }"), (short &&"-#{ short}")].compact
491
+
492
+ spec = {long:, short:, value:, keys:, opts:}
493
+
494
+ list.push(spec).uniq!
495
+ end
496
+
497
+ def Clee.options(*args, **kws, &block)
498
+ parse_spec(@@options ||= [], *args, **kws, &block)
499
+ end
500
+
501
+ def Clee.option(*args, **kws, &block)
502
+ options(*args, **kws, &block)
503
+ end
504
+
505
+ def Clee.opt(*args, **kws, &block)
506
+ options(*args, **kws, &block)
507
+ end
508
+
509
+ def Clee.envs(*args, **kws, &block)
510
+ parse_spec(@@envs ||= [], *args, **kws, &block)
511
+ end
512
+
513
+ def Clee.env(*args, **kws, &block)
514
+ envs(*args, **kws, &block)
515
+ end
516
+
517
+ def Clee.params(*args, **kws, &block)
518
+ parse_spec(@@options ||= [], *args, **kws, &block)
519
+ parse_spec(@@envs ||= [], *args, **kws, &block)
520
+
521
+ @@options + @@envs
522
+ end
523
+
524
+ def Clee.param(*args, **kws, &block)
525
+ params(*args, **kws, &block)
526
+ end
527
+
528
+ def Clee.help(*args)
529
+ @help ||= nil
530
+
531
+ unless args.empty?
532
+ @help = args.join("\n")
533
+ end
534
+
535
+ @help
536
+ end
537
+
538
+ def Clee.tldr(*args)
539
+ @tldr ||= nil
540
+
541
+ unless args.empty?
542
+ @tldr = args.join("\n")
543
+ end
544
+
545
+ @tldr
546
+ end
547
+
548
+ def Clee.run(*args, **kws, &block)
549
+ if args.empty?
550
+ method = :run
551
+ else
552
+ mode = mode_for(*args)
553
+ klass.modes.push(mode).uniq!
554
+ method = mode_method_for(mode)
555
+ end
556
+
557
+ define_method(method, &block)
558
+ end
559
+
560
+ def Clee.mode_for(*args)
561
+ args.flatten!
562
+ args.compact!
563
+
564
+ return nil if args.empty?
565
+
566
+ args.map(&:to_s).map(&:to_sym)
567
+ end
568
+
569
+ def Clee.modes
570
+ @@modes ||= []
571
+ end
572
+
573
+ def modes
574
+ klass.modes
575
+ end
576
+
577
+ def Clee.mode?(*args)
578
+ mode = mode_for(*args)
579
+ return mode if modes.include?(mode)
580
+ end
581
+
582
+ def Clee.mode_method_for(mode)
583
+ "__mode__#{ [mode].join('__') }"
584
+ end
585
+
586
+ #
587
+ def Clee.klass_for(name = 'clee', &block)
588
+ Class.new(Clee).tap do |klass|
589
+ klass.class_eval do
590
+ define_singleton_method(:name){ "#{ name }" }
591
+
592
+ option(:help, :h, value: :none)
593
+ end
594
+
595
+ klass.class_eval(&block)
596
+ end
597
+ end
598
+
599
+ def Clee._run!(name = 'clee', *args, &block)
600
+ $stdout.sync = true
601
+ $stderr.sync = true
602
+
603
+ %w[ PIPE INT ].each{|signal| Signal.trap(signal, "EXIT")}
604
+
605
+ klass = Clee.klass_for(name, &block)
606
+
607
+ clee = klass.new
608
+
609
+ clee._run!(*args)
610
+ end
611
+ end
612
+
613
+ BEGIN {
614
+ Object.send(:remove_const, :Clee) if Object.const_defined?(:Clee)
615
+
616
+ def Clee(*args, &block)
617
+ clee(*args, &block)
618
+ end
619
+
620
+ def clee(name = 'clee', *args, &block)
621
+ Clee._run!(name, *args, &block)
622
+ end
623
+ }
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: clee
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.2
5
+ platform: ruby
6
+ authors:
7
+ - Ara T. Howard
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-03-09 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: "`clee` has everything you need, and nothing you don't"
14
+ email: ara.t.howard@gmail.com
15
+ executables:
16
+ - clee
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE.md
21
+ - README.md
22
+ - Rakefile
23
+ - TODO.md
24
+ - bin/clee
25
+ - clee.gemspec
26
+ - docs/sunwukong.md
27
+ - lib/clee.rb
28
+ - lib/clee/_lib.rb
29
+ homepage: https://github.com/ahoward/clee
30
+ licenses:
31
+ - LicenseRef-LICENSE.md
32
+ metadata: {}
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '3.0'
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubygems_version: 3.5.16
49
+ signing_key:
50
+ specification_version: 4
51
+ summary: "`clee` is a tiny, 0 dependency, DSL for building über clean CLIs in Ruby"
52
+ test_files: []