0.02
The project is in a healthy, maintained state
Command Mapper maps an external command's arguments to Class attributes to allow safely and securely executing commands.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 2.0
 Project Readme

command_mapper

CI Code Climate Gem Version

Description

Command Mapper maps an external 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
      • Dec: decimal values
      • Hex: hexadecimal values
      • Map: maps Ruby values to other String values.
        • Map::YesNo: maps true/false to yes/no.
        • Map::EnabledDisabled: Maps true/false to enabled/disabled.
      • 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

Overriding the command name:

Grep.run(..., command_name: 'grep2')

Specifying the direct path to the command:

Grep.run(..., command_path: '/path/to/grep')

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: 
# ...

Defining sub-commands

module Git
  class Command < CommandMapper::Command

    command 'git' do
      option "--version"
      option "--help"
      option "-C", name: :dir, value: {type: InputDir.new}
      # ...

      subcommand :clone do
        option "--bare"
        option "--mirror"
        option "--depth", value: {type: Num.new}
        # ...

        argument :repository
        argument :directory, required: false
      end

      # ...
    end

  end
end

Invoking sub-commands

Git::Command.run(clone: {repository: 'https://github.com/user/repo.git'})

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

Real-World Examples

Requirements

Install

$ gem install command_mapper

Gemfile

gem 'command_mapper', '~> 0.2'

gemspec

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

Alternatives

License

Copyright (c) 2021-2022 Hal Brodigan

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