cli_utils 0.0.0

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