dencli 0.2.1 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b139df1946e2bbf2534d8bcd9a5d22927d8b18f2353f12687789405c783077cf
4
- data.tar.gz: 7cd494d8569c7fe6c56364d25f8b79da3d21511160cb308854096dc4f157e5da
3
+ metadata.gz: b59309b03abb2d1a44eda112c289335eed1a252bccd89404a4e5017ff876657f
4
+ data.tar.gz: 0ecfaff2dd2928bfdc134ee3dc92737ae2776737e6ada66fd1432de5b470ab76
5
5
  SHA512:
6
- metadata.gz: ecbab8d3d760c9c88a023b0bb13e6ca185e7b91b4c2519452db7bae7e3254b625fde572325423cc5476866978dd92c0d2e931819308e34b35ff50ce474578915
7
- data.tar.gz: d7f8d1dc2bba5c493864091c3b15c577892465a305a4fd826ecb6013c51e729f0245d1a1ad0ada935dd8f7a1928aac85f23d014a52a0ba468fd9c945b34cdc95
6
+ metadata.gz: d27d7f9d307e1e86684ebb7e771ff865d0d10e4cde501951947997e98d25aafff123826c1973e7615ab45edd7c2bd3d60eacefa56835eacd976a4e9a9f4ed2d8
7
+ data.tar.gz: 70f63e628dcd9b7c2cc8da2302ef0dc33a170efc6281173c05f31b09657c0f4b616cd71faaa3ea09738e629b021e51e6018fda33535d2d34d8570caa0db32acc
data/bin/example.rb CHANGED
@@ -1,25 +1,55 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ require 'pathname'
4
+ $:.unshift Pathname.new(__FILE__).dirname.dirname.join('lib').to_s
3
5
  require 'dencli'
4
6
 
5
7
  cli = DenCli.new 'example', "This is an example for generate a DenCli-API"
8
+ cli.cmd( :args, "Expects and prints given arguments", &lambda {|a, b, c:, d:, e:|
9
+ p a: a, b: b, c: c, d: d, e: e
10
+ }).
11
+ opt( :c, '-c=ForC', "Option c").
12
+ opt( :d, '-d=ForD', "Option d", default: "something").
13
+ opt( :e, '-e', "Toggle e", default: false).
14
+ opt( :f, '--[no-]f', "Toggle f", default: false)
15
+
6
16
  cli.cmd( :example, "I have an example command") { STDERR.puts "This is an example" }
7
- cli.cmd( :help, "", aliases: [nil, '-h', '--help']) {|*args| STDERR.puts cli.help(*args) }
17
+ cli.cmd( :help, "", aliases: [nil, '-h', '--help'], &lambda {|*args, full:|
18
+ if full
19
+ cli.help_full *args, output: STDERR
20
+ else
21
+ cli.help *args, output: STDERR
22
+ end
23
+ }).
24
+ opt( :full, '-f', '--[no-]full', "Print all commands and sub-commands.", default: false)
8
25
 
9
26
  cli.sub( :more, "Sub-Commands are also possible with a new cli") do |sub|
27
+ sub.cmd( :help, "", aliases: [nil, '-h', '--help']) {|*args| STDERR.puts sub.help(*args) }
10
28
  sub.cmd( :help, "") {|*args| STDERR.puts cli.help( 'more', *args) }
11
29
  sub.cmd( :example, "Here is an example, too") { STDERR.puts "This is an other example" }
12
30
  sub.cmd( :foo, "BAR") { STDERR.puts "FOO bar"}
13
31
 
32
+ sub.cmd( :args, "Expects and prints given arguments", &lambda {|a, b=1, c:, d: 5, e:|
33
+ p a: a, b: b, c: c, d: d, e: e
34
+ }).
35
+ opt( :c, '-c=ForC', "Option c").
36
+ opt( :d, '-d=ForD', "Option d (implicit default)").
37
+ opt( :e, '-e', "Toggle e")
38
+
14
39
  sub.sub( :deeper, "You want to have Sub-Sub-Commands?") do |sub2|
15
- sub2.cmd( :help, "") {|*args| STDERR.puts cli.help( 'more', 'deeper', *args) }
16
- sub2.cmd( :last, "The last example") { STDERR.puts "The last example" }
40
+ sub2.cmd( :help, "", aliases: [nil, '-h', '--help'], &lambda {|*args| sub2.help( *args, output: STDERR) })
41
+ sub2.cmd( :last, "The last example", &lambda { STDERR.puts "The last example" })
17
42
 
18
43
  sub2.sub( :'sub-commands', "Endless Sub-Sub- ...") do |sub3|
19
- sub2.cmd( :help, "") {|*args| STDERR.puts cli.help( 'more', 'deeper', 'sub-commands', *args) }
44
+ sub3.cmd( :help, "") {|*args| STDERR.puts sub3.help( sub3, *args) }
20
45
  sub3.cmd( :hehe, "The real last example", min: 2) { STDERR.puts "Trust me!" }
21
46
  end
22
47
  end
23
48
  end
24
49
 
25
- cli.call *ARGV
50
+ begin
51
+ cli.call *ARGV
52
+ rescue DenCli::UsageError
53
+ STDERR.puts $!
54
+ exit 1
55
+ end
data/lib/dencli/cmd.rb CHANGED
@@ -1,30 +1,214 @@
1
1
  require_relative '../dencli'
2
2
 
3
+
3
4
  class DenCli::CMD
4
- attr_reader :parent, :name, :desc, :exe, :completion
5
+ attr_reader :parent, :name, :description, :exe, :completion, :options
5
6
 
6
- def initialize parent, name, desc, exe
7
+ def initialize parent, name, description, exe
7
8
  raise "Proc expected, instead of: #{exe.inspect}" unless Proc === exe
8
- @parent, @name, @desc, @exe = parent, name, desc, exe
9
+ @parent, @name, @description, @exe = parent, name, description, lambda( &exe)
10
+ @options = {}
9
11
  completion {|*a| [] }
10
12
  end
11
13
 
12
14
  def _full_cmd( post) parent._full_cmd [@name]+post end
13
15
  def full_cmd() _full_cmd [] end
14
- def call( *a) @exe.call *a end
15
- def help() "#{parent.full_cmd.join ' '} #{name}\n#{ desc}" end
16
16
 
17
- def complete( *pre, str) @completion.call *pre, str end
17
+ def parameters() @exe.parameters end
18
+ def arguments_required() @exe.parameters.select {|e| :req == e[0] }.map {|e| e[1] } end
19
+ alias required arguments_required
20
+ def arguments_additional() @exe.parameters.select {|e| :opt == e[0] }.map {|e| e[1] } end
21
+ alias additional arguments_additional
22
+ def arguments() @exe.parameters.select {|e| :req == e[0] or :opt == e[0] }.map {|e| e[1] } end
23
+ def options_required() @exe.parameters.select {|e| :keyreq == e[0] }.map {|e| e[1] } end
24
+ def options_additional() @exe.parameters.select {|e| :key == e[0] }.map {|e| e[1] } end
25
+
26
+ def complete *pre, str
27
+ @completion.call *pre, str
28
+ end
29
+
30
+ def call *as
31
+ os = {}
32
+ unless @options.empty?
33
+ # options like --abc | -x will be provided in os
34
+ options = OptionParser.new
35
+ options.banner = "#{full_cmd.join ' '}"
36
+ # see also @options-array
37
+ @options.each {|_, opt| opt.on options, os }
38
+ as = options.parse! as
39
+ end
40
+ if @exe.lambda?
41
+ # The difference between a lambda and a Proc is, that Proc has anytime arity=-1.
42
+ # There will be no check if all arguments are given or some were missing or more than expected.
43
+ # lambda checks these arguments and has a arity.
44
+ # We will check it to provide useful errors.
45
+ pars = required
46
+ if as.length < pars.length
47
+ raise DenCli::UsageError, "Missing parameter(s): #{pars[as.length..-1].join " "}"
48
+ end
49
+ if parameters.select {|e| :rest == e[0] }.empty?
50
+ pars = pars + additional
51
+ if as.length > pars.length
52
+ raise DenCli::UsageError, "Unused parameter(s): #{as[-pars.length..-1].shelljoin}"
53
+ end
54
+ end
55
+ kr = @options.select {|_, o| o.required? and not os.has_key? o.name }
56
+ unless kr.empty?
57
+ raise DenCli::UsageError, "Missing argument(s): #{kr.map {|o| o.long || o.short }.join ', '}"
58
+ end
59
+ end
60
+ @exe.call *as, **os
61
+ end
62
+
63
+ def usage output: nil
64
+ output ||= ''
65
+ _usage output
66
+ output
67
+ end
68
+
69
+ def _usage output
70
+ output << full_cmd.join( ' ')
71
+ @options.each do |_, o|
72
+ s = "#{o.short||o.long}#{o.val ? ?= : ''}#{o.val}"
73
+ output << (o.required? ? " #{s}" : " [#{s}]")
74
+ end
75
+ if @exe.lambda?
76
+ required.each {|s| output << " <#{s}>" }
77
+ output << " [#{additional.map{|s|"<#{s}>"}.join " "}]" unless additional.empty?
78
+ else
79
+ output << ' ...'
80
+ end
81
+ end
82
+
83
+ def commands *args, &exe
84
+ yield self
85
+ end
86
+
87
+ def goto *args
88
+ self
89
+ end
90
+
91
+ def help output: nil
92
+ output ||= ''
93
+ _help output
94
+ output
95
+ end
96
+
97
+ def _help output
98
+ output << "Usage: #{usage}\n#{description}\n"
99
+ _help_options output
100
+ end
101
+
102
+ def help_options output: nil
103
+ output ||= ''
104
+ _help_options output
105
+ output
106
+ end
107
+
108
+ def _help_options output
109
+ sc, lc, dc = 0, 0, 0
110
+ @options.each do |_, o|
111
+ s = o.short&.length || 0
112
+ l = o.long&.length || 0
113
+ v = o.val&.length || 0
114
+ d = o.desc&.to_s&.length || 0
115
+ d += 3 + o.default.to_s.length if o.default?
116
+ if 0 == l
117
+ x = s + (0==v ? 0 : 1+v)
118
+ sc = x if sc < x
119
+ else
120
+ sc = s if sc < s
121
+ x = l + (0==v ? 0 : 1+v)
122
+ lc = x if lc < x
123
+ end
124
+ dc = d if dc < d
125
+ end
126
+ format = " %-#{sc}s%s %-#{lc}s %s\n"
127
+ @options.map do |_, o|
128
+ s, l, v, y = o.short, o.long, o.val, ','
129
+ if l.nil?
130
+ s += "=#{v}" if v
131
+ y = ' '
132
+ elsif s.nil?
133
+ l += "=#{v}" if v
134
+ y = ' '
135
+ end
136
+ d = o.desc || ''
137
+ d += " (#{o.default})" if o.default?
138
+ output << format % [ s, y, l, d ]
139
+ end
140
+ end
18
141
 
19
142
  def completion &exe
20
143
  @completion = exe
21
144
  self
22
145
  end
23
146
 
147
+ class Opt
148
+ attr_reader :name, :long, :short, :val, :desc, :os, :conv, :req
149
+ def required?() @req end
150
+ def default?() NilClass != @default end
151
+ def default() NilClass == @default ? nil : @default end
152
+
153
+ def parse_opt_string opt
154
+ case opt
155
+ when /\A(--\[no-\][^=]+)\z/
156
+ @long, @val = $1, nil
157
+ when /\A(--[^=]+)=(.+)\z/
158
+ @long, @val = $1, $2 || @val
159
+ when /\A(--[^=]+)\z/
160
+ @long, @val = $1, nil
161
+ when /\A(-[^=-]+)=(.+)\z/
162
+ @short, @val = $1, $2 || @val
163
+ when /\A(-[^=-]+)\z/
164
+ @short, @val = $1, nil
165
+ else
166
+ raise ArgumentError, "Unexpected format for option: #{opt.inspect}"
167
+ end
168
+ end
169
+ private :parse_opt_string
170
+
171
+ def initialize cmd, name, opt, *alts, desc, default: NilClass, **os, &conv
172
+ @name, @desc, @default, @os, @conv, @val =
173
+ name.to_s.to_sym, desc, default, os, conv || lambda{|v|v}, nil
174
+ parse_opt_string opt
175
+ alts.each &method( :parse_opt_string)
176
+ @req =
177
+ if NilClass != default
178
+ false
179
+ elsif cmd.exe.lambda?
180
+ ! cmd.exe.parameters.select {|e| [:keyreq, @name] == e[0..1] }.empty?
181
+ else
182
+ nil
183
+ end
184
+ end
185
+
186
+ def on parser, store
187
+ store[@name] = @default if default?
188
+ parser.on "#{@short}#{@val ? ?= : ''}#{@val}", "#{@long}#{@val ? ?= : ''}#{@val}", **@os do |val|
189
+ store[@name] = @conv[val]
190
+ end
191
+ end
192
+
193
+ def inspect
194
+ "#<%s:0x%016x %s %s %s %s (%p) %p os=%p conv=%s>" % [
195
+ self.class.name, object_id, @req ? "<#{@name}>" : "[#{@name}]",
196
+ @short, @long, @val, @default, @desc, @os,
197
+ @exe ? "<#{@exe.lambda? ? :lambda: :proc} ##{@exe.arity}>" : "nil"
198
+ ]
199
+ end
200
+ end
201
+
202
+ def opt name, opt, *alts, desc, **os, &conv
203
+ r = Opt.new( self, name, opt, *alts, desc, **os, &conv)
204
+ @options[r.name] = r
205
+ self
206
+ end
207
+
24
208
  def inspect
25
- "#<%s:0x%x %s @name=%p @desc=%p @parent=<%s:0x%x %s> @exe=<arity=%d>>" % [
209
+ "#<%s:0x%x %s @name=%p @description=%p @parent=<%s:0x%x %s> @exe=<arity=%d>>" % [
26
210
  self.class.name, self.object_id, self.full_cmd,
27
- @name, @desc, @parent.class.name, @parent.class.object_id, @parent.full_cmd,
211
+ @name, @description, @parent.class.name, @parent.class.object_id, @parent.full_cmd,
28
212
  @exe.arity
29
213
  ]
30
214
  end
@@ -90,7 +90,7 @@ class DenCli::Interactive
90
90
  c.subs.values.each do |n|
91
91
  case n
92
92
  when DenCli::Sub
93
- n.cmd :exit, "<- #{n.parent.full_cmd.join ' '} - #{n.parent.desc[3..-1]}", min: 2 do
93
+ n.cmd :exit, "<- #{n.parent.full_cmd.join ' '} - #{n.parent.description[3..-1]}", min: 2 do
94
94
  @cur = n.parent
95
95
  end
96
96
  n.cmd '', "", min: 2, aliases: [nil] do
data/lib/dencli/sub.rb CHANGED
@@ -1,26 +1,86 @@
1
1
  require_relative '../dencli'
2
2
 
3
3
  class DenCli::Sub
4
- attr_reader :parent, :name, :desc, :subs, :aliases
4
+ attr_reader :parent, :name, :description, :subs, :aliases
5
5
 
6
- def initialize parent, name, desc
7
- @parent, @name, @desc, @subs, @aliases = parent, name, "-> #{desc}", {}, {}
6
+ def initialize parent, name, description
7
+ @parent, @name, @description, @subs, @aliases = parent, name, "-> #{description}", {}, {}
8
8
  end
9
9
 
10
10
  def _full_cmd( post) parent._full_cmd [@name]+post end
11
11
  def full_cmd() _full_cmd [] end
12
12
  def []( k) @aliases[k] end
13
13
 
14
- def help n = nil, *a
14
+ def usage
15
+ "#{full_cmd.join ' '} ..."
16
+ end
17
+
18
+ def help n = nil, *a, output: nil
19
+ output ||= ''
20
+ _help output, n, *a
21
+ output
22
+ end
23
+
24
+ def _help output, n = nil, *a
15
25
  if n.nil?
16
- r = "#{full_cmd.join ' '}: #{desc}\n\n"
17
- m = @subs.map {|k,_| k.length }.max
18
- @subs.each do |k, c|
19
- r += " % -#{m}s %s\n" % [k, c.desc] unless k.nil?
20
- end
21
- r
26
+ output << "#{full_cmd.join ' '}: #{description}\n\n"
27
+ self.class._help_commands output, @subs
22
28
  elsif @aliases.has_key? n
23
- @aliases[n].help *a
29
+ @aliases[n]._help output, *a
30
+ else
31
+ raise DenCli::UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.join ' '}"
32
+ end
33
+ end
34
+
35
+ def commands &exe
36
+ yield self
37
+ @subs.each {|k, c| c.commands &exe }
38
+ end
39
+
40
+ def help_commands output: nil
41
+ output ||= ''
42
+ self.class._help_commands output, subs.map {|_,c| c}
43
+ output
44
+ end
45
+
46
+ def self._help_commands output, subs
47
+ m = subs.map {|c| x = c.usage.length; 25 < x ? 0 : x }.max
48
+ subs.each do |c|
49
+ if 25 < c.usage.length
50
+ output << "% -#{m}s\n#{' ' * m} " % [c.usage]
51
+ else
52
+ output << "% -#{m}s " % [c.usage]
53
+ end
54
+ n = m+2
55
+ prefix = nil
56
+ c.description.split /\n/ do |l|
57
+ c = 0
58
+ l.split %r< > do |w|
59
+ if prefix
60
+ output << prefix
61
+ prefix = nil
62
+ end
63
+ wl = w.length
64
+ if 75 < c+wl
65
+ output << "\n#{' ' * n}#{w}"
66
+ c = n+2+wl
67
+ else
68
+ output << " #{w}"
69
+ c += 1 + wl
70
+ end
71
+ end
72
+ prefix = "\n#{' ' * n}"
73
+ end
74
+ output << "\n"
75
+ end
76
+ output
77
+ end
78
+
79
+ def goto *a
80
+ return self if a.empty?
81
+ n, *a = *a
82
+ if @aliases.has_key? n
83
+ @aliases[n].goto *a
24
84
  else
25
85
  raise DenCli::UnknownCommand, "unknown command: #{_full_cmd( [n])[1..-1].join ' '}, available for #{full_cmd[1..-1].join' '}: #{@subs.keys.join ' '}"
26
86
  end
@@ -46,13 +106,13 @@ class DenCli::Sub
46
106
  end
47
107
  private :_add
48
108
 
49
- def sub name, desc, min: nil, aliases: nil, &exe
50
- r = _add name, min, DenCli::Sub.new( self, name, desc), aliases
109
+ def sub name, description, min: nil, aliases: nil, &exe
110
+ r = _add name, min, DenCli::Sub.new( self, name, description), aliases
51
111
  block_given? ? yield( r) : r
52
112
  end
53
113
 
54
- def cmd name, desc, min: nil, aliases: nil, &exe
55
- _add name, min, DenCli::CMD.new( self, name, desc, exe), aliases
114
+ def cmd name, description, min: nil, aliases: nil, &exe
115
+ _add name, min, DenCli::CMD.new( self, name, description, exe), aliases
56
116
  end
57
117
 
58
118
  def complete *pre, str
@@ -66,9 +126,9 @@ class DenCli::Sub
66
126
  end
67
127
 
68
128
  def inspect
69
- "#<%s:0x%x %s @name=%p @desc=%p @subs={%s} @aliases={%s} @parent=<%s:0x%x %s>>" % [
129
+ "#<%s:0x%x %s @name=%p @description=%p @subs={%s} @aliases={%s} @parent=<%s:0x%x %s>>" % [
70
130
  self.class.name, self.object_id, self.full_cmd,
71
- @name, @desc, @subs.keys.join(', '), @aliases.keys.join(', '), @parent.class.name, @parent.class.object_id, @parent.full_cmd
131
+ @name, @description, @subs.keys.join(', '), @aliases.keys.join(', '), @parent.class.name, @parent.class.object_id, @parent.full_cmd
72
132
  ]
73
133
  end
74
134
  end
@@ -1,3 +1,3 @@
1
1
  class DenCli
2
- VERSION = '0.2.1'
2
+ VERSION = '0.5.0'
3
3
  end
data/lib/dencli.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'optparse'
2
+
1
3
  class DenCli
2
4
  class UsageError < ::RuntimeError
3
5
  end
@@ -54,8 +56,8 @@ class DenCli
54
56
 
55
57
  attr_reader :subs
56
58
 
57
- def initialize progname, desc
58
- @subs = Sub.new self, progname, desc
59
+ def initialize progname, description
60
+ @subs = Sub.new self, progname, description
59
61
  end
60
62
 
61
63
  def full_cmd
@@ -66,20 +68,34 @@ class DenCli
66
68
  post
67
69
  end
68
70
 
69
- def sub *a, &exe
70
- @subs.sub *a, &exe
71
+ def sub *a, **o, &exe
72
+ @subs.sub *a, **o, &exe
71
73
  end
72
74
 
73
- def cmd *a, &exe
74
- @subs.cmd *a, &exe
75
+ def cmd *a, **o, &exe
76
+ @subs.cmd *a, **o, &exe
75
77
  end
76
78
 
77
79
  def call *a
78
80
  @subs.call *a
79
81
  end
80
82
 
81
- def help *args
82
- @subs.help *args
83
+ def usage *args, **opts
84
+ @subs.usage *args, **opts
85
+ end
86
+
87
+ def help *args, **opts
88
+ @subs.help *args, **opts
89
+ end
90
+
91
+ def help_full *args, output: nil
92
+ output ||= STDOUT
93
+ x = @subs.goto *args
94
+ _help_full output, x
95
+ end
96
+
97
+ def _help_full output, subs
98
+ Sub._help_commands output, subs.to_enum( :commands)
83
99
  end
84
100
 
85
101
  def [] k
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dencli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Knauf
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-27 00:00:00.000000000 Z
11
+ date: 2021-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec