Skip to content

Repository files navigation

command_mapper

CICode ClimateGem 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#classGrep < CommandMapper::Commandcommand"grep"dooption"--extended-regexp"option"--fixed-strings"option"--basic-regexp"option"--perl-regexp"option"--regexp",equals: true,value: trueoption"--file",name: :patterns_file,equals: true,value: trueoption"--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: trueoption"--only-matching"option"--quiet"option"--binary-files",equals: true,value: trueoption"--text"option"-I",name: # FIXME: nameoption"--directories",equals: true,value: trueoption"--devices",equals: true,value: trueoption"--recursive"option"--dereference-recursive"option"--include",equals: true,value: trueoption"--exclude",equals: true,value: trueoption"--exclude-from",equals: true,value: trueoption"--exclude-dir",equals: true,value: trueoption"--files-without-match",value: trueoption"--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: trueoption"--no-group-separator"option"--color",equals: :optional,value: {required: false}option"--colour",equals: :optional,value: {required: false}option"--binary"argument:patternsargument:file,required: false,repeats: trueendend

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:

deffoo@foo || @barenddeffoo=(value)@foo=casevaluewhenHashthen ...
whenArraythen ...
elsevalue.to_sendend

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:

deffoo@foo || @barenddeffoo=(value)@foo=casevaluewhenHashthen ...
whenArraythen ...
elsevalue.to_sendend

Custom Types

classPortRange < CommandMapper::Types::Typedefvalidate(value)casevaluewhenIntegertruewhenRangeifvalue.begin.kind_of?(Integer)trueelse[false,"port range can only contain Integers"]endelse[false,"port range must be an Integer or a Range of Integers"]endenddefformat(value)casevaluewhenInteger"#{value}"whenRange"#{value.begin}-#{value.end}"endendendoption:ports,value: {required: true,type: PortRange.new}

Running

Keyword arguments:

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

With a block:

Grep.rundo |grep|
grep.ignore_case=truegrep.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_linedo |line|
# ...end

sudo

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

Defining sub-commands

moduleGitclassCommand < CommandMapper::Commandcommand'git'dooption"--version"option"--help"option"-C",name: :dir,value: {type: InputDir.new}# ...subcommand:clonedooption"--bare"option"--mirror"option"--depth",value: {type: Num.new}# ...argument:repositoryargument:directory,required: falseend# ...endendend

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.

About

Maps external commands and options to Ruby classes

Topics

Resources

Stars

56 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages