Giter VIP home page Giter VIP logo

clamp's Introduction

Clamp

Gem Version Build Status

"Clamp" is a minimal framework for command-line utilities.

It handles boring stuff like parsing the command-line, and generating help, so you can get on with making your command actually do stuff.

Not another one!

Yeah, sorry. There are a bunch of existing command-line parsing libraries out there, and Clamp draws inspiration from a variety of sources, including Thor, optparse, and Clip. In the end, though, I wanted a slightly rounder wheel. (Although, Clamp has a lot in common with Ara T. Howard's main.rb. Had I been aware of that project at the time, I might not have written Clamp.)

Quick Start

A typical Clamp script looks like this:

require 'clamp'

Clamp do

  option "--loud", :flag, "say it loud"
  option ["-n", "--iterations"], "N", "say it N times", default: 1 do |s|
    Integer(s)
  end

  parameter "WORDS ...", "the thing to say", attribute_name: :words

  def execute
    the_truth = words.join(" ")
    the_truth.upcase! if loud?
    iterations.times do
      puts the_truth
    end
  end

end

Internally, Clamp models a command as a Ruby class (a subclass of Clamp::Command), and a command execution as an instance of that class. The example above is really just syntax-sugar for:

require 'clamp'

class SpeakCommand < Clamp::Command

  option "--loud", :flag, "say it loud"
  option ["-n", "--iterations"], "N", "say it N times", default: 1 do |s|
    Integer(s)
  end

  parameter "WORDS ...", "the thing to say", attribute_name: :words

  def execute
    the_truth = words.join(" ")
    the_truth.upcase! if loud?
    iterations.times do
      puts the_truth
    end
  end

end

SpeakCommand.run

Class-level methods like option and parameter declare attributes, in a similar way to attr_accessor, and arrange for them to be populated automatically based on command-line arguments. They are also used to generate help documentation.

There are more examples demonstrating various features of Clamp on Github.

Declaring options

Options are declared using the option method. The three required arguments are:

  1. the option switch (or switches),
  2. an option argument name
  3. a short description

For example:

option "--flavour", "FLAVOUR", "ice-cream flavour"

It works a little like attr_accessor, defining reader and writer methods on the command class. The attribute name is inferred from the switch (in this case, "flavour"). When you pass options to your command, Clamp will populate the attributes, which are then available for use in your #execute method.

def execute
  puts "You chose #{flavour}.  Excellent choice!"
end

If you don't like the inferred attribute name, you can override it:

option "--type", "TYPE", "type of widget", attribute_name: :widget_type
                                           # to avoid clobbering Object#type

Short/long option switches

The first argument to option can be an array, rather than a single string, in which case all the switches are treated as aliases:

option ["-s", "--subject"], "SUBJECT", "email subject line"

Flag options

Some options are just boolean flags. Pass ":flag" as the second parameter to tell Clamp not to expect an option argument:

option "--verbose", :flag, "be chatty"

For flag options, Clamp appends "?" to the generated reader method; ie. you get a method called "#verbose?", rather than just "#verbose".

Negatable flags are easy to generate, too:

option "--[no-]force", :flag, "be forceful (or not)"

Clamp will handle both "--force" and "--no-force" options, setting the value of "#force?" appropriately.

Required options

Although "required option" is an oxymoron, Clamp lets you mark an option as required, and will verify that a value is provided:

option "--password", "PASSWORD", "the secret password", required: true

Note that it makes no sense to mark a :flag option, or one with a :default, as :required.

Multivalued options

Declaring an option ":multivalued" allows it to be specified multiple times on the command line.

option "--format", "FORMAT", "output format", multivalued: true

The underlying attribute becomes an Array, and the suffix "_list" is appended to the default attribute name. In this case, an attribute called "format_list" would be generated (unless you override the default by specifying an :attribute_name).

Hidden options

Declaring an option ":hidden" will cause it to be hidden from --help output.

option "--some-option", "VALUE", "Just a little option", hidden: true

Version option

A common idiom is to have an option --version that outputs the command version and doesn't run any subcommands. This can be achieved by:

option "--version", :flag, "Show version" do
  puts MyGem::VERSION
  exit(0)
end

Declaring parameters

Positional parameters can be declared using parameter, specifying

  1. the parameter name, and
  2. a short description

For example:

parameter "SRC", "source file"

Like options, parameters are implemented as attributes of the command, with the default attribute name derived from the parameter name (in this case, "src"). By convention, parameter names are specified in uppercase, to make them obvious in usage help.

Optional parameters

Wrapping a parameter name in square brackets indicates that it's optional, e.g.

parameter "[TARGET_DIR]", "target directory"

Multivalued (aka "greedy") parameters

Three dots at the end of a parameter name makes it "greedy" - it will consume all remaining command-line arguments. For example:

parameter "FILE ...", "input files", attribute_name: :files

Like multivalued options, greedy parameters are backed by an Array attribute (named with a "_list" suffix, by default).

Parsing and validation of options and parameters

When you #run a command, it will first attempt to #parse command-line arguments, and map them onto the declared options and parameters, before invoking your #execute method.

Clamp will verify that all required (ie. non-optional) parameters are present, and signal a error if they aren't.

Validation

Both option and parameter accept an optional block. If present, the block will be called with the raw string argument, and is expected to validate it. The value returned by the block will be assigned to the underlying attribute, so it's also a good place to coerce the String to a different type, if appropriate.

For example:

option "--port", "PORT", "port to listen on" do |s|
  Integer(s)
end

If the block raises an ArgumentError, Clamp will catch it, and report that the value was bad:

!!!plain
ERROR: option '--port': invalid value for Integer: "blah"

For multivalued options and parameters, the validation block will be called for each value specified.

More complex validation, e.g. those involving multiple options/parameters, should be performed within the #execute method. Use #signal_usage_error to tell the user what they did wrong, e.g.

def execute
  if port < 1024 && user != 'root'
    signal_usage_error "port restricted for non-root users"
  end
  # ... carry on ...
end

Advanced option/parameter handling

While Clamp provides an attribute-writer method for each declared option or parameter, you always have the option of overriding it to provide custom argument-handling logic, e.g.

parameter "SERVER", "location of server"

def server=(server)
  @server_address, @server_port = server.split(":")
end

Default values

Default values can be specified for options, and optional parameters:

option "--flavour", "FLAVOUR", "ice-cream flavour", default: "chocolate"

parameter "[HOST]", "server host", default: "localhost"

For more advanced cases, you can also specify default values by defining a method called "default_#{attribute_name}":

option "--http-port", "PORT", "web-server port", default:  9000

option "--admin-port", "PORT", "admin port"

def default_admin_port
   http_port + 1
end

Environment variable support

Options (and optional parameters) can also be associated with environment variables:

option "--port", "PORT", "the port to listen on", environment_variable: "MYAPP_PORT" do |val|
  val.to_i
end

parameter "[HOST]", "server address", environment_variable: "MYAPP_HOST"

Clamp will check the specified envariables in the absence of values supplied on the command line, before looking for a default value.

Allowing options after parameters

By default, Clamp only recognises options before positional parameters.

Some other option-parsing libraries - notably GNU getopt(3) - allow option and parameter arguments to appear in any order on the command-line, e.g.

foobar --foo=bar something --fnord=snuffle another-thing

If you want Clamp to allow options and parameters to be "interspersed" in this way, set:

Clamp.allow_options_after_parameters = true

Declaring Subcommands

Subcommand support helps you wrap a number of related commands into a single script (ala tools like "git"). Clamp will inspect the first command-line argument (after options are parsed), and delegate to the named subcommand.

Unsuprisingly, subcommands are declared using the subcommand method. e.g.

Clamp do

  subcommand "init", "Initialize the repository" do

    def execute
      # ...
    end

  end

end

Clamp generates an anonymous subclass of the current class, to represent the subcommand. Alternatively, you can provide an explicit subcommand class:

class MainCommand < Clamp::Command

  subcommand "init", "Initialize the repository", InitCommand

end

class InitCommand < Clamp::Command

  def execute
    # ...
  end

end

Like options, subcommands may have aliases:

Clamp do

  subcommand ["initialize", "init"], "Initialize the repository" do
    # ...
  end

end

Default subcommand

You can set a default subcommand, at the class level, as follows:

Clamp do

  self.default_subcommand = "status"

  subcommand "status", "Display current status" do

    def execute
      # ...
    end

  end

end

Then, if when no SUBCOMMAND argument is provided, the default will be selected.

Subcommand options and parameters

Options are inheritable, so any options declared for a command are supported by it's sub-classes (e.g. those created using the block form of subcommand). Parameters, on the other hand, are not inherited - each subcommand must declare it's own parameter list.

Note that, if a subcommand accepts options, they must be specified on the command-line after the subcommand name.

You can define a subcommand_missing method that is called when user tries to run an unknown subcommand:

Clamp do
  def subcommand_missing(name)
    if name == "foo"
      return Object.const_get(:FooPlugin) if Object.const_defined?(:FooPlugin)
      abort "Subcommand 'foo' requires plugin X"
    end
  end
end

Getting help

All Clamp commands support a "--help" option, which outputs brief usage documentation, based on those seemingly useless extra parameters that you had to pass to option and parameter.

$ speak --help
Usage:
    speak [OPTIONS] WORDS ...

Arguments:
    WORDS ...                     the thing to say

Options:
    --loud                        say it loud
    -n, --iterations N            say it N times (default: 1)
    -h, --help                    print help

Localization

Clamp comes with support for overriding strings with custom translations. You can use localization library of your choice and override the strings at startup.

Example usage:

require 'gettext'

Clamp.messages = {
  too_many_arguments:        _("too many arguments"),
  option_required:           _("option '%<option>s' is required"),
  option_or_env_required:    _("option '%<option>s' (or env %<env>s) is required"),
  option_argument_error:     _("option '%<switch>s': %<message>s")
  # ...
}

See messages.rb for full list of available messages.

License

Copyright (C) 2011 Mike Williams

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Contributing to Clamp

Source-code for Clamp is on Github.

clamp's People

Contributors

acook avatar alexwayfer avatar ares avatar danp avatar ekohl avatar evgeni avatar fearoffish avatar gitter-badger avatar jordansissel avatar mdub avatar mjio avatar mschulkind avatar ronen avatar schneiderl avatar smileart 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  avatar  avatar  avatar  avatar  avatar

clamp's Issues

Showing "short usage" unless "--help" is provided

Ok, so this Clamp library is gorgeous. I love it, and it had me at "subcommand".

I am used to command line apps showing only a short "usage" pattern whenever they are executed without any argument, and only show the longer help when --help is provided.

With clamp, I see full help even without asking for it with --help.

Is there a way to change this behavior?

I am expecting to only see something like this:

$ ruby test.rb
Usage:
    test.rb [OPTIONS] SUBCOMMAND [ARG] ...

$ _

Is there a way to support global options?

e.g. - using git as an option:

git --namespace=blah branch delete branchname

also it seems that specific subcommand should ideally be able to support their own options as well.

Thanks,
Brian

Subcommand's aren't inherited

subcommands declared in a parent class don't appear in child classes:

#!  /usr/bin/env ruby
require 'clamp'

module Example
  class Base < Clamp::Command
    option "--version", :flag, "shows the version" do
      puts "VERSION"
      exit 0
    end

    def execute
      exit 0
    end

    subcommand "parent", "a command from the parent" do
    end
  end

  class Sub1 < Base
    parameter "ONE", "one thing"

    def execute
      puts one.inspect
    end
  end

  class Sub2 < Sub1
    def execute
      puts "from Sub2"
      super
    end
  end

  class Main < Base
    subcommand "one", "ones", Sub1
    subcommand "two", "twos", Sub2
  end
end

Example::Main.run

In the terminal:

$ cli --help
Subcommands:
one                           ones
two                           twos

I expected Base's subcommands to come along.

Allow options and flags after parameters

Both should work:
command delete --force filename.txt
command delete filename.txt --force

Also this should work:

command delete -- --filename.txt (filename is "--filename.txt")

..and I think it already does:

# option/parsing.rb:20
           break if switch == "--"

1.2.0 makes multivalued params always required

Setting required: false does not help:

require 'clamp'

Clamp do

  option ['--tag', '-t'], 'TAG', "Description", multivalued: true, required: false

  def execute
    puts tag_list.inspect
  end
end
$ ruby test_command.rb
ERROR: option '--tag' is required

See: 'test_command.rb --help'

Adding default: [] makes it fail with Specifying a :default value with :required doesn't make sense (ArgumentError).

Release new version

There are some unreleased neat patches :-) Could you release minor version?

Parameter ARGF like functionality

With current Clamp I often find it a bit complicated to handle input from either STDIN or a filename passed as parameter.

(Even more so if you try to mix in a default value for the filename (which is quite usual nowadays in many apps, if you have some Dockerfile, docker-compose.yml, Whateverfile in your current dir, it will be used by default if you omit the parameter).)

I believe Ruby's ARGF was made because it's such a common thing you have to deal with when making command line apps.

So, wouldn't it be super fantastic if you could do this:

Clamp do
  parameter '[FILE]', "File path", default: 'config.yml', argf: true
end

The value of file would then be either:

  1. File.read(file) if file
  2. $stdin.read unless $stdin.tty?
  3. File.read('config.yml')
  4. nil

(maybe it should signal_usage_error if param is given and there's something coming from stdin)

This would work a bit like the ruby ARGF:

# argf.rb
puts ARGF.read

$ ruby argf.rb file.txt
file.txt content

$ echo "hello" | ruby argf.rb
hello

# This I don't quite like about ARGF for my usecases because it's super difficult
# to separate the files:
$ ruby argf.rb file.txt file2.txt file3.txt
file.txt content
file2.txt content
file3.txt content

I have no idea if Ruby's ARGF can be used at all together with Clamp, probably not. Also it has some special abilities like the in place editing -i.

Another variation of the same type of scenario, but more easy to do with current clamp is:

Clamp do
  parameter '[FILE ...]', "File path"

  def default_file_list
     if $stdin.tty? #  perhaps something more elegant for checking is available?
       Array.new
     else
       $stdin.read.split(/\s+/)
     end
   end
end

Now you can do:

$ command x.txt y.txt z.txt
# or: (without using "xargs")
$ ls *.txt | command 

Not too difficult, but maybe this too could be handled by Clamp for you:

Clamp do
  parameter '[FILE ...]', "File path", default: 'file.txt', stdin: true
end

Default subcommand with options/arguments

Right now it seems that the default subcommand is only supported when passing nothing to the clamp script. This generates an "odd" behavior to me.

For example I have these two subcommands (just example code, not really working):

class MainCommand < Clamp::Command
  #
  # Global options
  #
  option "-V", :flag, "print out version" do
    puts(VERSION)
    exit(0)
  end

  option [ "-v", "--verbose" ], :flag, "print out stuff", :default => false 

  #
  # Sub commands
  #
  self.default_subcommand = "sub1"

  subcommand "sub1", "subcommand 1", SubCommand1
  subcommand "sub2", "subcommand 2", SubCommand2
end

class SubCommand1 < MainCommand
  option "-o", "OPTION_ARG", "I am a required option", :required => true

  def execute
    puts "I am subcommand 1"
  end
end

class SubCommand2 < MainCommand
  option "-z", "OPTION_ARG", "I am an option"

  def execute
    puts "I am subcommand 2"
  end
end

What happens is that, when calling the command without any arguments, it correctly requires to specify the -o OPTION_ARG option since it defaults to SubCommand1; when adding the -o OPTION_ARG option without any subcommand clamp calls MainCommand and results in: ERROR: Unrecognised option '-o'.

This seems to me an odd behavior, I would expect clamp to always default to the default subcommand and only trigger specific options/subcommands in case you specify some.
A consistent behavior to me would be to always try the default subcommand and print the MainCommand help in case problems arise.

What do you think? Am I getting something wrong here? If you think I have a point I can pull request the changes needed.

setting boolean/flag values

Use case: I have an option I default to enabled (on) that I want folks to be able to disable, but semantically defining a "--no-foo" flag seems awkward especially with the negatives I end up using in the help string.

If I define a flag:

option "--foo", :flag, "...", :default => true

I'd like to be able to accept --no-foo or even --foo=(no|false|off...)

Possible? Would you accept patches adding this if it's not available already?

option arg validation question.

I need to implement the following option, which allows one of three possible values (text, json, yaml):

option "--format", "FORMAT", "Output format"

How would I do the input validation against an array of valid strings? (And does the validation update the displayed usage text, or do I need to manually edit that text?)

I tried something like the following with no luck:

option "--format", "FORMAT", "Output format", :default => "json" do |s|
  ['json', 'yaml', 'text'].include? s
end

Oh one other thing, in subclasses I do need to override the default, and allowed FORMATs. Perhaps that might be interfering?

Thanks,
Brian

Parameter values not passed down to nested subcommands

Given command subbed:

Clamp do

  parameter "THING", "the thing"

  subcommand "speak", "say things" do
    subcommand "loud", "from the rooftops" do
      def execute
        puts "#{thing.upcase}"
      end
    end
  end

end

I'd expect ./subbed blahblah speak loud to print "BLAHBLAH". Instead, it dies with:

$ ./subbed blahblah speak loud
./subbed:12:in `execute': undefined method `upcase' for nil:NilClass (NoMethodError)
    from /Users/mdub/.gem/ruby/2.3.1/gems/clamp-1.0.0/lib/clamp/command.rb:68:in `run'
    from /Users/mdub/.gem/ruby/2.3.1/gems/clamp-1.0.0/lib/clamp/subcommand/execution.rb:11:in `execute'
    from /Users/mdub/.gem/ruby/2.3.1/gems/clamp-1.0.0/lib/clamp/command.rb:68:in `run'
    from /Users/mdub/.gem/ruby/2.3.1/gems/clamp-1.0.0/lib/clamp/subcommand/execution.rb:11:in `execute'
    from /Users/mdub/.gem/ruby/2.3.1/gems/clamp-1.0.0/lib/clamp/command.rb:68:in `run'
    from /Users/mdub/.gem/ruby/2.3.1/gems/clamp-1.0.0/lib/clamp/command.rb:133:in `run'
    from /Users/mdub/.gem/ruby/2.3.1/gems/clamp-1.0.0/lib/clamp.rb:6:in `Clamp'
    from ./subbed:5:in `<main>'

subcommands should be optional

I'm using clamp 0.1.8 and when i create a subcommand it stops the flag -v or --version that i added from working.

It's expecting a subcommand : ERROR: parameter 'SUBCOMMAND': no value provided

Add setter for :multivalued => true option

It would make testing easier if :multivalued options could also define a setter of the form "#{:attribute_name}list=". The only write method defined currently for options with :multivalued => true is "append_to#{:attribute_name}_list" which only allow adding to the list.

I'd like to be able to do:

class MyClamp < Clamp::Command
   option "--my-option", "a multi-valued option", :multivalued => true
end

# In tests
c = MyClamp.new("")
c.my_option_list = %w{Values for testing}

Now I have to do:

c = MyClamp.new("")
%w{Values for testing}.each {|v| c.append_to_my_option_list(c) }

Included parameter/option defaults do not work as expected

Not necessarily a bug, but something of an annoyance.

This works as expected and documented:

class FooCommand < Clamp::Command

  parameter "[FOO]", "Foo"

  def default_foo
    "bar"
  end

  def execute
    puts foo
  end
end

FooCommand.run

But this does not:

module CommonParam
  def self.included(where)
    where.parameter "[FOO]", "Foo"
  end

  def default_foo
    "bar"
  end
end

class FooCommand < Clamp::Command

  include CommonParam

  def execute
    puts foo
  end
end

FooCommand.run

The default_foo will never be called.

I believe this is because the default_method is defined during declaration and not "on the spot".

A workaround like this can be used:

module CommonParam

  def self.included(where)
    where.prepend InstanceMethods
    where.parameter "[FOO]", "Foo"
  end

  module InstanceMethods
    def default_foo
      "bar"
    end
  end
end

class FooCommand < Clamp::Command

  include CommonParam

  def execute
    puts foo
  end
end

FooCommand.run

options / arguments ordering

I'm in the process of converting my Thor cli to Clamp. I am more of a fan of the way Clamp handles commands and arguments, but one major thing that bugs me is the fact that Clamp requires options to be specified before arguments.

Is there a reason why this was chosen?

I must say this reads more natural to me:

rspec my_test.rb --backtrace

than this does:

rspec --backtrace my_test.rb

To elaborate a bit more on my issue:

$ hashi test subject_name
using subject: subject_name
with commit: 
$ hashi test subject_name --commit "abcd..awes"
ERROR: too many arguments

See: 'hashi test --help'
$ hashi test --commit abcd..awes subject_name
using subject: subject_name
with commit: abcd..awes

my implementation:

class Hashi::CLI::Test < Abstract
  parameter '[SUBJECT]', 'Run test suite against subject'

  option ['--commit', '-c'], '4htg3..kj8rv',
    'Test only touched subjects in this commit (range)'

  def execute
    puts "using subject: #{subject}"
    puts "with commit: #{commit}"
  end
end

Parameters implicit default value is always a new object

This is totally weird! Hah!

Example program:

require "clamp"
class Foo < Clamp::Command
  parameter "[ARGS] ...", "Hello", :attribute_name => :args
  def execute
    p args.object_id
    p args.object_id
    p args.object_id
  end
end

Foo.run(ARGV)

When run, this outputs 3 different object IDs for args. I traced this through the code, and it seems to be related to the implicit default parameter empty array.

If I define the parameter as:

parameter "[ARGS] ...", "Hello", :attribute_name => :args, :default => []

Then the object id is always the same.


Run 1 (no :default):

% ruby test.rb
70259856025040
70259856024820
70259856024660

Run 2 (with :default => [])

% ruby test.rb
70194689979260
70194689979260
70194689979260

This is easy to workaround. I found this bug while helping someone with an fpm bug. The bug is that, in fpm, we try to set some default parameters in certain cases, and did so with args << ... which doesn't work because args returns a new object every time, thus the attempt to set the default didn't work! Computers! :)

Related fpm code: https://github.com/jordansissel/fpm/blob/v1.3.3/lib/fpm/command.rb#L276

Strange behavior of attribute writers

Not sure if I'm crazy or if this is doing the wrong thing:

require 'clamp'

class WeirdCommand < Clamp::Command
  DEFAULT_FOO = 'fo0'

  parameter 'FOO', 'The foo', required: false

  def execute
    unless foo
      foo = DEFAULT_FOO         # Fails
      # @foo = DEFAULT_FOO      # Works
    end

    bar(foo)
  end

  def bar(f)
    puts %(The foo is "#{f}")
  end
end

RSpec.describe WeirdCommand do
  subject(:weird) do
    described_class.new('weird', {})
      .tap { |c| c.parse(args) }
  end
  let(:args) { [] }

  describe '#execute' do
    context 'without foo' do
      it 'bars the foo' do
        expect(subject).to receive(:bar).with(described_class::DEFAULT_FOO)
        subject.execute
      end
    end

    context 'with foo' do
      let(:foo_arg) { 'quux' }
      let(:args) { [foo_arg] }

      it 'bars the foo' do
        expect(subject).to receive(:bar).with(foo_arg)
        subject.execute
      end
    end
  end
end

Running spec:

$ rspec test_case.rb --format doc --color

WeirdCommand
  #execute
    without foo
      bars the foo
    with foo
      bars the foo (FAILED - 1)

Failures:

  1) WeirdCommand#execute with foo bars the foo
     Failure/Error: bar(foo)
     
       #<WeirdCommand:0x007fbd5391f178 @invocation_path="weird", @context={}, @remaining_arguments=[], @foo="quux"> received :bar with unexpected arguments
         expected: ("quux")
              got: (nil)
       Diff:
       @@ -1,2 +1,2 @@
       -["quux"]
       +[nil]
       
     # ./test_case.rb:13:in `execute'
     # ./test_case.rb:42:in `block (4 levels) in <top (required)>'

Finished in 0.02258 seconds (files took 0.12948 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./test_case.rb:40 # WeirdCommand#execute with foo bars the foo

The behavior of the command seems to be affected by a call to foo=() when that line should not even be reached, i.e., when foo is passed as a parameter (the unless block should not run, but foo is set to nil). Replace the call with a direct @foo = and the issue goes away.

I've been staring at this for a while and I don't see how it's possible. Please tell me what I'm doing wrong.

Is it possible to define parameters in module?

Thanks for your work on this project mdub! I really like it. Question: Is it possible to share parameters via modules. I've tried the following but the parameter doesn't appear in the help for my command nor does its attribute get set. What am I missing?

module SharedParameters
  extend Clamp::Parameter::Declaration
  parameter "NAME", "name of thing"
  def thing
     System.get(name).status 
  end
end

class StatusCommand
  include ShareParameters
  def execute
     puts thing
  end 
end

class MainCommand < Clamp::Command
  subcommand "status", "show status of thing", StatusCommand
end

Support emdash?

Copy/pasting --foo into pidgin, adium, skype, etc, will turn the two-character double dash into a single emdash.

This makes folks get confused when flag parsing goes awry because clamp doesn't think emdash (—) (and other long dash glyphs?) are valid for flags.

Thoughts? I can try to prepare a patch for this.

required:true breaks attribute writer functionality

This Works

#! /usr/bin/env ruby

require "clamp"

Clamp do
  option %w(--foo), 'FOO[:THING]',
         'A fooy thing',
         attribute_name: :fooilicious

  def fooilicious=(thing)
    @my_att = thing
  end

  def execute
    puts @my_att
  end
end

Output

$ ./required-setter-overwritten --foo bar
bar

This Fails

#! /usr/bin/env ruby

require "clamp"

Clamp do
  option %w(--foo), 'FOO[:THING]',
         'A fooy thing',
         attribute_name: :fooilicious,
+        required: true

  def fooilicious=(thing)
    @my_att = thing
  end

  def execute
    puts @my_att
  end
end

Output

$ ./required-setter-overwritten --foo bar
ERROR: option '--foo' is required

See: 'required-setter-overwritten --help'

Option validation block can read wrong values of other options

Lets have two options

option %w[-t --template], 'TEMPLATE_VM', 'Template VM name.', 
       default: :katello_base do |template|
  find_template template
end

def default_template
  find_template :katello_base
end

option %w[-s --snapshot], 'SNAPSHOT', 
       'Template snapshot name.', required: true do |snapshot|
  template.snapshots.include?(snapshot) or
      raise ArgumentError, "'#{snapshot}' not found between: #{template.snapshots.join(', ')}"
  snapshot
end

When --snapshot s --template a_temp is called it this order then snapshot is validated against default value of template instead against passed value 'a_temp'.

Would it be possible to defer evaluation of validation blocks and trigger it when accessed from other validation block to get always the correct value?


BTW 👏 Clamp has very nice design.

License missing from gemspec

RubyGems.org doesn't report a license for your gem. This is because it is not specified in the gemspec of your last release.

via e.g.

  spec.license = 'MIT'
  # or
  spec.licenses = ['MIT', 'GPL-2']

Including a license in your gemspec is an easy way for rubygems.org and other tools to check how your gem is licensed. As you can imagine, scanning your repository for a LICENSE file or parsing the README, and then attempting to identify the license or licenses is much more difficult and more error prone. So, even for projects that already specify a license, including a license in your gemspec is a good practice. See, for example, how rubygems.org uses the gemspec to display the rails gem license.

There is even a License Finder gem to help companies/individuals ensure all gems they use meet their licensing needs. This tool depends on license information being available in the gemspec. This is an important enough issue that even Bundler now generates gems with a default 'MIT' license.

I hope you'll consider specifying a license in your gemspec. If not, please just close the issue with a nice message. In either case, I'll follow up. Thanks for your time!

Appendix:

If you need help choosing a license (sorry, I haven't checked your readme or looked for a license file), GitHub has created a license picker tool. Code without a license specified defaults to 'All rights reserved'-- denying others all rights to use of the code.
Here's a list of the license names I've found and their frequencies

p.s. In case you're wondering how I found you and why I made this issue, it's because I'm collecting stats on gems (I was originally looking for download data) and decided to collect license metadata,too, and make issues for gemspecs not specifying a license as a public service :). See the previous link or my blog post about this project for more information.

Suggestion: 'execute do' instead of 'def execute'

Not sure if this was considered before and ruled out or not, but something that strikes me as a little odd in the syntax for creating an application with Clamp, is the fact that it has a def execute inside of a block. I don't recall seeing such an approach anywhere (but of course it may just mean that I have not seen enough gems...).

I was wondering if it would make sense to change or amend the syntax so that it will allow the use of execute do block, like so:

Clamp do
  subcommand "test", "Test it" do
    option "--loud", :flag, "say it loud"

    execute do
      ...
    end
  end
end

This feels a little more natural to me.

Long options break justification

      # help.rb
      DETAIL_FORMAT = "    %-29s %s"

It would be nice if it calculated the justification based on the longest string. Now it looks messed up with long parameter names.

Getting parameters and options dynamically from the execute method

I am using the Clamp in a tool where its parameters and options are dynamically created from a config file. This means that I only know during run time what are my params/options/flags.

When I get to writing the execute method I would like to have the ability to go over all params/options/flags... and send them to my program.

example:

Clamp do
  #Declarations of all parameters and options....

def execute
  options_and_their_values = self.get_options
  parameters... = self.get_params
 MyProgramClass.new(options_and_their_values)
end

end #end clamp

In this example I could send all params/options and their values without knowing what they are in run time.

I would like to hear what you think.

Running plugins or "external subcommands"

Many CLI utils (git, brew, ..) are using a plugin system that works by trying to execute for example brew-do-stuff if you run brew do-stuff which is an unknown subcommand. The plugin subcommand receives rest of the arguments and gets the ENV from the parent app.

How would you do something similiar using clamp?

I was thinking about something like this:

  app_basename = File.basename(__FILE__)
  ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
    Dir[File.join(path, "#{app_basename}-*")].select{|f| File.executable?(f) && !File.directory?(f)}.each do |plugin_path|
      subcommand File.basename(plugin_path)[/^#{app_basename}-(.*)/, 1], "External command" do
        def execute
          exec "#{app_basename}-#{ARGV[0]}", *ARGV[1..-1]
        end
      end
    end
  end

It even almost works, except if you try to pass arguments to the external command, which will give you "too many arguments" or "unrecognized option" because clamp tries to parse them.

"setup" method for global initialization?

Hi, thanks for the great gem.

In my current use, I have to do some initialization based on the global option values. I can define a setup method in my Clamp block, but then I need to call it at the start of each of my execute methods.

I wonder if you'd be interested in a PR that would DRY that up, automatically calling an optional setup?

It seems implementation would be easy, at command.rb#L61:

def run(arguments)
  parse(arguments)
  setup
  execute
end

def setup
  # base does nothing, sub-class can override 
end

That's all that I need for my current application, but one could also imagine going whole hog and providing setup, teardown, and around:

def run(arguments)
  parse(arguments)
  begin
    setup
    around do
      execute
    end
  ensure
    teardown
  end
end

def setup
  # base does nothing, sub-class can override 
end

def around
  yield # base does nothing interesting, sub-class can override
end

def teardown
  # base does nothing, sub-class can override
end

...but that might be overkill.

What do you think? I'm happy to submit a PR but don't want to put in the time for the specs etc if you're not up for it. Also could debate the names of the methods, the execution order of setup/teardown vs around, etc.

Thanks again

Hidden subcommands

It would be nice to be able to hide subcommands from help.

Something like this:

if plugin_installed?("foo")
  subcommand "foo", "Do fooey things", FooCommand
else
  subcommand "foo", "Do fooey things", hidden:true do
    puts "Install plugin foo first by running: gem install foo-plugin"
  end
end

Or perhaps allow this:

Clamp do
  parameter "[SUBCMD]", "Sub command", hidden: true, required: false

  subcommand "foo", "Do fooey things", FooCommand

  def execute
    if known_plugins.include?(subcmd)
      abort "Install plugin #{subcmd} first by running gem install #{subcmd}-plugin"
    end
  end
end

OR:

Clamp do
  def subcommand_missing(name)
    abort "Install plugin #{name} first by running gem install #{name}-plugin"
  end
end

subcommand_missing only invoked if command has defined at least one subcommand

This code runs as expected:

#!/usr/bin/env ruby

require 'clamp'

Clamp do
  subcommand 'foo', 'bar'

  def subcommand_missing(name)
    abort "would have looked for subcommand #{name} here"
  end
end

If you comment out the subcommand, however, you just get:

ERROR: too many arguments

See: 'clamptest.rb --help'

This appears to be due to the has_subcommands? check in parse_subcommand, but I don't understand the code well enough yet to suggest a solution.

"required" options

I treat what clamp calls an 'option' simply as a "named parameter" much as you would call a function with named parameters. I know this is is perhaps contentious, but hear me out :)

I would like to make the case that, while "option" is perhaps a poor choice of words, having a "required named parameter" (or 'option' in clamp's terms) is a very common thing.

Python's "optparse" says "the phrase “required option” is self-contradictory in English". Seems like clamp follows this definition as well.

But, if for a moment, you replace the word "option" with another word like "setting" or "flag" or "configuration" or "tunable" then the "required option is contradictory" dogma completely disappears while leaving the remaining functionality of clamp unscathed.

The primary differences between an "option" and a "parameter" in Clamp appear to be:

  • options are named, unordered, and always optional
  • parameters are unnamed, order-dependent, and sometimes optional.

In some applications, I have "required settings" that are quite numerous, and having to implement these as parameters (due to their 'requirable' property) is awkward once you get beyond more than 3 or 4 required settings. I would much rather use the named and unordered nature of Clamp's "option" feature while still making them required.

I would like to specify:

option "--foo", "FOO", "This is the foo", :required => true

Benefits here are:

  • that I can automatically have "--foo is required" appear in the usage output
  • without writing extra code, I can enforce that a given setting is present.

Thoughts? I am willing to provide patches to implement this, but I wanted to propose the solution before implementing to get feedback :)

No (automatic) indicator of required/optional option help

Default behavior:

option '--path', 'PATH', "File path", required: false

$ cmd --help
  --path PATH  File path

With required: true it looks the same:

option '--path', 'PATH', "File path", required: true

$ cmd --help
  --path PATH  File path

You can use [] around it, but that makes it look like the option param is optional and with just --path it would act as a flag:

option '--path', '[PATH]', "File path"

$ cmd --help
  --path [PATH]  File path

You can combine [] and required: true:

option '--path', '[PATH]', "File path", required: true

$ cmd --help
  --path [PATH]  File path

You can also use <>:

option '--path', '<PATH>', "File path", required: false

$ cmd --help
  --path <PATH>  File path

Maybe it should be something like:

option '--path1', 'PATH', "File path", required: false
option '--path2', 'PATH', "File path", required: true

$ cmd --help
  [--path1 <PATH>]  File 1 path
   --path2 <PATH>   File 2 path

Add :deprecated to options

I have flags I wish to deprecate, and I'd love it clamp could help with this.

Idea:

  • An option with :deprecated set will not appear in the usage output (ie; --help)
  • An option can be declared :deprecated => "The reason this is deprecated and some other notes" which can be displayed to users however the application developer decides.[1]
  • Add a new method deprecated_options intended for use in the Clamp::Command#execute method which allows an author to iterate over deprecated options that were given by the user

Rough sketch:

class Whoa < Clamp::Command
  option "--really-fast", :flag, "Go fast!", :deprecated => "Please use the --fast flag"
  option "--fast", :flag, "Go fast!"

  def execute
    deprecated_options.each do |flag, reason|
      puts "The #{flag} option is deprecated. #{reason}"
    end
  end
end

I am willing to provide the patches to achieve this, but I'd like to iterate on the design first before I write some code :)

Feature: environment variables as flag values

Howdy!

It's fairly common in some environements (like heroku, etc) to use environment variables to configure applications. I am happy to produce this feature as a patch, but am looking for feedback and thumbs-up before I spend time on it :)

Thoughts? General proposal below:

New feature: Flags can be set with environment variables

The problem

I see lots of programs that take flags but also support configuration via environment variables (rails, heroku, etc). In most cases, I find these programs using optparse, clamp, etc, for the flag handling, but then handling environment variables separately, and often, with code that is not shared across projects.

Automatic env handling:

option "--foo-bar", "FOO", "Some foo"

The above could be set with 'myapp --foo-bar value' like normal, but also FOO_BAR=value from the environment

Explicit env

option "--foo-bar", "FOO", "Some foo", :env => "FOO"

The above would allow FOO=value to set the value for --foo-bar.

--help improvements

Some hints should be given in the --help usage to indicate that environment variables can be used.

Conflicts

When given both env and flag values, the flag value should win.

support for exit codes

Hi,

Suggestion: It would be nice if the run method could call Kernel#exit with the executereturn value if not 0 or have another rescue clause just for doing exit(1) on error.

This would allow not doing explicit exit(1) in a command and be able to still use the command programmatically in another context than command line.

Option / flag order

When parsing, it seems the order of options / flags and arguments matters.

Ex:
command --option arg

But if I type
command arg --option

It fails. ERROR: Too many arguments

Could this be resolved so the order of parameters doesn't matter? (Obviously order of arguments would matter, but not flags / options)

[RFE] option groups

What do you think of adding groups to options so when you display help you can divide them? Especially useful when you have more than 15 options I think.

Allow command aliases

It would be nice, especially for subcommands, to be able to specify an alias. For example, rails server has an alias of rails s, rails console has an alias of rails c, and so on.

May have found a bug, or at least an unexpected behaviour in parse order.

For reference, here is my latest code: https://github.com/bgupta/foreman-hammer/blob/master/hammer

I setup most options, in HammerCommand, and setup a couple methods of the --format option there.

 option "--format", "FORMAT", "Output format", :default => "json"

 def allowed_content_types
    return [ "json", "yaml", "text" ]
 end

 def format= s
   raise ArgumentError, "must be one of: #{allowed_content_types.join(', ')}" unless allowed_content_types.include?(s)
   @format = s
 end

This seems to work fine.

The problem comes when I try to override the allowed_content_types method in the HostShowCommand subclass.

def allowed_content_types
  return [ "json", "yaml" ]
end

I would expect that if I call the "hammer --format blah show host" command ("blah" being the literal string), it should throw an error listing json and yaml as the valid content types. In my case it is throwing the following error:

ERROR: option '--format': must be one of: json, yaml, text

See: 'hammer --help'

For some reason the options seem to be being parsed before the subcommands? I tried a number of different things, and asked a friend who knows ruby a lot better, and he felt that it probably wasn't my code, and might be a bug in clamp, and suggested that I follow up here.

Options only work with longopts.

As soon as you use something like option "-d", it throws a trace, because the infer_attribute_name regex only matches for longopts.

To work around this one can manually specify an :attribute_name. Would be nice if that was either fixed or mentioned in the README.

How to define --version

It's quite common to have something like app --version output the version.

How to do this with clamp if you have subcommands in your main command?

How about other "global" options/flags such as ['-d', '--debug']?

Invalid @type symbol conversion error

So, this is not a bug in clamp. That's just me being inattentive about the type param.

definition.rb:70:in `+': can't convert Symbol into String (TypeError)

The source was simply a stray :bool where a :flag should have been.

Anyway. Not big with Ruby, so would it make sense to adapt Clamp::Option::Definition#help_lhs nevertheless with:

    lhs += " " + type unless unless type.is_a? Symbol

in place of just flag? probing?

For the sake of noobs, could you please

I have not used any CLI tool like Clamp, Thor etc before. Forgive me if I sound really off the scale.

Its taken me three days to get my head around what and how Clamp works. The most part of it at trying to run the scripts in the terminal. I am still having problems reason I can't post any hints right now, but I will when I get going.

For the sake of newbies, could someone update the README with some directions on how to run your scripts in Clamp. There is not much said about it here. Thanks.

Optional Parameter Default Must Be Accessed Thru default_param ...

I am using and really loving clamp.
I am excited to use the new optional parameter default, but it seems to me when getting the value of an option param that has a default, the default is not given.
To get the behavior I expected I had to use: param || default_param.
Is this the intended workflow?
I think that calling param should give the default if one was not given.

Thank you for a wonderful gem.

Mandatory options are not enforced

The following should abort with a meaningful error message :

require 'rubygems'
require 'clamp'

class RunCommand < Clamp::Command
  option "--test", "MANDATORY", "a mandatory thingie"

  def execute
    puts test.nil? ? "FAILED" : "OK"
  end
end

RunCommand.run

Instead, it displays "FAILED".
This was tested with clamp-0.2.1

Cannot include attribute writers from a module

Expected Behavior

Can include an attribute writer, like I would be able to do in other ruby situations, and have clamp use that method

Actual Behavior

Clamp doesn't see that the method exists at all, and it's never called

Repro Steps

#! /usr/bin/env ruby

require "clamp"

module Foo
  def fooilicious=(thing)
    @my_att = thing
    @fooilicious = thing
  end
end

class Bar < Clamp::Command
  option %w(--foo), 'FOO[:THING]', 'A fooy thing',
  attribute_name: :fooilicious

  include Foo

  def execute
    puts "executing"
    puts @my_att
  end
end

Bar.run

Workaround

#! /usr/bin/env ruby

require "clamp"

- module Foo
-  def fooilicious=(thing)
-    @fooilicious = thing
-  end
-end

class Bar < Clamp::Command
  option %w(--foo), 'FOO[:THING]', 'A fooy thing',
  attribute_name: :fooilicious

-  include Foo
+ def fooilicious=(thing)
+   @fooilicious = thing
+ end

  def execute
    puts "executing"
    puts @fooilicious
  end
end

Bar.run

This workaround is not ideal because I have several commands that have similar behavior and I don't want to have command inheritance.

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.