Class: Cooltrainer::DistorteD::ClickAgain

Inherits:
Object
  • Object
show all
Includes:
Invoker
Defined in:
lib/distorted/click_again.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

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 = global_options(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.expand_path(@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_optionsObject (readonly)

Returns the value of attribute lower_options.



19
20
21
# File 'lib/distorted/click_again.rb', line 19

def lower_options
  @lower_options
end

#outer_optionsObject (readonly)

Returns the value of attribute outer_options.



19
20
21
# File 'lib/distorted/click_again.rb', line 19

def outer_options
  @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