unimatrix-cli 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/README.md +12 -0
- data/Rakefile +6 -0
- data/VERSION +1 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/unimatrix +11 -0
- data/lib/unimatrix_cli.rb +74 -0
- data/lib/unimatrix_cli/alchemist/rendition/list_command.rb +72 -0
- data/lib/unimatrix_cli/alchemist/rendition_profile/assign_command.rb +59 -0
- data/lib/unimatrix_cli/alchemist/rendition_profile/list_command.rb +36 -0
- data/lib/unimatrix_cli/alchemist/video/create_command.rb +40 -0
- data/lib/unimatrix_cli/alchemist/video/describe_command.rb +40 -0
- data/lib/unimatrix_cli/alchemist/video_encoder/create_command.rb +37 -0
- data/lib/unimatrix_cli/alchemist/video_encoder/describe_command.rb +39 -0
- data/lib/unimatrix_cli/alchemist/video_encoder/encode_command.rb +102 -0
- data/lib/unimatrix_cli/alchemist/video_encoder/list_command.rb +35 -0
- data/lib/unimatrix_cli/archivist/blueprint/create_command.rb +94 -0
- data/lib/unimatrix_cli/citadel/app/build_command.rb +28 -0
- data/lib/unimatrix_cli/citadel/app/console_command.rb +17 -0
- data/lib/unimatrix_cli/citadel/app/db_setup_command.rb +17 -0
- data/lib/unimatrix_cli/citadel/app/environment_command.rb +30 -0
- data/lib/unimatrix_cli/citadel/app/logs_command.rb +17 -0
- data/lib/unimatrix_cli/citadel/app/migrate_command.rb +17 -0
- data/lib/unimatrix_cli/citadel/app/migrate_status_command.rb +17 -0
- data/lib/unimatrix_cli/citadel/app/rake_command.rb +18 -0
- data/lib/unimatrix_cli/citadel/app/routes_command.rb +17 -0
- data/lib/unimatrix_cli/citadel/citadel_command.rb +285 -0
- data/lib/unimatrix_cli/citadel/instance/details_command.rb +18 -0
- data/lib/unimatrix_cli/citadel/instance/scp_command.rb +48 -0
- data/lib/unimatrix_cli/citadel/instance/ssh_command.rb +29 -0
- data/lib/unimatrix_cli/citadel/instance/status_command.rb +17 -0
- data/lib/unimatrix_cli/citadel/instance/user_data_command.rb +17 -0
- data/lib/unimatrix_cli/citadel/instance/user_data_logs_command.rb +17 -0
- data/lib/unimatrix_cli/cli.rb +30 -0
- data/lib/unimatrix_cli/command.rb +138 -0
- data/lib/unimatrix_cli/config/acceptance.yml +23 -0
- data/lib/unimatrix_cli/config/configuration.rb +66 -0
- data/lib/unimatrix_cli/config/production.yml +15 -0
- data/lib/unimatrix_cli/config/staging.yml +15 -0
- data/lib/unimatrix_cli/config/unimatrix.rb +4 -0
- data/lib/unimatrix_cli/iris/stream/create_command.rb +43 -0
- data/lib/unimatrix_cli/iris/stream/describe_command.rb +35 -0
- data/lib/unimatrix_cli/iris/stream_encoder/create_command.rb +43 -0
- data/lib/unimatrix_cli/iris/stream_encoder/describe_command.rb +37 -0
- data/lib/unimatrix_cli/iris/stream_input/create_command.rb +43 -0
- data/lib/unimatrix_cli/iris/stream_input/describe_command.rb +37 -0
- data/lib/unimatrix_cli/iris/stream_output/create_command.rb +55 -0
- data/lib/unimatrix_cli/iris/stream_output/describe_command.rb +37 -0
- data/lib/unimatrix_cli/iris/stream_recorder/create_command.rb +49 -0
- data/lib/unimatrix_cli/iris/stream_recorder/describe_command.rb +37 -0
- data/lib/unimatrix_cli/iris/stream_transcriber/create_command.rb +54 -0
- data/lib/unimatrix_cli/iris/stream_transcriber/describe_command.rb +37 -0
- data/lib/unimatrix_cli/iris/stream_transmutator/create_command.rb +65 -0
- data/lib/unimatrix_cli/iris/stream_transmutator/describe_command.rb +38 -0
- data/lib/unimatrix_cli/login_command.rb +91 -0
- data/lib/unimatrix_cli/logout_command.rb +21 -0
- data/lib/unimatrix_cli/version.rb +3 -0
- data/lib/unimatrix_cli/zephyrus/input/create_command.rb +40 -0
- data/lib/unimatrix_cli/zephyrus/input/describe_command.rb +35 -0
- data/lib/unimatrix_cli/zephyrus/input/list_command.rb +38 -0
- data/lib/unimatrix_cli/zephyrus/output/list_command.rb +44 -0
- data/lib/unimatrix_cli/zephyrus/recording/configuration_command.rb +43 -0
- data/lib/unimatrix_cli/zephyrus/rendition/list_command.rb +40 -0
- data/lib/unimatrix_cli/zephyrus/routing/configuration_command.rb +43 -0
- data/lib/unimatrix_cli/zephyrus/transcoding/configuration_command.rb +55 -0
- data/lib/unimatrix_cli/zephyrus/transcribing/configuration_command.rb +55 -0
- data/lib/unimatrix_cli/zephyrus/transmutation/configuration_command.rb +55 -0
- data/lib/unimatrix_parser.rb +796 -0
- data/unimatrix-cli.gemspec +30 -0
- metadata +247 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
module UnimatrixCLI
|
2
|
+
module Zephyrus
|
3
|
+
module Transcribing
|
4
|
+
class ConfigurationCommand < Command
|
5
|
+
|
6
|
+
option :key, "The key for an input", type: :string, required: true
|
7
|
+
|
8
|
+
synopsis "Display transcribing configuration for an input"
|
9
|
+
|
10
|
+
def execute
|
11
|
+
response = transcribing_configuration_request
|
12
|
+
if response[ 'outputs' ].nil?
|
13
|
+
write(
|
14
|
+
message: "Configuration could not be found: #{ response.inspect }",
|
15
|
+
error: true
|
16
|
+
)
|
17
|
+
else
|
18
|
+
response[ 'outputs' ].each do | output |
|
19
|
+
write( message: parse_configuration_output( output ) )
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
#----------------------------------------------------------------------------
|
25
|
+
|
26
|
+
private; def transcribing_configuration_request
|
27
|
+
endpoint = "#{ Configuration.default_config[ 'zephyrus_url' ] }/" +
|
28
|
+
"transcribing/configuration?key=#{ @options[ :key ] }"
|
29
|
+
|
30
|
+
# Unimatrix SDK cannot handle Zephyrus configuration responses
|
31
|
+
make_request( endpoint, 'Get' )
|
32
|
+
end
|
33
|
+
|
34
|
+
private; def parse_configuration_output( output )
|
35
|
+
output = output.map do | key, value |
|
36
|
+
if key == "rendition"
|
37
|
+
renditions = value.map { | rendition | parse_rendition( rendition ) }.join
|
38
|
+
"rendition:\n*\n#{ renditions }"
|
39
|
+
else
|
40
|
+
"#{ key }: #{ value }\n"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
output << "\n"
|
44
|
+
end
|
45
|
+
|
46
|
+
private; def parse_rendition( rendition )
|
47
|
+
rendition = rendition.map do | key, value |
|
48
|
+
"#{ key }: #{ value }\n"
|
49
|
+
end
|
50
|
+
rendition << "*\n"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module UnimatrixCLI
|
2
|
+
module Zephyrus
|
3
|
+
module Transmutation
|
4
|
+
class ConfigurationCommand < Command
|
5
|
+
|
6
|
+
option :key, "The key for an input", type: :string, required: true
|
7
|
+
|
8
|
+
synopsis "Display transmutation configuration for an input"
|
9
|
+
|
10
|
+
def execute
|
11
|
+
response = transmutation_configuration_request
|
12
|
+
if response[ 'outputs' ].nil?
|
13
|
+
write(
|
14
|
+
message: "Configuration could not be found: #{ response.inspect }",
|
15
|
+
error: true
|
16
|
+
)
|
17
|
+
else
|
18
|
+
response[ 'outputs' ].each do | output |
|
19
|
+
write( message: parse_configuration_output( output ) )
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
#----------------------------------------------------------------------------
|
25
|
+
|
26
|
+
private; def transmutation_configuration_request
|
27
|
+
endpoint = "#{ Configuration.default_config[ 'zephyrus_url' ] }/" +
|
28
|
+
"transmutation/onfiguration?key=#{ @options[ :key ] }"
|
29
|
+
|
30
|
+
# Unimatrix SDK cannot handle Zephyrus configuration responses
|
31
|
+
make_request( endpoint, 'Get' )
|
32
|
+
end
|
33
|
+
|
34
|
+
private; def parse_configuration_output( output )
|
35
|
+
output = output.map do | key, value |
|
36
|
+
if key == "rendition"
|
37
|
+
renditions = value.map { | rendition | parse_rendition( rendition ) }.join
|
38
|
+
"rendition:\n*\n#{ renditions }"
|
39
|
+
else
|
40
|
+
"#{ key }: #{ value }\n"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
output << "\n"
|
44
|
+
end
|
45
|
+
|
46
|
+
private; def parse_rendition( rendition )
|
47
|
+
rendition = rendition.map do | key, value |
|
48
|
+
"#{ key }: #{ value }\n"
|
49
|
+
end
|
50
|
+
rendition << "*\n"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,796 @@
|
|
1
|
+
# Modified from trollop commandline processing library 2.1.2
|
2
|
+
# https://github.com/ManageIQ/trollop
|
3
|
+
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
module UnimatrixParser
|
7
|
+
|
8
|
+
#----------------------------------------------------------------------------
|
9
|
+
# Class Methods
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def option( name, description = "", options = {} )
|
13
|
+
option = Option.new( name, description, options )
|
14
|
+
@options = [] if @options.nil?
|
15
|
+
@options << option
|
16
|
+
end
|
17
|
+
|
18
|
+
def synopsis( synopsis = nil )
|
19
|
+
@synopsis = synopsis
|
20
|
+
end
|
21
|
+
|
22
|
+
def stop_on_unknown
|
23
|
+
@stop_on_unknown = true
|
24
|
+
end
|
25
|
+
|
26
|
+
def educate_if_empty
|
27
|
+
if ARGV.empty?
|
28
|
+
parser = initialize_parser
|
29
|
+
parser.educate
|
30
|
+
exit
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def with_standard_exception_handling( parser )
|
35
|
+
yield
|
36
|
+
rescue CommandlineError => error
|
37
|
+
parser.die( error.message, nil, error.error_code )
|
38
|
+
rescue HelpNeeded
|
39
|
+
parser.educate
|
40
|
+
exit
|
41
|
+
end
|
42
|
+
|
43
|
+
def options
|
44
|
+
parser = initialize_parser
|
45
|
+
with_standard_exception_handling( parser ) do
|
46
|
+
parser.parse( ARGV )
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private; def initialize_parser
|
51
|
+
option :help, "Display this help message", type: :string
|
52
|
+
parser = Parser.new
|
53
|
+
parser.options( @options )
|
54
|
+
parser.synopsis( @synopsis )
|
55
|
+
parser.stop_on_unknown if @stop_on_unknown
|
56
|
+
parser
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.included( base )
|
61
|
+
base.extend( ClassMethods )
|
62
|
+
end
|
63
|
+
|
64
|
+
#----------------------------------------------------------------------------
|
65
|
+
# Error Classes
|
66
|
+
|
67
|
+
class CommandlineError < StandardError
|
68
|
+
attr_reader :error_code
|
69
|
+
|
70
|
+
def initialize( message, error_code = nil )
|
71
|
+
super( message )
|
72
|
+
@error_code = error_code
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class HelpNeeded < StandardError; end
|
77
|
+
|
78
|
+
#----------------------------------------------------------------------------
|
79
|
+
# Option Class
|
80
|
+
|
81
|
+
class Option
|
82
|
+
FLAG_TYPES = [ :flag, :bool, :boolean ]
|
83
|
+
SINGLE_ARG_TYPES = [ :int, :integer, :string, :double, :float, :io, :date ]
|
84
|
+
MULTI_ARG_TYPES = [ :ints, :integers, :strings, :doubles, :floats, :ios, :dates ]
|
85
|
+
TYPES = FLAG_TYPES + SINGLE_ARG_TYPES + MULTI_ARG_TYPES
|
86
|
+
INVALID_SHORT_ARG_REGEX = /[\d-]/
|
87
|
+
|
88
|
+
attr_accessor :name, :options
|
89
|
+
|
90
|
+
def initialize( name, description, options )
|
91
|
+
options[ :description ] = description
|
92
|
+
options[ :type ] = normalize_type( options[ :type ] )
|
93
|
+
|
94
|
+
type_from_default = disambiguate_and_set_type_from_default( options )
|
95
|
+
|
96
|
+
if options[ :type ] && type_from_default && options[ :type ] != type_from_default
|
97
|
+
raise ArgumentError, ":type specification and default type don't match " +
|
98
|
+
"(default type is #{ type_from_default })"
|
99
|
+
end
|
100
|
+
|
101
|
+
options[ :type ] = options[ :type ] || type_from_default || :flag
|
102
|
+
options[ :long ] = set_long( options[ :long ], name )
|
103
|
+
options[ :short ] = set_short( options[ :short ] )
|
104
|
+
|
105
|
+
if options[ :short ] && options[ :short ] =~ INVALID_SHORT_ARG_REGEX
|
106
|
+
raise ArgumentError, "a short option name can't be a number or a dash"
|
107
|
+
end
|
108
|
+
|
109
|
+
options[ :default ] = false if options[ :type ] == :flag && options[ :default ].nil?
|
110
|
+
|
111
|
+
if options[ :default ] && options[ :multi ] && !options[ :default ].kind_of?( Array )
|
112
|
+
options[ :default ] = [ options[ :default ] ]
|
113
|
+
end
|
114
|
+
|
115
|
+
options[ :multi ] ||= false
|
116
|
+
|
117
|
+
self.name = name
|
118
|
+
self.options = options
|
119
|
+
end
|
120
|
+
|
121
|
+
#----------------------------------------------------------------------------
|
122
|
+
# Option Methods
|
123
|
+
|
124
|
+
def key?( name )
|
125
|
+
options.key?( name )
|
126
|
+
end
|
127
|
+
|
128
|
+
def type
|
129
|
+
options[ :type ]
|
130
|
+
end
|
131
|
+
|
132
|
+
def flag?
|
133
|
+
type == :flag
|
134
|
+
end
|
135
|
+
|
136
|
+
def single_arg?
|
137
|
+
SINGLE_ARG_TYPES.include?( type )
|
138
|
+
end
|
139
|
+
|
140
|
+
def multi
|
141
|
+
options[ :multi ]
|
142
|
+
end
|
143
|
+
|
144
|
+
alias multi? multi
|
145
|
+
|
146
|
+
def multi_arg?
|
147
|
+
MULTI_ARG_TYPES.include?( type )
|
148
|
+
end
|
149
|
+
|
150
|
+
def default
|
151
|
+
options[ :default ]
|
152
|
+
end
|
153
|
+
|
154
|
+
def array_default?
|
155
|
+
options[ :default ].kind_of?( Array )
|
156
|
+
end
|
157
|
+
|
158
|
+
def short
|
159
|
+
options[ :short ]
|
160
|
+
end
|
161
|
+
|
162
|
+
def short?
|
163
|
+
short && short != :none
|
164
|
+
end
|
165
|
+
|
166
|
+
def short=( value )
|
167
|
+
options[ :short ] = value
|
168
|
+
end
|
169
|
+
|
170
|
+
def long
|
171
|
+
options[ :long ]
|
172
|
+
end
|
173
|
+
|
174
|
+
def description
|
175
|
+
options[ :description ]
|
176
|
+
end
|
177
|
+
|
178
|
+
def required?
|
179
|
+
options[ :required ]
|
180
|
+
end
|
181
|
+
|
182
|
+
#----------------------------------------------------------------------------
|
183
|
+
# Private Option Methods
|
184
|
+
|
185
|
+
private; def normalize_type( type )
|
186
|
+
case type
|
187
|
+
when :boolean, :bool then :flag
|
188
|
+
when :integer then :int
|
189
|
+
when :integers then :ints
|
190
|
+
when :double then :float
|
191
|
+
when :doubles then :floats
|
192
|
+
when Class
|
193
|
+
case type.name
|
194
|
+
when 'TrueClass',
|
195
|
+
'FalseClass' then :flag
|
196
|
+
when 'String' then :string
|
197
|
+
when 'Integer' then :int
|
198
|
+
when 'Float' then :float
|
199
|
+
when 'IO' then :io
|
200
|
+
when 'Date' then :date
|
201
|
+
else
|
202
|
+
raise ArgumentError, "unsupported argument type '#{ type.class.name }'"
|
203
|
+
end
|
204
|
+
when nil then nil
|
205
|
+
else
|
206
|
+
raise ArgumentError, "unsupported argument type " +
|
207
|
+
"'#{ type }'" unless TYPES.include?( type )
|
208
|
+
type
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
private; def disambiguate_and_set_type_from_default( options )
|
213
|
+
disambiguated_default = if options[ :multi ] &&
|
214
|
+
options[ :default ].kind_of?( Array ) &&
|
215
|
+
!options[ :type ]
|
216
|
+
options[ :default ].first
|
217
|
+
else
|
218
|
+
options[ :default ]
|
219
|
+
end
|
220
|
+
|
221
|
+
type_from_default =
|
222
|
+
case disambiguated_default
|
223
|
+
when Integer then :int
|
224
|
+
when Numeric then :float
|
225
|
+
when TrueClass,
|
226
|
+
FalseClass then :flag
|
227
|
+
when String then :string
|
228
|
+
when IO then :io
|
229
|
+
when Date then :date
|
230
|
+
when Array
|
231
|
+
if options[ :default ].empty?
|
232
|
+
if options[ :type ]
|
233
|
+
unless MULTI_ARG_TYPES.include?( options[ :type ] )
|
234
|
+
raise ArgumentError, "multiple argument type must be plural"
|
235
|
+
end
|
236
|
+
nil
|
237
|
+
else
|
238
|
+
raise ArgumentError, "multiple argument type cannot be deduced " +
|
239
|
+
"from an empty array for '#{ options[ :default ][ 0 ].class.name }'"
|
240
|
+
end
|
241
|
+
else
|
242
|
+
case options[:default][0]
|
243
|
+
when Integer then :ints
|
244
|
+
when Numeric then :floats
|
245
|
+
when String then :strings
|
246
|
+
when IO then :ios
|
247
|
+
when Date then :dates
|
248
|
+
else
|
249
|
+
raise ArgumentError, "unsupported multiple argument type " +
|
250
|
+
"'#{ options[ :default ][ 0 ].class.name }'"
|
251
|
+
end
|
252
|
+
end
|
253
|
+
when nil then nil
|
254
|
+
else
|
255
|
+
raise ArgumentError, "unsupported argument type " +
|
256
|
+
"'#{ options[ :default ].class.name }'"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
private; def set_long( long, name )
|
261
|
+
long = long ? long.to_s : name.to_s.gsub( "_", "-" )
|
262
|
+
case long
|
263
|
+
when /^--([^-].*)$/ then $1
|
264
|
+
when /^[^-]/ then long
|
265
|
+
else
|
266
|
+
raise ArgumentError, "invalid long option name #{ long.inspect }"
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
private; def set_short( short )
|
271
|
+
short = short.to_s if short && short != :none
|
272
|
+
case short
|
273
|
+
when /^-(.)$/ then $1
|
274
|
+
when nil, :none, /^.$/ then short
|
275
|
+
else
|
276
|
+
raise ArgumentError, "invalid short option name '#{ short.inspect }'"
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
#----------------------------------------------------------------------------
|
282
|
+
# Parser Class
|
283
|
+
|
284
|
+
class Parser
|
285
|
+
INVALID_SHORT_ARG_REGEX = /[\d-]/
|
286
|
+
FLOAT_REGEX = /^-?((\d+(\.\d+)?)|(\.\d+))([eE][-+]?[\d]+)?$/
|
287
|
+
PARAM_REGEX = /^-(-|\.$|[^\d\.])/
|
288
|
+
|
289
|
+
attr_reader :leftovers
|
290
|
+
attr_reader :specs
|
291
|
+
attr_accessor :ignore_invalid_options
|
292
|
+
|
293
|
+
def initialize( *a, &b )
|
294
|
+
@leftovers = []
|
295
|
+
@specs = {}
|
296
|
+
@long = {}
|
297
|
+
@short = {}
|
298
|
+
@order = []
|
299
|
+
@constraints = []
|
300
|
+
@stop_on_unknown = false
|
301
|
+
@synopsis = nil
|
302
|
+
|
303
|
+
cloaker( &b ).bind( self ).call( *a ) if b
|
304
|
+
end
|
305
|
+
|
306
|
+
def options( options = [] )
|
307
|
+
options.each do | option |
|
308
|
+
if @specs.member?( option.name )
|
309
|
+
# raise ArgumentError, "you already have an argument named '#{ option.name }'"
|
310
|
+
options.delete( option ) # ignore duplicate option
|
311
|
+
elsif @long[ option.long ]
|
312
|
+
raise ArgumentError, "long option name #{ option.long.inspect } " +
|
313
|
+
"is already taken; please specify a (different) :long"
|
314
|
+
elsif @short[ option.short ]
|
315
|
+
raise ArgumentError, "short option name #{ option.short.inspect } " +
|
316
|
+
"is already taken; please specify a (different) :short"
|
317
|
+
else
|
318
|
+
@long[ option.long ] = option.name
|
319
|
+
@short[ option.short ] = option.name if option.short?
|
320
|
+
@specs[ option.name ] = option
|
321
|
+
@order << [ :option, option.name ]
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
def synopsis( synopsis = nil )
|
327
|
+
synopsis ? @synopsis = synopsis : @synopsis
|
328
|
+
end
|
329
|
+
|
330
|
+
def stop_on_unknown
|
331
|
+
@stop_on_unknown = true
|
332
|
+
end
|
333
|
+
|
334
|
+
def depends( *keys )
|
335
|
+
keys.each do | key |
|
336
|
+
raise ArgumentError, "unknown option '#{ key }'" unless @specs[ key ]
|
337
|
+
end
|
338
|
+
@constraints << [ :depends, keys ]
|
339
|
+
end
|
340
|
+
|
341
|
+
def conflicts( *keys )
|
342
|
+
keys.each do | key |
|
343
|
+
raise ArgumentError, "unknown option '#{ key }'" unless @specs[ key ]
|
344
|
+
end
|
345
|
+
@constraints << [ :conflicts, keys ]
|
346
|
+
end
|
347
|
+
|
348
|
+
def parse( cmdline = ARGV )
|
349
|
+
values = {}
|
350
|
+
required = {}
|
351
|
+
|
352
|
+
@specs.each do | key, option |
|
353
|
+
required[ key ] = true if option.required?
|
354
|
+
values[ key ] = option.default
|
355
|
+
values[ key ] = [] if option.multi && !option.default
|
356
|
+
end
|
357
|
+
|
358
|
+
resolve_default_short_options!
|
359
|
+
|
360
|
+
given_args = resolve_symbols( cmdline )
|
361
|
+
|
362
|
+
raise HelpNeeded if given_args.include? :help
|
363
|
+
|
364
|
+
@constraints.each do | type, syms |
|
365
|
+
constraint_sym = syms.find { | sym | given_args[ sym ] }
|
366
|
+
next unless constraint_sym
|
367
|
+
|
368
|
+
case type
|
369
|
+
when :depends
|
370
|
+
syms.each do | sym |
|
371
|
+
unless given_args.include? sym
|
372
|
+
raise CommandlineError, "--#{ @specs[ constraint_sym ].long } " +
|
373
|
+
"requires --#{ @specs[ sym ].long }"
|
374
|
+
end
|
375
|
+
end
|
376
|
+
when :conflicts
|
377
|
+
syms.each do | sym |
|
378
|
+
if given_args.include?( sym ) && ( sym != constraint_sym )
|
379
|
+
raise CommandlineError, "--#{ @specs[ constraint_sym ].long } " +
|
380
|
+
"conflicts with --#{ @specs[ sym ].long }"
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
required.each do | key, val |
|
387
|
+
unless given_args.include? key
|
388
|
+
raise CommandlineError, "option --#{ @specs[ key ].long } must be specified"
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
parse_parameters( given_args, values )
|
393
|
+
|
394
|
+
cmdline.clear
|
395
|
+
@leftovers.each { | leftover | cmdline << leftover }
|
396
|
+
|
397
|
+
# allow openstruct-style accessors
|
398
|
+
class << values
|
399
|
+
def method_missing( m, *_args )
|
400
|
+
self[ m ] || self[ m.to_s ]
|
401
|
+
end
|
402
|
+
end
|
403
|
+
values
|
404
|
+
end
|
405
|
+
|
406
|
+
def parse_date_parameter( param, arg )
|
407
|
+
begin
|
408
|
+
require 'chronic'
|
409
|
+
time = Chronic.parse(param)
|
410
|
+
rescue LoadError
|
411
|
+
# chronic is not available
|
412
|
+
end
|
413
|
+
time ? Date.new( time.year, time.month, time.day ) : Date.parse( param )
|
414
|
+
rescue ArgumentError
|
415
|
+
raise CommandlineError, "option '#{ arg }' needs a date"
|
416
|
+
end
|
417
|
+
|
418
|
+
def educate( stream = $stdout )
|
419
|
+
width # hack: calculate it now; otherwise we have to be careful not to
|
420
|
+
# call this unless the cursor's at the beginning of a line.
|
421
|
+
left = {}
|
422
|
+
@specs.each do | name, spec |
|
423
|
+
left[ name ] =
|
424
|
+
( spec.short? ? "-#{ spec.short }, " : "" ) + "--#{ spec.long }" +
|
425
|
+
case spec.type
|
426
|
+
when :flag then ""
|
427
|
+
when :int then "=<i>"
|
428
|
+
when :ints then "=<i+>"
|
429
|
+
when :string then "=<s>"
|
430
|
+
when :strings then "=<s+>"
|
431
|
+
when :float then "=<f>"
|
432
|
+
when :floats then "=<f+>"
|
433
|
+
when :io then "=<filename/uri>"
|
434
|
+
when :ios then "=<filename/uri+>"
|
435
|
+
when :date then "=<date>"
|
436
|
+
when :dates then "=<date+>"
|
437
|
+
end +
|
438
|
+
( spec.flag? && spec.default ? ", --no-#{ spec.long }" : "" )
|
439
|
+
end
|
440
|
+
|
441
|
+
leftcol_width = left.values.map( &:length ).max || 0
|
442
|
+
rightcol_start = leftcol_width + 6
|
443
|
+
|
444
|
+
unless @order.size > 0 && @order.first.first == :text
|
445
|
+
command_name = File.basename( $0 ).gsub( /\.[^.]+$/, '' )
|
446
|
+
stream.puts "#{ @synopsis }\n" if @synopsis
|
447
|
+
stream.puts "Options:"
|
448
|
+
end
|
449
|
+
|
450
|
+
@order.each do | type, option |
|
451
|
+
if type == :text
|
452
|
+
stream.puts wrap( option )
|
453
|
+
next
|
454
|
+
end
|
455
|
+
|
456
|
+
spec = @specs[ option ]
|
457
|
+
stream.printf " %-#{ leftcol_width }s ", left[ option ]
|
458
|
+
description = spec.description + begin
|
459
|
+
default_s = case spec.default
|
460
|
+
when $stdout then "<stdout>"
|
461
|
+
when $stdin then "<stdin>"
|
462
|
+
when $stderr then "<stderr>"
|
463
|
+
when Array
|
464
|
+
spec.default.join( ", " )
|
465
|
+
else
|
466
|
+
spec.default.to_s
|
467
|
+
end
|
468
|
+
|
469
|
+
if spec.default
|
470
|
+
if spec.description =~ /\.$/
|
471
|
+
" (Default: #{ default_s })"
|
472
|
+
else
|
473
|
+
" (default: #{ default_s })"
|
474
|
+
end
|
475
|
+
else
|
476
|
+
""
|
477
|
+
end
|
478
|
+
end
|
479
|
+
stream.puts wrap(
|
480
|
+
description,
|
481
|
+
:width => width - rightcol_start - 1,
|
482
|
+
:prefix => rightcol_start
|
483
|
+
)
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
def width
|
488
|
+
@width ||= if $stdout.tty?
|
489
|
+
begin
|
490
|
+
require 'io/console'
|
491
|
+
w = IO.console.winsize.last
|
492
|
+
w.to_i > 0 ? w : 80
|
493
|
+
rescue LoadError, NoMethodError, Errno::ENOTTY, Errno::EBADF, Errno::EINVAL
|
494
|
+
legacy_width
|
495
|
+
end
|
496
|
+
else
|
497
|
+
80
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
def wrap( str, opts = {} )
|
502
|
+
if str == ""
|
503
|
+
[ "" ]
|
504
|
+
else
|
505
|
+
inner = false
|
506
|
+
str.split( "\n" ).map do | s |
|
507
|
+
line = wrap_line s, opts.merge( :inner => inner )
|
508
|
+
inner = true
|
509
|
+
line
|
510
|
+
end.flatten
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
def die( arg, message = nil, error_code = nil )
|
515
|
+
if message
|
516
|
+
$stderr.puts "Error: argument --#{ @specs[ arg ].long } #{ message }."
|
517
|
+
else
|
518
|
+
$stderr.puts "Error: #{ arg }."
|
519
|
+
end
|
520
|
+
$stderr.puts
|
521
|
+
educate $stderr
|
522
|
+
exit( error_code || -1 )
|
523
|
+
end
|
524
|
+
|
525
|
+
#----------------------------------------------------------------------------
|
526
|
+
# Private Parser Methods
|
527
|
+
|
528
|
+
private; def resolve_symbols( cmdline )
|
529
|
+
given_args = {}
|
530
|
+
@leftovers = each_arg cmdline do | arg, params |
|
531
|
+
# handle --no- forms
|
532
|
+
arg, negative_given = if arg =~ /^--no-([^-]\S*)$/
|
533
|
+
[ "--#{ $1 }", true ]
|
534
|
+
else
|
535
|
+
[ arg, false ]
|
536
|
+
end
|
537
|
+
|
538
|
+
key = case arg
|
539
|
+
when /^-([^-])$/ then @short[ $1 ]
|
540
|
+
when /^--([^-]\S*)$/ then @long[ $1 ] || @long[ "no-#{ $1 }" ]
|
541
|
+
else raise CommandlineError, "invalid argument syntax: '#{ arg }'"
|
542
|
+
end
|
543
|
+
|
544
|
+
key = nil if arg =~ /--no-/ # explicitly invalidate --no-no- arguments
|
545
|
+
|
546
|
+
next nil if ignore_invalid_options && !key
|
547
|
+
raise CommandlineError, "unknown argument '#{ arg }'" unless key
|
548
|
+
|
549
|
+
if given_args.include?( key ) && !@specs[ key ].multi?
|
550
|
+
raise CommandlineError, "option '#{ arg }' specified multiple times"
|
551
|
+
end
|
552
|
+
|
553
|
+
given_args[ key ] ||= {}
|
554
|
+
given_args[ key ][ :arg ] = arg
|
555
|
+
given_args[ key ][ :negative_given ] = negative_given
|
556
|
+
given_args[ key ][ :params ] ||= []
|
557
|
+
|
558
|
+
# the block returns the number of parameters taken
|
559
|
+
num_params_taken = 0
|
560
|
+
|
561
|
+
unless params.empty?
|
562
|
+
if @specs[ key ].single_arg?
|
563
|
+
given_args[ key ][ :params ] << params[ 0, 1 ] # take the first parameter
|
564
|
+
num_params_taken = 1
|
565
|
+
elsif @specs[ key ].multi_arg?
|
566
|
+
given_args[ key ][ :params ] << params # take all the parameters
|
567
|
+
num_params_taken = params.size
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
num_params_taken
|
572
|
+
end
|
573
|
+
given_args
|
574
|
+
end
|
575
|
+
|
576
|
+
private; def parse_parameters( given_args, values )
|
577
|
+
given_args.each do | key, given_data |
|
578
|
+
arg, params, negative_given = given_data.values_at :arg, :params, :negative_given
|
579
|
+
|
580
|
+
option = @specs[ key ]
|
581
|
+
if params.empty? && !option.flag?
|
582
|
+
unless option.default
|
583
|
+
raise CommandlineError, "option '#{ arg }' needs a parameter"
|
584
|
+
end
|
585
|
+
params << ( option.array_default? ? option.default.clone : [ option.default ] )
|
586
|
+
end
|
587
|
+
|
588
|
+
# mark argument as specified on the commandline
|
589
|
+
values[ "#{ key }_given".intern ] = true
|
590
|
+
|
591
|
+
case option.type
|
592
|
+
when :flag
|
593
|
+
values[ key ] = ( key.to_s =~ /^no_/ ? negative_given : !negative_given )
|
594
|
+
when :int, :ints
|
595
|
+
values[ key ] = params.map do | param |
|
596
|
+
param.map { | p | parse_integer_parameter p, arg }
|
597
|
+
end
|
598
|
+
when :float, :floats
|
599
|
+
values[ key ] = params.map do | param |
|
600
|
+
param.map { | p | parse_float_parameter p, arg }
|
601
|
+
end
|
602
|
+
when :string, :strings
|
603
|
+
values[ key ] = params.map do | param |
|
604
|
+
param.map( &:to_s )
|
605
|
+
end
|
606
|
+
when :io, :ios
|
607
|
+
values[ key ] = params.map do | param |
|
608
|
+
param.map { | p | parse_io_parameter p, arg }
|
609
|
+
end
|
610
|
+
when :date, :dates
|
611
|
+
values[ key ] = params.map do | param |
|
612
|
+
param.map { | p | parse_date_parameter p, arg }
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
if option.single_arg?
|
617
|
+
if option.multi? # multiple options, each with a single parameter
|
618
|
+
values[ key ] = values[ key ].map { | p | p[ 0 ] }
|
619
|
+
else # single parameter
|
620
|
+
values[ key ] = values[ key ][ 0 ][ 0 ]
|
621
|
+
end
|
622
|
+
elsif option.multi_arg? && !option.multi?
|
623
|
+
values[ key ] = values[ key ][ 0 ] # single option, with multiple parameters
|
624
|
+
end
|
625
|
+
# else: multiple options, with multiple parameters
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
private; def legacy_width
|
630
|
+
# Support for older Rubies where io/console is not available
|
631
|
+
`tput cols`.to_i
|
632
|
+
rescue Errno::ENOENT
|
633
|
+
80
|
634
|
+
end
|
635
|
+
|
636
|
+
private; def each_arg( args )
|
637
|
+
remains = []
|
638
|
+
i = 0
|
639
|
+
|
640
|
+
until i >= args.length
|
641
|
+
# return remains += args[ i..-1 ] if @stop_words.member? args[ i ]
|
642
|
+
case args[ i ]
|
643
|
+
when /^--$/ # arg terminator
|
644
|
+
return remains += args[ ( i + 1 )..-1 ]
|
645
|
+
when /^--(\S+?)=(.*)$/ # long argument with equals
|
646
|
+
num_params_taken = yield "--#{ $1 }", [ $2 ]
|
647
|
+
if num_params_taken.nil?
|
648
|
+
remains << args[ i ]
|
649
|
+
if @stop_on_unknown
|
650
|
+
return remains += args[ i + 1..-1 ]
|
651
|
+
end
|
652
|
+
end
|
653
|
+
i += 1
|
654
|
+
when /^--(\S+)$/ # long argument
|
655
|
+
params = collect_argument_parameters( args, i + 1 )
|
656
|
+
num_params_taken = yield args[ i ], params
|
657
|
+
|
658
|
+
if num_params_taken.nil?
|
659
|
+
remains << args[ i ]
|
660
|
+
if @stop_on_unknown
|
661
|
+
return remains += args[ i + 1..-1 ]
|
662
|
+
end
|
663
|
+
else
|
664
|
+
i += num_params_taken
|
665
|
+
end
|
666
|
+
i += 1
|
667
|
+
when /^-(\S+)$/ # one or more short arguments
|
668
|
+
short_remaining = ""
|
669
|
+
shortargs = $1.split( // )
|
670
|
+
shortargs.each_with_index do | a, j |
|
671
|
+
if j == ( shortargs.length - 1 )
|
672
|
+
params = collect_argument_parameters( args, i + 1 )
|
673
|
+
|
674
|
+
num_params_taken = yield "-#{ a }", params
|
675
|
+
unless num_params_taken
|
676
|
+
short_remaining << a
|
677
|
+
if @stop_on_unknown
|
678
|
+
remains << "-#{ short_remaining }"
|
679
|
+
return remains += args[ i + 1..-1 ]
|
680
|
+
end
|
681
|
+
else
|
682
|
+
i += num_params_taken
|
683
|
+
end
|
684
|
+
else
|
685
|
+
unless yield "-#{ a }", []
|
686
|
+
short_remaining << a
|
687
|
+
if @stop_on_unknown
|
688
|
+
short_remaining += shortargs[ j + 1..-1 ].join
|
689
|
+
remains << "-#{ short_remaining }"
|
690
|
+
return remains += args[ i + 1..-1 ]
|
691
|
+
end
|
692
|
+
end
|
693
|
+
end
|
694
|
+
end
|
695
|
+
|
696
|
+
unless short_remaining.empty?
|
697
|
+
remains << "-#{ short_remaining }"
|
698
|
+
end
|
699
|
+
i += 1
|
700
|
+
else
|
701
|
+
if @stop_on_unknown
|
702
|
+
return remains += args[ i..-1 ]
|
703
|
+
else
|
704
|
+
remains << args[ i ]
|
705
|
+
i += 1
|
706
|
+
end
|
707
|
+
end
|
708
|
+
end
|
709
|
+
|
710
|
+
remains
|
711
|
+
end
|
712
|
+
|
713
|
+
private; def parse_integer_parameter( param, arg )
|
714
|
+
unless param =~ /^-?[\d_]+$/
|
715
|
+
raise CommandlineError, "option '#{ arg }' needs an integer"
|
716
|
+
end
|
717
|
+
param.to_i
|
718
|
+
end
|
719
|
+
|
720
|
+
private; def parse_float_parameter( param, arg )
|
721
|
+
unless param =~ FLOAT_REGEX
|
722
|
+
raise CommandlineError, "option '#{ arg }' needs a floating-point number"
|
723
|
+
end
|
724
|
+
param.to_f
|
725
|
+
end
|
726
|
+
|
727
|
+
private; def parse_io_parameter( param, arg )
|
728
|
+
if param =~ /^(stdin|-)$/i
|
729
|
+
$stdin
|
730
|
+
else
|
731
|
+
require 'open-uri'
|
732
|
+
begin
|
733
|
+
open param
|
734
|
+
rescue SystemCallError => e
|
735
|
+
raise CommandlineError, "file or url for option '#{ arg }' " +
|
736
|
+
"cannot be opened: #{ e.message }"
|
737
|
+
end
|
738
|
+
end
|
739
|
+
end
|
740
|
+
|
741
|
+
private; def collect_argument_parameters( args, start_at )
|
742
|
+
params = []
|
743
|
+
pos = start_at
|
744
|
+
while args[ pos ] && args[ pos ] !~ PARAM_REGEX do
|
745
|
+
params << args[ pos ]
|
746
|
+
pos += 1
|
747
|
+
end
|
748
|
+
params
|
749
|
+
end
|
750
|
+
|
751
|
+
private; def resolve_default_short_options!
|
752
|
+
@order.each do | type, name |
|
753
|
+
option = @specs[ name ]
|
754
|
+
next if type != :option || option.short
|
755
|
+
|
756
|
+
c = option.long.split( // ).find do | d |
|
757
|
+
d !~ INVALID_SHORT_ARG_REGEX && !@short.member?( d )
|
758
|
+
end
|
759
|
+
|
760
|
+
if c
|
761
|
+
option.short = c
|
762
|
+
@short[ c ] = name
|
763
|
+
end
|
764
|
+
end
|
765
|
+
end
|
766
|
+
|
767
|
+
private; def wrap_line( str, opts = {} )
|
768
|
+
prefix = opts[ :prefix ] || 0
|
769
|
+
width = opts[ :width ] || ( self.width - 1 )
|
770
|
+
start = 0
|
771
|
+
ret = []
|
772
|
+
until start > str.length
|
773
|
+
nextt =
|
774
|
+
if start + width >= str.length
|
775
|
+
str.length
|
776
|
+
else
|
777
|
+
x = str.rindex( /\s/, start + width )
|
778
|
+
x = str.index( /\s/, start ) if x && x < start
|
779
|
+
x || str.length
|
780
|
+
end
|
781
|
+
ret << ( ( ret.empty? && !opts[ :inner ] ) ? "" : " " * prefix ) + str[ start...nextt ]
|
782
|
+
start = nextt + 1
|
783
|
+
end
|
784
|
+
ret
|
785
|
+
end
|
786
|
+
|
787
|
+
private; def cloaker( &b )
|
788
|
+
( class << self; self; end ).class_eval do
|
789
|
+
define_method :cloaker_, &b
|
790
|
+
meth = instance_method :cloaker_
|
791
|
+
remove_method :cloaker_
|
792
|
+
meth
|
793
|
+
end
|
794
|
+
end
|
795
|
+
end
|
796
|
+
end
|