cli_utils 0.0.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.
Files changed (2) hide show
  1. data/lib/cli_utils.rb +216 -0
  2. metadata +46 -0
data/lib/cli_utils.rb ADDED
@@ -0,0 +1,216 @@
1
+ require 'json'
2
+
3
+ module Errors
4
+ class ArgumentError < StandardError
5
+ end
6
+
7
+ class ParseError < StandardError
8
+ end
9
+
10
+ class MissingCommandError < StandardError
11
+ end
12
+
13
+ class MissingFileError < StandardError
14
+ end
15
+ end
16
+
17
+ class CliUtils
18
+ include Errors
19
+ attr_accessor :commands, :command, :optional,:required, :config
20
+
21
+ def initialize(commands_filepath=nil, config_filepath=nil, suggestions_count=nil)
22
+ @s_count = suggestions_count || 4
23
+
24
+ begin
25
+ init_commands(commands_filepath)
26
+ init_config(config_filepath)
27
+ parse_options
28
+
29
+ if @command
30
+ method = @commands[@command]['eval']
31
+ if method
32
+ eval method
33
+ exit
34
+ end
35
+ end
36
+
37
+ rescue ParseError => e
38
+ render_error e
39
+ rescue ArgumentError => e
40
+ render_error e
41
+ rescue MissingFileError => e
42
+ render_error e
43
+ rescue MissingCommandError => e
44
+ err = "#{e.message} is not a command. Did you mean:\n\n"
45
+ alts = CliUtils::top_matches(e.message, @commands.keys, @s_count).map{|m| usage(m)}.join("\n")
46
+ $stderr.puts("#{err}#{alts}")
47
+ exit 1
48
+ end
49
+ end
50
+
51
+ def self.levenshtein_distance(s, t)
52
+ return 0 if s == t
53
+ return t.length if s.length == 0
54
+ return s.length if t.length == 0
55
+
56
+ a0 = (0..t.length + 1).to_a
57
+ a1 = []
58
+
59
+ (0..s.length - 1).each{|i|
60
+ a1[0] = i + 1
61
+
62
+ (0..t.length - 1).each{|j|
63
+ cost = (s[i] == t[j]) ? 0 : 1
64
+ a1[j + 1] = [a1[j] + 1, a0[j + 1] + 1, a0[j] + cost].min
65
+ }
66
+ a0 = a1.clone
67
+ }
68
+
69
+ return a1[t.length]
70
+ end
71
+
72
+ def self.test_lev
73
+ ts = [ ['' , 'abc', 3],
74
+ ['aaa' , 'aab', 1],
75
+ ['aa' , 'aab', 1],
76
+ ['aaaa', 'aab', 2],
77
+ ['aaa' , 'aaa', 0],
78
+ ]
79
+
80
+ ts.each_with_index{|arr,i|
81
+ condition = levenshtein_distance(arr[0],arr[1]) == arr[2]
82
+ puts "Test #{i}: #{(condition ? 'success' : 'failure')}"
83
+ }
84
+ end
85
+
86
+ def self.top_matches(str, candidates, top=4)
87
+ candidates.sort_by{|a| levenshtein_distance(str, a)}[0...top]
88
+ end
89
+
90
+ def render_error(err)
91
+ $stderr.puts err.message
92
+ exit 1
93
+ end
94
+
95
+ def format_json(struct)
96
+ if (@config['defaults'] || {})['pretty_print'].to_s.downcase == 'true'
97
+ return Json.pretty_generate(struct, {'max_nesting' => 100})
98
+ else
99
+ return Json.generate(struct, {'max_nesting' => 100})
100
+ end
101
+ end
102
+
103
+ def init_commands(commands_filepath)
104
+ @commands ={}
105
+ return unless commands_filepath
106
+
107
+ unless File.exist?(commands_filepath)
108
+ raise MissingFileError.new("Commands File not found: #{commands_filepath}")
109
+ end
110
+
111
+ begin
112
+ commands = JSON.parse(File.open(commands_filepath,'r').read)
113
+ rescue JSON::ParserError => e
114
+ raise ArgumentError.new("#{commands_filepath} contents is not valid JSON:\n#{e.message}")
115
+ end
116
+
117
+ raise ArgumentError.new("#{commands_filepath} is not an array") unless commands.is_a?(Array)
118
+
119
+ commands.each{|c|
120
+ mb_long = c['long']
121
+ mb_short = c['short']
122
+ @commands[mb_long] = c if mb_long
123
+ @commands[mb_short] = c if mb_short
124
+ }
125
+ end
126
+
127
+ def parse_options
128
+ @optional = {}
129
+ command_index = nil
130
+ ARGV.each_with_index {|arg, i|
131
+ next_arg = ARGV[i + 1]
132
+
133
+ if arg[0] == '-'
134
+ if arg[1] == '-'
135
+ raise ParseError.new("Missing argument to: #{arg}") unless next_arg
136
+ @optional[arg[2..-1]] = processValue(next_arg)
137
+ else
138
+ @optional[tail(arg)] = true
139
+ end
140
+ else
141
+ if dangling?(command_index, i, arg)
142
+ if ARGV.find{|e| @commands.has_key?(e)}
143
+ raise ParseError.new("Dangling command line element: #{arg}")
144
+ else
145
+ raise MissingCommandError.new(arg)
146
+ end
147
+ end
148
+
149
+ next if @command
150
+
151
+ if is_command?(arg)
152
+ @command = arg
153
+ command_index = i
154
+ req_keys = (@commands[@command]['required'] || [])
155
+ req_vals = ARGV[i + 1, req_keys.length]
156
+
157
+ err_str1 = 'Missing required arguments'
158
+ raise ParseError.new(err_str1) unless req_keys.length == req_vals.length
159
+
160
+ err_str2 = 'Required arguments may not begin with "-"'
161
+ raise ParseError.new(err_str2) if req_vals.map{|v| v.chr}.include?('-')
162
+
163
+ @required = {}
164
+ #old versions of ruby do not have a to_h array method
165
+ req_keys.zip(req_vals).each{|(k,v)| @required[k] = processValue(v)}
166
+ end
167
+ end
168
+ }
169
+ end
170
+
171
+ def dangling?(command_index, current_index, arg)
172
+ num_req = (@commands[@command]['required'] || []).length if @command
173
+ is_required =
174
+ command_index &&
175
+ (command_index + 1 + num_req) > current_index &&
176
+ current_index > command_index
177
+
178
+ is_value = current_index > 0 && ARGV[current_index - 1].start_with?('--')
179
+ is_first_command = is_command?(arg) && !@command
180
+
181
+ !(is_value || is_required || is_first_command)
182
+ end
183
+
184
+ def usage(command)
185
+ c = @commands[command]
186
+ long = c['long']
187
+ short = c['short']
188
+ required = c['required'].map{|r| "<#{r}>"}.join(' ')
189
+
190
+ optional = c['optional'].map{|o| "[#{o}#{o.start_with?('--') ? ' foo' : ''}]"}.join(' ')
191
+
192
+ "#{long} (#{short}) #{required} #{optional}".gsub(/ +/,' ')
193
+ end
194
+
195
+ def is_command?(str)
196
+ (@commands || {}).has_key?(str)
197
+ end
198
+
199
+ def tail(arr)
200
+ arr[1..-1]
201
+ end
202
+
203
+ def init_config(config_filepath)
204
+ #TODO
205
+ @config = {}
206
+ end
207
+
208
+ def processValue(val)
209
+ if val.start_with? '@'
210
+ fn = tail(val)
211
+ raise ArgumentError.new("File not found: #{fn}") unless File.exist?(fn)
212
+ return File.open(fn,'r').read
213
+ end
214
+ val
215
+ end
216
+ end
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cli_utils
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Connor Williams
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-02-12 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: generate CLIs and CLI wrappers quickly
15
+ email:
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/cli_utils.rb
21
+ homepage: http://rubygems.org/gems/cli_utils
22
+ licenses:
23
+ - MIT
24
+ post_install_message:
25
+ rdoc_options: []
26
+ require_paths:
27
+ - lib
28
+ required_ruby_version: !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ none: false
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ requirements: []
41
+ rubyforge_project:
42
+ rubygems_version: 1.8.23
43
+ signing_key:
44
+ specification_version: 3
45
+ summary: cli utilities
46
+ test_files: []