mm_tool 0.1.1
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/.gitattributes +4 -0
- data/.gitignore +24 -0
- data/Gemfile +3 -0
- data/LICENSE.md +22 -0
- data/README.md +15 -0
- data/Rakefile +2 -0
- data/Truth Tables.xlsx +0 -0
- data/bin/console +14 -0
- data/bin/mm_tool +19 -0
- data/bin/setup +8 -0
- data/lib/mm_tool.rb +21 -0
- data/lib/mm_tool/application_main.rb +172 -0
- data/lib/mm_tool/mm_movie.rb +217 -0
- data/lib/mm_tool/mm_movie_ignore_list.rb +68 -0
- data/lib/mm_tool/mm_movie_stream.rb +492 -0
- data/lib/mm_tool/mm_tool_cli.rb +322 -0
- data/lib/mm_tool/mm_user_defaults.rb +290 -0
- data/lib/mm_tool/output_helper.rb +121 -0
- data/lib/mm_tool/user_defaults.rb +377 -0
- data/lib/mm_tool/version.rb +3 -0
- data/mm_tool.gemspec +50 -0
- metadata +155 -0
@@ -0,0 +1,322 @@
|
|
1
|
+
module MmTool
|
2
|
+
|
3
|
+
#=============================================================================
|
4
|
+
# Implement the command line interface for MmTool::ApplicationMain
|
5
|
+
#=============================================================================
|
6
|
+
class MmToolCli
|
7
|
+
|
8
|
+
require 'tty-command'
|
9
|
+
require 'tty-which'
|
10
|
+
|
11
|
+
#------------------------------------------------------------
|
12
|
+
# Initialize
|
13
|
+
#------------------------------------------------------------
|
14
|
+
def initialize(app_instance = MmTool::ApplicationMain.shared_application)
|
15
|
+
@application = app_instance
|
16
|
+
@defaults = MmUserDefaults.shared_user_defaults
|
17
|
+
@defaults.register_defaults(with_hash: USER_DEFAULTS)
|
18
|
+
end
|
19
|
+
|
20
|
+
#------------------------------------------------------------
|
21
|
+
# Print Help
|
22
|
+
#------------------------------------------------------------
|
23
|
+
#noinspection RubyResolve
|
24
|
+
def print_help
|
25
|
+
# Amount of hanging indent is dependent on the space arguments take up.
|
26
|
+
hang = @defaults.example_args_by_length[-1].length + 4
|
27
|
+
|
28
|
+
header = <<~HEREDOC
|
29
|
+
#{C.bold('Usage:')} #{File.basename($0)} [options...] <directory|file> [options...] <directory|file> ...
|
30
|
+
|
31
|
+
Performs media analysis and reports on files that don't meet quality requirements and/or files that
|
32
|
+
have containers and/or streams of undesired types. Files with undesired containers and/or streams
|
33
|
+
can optionally be transcoded into a new file.
|
34
|
+
|
35
|
+
You must specify a file or a directory. Specifying a directory implies a recursive search for files
|
36
|
+
matching the #{C.bold('--container-extension')} option.
|
37
|
+
|
38
|
+
Options and files are processed as they occur, and remain in effect for subsequent input files until
|
39
|
+
encountered again with a different value. Boolean flags shown below modify default behavior. They
|
40
|
+
can be undone for subsequent input files with the capitalized version of the short flag, or by adding
|
41
|
+
or deleting #{C.bold('no-')} for the verbose argument form.
|
42
|
+
|
43
|
+
You can also set default options in #{PATH_USER_DEFAULTS}.
|
44
|
+
HEREDOC
|
45
|
+
|
46
|
+
puts OutputHelper.hanging_string(string: header, hang: 3, margin: OutputHelper.console_width)
|
47
|
+
|
48
|
+
@defaults.arguments_help_by_group.each_pair do |group, arguments|
|
49
|
+
puts group
|
50
|
+
arguments.each do |argument|
|
51
|
+
puts OutputHelper.hanging_string(string: argument, hang: hang, margin: OutputHelper.console_width) + "\n"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
#------------------------------------------------------------
|
57
|
+
# Validate pre-requisites.
|
58
|
+
#------------------------------------------------------------
|
59
|
+
#noinspection RubyResolve
|
60
|
+
def validate_prerequisites
|
61
|
+
commands = %w(ffmpeg)
|
62
|
+
codecs = %w(libx264 libx265 libfdk_aac)
|
63
|
+
task = TTY::Command.new(printer: :null)
|
64
|
+
success = true
|
65
|
+
|
66
|
+
# We'll check everything in items before failing, so that we can provide
|
67
|
+
# a comprehensive list to the user. No one wants to see what's missing
|
68
|
+
# on-by-one.
|
69
|
+
commands.each do |command|
|
70
|
+
unless TTY::Which.exist?(command)
|
71
|
+
OutputHelper.print_error("Error: #{C.bold(command)} is not installed (or not in your #{C.bold('$PATH')}).")
|
72
|
+
success = false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
exit 1 unless success
|
76
|
+
|
77
|
+
# Now we'll make sure that all of the codecs that we're interested in
|
78
|
+
# are installed as part of ffmpeg. This is necessary because not every
|
79
|
+
# binary distribution supports non-free.
|
80
|
+
|
81
|
+
# Again, we'll check them all before failing in order to list everything.
|
82
|
+
codecs.each do |codec|
|
83
|
+
result = task.run!("ffprobe -v quiet -codecs | grep #{codec}")
|
84
|
+
OutputHelper.print_error("Error: ffmpeg was built without support for the #{C.bold(codec)} codec, which is required.") if result.failure?
|
85
|
+
success = success && result.success?
|
86
|
+
end
|
87
|
+
exit 1 unless success
|
88
|
+
end
|
89
|
+
|
90
|
+
#------------------------------------------------------------
|
91
|
+
# Run the CLI.
|
92
|
+
#------------------------------------------------------------
|
93
|
+
#noinspection RubyResolve
|
94
|
+
def run(args)
|
95
|
+
|
96
|
+
path = nil
|
97
|
+
while args.count > 0 do
|
98
|
+
|
99
|
+
# Convert single hyphen arguments to one or more multi-hyphen
|
100
|
+
# arguments. Doing this as an extra step eliminates redundancy,
|
101
|
+
# but also allows -abc in place of -a -b -c.
|
102
|
+
if args[0] =~ /^-[A-Za-z]+$/
|
103
|
+
|
104
|
+
args[0][1..-1].reverse.each_char do |char|
|
105
|
+
|
106
|
+
case char
|
107
|
+
|
108
|
+
when 'h'
|
109
|
+
args.insert(1, "--help")
|
110
|
+
when 'i'
|
111
|
+
args.insert(1, "--no-info-header")
|
112
|
+
when 'I'
|
113
|
+
args.insert(1, '--info-header')
|
114
|
+
when 't'
|
115
|
+
args.insert(1, "--transcode")
|
116
|
+
when 'T'
|
117
|
+
args.insert(1, "--no-transcode")
|
118
|
+
when 'u'
|
119
|
+
args.insert(1, "--no-fix-undefined-language")
|
120
|
+
when 'U'
|
121
|
+
args.insert(1, "--fix-undefined-language")
|
122
|
+
when 'p'
|
123
|
+
args.insert(1, "--dump")
|
124
|
+
when 'P'
|
125
|
+
args.insert(1, "--no-dump")
|
126
|
+
else
|
127
|
+
OutputHelper.print_error_and_exit("Error: option #{C.bold(args[0])} was specified, but I don't know what that means.")
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
args.shift
|
133
|
+
next
|
134
|
+
end
|
135
|
+
|
136
|
+
# The main loop processes options, commands, and files in first-in,
|
137
|
+
# first out order, which is the normal Unix way compared to how
|
138
|
+
# Ruby scripts try to handle things.
|
139
|
+
case args[0]
|
140
|
+
|
141
|
+
#-----------------------
|
142
|
+
# Main Options
|
143
|
+
#-----------------------
|
144
|
+
|
145
|
+
when '--help'
|
146
|
+
self.print_help
|
147
|
+
exit 0
|
148
|
+
|
149
|
+
when '--containers'
|
150
|
+
@defaults[:container_files] = validate_arg_value(args[0], args[1]).split(',')
|
151
|
+
args.shift
|
152
|
+
|
153
|
+
when '--scan'
|
154
|
+
value = validate_arg_value(args[0], args[1]).downcase
|
155
|
+
unless %w(normal all flagged quality force).include?(value)
|
156
|
+
OutputHelper.print_error("Note: value #{C.bold(value)} doesn't make sense; assuming #{C.bold('normal')}.")
|
157
|
+
value = 'normal'
|
158
|
+
end
|
159
|
+
@defaults[:scan_type] = value
|
160
|
+
args.shift
|
161
|
+
|
162
|
+
when '--ignore-titles'
|
163
|
+
@defaults[:ignore_titles] = true
|
164
|
+
|
165
|
+
when '--no-ignore-titles'
|
166
|
+
@defaults[:ignore_titles] = false
|
167
|
+
|
168
|
+
when '--no-info-header'
|
169
|
+
@defaults[:info_header] = false
|
170
|
+
|
171
|
+
when '--info-header'
|
172
|
+
@defaults[:info_header] = true
|
173
|
+
|
174
|
+
when '--version'
|
175
|
+
puts "#{File.basename $0}, version #{MmTool::VERSION}"
|
176
|
+
|
177
|
+
when '--'
|
178
|
+
break
|
179
|
+
|
180
|
+
#-----------------------
|
181
|
+
# Command-like Options
|
182
|
+
#-----------------------
|
183
|
+
|
184
|
+
when '--transcode'
|
185
|
+
@defaults[:ignore_files] = false
|
186
|
+
@defaults[:unignore_files] = false
|
187
|
+
@defaults[:transcode] = true
|
188
|
+
|
189
|
+
when '--no-transcode'
|
190
|
+
@defaults[:transcode] = false
|
191
|
+
@defaults.tempfile = nil
|
192
|
+
|
193
|
+
when '--ignore-files'
|
194
|
+
@defaults[:ignore_files] = true
|
195
|
+
|
196
|
+
when '--no-ignore-files'
|
197
|
+
@defaults[:ignore_files] = false
|
198
|
+
|
199
|
+
when '--unignore-files'
|
200
|
+
@defaults[:unignore_files] = true
|
201
|
+
|
202
|
+
when '--no-unignore-files'
|
203
|
+
@defaults[:unignore_files] = false
|
204
|
+
|
205
|
+
|
206
|
+
#-----------------------
|
207
|
+
# Media
|
208
|
+
#-----------------------
|
209
|
+
|
210
|
+
when '--containers-preferred'
|
211
|
+
@defaults[:containers_preferred] = validate_arg_value(args[0], args[1]).split(',')
|
212
|
+
args.shift
|
213
|
+
|
214
|
+
when '--codecs-audio-preferred'
|
215
|
+
@defaults[:codec_audio_preferred] = validate_arg_value(args[0], args[1]).split(',')
|
216
|
+
args.shift
|
217
|
+
|
218
|
+
when '--codecs-video-preferred'
|
219
|
+
@defaults[:codec_video_preferred] = validate_arg_value(args[0], args[1]).split(',')
|
220
|
+
args.shift
|
221
|
+
|
222
|
+
when '--codecs-subs-preferred'
|
223
|
+
@defaults[:codec_subs_preferred] = validate_arg_value(args[0], args[1]).split(',')
|
224
|
+
args.shift
|
225
|
+
|
226
|
+
when '--keep-langs-audio'
|
227
|
+
@defaults[:keep_langs_audio] = validate_arg_value(args[0], args[1]).split(',')
|
228
|
+
args.shift
|
229
|
+
|
230
|
+
when '--keep_langs_video'
|
231
|
+
@defaults[:keep_langs_video] = validate_arg_value(args[0], args[1]).split(',')
|
232
|
+
args.shift
|
233
|
+
|
234
|
+
when '--keep-langs-subs'
|
235
|
+
@defaults[:keep_langs_subs] = validate_arg_value(args[0], args[1]).split(',')
|
236
|
+
args.shift
|
237
|
+
|
238
|
+
when '--no-use-external-subs'
|
239
|
+
@defaults[:use_external_subs] = false
|
240
|
+
|
241
|
+
when '--use-external-subs'
|
242
|
+
@defaults[:use_external_subs] = true
|
243
|
+
|
244
|
+
#-----------------------
|
245
|
+
# Transcoding
|
246
|
+
#-----------------------
|
247
|
+
|
248
|
+
when '--no-drop-subs'
|
249
|
+
@defaults[:drop_subs] = false
|
250
|
+
|
251
|
+
when '--suffix'
|
252
|
+
@defaults[:suffix] = validate_arg_value(args[0], args[1])
|
253
|
+
|
254
|
+
when '--undefined-language'
|
255
|
+
@defaults[:suffix] = validate_arg_value(args[0], args[1])
|
256
|
+
|
257
|
+
when '--no-fix-undefined-language'
|
258
|
+
@defaults[:fix_undefined_language] = false
|
259
|
+
|
260
|
+
when '--fix-undefined-language'
|
261
|
+
@defaults[:fix_undefined_language] = true
|
262
|
+
|
263
|
+
#-----------------------
|
264
|
+
# Quality
|
265
|
+
#-----------------------
|
266
|
+
|
267
|
+
when '--min-width'
|
268
|
+
@defaults[:min_width] = validate_arg_value(args[0], args[1])
|
269
|
+
|
270
|
+
when '--min-channels'
|
271
|
+
@defaults[:min_channels] = validate_arg_value(args[0], args[1])
|
272
|
+
|
273
|
+
#-----------------------
|
274
|
+
# Other
|
275
|
+
#-----------------------
|
276
|
+
|
277
|
+
else
|
278
|
+
|
279
|
+
# An unknown parameter was encountered, so let's stop everything.
|
280
|
+
if args[0] =~ /^--.*$/
|
281
|
+
OutputHelper.print_error_and_exit("Error: option #{C.bold(args[0])} was specified, but I don't know what that means.")
|
282
|
+
end
|
283
|
+
|
284
|
+
# Otherwise, check for existence of the path, and warn or proceed.
|
285
|
+
path = File.expand_path(args[0])
|
286
|
+
if File.exist?(path)
|
287
|
+
@application.run(path)
|
288
|
+
else
|
289
|
+
OutputHelper.print_error("Note: skipping #{C.bold(path)}, which seems not to exist.")
|
290
|
+
end
|
291
|
+
|
292
|
+
end # case
|
293
|
+
|
294
|
+
args.shift
|
295
|
+
|
296
|
+
end # while
|
297
|
+
|
298
|
+
# If path has never been set, then the user didn't specify anything to check,
|
299
|
+
# which is likely to be a mistake.
|
300
|
+
unless path
|
301
|
+
OutputHelper.print_error_and_exit("You did not specify any input file(s) or directory(s). Use #{C.bold(File.basename($0))} for help.")
|
302
|
+
end
|
303
|
+
|
304
|
+
end
|
305
|
+
|
306
|
+
#------------------------------------------------------------
|
307
|
+
# Perform a really simple validation of the given value for
|
308
|
+
# the given argument, returning the value if successful.
|
309
|
+
# ------------------------------------------------------------
|
310
|
+
#noinspection RubyResolve
|
311
|
+
def validate_arg_value(arg, value)
|
312
|
+
if !value
|
313
|
+
OutputHelper.print_error_and_exit("Error: option #{C.bold(arg)} was specified, but no value was given.")
|
314
|
+
elsif value =~ /^-.*$/
|
315
|
+
OutputHelper.print_error_and_exit("Error: option #{C.bold(arg)} was specified, but the value #{C.bold(value)} looks like another option argument.")
|
316
|
+
end
|
317
|
+
value
|
318
|
+
end
|
319
|
+
|
320
|
+
end # class
|
321
|
+
|
322
|
+
end # module
|
@@ -0,0 +1,290 @@
|
|
1
|
+
module MmTool
|
2
|
+
|
3
|
+
#=============================================================================
|
4
|
+
# A single user default.
|
5
|
+
#=============================================================================
|
6
|
+
class MmUserDefault
|
7
|
+
|
8
|
+
#------------------------------------------------------------
|
9
|
+
# Attributes
|
10
|
+
#------------------------------------------------------------
|
11
|
+
attr_reader :name
|
12
|
+
attr_reader :default
|
13
|
+
attr_reader :arg_short
|
14
|
+
attr_reader :arg_long
|
15
|
+
attr_reader :arg_format
|
16
|
+
attr_reader :item_label
|
17
|
+
attr_reader :help_group
|
18
|
+
attr_reader :help_desc
|
19
|
+
|
20
|
+
#------------------------------------------------------------
|
21
|
+
# Initialize
|
22
|
+
#------------------------------------------------------------
|
23
|
+
def initialize(key:, value:)
|
24
|
+
@name = key
|
25
|
+
@value = value[:value]
|
26
|
+
@default = value[:default]
|
27
|
+
@arg_short = value[:arg_short]
|
28
|
+
@arg_long = value[:arg_long]
|
29
|
+
@arg_format = value[:arg_format]
|
30
|
+
@item_label = value[:item_label]
|
31
|
+
@help_group = value[:help_group]
|
32
|
+
@help_desc = value[:help_desc]
|
33
|
+
@value_set = false
|
34
|
+
end
|
35
|
+
|
36
|
+
#------------------------------------------------------------
|
37
|
+
# Value attribute accessors.
|
38
|
+
#------------------------------------------------------------
|
39
|
+
def value
|
40
|
+
if value_set?
|
41
|
+
@value
|
42
|
+
else
|
43
|
+
@default
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def value=(value)
|
48
|
+
@value = value
|
49
|
+
@value_set = true
|
50
|
+
end
|
51
|
+
|
52
|
+
def value_printable
|
53
|
+
if self.value.instance_of?(Array)
|
54
|
+
self.value.join(',')
|
55
|
+
elsif [TrueClass, FalseClass].include?(self.value.class)
|
56
|
+
self.value.human
|
57
|
+
else
|
58
|
+
self.value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
#------------------------------------------------------------
|
63
|
+
# Indicates whether or not a value has been set.
|
64
|
+
#------------------------------------------------------------
|
65
|
+
def value_set?
|
66
|
+
@value_set
|
67
|
+
end
|
68
|
+
|
69
|
+
#------------------------------------------------------------
|
70
|
+
# Utility: get an argument for output, aligning the short
|
71
|
+
# and long forms.
|
72
|
+
#------------------------------------------------------------
|
73
|
+
def example_arg
|
74
|
+
s = self.arg_short ? "#{self.arg_short}," : nil
|
75
|
+
l = self.arg_format ? "#{self.arg_long} #{self.arg_format}" : "#{self.arg_long}"
|
76
|
+
# " %-3s %s " % [s, l]
|
77
|
+
"%-3s %s" % [s, l]
|
78
|
+
end
|
79
|
+
|
80
|
+
end # class
|
81
|
+
|
82
|
+
|
83
|
+
#=============================================================================
|
84
|
+
# Handles application user defaults as a singleton. This is specific to
|
85
|
+
# MmTool, and is not meant to be a general purpose User Defaults system.
|
86
|
+
#=============================================================================
|
87
|
+
class MmUserDefaults
|
88
|
+
|
89
|
+
require 'tty-table'
|
90
|
+
|
91
|
+
#------------------------------------------------------------
|
92
|
+
# Singleton accessor.
|
93
|
+
#------------------------------------------------------------
|
94
|
+
def self.shared_user_defaults
|
95
|
+
unless @self
|
96
|
+
@self = self.new
|
97
|
+
end
|
98
|
+
@self
|
99
|
+
end
|
100
|
+
|
101
|
+
#------------------------------------------------------------
|
102
|
+
# Initialize
|
103
|
+
#------------------------------------------------------------
|
104
|
+
def initialize
|
105
|
+
@defaults = {}
|
106
|
+
end
|
107
|
+
|
108
|
+
#------------------------------------------------------------
|
109
|
+
# The location on the filesystem where the file exists.
|
110
|
+
#------------------------------------------------------------
|
111
|
+
def file_path
|
112
|
+
PATH_USER_DEFAULTS
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
#------------------------------------------------------------
|
117
|
+
# Creates a new user default object and add it to the
|
118
|
+
# collection.
|
119
|
+
#------------------------------------------------------------
|
120
|
+
def define_default(for_key:, value:)
|
121
|
+
@defaults[for_key] = MmUserDefault.new(key:for_key,value:value)
|
122
|
+
end
|
123
|
+
|
124
|
+
#------------------------------------------------------------
|
125
|
+
# Sets up initial defaults from a large hash indicating all
|
126
|
+
# of the user defaults and attributes for the default.
|
127
|
+
# Also sets up the persistence system. If the file doesn't
|
128
|
+
# exist, it will be created with the current key-value
|
129
|
+
# pairs. If it does exist, existing key-value pairs will
|
130
|
+
# be reconciled with current key-value pairs. Finally,
|
131
|
+
# values from the file will be applied.
|
132
|
+
#------------------------------------------------------------
|
133
|
+
def register_defaults(with_hash:)
|
134
|
+
|
135
|
+
# *Define* each of the possible defaults. These will have
|
136
|
+
# default values. We're actually kind of good to go at
|
137
|
+
# this point if we don't want to use the rc file.
|
138
|
+
|
139
|
+
with_hash.each {|k,v| define_default(for_key:k, value:v) }
|
140
|
+
|
141
|
+
# If the file doesn't exist, create it and add our current
|
142
|
+
# key-value pairs to it. Otherwise, read the file, compare
|
143
|
+
# it to our current set of key-value pairs, and make
|
144
|
+
# adjustments, re-writing the file if necessary.
|
145
|
+
|
146
|
+
if !File.file?(file_path)
|
147
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
148
|
+
File.open(file_path, 'w') { |file| file.write(hash_representation.to_yaml) }
|
149
|
+
else
|
150
|
+
#noinspection RubyResolve
|
151
|
+
#@type [Hash] working
|
152
|
+
working = YAML.load(File.read(file_path))
|
153
|
+
new = working.select {|k,_| hash_representation.has_key?(k)} # only keeps working items if they are current.
|
154
|
+
new = new.merge(hash_representation.select {|k,_| !new.has_key?(k)}) # select the new items.
|
155
|
+
File.open(file_path, 'w') { |file| file.write(new.to_yaml) } unless working == new
|
156
|
+
new.each {|k,v| self[k] = v }
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
#------------------------------------------------------------
|
161
|
+
# Returns all default instances as an array.
|
162
|
+
#------------------------------------------------------------
|
163
|
+
def all_defaults
|
164
|
+
@defaults.values.sort_by(&:name)
|
165
|
+
end
|
166
|
+
|
167
|
+
#------------------------------------------------------------
|
168
|
+
# Returns all default instances as a hash, with key and
|
169
|
+
# value only. If there's no item label, assume that the
|
170
|
+
# default is a command and not an actual setting, and
|
171
|
+
# skip it.
|
172
|
+
#------------------------------------------------------------
|
173
|
+
def hash_representation
|
174
|
+
all_defaults.select {|d| d.item_label}
|
175
|
+
.collect { |d| [d.name, d.value] }.to_h
|
176
|
+
end
|
177
|
+
|
178
|
+
#------------------------------------------------------------
|
179
|
+
# Get a single default instance.
|
180
|
+
#------------------------------------------------------------
|
181
|
+
def default(key)
|
182
|
+
@defaults[key]
|
183
|
+
end
|
184
|
+
|
185
|
+
#------------------------------------------------------------
|
186
|
+
# Return the value of a setting by key or nil.
|
187
|
+
#------------------------------------------------------------
|
188
|
+
def [](key)
|
189
|
+
result = default(key)
|
190
|
+
result ? result.value : nil
|
191
|
+
end
|
192
|
+
|
193
|
+
#------------------------------------------------------------
|
194
|
+
# Set the value of a setting by key.
|
195
|
+
#------------------------------------------------------------
|
196
|
+
def []=(key, value)
|
197
|
+
if default(key)
|
198
|
+
default(key).value = value
|
199
|
+
else
|
200
|
+
define_default(for_key:key, value:value)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
#------------------------------------------------------------
|
205
|
+
# Get and set defaults via methods.
|
206
|
+
#------------------------------------------------------------
|
207
|
+
def method_missing(method, *args)
|
208
|
+
if defines_default?(method) && args.empty?
|
209
|
+
self[method]
|
210
|
+
elsif method.to_s =~ /^(\w+)=$/ && args.size == 1
|
211
|
+
self[Regexp.last_match(1).to_sym] = args[0]
|
212
|
+
else
|
213
|
+
super
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
#------------------------------------------------------------
|
218
|
+
# Ensure that we account for our dynamic methods.
|
219
|
+
#------------------------------------------------------------
|
220
|
+
def respond_to?(method, include_private = false)
|
221
|
+
super || defines_default?(method) || (method =~ /^(\w+)=$/ && defines_default?(Regexp.last_match(1)))
|
222
|
+
end
|
223
|
+
|
224
|
+
#------------------------------------------------------------
|
225
|
+
# Does the default exist?
|
226
|
+
#------------------------------------------------------------
|
227
|
+
def defines_default?(key)
|
228
|
+
@defaults.key?(key)
|
229
|
+
end
|
230
|
+
|
231
|
+
#------------------------------------------------------------
|
232
|
+
# Utility: get all of the argument help text by group, into
|
233
|
+
# a simple hash with group names as keys.
|
234
|
+
#------------------------------------------------------------
|
235
|
+
def arguments_help_by_group
|
236
|
+
|
237
|
+
table = {}
|
238
|
+
|
239
|
+
argument_groups.each do |group|
|
240
|
+
|
241
|
+
#noinspection RubyResolve
|
242
|
+
key = "\n#{C.bold(group)}\n\n"
|
243
|
+
table[key] = []
|
244
|
+
|
245
|
+
@defaults.values.select { |default| default.help_group == group }
|
246
|
+
.each do |default|
|
247
|
+
width = example_args_by_length[-1].length
|
248
|
+
arg = " %-#{width}.#{width}s " % default.example_arg
|
249
|
+
des = default.help_desc % default.value
|
250
|
+
table[key] << arg + des
|
251
|
+
end
|
252
|
+
end
|
253
|
+
table
|
254
|
+
end
|
255
|
+
|
256
|
+
#------------------------------------------------------------
|
257
|
+
# Utility: get an array of argument examples, sorted by
|
258
|
+
# length.
|
259
|
+
#------------------------------------------------------------
|
260
|
+
def example_args_by_length
|
261
|
+
@defaults.values
|
262
|
+
.sort {|a,b| a.example_arg.length <=> b.example_arg.length}
|
263
|
+
.collect {|d| d.example_arg}
|
264
|
+
end
|
265
|
+
|
266
|
+
#------------------------------------------------------------
|
267
|
+
# Utility: Return an array of argument groups. They will
|
268
|
+
# be in the order that they were added to user defaults.
|
269
|
+
#------------------------------------------------------------
|
270
|
+
def argument_groups
|
271
|
+
@defaults.values
|
272
|
+
.collect {|default| default.help_group}
|
273
|
+
.uniq
|
274
|
+
end
|
275
|
+
|
276
|
+
#------------------------------------------------------------
|
277
|
+
# Returns an array of labels and the current matching value.
|
278
|
+
# Accepts a block to add your own pairs.
|
279
|
+
#------------------------------------------------------------
|
280
|
+
def label_value_pairs
|
281
|
+
result = @defaults.values.select {|default| default.item_label}
|
282
|
+
.map {|default| ["#{default.item_label}:", default.value_printable]}
|
283
|
+
yield result if block_given?
|
284
|
+
result
|
285
|
+
end
|
286
|
+
|
287
|
+
|
288
|
+
end # class
|
289
|
+
|
290
|
+
end # module
|