nub 0.0.25 → 0.0.27

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: d8cba0302ba890a2a1332cd5e1e9696ccee3ac8a4235bc86d3d4292de23efa56
4
- data.tar.gz: a35b6c9a6d0925873e19325d903a417c56df346790bcc1f8d96f72f197e7d637
3
+ metadata.gz: 9d5f8ebb6c991951280634bd778890442ead23922928d9a113f6f04449660c5d
4
+ data.tar.gz: 49b25510286dcd087882043bd49209b63bb2688bb338054c1dd3a7d34d5da97c
5
5
  SHA512:
6
- metadata.gz: ca6ff75a2f4e7cf35d9dd0b7440bb82d48946081a01a4646913b0ee1a06da219ffa478d097a7b0dfc2a9dd5356c7cd885adb22985b2cd92115c0f98d2fd7ad7b
7
- data.tar.gz: 0340f674692f7840f4f33a0323d9fce5598991031b26e3b1164f54ec351cc4a7117bb1d24462e8d22e5e99a588a1545b0d5f6a406316af2b00d9e2754aec3630
6
+ metadata.gz: ae5491f1c835a8d9de56127c1b486a875195fd12259834a928c5bc9dd915964114a11c6c8a0b1d795c99d9c121386a72f271d9e48c2b6a66014777a8058c8b77
7
+ data.tar.gz: 1d19f786cfda8a5f21b4913ce42b6b22ad38a4a62bacafb3b72ddea9a4b14556be2d923a3194fb660f82a75977e8ade4573412863caa4993e122d33dba294965
data/lib/nub.rb CHANGED
@@ -22,7 +22,7 @@
22
22
 
23
23
  # Nub is the top level module useful for requiring all sub-modules at once.
24
24
  module Nub
25
- require 'nub/cmds'
25
+ require 'nub/commander'
26
26
  require 'nub/config'
27
27
  require 'nub/log'
28
28
  require 'nub/net'
@@ -0,0 +1,277 @@
1
+ #MIT License
2
+ #Copyright (c) 2018 phR0ze
3
+ #
4
+ #Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ #of this software and associated documentation files (the "Software"), to deal
6
+ #in the Software without restriction, including without limitation the rights
7
+ #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ #copies of the Software, and to permit persons to whom the Software is
9
+ #furnished to do so, subject to the following conditions:
10
+ #
11
+ #The above copyright notice and this permission notice shall be included in all
12
+ #copies or substantial portions of the Software.
13
+ #
14
+ #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ #SOFTWARE.
21
+
22
+ require 'colorize'
23
+
24
+ # Command option encapsulation
25
+ class Option
26
+ attr_reader(:key)
27
+ attr_reader(:short)
28
+ attr_reader(:long)
29
+ attr_reader(:hint)
30
+ attr_reader(:desc)
31
+ attr_reader(:type)
32
+ attr_reader(:allowed)
33
+ attr_reader(:required)
34
+
35
+ # Create a new option instance
36
+ # @param key [String] option short hand, long hand and hint e.g. -s|--skip=COMPONENTS
37
+ # @param desc [String] the option's description
38
+ # @param type [Type] the option's type
39
+ # @param required [Bool] require the option if true else optional
40
+ # @param allowed [Array] array of allowed string values
41
+ def initialize(key, desc, type:nil, required:false, allowed:[])
42
+ @hint = nil
43
+ @long = nil
44
+ @short = nil
45
+ @desc = desc
46
+ @allowed = allowed || []
47
+ @required = required || false
48
+
49
+ # Parse the key into its components (short hand, long hand, and hint)
50
+ #https://bneijt.nl/pr/ruby-regular-expressions/
51
+ # Valid forms to look for with chars [a-zA-Z0-9-_=|]
52
+ # --help, --help=HINT, -h|--help, -h|--help=HINT
53
+ !puts("Error: invalid option key #{key}".colorize(:red)) and
54
+ exit if key && (key.count('=') > 1 or key.count('|') > 1 or !key[/[^\w\-=|]/].nil? or
55
+ key[/(^--[a-zA-Z0-9\-_]+$)|(^--[a-zA-Z\-_]+=\w+$)|(^-[a-zA-Z]\|--[a-zA-Z0-9\-_]+$)|(^-[a-zA-Z]\|--[a-zA-Z0-9\-_]+=\w+$)/].nil?)
56
+ @key = key
57
+ if key
58
+ @hint = key[/.*=(.*)$/, 1]
59
+ @short = key[/^(-\w).*$/, 1]
60
+ @long = key[/(--[\w\-]+)(=.+)*$/, 1]
61
+ else
62
+ # Always require positional options
63
+ @required = true
64
+ end
65
+
66
+ # Validate and set type
67
+ !puts("Error: invalid option type #{type}".colorize(:red)) and
68
+ exit if ![String, Integer, Array, nil].any?{|x| type == x}
69
+ !puts("Error: option type must be set".colorize(:red)) and
70
+ exit if @hint && !type
71
+ @type = String if !key && !type
72
+ @type = FalseClass if key and !type
73
+ @type = type if type
74
+
75
+ # Validate allowed
76
+ if @allowed.any?
77
+ allowed_type = @allowed.first.class
78
+ !puts("Error: mixed allowed types".colorize(:red)) and
79
+ exit if @allowed.any?{|x| x.class != allowed_type}
80
+ end
81
+ end
82
+ end
83
+
84
+ # An implementation of git like command syntax for ruby applications:
85
+ # see https://github.com/phR0ze/ruby-nub
86
+ class Commander
87
+ attr_accessor(:cmds)
88
+ attr_reader(:config)
89
+ attr_reader(:banner)
90
+
91
+ Command = Struct.new(:name, :desc, :opts, :help)
92
+
93
+ # Initialize the commands for your application
94
+ # @param app [String] application name e.g. reduce
95
+ # @param version [String] version of the application e.g. 1.0.0
96
+ # @param examples [String] optional examples to list after the title before usage
97
+ def initialize(app, version, examples:nil)
98
+ @app = app
99
+ @version = version
100
+ @examples = examples
101
+ !puts("Error: app name and version are required".colorize(:red)) and
102
+ exit if @app.nil? or @app.empty? or @version.nil? or @version.empty?
103
+
104
+ @help_opt = Option.new('-h|--help', 'Print command/options help')
105
+ @just = 40
106
+
107
+ # Configuration - ordered list of commands
108
+ @config = []
109
+
110
+ # Incoming user set commands/options
111
+ # {command_name => {}}
112
+ @cmds = {}
113
+ end
114
+
115
+ # Hash like accessor for checking if a command or option is set
116
+ def [](key)
117
+ return @cmds[key] if @cmds[key]
118
+ end
119
+
120
+ # Add a command to the command list
121
+ # @param cmd [String] name of the command
122
+ # @param desc [String] description of the command
123
+ # @param opts [List] list of command options
124
+ def add(cmd, desc, options:[])
125
+ !puts("Error: command names must be pure lowercase letters".colorize(:red)) and
126
+ exit if cmd =~ /[^a-z]/
127
+
128
+ # Build help for command
129
+ help = "#{banner}\n#{desc}\n\nUsage: ./#{@app} #{cmd} [options]\n"
130
+ options << @help_opt
131
+
132
+ # Add positional options first
133
+ sorted_options = options.select{|x| x.key.nil?}
134
+ sorted_options += options.select{|x| !x.key.nil?}.sort{|x,y| x.key <=> y.key}
135
+ positional_index = -1
136
+ sorted_options.each{|x|
137
+ required = x.required ? ", Required" : ""
138
+ allowed = x.allowed.empty? ? "" : " (#{x.allowed * ','})"
139
+ positional_index += 1 if x.key.nil?
140
+ key = x.key.nil? ? "#{cmd}#{positional_index}" : x.key
141
+ type = x.type == FalseClass ? "Flag" : x.type
142
+ help += " #{key.ljust(@just)}#{x.desc}#{allowed}: #{type}#{required}\n"
143
+ }
144
+
145
+ # Create the command in the command config
146
+ @config << Command.new(cmd, desc, sorted_options, help)
147
+ end
148
+
149
+ # Returns banner string
150
+ # @returns [String] the app's banner
151
+ def banner
152
+ banner = "#{@app}_v#{@version}\n#{'-' * 80}".colorize(:light_yellow)
153
+ return banner
154
+ end
155
+
156
+ # Return the app's help string
157
+ # @returns [String] the app's help string
158
+ def help
159
+ help = "#{banner}\n"
160
+ help += "Examples:\n#{@examples}\n\n" if !@examples.nil? && !@examples.empty?
161
+ help += "Usage: ./#{@app} [commands] [options]\n"
162
+ help += " #{'-h|--help'.ljust(@just)}Print command/options help: Flag\n"
163
+ help += "COMMANDS:\n"
164
+ @config.each{|x| help += " #{x.name.ljust(@just)}#{x.desc}\n" }
165
+ help += "\nsee './#{@app} COMMAND --help' for specific command help\n"
166
+
167
+ return help
168
+ end
169
+
170
+ # Construct the command line parser and parse
171
+ def parse!
172
+
173
+ # Set help if nothing was given
174
+ ARGV.clear and ARGV << '-h' if ARGV.empty?
175
+
176
+ # Process global options
177
+ #---------------------------------------------------------------------------
178
+ cmd_names = @config.map{|x| x.name }
179
+ globals = ARGV.take_while{|x| !cmd_names.include?(x)}
180
+ !puts(help) and exit if globals.any?
181
+
182
+ # Process command options
183
+ #---------------------------------------------------------------------------
184
+ loop {
185
+ break if ARGV.first.nil?
186
+
187
+ if !(cmd = @config.find{|x| x.name == ARGV.first}).nil?
188
+ @cmds[ARGV.shift.to_sym] = {}
189
+ cmd_names.reject!{|x| x == cmd.name}
190
+
191
+ # Collect command options
192
+ opts = ARGV.take_while{|x| !cmd_names.include?(x) }
193
+ ARGV.shift(opts.size)
194
+ cmd_pos_opts = cmd.opts.select{|x| x.key.nil? }
195
+ cmd_named_opts = cmd.opts.select{|x| !x.key.nil? }
196
+ !puts("Error: positional option required".colorize(:red)) && !puts(cmd.help) and
197
+ exit if opts.size < cmd_pos_opts.size
198
+
199
+ # Process command options
200
+ pos = -1
201
+ loop {
202
+ break if opts.first.nil?
203
+ opt = opts.shift
204
+ cmd_opt = nil
205
+ value = nil
206
+ sym = nil
207
+
208
+ # Validate/set named options
209
+ # --------------------------------------------------------------------
210
+ # e.g. -s, --skip, --skip=VALUE
211
+ if opt.start_with?('-')
212
+ short = opt[/^(-\w).*$/, 1]
213
+ long = opt[/(--[\w\-]+)(=.+)*$/, 1]
214
+ value = opt[/.*=(.*)$/, 1]
215
+
216
+ # Set symbol converting dashes to underscores for named options
217
+ if (cmd_opt = cmd_named_opts.find{|x| x.short == short || x.long == long})
218
+ sym = cmd_opt.long[2..-1].gsub("-", "_").to_sym
219
+
220
+ # Collect value
221
+ if cmd_opt.type == FalseClass
222
+ value = true if !value
223
+ elsif !value
224
+ value = opts.shift
225
+ end
226
+ end
227
+
228
+ # Validate/set positional options
229
+ # --------------------------------------------------------------------
230
+ else
231
+ pos += 1
232
+ cmd_opt = cmd_pos_opts.shift
233
+ !puts("Error: invalid positional option '#{opt}'".colorize(:red)) && !puts(cmd.help) and
234
+ exit if cmd_opt.nil?
235
+ value = opt
236
+ sym = "#{cmd.name}#{pos}".to_sym
237
+ end
238
+
239
+ # Convert value to appropriate type
240
+ # --------------------------------------------------------------------
241
+ if value
242
+ if cmd_opt.type == Integer
243
+ value = value.to_i
244
+
245
+ # Validate allowed values
246
+ if cmd_opt.allowed.any?
247
+ !puts("Error: invalid integer value '#{value}'".colorize(:red)) && !puts(cmd.help) and
248
+ exit if !cmd_opt.allowed.include?(value)
249
+ end
250
+
251
+ elsif cmd_opt.type == Array
252
+ value = value.split(',')
253
+
254
+ # Validate allowed values
255
+ if cmd_opt.allowed.any?
256
+ value.each{|x|
257
+ !puts("Error: invalid array value '#{x}'".colorize(:red)) && !puts(cmd.help) and
258
+ exit if !cmd_opt.allowed.include?(x)
259
+ }
260
+ end
261
+ end
262
+ end
263
+
264
+ # Set option with value
265
+ # --------------------------------------------------------------------
266
+ !puts("Error: unknown named option '#{opt}' given".colorize(:red)) && !puts(cmd.help) and exit if !sym
267
+ @cmds[cmd.name.to_sym][sym] = value
268
+ }
269
+ end
270
+ }
271
+
272
+ # Ensure all options were consumed
273
+ !puts("Error: invalid options #{ARGV}".colorize(:red)) and exit if ARGV.any?
274
+ end
275
+ end
276
+
277
+ # vim: ft=ruby:ts=2:sw=2:sts=2
data/lib/nub/sys.rb CHANGED
@@ -20,11 +20,26 @@
20
20
  #SOFTWARE.
21
21
 
22
22
  require 'io/console'
23
+ require 'ostruct'
24
+ require 'stringio'
23
25
 
24
- class Sys
26
+ module Sys
27
+
28
+ # Capture STDOUT to a string
29
+ # @returns [String] the redirected output
30
+ def self.capture(&block)
31
+ stdout, stderr = StringIO.new, StringIO.new
32
+ $stdout, $stderr = stdout, stderr
33
+
34
+ result = block.call
35
+
36
+ $stdout, $stderr = STDOUT, STDERR
37
+
38
+ return OpenStruct.new(result: result, stdout: stdout.string, stderr: stderr.string)
39
+ end
25
40
 
26
41
  # Wait for any key to be pressed
27
- def any_key?
42
+ def self.any_key?
28
43
  begin
29
44
  state = `stty -g`
30
45
  `stty raw -echo -icanon isig`
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nub
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.25
4
+ version: 0.0.27
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick Crummett
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-23 00:00:00.000000000 Z
11
+ date: 2018-03-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -101,7 +101,7 @@ extensions: []
101
101
  extra_rdoc_files: []
102
102
  files:
103
103
  - lib/nub.rb
104
- - lib/nub/cmds.rb
104
+ - lib/nub/commander.rb
105
105
  - lib/nub/config.rb
106
106
  - lib/nub/log.rb
107
107
  - lib/nub/net.rb
data/lib/nub/cmds.rb DELETED
@@ -1,146 +0,0 @@
1
- #MIT License
2
- #Copyright (c) 2018 phR0ze
3
- #
4
- #Permission is hereby granted, free of charge, to any person obtaining a copy
5
- #of this software and associated documentation files (the "Software"), to deal
6
- #in the Software without restriction, including without limitation the rights
7
- #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
- #copies of the Software, and to permit persons to whom the Software is
9
- #furnished to do so, subject to the following conditions:
10
- #
11
- #The above copyright notice and this permission notice shall be included in all
12
- #copies or substantial portions of the Software.
13
- #
14
- #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
- #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
- #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
- #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
- #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
- #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
- #SOFTWARE.
21
-
22
- require 'optparse'
23
- require 'colorize'
24
-
25
- # Command option class provides a way to encapsulate a command with
26
- # any additional properties.
27
- class CmdOpt
28
- attr_reader(:key)
29
- attr_reader(:conf)
30
- attr_reader(:type)
31
- attr_reader(:desc)
32
- attr_reader(:required)
33
- def initialize(conf, desc, type:nil, required:false)
34
- @conf = conf.gsub(' ', '=')
35
- @key = conf.gsub('-', '').split('=').first.to_sym
36
- @type = type
37
- @desc = desc
38
- @required = required
39
- end
40
- end
41
-
42
- # Simple command wrapper around options parsing
43
- # When multiple commands are given they share the options passed along with them
44
- class Cmds
45
-
46
- # Option and command names have all hyphens removed
47
- attr_accessor(:cmds)
48
- attr_accessor(:opts)
49
-
50
- # Initialize the commands for your application
51
- # Params:
52
- # +app+:: application name e.g. reduce
53
- # +version+:: version of the application e.g. 1.0.0
54
- # +examples+:: optional examples to list after the title before usage
55
- def initialize(app, version, examples)
56
- @opts = {}
57
- @cmds = {}
58
- @cmds_config = {}
59
-
60
- @app = app
61
- @version = version
62
- @examples = examples || ''
63
- end
64
-
65
- # Hash like accessor for checking if a command is set
66
- def [](key)
67
- return @cmds[key] if @cmds[key]
68
- return @opts[key] if @opts[key]
69
- end
70
-
71
- # Hash like accessor for editing options
72
- def []=(key, value)
73
- @opts[key] = value
74
- end
75
-
76
- # Add a command to the command list
77
- # Params:
78
- # +cmd+:: name of the command
79
- # +desc+:: description of the command
80
- # +opts+:: list of command options
81
- def add(cmd, desc, opts)
82
- @cmds_config[cmd] = {desc: desc, inopts: opts, outopts: OptionParser.new{|parser|
83
- required = opts.map{|x| x.conf if x.required}.compact * ' '
84
- required += ' ' if not required.empty?
85
- parser.banner = "#{banner}\nUsage: ./#{@app} #{cmd} #{required}[options]"
86
- opts.each{|opt| parser.on(opt.conf, opt.type, opt.desc){|x| @opts[opt.key] = x }}
87
- }}
88
- end
89
-
90
- # Returns banner string
91
- def banner
92
- banner = "#{@app}_v#{@version}\n#{'-' * 80}".colorize(:light_yellow)
93
- return banner
94
- end
95
-
96
- # Construct the command line parser and parse
97
- def parse!
98
-
99
- # Construct help for the application
100
- help = "COMMANDS:\n"
101
- @cmds_config.each{|k,v| help += " #{k.ljust(33, ' ')}#{v[:desc]}\n" }
102
- help += "\nsee './#{@app} COMMAND --help' for specific command info"
103
-
104
- # Construct top level option parser
105
- @optparser = OptionParser.new do |parser|
106
- parser.banner = "#{banner}\n#{@examples}Usage: ./#{@app} commands [options]"
107
- parser.on('-h', '--help', 'Print command/options help') {|x| !puts(parser) and exit }
108
- parser.separator(help)
109
- end
110
-
111
- # Invoke the option parser with help if any un-recognized commands are given
112
- cmds = ARGV.select{|x| not x.start_with?('-')}
113
- ARGV.clear and ARGV << '-h' if ARGV.empty? or cmds.any?{|x| not @cmds_config[x]}
114
- cmds.each{|x| puts("Error: Invalid command '#{x}'".colorize(:red)) if not @cmds_config[x]}
115
- @optparser.order!
116
-
117
- # Now remove them from ARGV leaving only options
118
- ARGV.reject!{|x| not x.start_with?('-')}
119
-
120
- # Parse each command which will consume options from ARGV
121
- cmds.each do |cmd|
122
- begin
123
- @cmds[cmd.gsub('-', '_').to_sym] = true
124
- @cmds_config[cmd][:outopts].order!
125
-
126
- # Ensure that all required options were given
127
- @cmds_config[cmd][:inopts].each{|x|
128
- if x.required and not @opts[x.key]
129
- puts("Error: Missing required option '#{x.key}'".colorize(:red))
130
- ARGV.clear and ARGV << "-h"
131
- @cmds_config[cmd][:outopts].order!
132
- end
133
- }
134
- rescue OptionParser::InvalidOption => e
135
- # Options parser will raise an invalid exception if it doesn't recognize something
136
- # However we want to ignore that as it may be another command's option
137
- ARGV << e.to_s[/(-.*)/, 1]
138
- end
139
- end
140
-
141
- # Ensure all options were consumed
142
- !puts("Error: invalid options #{ARGV}".colorize(:red)) and exit if ARGV.any?
143
- end
144
- end
145
-
146
- # vim: ft=ruby:ts=2:sw=2:sts=2