dencli 0.2.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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