Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
OptionParser based CLI support for rapid CLI development in an object-oriented context.
This library wraps Ruby's OptionParser for parsing your options under the hood, so you get all the goodness that the Ruby standard library provides.
On top of that it adds a rich and powerful DSL for defining, validating, and
normalizing options, as well as automatic and gorgeous help output (modeled
after gem --help
).
Further documentation is available on rubydoc.info
Examples in this README are included from examples/readme. More examples can be found in examples. All examples are guaranteed to be up to date by the way of being verified on CI.
module Owners
class Add < Cl::Cmd
register :add
summary 'Add one or more owners to an existing owner group'
description <<~str
Use this command to add one or more owners to an existing
owner group.
[...]
str
args :owner
opt '-t', '--to TO', 'An existing owner group'
def run
# implement adding the owner as given in `owner` (as well as `args`)
# to the group given in `to` (as well as `opts[:to]`).
p owner: owner, to: to, to?: to?, args: args, opts: opts
end
end
end
# Running this, e.g. using `bin/owners add one,two --to group` will instantiate the
# class `Owners::Add`, and call the method `run` on it.
# e.g. bin/owners
#
# args normally would be ARGV
args = %w(add one --to group)
Cl.new('owners').run(args)
# Output:
#
# {:owner=>"one", :to=>"group", :to?=>true, :args=>["one"], :opts=>{:to=>"group"}}
Cl.new('owners').run(%w(add --help))
# Output:
#
# Usage: owners add [owner] [options]
#
# Summary:
#
# Add one or more owners to an existing owner group
#
# Description:
#
# Use this command to add one or more owners to an existing
# owner group.
#
# [...]
#
# Arguments:
#
# owner type: string
#
# Options:
#
# -t --to TO An existing owner group (type: string)
# --help Get help on this command
Commands are Ruby classes that extend the class Cl::Cmd
.
They register to a Ruby class registry in order to decouple looking up command classes from their Ruby namespace.
For example:
module Cmd
class One < Cl::Cmd
register :one
end
class Two < Cl::Cmd
register :two
end
end
p Cl::Cmd[:one] # => Cmd::One
p Cl::Cmd[:two] # => Cmd::Two
Commands can be registered like so:
module One
class Cmd < Cl::Cmd
register :'cmd:one'
end
end
By default commands auto register themselves with the underscored name of the last part of their class name (as seen in the example above). It is possible to turn this off using:
Cl::Cmd.auto_register = false
Command auto-registration can cause name clashes when namespaced commands have the same demodulized class name. For example:
class Git < Cl::Cmd
# auto-registers as :git
end
module Heroku
class Git < Cl::Cmd
# also auto-registers as :git
end
end
It is recommended to turn auto-registration off when using such module structures.
Runners lookup the command to execute from the registry, by checking the arguments given by the user for registered command keys.
With the two command classes One
and Two
from the example above (and
assuming that the executable that calls Cl
is bin/run
) the default runner
would recognize and run the following commands:
$ bin/run one something else
# instantiates One, passing the args array `["something", "else"]`, and calls the instance method `run`
$ bin/run two
# instantiates Two, passing an empty args arry `[]`, and calls the instance method `run`
The default runner also supports nested namespaces, and checks for command classes with keys separated by colons. For instance:
module Git
class Pull < Cl::Cmd
register :'git:pull'
arg :branch
def run
p cmd: registry_key, args: args
end
end
end
# With this class registered (and assuming the executable that calls `Cl` is
# `bin/run`) the default runner would recognize and run it:
#
# $ bin/run git:pull master # instantiates Git::Pull, and passes ["master"] as args
# $ bin/run git pull master # does the same
Cl.new('run').run(%w(git:pull master))
# Output:
#
# {:cmd=>:"git:pull", :args=>["master"]}
Cl.new('run').run(%w(git pull master))
# Output:
#
# {:cmd=>:"git:pull", :args=>["master"]}
Runners are registered on the module Cl::Runner
. It is possible to register custom
runners, and use them by passing the option runner
to Cl.new
:
module Git
class Pull < Cl::Cmd
register :'git:pull'
arg :branch
def run
p cmd: registry_key, args: args
end
end
end
# With this class registered (and assuming the executable that calls `Cl` is
# `bin/run`) the default runner would recognize and run it:
#
# $ bin/run git:pull master # instantiates Git::Pull, and passes ["master"] as args
# $ bin/run git pull master # does the same
Cl.new('run').run(%w(git:pull master))
# Output:
#
# {:cmd=>:"git:pull", :args=>["master"]}
Cl.new('run').run(%w(git pull master))
# Output:
#
# {:cmd=>:"git:pull", :args=>["master"]}
See Cl::Runner::Default
for more details.
There also is an experimental runner :multi
, which supports rake-style
execution of multiple commands at once, like so:
bin/rake db:drop production -f db:create db:migrate production -v 1
See the example rakeish for more details.
The DSL is defined on the class body.
Commands are classes that are derived from the base class Cl::Cmd
.
The description, summary, and examples are used in the help output.
module Owners
class Add < Cl::Cmd
register :add
summary 'Add one or more owners to an existing owner group'
description <<~str
Use this command to add one or more owners to an existing
owner group.
str
examples <<~str
Adding a single user to the group admins:
owners add user --to admins
Adding a several users at once:
owners add one two three --to admins
str
end
end
Cl.new('owners').run(%w(add --help))
# Output:
#
# Usage: owners add [options]
#
# Summary:
#
# Add one or more owners to an existing owner group
#
# Description:
#
# Use this command to add one or more owners to an existing
# owner group.
#
# Options:
#
# --help Get help on this command
#
# Examples:
#
# Adding a single user to the group admins:
#
# owners add user --to admins
#
# Adding a several users at once:
#
# owners add one two three --to admins
Command base classes can be declared abstract in order to prevent them from being identified as a runnable command and to omit them from help output.
This is only relevant if a command base class is registered. See Command Registry for details.
class Base < Cl::Cmd
abstract
end
class Add < Base
register :add
def run
puts 'Success'
end
end
Cl.new('owners').run(%w(add))
# Output:
#
# Success
Cl.new('owners').run(%w(base))
# Output:
#
# Unknown command: base
Arguments can be declared like so:
arg :arg_name, description: 'arg description', type: :[array|string|integer|float|boolean]
This will define an attr_accessor
on the Cmd
class. I.e. in the following
example the method ownsers
will be available on the Cmd
instance:
class Add < Cl::Cmd
register :add
arg :owner
def run
p owner: owner
end
end
Cl.new('owners').run(%w(add one))
# Output:
#
# {:owner=>"one"}
Arguments can have a type. Known types are: :array
, :string
, :integer
,
:float
, :boolean
.
The type :array
makes sure the argument accessible on the Cmd
instance is a
Ruby Array. (This currently only supports arrays of strings).
If the option sep
is given on the argument, then the argument value is split
using this separator.
class Add < Cl::Cmd
register :add
arg :owners, type: :array, sep: ','
def run
p owners: owners
end
end
Cl.new('owners').run(%w(add one,two))
# Output:
#
# {:owners=>["one", "two"]}
Other types cast the given argument to the expected Ruby type.
class Cmd < Cl::Cmd
register :cmd
arg :one, type: :integer
arg :two, type: :float
arg :three, type: :boolean
def run
p [one.class, two.class, three.class]
end
end
Cl.new('owners').run(%w(cmd 1 2.1 yes))
# Output:
#
# [Integer, Float, TrueClass]
Array arguments support splats, modeled after Ruby argument splats.
For example:
class Lft < Cl::Cmd
register :lft
arg :a, type: :array, splat: true
arg :b
arg :c
def run
p [a, b, c]
end
end
class Mid < Cl::Cmd
register :mid
arg :a
arg :b, type: :array, splat: true
arg :c
def run
p [a, b, c]
end
end
class Rgt < Cl::Cmd
register :rgt
arg :a
arg :b
arg :c, type: :array, splat: true
def run
p [a, b, c]
end
end
Cl.new('splat').run(%w(lft 1 2 3 4 5))
# Output:
#
# [["1", "2", "3"], "4", "5"]
Cl.new('splat').run(%w(mid 1 2 3 4 5))
# Output:
#
# ["1", ["2", "3", "4"], "5"]
Cl.new('splat').run(%w(rgt 1 2 3 4 5))
# Output:
#
# ["1", "2", ["3", "4", "5"]]
Declaring options can be done by calling the method opt
on the class body.
This will add the option, if given by the user, to the hash opts
on the Cmd
instance.
It also defines a reader method that returns the respective value from the opts
hash,
and a predicate that will be true if the option has been given.
For example:
class Add < Cl::Cmd
register :add
opt '--to GROUP', 'Target group to add owners to'
def run
p opts: opts, to: to, to?: to?
end
end
Cl.new('owners').run(%w(add --to one))
# Output:
#
# {:opts=>{:to=>"one"}, :to=>"one", :to?=>true}
Cl.new('owners').run(%w(add --help))
# Output:
#
# Usage: owners add [options]
#
# Options:
#
# --to GROUP Target group to add owners to (type: string)
# --help Get help on this command
Options optionally accept a block in case custom normalization is needed.
Depending on the block's arity the following arguments are passed to the block: option value, option name, option type, collection of all options defined on the command.
class Add < Cl::Cmd
register :add
# depending on its arity the block can receive:
#
# * value
# * value, name
# * value, name, type
# * value, name, type, opts
opt '--to GROUP' do |value|
opts[:to] = "group-#{value}"
end
def run
p to: to
end
end
Cl.new('owners').run(%w(add --to one))
# Output:
#
# {:to=>"group-one"}
Options can have one or many alias names, given as a Symbol or Array of Symbols:
class Add < Cl::Cmd
register :add
opt '--to GROUP', alias: :group
def run
# p opts: opts, to: to, to?: to?, group: group, group?: group?
p opts: opts, to: to, to?: to?
end
end
Cl.new('owners').run(%w(add --group one))
# Output:
#
# {:opts=>{:to=>"one", :group=>"one"}, :to=>"one", :to?=>true}
Options can have a default value.
I.e. this value is going to be used if the user does not provide the option:
class Add < Cl::Cmd
register :add
opt '--to GROUP', default: 'default'
def run
p to: to
end
end
Cl.new('owners').run(%w(add))
# Output:
#
# {:to=>"default"}
Options, and option alias name can be deprecated.
For a deprecated option:
class Add < Cl::Cmd
register :add
opt '--target GROUP', deprecated: 'Deprecated.'
def run
p target: target, deprecations: deprecations
end
end
Cl.new('owners').run(%w(add --target one))
# Output:
#
# {:target=>"one", :deprecations=>{:target=>"Deprecated."}}
For a deprecated option alias name:
class Add < Cl::Cmd
register :add
opt '--to GROUP', alias: :target, deprecated: :target
def run
p to: to, deprecations: deprecations
end
end
Cl.new('owners').run(%w(add --target one))
# Output:
#
# {:to=>"one", :deprecations=>{:target=>:to}}
Options can be declared to be downcased.
For example:
class Add < Cl::Cmd
register :add
opt '--to GROUP', downcase: true
def run
p to: to
end
end
Cl.new('owners').run(%w(add --to ONE))
# Output:
#
# {:to=>"one"}
Options can be enums (i.e. have known values).
If an unknown values is given by the user the parser will reject the option, and print the help output for this command.
For example:
class Add < Cl::Cmd
register :add
opt '--to GROUP', enum: %w(one two)
def run
p to: to
end
end
Cl.new('owners').run(%w(add --to one))
# Output:
#
# {:to=>"one"}
Cl.new('owners').run(%w(add --to unknown))
# Output:
#
# Unknown value: to=unknown (known values: one, two)
#
# Usage: owners add [options]
#
# Options:
#
# --to GROUP type: string, known values: one, two
# --help Get help on this command
Options can have examples that will be printed in the help output.
class Add < Cl::Cmd
register :add
opt '--to GROUP', example: 'group-one'
end
Cl.new('owners').run(%w(add --help))
# Output:
#
# Usage: owners add [options]
#
# Options:
#
# --to GROUP type: string, e.g.: group-one
# --help Get help on this command
Options can have a required format.
If a value is given by the user that does not match the format then the parser will reject the option, and print the help output for this command.
For example:
class Add < Cl::Cmd
register :add
opt '--to GROUP', format: /^\w+$/
def run
p to: to
end
end
Cl.new('owners').run(%w(add --to one))
# Output:
#
# {:to=>"one"}
Cl.new('owners').run(['add', '--to', 'does not match!'])
# Output:
#
# Invalid format: to (format: /^\w+$/)
#
# Usage: owners add [options]
#
# Options:
#
# --to GROUP type: string, format: /^\w+$/
# --help Get help on this command
Options can be declared to be internal, hiding the option from the help output.
For example:
class Add < Cl::Cmd
register :add
opt '--to GROUP'
opt '--hidden', internal: true
end
Cl.new('owners').run(%w(add --help))
# Output:
#
# Usage: owners add [options]
#
# Options:
#
# --to GROUP type: string
# --help Get help on this command
Options can have mininum and/or maximum values.
If a value is given by the user that does not match the required min and/or max values then the parser will reject the option, and print the help output for this command.
For example:
class Add < Cl::Cmd
register :add
opt '--retries COUNT', type: :integer, min: 1, max: 5
def run
p retries: retries
end
end
Cl.new('owners').run(%w(add --retries 1))
# Output:
#
# {:retries=>1}
Cl.new('owners').run(%w(add --retries 10))
# Output:
#
# Out of range: retries (min: 1, max: 5)
#
# Usage: owners add [options]
#
# Options:
#
# --retries COUNT type: integer, min: 1, max: 5
# --help Get help on this command
Flags (boolean options) automatically allow negation using --no-*
and
--no_*
using OptionParser's support for these. However, sometimes it can be
convenient to allow other terms for negating an option. Flags therefore accept
an option negate
like so:
class Add < Cl::Cmd
register :add
opt '--notifications', 'Send out notifications to the team', negate: %w(skip)
def run
p notifications?
end
end
Cl.new('owners').run(%w(add --notifications))
# Output:
#
# true
Cl.new('owners').run(%w(add --no_notifications))
# Output:
#
# false
Cl.new('owners').run(%w(add --no-notifications))
# Output:
#
# false
Cl.new('owners').run(%w(add --skip_notifications))
# Output:
#
# false
Cl.new('owners').run(%w(add --skip-notifications))
# Output:
#
# false
Options can have a note that will be printed in the help output.
class Add < Cl::Cmd
register :add
opt '--to GROUP', note: 'needs to be a group'
end
Cl.new('owners').run(%w(add --help))
# Output:
#
# Usage: owners add [options]
#
# Options:
#
# --to GROUP type: string, note: needs to be a group
# --help Get help on this command
Options can be declared as secret.
This makes it possible for client code to inspect if a given option is secret. Also, option values given by the user will be tainted, so client code can rely on this in order to, for example, obfuscate values from log output.
class Add < Cl::Cmd
register :add
opt '--pass PASS', secret: true
def run
p(
secret?: self.class.opts[:pass].secret?,
tainted?: pass
)
end
end
Cl.new('owners').run(%w(add --pass pass))
# Output:
#
# {:secret?=>true, :tainted?=>true}
Options can refer to documentation using the see
option. This will be printed
in the help output.
For example:
class Add < Cl::Cmd
register :add
opt '--to GROUP', see: 'https://docs.io/cli/owners/add'
end
Cl.new('owners').run(%w(add --help))
# Output:
#
# Usage: owners add [options]
#
# Options:
#
# --to GROUP type: string, see: https://docs.io/cli/owners/add
# --help Get help on this command
Options can have a type. Known types are: :array
, :string
, :integer
,
:float
, :boolean
.
The type :array
allows an option to be given multiple times, and makes sure
the value accessible on the Cmd
instance is a Ruby Array. (This currently
only supports arrays of strings).
class Add < Cl::Cmd
register :add
opt '--to GROUP', type: :array
def run
p to
end
end
Cl.new('owners').run(%w(add --to one --to two))
# Output:
#
# ["one", "two"]
Other types cast the given value to the expected Ruby type.
class Add < Cl::Cmd
register :add
opt '--active BOOL', type: :boolean
opt '--retries INT', type: :integer
opt '--sleep FLOAT', type: :float
def run
p active: active.class, retries: retries.class, sleep: sleep.class
end
end
Cl.new('owners').run(%w(add --active yes --retries 1 --sleep 0.1))
# Output:
#
# {:active=>TrueClass, :retries=>Integer, :sleep=>Float}
There are three ways options can be required:
required: true
on the option: the option itself is required to be givenrequires: :other
on the option: the option requires another option to be givenrequired :one, [:two, :three]
on the class: either one
or both two
and three
must be givenFor example, this simply requires the option --to
:
class Add < Cl::Cmd
register :add
opt '--to GROUP', required: true
def run
p to: to
end
end
Cl.new('owners').run(%w(add --to one))
# Output:
#
# {:to=>"one"}
Cl.new('owners').run(%w(add))
# Output:
#
# Missing required option: to
#
# Usage: owners add [options]
#
# Options:
#
# --to GROUP type: string, required
# --help Get help on this command
This will make the option --retries
depend on the option --to
:
class Add < Cl::Cmd
register :add
opt '--to GROUP'
opt '--other GROUP', requires: :to
def run
p to: to, other: other
end
end
Cl.new('owners').run(%w(add --to one --other two))
# Output:
#
# {:to=>"one", :other=>"two"}
Cl.new('owners').run(%w(add --other two))
# Output:
#
# Missing option: to (required by other)
#
# Usage: owners add [options]
#
# Options:
#
# --to GROUP type: string
# --other GROUP type: string, requires: to
# --help Get help on this command
This requires either the option --api_key
or both options --username
and
--password
to be given:
class Add < Cl::Cmd
register :add
# read DNF, i.e. "token OR user AND pass
required :token, [:user, :pass]
opt '--token TOKEN'
opt '--user NAME'
opt '--pass PASS'
def run
p token: token, user: user, pass: pass
end
end
Cl.new('owners').run(%w(add --token token))
# Output:
#
# {:token=>"token", :user=>nil, :pass=>nil}
Cl.new('owners').run(%w(add --user user --pass pass))
# Output:
#
# {:token=>nil, :user=>"user", :pass=>"pass"}
Cl.new('owners').run(%w(add))
# Output:
#
# Missing options: token, or user and pass
#
# Usage: owners add [options]
#
# Options:
#
# Either token, or user and pass are required.
#
# --token TOKEN type: string
# --user NAME type: string
# --pass PASS type: string
# --help Get help on this command
Cl automatically reads config files that match the given executable name (inspired by gem-release), stored either in the user directory or the current working directory.
For example:
module Api
class Login < Cl::Cmd
opt '--username USER'
opt '--password PASS'
end
end
# bin/api
CL.new('api').run(ARGV)
# ~/api.yml
login:
username: 'someone'
password: 'password'
# ./api.yml
login:
username: 'someone else'
then running
$ bin/api login
instantiates Api::Login
, and passes the hash
{ username: 'someone else', password: 'password' }
as opts
.
Options passed by the user take precedence over defaults defined in config files.
Cl automatically defaults options to environment variables that are prefixed with the given executable name (inspired by gem-release).
module Api
class Login < Cl::Cmd
opt '--username USER'
opt '--password PASS'
end
end
# bin/api
CL.new('api').run(ARGV)
then running
$ API_USERNAME=someone API_PASSWORD=password bin/api login
instantiates Api::Login
, and passes the hash
{ username: 'someone', password: 'password' }
Options passed by the user take precedence over defaults given as environment variables, and environment variables take precedence over defaults defined in config files.
FAQs
Unknown package
We found that travis-cl demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.