Skip to content

postmodern/command_mapper.rb

Folders and files

NameName
Last commit message
Last commit date
Apr 17, 2022
Apr 18, 2022
Apr 17, 2022
Apr 17, 2022
Oct 2, 2021
Sep 9, 2021
Nov 19, 2021
Apr 19, 2022
Jan 25, 2022
Mar 25, 2022
Apr 17, 2022
Sep 9, 2021
Sep 9, 2021
Nov 17, 2021

Repository files navigation

command_mapper

CI Code Climate Gem Version

Description

Command Mapper maps a command's options and arguments to Class attributes to allow safely and securely executing commands.

Features

  • Supports defining commands as Ruby classes.
  • Supports mapping in options and additional arguments.
    • Supports common option types:
      • Str: string values
      • Num: numeric values
      • Hex: hexadecimal values
      • Map: maps true/false to yes/no, or enabled/disabled (aka --opt=yes|no or --opt=enabled|disabled values).
      • Enum: maps a finite set of Symbols to a finite set of Strings (aka --opt={foo|bar|baz} values).
      • List: comma-separated list (aka --opt VALUE,...).
      • KeyValue: maps a Hash or Array to key:value Strings (aka --opt KEY:VALUE or --opt KEY=VALUE values).
      • KeyValueList: a key-value list (aka --opt KEY:VALUE,... or --opt KEY=VALUE;... values).
      • InputPath: a path to a pre-existing file or directory
      • InputFile: a path to a pre-existing file
      • InputDir: a path to a pre-existing directory
  • Supports mapping in sub-commands.
  • Allows running the command via IO.popen to read the command's output.
  • Allows running commands with additional environment variables.
  • Allows overriding the command name or path to the command.
  • Allows running commands via sudo.
  • Prevents command injection and option injection.

Examples

require 'command_mapper/command'

#
# Represents the `grep` command
#
class Grep < CommandMapper::Command

  command "grep" do
    option "--extended-regexp"
    option "--fixed-strings"
    option "--basic-regexp"
    option "--perl-regexp"
    option "--regexp", equals: true, value: true
    option "--file", name: :patterns_file, equals: true, value: true
    option "--ignore-case"
    option "--no-ignore-case"
    option "--word-regexp"
    option "--line-regexp"
    option "--null-data"
    option "--no-messages"
    option "--invert-match"
    option "--version"
    option "--help"
    option "--max-count", equals: true, value: {type: Num.new}
    option "--byte-offset"
    option "--line-number"
    option "--line-buffered"
    option "--with-filename"
    option "--no-filename"
    option "--label", equals: true, value: true
    option "--only-matching"
    option "--quiet"
    option "--binary-files", equals: true, value: true
    option "--text"
    option "-I", name: 	# FIXME: name
    option "--directories", equals: true, value: true
    option "--devices", equals: true, value: true
    option "--recursive"
    option "--dereference-recursive"
    option "--include", equals: true, value: true
    option "--exclude", equals: true, value: true
    option "--exclude-from", equals: true, value: true
    option "--exclude-dir", equals: true, value: true
    option "--files-without-match", value: true
    option "--files-with-matches"
    option "--count"
    option "--initial-tab"
    option "--null"
    option "--before-context", equals: true, value: {type: Num.new}
    option "--after-context", equals: true, value: {type: Num.new}
    option "--context", equals: true, value: {type: Num.new}
    option "--group-separator", equals: true, value: true
    option "--no-group-separator"
    option "--color", equals: :optional, value: {required: false}
    option "--colour", equals: :optional, value: {required: false}
    option "--binary"

    argument :patterns
    argument :file, required: false, repeats: true
  end

end

Defining Options

option "--opt"

Define a short option:

option "-o", name: :opt

Defines an option with a required value:

option "--output", value: {required: true}

Defines an option that uses an equals sign (ex: --output=value):

option "--output", equals: true, value: {required: true}

Defines an option where the value is embedded into the flag (ex: -Ivalue):

option "-I", value: {required: true}, value_in_flag: true

Defines an option that can be specified multiple times:

option "--include-dir", repeats: true

Defines an option that accepts a numeric value:

option "--count", value: {type: Num.new}

Define an option that only accepts a range of acceptable values:

option "--count", value: {type: Num.new(range: 1..100)}

Defines an option that accepts a comma-separated list:

option "--list", value: {type: List.new}

Defines an option that accepts a key=value pair:

option "--param", value: {type: KeyValue.new}

Defines an option that accepts a key:value pair:

option "--param", value: {type: KeyValue.new(separator: ':')}

Defines an option that accepts a finite number of values:

option "--type", value: {type: Enum[:foo, :bar, :baz]}

Custom methods:

def foo
  @foo || @bar
end

def foo=(value)
  @foo = case value
         when Hash  then ...
         when Array then ...
         else            value.to_s
         end
end

Defining Arguments

argument :host

Define an optional argument:

argument :optional_output, required: false

Define an argument that can be repeated:

argument :files, repeats: true

Define an argument that accepts an existing file:

argument :file, type: InputFile.new

Define an argument that accepts an existing directory:

argument :dir, type: InputDir.new

Custom methods:

def foo
  @foo || @bar
end

def foo=(value)
  @foo = case value
         when Hash  then ...
         when Array then ...
         else            value.to_s
         end
end

Custom Types

class PortRange < CommandMapper::Types::Type

  def validate(value)
    case value
    when Integer
      true
    when Range
      if value.begin.kind_of?(Integer)
        true
      else
        [false, "port range can only contain Integers"]
      end
    else
      [false, "port range must be an Integer or a Range of Integers"]
    end
  end

  def format(value)
    case value
    when Integer
      "#{value}"
    when Range
      "#{value.begin}-#{value.end}"
    end
  end

end

option :ports, value: {required: true, type: PortRange.new}

Running

Keyword arguments:

Grep.run(ignore_case: true, patterns: "foo", file: "file.txt")
# ...

With a block:

Grep.run do |grep|
  grep.ignore_case = true
  grep.patterns    = "foo"
  grep.file        = "file.txt"
end

Capturing output

Grep.capture(ignore_case: true, patterns: "foo", file: "file.txt")
# => "..."

popen

io = Grep.popen(ignore_case: true, patterns: "foo", file: "file.txt")

io.each_line do |line|
  # ...
end

sudo

Grep.sudo(patterns: "Error", file: "/var/log/syslog")
# Password: 
# ...

Code Gen

command_mapper-gen can automatically generate command classes from a command's --help output and/or man page.

$ gem install command_mapper-gen
$ command_mapper-gen cat
require 'command_mapper/command'

#
# Represents the `cat` command
#
class Cat < CommandMapper::Command

  command "cat" do
    option "--show-all"
    option "--number-nonblank"
    option "-e", name: 	# FIXME: name
    option "--show-ends"
    option "--number"
    option "--squeeze-blank"
    option "-t", name: 	# FIXME: name
    option "--show-tabs"
    option "-u", name: 	# FIXME: name
    option "--show-nonprinting"
    option "--help"
    option "--version"

    argument :file, required: false, repeats: true
  end

end

Requirements

Install

$ gem install command_mapper

Gemfile

gem 'command_mapper', '~> 0.2'

gemspec

gemspec.add_dependency 'command_mapper', '~> 0.2'

License

Copyright (c) 2021-2022 Hal Brodigan

See {file:LICENSE.txt} for license information.