image_optim 0.17.1 → 0.18.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/.gitignore +1 -0
- data/.travis.yml +5 -18
- data/CHANGELOG.markdown +10 -0
- data/README.markdown +31 -1
- data/bin/image_optim +3 -137
- data/image_optim.gemspec +6 -3
- data/lib/image_optim.rb +20 -3
- data/lib/image_optim/bin_resolver.rb +28 -1
- data/lib/image_optim/bin_resolver/bin.rb +17 -7
- data/lib/image_optim/cmd.rb +49 -0
- data/lib/image_optim/config.rb +64 -4
- data/lib/image_optim/image_path.rb +5 -0
- data/lib/image_optim/option_definition.rb +5 -3
- data/lib/image_optim/runner.rb +1 -2
- data/lib/image_optim/runner/option_parser.rb +216 -0
- data/lib/image_optim/worker.rb +32 -17
- data/lib/image_optim/worker/advpng.rb +7 -1
- data/lib/image_optim/worker/gifsicle.rb +16 -3
- data/lib/image_optim/worker/jhead.rb +15 -8
- data/lib/image_optim/worker/jpegoptim.rb +6 -2
- data/lib/image_optim/worker/jpegtran.rb +10 -3
- data/lib/image_optim/worker/optipng.rb +6 -1
- data/lib/image_optim/worker/pngcrush.rb +8 -1
- data/lib/image_optim/worker/pngout.rb +8 -1
- data/lib/image_optim/worker/svgo.rb +4 -1
- data/script/worker_analysis +523 -0
- data/script/worker_analysis.haml +153 -0
- data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +4 -5
- data/spec/image_optim/bin_resolver/simple_version_spec.rb +44 -21
- data/spec/image_optim/bin_resolver_spec.rb +63 -29
- data/spec/image_optim/cmd_spec.rb +66 -0
- data/spec/image_optim/config_spec.rb +38 -38
- data/spec/image_optim/handler_spec.rb +15 -12
- data/spec/image_optim/hash_helpers_spec.rb +14 -13
- data/spec/image_optim/image_path_spec.rb +22 -7
- data/spec/image_optim/runner/glob_helpers_spec.rb +6 -5
- data/spec/image_optim/runner/option_parser_spec.rb +99 -0
- data/spec/image_optim/space_spec.rb +5 -4
- data/spec/image_optim/worker_spec.rb +6 -5
- data/spec/image_optim_spec.rb +209 -237
- data/spec/spec_helper.rb +3 -0
- metadata +43 -11
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'image_optim/bin_resolver/error'
|
2
2
|
require 'image_optim/bin_resolver/simple_version'
|
3
3
|
require 'image_optim/bin_resolver/comparable_condition'
|
4
|
+
require 'image_optim/cmd'
|
5
|
+
require 'shellwords'
|
4
6
|
|
5
7
|
class ImageOptim
|
6
8
|
class BinResolver
|
@@ -11,7 +13,7 @@ class ImageOptim
|
|
11
13
|
attr_reader :name, :path, :version
|
12
14
|
def initialize(name, path)
|
13
15
|
@name = name.to_sym
|
14
|
-
@path = path
|
16
|
+
@path = path.to_s
|
15
17
|
@version = detect_version
|
16
18
|
end
|
17
19
|
|
@@ -59,18 +61,18 @@ class ImageOptim
|
|
59
61
|
def version_string
|
60
62
|
case name
|
61
63
|
when :advpng, :gifsicle, :jpegoptim, :optipng, :pngquant
|
62
|
-
|
64
|
+
capture("#{escaped_path} --version 2> /dev/null")[/\d+(\.\d+){1,}/]
|
63
65
|
when :svgo
|
64
|
-
|
66
|
+
capture("#{escaped_path} --version 2>&1")[/\d+(\.\d+){1,}/]
|
65
67
|
when :jhead
|
66
|
-
|
68
|
+
capture("#{escaped_path} -V 2> /dev/null")[/\d+(\.\d+){1,}/]
|
67
69
|
when :jpegtran
|
68
|
-
|
70
|
+
capture("#{escaped_path} -v - 2>&1")[/version (\d+\S*)/, 1]
|
69
71
|
when :pngcrush
|
70
|
-
|
72
|
+
capture("#{escaped_path} -version 2>&1")[/\d+(\.\d+){1,}/]
|
71
73
|
when :pngout
|
72
74
|
date_regexp = /[A-Z][a-z]{2} (?: |\d)\d \d{4}/
|
73
|
-
date_str =
|
75
|
+
date_str = capture("#{escaped_path} 2>&1")[date_regexp]
|
74
76
|
Date.parse(date_str).strftime('%Y%m%d') if date_str
|
75
77
|
when :jpegrescan
|
76
78
|
# jpegrescan has no version so just check presence
|
@@ -79,6 +81,14 @@ class ImageOptim
|
|
79
81
|
fail "getting `#{name}` version is not defined"
|
80
82
|
end
|
81
83
|
end
|
84
|
+
|
85
|
+
def capture(cmd)
|
86
|
+
Cmd.capture(cmd)
|
87
|
+
end
|
88
|
+
|
89
|
+
def escaped_path
|
90
|
+
path.shellescape
|
91
|
+
end
|
82
92
|
end
|
83
93
|
end
|
84
94
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'English'
|
2
|
+
|
3
|
+
class ImageOptim
|
4
|
+
# Helper for running commands
|
5
|
+
module Cmd
|
6
|
+
class << self
|
7
|
+
# Run using `system`
|
8
|
+
# Return success status
|
9
|
+
# Will raise SignalException if process was interrupted
|
10
|
+
def run(*args)
|
11
|
+
success = system(*args)
|
12
|
+
|
13
|
+
check_status!
|
14
|
+
|
15
|
+
success
|
16
|
+
end
|
17
|
+
|
18
|
+
# Run using backtick
|
19
|
+
# Return captured output
|
20
|
+
# Will raise SignalException if process was interrupted
|
21
|
+
def capture(cmd)
|
22
|
+
output = `#{cmd}`
|
23
|
+
|
24
|
+
check_status!
|
25
|
+
|
26
|
+
output
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def check_status!
|
32
|
+
status = $CHILD_STATUS
|
33
|
+
|
34
|
+
return unless status.signaled?
|
35
|
+
|
36
|
+
# jruby incorrectly returns true for `signaled?` if process exits with
|
37
|
+
# non zero status. For following code
|
38
|
+
#
|
39
|
+
# `sh -c 'exit 66'`
|
40
|
+
# p [$?.signaled?, $?.exitstatus, $?.termsig]
|
41
|
+
#
|
42
|
+
# jruby outputs `[true, 66, 66]` instead of expected `[false, 66, nil]`
|
43
|
+
return if defined?(JRUBY_VERSION) && status.exitstatus == status.termsig
|
44
|
+
|
45
|
+
fail SignalException, status.termsig
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/image_optim/config.rb
CHANGED
@@ -2,6 +2,7 @@ require 'image_optim/option_helpers'
|
|
2
2
|
require 'image_optim/configuration_error'
|
3
3
|
require 'image_optim/hash_helpers'
|
4
4
|
require 'image_optim/worker'
|
5
|
+
require 'image_optim/cmd'
|
5
6
|
require 'set'
|
6
7
|
require 'yaml'
|
7
8
|
|
@@ -10,9 +11,13 @@ class ImageOptim
|
|
10
11
|
class Config
|
11
12
|
include OptionHelpers
|
12
13
|
|
14
|
+
# Global config path at `$XDG_CONFIG_HOME/image_optim.yml` (by default
|
15
|
+
# `~/.config/image_optim.yml`)
|
13
16
|
GLOBAL_PATH = begin
|
14
17
|
File.join(ENV['XDG_CONFIG_HOME'] || '~/.config', 'image_optim.yml')
|
15
18
|
end
|
19
|
+
|
20
|
+
# Local config path at `./.image_optim.yml`
|
16
21
|
LOCAL_PATH = './.image_optim.yml'
|
17
22
|
|
18
23
|
class << self
|
@@ -37,6 +42,9 @@ class ImageOptim
|
|
37
42
|
end
|
38
43
|
end
|
39
44
|
|
45
|
+
# Merge config from files with passed options
|
46
|
+
# Config files are checked at `GLOBAL_PATH` and `LOCAL_PATH` unless
|
47
|
+
# overriden using `:config_paths`
|
40
48
|
def initialize(options)
|
41
49
|
config_paths = options.delete(:config_paths) || [GLOBAL_PATH, LOCAL_PATH]
|
42
50
|
config_paths = Array(config_paths)
|
@@ -50,18 +58,31 @@ class ImageOptim
|
|
50
58
|
@used = Set.new
|
51
59
|
end
|
52
60
|
|
61
|
+
# Gets value for key converted to symbol and mark option as used
|
53
62
|
def get!(key)
|
54
63
|
key = key.to_sym
|
55
64
|
@used << key
|
56
65
|
@options[key]
|
57
66
|
end
|
58
67
|
|
68
|
+
# Check if key is present
|
69
|
+
def key?(key)
|
70
|
+
key = key.to_sym
|
71
|
+
@options.key?(key)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Fail unless all options were marked as used (directly or indirectly
|
75
|
+
# accessed using `get!`)
|
59
76
|
def assert_no_unused_options!
|
60
77
|
unknown_options = @options.reject{ |key, _value| @used.include?(key) }
|
61
78
|
return if unknown_options.empty?
|
62
79
|
fail ConfigurationError, "unknown options #{unknown_options.inspect}"
|
63
80
|
end
|
64
81
|
|
82
|
+
# Nice level:
|
83
|
+
# * `10` by default and for `nil` or `true`
|
84
|
+
# * `0` for `false`
|
85
|
+
# * otherwise convert to integer
|
65
86
|
def nice
|
66
87
|
nice = get!(:nice)
|
67
88
|
|
@@ -75,6 +96,10 @@ class ImageOptim
|
|
75
96
|
end
|
76
97
|
end
|
77
98
|
|
99
|
+
# Number of parallel threads:
|
100
|
+
# * `processor_count` by default and for `nil` or `true`
|
101
|
+
# * `1` for `false`
|
102
|
+
# * otherwise convert to integer
|
78
103
|
def threads
|
79
104
|
threads = get!(:threads)
|
80
105
|
|
@@ -88,10 +113,40 @@ class ImageOptim
|
|
88
113
|
end
|
89
114
|
end
|
90
115
|
|
116
|
+
# Verbose mode, converted to boolean
|
91
117
|
def verbose
|
92
118
|
!!get!(:verbose)
|
93
119
|
end
|
94
120
|
|
121
|
+
# Using image_optim_pack:
|
122
|
+
# * `false` to disable
|
123
|
+
# * `nil` to use if available
|
124
|
+
# * everything else to require
|
125
|
+
def pack
|
126
|
+
pack = get!(:pack)
|
127
|
+
return false if pack == false
|
128
|
+
|
129
|
+
require 'image_optim/pack'
|
130
|
+
true
|
131
|
+
rescue LoadError => e
|
132
|
+
raise "Cannot load image_optim_pack: #{e}" if pack
|
133
|
+
false
|
134
|
+
end
|
135
|
+
|
136
|
+
# Skip missing workers, converted to boolean
|
137
|
+
def skip_missing_workers
|
138
|
+
if key?(:skip_missing_workers)
|
139
|
+
!!get!(:skip_missing_workers)
|
140
|
+
else
|
141
|
+
pack
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Options for worker class by its `bin_sym`:
|
146
|
+
# * `Hash` passed as is
|
147
|
+
# * `{}` for `true` or `nil`
|
148
|
+
# * `false` for `false`
|
149
|
+
# * otherwise fail with `ConfigurationError`
|
95
150
|
def for_worker(klass)
|
96
151
|
worker_options = get!(klass.bin_sym)
|
97
152
|
|
@@ -108,6 +163,7 @@ class ImageOptim
|
|
108
163
|
end
|
109
164
|
end
|
110
165
|
|
166
|
+
# yaml dump without document beginning prefix `---`
|
111
167
|
def to_s
|
112
168
|
YAML.dump(HashHelpers.deep_stringify_keys(@options)).sub(/\A---\n/, '')
|
113
169
|
end
|
@@ -118,13 +174,17 @@ class ImageOptim
|
|
118
174
|
def processor_count
|
119
175
|
@processor_count ||= case host_os = RbConfig::CONFIG['host_os']
|
120
176
|
when /darwin9/
|
121
|
-
|
177
|
+
Cmd.capture 'hwprefs cpu_count'
|
122
178
|
when /darwin/
|
123
|
-
(
|
179
|
+
if (Cmd.capture 'which hwprefs') != ''
|
180
|
+
Cmd.capture 'hwprefs thread_count'
|
181
|
+
else
|
182
|
+
Cmd.capture 'sysctl -n hw.ncpu'
|
183
|
+
end
|
124
184
|
when /linux/
|
125
|
-
|
185
|
+
Cmd.capture 'grep -c processor /proc/cpuinfo'
|
126
186
|
when /freebsd/
|
127
|
-
|
187
|
+
Cmd.capture 'sysctl -n hw.ncpu'
|
128
188
|
when /mswin|mingw/
|
129
189
|
require 'win32ole'
|
130
190
|
WIN32OLE.
|
@@ -3,9 +3,11 @@ class ImageOptim
|
|
3
3
|
class OptionDefinition
|
4
4
|
attr_reader :name, :default, :type, :description, :proc
|
5
5
|
|
6
|
-
def initialize(name, default,
|
7
|
-
if
|
8
|
-
type
|
6
|
+
def initialize(name, default, type_or_description, description = nil, &proc)
|
7
|
+
if type_or_description.is_a?(Class)
|
8
|
+
type = type_or_description
|
9
|
+
else
|
10
|
+
type, description = default.class, type_or_description
|
9
11
|
end
|
10
12
|
|
11
13
|
@name = name.to_sym
|
data/lib/image_optim/runner.rb
CHANGED
@@ -3,7 +3,6 @@ require 'image_optim/hash_helpers'
|
|
3
3
|
require 'image_optim/runner/glob_helpers'
|
4
4
|
require 'image_optim/space'
|
5
5
|
require 'progress'
|
6
|
-
require 'optparse'
|
7
6
|
require 'find'
|
8
7
|
require 'yaml'
|
9
8
|
|
@@ -127,7 +126,7 @@ class ImageOptim
|
|
127
126
|
basename = File.basename(path)
|
128
127
|
globs.any? do |glob|
|
129
128
|
File.fnmatch(glob, relative_path, File::FNM_PATHNAME) ||
|
130
|
-
|
129
|
+
File.fnmatch(glob, basename, File::FNM_PATHNAME)
|
131
130
|
end
|
132
131
|
end
|
133
132
|
|
@@ -0,0 +1,216 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'image_optim'
|
4
|
+
require 'image_optim/true_false_nil'
|
5
|
+
require 'image_optim/non_negative_integer_range'
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
class ImageOptim
|
9
|
+
class Runner
|
10
|
+
# Parse options from arguments to image_optim binary
|
11
|
+
class OptionParser < ::OptionParser
|
12
|
+
# Parse and remove options from args, return options Hash
|
13
|
+
# Calls abort in case of parse error
|
14
|
+
def self.parse!(args)
|
15
|
+
# assume -v to be a request to print version if it is the only argument
|
16
|
+
args = %w[--version] if args == %w[-v]
|
17
|
+
|
18
|
+
options = {}
|
19
|
+
parser = new(options)
|
20
|
+
parser.parse!(args)
|
21
|
+
options
|
22
|
+
rescue OptionParser::ParseError => e
|
23
|
+
abort "#{e}\n\n#{parser.help}"
|
24
|
+
end
|
25
|
+
|
26
|
+
# After initialization passes self and options to DEFINE
|
27
|
+
def initialize(options)
|
28
|
+
super
|
29
|
+
DEFINE.call(self, options)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Wraps and indents lines of overriden method
|
33
|
+
def help
|
34
|
+
text = super
|
35
|
+
|
36
|
+
# reserve one column
|
37
|
+
columns = terminal_columns - 1
|
38
|
+
# 1 for distance between summary and description
|
39
|
+
# 2 for additional indent
|
40
|
+
wrapped_indent = summary_indent + ' ' * (summary_width + 1 + 2)
|
41
|
+
wrapped_width = columns - wrapped_indent.length
|
42
|
+
# don't try to wrap if there is too little space for description
|
43
|
+
return text if wrapped_width < 20
|
44
|
+
|
45
|
+
wrapped = ''
|
46
|
+
text.split("\n").each do |line|
|
47
|
+
if line.length <= columns
|
48
|
+
wrapped << line << "\n"
|
49
|
+
else
|
50
|
+
indented = line =~ /^\s/
|
51
|
+
wrapped << line.slice!(wrap_regex(columns)) << "\n"
|
52
|
+
line.scan(wrap_regex(wrapped_width)) do |part|
|
53
|
+
wrapped << wrapped_indent if indented
|
54
|
+
wrapped << part << "\n"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
wrapped
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def terminal_columns
|
64
|
+
stty_columns = `stty size 2> /dev/null`[/^\d+ (\d+)$/, 1]
|
65
|
+
stty_columns ? stty_columns.to_i : `tput cols`.to_i
|
66
|
+
end
|
67
|
+
|
68
|
+
def wrap_regex(width)
|
69
|
+
/.*?.{1,#{width}}(?:\s|\z)/
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
ImageOptim::Runner::OptionParser::DEFINE = proc do |op, options|
|
76
|
+
unless op.is_a?(OptionParser)
|
77
|
+
fail ArgumentError, "expected instance of OptionParser, got #{op.inspect}"
|
78
|
+
end
|
79
|
+
unless options.is_a?(Hash)
|
80
|
+
fail ArgumentError, "expected instance of Hash, got #{options.inspect}"
|
81
|
+
end
|
82
|
+
|
83
|
+
ImageOptim::TrueFalseNil.add_to_option_parser(op)
|
84
|
+
ImageOptim::NonNegativeIntegerRange.add_to_option_parser(op)
|
85
|
+
|
86
|
+
op.banner = <<-TEXT.gsub(/^\s*\|/, '')
|
87
|
+
|#{ImageOptim.full_version}
|
88
|
+
|
|
89
|
+
|Usege:
|
90
|
+
| #{op.program_name} [options] image_path …
|
91
|
+
|
|
92
|
+
|Configuration will be read and prepanded to options from two paths:
|
93
|
+
| #{ImageOptim::Config::GLOBAL_PATH}
|
94
|
+
| #{ImageOptim::Config::LOCAL_PATH}
|
95
|
+
|
|
96
|
+
TEXT
|
97
|
+
|
98
|
+
op.on('--config-paths PATH1,PATH2', Array, 'Config paths to use instead of '\
|
99
|
+
'default ones') do |paths|
|
100
|
+
options[:config_paths] = paths
|
101
|
+
end
|
102
|
+
|
103
|
+
op.separator nil
|
104
|
+
|
105
|
+
op.on('-r', '-R', '--recursive', 'Recursively scan directories '\
|
106
|
+
'for images') do |recursive|
|
107
|
+
options[:recursive] = recursive
|
108
|
+
end
|
109
|
+
|
110
|
+
op.on("--exclude-dir 'GLOB'", 'Glob for excluding directories '\
|
111
|
+
'(defaults to .*)') do |glob|
|
112
|
+
options[:exclude_dir_glob] = glob
|
113
|
+
end
|
114
|
+
|
115
|
+
op.on("--exclude-file 'GLOB'", 'Glob for excluding files '\
|
116
|
+
'(defaults to .*)') do |glob|
|
117
|
+
options[:exclude_file_glob] = glob
|
118
|
+
end
|
119
|
+
|
120
|
+
op.on("--exclude 'GLOB'", 'Set glob for excluding both directories and '\
|
121
|
+
'files') do |glob|
|
122
|
+
options[:exclude_file_glob] = options[:exclude_dir_glob] = glob
|
123
|
+
end
|
124
|
+
|
125
|
+
op.separator nil
|
126
|
+
|
127
|
+
op.on('--[no-]threads N', Integer, 'Number of threads or disable '\
|
128
|
+
'(defaults to number of processors)') do |threads|
|
129
|
+
options[:threads] = threads
|
130
|
+
end
|
131
|
+
|
132
|
+
op.on('--[no-]nice N', Integer, 'Nice level (defaults to 10)') do |nice|
|
133
|
+
options[:nice] = nice
|
134
|
+
end
|
135
|
+
|
136
|
+
op.on('--[no-]pack', 'Require image_optim_pack or disable it, '\
|
137
|
+
'by default image_optim_pack will be used if available, '\
|
138
|
+
'will turn on skip-missing-workers unless explicitly disabled') do |pack|
|
139
|
+
options[:pack] = pack
|
140
|
+
end
|
141
|
+
|
142
|
+
op.separator nil
|
143
|
+
op.separator ' Disabling workers:'
|
144
|
+
|
145
|
+
op.on('--[no-]skip-missing-workers', 'Skip workers with missing or '\
|
146
|
+
'problematic binaries') do |skip|
|
147
|
+
options[:skip_missing_workers] = skip
|
148
|
+
end
|
149
|
+
|
150
|
+
ImageOptim::Worker.klasses.each do |klass|
|
151
|
+
bin = klass.bin_sym
|
152
|
+
op.on("--no-#{bin}", "disable #{bin} worker") do |enable|
|
153
|
+
options[bin] = enable
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
op.separator nil
|
158
|
+
op.separator ' Worker options:'
|
159
|
+
|
160
|
+
ImageOptim::Worker.klasses.each_with_index do |klass, i|
|
161
|
+
next if klass.option_definitions.empty?
|
162
|
+
op.separator nil unless i.zero?
|
163
|
+
|
164
|
+
bin = klass.bin_sym
|
165
|
+
klass.option_definitions.each do |option_definition|
|
166
|
+
name = option_definition.name.to_s.gsub('_', '-')
|
167
|
+
default = option_definition.default
|
168
|
+
type = option_definition.type
|
169
|
+
|
170
|
+
type, marking = case
|
171
|
+
when [TrueClass, FalseClass, ImageOptim::TrueFalseNil].include?(type)
|
172
|
+
[type, 'B']
|
173
|
+
when Integer >= type
|
174
|
+
[Integer, 'N']
|
175
|
+
when Array >= type
|
176
|
+
[Array, 'a,b,c']
|
177
|
+
when ImageOptim::NonNegativeIntegerRange == type
|
178
|
+
[type, 'M-N']
|
179
|
+
else
|
180
|
+
fail "Unknown type #{type}"
|
181
|
+
end
|
182
|
+
|
183
|
+
description_lines = %W[
|
184
|
+
#{option_definition.description.gsub(' - ', ' - ')}
|
185
|
+
(defaults to #{default})
|
186
|
+
].join(' ')
|
187
|
+
|
188
|
+
op.on("--#{bin}-#{name} #{marking}", type, *description_lines) do |value|
|
189
|
+
options[bin] = {} unless options[bin].is_a?(Hash)
|
190
|
+
options[bin][option_definition.name.to_sym] = value
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
op.separator nil
|
196
|
+
op.separator ' Common options:'
|
197
|
+
|
198
|
+
op.on_tail('-v', '--verbose', 'Verbose output') do
|
199
|
+
options[:verbose] = true
|
200
|
+
end
|
201
|
+
|
202
|
+
op.on_tail('-h', '--help', 'Show help and exit') do
|
203
|
+
puts op.help
|
204
|
+
exit
|
205
|
+
end
|
206
|
+
|
207
|
+
op.on_tail('--version', 'Show version and exit') do
|
208
|
+
puts ImageOptim.version
|
209
|
+
exit
|
210
|
+
end
|
211
|
+
|
212
|
+
op.on_tail('--info', 'Show environment info and exit') do
|
213
|
+
options[:verbose] = true
|
214
|
+
options[:only_info] = true
|
215
|
+
end
|
216
|
+
end
|