clamp-1.1.1/0000755000004100000410000000000013007336773012647 5ustar www-datawww-dataclamp-1.1.1/Rakefile0000644000004100000410000000036113007336773014314 0ustar www-datawww-datarequire "bundler" Bundler::GemHelper.install_tasks require "rspec/core/rake_task" task "default" => "spec" RSpec::Core::RakeTask.new do |t| t.pattern = "spec/**/*_spec.rb" t.rspec_opts = ["--colour", "--format", "documentation"] end clamp-1.1.1/Gemfile0000644000004100000410000000043413007336773014143 0ustar www-datawww-datasource "http://rubygems.org" gemspec group :development do gem "guard-rspec", "~> 4.6.5", :require => false gem "listen", "~> 3.0.2" gem "rake", "~> 10.4" gem "rubocop", "~> 0.43.0", :require => false end group :test do gem "rspec", "~> 3.5.0" gem "rr", "~> 1.2.0" end clamp-1.1.1/examples/0000755000004100000410000000000013007336773014465 5ustar www-datawww-dataclamp-1.1.1/examples/speak0000755000004100000410000000075513007336773015525 0ustar www-datawww-data#! /usr/bin/env ruby # A simple Clamp command, with options and parameters require "clamp" Clamp do banner %( Say something. ) 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 clamp-1.1.1/examples/admin0000755000004100000410000000071713007336773015510 0ustar www-datawww-data#! /usr/bin/env ruby # An example of subcommands require "clamp" Clamp do option "--timeout", "SECONDS", "connection timeout", :default => 5, :environment_variable => "MYAPP_TIMEOUT" do |x| Integer(x) end parameter "HOST", "server address" parameter "[PORT]", "server port", :default => 80, :environment_variable => "MYAPP_PORT" def execute puts "trying to connect to #{host} on port #{port} (waiting up to #{timeout} seconds)" end end clamp-1.1.1/examples/subcommand_missing0000755000004100000410000000036213007336773020275 0ustar www-datawww-data#! /usr/bin/env ruby require "clamp" Clamp do subcommand "hello", "Say hello" do def execute puts "Hello" end end def subcommand_missing(name) abort "Install bye plugin first" if name == "bye" super end end clamp-1.1.1/examples/flipflop0000755000004100000410000000066213007336773016232 0ustar www-datawww-data#! /usr/bin/env ruby # An example of subcommands require "clamp" require "clamp/version" Clamp do option ["--version", "-v"], :flag, "Show version" do puts "Powered by Clamp-#{Clamp::VERSION}" exit(0) end self.default_subcommand = "flip" subcommand "flip", "flip it" do def execute puts "FLIPPED" end end subcommand "flop", "flop it" do def execute puts "FLOPPED" end end end clamp-1.1.1/examples/gitdown0000755000004100000410000000244113007336773016067 0ustar www-datawww-data#! /usr/bin/env ruby # Demonstrate how subcommands can be declared as classes require "clamp" module GitDown class AbstractCommand < Clamp::Command option ["-v", "--verbose"], :flag, "be verbose" option "--version", :flag, "show version" do puts "GitDown-0.0.0a" exit(0) end def say(message) message = message.upcase if verbose? puts message end end class CloneCommand < AbstractCommand parameter "REPOSITORY", "repository to clone" parameter "[DIR]", "working directory", :default => "." def execute say "cloning to #{dir}" end end class PullCommand < AbstractCommand option "--[no-]commit", :flag, "Perform the merge and commit the result." def execute say "pulling" end end class StatusCommand < AbstractCommand option ["-s", "--short"], :flag, "Give the output in the short-format." def execute if short? say "good" else say "it's all good ..." end end end class MainCommand < AbstractCommand subcommand "clone", "Clone a remote repository.", CloneCommand subcommand "pull", "Fetch and merge updates.", PullCommand subcommand "status", "Display status of local repository.", StatusCommand end end GitDown::MainCommand.run clamp-1.1.1/examples/scoop0000644000004100000410000000047013007336773015534 0ustar www-datawww-data#! /usr/bin/env ruby # An example of multi-valued options require "clamp" Clamp do option ["-f", "--flavour"], "FLAVOUR", "flavour", :multivalued => true, :default => ["chocolate"], :attribute_name => :flavours def execute puts "one #{flavours.join(' and ')} ice-cream" end end clamp-1.1.1/examples/defaulted0000755000004100000410000000112413007336773016346 0ustar www-datawww-data#! /usr/bin/env ruby # An example of default values and methods require "clamp" require "highline" Clamp do option ["-U", "--user"], "USER", "user name", :environment_variable => "THE_USER", :default => "bob" option ["-P", "--password"], "PASSWORD", "password", :environment_variable => "THE_PASSWORD" def execute puts "User: #{user}, Password: #{password}" end private def default_password terminal.ask("Password [#{user}]: ") { |q| q.echo = "*" } end def terminal tty = open("/dev/tty", "w+") HighLine.new(tty, tty) end end clamp-1.1.1/examples/fubar0000755000004100000410000000034613007336773015515 0ustar www-datawww-data#! /usr/bin/env ruby # An example of subcommands require "clamp" Clamp do subcommand "foo", "Foo!" do subcommand ["bar", "bah", "baa"], "Baaaa!" do def execute puts "FUBAR" end end end end clamp-1.1.1/.autotest0000644000004100000410000000036113007336773014520 0ustar www-datawww-datarequire "autotest/bundler" Autotest.add_hook :initialize do |at| at.add_exception ".git" at.add_mapping(%r{^lib/(.*)\.rb$}, :prepend) do |_, match| ["spec/unit/#{match[1]}_spec.rb"] + Dir['spec/clamp/command*_spec.rb'] end end clamp-1.1.1/.rspec0000644000004100000410000000001013007336773013753 0ustar www-datawww-data--color clamp-1.1.1/spec/0000755000004100000410000000000013007336773013601 5ustar www-datawww-dataclamp-1.1.1/spec/spec_helper.rb0000644000004100000410000000136113007336773016420 0ustar www-datawww-datarequire "rspec" require "clamp" require "stringio" RSpec.configure do |config| config.mock_with :rr end module OutputCapture def self.included(target) target.before do $stdout = @out = StringIO.new $stderr = @err = StringIO.new end target.after do $stdout = STDOUT $stderr = STDERR end end def stdout @out.string end def stderr @err.string end end module CommandFactory def given_command(name, &block) let(:command_class) do Class.new(Clamp::Command, &block) end let(:command) do command_class.new(name) end end end module SetEnv def set_env(name, value) if value ENV[name] = value else ENV.delete(name) end end end clamp-1.1.1/spec/clamp/0000755000004100000410000000000013007336773014675 5ustar www-datawww-dataclamp-1.1.1/spec/clamp/option_module_spec.rb0000644000004100000410000000115413007336773021112 0ustar www-datawww-datarequire "spec_helper" describe Clamp::Command do include OutputCapture context "with included module" do let(:command) do shared_options = Module.new do extend Clamp::Option::Declaration option "--size", "SIZE", :default => 4 end command_class = Class.new(Clamp::Command) do include shared_options def execute puts "size = #{size}" end end command_class.new("foo") end it "accepts options from included module" do command.run(["--size", "42"]) expect(stdout).to eql "size = 42\n" end end end clamp-1.1.1/spec/clamp/parameter/0000755000004100000410000000000013007336773016655 5ustar www-datawww-dataclamp-1.1.1/spec/clamp/parameter/definition_spec.rb0000644000004100000410000001152513007336773022350 0ustar www-datawww-datarequire "spec_helper" describe Clamp::Parameter::Definition do context "normal" do let(:parameter) do described_class.new("COLOR", "hue of choice") end it "has a name" do expect(parameter.name).to eql "COLOR" end it "has a description" do expect(parameter.description).to eql "hue of choice" end it "is single-valued" do expect(parameter).to_not be_multivalued end describe "#attribute_name" do it "is derived from the name" do expect(parameter.attribute_name).to eql "color" end it "can be overridden" do parameter = described_class.new("COLOR", "hue of choice", :attribute_name => "hue") expect(parameter.attribute_name).to eql "hue" end end describe "#consume" do it "consumes one argument" do arguments = %w(a b c) expect(parameter.consume(arguments)).to eql ["a"] expect(arguments).to eql %w(b c) end describe "with no arguments" do it "raises an Argument error" do arguments = [] expect do parameter.consume(arguments) end.to raise_error(ArgumentError) end end end end context "optional (name in square brackets)" do let(:parameter) do described_class.new("[COLOR]", "hue of choice") end it "is single-valued" do expect(parameter).to_not be_multivalued end describe "#attribute_name" do it "omits the brackets" do expect(parameter.attribute_name).to eql "color" end end describe "#consume" do it "consumes one argument" do arguments = %w(a b c) expect(parameter.consume(arguments)).to eql ["a"] expect(arguments).to eql %w(b c) end describe "with no arguments" do it "consumes nothing" do arguments = [] expect(parameter.consume(arguments)).to eql [] end end end end context "list (name followed by ellipsis)" do let(:parameter) do described_class.new("FILE ...", "files to process") end it "is multi-valued" do expect(parameter).to be_multivalued end describe "#attribute_name" do it "gets a _list suffix" do expect(parameter.attribute_name).to eql "file_list" end end describe "#append_method" do it "is derived from the attribute_name" do expect(parameter.append_method).to eql "append_to_file_list" end end describe "#consume" do it "consumes all the remaining arguments" do arguments = %w(a b c) expect(parameter.consume(arguments)).to eql %w(a b c) expect(arguments).to eql [] end describe "with no arguments" do it "raises an Argument error" do arguments = [] expect do parameter.consume(arguments) end.to raise_error(ArgumentError) end end end context "with a weird parameter name, and an explicit attribute_name" do let(:parameter) do described_class.new("KEY=VALUE ...", "config-settings", :attribute_name => :config_settings) end describe "#attribute_name" do it "is the specified one" do expect(parameter.attribute_name).to eql "config_settings" end end end end context "optional list" do let(:parameter) do described_class.new("[FILES] ...", "files to process") end it "is multi-valued" do expect(parameter).to be_multivalued end describe "#attribute_name" do it "gets a _list suffix" do expect(parameter.attribute_name).to eql "files_list" end end describe "#default_value" do it "is an empty list" do expect(parameter.default_value).to eql [] end end describe "#help" do it "does not include default" do expect(parameter.help_rhs).to_not include("default:") end end context "with specified default value" do let(:parameter) do described_class.new("[FILES] ...", "files to process", :default => %w(a b c)) end describe "#default_value" do it "is that specified" do expect(parameter.default_value).to eql %w(a b c) end end describe "#help" do it "includes the default value" do expect(parameter.help_rhs).to include("default:") end end describe "#consume" do it "consumes all the remaining arguments" do arguments = %w(a b c) expect(parameter.consume(arguments)).to eql %w(a b c) expect(arguments).to eql [] end context "with no arguments" do it "don't override defaults" do arguments = [] expect(parameter.consume(arguments)).to eql [] end end end end end end clamp-1.1.1/spec/clamp/option/0000755000004100000410000000000013007336773016205 5ustar www-datawww-dataclamp-1.1.1/spec/clamp/option/definition_spec.rb0000644000004100000410000001516513007336773021704 0ustar www-datawww-datarequire "spec_helper" describe Clamp::Option::Definition do context "with String argument" do let(:option) do described_class.new("--key-file", "FILE", "SSH identity") end it "has a long_switch" do expect(option.long_switch).to eql "--key-file" end it "has a type" do expect(option.type).to eql "FILE" end it "has a description" do expect(option.description).to eql "SSH identity" end describe "#attribute_name" do it "is derived from the (long) switch" do expect(option.attribute_name).to eql "key_file" end it "can be overridden" do option = described_class.new("--key-file", "FILE", "SSH identity", :attribute_name => "ssh_identity") expect(option.attribute_name).to eql "ssh_identity" end end describe "#write_method" do it "is derived from the attribute_name" do expect(option.write_method).to eql "key_file=" end end describe "#default_value" do it "defaults to nil" do option = described_class.new("-n", "N", "iterations") expect(option.default_value).to eql nil end it "can be overridden" do option = described_class.new("-n", "N", "iterations", :default => 1) expect(option.default_value).to eql 1 end end describe "#help" do it "combines switch, type and description" do expect(option.help).to eql ["--key-file FILE", "SSH identity"] end end end context "flag" do let(:option) do described_class.new("--verbose", :flag, "Blah blah blah") end describe "#default_conversion_block" do it "converts truthy values to true" do expect(option.default_conversion_block.call("true")).to eql true expect(option.default_conversion_block.call("yes")).to eql true end it "converts falsey values to false" do expect(option.default_conversion_block.call("false")).to eql false expect(option.default_conversion_block.call("no")).to eql false end end describe "#help" do it "excludes option argument" do expect(option.help).to eql ["--verbose", "Blah blah blah"] end end end context "negatable flag" do let(:option) do described_class.new("--[no-]force", :flag, "Force installation") end it "handles both positive and negative forms" do expect(option.handles?("--force")).to be true expect(option.handles?("--no-force")).to be true end describe "#flag_value" do it "returns true for the positive variant" do expect(option.flag_value("--force")).to be true expect(option.flag_value("--no-force")).to be false end end describe "#attribute_name" do it "is derived from the (long) switch" do expect(option.attribute_name).to eql "force" end end end context "with both short and long switches" do let(:option) do described_class.new(["-k", "--key-file"], "FILE", "SSH identity") end it "handles both switches" do expect(option.handles?("--key-file")).to be true expect(option.handles?("-k")).to be true end describe "#help" do it "includes both switches" do expect(option.help).to eql ["-k, --key-file FILE", "SSH identity"] end end end context "with an associated environment variable" do let(:option) do described_class.new("-x", "X", "mystery option", :environment_variable => "APP_X") end describe "#help" do it "describes environment variable" do expect(option.help).to eql ["-x X", "mystery option (default: $APP_X)"] end end context "and a default value" do let(:option) do described_class.new("-x", "X", "mystery option", :environment_variable => "APP_X", :default => "xyz") end describe "#help" do it "describes both environment variable and default" do expect(option.help).to eql ["-x X", %{mystery option (default: $APP_X, or "xyz")}] end end end end context "multivalued" do let(:option) do described_class.new(["-H", "--header"], "HEADER", "extra header", :multivalued => true) end it "is multivalued" do expect(option).to be_multivalued end describe "#default_value" do it "defaults to an empty Array" do expect(option.default_value).to eql [] end it "can be overridden" do option = described_class.new("-H", "HEADER", "extra header", :multivalued => true, :default => [1, 2, 3]) expect(option.default_value).to eql [1, 2, 3] end end describe "#attribute_name" do it "gets a _list suffix" do expect(option.attribute_name).to eql "header_list" end end describe "#append_method" do it "is derived from the attribute_name" do expect(option.append_method).to eql "append_to_header_list" end end end describe "in subcommand" do let(:command_class) do Class.new(Clamp::Command) do subcommand "foo", "FOO!" do option "--bar", "BAR", "Bars foo." end end end describe "Command#help" do it "includes help for each option exactly once" do subcommand = command_class.send(:find_subcommand, "foo") subcommand_help = subcommand.subcommand_class.help("") expect(subcommand_help.lines.grep(/--bar BAR/).count).to eql 1 end end end describe "a required option" do it "rejects :default" do expect do described_class.new("--key-file", "FILE", "SSH identity", :required => true, :default => "hello") end.to raise_error(ArgumentError) end it "rejects :flag options" do expect do described_class.new("--awesome", :flag, "Be awesome?", :required => true) end.to raise_error(ArgumentError) end end describe "a hidden option" do let(:option) { described_class.new("--unseen", :flag, "Something", :hidden => true) } it "is hidden" do expect(option).to be_hidden end end describe "a hidden option in a command" do let(:command_class) do Class.new(Clamp::Command) do option "--unseen", :flag, "Something", :hidden => true def execute # this space intentionally left blank end end end it "is not shown in the help" do expect(command_class.help("foo")).not_to match /^ +--unseen +Something$/ end it "sets the expected accessor" do command = command_class.new("foo") command.run(["--unseen"]) expect(command.unseen?).to be_truthy end end end clamp-1.1.1/spec/clamp/messages_spec.rb0000644000004100000410000000200713007336773020042 0ustar www-datawww-data require "spec_helper" describe Clamp::Messages do describe "message" do before do Clamp.messages = { :too_many_arguments => "Way too many!", :custom_message => "Say %s to %s" } end after do Clamp.clear_messages! end it "allows setting custom messages" do expect(Clamp.message(:too_many_arguments)).to eql "Way too many!" end it "fallbacks to a default message" do expect(Clamp.message(:no_value_provided)).to eql "no value provided" end it "formats the message" do expect(Clamp.message(:custom_message, :what => "hello", :whom => "Clamp")).to eql "Say hello to Clamp" end end describe "clear_messages!" do it "clears messages to the defualt state" do default_msg = Clamp.message(:too_many_arguments).clone Clamp.messages = { :too_many_arguments => "Way too many!" } Clamp.clear_messages! expect(Clamp.message(:too_many_arguments)).to eql default_msg end end end clamp-1.1.1/spec/clamp/command_group_spec.rb0000644000004100000410000001760313007336773021075 0ustar www-datawww-datarequire "spec_helper" describe Clamp::Command do extend CommandFactory include OutputCapture context "with subcommands" do given_command "flipflop" do def execute puts message end subcommand "flip", "flip it" do def message "FLIPPED" end end subcommand "flop", "flop it\nfor extra flop" do def message "FLOPPED" end end end it "delegates to sub-commands" do command.run(["flip"]) expect(stdout).to match(/FLIPPED/) command.run(["flop"]) expect(stdout).to match(/FLOPPED/) end context "executed with no subcommand" do it "triggers help" do expect do command.run([]) end.to raise_error(Clamp::HelpWanted) end end describe "#help" do it "shows subcommand parameters in usage" do expect(command.help).to include("flipflop [OPTIONS] SUBCOMMAND [ARG] ...") end it "lists subcommands" do help = command.help expect(help).to match(/Subcommands:/) expect(help).to match(/flip +flip it/) expect(help).to match(/flop +flop it/) end it "handles new lines in subcommand descriptions" do expect(command.help).to match(/flop +flop it\n +for extra flop/) end end describe ".find_subcommand_class" do it "finds subcommand classes" do flip_class = command_class.find_subcommand_class("flip") expect(flip_class.new("xx").message).to eq("FLIPPED") end end end context "with an aliased subcommand" do given_command "blah" do subcommand ["say", "talk"], "Say something" do parameter "WORD ...", "stuff to say" def execute puts word_list end end end it "responds to both aliases" do command.run(["say", "boo"]) expect(stdout).to match(/boo/) command.run(["talk", "jive"]) expect(stdout).to match(/jive/) end describe "#help" do it "lists all aliases" do help = command.help expect(help).to match(/say, talk .* Say something/) end end end context "with nested subcommands" do given_command "fubar" do subcommand "foo", "Foo!" do subcommand "bar", "Baaaa!" do def self.this_is_bar end def execute puts "FUBAR" end end end end it "delegates multiple levels" do command.run(["foo", "bar"]) expect(stdout).to match(/FUBAR/) end describe ".find_subcommand_class" do it "finds nested subcommands" do expect(command_class.find_subcommand_class("foo", "bar")).to respond_to(:this_is_bar) end end end context "with a default subcommand" do given_command "admin" do self.default_subcommand = "status" subcommand "status", "Show status" do def execute puts "All good!" end end end context "executed with no subcommand" do it "invokes the default subcommand" do command.run([]) expect(stdout).to match(/All good/) end end end context "with a default subcommand, declared the old way" do given_command "admin" do default_subcommand "status", "Show status" do def execute puts "All good!" end end end context "executed with no subcommand" do it "invokes the default subcommand" do command.run([]) expect(stdout).to match(/All good/) end end end context "declaring a default subcommand after subcommands" do it "is not supported" do expect do Class.new(Clamp::Command) do subcommand "status", "Show status" do def execute puts "All good!" end end self.default_subcommand = "status" end end.to raise_error(/default_subcommand must be defined before subcommands/) end end context "with subcommands, declared after a parameter" do given_command "with" do parameter "THING", "the thing" subcommand "spit", "spit it" do def execute puts "spat the #{thing}" end end subcommand "say", "say it" do subcommand "loud", "yell it" do def execute puts thing.upcase end end end end it "allows the parameter to be specified first" do command.run(["dummy", "spit"]) expect(stdout.strip).to eql "spat the dummy" end it "passes the parameter down the stack" do command.run(["money", "say", "loud"]) expect(stdout.strip).to eql "MONEY" end end describe "each subcommand" do let(:command_class) do speed_options = Module.new do extend Clamp::Option::Declaration option "--speed", "SPEED", "how fast", :default => "slowly" end Class.new(Clamp::Command) do option "--direction", "DIR", "which way", :default => "home" include speed_options subcommand "move", "move in the appointed direction" do def execute motion = context[:motion] || "walking" puts "#{motion} #{direction} #{speed}" end end end end let(:command) do command_class.new("go") end it "accepts options defined in superclass (specified after the subcommand)" do command.run(["move", "--direction", "north"]) expect(stdout).to match(/walking north/) end it "accepts options defined in superclass (specified before the subcommand)" do command.run(["--direction", "north", "move"]) expect(stdout).to match(/walking north/) end it "accepts options defined in included modules" do command.run(["move", "--speed", "very quickly"]) expect(stdout).to match(/walking home very quickly/) end it "has access to command context" do command = command_class.new("go", :motion => "wandering") command.run(["move"]) expect(stdout).to match(/wandering home/) end end context "with a subcommand, with options" do given_command "weeheehee" do option "--json", "JSON", "a json blob" do |option| print "parsing!" option end subcommand "woohoohoo", "like weeheehee but with more o" do def execute end end end it "only parses options once" do command.run(["--json", '{"a":"b"}', "woohoohoo"]) expect(stdout).to eql "parsing!" end end context "with an unknown subcommand" do let(:subcommand_missing) do Module.new do def subcommand_missing(_name) abort "there is no such thing" end end end let(:subcommand_missing_with_return) do Module.new do def subcommand_missing(_name) self.class.recognised_subcommands.first.subcommand_class end end end let(:command_class) do Class.new(Clamp::Command) do subcommand "test", "test subcommand" do def execute puts "known subcommand" end end def execute end end end let(:command) do command_class.new("foo") end it "should signal no such subcommand usage error" do expect { command.run(["foo"]) }.to raise_error(Clamp::UsageError) do |exception| expect(exception.message).to eq "No such sub-command 'foo'" end end it "should execute the subcommand missing method" do command.extend subcommand_missing expect { command.run(["foo"]) }.to raise_error(SystemExit) expect(stderr).to match(/there is no such thing/) end it "should use the subcommand class returned from subcommand_missing" do command.extend subcommand_missing_with_return command.run(["foo"]) expect(stdout).to match(/known subcommand/) end end end clamp-1.1.1/spec/clamp/command_spec.rb0000644000004100000410000005350713007336773017664 0ustar www-datawww-data require "spec_helper" describe Clamp::Command do extend CommandFactory include OutputCapture include SetEnv given_command("cmd") do def execute puts "Hello, world" end end describe "#help" do it "describes usage" do expect(command.help).to match(/^Usage:\n cmd.*\n/) end end describe "#run" do before do command.run([]) end it "executes the #execute method" do expect(stdout).to_not be_empty end end describe ".option" do it "declares option argument accessors" do command.class.option "--flavour", "FLAVOUR", "Flavour of the month" expect(command.flavour).to eql nil command.flavour = "chocolate" expect(command.flavour).to eql "chocolate" end context "with type :flag" do before do command.class.option "--verbose", :flag, "Be heartier" end it "declares a predicate-style reader" do expect(command).to respond_to(:verbose?) expect(command).to_not respond_to(:verbose) end end context "with explicit :attribute_name" do before do command.class.option "--foo", "FOO", "A foo", :attribute_name => :bar end it "uses the specified attribute_name name to name accessors" do command.bar = "chocolate" expect(command.bar).to eql "chocolate" end it "does not attempt to create the default accessors" do expect(command).to_not respond_to(:foo) expect(command).to_not respond_to(:foo=) end end context "with default method" do before do command.class.option "--port", "PORT", "port" command.class.class_eval do def default_port 4321 end end end it "sets the specified default value" do expect(command.port).to eql 4321 end end context "with :default value" do before do command.class.option "--port", "PORT", "port to listen on", :default => 4321 end it "declares default method" do expect(command.default_port).to eql 4321 end describe "#help" do it "describes the default value" do expect(command.help).to include("port to listen on (default: 4321)") end end end context "with :multivalued" do before do command.class.option "--flavour", "FLAVOUR", "flavour(s)", :multivalued => true, :attribute_name => :flavours end it "defaults to empty array" do expect(command.flavours).to eql [] end it "supports multiple values" do command.parse(%w(--flavour chocolate --flavour vanilla)) expect(command.flavours).to eql %w(chocolate vanilla) end it "generates a single-value appender method" do command.append_to_flavours("mud") command.append_to_flavours("pie") expect(command.flavours).to eql %w(mud pie) end it "generates a multi-value setter method" do command.append_to_flavours("replaceme") command.flavours = %w(mud pie) expect(command.flavours).to eql %w(mud pie) end end context "with :environment_variable" do let(:environment_value) { nil } let(:args) { [] } before do command.class.option "--port", "PORT", "port to listen on", :default => 4321, :environment_variable => "PORT", &:to_i set_env("PORT", environment_value) command.parse(args) end context "when no environment variable is present" do it "uses the default" do expect(command.port).to eql 4321 end end context "when environment variable is present" do let(:environment_value) { "12345" } it "uses the environment variable" do expect(command.port).to eql 12_345 end context "and a value is specified on the command-line" do let(:args) { %w(--port 1500) } it "uses command-line value" do expect(command.port).to eql 1500 end end end describe "#help" do it "describes the default value and env usage" do expect(command.help).to include("port to listen on (default: $PORT, or 4321)") end end end context "with :environment_variable and type :flag" do let(:environment_value) { nil } before do command.class.option "--[no-]enable", :flag, "enable?", :default => false, :environment_variable => "ENABLE" set_env("ENABLE", environment_value) command.parse([]) end context "when no environment variable is present" do it "uses the default" do expect(command.enable?).to eql false end end %w(1 yes enable on true).each do |truthy_value| context "when environment variable is #{truthy_value.inspect}" do let(:environment_value) { truthy_value } it "sets the flag" do expect(command.enable?).to eql true end end end %w(0 no disable off false).each do |falsey_value| context "when environment variable is #{falsey_value.inspect}" do let(:environment_value) { falsey_value } it "clears the flag" do expect(command.enable?).to eql false end end end end context "with :required" do before do command.class.option "--port", "PORT", "port to listen on", :required => true end context "when no value is provided" do it "raises a UsageError" do expect do command.parse([]) end.to raise_error(Clamp::UsageError) end end context "when a value is provided" do it "does not raise an error" do expect do command.parse(["--port", "12345"]) end.not_to raise_error end end end context "with a block" do before do command.class.option "--port", "PORT", "Port to listen on" do |port| Integer(port) end end it "uses the block to validate and convert the option argument" do expect do command.port = "blah" end.to raise_error(ArgumentError) command.port = "1234" expect(command.port).to eql 1234 end end end context "with options declared" do before do command.class.option ["-f", "--flavour"], "FLAVOUR", "Flavour of the month" command.class.option ["-c", "--color"], "COLOR", "Preferred hue" command.class.option ["--scoops"], "N", "Number of scoops", :default => 1, :environment_variable => "DEFAULT_SCOOPS" do |arg| Integer(arg) end command.class.option ["-n", "--[no-]nuts"], :flag, "Nuts (or not)\nMay include nuts" command.class.parameter "[ARG] ...", "extra arguments", :attribute_name => :arguments end describe "#parse" do context "with an unrecognised option" do it "raises a UsageError" do expect do command.parse(%w(--foo bar)) end.to raise_error(Clamp::UsageError) end end context "with options" do before do command.parse(%w(--flavour strawberry --nuts --color blue)) end it "maps the option values onto the command object" do expect(command.flavour).to eql "strawberry" expect(command.color).to eql "blue" expect(command.nuts?).to eql true end end context "with short options" do before do command.parse(%w(-f strawberry -c blue)) end it "recognises short options as aliases" do expect(command.flavour).to eql "strawberry" expect(command.color).to eql "blue" end end context "with a value appended to a short option" do before do command.parse(%w(-fstrawberry)) end it "works as though the value were separated" do expect(command.flavour).to eql "strawberry" end end context "with combined short options" do before do command.parse(%w(-nf strawberry)) end it "works as though the options were separate" do expect(command.flavour).to eql "strawberry" expect(command.nuts?).to eql true end end context "with option arguments attached using equals sign" do before do command.parse(%w(--flavour=strawberry --color=blue)) end it "works as though the option arguments were separate" do expect(command.flavour).to eql "strawberry" expect(command.color).to eql "blue" end end context "with option-like things beyond the arguments" do it "treats them as positional arguments" do command.parse(%w(a b c --flavour strawberry)) expect(command.arguments).to eql %w(a b c --flavour strawberry) end end context "with multi-line arguments that look like options" do before do command.parse(["foo\n--flavour=strawberry", "bar\n-cblue"]) end it "treats them as positional arguments" do expect(command.arguments).to eql ["foo\n--flavour=strawberry", "bar\n-cblue"] expect(command.flavour).to be_nil expect(command.color).to be_nil end end context "with an option terminator" do it "considers everything after the terminator to be an argument" do command.parse(%w(--color blue -- --flavour strawberry)) expect(command.arguments).to eql %w(--flavour strawberry) end end context "with --flag" do before do command.parse(%w(--nuts)) end it "sets the flag" do expect(command.nuts?).to be true end end context "with --no-flag" do before do command.nuts = true command.parse(%w(--no-nuts)) end it "clears the flag" do expect(command.nuts?).to be false end end context "with --help" do it "requests help" do expect do command.parse(%w(--help)) end.to raise_error(Clamp::HelpWanted) end end context "with -h" do it "requests help" do expect do command.parse(%w(-h)) end.to raise_error(Clamp::HelpWanted) end end context "when a bad option value is specified on the command-line" do it "signals a UsageError" do expect do command.parse(%w(--scoops reginald)) end.to raise_error(Clamp::UsageError, /^option '--scoops': invalid value for Integer/) end end context "when a bad option value is specified in the environment" do it "signals a UsageError" do ENV["DEFAULT_SCOOPS"] = "marjorie" expect do command.parse([]) end.to raise_error(Clamp::UsageError, /^\$DEFAULT_SCOOPS: invalid value for Integer/) end end end describe "#help" do it "indicates that there are options" do expect(command.help).to include("cmd [OPTIONS]") end it "includes option details" do expect(command.help).to match(/--flavour FLAVOUR +Flavour of the month/) expect(command.help).to match(/--color COLOR +Preferred hue/) end it "handles new lines in option descriptions" do expect(command.help).to match(/--\[no-\]nuts +Nuts \(or not\)\n +May include nuts/) end end end context "with an explicit --help option declared" do before do command.class.option ["--help"], :flag, "help wanted" end it "does not generate implicit help option" do expect do command.parse(%w(--help)) end.to_not raise_error expect(command.help?).to be true end it "does not recognise -h" do expect do command.parse(%w(-h)) end.to raise_error(Clamp::UsageError) end end context "with an explicit -h option declared" do before do command.class.option ["-h", "--humidity"], "PERCENT", "relative humidity" do |n| Integer(n) end end it "does not map -h to help" do expect(command.help).to_not match(/-h[, ].*help/) end it "still recognises --help" do expect do command.parse(%w(--help)) end.to raise_error(Clamp::HelpWanted) end end describe ".parameter" do it "declares option argument accessors" do command.class.parameter "FLAVOUR", "flavour of the month" expect(command.flavour).to eql nil command.flavour = "chocolate" expect(command.flavour).to eql "chocolate" end context "with explicit :attribute_name" do before do command.class.parameter "FOO", "a foo", :attribute_name => :bar end it "uses the specified attribute_name name to name accessors" do command.bar = "chocolate" expect(command.bar).to eql "chocolate" end end context "with :default value" do before do command.class.parameter "[ORIENTATION]", "direction", :default => "west" end it "sets the specified default value" do expect(command.orientation).to eql "west" end describe "#help" do it "describes the default value" do expect(command.help).to include("direction (default: \"west\")") end end end context "with a block" do before do command.class.parameter "PORT", "port to listen on" do |port| Integer(port) end end it "uses the block to validate and convert the argument" do expect do command.port = "blah" end.to raise_error(ArgumentError) command.port = "1234" expect(command.port).to eql 1234 end end context "with ellipsis" do before do command.class.parameter "FILE ...", "files" end it "accepts multiple arguments" do command.parse(%w(X Y Z)) expect(command.file_list).to eql %w(X Y Z) end end context "optional, with ellipsis" do before do command.class.parameter "[FILE] ...", "files" end it "defaults to an empty list" do command.parse([]) expect(command.default_file_list).to eql [] expect(command.file_list).to eql [] end it "is mutable" do command.parse([]) command.file_list << "treasure" expect(command.file_list).to eql ["treasure"] end end context "with :environment_variable" do before do command.class.parameter "[FILE]", "a file", :environment_variable => "FILE", :default => "/dev/null" end let(:args) { [] } let(:environment_value) { nil } before do set_env("FILE", environment_value) command.parse(args) end context "when neither argument nor environment variable are present" do it "uses the default" do expect(command.file).to eql "/dev/null" end end context "when environment variable is present" do let(:environment_value) { "/etc/motd" } describe "and no argument is provided" do it "uses the environment variable" do expect(command.file).to eql "/etc/motd" end end describe "and an argument is provided" do let(:args) { ["/dev/null"] } it "uses the argument" do expect(command.file).to eql "/dev/null" end end end describe "#help" do it "describes the default value and env usage" do expect(command.help).to include(%{ (default: $FILE, or "/dev/null")}) end end end end context "with no parameters declared" do describe "#parse" do context "with arguments" do it "raises a UsageError" do expect do command.parse(["crash"]) end.to raise_error(Clamp::UsageError, "too many arguments") end end end end context "with parameters declared" do before do command.class.parameter "X", "x\nxx" command.class.parameter "Y", "y" command.class.parameter "[Z]", "z", :default => "ZZZ" end describe "#parse" do context "with arguments for all parameters" do before do command.parse(["crash", "bang", "wallop"]) end it "maps arguments onto the command object" do expect(command.x).to eql "crash" expect(command.y).to eql "bang" expect(command.z).to eql "wallop" end end context "with insufficient arguments" do it "raises a UsageError" do expect do command.parse(["crash"]) end.to raise_error(Clamp::UsageError, "parameter 'Y': no value provided") end end context "with optional argument omitted" do it "defaults the optional argument" do command.parse(["crash", "bang"]) expect(command.x).to eql "crash" expect(command.y).to eql "bang" expect(command.z).to eql "ZZZ" end end context "with multi-line arguments" do it "parses them correctly" do command.parse(["foo\nhi", "bar", "baz"]) expect(command.x).to eql "foo\nhi" expect(command.y).to eql "bar" expect(command.z).to eql "baz" end end context "with too many arguments" do it "raises a UsageError" do expect do command.parse(["crash", "bang", "wallop", "kapow"]) end.to raise_error(Clamp::UsageError, "too many arguments") end end end describe "#help" do it "indicates that there are parameters" do expect(command.help).to include("cmd [OPTIONS] X Y [Z]") end it "includes parameter details" do expect(command.help).to match(/X +x/) expect(command.help).to match(/Y +y/) expect(command.help).to match(/\[Z\] +z \(default: "ZZZ"\)/) end it "handles new lines in option descriptions" do expect(command.help).to match(/X +x\n +xx/) end end end context "with explicit usage" do given_command("blah") do usage "FOO BAR ..." end describe "#help" do it "includes the explicit usage" do expect(command.help).to include("blah FOO BAR ...\n") end end end context "with multiple usages" do given_command("put") do usage "THIS HERE" usage "THAT THERE" end describe "#help" do it "includes both potential usages" do expect(command.help).to include("put THIS HERE\n") expect(command.help).to include("put THAT THERE\n") end end end context "with a banner" do given_command("punt") do banner <<-EOF Punt is an example command. It doesn't do much, really. The prefix at the beginning of this description should be normalised to two spaces. EOF end describe "#help" do it "includes the banner" do expect(command.help).to match(/^ Punt is an example command/) expect(command.help).to match(/^ The prefix/) end end end describe ".run" do it "creates a new Command instance and runs it" do command.class.class_eval do parameter "WORD ...", "words" def execute print word_list.inspect end end @xyz = %w(x y z) command.class.run("cmd", @xyz) expect(stdout).to eql @xyz.inspect end context "invoked with a context hash" do it "makes the context available within the command" do command.class.class_eval do def execute print context[:foo] end end command.class.run("xyz", [], :foo => "bar") expect(stdout).to eql "bar" end end context "when there's a CommandError" do before do command.class.class_eval do def execute signal_error "Oh crap!", :status => 456 end end begin command.class.run("cmd", []) rescue SystemExit => e @system_exit = e end end it "outputs the error message" do expect(stderr).to include "ERROR: Oh crap!" end it "exits with the specified status" do expect(@system_exit.status).to eql 456 end end context "when there's a UsageError" do before do command.class.class_eval do def execute signal_usage_error "bad dog!" end end begin command.class.run("cmd", []) rescue SystemExit => e @system_exit = e end end it "outputs the error message" do expect(stderr).to include "ERROR: bad dog!" end it "outputs help" do expect(stderr).to include "See: 'cmd --help'" end it "exits with a non-zero status" do expect(@system_exit).to_not be_nil expect(@system_exit.status).to eql 1 end end context "when help is requested" do it "outputs help" do command.class.run("cmd", ["--help"]) expect(stdout).to include "Usage:" end end end describe "subclass" do let(:command) do parent_command_class = Class.new(Clamp::Command) do option "--verbose", :flag, "be louder" end derived_command_class = Class.new(parent_command_class) do option "--iterations", "N", "number of times to go around" end derived_command_class.new("cmd") end it "inherits options from it's superclass" do command.parse(["--verbose"]) expect(command).to be_verbose end end end clamp-1.1.1/.travis.yml0000644000004100000410000000016213007336773014757 0ustar www-datawww-datalanguage: ruby rvm: - 2.1.8 - 2.2.4 - 2.3.1 before_install: - gem update --system - gem install bundler clamp-1.1.1/lib/0000755000004100000410000000000013007336773013415 5ustar www-datawww-dataclamp-1.1.1/lib/clamp.rb0000644000004100000410000000016013007336773015033 0ustar www-datawww-datarequire "clamp/version" require "clamp/command" def Clamp(&block) Class.new(Clamp::Command, &block).run end clamp-1.1.1/lib/clamp/0000755000004100000410000000000013007336773014511 5ustar www-datawww-dataclamp-1.1.1/lib/clamp/errors.rb0000644000004100000410000000125513007336773016355 0ustar www-datawww-datamodule Clamp class DeclarationError < StandardError end class RuntimeError < StandardError def initialize(message, command) super(message) @command = command end attr_reader :command end # raise to signal incorrect command usage class UsageError < RuntimeError; end # raise to request usage help class HelpWanted < RuntimeError def initialize(command) super("I need help", command) end end # raise to signal error during execution class ExecutionError < RuntimeError def initialize(message, command, status = 1) super(message, command) @status = status end attr_reader :status end end clamp-1.1.1/lib/clamp/subcommand/0000755000004100000410000000000013007336773016641 5ustar www-datawww-dataclamp-1.1.1/lib/clamp/subcommand/declaration.rb0000644000004100000410000000451613007336773021461 0ustar www-datawww-datarequire "clamp/errors" require "clamp/subcommand/definition" module Clamp module Subcommand module Declaration def recognised_subcommands @recognised_subcommands ||= [] end def subcommand(name, description, subcommand_class = self, &block) subcommand_class = Class.new(subcommand_class, &block) if block declare_subcommand_parameters unless has_subcommands? recognised_subcommands << Subcommand::Definition.new(name, description, subcommand_class) end def has_subcommands? !recognised_subcommands.empty? end def find_subcommand(name) recognised_subcommands.find { |sc| sc.is_called?(name) } end def find_subcommand_class(*names) names.inject(self) do |command_class, name| return nil unless command_class subcommand = command_class.find_subcommand(name) subcommand.subcommand_class if subcommand end end def inheritable_attributes recognised_options + inheritable_parameters end def default_subcommand=(name) if has_subcommands? raise Clamp::DeclarationError, "default_subcommand must be defined before subcommands" end @default_subcommand = name end def default_subcommand(*args, &block) if args.empty? @default_subcommand else $stderr.puts "WARNING: Clamp default_subcommand syntax has changed; check the README." $stderr.puts " (from #{caller.first})" self.default_subcommand = args.first subcommand(*args, &block) end end private def declare_subcommand_parameters if @default_subcommand parameter "[SUBCOMMAND]", "subcommand", :attribute_name => :subcommand_name, :default => @default_subcommand, :inheritable => false else parameter "SUBCOMMAND", "subcommand", :attribute_name => :subcommand_name, :required => false, :inheritable => false end remove_method :default_subcommand_name parameter "[ARG] ...", "subcommand arguments", :attribute_name => :subcommand_arguments, :inheritable => false end end end end clamp-1.1.1/lib/clamp/subcommand/execution.rb0000644000004100000410000000160313007336773021171 0ustar www-datawww-datamodule Clamp module Subcommand module Execution # override default Command behaviour def execute # delegate to subcommand subcommand = instantiate_subcommand(subcommand_name) subcommand.run(subcommand_arguments) end private def instantiate_subcommand(name) subcommand_class = find_subcommand_class(name) subcommand = subcommand_class.new("#{invocation_path} #{name}", context) self.class.inheritable_attributes.each do |attribute| next unless attribute.of(self).defined? attribute.of(subcommand).set(attribute.of(self).get) end subcommand end def find_subcommand_class(name) subcommand_def = self.class.find_subcommand(name) return subcommand_def.subcommand_class if subcommand_def subcommand_missing(name) end end end end clamp-1.1.1/lib/clamp/subcommand/definition.rb0000644000004100000410000000076013007336773021321 0ustar www-datawww-datamodule Clamp module Subcommand Definition = Struct.new(:name, :description, :subcommand_class) do def initialize(names, description, subcommand_class) @names = Array(names) @description = description @subcommand_class = subcommand_class end attr_reader :names, :description, :subcommand_class def is_called?(name) names.member?(name) end def help [names.join(", "), description] end end end end clamp-1.1.1/lib/clamp/subcommand/parsing.rb0000644000004100000410000000056613007336773020640 0ustar www-datawww-datarequire "clamp/subcommand/execution" module Clamp module Subcommand module Parsing protected def parse_subcommand return false unless self.class.has_subcommands? extend(Subcommand::Execution) end private def default_subcommand_name self.class.default_subcommand || request_help end end end end clamp-1.1.1/lib/clamp/command.rb0000644000004100000410000001006213007336773016453 0ustar www-datawww-datarequire "clamp/messages" require "clamp/errors" require "clamp/help" require "clamp/option/declaration" require "clamp/option/parsing" require "clamp/parameter/declaration" require "clamp/parameter/parsing" require "clamp/subcommand/declaration" require "clamp/subcommand/parsing" module Clamp # {Command} models a shell command. Each command invocation is a new object. # Command options and parameters are represented as attributes # (see {Command::Declaration}). # # The main entry-point is {#run}, which uses {#parse} to populate attributes based # on an array of command-line arguments, then calls {#execute} (which you provide) # to make it go. # class Command # Create a command execution. # # @param [String] invocation_path the path used to invoke the command # @param [Hash] context additional data the command may need # def initialize(invocation_path, context = {}) @invocation_path = invocation_path @context = context end # @return [String] the path used to invoke this command # attr_reader :invocation_path # @return [Array] unconsumed command-line arguments # attr_reader :remaining_arguments # Parse command-line arguments. # # @param [Array] arguments command-line arguments # @return [Array] unconsumed arguments # def parse(arguments) @remaining_arguments = arguments.dup parse_options parse_parameters parse_subcommand handle_remaining_arguments end # Run the command, with the specified arguments. # # This calls {#parse} to process the command-line arguments, # then delegates to {#execute}. # # @param [Array] arguments command-line arguments # def run(arguments) parse(arguments) execute end # Execute the command (assuming that all options/parameters have been set). # # This method is designed to be overridden in sub-classes. # def execute raise "you need to define #execute" end # @return [String] usage documentation for this command # def help self.class.help(invocation_path) end # Abort with subcommand missing usage error # # @ param [String] name subcommand_name def subcommand_missing(name) signal_usage_error(Clamp.message(:no_such_subcommand, :name => name)) end include Clamp::Option::Parsing include Clamp::Parameter::Parsing include Clamp::Subcommand::Parsing protected attr_accessor :context def handle_remaining_arguments signal_usage_error Clamp.message(:too_many_arguments) unless remaining_arguments.empty? end private def signal_usage_error(message) e = UsageError.new(message, self) e.set_backtrace(caller) raise e end def signal_error(message, options = {}) status = options.fetch(:status, 1) e = ExecutionError.new(message, self, status) e.set_backtrace(caller) raise e end def request_help raise HelpWanted, self end class << self include Clamp::Option::Declaration include Clamp::Parameter::Declaration include Clamp::Subcommand::Declaration include Help # Create an instance of this command class, and run it. # # @param [String] invocation_path the path used to invoke the command # @param [Array] arguments command-line arguments # @param [Hash] context additional data the command may need # def run(invocation_path = File.basename($PROGRAM_NAME), arguments = ARGV, context = {}) new(invocation_path, context).run(arguments) rescue Clamp::UsageError => e $stderr.puts "ERROR: #{e.message}" $stderr.puts "" $stderr.puts "See: '#{e.command.invocation_path} --help'" exit(1) rescue Clamp::HelpWanted => e puts e.command.help rescue Clamp::ExecutionError => e $stderr.puts "ERROR: #{e.message}" exit(e.status) rescue SignalException => e exit(128 + e.signo) end end end end clamp-1.1.1/lib/clamp/parameter/0000755000004100000410000000000013007336773016471 5ustar www-datawww-dataclamp-1.1.1/lib/clamp/parameter/declaration.rb0000644000004100000410000000160313007336773021303 0ustar www-datawww-datarequire "clamp/attribute/declaration" require "clamp/parameter/definition" module Clamp module Parameter module Declaration include Clamp::Attribute::Declaration def parameters @parameters ||= [] end def has_parameters? !parameters.empty? end def parameter(name, description, options = {}, &block) Parameter::Definition.new(name, description, options).tap do |parameter| define_accessors_for(parameter, &block) parameters << parameter end end protected def inheritable_parameters superclass_inheritable_parameters + parameters.select(&:inheritable?) end private def superclass_inheritable_parameters return [] unless superclass.respond_to?(:inheritable_parameters, true) superclass.inheritable_parameters end end end end clamp-1.1.1/lib/clamp/parameter/definition.rb0000644000004100000410000000240213007336773021144 0ustar www-datawww-datarequire "clamp/attribute/definition" module Clamp module Parameter class Definition < Attribute::Definition def initialize(name, description, options = {}) @name = name @description = description super(options) @multivalued = (@name =~ ELLIPSIS_SUFFIX) @required = options.fetch(:required) do (@name !~ OPTIONAL) end @inheritable = options.fetch(:inheritable, true) end attr_reader :name def inheritable? @inheritable end def help_lhs name end def consume(arguments) raise ArgumentError, Clamp.message(:no_value_provided) if required? && arguments.empty? arguments.shift(multivalued? ? arguments.length : 1) end private ELLIPSIS_SUFFIX = / \.\.\.$/ OPTIONAL = /^\[(.*)\]/ VALID_ATTRIBUTE_NAME = /^[a-z0-9_]+$/ def infer_attribute_name inferred_name = name.downcase.tr("-", "_").sub(ELLIPSIS_SUFFIX, "").sub(OPTIONAL) { Regexp.last_match(1) } unless inferred_name =~ VALID_ATTRIBUTE_NAME raise "cannot infer attribute_name from #{name.inspect}" end inferred_name += "_list" if multivalued? inferred_name end end end end clamp-1.1.1/lib/clamp/parameter/parsing.rb0000644000004100000410000000147713007336773020472 0ustar www-datawww-datamodule Clamp module Parameter module Parsing protected def parse_parameters set_parameters_from_command_line default_parameters_from_environment end private def set_parameters_from_command_line self.class.parameters.each do |parameter| begin parameter.consume(remaining_arguments).each do |value| parameter.of(self).take(value) end rescue ArgumentError => e signal_usage_error Clamp.message(:parameter_argument_error, :param => parameter.name, :message => e.message) end end end def default_parameters_from_environment self.class.parameters.each do |parameter| parameter.of(self).default_from_environment end end end end end clamp-1.1.1/lib/clamp/option/0000755000004100000410000000000013007336773016021 5ustar www-datawww-dataclamp-1.1.1/lib/clamp/option/declaration.rb0000644000004100000410000000277413007336773020645 0ustar www-datawww-datarequire "clamp/attribute/declaration" require "clamp/option/definition" module Clamp module Option module Declaration include Clamp::Attribute::Declaration def option(switches, type, description, opts = {}, &block) Option::Definition.new(switches, type, description, opts).tap do |option| block ||= option.default_conversion_block define_accessors_for(option, &block) declared_options << option end end def find_option(switch) recognised_options.find { |o| o.handles?(switch) } end def declared_options @declared_options ||= [] end def recognised_options unless @implicit_options_declared declare_implicit_help_option @implicit_options_declared = true end effective_options end private def declare_implicit_help_option return false if effective_options.find { |o| o.handles?("--help") } help_switches = ["--help"] help_switches.unshift("-h") unless effective_options.find { |o| o.handles?("-h") } option help_switches, :flag, "print help" do request_help end end def effective_options ancestors.inject([]) do |options, ancestor| options + options_declared_on(ancestor) end end def options_declared_on(ancestor) return [] unless ancestor.is_a?(Clamp::Option::Declaration) ancestor.declared_options end end end end clamp-1.1.1/lib/clamp/option/definition.rb0000644000004100000410000000422613007336773020502 0ustar www-datawww-datarequire "clamp/attribute/definition" require "clamp/truthy" module Clamp module Option class Definition < Attribute::Definition def initialize(switches, type, description, options = {}) @switches = Array(switches) @type = type @description = description super(options) @multivalued = options[:multivalued] return unless options.key?(:required) @required = options[:required] # Do some light validation for conflicting settings. raise ArgumentError, "Specifying a :default value with :required doesn't make sense" if options.key?(:default) raise ArgumentError, "A required flag (boolean) doesn't make sense." if type == :flag end attr_reader :switches, :type def long_switch switches.find { |switch| switch =~ /^--/ } end def handles?(switch) recognised_switches.member?(switch) end def flag? @type == :flag end def flag_value(switch) !(switch =~ /^--no-(.*)/ && switches.member?("--\[no-\]#{Regexp.last_match(1)}")) end def read_method if flag? super + "?" else super end end def extract_value(switch, arguments) if flag? flag_value(switch) else arguments.shift end end def default_conversion_block Clamp.method(:truthy?) if flag? end def help_lhs lhs = switches.join(", ") lhs += " " + type unless flag? lhs end private def recognised_switches switches.map do |switch| if switch =~ /^--\[no-\](.*)/ ["--#{Regexp.last_match(1)}", "--no-#{Regexp.last_match(1)}"] else switch end end.flatten end def infer_attribute_name unless long_switch raise Clamp::DeclarationError, "You must specify either a long-switch or an :attribute_value" end inferred_name = long_switch.sub(/^--(\[no-\])?/, "").tr("-", "_") inferred_name += "_list" if multivalued? inferred_name end end end end clamp-1.1.1/lib/clamp/option/parsing.rb0000644000004100000410000000445113007336773020015 0ustar www-datawww-datamodule Clamp module Option module Parsing protected def parse_options set_options_from_command_line default_options_from_environment verify_required_options_are_set end private def set_options_from_command_line while remaining_arguments.first && remaining_arguments.first.start_with?("-") switch = remaining_arguments.shift break if switch == "--" case switch when /\A(-\w)(.+)\z/m # combined short options switch = Regexp.last_match(1) if find_option(switch).flag? remaining_arguments.unshift("-" + Regexp.last_match(2)) else remaining_arguments.unshift(Regexp.last_match(2)) end when /\A(--[^=]+)=(.*)\z/m switch = Regexp.last_match(1) remaining_arguments.unshift(Regexp.last_match(2)) end option = find_option(switch) value = option.extract_value(switch, remaining_arguments) begin option.of(self).take(value) rescue ArgumentError => e signal_usage_error Clamp.message(:option_argument_error, :switch => switch, :message => e.message) end end end def default_options_from_environment self.class.recognised_options.each do |option| option.of(self).default_from_environment end end def verify_required_options_are_set self.class.recognised_options.each do |option| # If this option is required and the value is nil, there's an error. next unless option.required? && send(option.attribute_name).nil? if option.environment_variable message = Clamp.message(:option_or_env_required, :option => option.switches.first, :env => option.environment_variable) else message = Clamp.message(:option_required, :option => option.switches.first) end signal_usage_error message end end def find_option(switch) self.class.find_option(switch) || signal_usage_error(Clamp.message(:unrecognised_option, :switch => switch)) end end end end clamp-1.1.1/lib/clamp/help.rb0000644000004100000410000000434713007336773015776 0ustar www-datawww-datarequire "stringio" require "clamp/messages" module Clamp module Help def usage(usage) @declared_usage_descriptions ||= [] @declared_usage_descriptions << usage end attr_reader :declared_usage_descriptions def description=(description) @description = description.dup if @description =~ /^\A\n*( +)/ indent = Regexp.last_match(1) @description.gsub!(/^#{indent}/, "") end @description.strip! end def banner(description) self.description = description end attr_reader :description def derived_usage_description parts = ["[OPTIONS]"] parts += parameters.map(&:name) parts.join(" ") end def usage_descriptions declared_usage_descriptions || [derived_usage_description] end def help(invocation_path, builder = Builder.new) help = builder help.add_usage(invocation_path, usage_descriptions) help.add_description(description) if has_parameters? help.add_list(Clamp.message(:parameters_heading), parameters) end if has_subcommands? help.add_list(Clamp.message(:subcommands_heading), recognised_subcommands) end help.add_list(Clamp.message(:options_heading), recognised_options) help.string end class Builder def initialize @out = StringIO.new end def string @out.string end def add_usage(invocation_path, usage_descriptions) puts Clamp.message(:usage_heading) + ":" usage_descriptions.each do |usage| puts " #{invocation_path} #{usage}".rstrip end end def add_description(description) return unless description puts "" puts description.gsub(/^/, " ") end DETAIL_FORMAT = " %-29s %s".freeze def add_list(heading, items) puts "\n#{heading}:" items.reject { |i| i.respond_to?(:hidden?) && i.hidden? }.each do |item| label, description = item.help description.each_line do |line| puts format(DETAIL_FORMAT, label, line) label = "" end end end private def puts(*args) @out.puts(*args) end end end end clamp-1.1.1/lib/clamp/version.rb0000644000004100000410000000005413007336773016522 0ustar www-datawww-datamodule Clamp VERSION = "1.1.1".freeze end clamp-1.1.1/lib/clamp/truthy.rb0000644000004100000410000000022213007336773016371 0ustar www-datawww-datamodule Clamp TRUTHY_VALUES = %w(1 yes enable on true).freeze def self.truthy?(arg) TRUTHY_VALUES.include?(arg.to_s.downcase) end end clamp-1.1.1/lib/clamp/attribute/0000755000004100000410000000000013007336773016514 5ustar www-datawww-dataclamp-1.1.1/lib/clamp/attribute/declaration.rb0000644000004100000410000000254013007336773021327 0ustar www-datawww-datamodule Clamp module Attribute module Declaration protected def define_accessors_for(attribute, &block) define_reader_for(attribute) define_default_for(attribute) if attribute.multivalued? define_appender_for(attribute, &block) define_multi_writer_for(attribute) else define_simple_writer_for(attribute, &block) end end private def define_reader_for(attribute) define_method(attribute.read_method) do attribute.of(self)._read end end def define_default_for(attribute) define_method(attribute.default_method) do attribute.default_value end end def define_simple_writer_for(attribute, &block) define_method(attribute.write_method) do |value| value = instance_exec(value, &block) if block attribute.of(self).set(value) end end def define_appender_for(attribute, &block) define_method(attribute.append_method) do |value| value = instance_exec(value, &block) if block attribute.of(self)._append(value) end end def define_multi_writer_for(attribute) define_method(attribute.write_method) do |values| attribute.of(self)._replace(values) end end end end end clamp-1.1.1/lib/clamp/attribute/definition.rb0000644000004100000410000000346113007336773021175 0ustar www-datawww-datarequire "clamp/attribute/instance" module Clamp module Attribute class Definition def initialize(options) if options.key?(:attribute_name) @attribute_name = options[:attribute_name].to_s end @default_value = options[:default] if options.key?(:default) if options.key?(:environment_variable) @environment_variable = options[:environment_variable] end @hidden = options[:hidden] if options.key?(:hidden) end attr_reader :description, :environment_variable def help_rhs description + default_description end def help [help_lhs, help_rhs] end def ivar_name "@#{attribute_name}" end def read_method attribute_name end def default_method "default_#{read_method}" end def write_method "#{attribute_name}=" end def append_method "append_to_#{attribute_name}" if multivalued? end def multivalued? @multivalued end def required? @required end def hidden? @hidden end def attribute_name @attribute_name ||= infer_attribute_name end def default_value if defined?(@default_value) @default_value elsif multivalued? [] end end def of(command) Attribute::Instance.new(self, command) end private def default_description default_sources = [ ("$#{@environment_variable}" if defined?(@environment_variable)), (@default_value.inspect if defined?(@default_value)) ].compact return "" if default_sources.empty? " (default: " + default_sources.join(", or ") + ")" end end end end clamp-1.1.1/lib/clamp/attribute/instance.rb0000644000004100000410000000367613007336773020661 0ustar www-datawww-datamodule Clamp module Attribute # Represents an option/parameter of a Clamp::Command instance. # class Instance def initialize(attribute, command) @attribute = attribute @command = command end attr_reader :attribute, :command def defined? command.instance_variable_defined?(attribute.ivar_name) end # get value directly def get command.instance_variable_get(attribute.ivar_name) end # set value directly def set(value) command.instance_variable_set(attribute.ivar_name, value) end def default command.send(attribute.default_method) end # default implementation of read_method def _read set(default) unless self.defined? get end # default implementation of append_method def _append(value) current_values = get || [] set(current_values + [value]) end # default implementation of write_method for multi-valued attributes def _replace(values) set([]) Array(values).each { |value| take(value) } end def read command.send(attribute.read_method) end def take(value) if attribute.multivalued? command.send(attribute.append_method, value) else command.send(attribute.write_method, value) end end def default_from_environment return if self.defined? return if attribute.environment_variable.nil? return unless ENV.key?(attribute.environment_variable) # Set the parameter value if it's environment variable is present value = ENV[attribute.environment_variable] begin take(value) rescue ArgumentError => e command.send(:signal_usage_error, Clamp.message(:env_argument_error, :env => attribute.environment_variable, :message => e.message)) end end end end end clamp-1.1.1/lib/clamp/messages.rb0000644000004100000410000000231213007336773016643 0ustar www-datawww-datamodule Clamp module Messages def messages=(new_messages) messages.merge!(new_messages) end def message(key, options = {}) format(messages.fetch(key), options) end def clear_messages! init_default_messages end private DEFAULTS = { :too_many_arguments => "too many arguments", :option_required => "option '%