Class: Pvectl::ArgvPreprocessor

Inherits:
Object
  • Object
show all
Defined in:
lib/pvectl/argv_preprocessor.rb,
sig/pvectl/argv_preprocessor.rbs

Overview

Command line argument preprocessor.

Normalizes ARGV arguments before passing to GLI by reordering flags to appear before positional arguments, using GLI command metadata for dynamic flag discovery.

GLI with subcommand_option_handling :normal requires flags before positional arguments. This preprocessor allows kubectl-style flag placement anywhere on the command line.

Examples:

Global flags moved to beginning

ArgvPreprocessor.process(["get", "nodes", "-o", "json"], cli_app: CLI)
#=> ["-o", "json", "get", "nodes"]

Command flags moved before positional args

ArgvPreprocessor.process(["delete", "vm", "103", "--yes"], cli_app: CLI)
#=> ["delete", "--yes", "vm", "103"]

Passthrough mode for --help

ArgvPreprocessor.process(["--help", "get"], cli_app: CLI)
#=> ["--help", "get"]  # unchanged

Defined Under Namespace

Classes: DuplicateFlagError

Constant Summary collapse

MAX_ARGUMENTS =

Returns Maximum number of arguments (DoS protection).

Returns:

  • (Integer)

    Maximum number of arguments (DoS protection)

10_000
MAX_ARGUMENT_LENGTH =

Returns Maximum length of single argument in bytes.

Returns:

  • (Integer)

    Maximum length of single argument in bytes

4096
PASSTHROUGH_FLAGS =

Returns Flags passed through without processing.

Returns:

  • (Array<String>)

    Flags passed through without processing

%w[--help -h --version].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(argv, cli_app: Pvectl::CLI) ⇒ ArgvPreprocessor

Initializes preprocessor with a copy of arguments.

Parameters:

  • argv (Array<String>)

    command line arguments

  • cli_app (GLI::App) (defaults to: Pvectl::CLI)

    CLI application with registered commands

  • cli_app: (Object) (defaults to: Pvectl::CLI)


63
64
65
66
# File 'lib/pvectl/argv_preprocessor.rb', line 63

def initialize(argv, cli_app: Pvectl::CLI)
  @argv = argv.dup
  @cli_app = cli_app
end

Class Method Details

.process(argv, cli_app: Pvectl::CLI) ⇒ Array<String>

Processes command line arguments.

Parameters:

  • argv (Array<String>)

    command line arguments

  • cli_app (GLI::App) (defaults to: Pvectl::CLI)

    CLI application with registered commands

  • cli_app: (Object) (defaults to: Pvectl::CLI)

Returns:

  • (Array<String>)

    normalized arguments

Raises:

  • (ArgumentError)

    when input limits are exceeded

  • (DuplicateFlagError)

    when global flag has different values



55
56
57
# File 'lib/pvectl/argv_preprocessor.rb', line 55

def self.process(argv, cli_app: Pvectl::CLI)
  new(argv, cli_app: cli_app).call
end

Instance Method Details

#callArray<String>

Executes argument processing.

Returns:

  • (Array<String>)

    normalized arguments

Raises:

  • (ArgumentError)

    when input limits are exceeded

  • (DuplicateFlagError)

    when global flag has different values



73
74
75
76
77
78
79
# File 'lib/pvectl/argv_preprocessor.rb', line 73

def call
  validate_input_limits!
  return @argv if passthrough_mode?
  return [] if @argv.empty?

  reorder_all_flags
end

#extract_global_flags(args) ⇒ Array(Array<String>, Array<String>)

Extracts global flags from anywhere in the argument list.

Parameters:

  • args (Array<String>)

    arguments

Returns:

  • (Array(Array<String>, Array<String>))

    [extracted_global_flags, remaining_args]



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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/pvectl/argv_preprocessor.rb', line 137

def extract_global_flags(args)
  global_flags_collected = {}
  global_result = []
  remaining = []
  index = 0

  while index < args.length
    arg = args[index]

    if arg == "--"
      remaining.concat(args[index..])
      break
    end

    flag_info = find_global_flag(arg)
    if flag_info
      name, has_value = flag_info
      if arg.include?("=")
        _, value = arg.split("=", 2)
        validate_value!(value, name)
        store_global_flag(global_flags_collected, name, value)
        global_result << arg
      elsif has_value
        raise ArgumentError, "Missing value for flag #{arg}" if index + 1 >= args.length

        value = args[index + 1]
        validate_value!(value, name)
        store_global_flag(global_flags_collected, name, value)
        global_result << arg << value
        index += 1
      else
        store_global_flag(global_flags_collected, name, true)
        global_result << arg
      end
    else
      remaining << arg
    end

    index += 1
  end

  [global_result, remaining]
end

#find_command_flag(arg, cmd) ⇒ Boolean?

Finds a command flag/switch definition matching the given argument.

Parameters:

  • arg (String)

    argument to check

  • cmd (GLI::Command)

    command to search in

Returns:

  • (Boolean, nil)

    has_value (true for flags, false for switches) or nil if not found



320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/pvectl/argv_preprocessor.rb', line 320

def find_command_flag(arg, cmd)
  flag_part = arg.split("=", 2).first

  cmd.flags.each_value do |flag|
    return true if flag_matches?(flag, flag_part)
  end

  cmd.switches.each_value do |sw|
    return false if flag_matches?(sw, flag_part)
  end

  nil
end

#find_gli_command(name) ⇒ GLI::Command?

Finds a GLI command by name (supports aliases and dash-to-underscore).

Parameters:

  • name (String)

    command name

Returns:



265
266
267
# File 'lib/pvectl/argv_preprocessor.rb', line 265

def find_gli_command(name)
  @cli_app.commands[name.to_sym] || @cli_app.commands[name.tr("-", "_").to_sym]
end

#find_gli_subcommand(cmd, name) ⇒ GLI::Command?

Finds a GLI subcommand by name (supports aliases and dash-to-underscore).

Parameters:

  • cmd (GLI::Command)

    parent command

  • name (String)

    subcommand name

Returns:



274
275
276
# File 'lib/pvectl/argv_preprocessor.rb', line 274

def find_gli_subcommand(cmd, name)
  cmd.commands[name.to_sym] || cmd.commands[name.tr("-", "_").to_sym]
end

#find_global_flag(arg) ⇒ Array(Symbol, Boolean)?

Finds a global flag definition matching the given argument.

Parameters:

  • arg (String)

    argument to check

Returns:

  • (Array(Symbol, Boolean), nil)

    [flag_name, has_value] or nil



185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/pvectl/argv_preprocessor.rb', line 185

def find_global_flag(arg)
  flag_part = arg.split("=", 2).first

  @cli_app.flags.each_value do |flag|
    return [flag_display_name(flag), true] if flag_matches?(flag, flag_part)
  end

  @cli_app.switches.each_value do |sw|
    return [flag_display_name(sw), false] if flag_matches?(sw, flag_part)
  end

  nil
end

#flag_display_name(gli_flag) ⇒ Symbol

Returns the display name for a GLI flag/switch (prefers long name).

Parameters:

  • gli_flag (GLI::Flag, GLI::Switch)

    GLI flag or switch object

Returns:

  • (Symbol)

    display name (long form if available)



216
217
218
219
220
# File 'lib/pvectl/argv_preprocessor.rb', line 216

def flag_display_name(gli_flag)
  all_names = [gli_flag.name] + (gli_flag.aliases || [])
  long_name = all_names.find { |n| n.to_s.length > 1 }
  long_name || gli_flag.name
end

#flag_matches?(gli_flag, flag_part) ⇒ Boolean

Checks if a GLI flag/switch matches the given argument string.

Parameters:

  • gli_flag (GLI::Flag, GLI::Switch)

    GLI flag or switch object

  • flag_part (String)

    argument to match (e.g., "-o", "--output")

Returns:

  • (Boolean)

    true if matches



204
205
206
207
208
209
210
# File 'lib/pvectl/argv_preprocessor.rb', line 204

def flag_matches?(gli_flag, flag_part)
  all_names = [gli_flag.name] + (gli_flag.aliases || [])
  all_names.any? do |name|
    prefix = name.to_s.length == 1 ? "-" : "--"
    "#{prefix}#{name}" == flag_part
  end
end

#identify_command(args) ⇒ Array(String, Array<String>, Array<String>)

Identifies the command name from remaining args (after global flag extraction).

Parameters:

  • args (Array<String>)

    arguments without global flags

Returns:

  • (Array(String, Array<String>, Array<String>))

    [command_name, command_tokens, rest]



254
255
256
257
258
259
# File 'lib/pvectl/argv_preprocessor.rb', line 254

def identify_command(args)
  return [nil, [], args] if args.empty? || args.first.start_with?("-")

  command_name = args.first
  [command_name, [command_name], args[1..]]
end

#passthrough_mode?Boolean

Checks if arguments contain flags requiring passthrough.

Returns:

  • (Boolean)

    true if --help, -h or --version detected



98
99
100
# File 'lib/pvectl/argv_preprocessor.rb', line 98

def passthrough_mode?
  (@argv & PASSTHROUGH_FLAGS).any?
end

#reorder_all_flagsArray<String>

Main reordering logic. Three phases:

  1. Extract global flags to front
  2. Identify command (and optional subcommand)
  3. Reorder command/subcommand flags before positional args

Returns:

  • (Array<String>)

    reordered arguments



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/pvectl/argv_preprocessor.rb', line 108

def reorder_all_flags
  global_flags, rest = extract_global_flags(@argv)
  return global_flags + rest if rest.empty?

  command_name, command_tokens, after_command = identify_command(rest)
  return global_flags + rest unless command_name

  cmd = find_gli_command(command_name)
  return global_flags + rest unless cmd

  # Check for subcommand
  if after_command.any? && cmd.commands.any?
    sub_name = after_command.first
    sub_cmd = find_gli_subcommand(cmd, sub_name)
    if sub_cmd
      subcommand_tokens = [after_command.shift]
      reordered = reorder_command_flags(after_command, sub_cmd)
      return global_flags + command_tokens + subcommand_tokens + reordered
    end
  end

  reordered = reorder_command_flags(after_command, cmd)
  global_flags + command_tokens + reordered
end

#reorder_command_flags(args, cmd) ⇒ Array<String>

Reorders command flags to appear before positional arguments.

Parameters:

  • args (Array<String>)

    arguments after command name

  • cmd (GLI::Command)

    GLI command with flag/switch metadata

Returns:

  • (Array<String>)

    reordered arguments



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/pvectl/argv_preprocessor.rb', line 283

def reorder_command_flags(args, cmd)
  flags = []
  positional = []
  index = 0

  while index < args.length
    arg = args[index]

    if arg == "--"
      positional.concat(args[index..])
      break
    end

    flag_info = find_command_flag(arg, cmd)
    unless flag_info.nil?
      has_value = flag_info
      if has_value && !arg.include?("=") && index + 1 < args.length
        flags << arg << args[index + 1]
        index += 1
      else
        flags << arg
      end
    else
      positional << arg
    end

    index += 1
  end

  flags + positional
end

#store_global_flag(store, name, value) ⇒ void

This method returns an undefined value.

Stores a global flag value with duplicate detection.

Parameters:

  • store (Hash)

    flag storage

  • name (Symbol)

    flag name

  • value (String, Boolean)

    flag value

Raises:



229
230
231
232
233
234
235
236
# File 'lib/pvectl/argv_preprocessor.rb', line 229

def store_global_flag(store, name, value)
  if store.key?(name)
    existing = store[name]
    raise DuplicateFlagError.new(name, existing, value) if existing != value
  else
    store[name] = value
  end
end

#validate_input_limits!void

This method returns an undefined value.

Validates input limits for DoS attack protection.

Raises:

  • (ArgumentError)

    when argument count or length exceeds limits



87
88
89
90
91
92
93
# File 'lib/pvectl/argv_preprocessor.rb', line 87

def validate_input_limits!
  raise ArgumentError, "Too many arguments (max #{MAX_ARGUMENTS})" if @argv.length > MAX_ARGUMENTS

  @argv.each do |arg|
    raise ArgumentError, "Argument too long (max #{MAX_ARGUMENT_LENGTH})" if arg.length > MAX_ARGUMENT_LENGTH
  end
end

#validate_value!(value, flag_name) ⇒ void

This method returns an undefined value.

Validates flag value for security.

Parameters:

  • value (String, Boolean)

    value to validate

  • flag_name (Symbol)

    flag name (for error message)

Raises:

  • (ArgumentError)

    when value contains null byte



244
245
246
247
248
# File 'lib/pvectl/argv_preprocessor.rb', line 244

def validate_value!(value, flag_name)
  return if value == true

  raise ArgumentError, "Invalid null byte in value for --#{flag_name}" if value.to_s.include?("\x00")
end