Class: Cooltrainer::DistorteD::ClickAgain
- Inherits:
-
Object
- Object
- Cooltrainer::DistorteD::ClickAgain
- Includes:
- Invoker
- Defined in:
- lib/distorted/click_again.rb
Instance Attribute Summary collapse
-
#lower_options ⇒ Object
readonly
Returns the value of attribute lower_options.
-
#outer_options ⇒ Object
readonly
Returns the value of attribute outer_options.
Instance Method Summary collapse
-
#initialize(argv, exe_name) ⇒ ClickAgain
constructor
Set up and parse a given Array of command-line switches based on our global OptionParser and its Type/Molecule-specific sub-commands.
-
#write(dest_root) ⇒ Object
Writes all intended output files to a given directory.
Methods included from Invoker
#basename, #lower_world, #method_missing, #outer_limits, #respond_to_missing?, #type_mars
Constructor Details
#initialize(argv, exe_name) ⇒ ClickAgain
Set up and parse a given Array of command-line switches based on our global OptionParser and its Type/Molecule-specific sub-commands.
:argv will be operated on destructively! Consider passing a duplicate of ARGV instead of passing it directly.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/distorted/click_again.rb', line 27 def initialize(argv, exe_name) # Partition argv into (switches and their arguments) and (filenames or wanted type Strings) switches, @get_out = partition_argv(argv) # Initialize Hashes to store our three types of Options using a small # custom subclass that will store items as a Set but won't store :nil alone. @global_options = Hash.new { |h,k| h[k] = h.class.new(&h.default_proc) } @lower_options = Hash.new { |h,k| h[k] = h.class.new(&h.default_proc) } @outer_options = Hash.new { |h,k| h[k] = h.class.new(&h.default_proc) } # Temporary Array for unmatched Switches when parsing subcommands. sorry_try_again = Array.new # Pass our executable name in for the global OptionParser's banner String, # then parse the complete/raw user-given-arguments-list first with this Parser. # # I am intentionally using OptionParser's non-POSIXy :permute! method # instead of the POSIX-compatible :order! method, # because I want to :) # Otherwise users would have to define all switch arguments # ahead of all positional arguments in the command, # and I think that would be frustrating and silly. # # In strictly-POSIX mode, one would have to call e.g. # `distorted -o image/png inputfile.webp outfilewithnofileextension` # instead of # `distorted inputfile.webp -o image/png outfilewithnofileextension`, # which I find to be much more intuitive. # # Note that `:parse!` would call one of the other of :order!/:permute! based on # an invironment variable `POSIXLY_CORRECT`. Talk about a footgun! # Be explicit!! global = (exe_name) begin switches = global.permute!(switches, into: @global_options) rescue OptionParser::InvalidOption, OptionParser::MissingArgument, OptionParser::ParseError => nope nope.recover(sorry_try_again) # Will :unshift the :nope value to the recovery Array. #if switches&.first&.chr == '-'.freeze # sorry_try_again.unshift(switches.shift) #end retry end switches.unshift(*sorry_try_again.reverse) # The global OptionParser#permute! call will strip our `:argv` Array of # any `--help` or Molecule-picking switches. # Molecule-specific switches (both 'lower' and 'outer') and positional # file-name arguments remain. # # The first remaining `argv` will be our input filename if one was given! # # NOTE: Never assume this filename will be a complete, absolute, usable path. # POSIX shells do not do tilde expansion, for example, on quoted switch arguments, # so a quoted filename argument '~/cover.png' will come through to Ruby-land # as the literal String '~/cover.png' while the same filename argument sans-quotes # will be expanded to e.g. '/home/okeeblow/cover.png' (based on `$HOME` env var). # Additional Ruby-side path validation will almost certainly be needed! # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_01 @name = @get_out&.shift # Print some sort of help message or list of supported input/output Types # if no source filename was given. unless @name puts case when @global_options.has_key?(:help) then global when @global_options.has_key?(:"lower-world") "Supported input media types:\n#{lower_world.keys.join("\n")}" when @global_options.has_key?(:"outer-limits") "Supported output media types:\n#{outer_limits(all: true).values.map{|m| m.keys}.join("\n")}" else global end exit end # Here's that additional filename validation I was talking about. # I don't do this as a one-shot with the argv.shift because # File::expand_path raises an error on :nil argument, # and we already checked for that when we checked for 'help' switches. @name = File.(@name) # Check for 'help' switches *again* now that we have a source file path, # because the output can be file-specific instead of generic. # This is where we display subcommands' help! specific_help = case when @get_out.empty? # Only input filename given; no outputs; nothing left to do! lower_subcommands.merge(outer_subcommands).values.unshift(Hash[:DistorteD => [global]]).map { |l| l.values.join("\n") }.join("\n") when @global_options.has_key?(:help), @global_options.has_key?(:"lower-world") lower_subcommands.values.map { |l| l.values.join("\n") }.join("\n") when @global_options.has_key?(:"outer-limits") # Trigger this help message on `-o` iff that switch is used bare. # If `-o` is given an argument it will inform the MIME::Type # of the same-index output file, e.g. # `-o image/png -o image/webp pngnoextension webpnoextension` # will work exactly as that example implies. @global_options.dig(:"outer-limits")&.empty? ? outer_subcommands.values.map { |o| o.values.join("\n") }.join("\n") : nil else nil end if specific_help puts specific_help exit end # Our "subcommands" are additional instances of OptionParser, # one for every MediaMolecule that can load the source file, # and one for every intended output variation. lower_subcommands.each_pair { |type, molecule_commands| molecule_commands.each_pair { |molecule, subcommand| begin switches = subcommand.permute!(switches, into: @lower_options[type][molecule]) rescue OptionParser::InvalidOption, OptionParser::MissingArgument, OptionParser::ParseError => nope nope.recover(sorry_try_again) # Will :unshift the :nope value to the recovery Array. retry end switches.unshift(*sorry_try_again.reverse) @lower_options[type][molecule].store(:molecule, molecule) } } outer_subcommands.each_pair { |molecule, type_commands| type_commands.each_pair { |type, subcommand| begin switches = subcommand.permute!(switches, into: @outer_options[molecule][type]) rescue OptionParser::InvalidOption, OptionParser::MissingArgument, OptionParser::ParseError => nope nope.recover(sorry_try_again) # Will :unshift the :nope value to the recovery Array. retry end switches.unshift(*sorry_try_again.reverse) @outer_options[molecule][type].store(:molecule, molecule) } } end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method in the class Cooltrainer::DistorteD::Invoker
Instance Attribute Details
#lower_options ⇒ Object (readonly)
Returns the value of attribute lower_options.
19 20 21 |
# File 'lib/distorted/click_again.rb', line 19 def @lower_options end |
#outer_options ⇒ Object (readonly)
Returns the value of attribute outer_options.
19 20 21 |
# File 'lib/distorted/click_again.rb', line 19 def @outer_options end |
Instance Method Details
#write(dest_root) ⇒ Object
Writes all intended output files to a given directory.
167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/distorted/click_again.rb', line 167 def write(dest_root) changes.each { |change| if self.respond_to?(change.type.distorted_file_method) # WISHLIST: Remove the empty final positional Hash argument once we require a Ruby version # that will not perform the implicit Change-to-Hash conversion due to Change's # implementation of :to_hash. Ruby 2.7 will complain but still do the conversion, # breaking downstream callers that want a Struct they can call arbitrary key methods on. # https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/ self.send(change.type.distorted_file_method, dest_root, change, **{}) else raise MediaTypeOutputNotImplementedError.new(change.name, change.type, self.class.name) end } end |