clamp-1.0.0/0000755000004100000410000000000012555647150012646 5ustar www-datawww-dataclamp-1.0.0/Rakefile0000644000004100000410000000036112555647150014313 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.0.0/Gemfile0000644000004100000410000000020312555647150014134 0ustar www-datawww-datasource "http://rubygems.org" gemspec group :test do gem "rake", "~> 10.4" gem "rspec", "~> 3.1.0" gem "rr", "~> 1.1.2" end clamp-1.0.0/examples/0000755000004100000410000000000012555647150014464 5ustar www-datawww-dataclamp-1.0.0/examples/speak0000755000004100000410000000075712555647150015526 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.0.0/examples/admin0000755000004100000410000000071712555647150015507 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.0.0/examples/flipflop0000755000004100000410000000066212555647150016231 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.0.0/examples/gitdown0000755000004100000410000000244112555647150016066 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.0.0/examples/scoop0000644000004100000410000000045612555647150015537 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.0.0/examples/fubar0000755000004100000410000000034612555647150015514 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.0.0/.autotest0000644000004100000410000000036112555647150014517 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.0.0/.rspec0000644000004100000410000000001012555647150013752 0ustar www-datawww-data--color clamp-1.0.0/spec/0000755000004100000410000000000012555647150013600 5ustar www-datawww-dataclamp-1.0.0/spec/spec_helper.rb0000644000004100000410000000136312555647150016421 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.0.0/spec/clamp/0000755000004100000410000000000012555647150014674 5ustar www-datawww-dataclamp-1.0.0/spec/clamp/option_module_spec.rb0000644000004100000410000000115412555647150021111 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.0.0/spec/clamp/parameter/0000755000004100000410000000000012555647150016654 5ustar www-datawww-dataclamp-1.0.0/spec/clamp/parameter/definition_spec.rb0000644000004100000410000001152512555647150022347 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.0.0/spec/clamp/option/0000755000004100000410000000000012555647150016204 5ustar www-datawww-dataclamp-1.0.0/spec/clamp/option/definition_spec.rb0000644000004100000410000001515712555647150021704 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.0.0/spec/clamp/messages_spec.rb0000644000004100000410000000200712555647150020041 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.0.0/spec/clamp/command_group_spec.rb0000644000004100000410000001432112555647150021066 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 end it "allows the parameter to be specified first" do command.run(["dummy", "spit"]) expect(stdout.strip).to eql "spat the dummy" 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 end clamp-1.0.0/spec/clamp/command_spec.rb0000644000004100000410000005331712555647150017662 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" do |value| value.to_i end 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 12345 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 %r(--flavour FLAVOUR +Flavour of the month) expect(command.help).to match %r(--color COLOR +Preferred hue) end it "handles new lines in option descriptions" do expect(command.help).to match %r(--\[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 %r( -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 %r(X +x) expect(command.help).to match %r(Y +y) expect(command.help).to match %r(\[Z\] +z \(default: "ZZZ"\)) end it "handles new lines in option descriptions" do expect(command.help).to match %r(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.0.0/.travis.yml0000644000004100000410000000012012555647150014750 0ustar www-datawww-datalanguage: ruby rvm: - 1.8.7 - 1.9.2 - 1.9.3 - 2.0.0 - 2.1.5 - 2.2.0 clamp-1.0.0/lib/0000755000004100000410000000000012555647150013414 5ustar www-datawww-dataclamp-1.0.0/lib/clamp.rb0000644000004100000410000000016012555647150015032 0ustar www-datawww-datarequire 'clamp/version' require 'clamp/command' def Clamp(&block) Class.new(Clamp::Command, &block).run end clamp-1.0.0/lib/clamp/0000755000004100000410000000000012555647150014510 5ustar www-datawww-dataclamp-1.0.0/lib/clamp/errors.rb0000644000004100000410000000125512555647150016354 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.0.0/lib/clamp/subcommand/0000755000004100000410000000000012555647150016640 5ustar www-datawww-dataclamp-1.0.0/lib/clamp/subcommand/declaration.rb0000644000004100000410000000440312555647150021453 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) unless has_subcommands? @subcommand_parameter = if @default_subcommand parameter "[SUBCOMMAND]", "subcommand", :attribute_name => :subcommand_name, :default => @default_subcommand else parameter "SUBCOMMAND", "subcommand", :attribute_name => :subcommand_name, :required => false end remove_method :default_subcommand_name parameter "[ARG] ...", "subcommand arguments", :attribute_name => :subcommand_arguments end if block # generate a anonymous sub-class subcommand_class = Class.new(subcommand_class, &block) end 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| if command_class if subcommand = command_class.find_subcommand(name) subcommand.subcommand_class end end end end def parameters_before_subcommand parameters.take_while { |p| p != @subcommand_parameter } end def inheritable_attributes recognised_options + parameters_before_subcommand 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 end end end clamp-1.0.0/lib/clamp/subcommand/execution.rb0000644000004100000410000000167412555647150021200 0ustar www-datawww-datamodule Clamp module Subcommand module Execution # override default Command behaviour def execute # delegate to subcommand subcommand = instatiate_subcommand(subcommand_name) subcommand.run(subcommand_arguments) end private def instatiate_subcommand(name) subcommand_class = find_subcommand_class(name) parent_attribute_values = {} self.class.inheritable_attributes.each do |attribute| if attribute.of(self).defined? parent_attribute_values[attribute] = attribute.of(self).get end end subcommand_class.new("#{invocation_path} #{name}", context, parent_attribute_values) end def find_subcommand_class(name) subcommand_def = self.class.find_subcommand(name) || signal_usage_error(Clamp.message(:no_such_subcommand, :name => name)) subcommand_def.subcommand_class end end end end clamp-1.0.0/lib/clamp/subcommand/definition.rb0000644000004100000410000000076312555647150021323 0ustar www-datawww-datamodule Clamp module Subcommand class Definition < Struct.new(:name, :description, :subcommand_class) 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.0.0/lib/clamp/subcommand/parsing.rb0000644000004100000410000000057312555647150020635 0ustar www-datawww-datarequire 'clamp/subcommand/execution' module Clamp module Subcommand module Parsing protected def parse_subcommand return false unless self.class.has_subcommands? self.extend(Subcommand::Execution) end private def default_subcommand_name self.class.default_subcommand || request_help end end end end clamp-1.0.0/lib/clamp/command.rb0000644000004100000410000001006612555647150016456 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 = {}, parent_attribute_values = {}) @invocation_path = invocation_path @context = context parent_attribute_values.each do |attribute, value| attribute.of(self).set(value) end end # @return [String] the path used to invoke this command # attr_reader :invocation_path # @return [Array] unconsumed command-line arguments # def remaining_arguments @remaining_arguments end # 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 include Clamp::Option::Parsing include Clamp::Parameter::Parsing include Clamp::Subcommand::Parsing protected attr_accessor :context def handle_remaining_arguments unless remaining_arguments.empty? signal_usage_error Clamp.message(:too_many_arguments) end 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($0), arguments = ARGV, context = {}) begin 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 end clamp-1.0.0/lib/clamp/parameter/0000755000004100000410000000000012555647150016470 5ustar www-datawww-dataclamp-1.0.0/lib/clamp/parameter/declaration.rb0000644000004100000410000000107112555647150021301 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| parameters << parameter define_accessors_for(parameter, &block) end end end end end clamp-1.0.0/lib/clamp/parameter/definition.rb0000644000004100000410000000220012555647150021137 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 end attr_reader :name 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) { $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.0.0/lib/clamp/parameter/parsing.rb0000644000004100000410000000115712555647150020464 0ustar www-datawww-datamodule Clamp module Parameter module Parsing protected def parse_parameters 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 self.class.parameters.each do |parameter| parameter.of(self).default_from_environment end end end end end clamp-1.0.0/lib/clamp/option/0000755000004100000410000000000012555647150016020 5ustar www-datawww-dataclamp-1.0.0/lib/clamp/option/declaration.rb0000644000004100000410000000272312555647150020636 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| declared_options << option block ||= option.default_conversion_block define_accessors_for(option, &block) end end def find_option(switch) recognised_options.find { |o| o.handles?(switch) } end def declared_options @declared_options ||= [] end def recognised_options declare_implicit_options effective_options end private def declare_implicit_options return nil if defined?(@implicit_options_declared) unless 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 @implicit_options_declared = true end def effective_options ancestors.inject([]) do |options, ancestor| if ancestor.kind_of?(Clamp::Option::Declaration) options + ancestor.declared_options else options end end end end end end clamp-1.0.0/lib/clamp/option/definition.rb0000644000004100000410000000405412555647150020500 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] if options.has_key?(:required) @required = options[:required] # Do some light validation for conflicting settings. if options.has_key?(:default) raise ArgumentError, "Specifying a :default value also :required doesn't make sense" end if type == :flag raise ArgumentError, "A required flag (boolean) doesn't make sense." end end 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-\]#{$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 if flag? Clamp.method(:truthy?) end end def help_lhs lhs = switches.join(", ") lhs += " " + type unless flag? lhs end private def recognised_switches switches.map do |switch| if switch =~ /^--\[no-\](.*)/ ["--#{$1}", "--no-#{$1}"] else switch end end.flatten end def infer_attribute_name inferred_name = long_switch.sub(/^--(\[no-\])?/, '').tr('-', '_') inferred_name += "_list" if multivalued? inferred_name end end end end clamp-1.0.0/lib/clamp/option/parsing.rb0000644000004100000410000000363212555647150020014 0ustar www-datawww-datamodule Clamp module Option module Parsing protected def parse_options while remaining_arguments.first =~ /\A-/ switch = remaining_arguments.shift break if switch == "--" case switch when /\A(-\w)(.+)\z/m # combined short options switch = $1 if find_option(switch).flag? remaining_arguments.unshift("-" + $2) else remaining_arguments.unshift($2) end when /\A(--[^=]+)=(.*)\z/m switch = $1 remaining_arguments.unshift($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 # Fill in gap from environment self.class.recognised_options.each do |option| option.of(self).default_from_environment end # Verify that all required options are present self.class.recognised_options.each do |option| # If this option is required and the value is nil, there's an error. if option.required? and 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 end private def find_option(switch) self.class.find_option(switch) || signal_usage_error(Clamp.message(:unrecognised_option, :switch => switch)) end end end end clamp-1.0.0/lib/clamp/help.rb0000644000004100000410000000432512555647150015771 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 = $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 { |a| a.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) if description puts "" puts description.gsub(/^/, " ") end end DETAIL_FORMAT = " %-29s %s" 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 DETAIL_FORMAT % [label, line] label = '' end end end private def puts(*args) @out.puts(*args) end end end end clamp-1.0.0/lib/clamp/version.rb0000644000004100000410000000005412555647150016521 0ustar www-datawww-datamodule Clamp VERSION = "1.0.0".freeze end clamp-1.0.0/lib/clamp/truthy.rb0000644000004100000410000000021312555647150016370 0ustar www-datawww-datamodule Clamp TRUTHY_VALUES = %w(1 yes enable on true) def self.truthy?(arg) TRUTHY_VALUES.include?(arg.to_s.downcase) end end clamp-1.0.0/lib/clamp/attribute/0000755000004100000410000000000012555647150016513 5ustar www-datawww-dataclamp-1.0.0/lib/clamp/attribute/declaration.rb0000644000004100000410000000252112555647150021325 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 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.0.0/lib/clamp/attribute/definition.rb0000644000004100000410000000360312555647150021172 0ustar www-datawww-datarequire 'clamp/attribute/instance' module Clamp module Attribute class Definition def initialize(options) if options.has_key?(:attribute_name) @attribute_name = options[:attribute_name].to_s end if options.has_key?(:default) @default_value = options[:default] end if options.has_key?(:environment_variable) @environment_variable = options[:environment_variable] end if options.has_key?(:hidden) @hidden = options[:hidden] end 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 if multivalued? "append_to_#{attribute_name}" end 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.0.0/lib/clamp/attribute/instance.rb0000644000004100000410000000370212555647150020646 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.has_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.0.0/lib/clamp/messages.rb0000644000004100000410000000337312555647150016652 0ustar www-datawww-datamodule Clamp module Messages def messages=(new_messages) messages.merge!(new_messages) end def message(key, options={}) format_string(messages.fetch(key), options) end def clear_messages! init_default_messages end private DEFAULTS = { :too_many_arguments => "too many arguments", :option_required => "option '%