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.
- data/lib/cli_utils.rb +216 -0
- 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: []
|