unimatrix-cli 2.2.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.
- 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
|