Coder Social home page Coder Social logo

cri's Introduction

Cri

Gem Gem downloads

Cri is a library for building easy-to-use command-line tools with support for nested commands.

Requirements

Cri requires Ruby 2.6 or newer (including Ruby 3.x).

Compatibility policy

Cri is guaranteed to be supported on any officially supported Ruby version, as well as the version of Ruby that comes by default on

Usage

The central concept in Cri is the command, which has option definitions as well as code for actually executing itself. In Cri, the command-line tool itself is a command as well.

Here’s a sample command definition:

command = Cri::Command.define do
  name        'dostuff'
  usage       'dostuff [options]'
  aliases     :ds, :stuff
  summary     'does stuff'
  description 'This command does a lot of stuff. I really mean a lot.'

  flag   :h,  :help,  'show help for this command' do |value, cmd|
    puts cmd.help
    exit 0
  end
  flag   nil, :more,  'do even more stuff'
  option :s,  :stuff, 'specify stuff to do', argument: :required

  run do |opts, args, cmd|
    stuff = opts.fetch(:stuff, 'generic stuff')
    puts "Doing #{stuff}!"

    if opts[:more]
      puts 'Doing it even more!'
    end
  end
end

To run this command, invoke the #run method with the raw arguments. For example, for a root command (the command-line tool itself), the command could be called like this:

command.run(ARGV)

Each command has automatically generated help. This help can be printed using Cri::Command#help; something like this will be shown:

usage: dostuff [options]

does stuff

    This command does a lot of stuff. I really mean a lot.

options:

    -h --help      show help for this command
       --more      do even more stuff
    -s --stuff     specify stuff to do

General command metadata

Let’s disect the command definition and start with the first five lines:

name        'dostuff'
usage       'dostuff [options]'
aliases     :ds, :stuff
summary     'does stuff'
description 'This command does a lot of stuff. I really mean a lot.'

These lines of the command definition specify the name of the command (or the command-line tool, if the command is the root command), the usage, a list of aliases that can be used to call this command, a one-line summary and a (long) description. The usage should not include a “usage:” prefix nor the name of the supercommand, because the latter will be automatically prepended.

Aliases don’t make sense for root commands, but for subcommands they do.

Command-line options

The next few lines contain the command’s option definitions:

flag   :h,  :help,  'show help for this command' do |value, cmd|
  puts cmd.help
  exit 0
end
flag   nil, :more,  'do even more stuff'
option :s,  :stuff, 'specify stuff to do', argument: :required

The most generic way of definition an option is using either #option or #opt. It takes the following arguments:

  1. a short option name
  2. a long option name
  3. a description
  4. optional extra parameters
    • argument: (default: :forbidden)
    • transform:
    • default:
    • multiple: (default: false)
  5. optionally, a block

In more detail:

  • The short option name is a symbol containing one character, to be used in single-dash options, e.g. :f (corresponds to -f). The long option name is a symbol containing a string, to be used in double-dash options, e.g. :force (corresponds to --force). Either the short or the long option name can be nil, but not both.

  • The description is a short, one-line text that shows up in the command’s help. For example, the -v/--version option might have the description show version information and quit.

  • The extra parameters, argument:, multiple:, default:, and transform:, are described in the sections below.

  • The block, if given, will be executed when the option is found. The arguments to the block are the option value (true in case the option does not have an argument) and the command.

There are several convenience methods that are alternatives to #option/#opt:

  • #flag sets argument: to :forbidden
  • (deprecated) #required sets argument: to :required -- deprecated because #required suggests that the option is required, wich is incorrect; the argument is required.)
  • (deprecated) #optional sets argument: to :optional -- deprecated because #optional looks too similar to #option.

Forbidden, required, and optional arguments (argument:)

The :argument parameter can be set to :forbidden, :required, or :optional.

  • :forbidden means that when the option is present, the value will be set to true, and false otherwise. For example:

    option :f, :force, 'push with force', argument: :forbidden
    
    run do |opts, args, cmd|
      puts "Force? #{opts[:force]}"
    end
    % ./push mypackage.zip
    Force? false
    
    % ./push --force mypackage.zip
    Force? true

    :argument is set to :forbidden by default.

  • :required means that the option must be followed by an argument, which will then be treated as the value for the option. It does not mean that the option itself is required. For example:

    option :o, :output, 'specify output file', argument: :required
    option :f, :fast, 'fetch faster', argument: :forbidden
    
    run do |opts, args, cmd|
      puts "Output file: #{opts[:output]}"
    end
    % ./fetch http://example.com/source.zip
    Output file: nil
    
    % ./fetch --output example.zip http://example.com/source.zip
    Output file: example.zip
    
    % ./fetch http://example.com/source.zip --output
    fetch: option requires an argument -- output
    
    % ./fetch --output --fast http://example.com/source.zip
    fetch: option requires an argument -- output
  • :optional means that the option can be followed by an argument. If it is, then the argument is treated as the value for the option; if it isn’t, the value for the option will be true. For example:

    option :o, :output, 'specify output file', argument: :optional
    option :f, :fast, 'fetch faster', argument: :forbidden
    
    run do |opts, args, cmd|
      puts "Output file: #{opts[:output]}"
    end
    % ./fetch http://example.com/source.zip
    Output file: nil
    
    % ./fetch --output example.zip http://example.com/source.zip
    Output file: example.zip
    
    % ./fetch http://example.com/source.zip --output
    Output file: true
    
    % ./fetch --output --fast http://example.com/source.zip
    Output file: true

Transforming options (transform:)

The :transform parameter specifies how the value should be transformed. It takes any object that responds to #call:

option :p, :port, 'set port', argument: :required,
  transform: -> (x) { Integer(x) }

The following example uses #Integer to transform a string into an integer:

option :p, :port, 'set port', argument: :required, transform: method(:Integer)

The following example uses a custom object to perform transformation, as well as validation:

class PortTransformer
  def call(str)
    raise ArgumentError unless str.is_a?(String)
    Integer(str).tap do |int|
      raise unless (0x0001..0xffff).include?(int)
    end
  end
end

option :p, :port, 'set port', argument: :required, transform: PortTransformer.new

Default values are not transformed:

option :p, :port, 'set port', argument: :required, default: 8080, transform: PortTransformer.new

Options with default values (default:)

The :default parameter sets the option value that will be used if the option is passed without an argument or isn't passed at all:

option :a, :animal, 'add animal', default: 'giraffe', argument: :optional

In the example above, the value for the --animal option will be the string "giraffe", unless otherwise specified:

OPTIONS
    -a --animal[=<value>]      add animal (default: giraffe)

If the option is not given on the command line, the options hash will not have key for this option, but will still have a default value:

option :a, :animal, 'add animal', default: 'giraffe', argument: :required

run do |opts, args, cmd|
  puts "Animal = #{opts[:animal]}"
  puts "Option given? #{opts.key?(:animal)}"
end
% ./run --animal=donkey
Animal = donkey
Option given? true

% ./run --animal=giraffe
Animal = giraffe
Option given? true

% ./run
Animal = giraffe
Option given? false

This can be useful to distinguish between an explicitly-passed-in value and a default value. In the example above, the animal option is set to giraffe in the second and third cases, but it is possible to detect whether the value is a default or not.

Multivalued options (multiple:)

The :multiple parameter allows an option to be specified more than once on the command line. When set to true, multiple option valus are accepted, and the option values will be stored in an array.

For example, to parse the command line options string -o foo.txt -o bar.txt into an array, so that options[:output] contains [ 'foo.txt', 'bar.txt' ], you can use an option definition like this:

option :o, :output, 'specify output paths', argument: :required, multiple: true

This can also be used for flags (options without arguments). In this case, the length of the options array is relevant.

For example, you can allow setting the verbosity level using -v -v -v. The value of options[:verbose].size would then be the verbosity level (three in this example). The option definition would then look like this:

flag :v, :verbose, 'be verbose (use up to three times)', multiple: true

Skipping option parsing

If you want to skip option parsing for your command or subcommand, you can add the skip_option_parsing method to your command definition and everything on your command line after the command name will be passed to your command as arguments.

command = Cri::Command.define do
  name        'dostuff'
  usage       'dostuff [args]'
  aliases     :ds, :stuff
  summary     'does stuff'
  description 'This command does a lot of stuff, but not option parsing.'

  skip_option_parsing

  run do |opts, args, cmd|
    puts args.inspect
  end
end

When executing this command with dostuff --some=value -f yes, the opts hash that is passed to your run block will be empty and the args array will be ["--some=value", "-f", "yes"].

Argument parsing

Cri supports parsing arguments, as well as parsing options. To define the parameters of a command, use #param, which takes a symbol containing the name of the parameter. For example:

command = Cri::Command.define do
  name        'publish'
  usage       'publish filename'
  summary     'publishes the given file'
  description 'This command does a lot of stuff, but not option parsing.'

  flag :q, :quick, 'publish quicker'
  param :filename

  run do |opts, args, cmd|
    puts "Publishing #{args[:filename]}…"
  end
end

The command in this example has one parameter named filename. This means that the command takes a single argument, named filename.

As with options, parameter definitions take transform:, which can be used for transforming and validating arguments:

param :port, transform: method(:Integer)

(Why the distinction between argument and parameter? A parameter is a name, e.g. filename, while an argument is a value for a parameter, e.g. kitten.jpg.)

Allowing arbitrary arguments

If no parameters are specified, Cri performs no argument parsing or validation; any number of arguments is allowed.

command = Cri::Command.define do
  name        'publish'
  usage       'publish [filename...]'
  summary     'publishes the given file(s)'
  description 'This command does a lot of stuff, but not option parsing.'

  flag :q, :quick, 'publish quicker'

  run do |opts, args, cmd|
    args.each do |arg|
      puts "Publishing #{arg}…"
    end
  end
end
% my-tool publish foo.zip bar.zip
Publishing foo.zip…
Publishing bar.zip…
%

Forbidding any arguments

To explicitly specify that a command has no parameters, use #no_params:

name        'reset'
usage       'reset'
summary     'resets the site'
description '…'
no_params

run do |opts, args, cmd|
  puts "Resetting…"
end
% my-tool reset x
reset: incorrect number of arguments given: expected 0, but got 1
% my-tool reset
Resetting…
%

A future version of Cri will likely make #no_params the default behavior.

The run block

The last part of the command defines the execution itself:

run do |opts, args, cmd|
  stuff = opts.fetch(:stuff, 'generic stuff')
  puts "Doing #{stuff}!"

  if opts[:more]
    puts 'Doing it even more!'
  end
end

The +Cri::CommandDSL#run+ method takes a block with the actual code to execute. This block takes three arguments: the options, any arguments passed to the command, and the command itself.

The command runner

Instead of defining a run block, it is possible to declare a class, the command runner class that will perform the actual execution of the command. This makes it easier to break up large run blocks into manageable pieces.

name 'push'
option :f, :force, 'force'
param :filename

class MyRunner < Cri::CommandRunner
  def run
    puts "Pushing #{arguments[:filename]}…"
    puts "… with force!" if options[:force]
  end
end

runner MyRunner

To create a command runner, subclass Cri::CommandRunner, and define a #run method with no params. Inside the #run block, you can access options and arguments. Lastly, to connect the command to the command runner, call #runner with the class of the command runner.

Here is an example interaction with the example command, defined above:

% push
push: incorrect number of arguments given: expected 1, but got 0

% push a
Pushing a…

% push -f
push: incorrect number of arguments given: expected 1, but got 0

% push -f a
Pushing a…
… with force!

Subcommands

Commands can have subcommands. For example, the git command-line tool would be represented by a command that has subcommands named commit, add, and so on. Commands with subcommands do not use a run block; execution will always be dispatched to a subcommand (or none, if no subcommand is found).

To add a command as a subcommand to another command, use the Cri::Command#add_command method, like this:

root_cmd.add_command(cmd_add)
root_cmd.add_command(cmd_commit)
root_cmd.add_command(cmd_init)

You can also define a subcommand on the fly without creating a class first using Cri::Command#define_command (name can be skipped if you set it inside the block instead):

root_cmd.define_command('add') do
  # option ...
  run do |opts, args, cmd|
    # ...
  end
end

You can specify a default subcommand. This subcommand will be executed when the command has subcommands, and no subcommands are otherwise explicitly specified:

default_subcommand 'compile'

Loading commands from separate files

You can use Cri::Command.load_file to load a command from a file.

For example, given the file commands/check.rb with the following contents:

name        'check'
usage       'check'
summary     'runs all checks'
description '…'

run do |opts, args, cmd|
  puts "Running checks…"
end

To load this command:

Cri::Command.load_file('commands/check.rb')

Cri::Command.load_file expects the file to be in UTF-8.

You can also use it to load subcommands:

root_cmd = Cri::Command.load_file('commands/nanoc.rb')
root_cmd.add_command(Cri::Command.load_file('commands/comile.rb'))
root_cmd.add_command(Cri::Command.load_file('commands/view.rb'))
root_cmd.add_command(Cri::Command.load_file('commands/check.rb'))

Automatically inferring command names

Pass infer_name: true to Cri::Command.load_file to use the file basename as the name of the command.

For example, given a file commands/check.rb with the following contents:

usage       'check'
summary     'runs all checks'
description '…'

run do |opts, args, cmd|
  puts "Running checks…"
end

To load this command and infer the name:

cmd = Cri::Command.load_file('commands/check.rb', infer_name: true)

cmd.name will be check, derived from the filename.

Contributors

  • Bart Mesuere
  • Ken Coar
  • Tim Sharpe
  • Toon Willems

Thanks for Lee “injekt” Jarvis for Slop, which has inspired the design of Cri 2.0.

cri's People

Contributors

bastelfreak avatar bmesuere avatar boutil avatar denisdefreyne avatar mackuba avatar marcandre avatar rodjek avatar rous avatar rrrene avatar vstone avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

cri's Issues

FREQ: a #skip_subcommand_parsing method to allow options but arbitrary parameters

The #skip_option_parsing is good as far as it goes, but (IMHO) it addresses only half of the issue.

As far as I can tell, there is no way to define a command that will accept ab arbitrary number of arbitrary parameters without trying to treat them as subcommands. For examine, I don't see any way to do this:

configure set -v user=foo server=http://example.com/

because it gritches about user=foo not being a valid subcommand.

Is there a trick to this, or is it actually not currently supported?

Thanks!

Cri::CommandRunner usage understanding

Is there some extended example/documentation how to use Cri::CommandRunner? I want to build a command in a class.
It clear it will call run. But where arguments coming from? How this class can be loaded/declared?

Thanks in advance.

Kirby

Wrong empty argument group check

Since -- is used to separate argument groups and stop option parsing, using both at the same time can lead to conflicting requirements.

Imagine having rm -f -- -blah to delete the file “-blah”. The -- stops option parsing and starts a new argument group. Currently, cri only creates a new argument group if the previous one is not empty. So the argument groups in this case will be

[
  [ '-blah' ]
]

which is expected and good.

However, in the case of git checkout -- blah, the argument_groups will be

[
  [ 'blah' ]
]

and not, as expected,

[
  [],
  [ 'blah' ],
]

The latter is necessary, because there is no way to disambiguate the blah argument otherwise.

A solution would be to remove the empty argument group check.

CC @bobthecow

Display help for sub-commands that require an argument?

Is it possible to make sub-commands accept -h even if the wrong number of arguments to it are provided? Right now I just get the wrong number of arguments, but -h/--help should be there to help them find out what arguments are needed I would think?

Allow the use of single newlines in help output

Due to the text reflow in the help output, single newlines are ignored, double newlines are used to indicate the start of a new paragraph and paragraphs are joined with a double newline. This means it's not possible to use a line break in the description without introducing an empty line. When using lists, this results in ugly output:

This command has 3 features:

- awesome feature 1

- awesome feature 2

- awesome feature 3

We also have a lot of options.

preferred output:

This command has 3 features:
- awesome feature 1
- awesome feature 2
- awesome feature 3

We also have a lot of options.

iterating over arguments fails since 2.12

Due to changes introduced in cri 2.12, my way of iterating over arguments no longer works. (This might very well be a bug in my code and not in cri).

My code extending the commandRunner:

    # Returns an input iterator to use for the request.
    # - if arguments are given, uses arguments
    # - if the input file option is given, uses file input
    # - if none of the previous are given, uses stdin
    def input_iterator
      return arguments.each unless arguments.empty?
      return IO.foreach(options[:input]) if options[:input]
      $stdin.each_line
    end

This now throws a LocalJumpError, probably because the array was swapped for an argument_list in the underlying code:

LocalJumpError: no block given (yield)
    /Users/bart/.rvm/gems/ruby-2.6.0/gems/cri-2.12.0/lib/cri/argument_list.rb:39:in `block in each'
    /Users/bart/.rvm/gems/ruby-2.6.0/gems/cri-2.12.0/lib/cri/argument_list.rb:39:in `each'
    /Users/bart/.rvm/gems/ruby-2.6.0/gems/cri-2.12.0/lib/cri/argument_list.rb:39:in `each'
    /Users/bart/Code/Rails/unipept-cli/lib/commands/unipept/api_runner.rb:41:in `input_iterator'
    /Users/bart/Code/Rails/unipept-cli/lib/commands/unipept/api_runner.rb:116:in `run'
    /Users/bart/.rvm/gems/ruby-2.6.0/gems/cri-2.12.0/lib/cri/command_runner.rb:34:in `call'
    /Users/bart/.rvm/gems/ruby-2.6.0/gems/cri-2.12.0/lib/cri/command_dsl.rb:257:in `block in runner'
    /Users/bart/.rvm/gems/ruby-2.6.0/gems/cri-2.12.0/lib/cri/command.rb:337:in `run_this'
    /Users/bart/.rvm/gems/ruby-2.6.0/gems/cri-2.12.0/lib/cri/command.rb:275:in `run'
    /Users/bart/.rvm/gems/ruby-2.6.0/gems/cri-2.12.0/lib/cri/command.rb:293:in `run'

Do you have a suggestion on how to resolve this?

uninitialized constant Cri::Error

When running r10k puppetfile check, this library cast an exception
Version installed cri-2.13.0
Version working cri-2.12.0
r10k version installed 3.0.0

Traceback (most recent call last):
        13: from /usr/local/bin/r10k:23:in `<main>'
        12: from /usr/local/bin/r10k:23:in `load'
        11: from /var/lib/gems/2.5.0/gems/r10k-3.0.0/bin/r10k:3:in `<top (required)>'
        10: from /usr/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'
         9: from /usr/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'
         8: from /var/lib/gems/2.5.0/gems/r10k-3.0.0/lib/r10k/cli.rb:3:in `<top (required)>'
         7: from /usr/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'
         6: from /usr/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'
         5: from /var/lib/gems/2.5.0/gems/r10k-3.0.0/lib/r10k/cli/ext/logging.rb:1:in `<top (required)>'
         4: from /usr/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'
         3: from /usr/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'
         2: from /var/lib/gems/2.5.0/gems/cri-2.13.0/lib/cri/command_dsl.rb:3:in `<top (required)>'
         1: from /var/lib/gems/2.5.0/gems/cri-2.13.0/lib/cri/command_dsl.rb:6:in `<module:Cri>'
/var/lib/gems/2.5.0/gems/cri-2.13.0/lib/cri/command_dsl.rb:9:in `<class:CommandDSL>': uninitialized constant Cri::Error (NameError)
Did you mean?  IOError
               Errno

I am not sure if this is a r10k issue or cri issue.

Color output on windows

If the terminal doesn't support colored output, it should not be shown.

This is mostly a problem on Windows.

Allow splitting positional arguments by `--`

It would be super useful to be able to split positional arguments using a --. This is often used to disambiguate positional arguments, for example:

git checkout origin/master    # branch master on origin
git checkout -- origin/master # local file master in origin directory

In my case, I'd like to split arbitrary arguments:

mycommand foo bar -- baz -- qux

The ideal backwards-compatible way to implement this is probably to make the arguments list a subclass of Array, with an accessor for getting groups of arguments. In the above example:

mycommand.arguments
=> ['foo', 'bar', 'baz', 'qux']

mycommand.argument_groups
=> ['foo', 'bar'], ['baz'], ['qux']

Or something. I'm not 100% sold on the accessor name :)

Forbidden optional parameters set to ` nil` when not present in command line

Currently, forbidden optional parameters set to nil when not present in command line. From documentation, it is expected to return false

option :f, :force, 'push with force', argument: :forbidden

run do |opts, args, cmd|
  puts "Force? #{opts[:force].inspect}!"
end
% ./push
Force? nil!

% ./push --force
Force? true!

Allow `--help --verbose`

Currently, --help --verbose does not print the help verbosely, because both options are independent and do not know about each other.

Not sure how to solve this.

Is the argument: option required when using multiple: true

I'm trying to use multiple arguments and I have a option block that looks like this:

option :g, :groups, "specify the groups", argument: :required, multiple: true do |value, cmd|
  puts value
end

It works fine if argument: :required or argument: optional is present. If I remove that, value is just an array of true elements. Should argument: be present when using multiple: or is this a bug? I couldn't figure it out from the docs.

Allow array of names for options

I'd like to have a --force option with no shorter version, but I can't figure out how to do it.

I've had the desire to have multiple long-form names for an option, as well.

Both of these might be addressed by being able to pass the option names as an array, and

short_form = options.find { |o| o.to_s.length == 1 }
long_forms = options - [ short_form ]

or something like that.

Thanks!

Commands without `summary` does not even show command name

A simple command like this:

require 'cri'

c = Cri::Command.define do
  name 'test'
  #summary 'foo'

  run do |opts, args, cmd|
    puts cmd.help
  end
end

c.run(ARGV)

Will not output anything:

$ ruby wat.rb

Once I uncomment the summary call, then it starts showing. I understand that summaries are useful for the end-user, but it would be nice if it at least showed the command name (instead of a blank line).

Invalid syntax in command.rb

/usr/local/share/gems/gems/r10k-2.6.0.dev/lib/r10k/cli.rb:9:in `command': /usr/local/share/gems/gems/cri-2.9.1/lib/cri/command.rb:46: syntax error, unexpected ')' (SyntaxError)

def initialize(is_error:)

Consider making it clearer that --verbose is an option for the help subcommand itself

When using the basic help command, the way the --verbose option is currently presented can be a bit confusing for users:

$ mycommand help new

<snip>

OPTIONS FOR NEW
    -d --debug                     Enable debug output.
    -h --help                      Show help for this command.
    -v --verbose                   show more detailed help
       --version                   Show version of mycommand.

It would be clearer if --verbose was listed in it's own section labelled OPTIONS FOR HELP or something similar:

$ mycommand help new

<snip>

OPTIONS FOR NEW
    -d --debug                     Enable debug output.
    -h --help                      Show help for this command.

OPTIONS FOR HELP
    -v --verbose                   show more detailed help

Prevent options from clashing

Cri should provide a way to ensure that long and short options don’t clash between subcommand and supercommand.

Options propagation on subcommand tree

Having this command and subcommand configuration:

base_cmd = Cri::Command.define do
  name        'base_cmd'
  usage       'base_cmd [options]'
  summary     'does stuff'
  description 'This command does a lot of stuff. I really mean a lot.'
  flag   :a, :aaaa,  'do even more stuff'
end

level1_cmd = Cri::Command.define do
  name        'level1_cmd'
  usage       'level1_cmd [options]'
  summary     'does stuff'
  description 'This command does a lot of stuff. I really mean a lot.'
end

level2_cmd = Cri::Command.define do
  name        'level2_cmd'
  usage       'level2_cmd [options]'
  summary     'does stuff'
  description 'This command does a lot of stuff. I really mean a lot.'
  run do |opts, args, cmd|
    puts opts.to_s
  end
end

level1_cmd.add_command(level2_cmd)
base_cmd.add_command(level1_cmd)
base_cmd.run(ARGV)

Note that there is a flag a configured on the base command base_cmd and lowest level subcommand prints in stdout available opts

I find this behavior unexpected:

$ base_cmd -a level1_cmd level2_cmd
{}
$ base_cmd level1_cmd -a level2_cmd
{:aaaa=>true}
$ base_cmd level1_cmd level2_cmd -a
{:aaaa=>true}

Is this the expected behavior?

2.15.8 breaks nanoc

When using 2.15.8+ with nanoc and a port is specifed, nanoc errors out:

$ bundle exec nanoc live -p 3005
#<Thread:0x00007fda5490b380@/Users/ash/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/guard-nanoc-2.1.6/lib/guard/../nanoc/cli/commands/live.rb:22 run> terminated with exception (report_on_exception is true):
/Users/ash/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/nanoc-4.11.5/lib/nanoc/cli/commands/view.rb:34:in `fetch': key not found: :host (KeyError)
	from /Users/ash/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/nanoc-4.11.5/lib/nanoc/cli/commands/view.rb:34:in `run'
	from /Users/ash/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/guard-nanoc-2.1.6/lib/guard/../nanoc/cli/commands/live.rb:26:in `block in run'

Captain! We’ve been hit!

KeyError: key not found: :host

  0. /Users/ash/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/nanoc-4.11.5/lib/nanoc/cli/commands/view.rb:34:in `fetch'
  1. /Users/ash/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/nanoc-4.11.5/lib/nanoc/cli/commands/view.rb:34:in `run'
  2. /Users/ash/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/guard-nanoc-2.1.6/lib/guard/../nanoc/cli/commands/live.rb:26:in `block in run'

A detailed crash log has been written to ./crash.log.

Looking into the problem, I created the following which takes the Cri defintions straight from https://github.com/nanoc/nanoc/blob/4.11.5/nanoc/lib/nanoc/cli/commands/view.rb#L3-L15

nanoc_example.rb

#!/usr/bin/env ruby

require 'cri'
require 'nanoc'
require 'nanoc/cli'

command = Cri::Command.define do
  summary 'start the web server that serves static files'
  description <<~EOS
    Start the static web server. Unless specified, the web server will run on port
    3000 and listen on all IP addresses. Running this static web server requires
    `adsf` (not `asdf`!).
  EOS

  required :H, :handler, 'specify the handler to use (webrick/mongrel/...)'
  required :o, :host,    'specify the host to listen on (default: 127.0.0.1)', default: '127.0.0.1'
  required :p, :port,    'specify the port to listen on (default: 3000)', transform: Nanoc::CLI::Transform::Port, default: 3000
  flag :L, :'live-reload', 'reload on changes'
  no_params

  run do |opts, args|
    p opts.fetch(:port)
  end
end

command.run(ARGV)

Gemfile

source "https://rubygems.org"

gem 'cri'
gem 'nanoc'

2.15.7

$ bundle exec ./nanoc_example.rb
3000

2.15.9

$ bundle exec ./nanoc_example.rb
bundler: failed to load command: ./nanoc_example.rb (./nanoc_example.rb)
KeyError: key not found: :port
  /private/tmp/nanoc_example.rb:22:in `fetch'
  /private/tmp/nanoc_example.rb:22:in `block (2 levels) in <top (required)>'
  /Users/ash/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/cri-2.15.9/lib/cri/command.rb:360:in `run_this'
  /Users/ash/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/cri-2.15.9/lib/cri/command.rb:296:in `run'
  /private/tmp/nanoc_example.rb:26:in `<top (required)>'

Edit: Updated to reflect issue is present since 2.15.8+ and final example was for 2.15.9.

Quick question

Is there a way to exec the run block from a subcommand ? for instance

if I'm in a subcommand to do something like

run do |opts, args, cmd|
cmd.supercommand.run(opts)
end
thanks in advance for any help

Line wrapping for CRI help text?

Cri creates really nicely formatted help output. Since it already does indenting so nicely, is there currently any way to control line-wrapping, so options with long summaries don't go past, say, the 80th column, but are wrapped and indented instead? (Substitute 'a configurable' for '80th'.)

Thanks!

Run block executes even if --help option is specified

Help flag takes a block and then we have a run block which executes the command. If the command has --help option specified, the help block executes and then the run block also executes. Ideally if help option is specified then run block should not execute.
Again, coming from my application which is using Cri in a command loop. I cannot exit in the help block and have no control in Cri to stop execution of run block. Currently Im checking the presence of help option inside run block and just return if it is present.

Make colors optional and/or configurable

The colours are nice but they tend to hurt my eyes after a while.

It would be nice to be able to theme/change the colours to my liking and/or disable them. Possibly by using a switch.

I tried working on this a little while, but I'm still getting used to ruby so I probably made a mess. Maybe I'll send a pull request later unless you have an elegant solution in mind.

Add docs badge to README

Hi Denis,

I tried to add the Inch Pages badge to your README, but unfortunately my AsciiDoc-fu is not strong enough.

I tried image::http://inch-pages.github.io/github/ddfreyne/cri.png, but that did not seem to render right. Would mind adding this one yourself?

The badge looks like this Inline docs (looking good!) and the URL of the Inch Page would be http://inch-pages.github.io/github/ddfreyne/cri/

[doc] name for @param missing

Hi!

This is really a minor issue, but I noticed a warning when building the doc:

[warn]: @param tag has unknown parameter name: The 
    in file `lib/cri/command.rb' near line 96

Indeed, line 93 of lib/cri/command.rb, the name for the @param is missing.

Cédric

Splitting commands up?

Trying to split my command up in to separate files with the sub-commands each in their own file. However, this seems to not work as you can't pass in root_cmd when you require (so you could do the add_command in each file) and if you set a variable in the sub-command file it won't exist in the root_cmd file...

So, is it possible to create classes instead, or am I missing something else obvious?

Feature: Varargs parameter definitions

Idea: support parameter definitions for more than one argument. For example:

param :host
param :port, transform: method(:Integer)
param :filenames, varargs: true

varargs: true defines the parameter as corresponding to zero or more arguments.

For the time being, allowing it only as the last defined parameter is probably fine.

Specify default command

In Nanoc, nanoc is a shorthand for nanoc compile. The behavior of nanoc differs slightly from nanoc compile, as options are not passed through, and Cri has no method of doing so.

Suggestion: Let Cri support default subcommand properly, e.g. through

default_subcommand :compile

Subcommand where the root command has an option

Is it possible to implement something like command --option subcommand --another-option?

There is an option in my design that is "common" to all subcommands, for me it makes sense to be passed to the root command instead of repeating its definition in all subcommand blocks. For sure I could use a helper, but the pattern I am proposing looks better to me. Thanks in advance.

Add be_hidden variant for options

I'd like my application to be able to have an option set, without it being visible to regular users. A be_hidden variant as already exists for commands would be helpful here.

How to use '--' to signal arbitrary passthrough args/flags/options?

I have an involved command suite defined using x = Cri::Command.define and x..add_command().

One of the subcommands needs to be able to accept arbitrary arguments and options to be passed through to a shell script. Usually, this is done by using '--' to signal 'don't process the rest of this line.'

I can see getting access to the unprocessed bits using the #unprocessed_arguments_and_options attribute, but I can't figure out how to spot -- in order to stop the parser and leave the rest of the commandline accessible that way. Nor can i find any examples or documentation on how to do it.

The parse/processing is begun with root_command.run(ARGV)

Is the mechanism actually in there somewhere and I'm just too thick to find it?

Thans!

`--help --verbose` does not enable verbose mode

When passing both --help and --verbose, the --verbose option has no effect on the help output:

[..]
COMMANDS
[..]
    update            update the data stored by the data source to a newer version
    view              start the web server that serves static files
    (5 hidden commands ommitted; show them with --verbose)
[..]

colored breaks awesome_print

Great gem! I've been using cri on a couple projects and it's been working great. A user reported a bug on my rspec_n gem (which uses cri) and stated rspec_n breaks awesome_print when both gems are listed in a Gemfile. I traced the issue back to the colored gem that cri uses and thought I would make sure you were aware (especially since awesome_print is such a popular gem).

colored adds colorize, green, red, etc... methods directly on String. awesome_print does the same thing. The gem that gets listed last in a Gemfile has their implementation loaded by Bundler. If colored is listed last, it breaks awesome_print.

Based on this comment in the awesome_print code, awesome_print seems to have compatibility with the colorize gem (an alternative to colored), but it doesn't seem to know about colored.

Command help output with supercommand is not consistent on Ruby 1.8

On Ruby 1.8.7, order for Hash#each_pair is not deterministic, which can lead to the order of options and options for #{supercommand} in the help output to intermittently switch. I found out about this at puppetlabs/r10k#26 (comment) . When building Cri::Command#help, each_pair which can cause the order to switch.

Ruby 1.9 has consistent hash ordering that's based on insertion so this only affects Ruby 1.8.7. If Ruby 1.8.7 is fully supported I would be very happy to fix this, although testing this could prove difficult because of the nondeterministic duplication of the issue.

example code in readme contains error?

command.run(ARGS) should be command.run(ARGV), no?

probot :: Projects/Personal/testing % ruby critest.rb
critest.rb:29:in `<main>': uninitialized constant ARGS (NameError)

`help --verbose` lists hidden commands at the bottom

The ordering of commands in nanoc help is not alphabetical. The hidden commands appear at the bottom:

    check              run issue checks
    compile            compile items of this site
[...]
    update             update the data stored by the data source to a newer version
    view               start the web server that serves static files
    autocompile        start the autocompiler
    validate-css       validate the site’s CSS
    validate-html      validate the site’s HTML
    validate-links     validate links in site
    watch              start the watcher

Pass an arbitrary set of parameters to command definition

I am trying to build a set of subcommands in a kind of modular way, something like:

class Whatever

    def initialize
      @root = self.class.root
      @root.add_command(self.class.subcommand)
    end

    def config
      @root
    end

    private

    def self.root`
      subcommand = Cri::Command.define do
         ...
      end 
      subcommand
   end

    def self.subcommand`
      subcommand = Cri::Command.define do
         ...
      end 
      subcommand
   end

end

I would like to do some initializations in the main class and pass parameters to the methods to make them available to the command definitions. Cri::Command.define could use an additional parameter (it has three now) that is an arbitrary hash. Not sure if this is the intended usage, or maybe I am missing a pattern to build a subcommand set in a modular fashion.

Move defaults to declaration

Please add a way to specify the default value for an option in a way that gets exposed to --help, e.g.:

optional nil, :source, 'The source repository to load templates from.', default: '[email protected]:puppetlabs/modulesync_configs.git'
options:

    -h --help      show help for this command
       --summary   The source repository to load templates from.
                   Default value: [email protected]:puppetlabs/modulesync_configs.git

instead of

optional nil, :source, 'The source repository to load templates from.'
run do |opts, args, cmd|
  source = opts.fetch(:source, '[email protected]:puppetlabs/modulesync_configs.git')

Missing feature for parsing and validation of options and arguments

Would love to see feature for parsing, validation and transformation of options.

Something like delegate of Cri::OptionParser but exposed to be used.

Some food for thought

option :p, :port, 'port number' do |val, _cmd|  
  Integer(val)
end

or even part of call to option

option :p, :port, 'port number', Integer do |val, _cmd|  
  val.is_a? Integer => true
end

or

required :c, 'config-file', 'configuration file' do |val, _cmd|  
  Configuration.new(val)
end

And validation

required :n, :number, 'some number' do |val, _cmd|  
  raise SomeException if val.to_i) > 5
end

Arguments could also be validated. Some way to say, this command expects one positional argument, if not there, command is not executed and error message shown.

Version 2.15.7 causes runtime error

r10k: Runtime error: #<ArgumentError: R10K::Action::Puppetfile::Install cannot handle option 'help'>
Error while running: #<RuntimeError: r10k could not install all required modules>

Allow specifying default values for non-provided options

See #52.

The current implementation of default values only works for optional options, and the default value will not be used if the option is not provided. This is counter-intuitive; the default value should be the one that is used even when the option is not specified.

Make option requiredness more clear

From the option description, is it not clear whether an argument is required or not.

Actual:

    -f --format     define the output format

Expected:

    -f --format=[value]    define the output format

Feature: Parameters with default value

Idea: allow parameters to be defined with a default value. For example:

param :host
param :port, transform: method(:Integer), default: 3000

For simplicity, allow parameters with a default value to only appear at the end. The following would therefore not be possible:

param :foo, default: 'xxx'
param :bar
param :foo
param :bar, default: 'xxx'
param :qux
param :abc, default: 'yyy'

I image that this restriction will be fine, because the CLI will become complicated to use otherwise anyway.

combined options where the last option has an argument fails

When you combine options, and the last option has an argument, the argument is ignored.

For example, I think the following code should run fine, but results in an error:

    def test_combined_options_with_argument
      input       = %w( foo -ab xxx bar )
      definitions = [
        { :long => 'aaa', :short => 'a', :argument => :forbidden },
        { :long => 'bbb', :short => 'b', :argument => :required },
      ]

      parser = Cri::OptionParser.parse(input, definitions)
      assert(parser.options[:aaa])
      assert_equal({ :bbb => 'xxx' },  parser.options)
      assert_equal(%w(foo bar), parser.arguments)
    end

error:

Cri::OptionParserTestCase#test_combined_options_with_argument:
Cri::OptionParser::OptionRequiresAnArgumentError: b
    /Users/bart/Code/ruby/cri/lib/cri/option_parser.rb:230:in `block in handle_dash_option'
    /Users/bart/Code/ruby/cri/lib/cri/option_parser.rb:223:in `each'
    /Users/bart/Code/ruby/cri/lib/cri/option_parser.rb:223:in `handle_dash_option'
    /Users/bart/Code/ruby/cri/lib/cri/option_parser.rb:173:in `run'
    /Users/bart/Code/ruby/cri/lib/cri/option_parser.rb:105:in `parse'
    /Users/bart/Code/ruby/cri/test/test_option_parser.rb:289:in `test_combined_options_with_argument'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.