├── var ├── name ├── version ├── created ├── organization ├── summary ├── authors ├── copyrights ├── repositories ├── requirements ├── resources └── description ├── lib ├── executable.yml ├── executable │ ├── core_ext.rb │ ├── version.rb │ ├── errors.rb │ ├── dispatch.rb │ ├── utils.rb │ ├── completion.rb │ ├── core_ext │ │ └── unbound_method.rb │ ├── domain.rb │ ├── parser.rb │ └── help.rb └── executable.rb ├── demo ├── applique │ ├── ae.rb │ ├── exec.rb │ └── compare.rb ├── 00_introduction.md ├── samples │ ├── man │ │ ├── hello.1.ronn │ │ ├── hello.1 │ │ └── hello.1.html │ └── bin │ │ └── hello ├── 04_manpage.md ├── 08_dispatach.md ├── 07_command_methods.md ├── 06_delegate_example.md ├── 01_single_command.md ├── 03_help_text.md ├── 02_multiple_commands.md └── 09_optparse_example.md ├── Gemfile ├── .ruby ├── .gitignore ├── .yardopts ├── .travis.yml ├── work ├── sandbox │ ├── example.rb │ ├── readme-example.rb │ └── mycli.rb ├── deprecated │ ├── simple-help │ │ ├── 04_executable_help.rdoc │ │ ├── test_executable.rb │ │ └── executable.rb │ ├── simple-executable │ │ ├── test_executable.rb │ │ └── executable.rb │ ├── command1 │ │ └── command-facets.rb │ ├── command3 │ │ └── command.f3.rb │ └── command2 │ │ └── command.f2.rb └── reference │ └── bash_completion.sh ├── Config.rb ├── Rakefile ├── Assembly ├── MANIFEST ├── test └── test_executable.rb ├── LICENSE.txt ├── .index ├── HISTORY.md ├── README.md └── .gemspec /var/name: -------------------------------------------------------------------------------- 1 | executable 2 | -------------------------------------------------------------------------------- /var/version: -------------------------------------------------------------------------------- 1 | 1.2.1 2 | -------------------------------------------------------------------------------- /lib/executable.yml: -------------------------------------------------------------------------------- 1 | ../.index -------------------------------------------------------------------------------- /var/created: -------------------------------------------------------------------------------- 1 | 2008-08-08 2 | -------------------------------------------------------------------------------- /var/organization: -------------------------------------------------------------------------------- 1 | rubyworks 2 | -------------------------------------------------------------------------------- /demo/applique/ae.rb: -------------------------------------------------------------------------------- 1 | require 'ae' 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec 3 | -------------------------------------------------------------------------------- /var/summary: -------------------------------------------------------------------------------- 1 | Commandline Object Mapper 2 | -------------------------------------------------------------------------------- /demo/applique/exec.rb: -------------------------------------------------------------------------------- 1 | require 'executable' 2 | -------------------------------------------------------------------------------- /var/authors: -------------------------------------------------------------------------------- 1 | --- 2 | - 7rans 3 | -------------------------------------------------------------------------------- /.ruby: -------------------------------------------------------------------------------- 1 | ruby 1.9.3p327 (2012-11-10 revision 37606) [x86_64-linux] 2 | -------------------------------------------------------------------------------- /var/copyrights: -------------------------------------------------------------------------------- 1 | --- 2 | - (c) 2008 Rubyworks (BSD-2-Clause) 3 | 4 | -------------------------------------------------------------------------------- /lib/executable/core_ext.rb: -------------------------------------------------------------------------------- 1 | require 'executable/core_ext/unbound_method' 2 | -------------------------------------------------------------------------------- /var/repositories: -------------------------------------------------------------------------------- 1 | --- 2 | upstream: git://github.com/rubyworks/executable.git 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .reap/digest 2 | .yardoc 3 | doc 4 | log 5 | pkg 6 | web 7 | DEMO.md 8 | *.lock 9 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --title "Executable" 2 | --readme README.rdoc 3 | --protected 4 | --private 5 | lib 6 | - 7 | [A-Z]*.* 8 | -------------------------------------------------------------------------------- /demo/00_introduction.md: -------------------------------------------------------------------------------- 1 | # Executable 2 | 3 | Require Executable library. 4 | 5 | require 'executable' 6 | 7 | -------------------------------------------------------------------------------- /var/requirements: -------------------------------------------------------------------------------- 1 | --- 2 | - qed (test) 3 | - ae (test) 4 | - detroit (build) 5 | - simplecov (build) 6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | script: "bundle exec qed" 3 | rvm: 4 | - 1.9.2 5 | - 1.9.3 6 | - rbx-19mode 7 | - jruby-19mode 8 | 9 | -------------------------------------------------------------------------------- /demo/applique/compare.rb: -------------------------------------------------------------------------------- 1 | When 'should be clearly laid out as follows' do |text| 2 | text = text.sub('$0', File.basename($0)) 3 | @out.strip.assert == text.strip 4 | end 5 | -------------------------------------------------------------------------------- /work/sandbox/example.rb: -------------------------------------------------------------------------------- 1 | require 'executable' 2 | 3 | class MyCli < Executable::Command 4 | 5 | # Example command. 6 | def call 7 | end 8 | 9 | end 10 | 11 | x = MyCli.new 12 | 13 | p x 14 | 15 | puts x 16 | -------------------------------------------------------------------------------- /var/resources: -------------------------------------------------------------------------------- 1 | --- 2 | home: http://rubyworks.github.com/executable 3 | code: http://github.com/rubyworks/executable 4 | bugs: http://github.com/rubyworks/executable/issues 5 | mail: http://groups.google.com/rubyworks-mailinglist 6 | chat: irc://chat.us.freenode.net#rubyworks 7 | 8 | -------------------------------------------------------------------------------- /var/description: -------------------------------------------------------------------------------- 1 | Think of Executable as a *COM*, a Commandline Object Mapper, 2 | in much the same way that ActiveRecord is an ORM, 3 | an Object Relational Mapper. A class utilizing Executable 4 | can define a complete command line tool using nothing more 5 | than Ruby's own method definitions. 6 | 7 | -------------------------------------------------------------------------------- /demo/samples/man/hello.1.ronn: -------------------------------------------------------------------------------- 1 | hellocmd(1) - Say hello. 2 | ======================== 3 | 4 | ## SYNOPSIS 5 | 6 | `hellocmd` [options...] [subcommand] 7 | 8 | ## DESCRIPTION 9 | 10 | Say hello. 11 | 12 | ## OPTIONS 13 | 14 | * `--load=BOOL`: 15 | Say it in uppercase? 16 | 17 | ## COPYRIGHT 18 | 19 | Copyright (c) 2012 -------------------------------------------------------------------------------- /Config.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Code coverage. 4 | profile :cov do 5 | 6 | # Setup QED. 7 | config :qed do 8 | require 'simplecov' 9 | SimpleCov.start do 10 | coverage_dir 'log/coverage' 11 | add_filter "/demo/" 12 | #add_group "RSpec", "lib/assay/rspec.rb" 13 | end 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /work/deprecated/simple-help/04_executable_help.rdoc: -------------------------------------------------------------------------------- 1 | = Executable::Help 2 | 3 | require 'executable/help' 4 | 5 | Now lets try it. 6 | 7 | class X 8 | extend Executable::Help 9 | 10 | help "this is a test" 11 | 12 | def test 13 | end 14 | end 15 | 16 | And the result should be 17 | 18 | X.help.assert == {:test=>"this is a test"} 19 | 20 | -------------------------------------------------------------------------------- /lib/executable/version.rb: -------------------------------------------------------------------------------- 1 | module Exectuable 2 | 3 | # 4 | DIRECTORY = File.dirname(__FILE__) 5 | 6 | # 7 | def self.const_missing(name) 8 | index[name.to_s.downcase] || super(name) 9 | end 10 | 11 | # 12 | def self.index 13 | @index ||= ( 14 | require 'yaml' 15 | YAML.load(File.new(DIRECTORY + '/../executable.yml')) 16 | ) 17 | end 18 | 19 | end 20 | 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :default => [:test] 2 | 3 | desc "run unit tests (needs rubytest)" 4 | task :test do 5 | sh "rubytest -Ilib test/*.rb" 6 | end 7 | 8 | desc "render README.rdoc to web/readme.html (need malt)" 9 | task :readme do 10 | sh "malt README.rdoc > web/readme.html" 11 | end 12 | 13 | # if `README.rdoc` changes generate `web/readme.html`. 14 | file 'README.rdoc' do 15 | sh "malt README.rdoc > web/readme.html" 16 | end 17 | 18 | -------------------------------------------------------------------------------- /demo/04_manpage.md: -------------------------------------------------------------------------------- 1 | ### Manpages 2 | 3 | If a man page is available for a given command using the #show_help 4 | method will automatically find the manpage and display it. 5 | 6 | sample = File.dirname(__FILE__) + '/samples' 7 | 8 | manpage = `ruby #{sample}/bin/hello --manpage`.strip 9 | 10 | manpage.assert == sample + '/man/hello.1' 11 | 12 | Note: Would rather use #load for this, but without a `.rb` on 13 | the `hello` file, it doesn't seem possible. 14 | 15 | -------------------------------------------------------------------------------- /demo/samples/man/hello.1: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn/v0.7.3 2 | .\" http://github.com/rtomayko/ronn/tree/0.7.3 3 | . 4 | .TH "HELLOCMD" "1" "January 2012" "" "" 5 | . 6 | .SH "NAME" 7 | \fBhellocmd\fR \- Say hello\. 8 | . 9 | .SH "SYNOPSIS" 10 | \fBhellocmd\fR [options\.\.\.] [subcommand] 11 | . 12 | .SH "DESCRIPTION" 13 | Say hello\. 14 | . 15 | .SH "OPTIONS" 16 | . 17 | .TP 18 | \fB\-\-load=BOOL\fR 19 | Say it in uppercase? 20 | . 21 | .SH "COPYRIGHT" 22 | Copyright (c) 2012 23 | -------------------------------------------------------------------------------- /work/reference/bash_completion.sh: -------------------------------------------------------------------------------- 1 | # ~/.bash_completion.d/yaml_command 2 | 3 | _yaml() 4 | { 5 | local cur=${COMP_WORDS[COMP_CWORD]} 6 | local pos=(COMP_CWORD - 1) 7 | local pre=${COMP_WORDS[@]:0:$pos} 8 | local cpl=$($pre _) 9 | 10 | if [[ "$cur" == -* ]]; then 11 | cpl=${cpl[@]//^[^-]/} 12 | else 13 | cpl=${cpl[@]//-*/} 14 | fi 15 | 16 | COMPREPLY=( $(compgen -W "$cpl" -- $cur) ) 17 | } 18 | complete -F _yaml yaml 19 | 20 | -------------------------------------------------------------------------------- /Assembly: -------------------------------------------------------------------------------- 1 | --- 2 | github: 3 | gh_pages: web 4 | 5 | gem: 6 | active: true 7 | 8 | dnote: 9 | title: Source Notes 10 | output: log/notes.html 11 | 12 | vclog: 13 | output: 14 | - log/history.html 15 | - log/changes.html 16 | 17 | email: 18 | mailto: 19 | - ruby-talk@ruby-lang.org 20 | - rubyworks-mailinglist@googlegroups.com 21 | 22 | qed: 23 | files: demo/ 24 | 25 | qedoc: 26 | title: Executable Demonstrations 27 | files: demo/ 28 | output: 29 | - DEMO.md 30 | - web/demo.html 31 | 32 | -------------------------------------------------------------------------------- /lib/executable/errors.rb: -------------------------------------------------------------------------------- 1 | module Executable 2 | 3 | class NoOptionError < ::NoMethodError # ArgumentError ? 4 | def initialize(name, *arg) 5 | super("unknown option -- #{name}", name, *args) 6 | end 7 | end 8 | 9 | #class NoCommandError < ::NoMethodError 10 | # def initialize(name, *args) 11 | # super("unknown command -- #{name}", name, *args) 12 | # end 13 | #end 14 | 15 | class NoCommandError < ::NoMethodError 16 | def initialize(*args) 17 | super("missing command", *args) 18 | end 19 | end 20 | 21 | end 22 | 23 | -------------------------------------------------------------------------------- /work/sandbox/readme-example.rb: -------------------------------------------------------------------------------- 1 | require 'executable' 2 | 3 | class HelloCommand 4 | include Executable 5 | 6 | # Say it in uppercase? 7 | def loud=(bool) 8 | @loud = bool 9 | end 10 | 11 | # 12 | def loud? 13 | @loud 14 | end 15 | 16 | # Show this message. 17 | def help! 18 | cli.show_help 19 | exit 20 | end 21 | alias :h! :help! 22 | 23 | # Say hello. 24 | def call(name=nil) 25 | name = name || 'World' 26 | str = "Hello, #{name}!" 27 | str = str.upcase if loud? 28 | puts str 29 | end 30 | end 31 | 32 | HelloCommand.execute 33 | 34 | -------------------------------------------------------------------------------- /demo/samples/bin/hello: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'executable' 4 | 5 | class Hello < Executable::Command 6 | # Say it in uppercase? 7 | def loud=(bool) 8 | @loud = bool 9 | end 10 | 11 | # 12 | def loud? 13 | @loud 14 | end 15 | 16 | # Show this message. 17 | def help? 18 | cli.show_help 19 | exit 20 | end 21 | alias :h? :help? 22 | 23 | # Say hello. 24 | def call(name) 25 | name = name || 'World' 26 | str = "Hello, #{name}!" 27 | str = str.upcase if loud? 28 | puts str 29 | end 30 | 31 | # Where's the manpage? 32 | def manpage! 33 | puts cli.manpage 34 | exit 35 | end 36 | end 37 | 38 | Hello.execute 39 | -------------------------------------------------------------------------------- /demo/08_dispatach.md: -------------------------------------------------------------------------------- 1 | ## Legacy/Dispath 2 | 3 | The Dispatch mixin, which is also called Legacy b/c this is how older 4 | version of Executable worked, provides Executable with a `#call` method 5 | that automatically routes the to a method given by the first argument. 6 | 7 | class DispatchExample < Executable::Command 8 | include Legacy 9 | 10 | attr :result 11 | 12 | def foo 13 | @result = :foo 14 | end 15 | 16 | def bar 17 | @result = :bar 18 | end 19 | 20 | end 21 | 22 | Now when we invoke the command, the 23 | 24 | eg = DispatchExample.run('foo') 25 | eg.result.assert == :foo 26 | 27 | eg = DispatchExample.run('bar') 28 | eg.result.assert == :bar 29 | 30 | -------------------------------------------------------------------------------- /work/deprecated/simple-executable/test_executable.rb: -------------------------------------------------------------------------------- 1 | require 'executable' 2 | 3 | class TestExecutable < Test::Unit::TestCase 4 | 5 | class SampleCli 6 | include Executable 7 | 8 | attr :result 9 | 10 | def initialize 11 | @result = [] 12 | end 13 | 14 | def output=(value) 15 | @result << "output: #{value}" 16 | end 17 | 18 | def jump 19 | @result << "jump" 20 | end 21 | end 22 | 23 | # 24 | def test_parse_without_option 25 | s = SampleCli.new 26 | s.execute!("jump") 27 | assert_equal(s.result, ["jump"]) 28 | end 29 | 30 | # 31 | def test_parse_with_option 32 | s = SampleCli.new 33 | s.execute!("jump --output=home") 34 | assert_equal(s.result, ['output: home', 'jump']) 35 | end 36 | 37 | end 38 | 39 | -------------------------------------------------------------------------------- /work/deprecated/simple-help/test_executable.rb: -------------------------------------------------------------------------------- 1 | require 'facets/executable' 2 | 3 | test_case Executable do 4 | 5 | class SampleCli 6 | include Executable 7 | 8 | attr :result 9 | 10 | def initialize 11 | @result = [] 12 | end 13 | 14 | def output=(value) 15 | @result << "output: #{value}" 16 | end 17 | 18 | def jump 19 | @result << "jump" 20 | end 21 | end 22 | 23 | # 24 | method :execute! do 25 | test "parse without an option" do 26 | s = SampleCli.new 27 | s.execute!("jump") 28 | s.result.assert == ["jump"] 29 | end 30 | 31 | test "parse with an option" do 32 | s = SampleCli.new 33 | s.execute!("jump --output=home") 34 | s.result.assert == ['output: home', 'jump'] 35 | end 36 | end 37 | 38 | end 39 | 40 | -------------------------------------------------------------------------------- /demo/07_command_methods.md: -------------------------------------------------------------------------------- 1 | ## README Example 2 | 3 | This is the example used in the documentation. 4 | 5 | class Example 6 | include Executable 7 | 8 | attr_switch :quiet 9 | 10 | def bread(*args) 11 | ["bread", quiet?, *args] 12 | end 13 | 14 | def butter(*args) 15 | ["butter", quiet?, *args] 16 | end 17 | 18 | # Route call to methods. 19 | def call(name, *args) 20 | meth = public_method(name) 21 | meth.call(*args) 22 | end 23 | end 24 | 25 | Use a subcommand and an argument. 26 | 27 | c, a = Example.parse(['butter', 'yum']) 28 | r = c.call(*a) 29 | r.assert == ["butter", nil, "yum"] 30 | 31 | A subcommand and a boolean option. 32 | 33 | c, a = Example.parse(['bread', '--quiet']) 34 | r = c.call(*a) 35 | r.assert == ["bread", true] 36 | 37 | -------------------------------------------------------------------------------- /lib/executable/dispatch.rb: -------------------------------------------------------------------------------- 1 | module Executable 2 | 3 | # Variation of Executable which provides basic compatibility with 4 | # previous versions of Executable. It provides a call method that 5 | # automatically dispatches to public methods. 6 | # 7 | # Among other uses, Dispatch can be useful for dropping into any class 8 | # as a quick and dirty way to work with it on the command line. 9 | # 10 | # @since 1.2.0 11 | module Dispatch 12 | Executable.__send__(:append_features, self) 13 | 14 | # 15 | # When Dispatchable is included into a class, the class is 16 | # also extended by `Executable::Domain`. 17 | # 18 | def self.included(base) 19 | base.extend Domain 20 | end 21 | 22 | def call(name, *args) 23 | public_method(name).call(*args) 24 | end 25 | end 26 | 27 | # Dispatch is Legacy. 28 | Legacy = Dispatch 29 | 30 | end 31 | -------------------------------------------------------------------------------- /demo/06_delegate_example.md: -------------------------------------------------------------------------------- 1 | ## Subclass Example 2 | 3 | Lets say we have a class that we would like to work with on 4 | the command line, but want to keep the class itself unchanaged 5 | without mixin. 6 | 7 | class Hello 8 | attr_accessor :name 9 | 10 | def initialize(name="World") 11 | @name = name 12 | end 13 | 14 | def hello 15 | @output = "Hello, #{name}!" 16 | end 17 | 18 | def output 19 | @output 20 | end 21 | end 22 | 23 | Rather then including Exectuable in the class directly, we can 24 | create a subclass and use it instead. 25 | 26 | class HelloCommand < Hello 27 | include Executable 28 | 29 | def call(*args) 30 | hello 31 | end 32 | end 33 | 34 | Now we can execute the command perfectly well. 35 | 36 | cmd = HelloCommand.execute(['hello', '--name=Fred']) 37 | cmd.output.assert == "Hello, Fred!" 38 | 39 | And the original class remains undisturbed. 40 | 41 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | #!mast -x *.lock .index .ruby .yardopts bin demo lib share spec test [A-Z]*.* 2 | .index 3 | .ruby 4 | .yardopts 5 | demo/00_introduction.md 6 | demo/01_single_command.md 7 | demo/02_multiple_commands.md 8 | demo/03_help_text.md 9 | demo/04_manpage.md 10 | demo/06_delegate_example.md 11 | demo/07_command_methods.md 12 | demo/08_dispatach.md 13 | demo/09_optparse_example.md 14 | demo/applique/ae.rb 15 | demo/applique/compare.rb 16 | demo/applique/exec.rb 17 | demo/samples/bin/hello 18 | demo/samples/man/hello.1 19 | demo/samples/man/hello.1.html 20 | demo/samples/man/hello.1.ronn 21 | lib/executable/completion.rb 22 | lib/executable/core_ext.rb 23 | lib/executable/dispatch.rb 24 | lib/executable/domain.rb 25 | lib/executable/errors.rb 26 | lib/executable/help.rb 27 | lib/executable/parser.rb 28 | lib/executable/utils.rb 29 | lib/executable/version.rb 30 | lib/executable.rb 31 | lib/executable.yml 32 | test/test_executable.rb 33 | LICENSE.txt 34 | HISTORY.md 35 | README.md 36 | DEMO.md 37 | Config.rb 38 | -------------------------------------------------------------------------------- /test/test_executable.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.dirname(__FILE__)+'/../lib') 2 | 3 | require 'microtest' 4 | require 'ae' 5 | 6 | require 'executable' 7 | 8 | class ExecutableTestCase < MicroTest::TestCase 9 | 10 | class MyCommand 11 | include Executable 12 | 13 | attr_reader :size, :quiet, :file 14 | 15 | def initialize 16 | @file = 'hey.txt' # default 17 | end 18 | 19 | def quiet=(bool) 20 | @quiet = bool 21 | end 22 | 23 | def quiet? 24 | @quiet 25 | end 26 | 27 | def size=(integer) 28 | @size = integer.to_i 29 | end 30 | 31 | def file=(fname) 32 | @file = fname 33 | end 34 | 35 | def call(*args) 36 | @args = args 37 | end 38 | end 39 | 40 | def test_boolean_optiion 41 | mc = MyCommand.execute('--quiet') 42 | mc.assert.quiet? 43 | end 44 | 45 | def test_integer_optiion 46 | mc = MyCommand.execute('--size=4') 47 | mc.size.assert == 4 48 | end 49 | 50 | def test_default_value 51 | mc = MyCommand.execute('') 52 | mc.file.assert == 'hey.txt' 53 | end 54 | 55 | #def usage_output 56 | # MyCommand.help.usage.assert == "{$0} [options] [subcommand]" 57 | #end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /demo/01_single_command.md: -------------------------------------------------------------------------------- 1 | ## No Subcommmands 2 | 3 | This example demonstrates using Executable::Command to create a simple command line 4 | interface without subcommands. (Note the Executable mixin could be used just 5 | as well). 6 | 7 | class NoSubCommandCLI < Executable::Command 8 | 9 | attr :result 10 | 11 | def o? 12 | @o 13 | end 14 | 15 | def o=(flag) 16 | @o = flag 17 | end 18 | 19 | def call 20 | if o? 21 | @result = "with" 22 | else 23 | @result = "without" 24 | end 25 | end 26 | 27 | end 28 | 29 | Execute the CLI on an example command line. 30 | 31 | cli = NoSubCommandCLI.run('') 32 | cli.result.assert == 'without' 33 | 34 | Execute the CLI on an example command line. 35 | 36 | cli = NoSubCommandCLI.run('-o') 37 | cli.result.assert == 'with' 38 | 39 | There are two important things to notices heres. Frist, that #main is being 40 | called in each case. It is the method called with no other subcommands are 41 | defined. And second, the fact the a `o?` method is defined to compliment the 42 | `o=` writer, informs Executable that `-o` is an option _flag_, not taking 43 | any parameters. 44 | 45 | -------------------------------------------------------------------------------- /lib/executable/utils.rb: -------------------------------------------------------------------------------- 1 | module Executable 2 | 3 | # Some handy-dandy CLI utility methods. 4 | # 5 | module Utils 6 | extend self 7 | 8 | # TODO: Maybe #ask chould serve all purposes depending on degfault? 9 | # e.g. `ask?("ok?", default=>true)`, would be same as `yes?("ok?")`. 10 | 11 | # Strings to interprest as boolean values. 12 | BOOLEAN_MAP = {"y"=>true, "yes"=>true, "n"=>false, "no"=>false} 13 | 14 | # Query the user for a yes/no answer, defaulting to yes. 15 | def yes?(question, options={}) 16 | print "#{question} [Y/n] " 17 | input = STDIN.readline.chomp.downcase 18 | BOOLEAN_MAP[input] || true 19 | end 20 | 21 | # Query the user for a yes/no answer, defaulting to no. 22 | def no?(question, options={}) 23 | print "#{question} [y/N] " 24 | input = STDIN.readline.chomp.downcase 25 | BOOLEAN_MAP[input] || false 26 | end 27 | 28 | # Query the user for an answer. 29 | def ask(question, options={}) 30 | print "#{question} [default: #{options[:default]}] " 31 | reply = STDIN.readline.chomp 32 | if reply.empty? 33 | options[:default] 34 | else 35 | reply 36 | end 37 | end 38 | 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /work/sandbox/mycli.rb: -------------------------------------------------------------------------------- 1 | # Executable is the sup'd-up base class for creating robust, inheritable and 2 | # reusable command line interfaces. 3 | 4 | class MyCLI 5 | include Executable 6 | 7 | # foo --debug 8 | 9 | def debug? 10 | $DEBUG 11 | end 12 | 13 | def debug=(bool) 14 | $DEBUG = bool 15 | end 16 | 17 | # $ foo remote 18 | 19 | class Remote 20 | 21 | # $ foo remote --verbose 22 | 23 | def verbose? 24 | @verbose 25 | end 26 | 27 | def verbose=(bool) 28 | @verbose = bool 29 | end 30 | 31 | # $ foo remote --force 32 | 33 | def force? 34 | @force 35 | end 36 | 37 | def force=(bool) 38 | @force = bool 39 | end 40 | 41 | # $ foo remote --output 42 | 43 | def output=(path) 44 | @path = path 45 | end 46 | 47 | # $ foo remote -o 48 | 49 | alias_method :o=, :output= 50 | 51 | # $ foo remote add 52 | 53 | class Add < self 54 | 55 | def main(name, branch) 56 | # ... 57 | end 58 | 59 | end 60 | 61 | # $ foo remote show 62 | 63 | class Show < self 64 | 65 | def main(name) 66 | # ... 67 | end 68 | 69 | end 70 | 71 | end 72 | 73 | end 74 | 75 | 76 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD-2-Clause License 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 15 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 16 | COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 17 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 18 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 19 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 20 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 22 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /.index: -------------------------------------------------------------------------------- 1 | --- 2 | type: ruby 3 | revision: 2013 4 | sources: 5 | - var 6 | authors: 7 | - name: 7rans 8 | email: transfire@gmail.com 9 | organizations: [] 10 | requirements: 11 | - groups: 12 | - test 13 | development: true 14 | name: qed 15 | - groups: 16 | - test 17 | development: true 18 | name: ae 19 | - groups: 20 | - build 21 | development: true 22 | name: detroit 23 | - groups: 24 | - build 25 | development: true 26 | name: simplecov 27 | conflicts: [] 28 | alternatives: [] 29 | resources: 30 | - type: home 31 | uri: http://rubyworks.github.com/executable 32 | label: Homepage 33 | - type: code 34 | uri: http://github.com/rubyworks/executable 35 | label: Source Code 36 | - type: bugs 37 | uri: http://github.com/rubyworks/executable/issues 38 | label: Issue Tracker 39 | - type: mail 40 | uri: http://groups.google.com/rubyworks-mailinglist 41 | label: Mailing List 42 | - type: chat 43 | uri: irc://chat.us.freenode.net#rubyworks 44 | label: IRC Channel 45 | repositories: 46 | - name: upstream 47 | scm: git 48 | uri: git://github.com/rubyworks/executable.git 49 | categories: [] 50 | paths: 51 | load: 52 | - lib 53 | copyrights: 54 | - holder: Rubyworks 55 | year: '2008' 56 | license: BSD-2-Clause 57 | created: '2008-08-08' 58 | summary: Commandline Object Mapper 59 | version: 1.2.1 60 | name: executable 61 | title: Executable 62 | description: ! 'Think of Executable as a *COM*, a Commandline Object Mapper, 63 | 64 | in much the same way that ActiveRecord is an ORM, 65 | 66 | an Object Relational Mapper. A class utilizing Executable 67 | 68 | can define a complete command line tool using nothing more 69 | 70 | than Ruby''s own method definitions.' 71 | date: '2012-12-18' 72 | -------------------------------------------------------------------------------- /lib/executable/completion.rb: -------------------------------------------------------------------------------- 1 | module Executable 2 | 3 | # Encpsulates command completion. 4 | # 5 | class Completion 6 | 7 | # 8 | # Setup new completion object. 9 | # 10 | def initialize(cli_class) 11 | @cli_class = cli_class 12 | 13 | @subcommands = nil 14 | @options = nil 15 | end 16 | 17 | # 18 | alias_method :inspect, :to_s 19 | 20 | # 21 | # The Executable subclass to which this help applies. 22 | # 23 | attr :cli_class 24 | 25 | # 26 | # List of subcommands converted to a printable string. 27 | # But will return +nil+ if there are no subcommands. 28 | # 29 | # @return [String,NilClass] subcommand list text 30 | # 31 | def subcommands 32 | @subcommands ||= @cli_class.subcommands.keys 33 | end 34 | 35 | # 36 | def options 37 | @options ||= ( 38 | method_list.map do |meth| 39 | case meth.name 40 | when /^(.*?)[\!\=]$/ 41 | name = meth.name.to_s.chomp('!').chomp('=') 42 | mark = name.to_s.size == 1 ? '-' : '--' 43 | mark + name 44 | end 45 | end.compact.sort 46 | ) 47 | end 48 | 49 | # 50 | def to_s 51 | (subcommands + options).join(' ') 52 | end 53 | 54 | # 55 | def call(*args) 56 | puts self 57 | end 58 | 59 | private 60 | 61 | # 62 | # Produce a list relavent methods. 63 | # 64 | def method_list 65 | list = [] 66 | methods = [] 67 | stop_at = cli_class.ancestors.index(Executable::Command) || 68 | cli_class.ancestors.index(Executable) || 69 | -1 70 | ancestors = cli_class.ancestors[0...stop_at] 71 | ancestors.reverse_each do |a| 72 | a.instance_methods(false).each do |m| 73 | list << cli_class.instance_method(m) 74 | end 75 | end 76 | list 77 | end 78 | 79 | end 80 | 81 | end 82 | 83 | -------------------------------------------------------------------------------- /lib/executable.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'executable/errors' 4 | require 'executable/parser' 5 | require 'executable/help' 6 | require 'executable/completion' 7 | require 'executable/utils' 8 | require 'executable/domain' 9 | require 'executable/dispatch' 10 | 11 | # Executable is a mixin for creating robust, inheritable and 12 | # reusable command line interfaces. 13 | # 14 | module Executable 15 | 16 | # 17 | # When Exectuable is included into a class, the class is 18 | # also extended by `Executable::Doamin`. 19 | # 20 | def self.included(base) 21 | base.extend Domain 22 | end 23 | 24 | # 25 | # Default initializer, simply takes a hash of settings 26 | # to set attributes via writer methods. Not existant 27 | # attributes are simply ignored. 28 | # 29 | def initialize(settings={}) 30 | settings.each do |k,v| 31 | __send__("#{k}=", v) if respond_to?("#{k}=") 32 | end 33 | end 34 | 35 | public 36 | 37 | # 38 | # Command invocation abstract method. 39 | # 40 | def call(*args) 41 | #puts cli.show_help # TODO: show help instead of error ? 42 | raise NotImplementedError 43 | end 44 | 45 | # 46 | # Convert Executable to Proc object. 47 | # 48 | def to_proc 49 | lambda { |*args| call(*args) } 50 | end 51 | 52 | alias_method :inspect, :to_s 53 | 54 | # 55 | # Output command line help. 56 | # 57 | def to_s 58 | self.class.help.to_s # usage ? 59 | end 60 | 61 | # 62 | # Access to underlying cli instance. 63 | # 64 | def cli 65 | self.class.cli 66 | end 67 | 68 | # 69 | # Override option_missing if needed. This receives the name of the option 70 | # and the remaining arguments list. It must consume any arguments it uses 71 | # from the begining of the list (i.e. in-place manipulation). 72 | # 73 | def option_missing(opt, argv) 74 | raise NoOptionError, opt 75 | end 76 | 77 | # Base class alteranative ot using the mixin. 78 | # 79 | class Command 80 | include Executable 81 | end 82 | 83 | end 84 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # RELEASE HISTORY 2 | 3 | ## 1.2.1 / 2012-12-19 4 | 5 | This release imporves the help output and manpage lookup 6 | as well as a few helpful additions the the API. 7 | 8 | Changes: 9 | 10 | * Improve manpage lookup. 11 | * Improve overall help output. 12 | * Add alias_switch and alias_accessor helpers. 13 | 14 | 15 | ## 1.2.0 / 2012-01-31 16 | 17 | Version 1.2.0 is complete rewrite of Executable. Actually it was decided that 18 | the old design was too simplistic in it design concept, so another library 19 | that was in the works, called Executioner, and briefly CLI::Base, was 20 | ported over. And with some API changes, it is now the new Executable project. 21 | The idea of the project is generally the same, but Executable now offers 22 | more features, such as good help output and namespace-based subcomamnds. 23 | Of course, to accommodate all this the API had to change some over 24 | the previous version, so be sure to read the API documentation. 25 | 26 | Changes: 27 | 28 | * Deprecate old implementation. 29 | * Port Executioner project over to become new Executable project. 30 | * Supports namespace-base subcommmands. 31 | * Supports formatted help output in plain text and markdown. 32 | * Supports manpage look-up and display. 33 | 34 | 35 | ## 1.1.0 / 2011-04-21 36 | 37 | This release simplifies Executable, removing the #option_missing method 38 | and using the standard #method_missing callback instead. Along with this 39 | the special error class, +NoOptionError+, has been removed. This release 40 | also fixes an issue with inconsistent arguments being passed to the callback. 41 | Finally it renames the #execute_command method to simple #execute!. 42 | 43 | Changes: 44 | 45 | * Name Changes 46 | 47 | * Renamed `#execute_command` method to `#execute!`. 48 | 49 | * Deprecations 50 | 51 | * Rely on #method_missing callback instead of special #option_missing method. 52 | * The +NoOptionError+ exception class is no longer needed because of above. 53 | 54 | * Bug Fixes 55 | 56 | * The #method_missing callback takes the value of the option being set. 57 | 58 | 59 | ## 1.0.0 / 2011-04-15 60 | 61 | This is the initialize release of Executable (as a stand alone project). 62 | Executable is a mixin that can turn any class into an commandline interface. 63 | 64 | * 1 Major Enhancement 65 | 66 | * Birthday! 67 | 68 | -------------------------------------------------------------------------------- /demo/03_help_text.md: -------------------------------------------------------------------------------- 1 | ## Command Help 2 | 3 | Executable Commands can generate help output. It does this by extracting 4 | the commenst associated with the option methods. A description of the 5 | command itself is taken from the comment on the `#call` method. Only 6 | the first line of a comment is used, so the reset of the comment can 7 | still be catered to documention tools such as YARD and RDoc. 8 | 9 | Let's setup an example CLI subclass to demonstrate this. 10 | 11 | class MyCLI < Executable::Command 12 | 13 | # This is global option -g. 14 | # Yadda yadda yadda... 15 | def g=(bool) 16 | @g = bool 17 | end 18 | 19 | def g?; @g; end 20 | 21 | # Subcommand `c1`. 22 | class C1 < self 23 | 24 | # This does c1. 25 | def call(*args) 26 | end 27 | 28 | # This is option --o1 for c1. 29 | def o1=(value) 30 | end 31 | 32 | # This is option --o2 for c1. 33 | def o2=(value) 34 | end 35 | 36 | end 37 | 38 | # Subcommand `c2`. 39 | class C2 < self 40 | 41 | # This does c2. 42 | def call(*args) 43 | end 44 | 45 | # This is option --o1 for c2. 46 | def o1=(value) 47 | end 48 | 49 | # This is option --o2 for c2. 50 | def o2=(value) 51 | end 52 | 53 | end 54 | 55 | end 56 | 57 | === Plain Text 58 | 59 | The help output, 60 | 61 | @out = MyCLI::C1.help.to_s 62 | 63 | should be clearly laid out as follows: 64 | 65 | Usage: my c1 [options...] [subcommand] 66 | 67 | This does c1. 68 | 69 | OPTIONS 70 | -g This is global option -g. 71 | --o1=VALUE This is option --o1 for c1. 72 | --o2=VALUE This is option --o2 for c1. 73 | 74 | Copyright (c) 2012 75 | 76 | === Markdown 77 | 78 | The help feature can also output ronn-style markdown, 79 | 80 | @out = MyCLI::C1.help.markdown 81 | 82 | should be clearly laid out as follows: 83 | 84 | my-c1(1) - This does c1. 85 | ======================== 86 | 87 | ## SYNOPSIS 88 | 89 | `my c1` [options...] [subcommand] 90 | 91 | ## DESCRIPTION 92 | 93 | This does c1. 94 | 95 | ## OPTIONS 96 | 97 | * `-g`: 98 | This is global option -g. 99 | 100 | * `--o1=VALUE`: 101 | This is option --o1 for c1. 102 | 103 | * `--o2=VALUE`: 104 | This is option --o2 for c1. 105 | 106 | ## COPYRIGHT 107 | 108 | Copyright (c) 2012 109 | 110 | -------------------------------------------------------------------------------- /demo/02_multiple_commands.md: -------------------------------------------------------------------------------- 1 | ## Multiple Subcommmands 2 | 3 | Setup an example CLI subclass. 4 | 5 | class MyCLI < Executable::Command 6 | attr :result 7 | 8 | def initialize 9 | @result = [] 10 | end 11 | 12 | def g=(value) 13 | @result << "g" if value 14 | end 15 | 16 | def g? 17 | @result.include?("g") 18 | end 19 | 20 | # 21 | class C1 < self 22 | def call 23 | @result << "c1" 24 | end 25 | 26 | def o1=(value) 27 | @result << "c1_o1 #{value}" 28 | end 29 | 30 | def o2=(value) 31 | @result << "c1_o2 #{value}" 32 | end 33 | end 34 | 35 | # 36 | class C2 < Executable::Command 37 | attr :result 38 | 39 | def initialize 40 | @result = [] 41 | end 42 | 43 | def call 44 | @result << "c2" 45 | end 46 | 47 | def o1=(value) 48 | @result << "c2_o1 #{value}" 49 | end 50 | 51 | def o2=(value) 52 | @result << "c2_o2" if value 53 | end 54 | 55 | def o2? 56 | @result.include?("c2_o2") 57 | end 58 | end 59 | 60 | end 61 | 62 | Instantiate and run the class on an example command line. 63 | 64 | Just a command. 65 | 66 | cli = MyCLI.run('c1') 67 | cli.result.assert == ['c1'] 68 | 69 | Command with global option. 70 | 71 | cli = MyCLI.run('c1 -g') 72 | cli.result.assert == ['g', 'c1'] 73 | 74 | Command with an option. 75 | 76 | cli = MyCLI.run('c1 --o1 A') 77 | cli.result.assert == ['c1_o1 A', 'c1'] 78 | 79 | Command with two options. 80 | 81 | cli = MyCLI.run('c1 --o1 A --o2 B') 82 | cli.result.assert == ['c1_o1 A', 'c1_o2 B', 'c1'] 83 | 84 | Try out the second command. 85 | 86 | cli = MyCLI.run('c2') 87 | cli.result.assert == ['c2'] 88 | 89 | Seoncd command with an option. 90 | 91 | cli = MyCLI.run('c2 --o1 A') 92 | cli.result.assert == ['c2_o1 A', 'c2'] 93 | 94 | Second command with two options. 95 | 96 | cli = MyCLI.run('c2 --o1 A --o2') 97 | cli.result.assert == ['c2_o1 A', 'c2_o2', 'c2'] 98 | 99 | Since C1#main takes not arguments, if we try to issue a command 100 | that will have left over arguments, then an ArgumentError will be raised. 101 | 102 | expect ArgumentError do 103 | cli = MyCLI.run('c1 a') 104 | end 105 | 106 | How about a non-existenct subcommand. 107 | 108 | expect NotImplementedError do 109 | cli = MyCLI.run('q') 110 | cli.result.assert == ['q'] 111 | end 112 | 113 | How about an option only. 114 | 115 | expect NotImplementedError do 116 | cli = MyCLI.run('-g') 117 | cli.result.assert == ['-g'] 118 | end 119 | 120 | How about a non-existant options. 121 | 122 | expect Executable::NoOptionError do 123 | MyCLI.run('c1 --foo') 124 | end 125 | 126 | -------------------------------------------------------------------------------- /lib/executable/core_ext/unbound_method.rb: -------------------------------------------------------------------------------- 1 | class UnboundMethod 2 | if !method_defined?(:source_location) 3 | if Proc.method_defined? :__file__ # /ree/ 4 | def source_location 5 | [__file__, __line__] rescue nil 6 | end 7 | elsif defined?(RUBY_ENGINE) && RUBY_ENGINE =~ /jruby/ 8 | require 'java' 9 | def source_location 10 | to_java.source_location(Thread.current.to_java.getContext()) 11 | end 12 | end 13 | end 14 | 15 | # 16 | def comment 17 | Source.get_above_comment(*source_location) 18 | end 19 | 20 | # Source lookup. 21 | # 22 | module Source 23 | extend self 24 | 25 | # Read and cache file. 26 | # 27 | # @param file [String] filename, should be full path 28 | # 29 | # @return [Array] file content in array of lines 30 | def read(file) 31 | @read ||= {} 32 | @read[file] ||= File.readlines(file) 33 | end 34 | 35 | # Get comment from file searching up from given line number. 36 | # 37 | # @param file [String] filename, should be full path 38 | # @param line [Integer] line number in file 39 | # 40 | def get_above_comment(file, line) 41 | get_above_comment_lines(file, line).join("\n").strip 42 | end 43 | 44 | # Get comment from file searching up from given line number. 45 | # 46 | # @param file [String] filename, should be full path 47 | # @param line [Integer] line number in file 48 | # 49 | def get_above_comment_lines(file, line) 50 | text = read(file) 51 | index = line - 1 52 | while index >= 0 && text[index] !~ /^\s*\#/ 53 | return [] if text[index] =~ /^\s*end/ 54 | index -= 1 55 | end 56 | rindex = index 57 | while text[index] =~ /^\s*\#/ 58 | index -= 1 59 | end 60 | result = text[index..rindex] 61 | result = result.map{ |s| s.strip } 62 | result = result.reject{ |s| s[0,1] != '#' } 63 | result = result.map{ |s| s.sub(/^#/,'').strip } 64 | #result = result.reject{ |s| s == "" } 65 | result 66 | end 67 | 68 | # Get comment from file searching down from given line number. 69 | # 70 | # @param file [String] filename, should be full path 71 | # @param line [Integer] line number in file 72 | # 73 | def get_following_comment(file, line) 74 | get_following_comment_lines(file, line).join("\n").strip 75 | end 76 | 77 | # Get comment from file searching down from given line number. 78 | # 79 | # @param file [String] filename, should be full path 80 | # @param line [Integer] line number in file 81 | # 82 | def get_following_comment_lines(file, line) 83 | text = read(file) 84 | index = line || 0 85 | while text[index] !~ /^\s*\#/ 86 | return nil if text[index] =~ /^\s*(class|module)/ 87 | index += 1 88 | end 89 | rindex = index 90 | while text[rindex] =~ /^\s*\#/ 91 | rindex += 1 92 | end 93 | result = text[index..(rindex-2)] 94 | result = result.map{ |s| s.strip } 95 | result = result.reject{ |s| s[0,1] != '#' } 96 | result = result.map{ |s| s.sub(/^#/,'').strip } 97 | result.join("\n").strip 98 | end 99 | 100 | end 101 | 102 | end 103 | -------------------------------------------------------------------------------- /demo/samples/man/hello.1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | hellocmd(1) - Say hello. 7 | 44 | 45 | 52 | 53 |
54 | 55 | 62 | 63 |
    64 |
  1. hellocmd(1)
  2. 65 |
  3. 66 |
  4. hellocmd(1)
  5. 67 |
68 | 69 |

NAME

70 |

71 | hellocmd - Say hello. 72 |

73 | 74 |

SYNOPSIS

75 | 76 |

hellocmd [options...] [subcommand]

77 | 78 |

DESCRIPTION

79 | 80 |

Say hello.

81 | 82 |

OPTIONS

83 | 84 |
85 |
--load=BOOL
Say it in uppercase?
86 |
87 | 88 | 89 | 90 | 91 |

Copyright (c) 2012

92 | 93 | 94 |
    95 |
  1. 96 |
  2. January 2012
  3. 97 |
  4. hellocmd(1)
  5. 98 |
99 | 100 |
101 | 102 | 103 | -------------------------------------------------------------------------------- /lib/executable/domain.rb: -------------------------------------------------------------------------------- 1 | module Executable 2 | 3 | # 4 | module Domain 5 | 6 | # TODO: Should this be in Help class? 7 | def usage_name 8 | list = [] 9 | ancestors.each do |ancestor| 10 | break if Executable == ancestor 11 | list.unshift calculate_command_name(ancestor).to_s.strip 12 | end 13 | list.reject{|n| n.empty?}.join(" ") 14 | end 15 | 16 | # 17 | def calculate_command_name(ancestor) 18 | if ancestor.methods(false).include?(:command_name) 19 | command_name.to_s 20 | else 21 | cname = ancestor.name.sub(/\#\<.*?\>\:\:/,'').split('::').last.downcase 22 | cname.chomp('command').chomp('cli') 23 | end 24 | end 25 | 26 | private :calculate_command_name 27 | 28 | # 29 | # Helper method for creating switch attributes. 30 | # 31 | # This is equivalent to: 32 | # 33 | # def name=(val) 34 | # @name = val 35 | # end 36 | # 37 | # def name? 38 | # @name 39 | # end 40 | # 41 | # TODO: Currently there is an unfortunate issue with using 42 | # this helper method. If does not correctly record the location 43 | # the method is called, so default help message is wrong. 44 | # 45 | def attr_switch(name) 46 | file, line = *caller[0].split(':')[0..1] 47 | module_eval(<<-END, file, line.to_i) 48 | def #{name}=(value) 49 | @#{name}=(value) 50 | end 51 | def #{name}? 52 | @#{name} 53 | end 54 | END 55 | end 56 | 57 | # 58 | # Alias a switch. 59 | # 60 | def alias_switch(name, origin) 61 | alias_method "#{name}=", "#{origin}=" 62 | alias_method "#{name}?", "#{origin}?" 63 | end 64 | 65 | # 66 | # Alias an accessor. 67 | # 68 | def alias_accessor(name, origin) 69 | alias_method "#{name}=", "#{origin}=" 70 | alias_method "#{name}", "#{origin}" 71 | end 72 | 73 | # 74 | # Inspection method. This must be redefined b/c #to_s is overridden. 75 | # 76 | def inspect 77 | name 78 | end 79 | 80 | # 81 | # Returns `help.to_s`. If you want to provide your own help 82 | # text you can override this method in your command subclass. 83 | # 84 | def to_s 85 | cli.to_s 86 | end 87 | 88 | # 89 | # Interface with cooresponding cli/help object. 90 | # 91 | def help 92 | @help ||= Help.new(self) 93 | end 94 | 95 | # 96 | # Interface with cooresponding cli/help object. 97 | # 98 | alias_method :cli, :help 99 | 100 | # 101 | # Execute the command. 102 | # 103 | # @param argv [Array] command-line arguments 104 | # 105 | def execute(argv=ARGV) 106 | cli, args = parser.parse(argv) 107 | cli.call(*args) 108 | return cli 109 | end 110 | 111 | # 112 | # Executables don't run, they execute! But... 113 | # 114 | alias_method :run, :execute 115 | 116 | # 117 | # 118 | # @return [Array] The executable and call arguments. 119 | def parse(argv) 120 | parser.parse(argv) 121 | end 122 | 123 | # 124 | # The parser for this command. 125 | # 126 | def parser 127 | @parser ||= Parser.new(self) 128 | end 129 | 130 | # 131 | # Index of subcommands. 132 | # 133 | # @return [Hash] name mapped to subcommnd class 134 | # 135 | def subcommands 136 | @subcommands ||= ( 137 | consts = constants - superclass.constants 138 | consts.inject({}) do |h, c| 139 | c = const_get(c) 140 | if Class === c && Executable > c 141 | n = c.name.split('::').last 142 | n = n.chomp('Command').chomp('CLI') 143 | n = n.downcase 144 | h[n] = c 145 | end 146 | h 147 | end 148 | ) 149 | end 150 | 151 | end 152 | 153 | end 154 | -------------------------------------------------------------------------------- /work/deprecated/simple-executable/executable.rb: -------------------------------------------------------------------------------- 1 | # = Executable Mixin 2 | # 3 | # The Executable mixin is a very quick and and easy way to make almost 4 | # any class usable via a command line interface. It simply uses writer 5 | # methods as option setters, and the first command line argument as a 6 | # method to call with the subsequent arguments passed to the method. 7 | # 8 | # The only limitation of this approach is that non-boolean options must 9 | # be specified with `key=value` notation. 10 | # 11 | # class Example 12 | # include Executable 13 | # 14 | # attr_accessor :quiet 15 | # 16 | # def bread(*args) 17 | # ["bread", quiet, *args] 18 | # end 19 | # 20 | # def butter(*args) 21 | # ["butter", quiet, *args] 22 | # end 23 | # end 24 | # 25 | # ex = Example.new 26 | # 27 | # ex.execute!("butter yum") 28 | # => ["butter", nil, "yum"] 29 | # 30 | # ex.execute!("bread --quiet") 31 | # => ["butter", true] 32 | # 33 | # Executable also provides #option_missing, which you can overriden to provide 34 | # suitable results when a given command line option has no corresponding 35 | # writer method. 36 | # 37 | module Executable 38 | 39 | # Used the #excute! method to invoke the command. 40 | def execute!(argv=ARGV) 41 | Executable.execute(self, argv) 42 | end 43 | 44 | ## When no attribute write exists for an option that has been given on 45 | ## the command line #option_missing is called. Override #option_missing 46 | ## to handle these cases, if needed. Otherwise a NoMethodArgument will be 47 | ## raised. This callback method receives the name and value of the option. 48 | #def option_missing(opt, arg) 49 | # raise NoMethodError, "undefined option `#{opt}=' for #{self}" 50 | #end 51 | 52 | class << self 53 | 54 | # Process the arguments as an exectuable against the given object. 55 | def execute(obj, argv=ARGV) 56 | args = parse(obj, argv) 57 | subcmd = args.first 58 | if subcmd && !obj.respond_to?("#{subcmd}=") 59 | obj.send(*args) 60 | else 61 | obj.method_missing(*args) 62 | end 63 | end 64 | 65 | # The original name for #execute. 66 | alias_method :run, :execute 67 | 68 | # Parse command line with respect to +obj+. 69 | def parse(obj, argv) 70 | case argv 71 | when String 72 | require 'shellwords' 73 | argv = Shellwords.shellwords(argv) 74 | else 75 | argv = argv.dup 76 | end 77 | 78 | argv = argv.dup 79 | args, opts, i = [], {}, 0 80 | while argv.size > 0 81 | case opt = argv.shift 82 | when /=/ 83 | parse_equal(obj, opt, argv) 84 | when /^--/ 85 | parse_option(obj, opt, argv) 86 | when /^-/ 87 | parse_flags(obj, opt, argv) 88 | else 89 | args << opt 90 | end 91 | end 92 | return args 93 | end 94 | 95 | # Parse a setting option. 96 | def parse_equal(obj, opt, argv) 97 | if md = /^[-]*(.*?)=(.*?)$/.match(opt) 98 | x, v = md[1], md[2] 99 | else 100 | raise ArgumentError, "#{x}" 101 | end 102 | # TODO: to_b if 'true' or 'false' ? 103 | #if obj.respond_to?("#{x}=") 104 | obj.send("#{x}=", v) 105 | #else 106 | # obj.option_missing(x, v) 107 | #end 108 | end 109 | 110 | # Parse a named boolean option. 111 | def parse_option(obj, opt, argv) 112 | x = opt.sub(/^--/, '') 113 | #if obj.respond_to?("#{x}=") 114 | obj.send("#{x}=", true) 115 | #else 116 | # obj.option_missing(x, true) 117 | #end 118 | end 119 | 120 | # Parse flags. Each character of a flag set is treated as a separate option. 121 | # For example: 122 | # 123 | # $ foo -abc 124 | # 125 | # Would be parsed the same as: 126 | # 127 | # $ foo -a -b -c 128 | # 129 | def parse_flags(obj, opt, args) 130 | x = opt.sub(/^-/, '') 131 | #c = 0 132 | x.split(//).each do |k| 133 | #if obj.respond_to?("#{k}=") 134 | obj.send("#{k}=", true) 135 | #else 136 | # obj.option_missing(x, true) 137 | #end 138 | end 139 | end 140 | 141 | end #class << self 142 | 143 | end 144 | -------------------------------------------------------------------------------- /demo/09_optparse_example.md: -------------------------------------------------------------------------------- 1 | ## OptionParser Example 2 | 3 | This example mimics the one given in optparse.rb documentation. 4 | 5 | require 'ostruct' 6 | require 'time' 7 | 8 | class ExampleCLI < Executable::Command 9 | 10 | CODES = %w[iso-2022-jp shift_jis euc-jp utf8 binary] 11 | CODE_ALIASES = { "jis" => "iso-2022-jp", "sjis" => "shift_jis" } 12 | 13 | attr :options 14 | 15 | def initialize 16 | super 17 | reset 18 | end 19 | 20 | def reset 21 | @options = OpenStruct.new 22 | @options.library = [] 23 | @options.inplace = false 24 | @options.encoding = "utf8" 25 | @options.transfer_type = :auto 26 | @options.verbose = false 27 | end 28 | 29 | # Require the LIBRARY before executing your script 30 | def require=(lib) 31 | options.library << lib 32 | end 33 | alias :r= :require= 34 | 35 | # Edit ARGV files in place (make backup if EXTENSION supplied) 36 | def inplace=(ext) 37 | options.inplace = true 38 | options.extension = ext 39 | options.extension.sub!(/\A\.?(?=.)/, ".") # ensure extension begins with dot. 40 | end 41 | alias :i= :inplace= 42 | 43 | # Delay N seconds before executing 44 | # Cast 'delay' argument to a Float. 45 | def delay=(n) 46 | options.delay = n.to_float 47 | end 48 | 49 | # Begin execution at given time 50 | # Cast 'time' argument to a Time object. 51 | def time=(time) 52 | options.time = Time.parse(time) 53 | end 54 | alias :t= :time= 55 | 56 | # Specify record separator (default \\0) 57 | # Cast to octal integer. 58 | def irs=(octal) 59 | options.record_separator = octal.to_i(8) 60 | end 61 | alias :F= :irs= 62 | 63 | # Example 'list' of arguments 64 | # List of arguments. 65 | def list=(args) 66 | options.list = list.split(',') 67 | end 68 | 69 | # Keyword completion. We are specifying a specific set of arguments (CODES 70 | # and CODE_ALIASES - notice the latter is a Hash), and the user may provide 71 | # the shortest unambiguous text. 72 | CODE_LIST = (CODE_ALIASES.keys + CODES) 73 | 74 | help.option(:code, "Select encoding (#{CODE_LIST})") 75 | 76 | # Select encoding 77 | def code=(code) 78 | codes = CODE_LIST.select{ |x| /^#{code}/ =~ x } 79 | codes = codes.map{ |x| CODE_ALIASES.key?(x) ? CODE_ALIASES[x] : x }.uniq 80 | raise ArgumentError unless codes.size == 1 81 | options.encoding = codes.first 82 | end 83 | 84 | # Select transfer type (text, binary, auto) 85 | # Optional argument with keyword completion. 86 | def type=(type) 87 | raise ArgumentError unless %w{text binary auto}.include(type.downcase) 88 | options.transfer_type = type.downcase 89 | end 90 | 91 | # Run verbosely 92 | # Boolean switch. 93 | def verbose=(bool) 94 | options.verbose = bool 95 | end 96 | def verbose? 97 | @options.verbose 98 | end 99 | alias :v= :verbose= 100 | alias :v? :verbose? 101 | 102 | # Show this message 103 | # No argument, shows at tail. This will print an options summary. 104 | def help! 105 | puts help_text 106 | exit 107 | end 108 | alias :h! :help! 109 | 110 | # Show version 111 | # Another typical switch to print the version. 112 | def version? 113 | puts Executor::VERSION 114 | exit 115 | end 116 | 117 | # 118 | def call 119 | # ... main procedure here ... 120 | end 121 | end 122 | 123 | We will run some scenarios on this example to make sure it works. 124 | 125 | cli = ExampleCLI.execute('-r=facets') 126 | cli.options.library.assert == ['facets'] 127 | 128 | Make sure time option parses. 129 | 130 | cli = ExampleCLI.execute('--time=2010-10-10') 131 | cli.options.time.assert == Time.parse('2010-10-10') 132 | 133 | Make sure code lookup words and is limted to the selections provided. 134 | 135 | cli = ExampleCLI.execute('--code=ji') 136 | cli.options.encoding.assert == 'iso-2022-jp' 137 | 138 | expect ArgumentError do 139 | ExampleCLI.execute('--code=xxx') 140 | end 141 | 142 | Ensure +irs+ is set to an octal number. 143 | 144 | cli = ExampleCLI.execute('-F 32') 145 | cli.options.record_separator.assert == 032 146 | 147 | Ensure extension begins with dot and inplace is set to true. 148 | 149 | cli = ExampleCLI.execute('--inplace txt') 150 | cli.options.extension.assert == '.txt' 151 | cli.options.inplace.assert == true 152 | 153 | -------------------------------------------------------------------------------- /work/deprecated/simple-help/executable.rb: -------------------------------------------------------------------------------- 1 | # = Executable Mixin 2 | # 3 | # The Executable mixin is a very quick and and easy way to make almost 4 | # any class usable via a command line interface. It simply uses writer 5 | # methods as option setters, and the first command line argument as a 6 | # method to call with the subsequent arguments passed to the method. 7 | # 8 | # The only limitation of this approach is that non-boolean options must 9 | # be specified with `key=value` notation. 10 | # 11 | # class Example 12 | # include Executable 13 | # 14 | # attr_accessor :quiet 15 | # 16 | # def bread(*args) 17 | # ["bread", quiet, *args] 18 | # end 19 | # 20 | # def butter(*args) 21 | # ["butter", quiet, *args] 22 | # end 23 | # end 24 | # 25 | # ex = Example.new 26 | # 27 | # ex.execute!("butter yum") 28 | # => ["butter", nil, "yum"] 29 | # 30 | # ex.execute!("bread --quiet") 31 | # => ["butter", true] 32 | # 33 | # Executable also provides #option_missing, which you can overriden to provide 34 | # suitable results when a given command line option has no corresponding 35 | # writer method. 36 | # 37 | module Executable 38 | 39 | # Used the #excute! method to invoke the command. 40 | def execute!(argv=ARGV) 41 | Executable.execute(self, argv) 42 | end 43 | 44 | ## When no attribute write exists for an option that has been given on 45 | ## the command line #option_missing is called. Override #option_missing 46 | ## to handle these cases, if needed. Otherwise a NoMethodArgument will be 47 | ## raised. This callback method receives the name and value of the option. 48 | #def option_missing(opt, arg) 49 | # raise NoMethodError, "undefined option `#{opt}=' for #{self}" 50 | #end 51 | 52 | class << self 53 | 54 | # Process the arguments as an exectuable against the given object. 55 | def execute(obj, argv=ARGV) 56 | args = parse(obj, argv) 57 | subcmd = args.first 58 | if subcmd && !obj.respond_to?("#{subcmd}=") 59 | obj.send(*args) 60 | else 61 | obj.method_missing(*args) 62 | end 63 | end 64 | 65 | # The original name for #execute. 66 | alias_method :run, :execute 67 | 68 | # Parse command line with respect to +obj+. 69 | def parse(obj, argv) 70 | case argv 71 | when String 72 | require 'shellwords' 73 | argv = Shellwords.shellwords(argv) 74 | else 75 | argv = argv.dup 76 | end 77 | 78 | argv = argv.dup 79 | args, opts, i = [], {}, 0 80 | while argv.size > 0 81 | case opt = argv.shift 82 | when /=/ 83 | parse_equal(obj, opt, argv) 84 | when /^--/ 85 | parse_option(obj, opt, argv) 86 | when /^-/ 87 | parse_flags(obj, opt, argv) 88 | else 89 | args << opt 90 | end 91 | end 92 | return args 93 | end 94 | 95 | # Parse a setting option. 96 | def parse_equal(obj, opt, argv) 97 | if md = /^[-]*(.*?)=(.*?)$/.match(opt) 98 | x, v = md[1], md[2] 99 | else 100 | raise ArgumentError, "#{x}" 101 | end 102 | # TODO: to_b if 'true' or 'false' ? 103 | #if obj.respond_to?("#{x}=") 104 | obj.send("#{x}=", v) 105 | #else 106 | # obj.option_missing(x, v) 107 | #end 108 | end 109 | 110 | # Parse a named boolean option. 111 | def parse_option(obj, opt, argv) 112 | x = opt.sub(/^--/, '') 113 | #if obj.respond_to?("#{x}=") 114 | obj.send("#{x}=", true) 115 | #else 116 | # obj.option_missing(x, true) 117 | #end 118 | end 119 | 120 | # Parse flags. Each character of a flag set is treated as a separate option. 121 | # For example: 122 | # 123 | # $ foo -abc 124 | # 125 | # Would be parsed the same as: 126 | # 127 | # $ foo -a -b -c 128 | # 129 | def parse_flags(obj, opt, args) 130 | x = opt.sub(/^-/, '') 131 | #c = 0 132 | x.split(//).each do |k| 133 | #if obj.respond_to?("#{k}=") 134 | obj.send("#{k}=", true) 135 | #else 136 | # obj.option_missing(x, true) 137 | #end 138 | end 139 | end 140 | 141 | end #class << self 142 | 143 | # Optional support for implicit flags. 144 | # 145 | # TODO: implement Flags mixin 146 | module Flags 147 | 148 | end #module Flags 149 | 150 | # Optional help mixin. 151 | # 152 | module Help 153 | 154 | def self.included(base) 155 | base.extend Domain 156 | super(base) 157 | end 158 | 159 | # Class-level domain. 160 | module Domain 161 | # 162 | def help(text=nil) 163 | return(@help ||= {}) unless text 164 | singleton_class = (class << self; self; end) 165 | singleton_class.class_eval do 166 | define_method(:method_added) do |name| 167 | @help ||= {} 168 | @help[name] = text 169 | singleton_class.send(:remove_method, :method_added) 170 | end 171 | end 172 | end 173 | end 174 | 175 | end #module Help 176 | 177 | end #module Executable 178 | -------------------------------------------------------------------------------- /lib/executable/parser.rb: -------------------------------------------------------------------------------- 1 | module Executable 2 | 3 | # The Parser class does all the heavy lifting for Executable. 4 | # 5 | class Parser 6 | 7 | # 8 | # @param [Executable] cli_class 9 | # An executabe class. 10 | # 11 | def initialize(cli_class) 12 | @cli_class = cli_class 13 | end 14 | 15 | attr :cli_class 16 | 17 | # Parse command-line. 18 | # 19 | # @param argv [Array,String] command-line arguments 20 | # 21 | def parse(argv=ARGV) 22 | # duplicate to make sure ARGV stay intact. 23 | argv = argv.dup 24 | argv = parse_shellwords(argv) 25 | 26 | cmd, argv = parse_subcommand(argv) 27 | cli = cmd.new 28 | 29 | if argv.first == "_" 30 | cli = Completion.new(cli.class) 31 | else 32 | args = parse_arguments(cli, argv) 33 | end 34 | 35 | return cli, args 36 | end 37 | 38 | # Make sure arguments are an array. If argv is a String, 39 | # then parse using Shellwords module. 40 | # 41 | # @param argv [Array,String] commmand-line arguments 42 | def parse_shellwords(argv) 43 | if String === argv 44 | require 'shellwords' 45 | argv = Shellwords.shellwords(argv) 46 | end 47 | argv.to_a 48 | end 49 | 50 | # 51 | # 52 | # 53 | def parse_subcommand(argv) 54 | cmd = cli_class 55 | arg = argv.first 56 | 57 | while c = cmd.subcommands[arg] 58 | cmd = c 59 | argv.shift 60 | arg = argv.first 61 | end 62 | 63 | return cmd, argv 64 | end 65 | 66 | # 67 | # Parse command line options based on given object. 68 | # 69 | # @param obj [Object] basis for command-line parsing 70 | # @param argv [Array,String] command-line arguments 71 | # @param args [Array] pre-seeded arguments to add to 72 | # 73 | # @return [Array] parsed arguments 74 | # 75 | def parse_arguments(obj, argv, args=[]) 76 | case argv 77 | when String 78 | require 'shellwords' 79 | argv = Shellwords.shellwords(argv) 80 | #else 81 | # argv = argv.dup 82 | end 83 | 84 | #subc = nil 85 | #@args = [] #opts, i = {}, 0 86 | 87 | while argv.size > 0 88 | case arg = argv.shift 89 | when /=/ 90 | parse_equal(obj, arg, argv, args) 91 | when /^--/ 92 | parse_long(obj, arg, argv, args) 93 | when /^-/ 94 | parse_flags(obj, arg, argv, args) 95 | else 96 | #if Executable === obj 97 | # if cmd_class = obj.class.subcommands[arg] 98 | # cmd = cmd_class.new(obj) 99 | # subc = cmd 100 | # parse(cmd, argv, args) 101 | # else 102 | args << arg 103 | # end 104 | #end 105 | end 106 | end 107 | 108 | return args 109 | end 110 | 111 | # 112 | # Parse equal setting comman-line option. 113 | # 114 | def parse_equal(obj, opt, argv, args) 115 | if md = /^[-]*(.*?)=(.*?)$/.match(opt) 116 | x, v = md[1], md[2] 117 | else 118 | raise ArgumentError, "#{x}" 119 | end 120 | if obj.respond_to?("#{x}=") 121 | v = true if v == 'true' # yes or on ? 122 | v = false if v == 'false' # no or off ? 123 | obj.send("#{x}=", v) 124 | else 125 | obj.__send__(:option_missing, x, v) # argv? 126 | end 127 | end 128 | 129 | # 130 | # Parse double-dash command-line option. 131 | # 132 | def parse_long(obj, opt, argv, args) 133 | x = opt.sub(/^\-+/, '') # remove '--' 134 | if obj.respond_to?("#{x}=") 135 | m = obj.method("#{x}=") 136 | if obj.respond_to?("#{x}?") 137 | m.call(true) 138 | else 139 | invoke(obj, m, argv) 140 | end 141 | elsif obj.respond_to?("#{x}!") 142 | invoke(obj, "#{x}!", argv) 143 | else 144 | # call even if private method 145 | obj.__send__(:option_missing, x, argv) 146 | end 147 | end 148 | 149 | # TODO: parse_flags needs some thought concerning character spliting and arguments. 150 | 151 | # 152 | # Parse single-dash command-line option. 153 | # 154 | def parse_flags(obj, opt, argv, args) 155 | x = opt[1..-1] 156 | c = 0 157 | x.split(//).each do |k| 158 | if obj.respond_to?("#{k}=") 159 | m = obj.method("#{k}=") 160 | if obj.respond_to?("#{x}?") 161 | m.call(true) 162 | else 163 | invoke(obj, m, argv) #m.call(argv.shift) 164 | end 165 | elsif obj.respond_to?("#{k}!") 166 | invoke(obj, "#{k}!", argv) 167 | else 168 | long = find_long_option(obj, k) 169 | if long 170 | if long.end_with?('=') && obj.respond_to?(long.chomp('=')+'?') 171 | invoke(obj, long, [true]) 172 | else 173 | invoke(obj, long, argv) 174 | end 175 | else 176 | obj.__send__(:option_missing, x, argv) 177 | end 178 | end 179 | end 180 | end 181 | 182 | # 183 | # 184 | # @todo Sort alphabetically? 185 | # 186 | def find_long_option(obj, char) 187 | meths = obj.methods.map{ |m| m.to_s } 188 | meths = meths.select do |m| 189 | m.start_with?(char) and (m.end_with?('=') or m.end_with?('!')) 190 | end 191 | meths.first 192 | end 193 | 194 | # 195 | # 196 | def invoke(obj, meth, argv) 197 | m = (Method === meth ? meth : obj.method(meth)) 198 | a = [] 199 | m.arity.abs.times{ a << argv.shift } 200 | m.call(*a) 201 | end 202 | 203 | # Index of subcommands. 204 | # 205 | # @return [Hash] name mapped to subcommnd class 206 | def subcommands 207 | @cli_class.subcommands 208 | end 209 | 210 | end 211 | 212 | end 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Website](http://rubyworks.github.com/executable) | 2 | [Report Issue](http://github.com/rubyworks/executable/features) | 3 | [Source Code](http://github.com/rubyworks/executable) 4 | ( [![Build Status](https://secure.travis-ci.org/rubyworks/indexer.png)](http://travis-ci.org/rubyworks/indexer) ) 5 | 6 | 7 | # Executable 8 | 9 | Executable is to the commandline, what ActiveRecord is the database. 10 | You can think of Executable as a *COM*, a Command-line Object Mapper, 11 | just as ActiveRecord is an ORM (Object Relational Mapper). Any class 12 | mixing in Executable or subclassing Executable::Command can define 13 | a complete command line tool using nothing more than Ruby's standard 14 | syntax. No special DSL is required. 15 | 16 | 17 | ## Features 18 | 19 | * Easy to use, just mixin or subclass. 20 | * Define #call to control the command procedure. 21 | * Public writers become options. 22 | * Namespace children become subcommands. 23 | * Or easily dispatch subcommands to public methods. 24 | * Generate help in plain text or markdown. 25 | 26 | 27 | ## Limitations 28 | 29 | * Ruby 1.9+ only. 30 | * Help doesn't handle aliases well (yet). 31 | 32 | 33 | ## Overview 34 | 35 | CLIs can be built by using a Executable as a mixin, or by subclassing 36 | `Executable::Command`. Methods seemlessly handle command-line options. 37 | Writer methods (those ending in '=') correspond to options and query 38 | methods (those ending in '?') modify them to be boolean switches. 39 | 40 | For example, here is a simple "Hello, World!" commandline tool. 41 | 42 | ```ruby 43 | require 'executable' 44 | 45 | class HelloCommand 46 | include Executable 47 | 48 | # Say it in uppercase? 49 | def loud=(bool) 50 | @loud = bool 51 | end 52 | 53 | # 54 | def loud? 55 | @loud 56 | end 57 | 58 | # Show this message. 59 | def help! 60 | cli.show_help 61 | exit 62 | end 63 | alias :h! :help! 64 | 65 | # Say hello. 66 | def call(name) 67 | name = name || 'World' 68 | str = "Hello, #{name}!" 69 | str = str.upcase if loud? 70 | puts str 71 | end 72 | end 73 | ``` 74 | 75 | To make the command available on the command line, add an executable 76 | to your project calling the #execute or #run methods. 77 | 78 | ```ruby 79 | #!usr/bin/env ruby 80 | require 'hello.rb' 81 | HelloCommand.run 82 | ``` 83 | 84 | If we named this file `hello`, set its execute flag and made it available 85 | on our systems $PATH, then: 86 | 87 | ``` 88 | $ hello 89 | Hello, World! 90 | 91 | $ hello John 92 | Hello, John! 93 | 94 | $ hello --loud John 95 | HELLO, JOHN! 96 | ``` 97 | 98 | Executable can also generate help text for commands. 99 | 100 | ``` 101 | $ hello --help 102 | USAGE: hello [options] 103 | 104 | Say hello. 105 | 106 | --loud Say it in uppercase? 107 | --help Show this message 108 | ``` 109 | 110 | If you look back at the class definition you can see it's pulling 111 | comments from the source to provide descriptions. It pulls the 112 | description for the command itself from the `#call` method. 113 | 114 | Basic help like this is fine for personal tools, but for public facing 115 | production applications it is desirable to utilize manpages. To this end, 116 | Executable provides Markdown formatted help as well. We can access this, 117 | for example, via `HelloCommand.help.markdown`. The idea with this is that 118 | we can save the output to `man/hello.ronn` or copy it the top of our `bin/` 119 | file, edit it to perfection and then use tools such a [ronn](https://github.com/rtomayko/ronn), 120 | [binman](https://github.com/sunaku/binman) or [md2man](https://github.com/sunaku/md2man) 121 | to generate the manpages. What's particularly cool about Executable, 122 | is that once we have a manpage in the standard `man/` location in our project, 123 | the `#show_help` method will use it instead of the plain text. 124 | 125 | For a more detailed example see [QED](http://rubyworks.github.com/executable/demo.html), 126 | [API](http://rubydoc.info/gems/executable/frames) documentation and, in particular, 127 | the [Wiki](http://wiki.github.com/rubyworks/). 128 | 129 | 130 | ## Installation 131 | 132 | Install with RubyGems in the usual fashion. 133 | 134 | ``` 135 | $ gem install executable 136 | ``` 137 | 138 | ## Contributing 139 | 140 | Executable is a [Rubyworks](http://rubyworks.github.com) project. As such it largely 141 | uses in-house tools for development. 142 | 143 | ### Submitting Patches 144 | 145 | If it is a very small change, just pasting it to an issue is fine. For anything more than 146 | this please send us a traditional patch, but even better use Github pull requests. 147 | Good contributions have the following: 148 | 149 | * Well documented code following the conventions of the project. 150 | * Clearly written tests with good test coverage written using the project's chosen test framework. 151 | * Use of a git topic branch to keep the change set well isolated. 152 | 153 | The more of these bullet points a pull request covers, the more likely and quickly it will 154 | be accepted and merged. 155 | 156 | ### Testing 157 | 158 | [QED](http://rubyworks.github.com/qed) and [Microtest](http://rubyworks.github.com/microtest) 159 | are used for this project. To run the QED demos just run the `qed` command, probably with bundler, 160 | so `bundle exec qed`. And to run the microtests you can use `rubytest test/`, again with bundler, 161 | `bundle exec rubytest test/`. 162 | 163 | ### Getting In Touch 164 | 165 | For direct dialog we have an IRC channel, #rubyworks on freenode. But it's not always manned, 166 | so a [mailing list](http://groups.google.com/groups/rubyworks-mailinglist) is also available. 167 | Of course these days, the GitHub [issues page](http://github.com/rubyworks/executable) is 168 | generally the place get in touch for anything specific to this project. 169 | 170 | 171 | ## Copyrights 172 | 173 | Executable is copyrighted open source software. 174 | 175 | Copyright (c) 2008 Rubyworks (BSD-2-Clause) 176 | 177 | It can be distributed and modified in accordance with the **BSD-2-Clause** license. 178 | 179 | See LICENSE.txt for details. 180 | -------------------------------------------------------------------------------- /.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'yaml' 4 | require 'pathname' 5 | 6 | module Indexer 7 | 8 | # Convert index data into a gemspec. 9 | # 10 | # Notes: 11 | # * Assumes all executables are in bin/. 12 | # * Does not yet handle default_executable setting. 13 | # * Does not yet handle platform setting. 14 | # * Does not yet handle required_ruby_version. 15 | # * Support for rdoc entries is weak. 16 | # 17 | class GemspecExporter 18 | 19 | # File globs to include in package --unless a manifest file exists. 20 | FILES = ".index .yardopts alt bin data demo ext features lib man spec test try* [A-Z]*.*" unless defined?(FILES) 21 | 22 | # File globs to omit from FILES. 23 | OMIT = "Config.rb" unless defined?(OMIT) 24 | 25 | # Standard file patterns. 26 | PATTERNS = { 27 | :root => '{.index,Gemfile}', 28 | :bin => 'bin/*', 29 | :lib => 'lib/{**/}*', #.rb', 30 | :ext => 'ext/{**/}extconf.rb', 31 | :doc => '*.{txt,rdoc,md,markdown,tt,textile}', 32 | :test => '{test,spec}/{**/}*.rb' 33 | } unless defined?(PATTERNS) 34 | 35 | # For which revision of indexer spec is this converter intended? 36 | REVISION = 2013 unless defined?(REVISION) 37 | 38 | # 39 | def self.gemspec 40 | new.to_gemspec 41 | end 42 | 43 | # 44 | attr :metadata 45 | 46 | # 47 | def initialize(metadata=nil) 48 | @root_check = false 49 | 50 | if metadata 51 | root_dir = metadata.delete(:root) 52 | if root_dir 53 | @root = root_dir 54 | @root_check = true 55 | end 56 | metadata = nil if metadata.empty? 57 | end 58 | 59 | @metadata = metadata || YAML.load_file(root + '.index') 60 | 61 | if @metadata['revision'].to_i != REVISION 62 | warn "This gemspec exporter was not designed for this revision of index metadata." 63 | end 64 | end 65 | 66 | # 67 | def has_root? 68 | root ? true : false 69 | end 70 | 71 | # 72 | def root 73 | return @root if @root || @root_check 74 | @root_check = true 75 | @root = find_root 76 | end 77 | 78 | # 79 | def manifest 80 | return nil unless root 81 | @manifest ||= Dir.glob(root + 'manifest{,.txt}', File::FNM_CASEFOLD).first 82 | end 83 | 84 | # 85 | def scm 86 | return nil unless root 87 | @scm ||= %w{git hg}.find{ |m| (root + ".#{m}").directory? }.to_sym 88 | end 89 | 90 | # 91 | def files 92 | return [] unless root 93 | @files ||= \ 94 | if manifest 95 | File.readlines(manifest). 96 | map{ |line| line.strip }. 97 | reject{ |line| line.empty? || line[0,1] == '#' } 98 | else 99 | list = [] 100 | Dir.chdir(root) do 101 | FILES.split(/\s+/).each do |pattern| 102 | list.concat(glob(pattern)) 103 | end 104 | OMIT.split(/\s+/).each do |pattern| 105 | list = list - glob(pattern) 106 | end 107 | end 108 | list 109 | end.select{ |path| File.file?(path) }.uniq 110 | end 111 | 112 | # 113 | def glob_files(pattern) 114 | return [] unless root 115 | Dir.chdir(root) do 116 | Dir.glob(pattern).select do |path| 117 | File.file?(path) && files.include?(path) 118 | end 119 | end 120 | end 121 | 122 | def patterns 123 | PATTERNS 124 | end 125 | 126 | def executables 127 | @executables ||= \ 128 | glob_files(patterns[:bin]).map do |path| 129 | File.basename(path) 130 | end 131 | end 132 | 133 | def extensions 134 | @extensions ||= \ 135 | glob_files(patterns[:ext]).map do |path| 136 | File.basename(path) 137 | end 138 | end 139 | 140 | def name 141 | metadata['name'] || metadata['title'].downcase.gsub(/\W+/,'_') 142 | end 143 | 144 | def homepage 145 | page = ( 146 | metadata['resources'].find{ |r| r['type'] =~ /^home/i } || 147 | metadata['resources'].find{ |r| r['name'] =~ /^home/i } || 148 | metadata['resources'].find{ |r| r['name'] =~ /^web/i } 149 | ) 150 | page ? page['uri'] : false 151 | end 152 | 153 | def licenses 154 | metadata['copyrights'].map{ |c| c['license'] }.compact 155 | end 156 | 157 | def require_paths 158 | paths = metadata['paths'] || {} 159 | paths['load'] || ['lib'] 160 | end 161 | 162 | # 163 | # Convert to gemnspec. 164 | # 165 | def to_gemspec 166 | if has_root? 167 | Gem::Specification.new do |gemspec| 168 | to_gemspec_data(gemspec) 169 | to_gemspec_paths(gemspec) 170 | end 171 | else 172 | Gem::Specification.new do |gemspec| 173 | to_gemspec_data(gemspec) 174 | to_gemspec_paths(gemspec) 175 | end 176 | end 177 | end 178 | 179 | # 180 | # Convert pure data settings. 181 | # 182 | def to_gemspec_data(gemspec) 183 | gemspec.name = name 184 | gemspec.version = metadata['version'] 185 | gemspec.summary = metadata['summary'] 186 | gemspec.description = metadata['description'] 187 | 188 | metadata['authors'].each do |author| 189 | gemspec.authors << author['name'] 190 | 191 | if author.has_key?('email') 192 | if gemspec.email 193 | gemspec.email << author['email'] 194 | else 195 | gemspec.email = [author['email']] 196 | end 197 | end 198 | end 199 | 200 | gemspec.licenses = licenses 201 | 202 | requirements = metadata['requirements'] || [] 203 | requirements.each do |req| 204 | next if req['optional'] 205 | next if req['external'] 206 | 207 | name = req['name'] 208 | groups = req['groups'] || [] 209 | 210 | version = gemify_version(req['version']) 211 | 212 | if groups.empty? or groups.include?('runtime') 213 | # populate runtime dependencies 214 | if gemspec.respond_to?(:add_runtime_dependency) 215 | gemspec.add_runtime_dependency(name,*version) 216 | else 217 | gemspec.add_dependency(name,*version) 218 | end 219 | else 220 | # populate development dependencies 221 | if gemspec.respond_to?(:add_development_dependency) 222 | gemspec.add_development_dependency(name,*version) 223 | else 224 | gemspec.add_dependency(name,*version) 225 | end 226 | end 227 | end 228 | 229 | # convert external dependencies into gemspec requirements 230 | requirements.each do |req| 231 | next unless req['external'] 232 | gemspec.requirements << ("%s-%s" % req.values_at('name', 'version')) 233 | end 234 | 235 | gemspec.homepage = homepage 236 | gemspec.require_paths = require_paths 237 | gemspec.post_install_message = metadata['install_message'] 238 | end 239 | 240 | # 241 | # Set gemspec settings that require a root directory path. 242 | # 243 | def to_gemspec_paths(gemspec) 244 | gemspec.files = files 245 | gemspec.extensions = extensions 246 | gemspec.executables = executables 247 | 248 | if Gem::VERSION < '1.7.' 249 | gemspec.default_executable = gemspec.executables.first 250 | end 251 | 252 | gemspec.test_files = glob_files(patterns[:test]) 253 | 254 | unless gemspec.files.include?('.document') 255 | gemspec.extra_rdoc_files = glob_files(patterns[:doc]) 256 | end 257 | end 258 | 259 | # 260 | # Return a copy of this file. This is used to generate a local 261 | # .gemspec file that can automatically read the index file. 262 | # 263 | def self.source_code 264 | File.read(__FILE__) 265 | end 266 | 267 | private 268 | 269 | def find_root 270 | root_files = patterns[:root] 271 | if Dir.glob(root_files).first 272 | Pathname.new(Dir.pwd) 273 | elsif Dir.glob("../#{root_files}").first 274 | Pathname.new(Dir.pwd).parent 275 | else 276 | #raise "Can't find root of project containing `#{root_files}'." 277 | warn "Can't find root of project containing `#{root_files}'." 278 | nil 279 | end 280 | end 281 | 282 | def glob(pattern) 283 | if File.directory?(pattern) 284 | Dir.glob(File.join(pattern, '**', '*')) 285 | else 286 | Dir.glob(pattern) 287 | end 288 | end 289 | 290 | def gemify_version(version) 291 | case version 292 | when /^(.*?)\+$/ 293 | ">= #{$1}" 294 | when /^(.*?)\-$/ 295 | "< #{$1}" 296 | when /^(.*?)\~$/ 297 | "~> #{$1}" 298 | else 299 | version 300 | end 301 | end 302 | 303 | end 304 | 305 | end 306 | 307 | Indexer::GemspecExporter.gemspec -------------------------------------------------------------------------------- /lib/executable/help.rb: -------------------------------------------------------------------------------- 1 | require 'executable/core_ext' 2 | 3 | module Executable 4 | 5 | # Encpsulates command help for defining and displaying well formated help 6 | # output in plain text, markdown or via manpages if found. 7 | # 8 | # Creating text help in the fly is fine for personal projects, but 9 | # for production app, ideally you want to have man pages. You can 10 | # use the #markdown method to generate `.ronn` files and use the 11 | # ronn tool to build manpages for your project. There is also the 12 | # binman and md2man projects which can be used similarly. 13 | # 14 | class Help 15 | 16 | # 17 | def self.section(name, &default) 18 | define_method("default_#{name}", &default) 19 | class_eval %{ 20 | def #{name}(text=nil) 21 | @#{name} = text.to_s unless text.nil? 22 | @#{name} ||= default_#{name} 23 | end 24 | def #{name}=(text) 25 | @#{name} = text.to_s 26 | end 27 | } 28 | end 29 | 30 | # 31 | # Setup new help object. 32 | # 33 | def initialize(cli_class) 34 | @cli_class = cli_class 35 | 36 | @name = nil 37 | @usage = nil 38 | @decription = nil 39 | @copying = nil 40 | @see_also = nil 41 | 42 | @options = {} 43 | @subcmds = {} 44 | end 45 | 46 | # 47 | alias_method :inspect, :to_s 48 | 49 | # 50 | # The Executable subclass to which this help applies. 51 | # 52 | attr :cli_class 53 | 54 | # 55 | # Get or set command name. 56 | # 57 | # By default the name is assumed to be the class name, substituting 58 | # dashes for double colons. 59 | # 60 | # @todo Should this instead default to `File.basename($0)` ? 61 | # 62 | # @method name(text=nil) 63 | # 64 | section(:name) do 65 | cli_class.usage_name 66 | end 67 | 68 | # 69 | # Get or set command usage string. 70 | # 71 | # @method usage(text=nil) 72 | # 73 | section(:usage) do 74 | "Usage: " + name + ' [options...] [subcommand]' 75 | end 76 | 77 | # 78 | # Get or set command description. 79 | # 80 | # @method description(text=nil) 81 | # 82 | section(:description) do 83 | nil 84 | end 85 | 86 | # 87 | # Get or set copyright text. 88 | # 89 | # @method copyright(text=nil) 90 | # 91 | section(:copyright) do 92 | 'Copyright (c) ' + Time.now.strftime('%Y') 93 | end 94 | 95 | # 96 | # Get or set "see also" text. 97 | # 98 | # @method see_also(text=nil) 99 | # 100 | section(:see_also) do 101 | nil 102 | end 103 | 104 | # 105 | # Set description of an option. 106 | # 107 | def option(name, description) 108 | @options[name.to_s] = description 109 | end 110 | 111 | # 112 | # Set desciption of a subcommand. 113 | # 114 | def subcommand(name, description) 115 | @subcmds[name.to_s] = description 116 | end 117 | 118 | # 119 | # Show help. 120 | # 121 | # @todo man-pages will probably fail on Windows. 122 | # 123 | def show_help(hint=nil) 124 | if file = manpage(hint) 125 | show_manpage(file) 126 | else 127 | puts self 128 | end 129 | end 130 | 131 | # 132 | alias :show :show_help 133 | 134 | # 135 | def show_manpage(file) 136 | #exec "man #{file}" 137 | system "man #{file}" 138 | end 139 | 140 | # 141 | # Get man-page if there is one. 142 | # 143 | def manpage(dir=nil) 144 | @manpage ||= ( 145 | man = [] 146 | dir = nil 147 | 148 | if dir 149 | raise unless File.directory?(dir) 150 | end 151 | 152 | if !dir && call_method 153 | file, line = call_method.source_location 154 | dir = File.dirname(file) 155 | end 156 | 157 | man_name = name.gsub(/\s+/, '-') + '.1' 158 | 159 | if dir 160 | glob = "man/{man1/,}#{man_name}" 161 | lookup(glob, dir) 162 | else 163 | nil 164 | end 165 | ) 166 | end 167 | 168 | # 169 | # Render help text to a given +format+. If no format it given 170 | # then renders to plain text. 171 | # 172 | def to_s(format=nil) 173 | case format 174 | when :markdown, :md 175 | markdown 176 | else 177 | text 178 | end 179 | end 180 | 181 | # 182 | # Generate plain text output. 183 | # 184 | def text 185 | commands = text_subcommands 186 | options = text_options 187 | 188 | s = [] 189 | 190 | s << usage 191 | s << text_description 192 | 193 | if !commands.empty? 194 | s << "COMMANDS\n" + commands.map{ |cmd, desc| 195 | " %-17s %s" % [cmd, desc] 196 | }.join("\n") 197 | end 198 | 199 | if !options.empty? 200 | s << "OPTIONS\n" + options.map{ |max, opts, desc| 201 | " %-#{max}s %s" % [opts.join(' '), desc] 202 | }.join("\n") 203 | end 204 | 205 | s << copyright 206 | s << see_also 207 | 208 | s.compact.join("\n\n") 209 | end 210 | 211 | # 212 | alias_method :txt, :text 213 | 214 | # 215 | # Generate a RONN-style Markdown. 216 | # 217 | def markdown 218 | commands = text_subcommands 219 | options = text_options 220 | dashname = name.sub(/\s+/, '-') 221 | 222 | s = [] 223 | 224 | h = "#{dashname}(1) - #{text_description}" 225 | s << h + "\n" + ("=" * h.size) 226 | 227 | s << "## SYNOPSIS" 228 | s << "`" + name + "` [options...] [subcommand]" 229 | 230 | s << "## DESCRIPTION" 231 | s << text_description 232 | 233 | if !commands.empty? 234 | s << "## COMMANDS" 235 | s << commands.map{ |cmd, desc| 236 | " * `%s:`\n %s" % [cmd, desc] 237 | }.join("\n") 238 | end 239 | 240 | if !options.empty? 241 | s << "## OPTIONS" 242 | s << options.map{ |max, opts, desc| 243 | " * `%s`:\n %s" % [opts.join(' '), desc] 244 | }.join("\n\n") 245 | end 246 | 247 | if copyright && !copyright.empty? 248 | s << "## COPYRIGHT" 249 | s << copyright 250 | end 251 | 252 | if see_also && !see_also.empty? 253 | s << "## SEE ALSO" 254 | s << see_also 255 | end 256 | 257 | s.compact.join("\n\n") 258 | end 259 | 260 | # 261 | alias_method :md, :markdown 262 | 263 | # 264 | # Description of command in printable form. 265 | # But will return +nil+ if there is no description. 266 | # 267 | # @return [String,NilClass] command description 268 | # 269 | def text_description 270 | return description if description 271 | #return Source.get_above_comment(@file, @line) if @file 272 | 273 | call_method ? call_method.comment : nil 274 | end 275 | 276 | # 277 | # List of subcommands converted to a printable string. 278 | # But will return +nil+ if there are no subcommands. 279 | # 280 | # @return [String,NilClass] subcommand list text 281 | # 282 | def text_subcommands 283 | commands = @cli_class.subcommands 284 | commands.map do |cmd, klass| 285 | desc = klass.help.text_description.to_s.split("\n").first 286 | [cmd, desc] 287 | end 288 | end 289 | 290 | # 291 | # List of options coverted to a printable string. 292 | # But will return +nil+ if there are no options. 293 | # 294 | # @return [Array] option list for output 295 | # 296 | def text_options 297 | option_list.each do |opt| 298 | if @options.key?(opt.name) 299 | opt.description = @options[opt.name] 300 | end 301 | end 302 | 303 | # if two options have the same description, they must aliases 304 | aliased_options = option_list.group_by{ |opt| opt.description } 305 | 306 | list = aliased_options.map do |desc, opts| 307 | [opts.map{ |o| "%s%s" % [o.mark, o.usage] }, desc] 308 | end 309 | 310 | max = list.map{ |opts, desc| opts.join(' ').size }.max.to_i + 2 311 | 312 | list.map do |opts, desc| 313 | [max, opts, desc] 314 | end 315 | end 316 | 317 | # 318 | #def text_common_options 319 | #s << "\nCOMMON OPTIONS:\n\n" 320 | #global_options.each do |(name, meth)| 321 | # if name.size == 1 322 | # s << " -%-15s %s\n" % [name, descriptions[meth]] 323 | # else 324 | # s << " --%-15s %s\n" % [name, descriptions[meth]] 325 | # end 326 | #end 327 | #end 328 | 329 | # 330 | def option_list 331 | @option_list ||= ( 332 | method_list.map do |meth| 333 | case meth.name 334 | when /^(.*?)[\!\=]$/ 335 | Option.new(meth) 336 | end 337 | end.compact.sort 338 | ) 339 | end 340 | 341 | private 342 | 343 | # 344 | def call_method 345 | @call_method ||= method_list.find{ |m| m.name == :call } 346 | end 347 | 348 | # 349 | # Produce a list relavent methods. 350 | # 351 | def method_list 352 | list = [] 353 | methods = [] 354 | stop_at = cli_class.ancestors.index(Executable::Command) || 355 | cli_class.ancestors.index(Executable) || 356 | -1 357 | ancestors = cli_class.ancestors[0...stop_at] 358 | ancestors.reverse_each do |a| 359 | a.public_instance_methods(false).each do |m| 360 | list << cli_class.instance_method(m) 361 | end 362 | end 363 | list #.uniq 364 | end 365 | 366 | # 367 | # 368 | # 369 | def lookup(glob, dir) 370 | dir = File.expand_path(dir) 371 | root = '/' 372 | home = File.expand_path('~') 373 | list = [] 374 | 375 | while dir != home && dir != root 376 | list.concat(Dir.glob(File.join(dir, glob))) 377 | break unless list.empty? 378 | dir = File.dirname(dir) 379 | end 380 | 381 | list.first 382 | end 383 | 384 | # Encapsualtes a command line option. 385 | # 386 | class Option 387 | def initialize(method) 388 | @method = method 389 | end 390 | 391 | def name 392 | @method.name.to_s.chomp('!').chomp('=') 393 | end 394 | 395 | def comment 396 | @method.comment 397 | end 398 | 399 | def description 400 | @description ||= comment.split("\n").first 401 | end 402 | 403 | # Set description manually. 404 | def description=(desc) 405 | @description = desc 406 | end 407 | 408 | def parameter 409 | begin 410 | @method.owner.instance_method(@method.name.to_s.chomp('=') + '?') 411 | false 412 | rescue 413 | param = @method.parameters.first 414 | param.last if param 415 | end 416 | end 417 | 418 | # 419 | def usage 420 | if parameter 421 | "#{name}=#{parameter.to_s.upcase}" 422 | else 423 | "#{name}" 424 | end 425 | end 426 | 427 | def <=>(other) 428 | self.name <=> other.name 429 | end 430 | 431 | # 432 | def mark 433 | name.to_s.size == 1 ? '-' : '--' 434 | end 435 | 436 | end 437 | end 438 | 439 | end 440 | -------------------------------------------------------------------------------- /work/deprecated/command1/command-facets.rb: -------------------------------------------------------------------------------- 1 | # == CLI::Command 2 | # 3 | # Here is an example of usage: 4 | # 5 | # # General Options 6 | # 7 | # module GeneralOptions 8 | # attr_accessor :dryrun ; alias_accessor :n, :noharm, :dryrun 9 | # attr_accessor :quiet ; alias_accessor :q, :quiet 10 | # attr_accessor :force 11 | # attr_accessor :trace 12 | # end 13 | # 14 | # # Build Subcommand 15 | # 16 | # class BuildCommand < CLI::Command 17 | # include GeneralOptions 18 | # 19 | # # metadata files 20 | # attr_accessor :file ; alias_accessor :f, :file 21 | # attr_accessor :manifest ; alias_accessor :m, :manifest 22 | # 23 | # def call 24 | # # do stuf here 25 | # end 26 | # end 27 | # 28 | # # Box Master Command 29 | # 30 | # class BoxCommand < CLI::Command 31 | # subcommand :build, BuildCommand 32 | # end 33 | # 34 | # BoxCommand.start 35 | # 36 | # == Authors 37 | # 38 | # * Trans 39 | # 40 | # == Todo 41 | # 42 | # * Move helpers into core. 43 | # * Add global options to master command, or "all are master options" flag (?) 44 | # * Add usage/help/documentation/man features (?) 45 | # 46 | # == Copying 47 | # 48 | # Copyright (c) 2005,2008 Thomas Sawyer 49 | # 50 | # Ruby License 51 | # 52 | # This module is free software. You may use, modify, and/or 53 | # redistribute this software under the same terms as Ruby. 54 | # 55 | # This program is distributed in the hope that it will be 56 | # useful, but WITHOUT ANY WARRANTY; without even the implied 57 | # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 58 | # PURPOSE. 59 | 60 | warn "command.rb will be deprecated, use clio instead (clio.rubyforge.org)" 61 | 62 | require 'facets/arguments' 63 | require 'facets/consoleutils' 64 | require 'facets/array/not_empty' 65 | 66 | module CLI 67 | 68 | # For CommandOptions, but defined external to it, so 69 | # that it is easy to access from user defined commands. 70 | # (This lookup issue should be fixed in Ruby 1.9+, and then 71 | # the class can be moved back into Command namespace.) 72 | 73 | class NoOptionError < NoMethodError 74 | def initialize(name, *arg) 75 | super("unknown option -- #{name}", name, *args) 76 | end 77 | end 78 | 79 | class NoCommandError < NoMethodError 80 | def initialize(name, *arg) 81 | super("unknown subcommand -- #{name}", name, *args) 82 | end 83 | end 84 | 85 | # Here is an example of usage: 86 | # 87 | # # General Options 88 | # 89 | # module GeneralOptions 90 | # attr_writer :dryrun ; alias_writer :n, :noharm, :dryrun 91 | # attr_writer :quiet ; alias_writer :q, :quiet 92 | # attr_writer :force 93 | # attr_writer :trace 94 | # end 95 | # 96 | # # Build Subcommand 97 | # 98 | # class BuildCommand < Command 99 | # include GeneralOptions 100 | # 101 | # # metadata files 102 | # attr_writer :file ; alias_writer :f, :file 103 | # attr_writer :manifest ; alias_writer :m, :manifest 104 | # 105 | # def call 106 | # # do stuf here 107 | # end 108 | # end 109 | # 110 | # # Box Master Command 111 | # 112 | # class BoxCommand < CLI::Command 113 | # subcommand :build, BuildCommand 114 | # end 115 | # 116 | # BoxCommand.start 117 | 118 | class Command 119 | 120 | # Include this in your dispatch command if you want 121 | # all options to be traeted the same. 122 | 123 | module UniversalOptions 124 | end 125 | 126 | # 127 | 128 | def self.option_arity(arity_hash=nil) 129 | @option_arity ||= {} 130 | if arity_hash 131 | @option_arity.merge!(arity_hash) 132 | end 133 | @option_arity 134 | end 135 | 136 | # 137 | 138 | def self.start(line=nil) 139 | cargs = Arguments.new(line || ARGV, option_arity) 140 | pre = cargs.preoptions 141 | 142 | if instance_method(:call).arity == 0 #is_a?(SingleCommand) 143 | args, opts = *cargs.parameters 144 | new(args, opts).call 145 | else 146 | subc, args, opts = *cargs.subcommand 147 | if self < UniversalOptions 148 | new(pre, opts).call(subc, args, opts) 149 | else 150 | new(pre).call(subc, args, opts) 151 | end 152 | end 153 | end 154 | 155 | # Command Arguments (for single commands). 156 | 157 | attr :arguments 158 | 159 | # Command options. For dispatch commands these are the pre-options. 160 | 161 | attr :options 162 | 163 | # For dispatchers, this is a convenience method for creating subcommands. 164 | 165 | def self.subcommand(name, command_class, options=nil) 166 | options ||= {} 167 | if options[:no_merge] 168 | file, line = __FILE__, __LINE__+1 169 | code = %{ 170 | def #{name}(args, opts) 171 | #{command_class}.new(args, opts).call 172 | end 173 | } 174 | else 175 | file, line = __FILE__, __LINE__+1 176 | code = %{ 177 | def #{name}(args, opts) 178 | opts.merge(options) 179 | #{command_class}.new(args, opts).call 180 | end 181 | } 182 | end 183 | class_eval(code, file, line) 184 | end 185 | 186 | private 187 | 188 | # 189 | 190 | def initialize(*args) 191 | @arguments = [] 192 | @options = {} 193 | 194 | opts, args = *args.partition{ |e| Hash===e } 195 | #TEST("options should all be hashes"){ ! opts.all?{ |e| Hash===e } 196 | initialize_arguments(*args) 197 | initialize_options(*opts) 198 | end 199 | 200 | # 201 | 202 | def initialize_arguments(*arguments) 203 | @arguments.concat(arguments) 204 | end 205 | 206 | # 207 | 208 | def initialize_options(*options) 209 | options = options.inject{ |h,o| h.merge(o) } 210 | begin 211 | opt, val = nil, nil 212 | options.each do |opt, val| 213 | opt = opt.gsub('-','_') 214 | send("#{opt}=", val) 215 | end 216 | rescue NoMethodError 217 | option_missing(opt, val) 218 | end 219 | @options.update(options) 220 | end 221 | 222 | public 223 | 224 | # For a single command (ie. a subcommand) override #call with arity=0. 225 | 226 | def call(cmd=nil, *args) 227 | opts = Hash==args.last ? args.pop : {} 228 | #TEST("options should all be hashes"){ ! opts.all?{ |e| Hash===e } 229 | #cmd = :default if cmd.nil? 230 | if cmd.nil? 231 | default 232 | else 233 | begin 234 | # FIXME: rename call to [] ? 235 | raise NameError if cmd == 'call' 236 | raise NameError unless commands.include?(cmd.to_sym) 237 | subcommand = method(cmd) 238 | parameters = [args, opts] 239 | rescue NameError 240 | subcommand = method(:command_missing) 241 | parameters = [cmd, args, opts] 242 | end 243 | if subcommand.arity < 0 244 | subcommand.call(*parameters[0..subcommand.arity]) 245 | else 246 | subcommand.call(*parameters[0,subcommand.arity]) 247 | end 248 | end 249 | end 250 | 251 | # Display help message. 252 | # The one provided is just a very limited dummy routine. 253 | 254 | # def help 255 | # puts "USAGE #{File.basename($0)} [options]" 256 | # puts "\nOptions:" 257 | # options = self.class.instance_methods(false) 258 | # options = options - Command.instance_methods(true) 259 | # options = options.select{ |m| m.to_s =~ /=$/ } 260 | # options.each do |opt| 261 | # puts " --#{opt.to_s.chomp('=')}" 262 | # end 263 | # end 264 | 265 | private 266 | 267 | # Override default to provide non-subcommand functionality. 268 | 269 | def default; end 270 | 271 | # 272 | def commands 273 | @_commands ||= ( 274 | cmds = self.class.instance_methods(true) - Command.instance_methods(true) 275 | cmds.select{ |c| c !~ /\W/ } 276 | cmds.collect{ |c| c.to_sym } 277 | ) 278 | end 279 | 280 | # 281 | 282 | def command_missing(cmd, args, opt) 283 | raise NoCommandError.new(cmd, args << opt) 284 | end 285 | 286 | # 287 | 288 | def option_missing(opt, arg=nil) 289 | raise NoOptionError.new(opt) 290 | end 291 | 292 | end 293 | 294 | # Temporary backward compatability. 295 | MasterCommand = Command 296 | 297 | end 298 | 299 | 300 | module Console #:nodoc: 301 | # For backward compatibility. 302 | Command = CLI::Command 303 | end 304 | 305 | 306 | # SCRAP CODE FOR REFERENCE TO POSSIBLE ADD FUTURE FEATURES 307 | 308 | =begin 309 | 310 | # We include a module here so you can define your own help 311 | # command and call #super to utilize this one. 312 | 313 | module Help 314 | 315 | def help 316 | opts = help_options 317 | s = "" 318 | s << "#{File.basename($0)}\n\n" 319 | unless opts.empty? 320 | s << "OPTIONS\n" 321 | s << help_options 322 | s << "\n" 323 | end 324 | s << "COMMANDS\n" 325 | s << help_commands 326 | puts s 327 | end 328 | 329 | private 330 | 331 | def help_commands 332 | help = self.class.help 333 | bufs = help.keys.collect{ |a| a.to_s.size }.max + 3 334 | lines = [] 335 | help.each { |cmd, str| 336 | cmd = cmd.to_s 337 | if cmd !~ /^_/ 338 | lines << " " + cmd + (" " * (bufs - cmd.size)) + str 339 | end 340 | } 341 | lines.join("\n") 342 | end 343 | 344 | def help_options 345 | help = self.class.help 346 | bufs = help.keys.collect{ |a| a.to_s.size }.max + 3 347 | lines = [] 348 | help.each { |cmd, str| 349 | cmd = cmd.to_s 350 | if cmd =~ /^_/ 351 | lines << " " + cmd.gsub(/_/,'-') + (" " * (bufs - cmd.size)) + str 352 | end 353 | } 354 | lines.join("\n") 355 | end 356 | 357 | module ClassMethods 358 | 359 | def help( str=nil ) 360 | return (@help ||= {}) unless str 361 | @current_help = str 362 | end 363 | 364 | def method_added( meth ) 365 | if @current_help 366 | @help ||= {} 367 | @help[meth] = @current_help 368 | @current_help = nil 369 | end 370 | end 371 | 372 | end 373 | 374 | end 375 | 376 | include Help 377 | extend Help::ClassMethods 378 | 379 | =end 380 | 381 | =begin 382 | 383 | # Provides a very basic usage help string. 384 | # 385 | # TODO Add support for __options. 386 | def usage 387 | str = [] 388 | public_methods(false).sort.each do |meth| 389 | meth = meth.to_s 390 | case meth 391 | when /^_/ 392 | opt = meth.sub(/^_+/, '') 393 | meth = method(meth) 394 | if meth.arity == 0 395 | str << (opt.size > 1 ? "[--#{opt}]" : "[-#{opt}]") 396 | elsif meth.arity == 1 397 | str << (opt.size > 1 ? "[--#{opt} value]" : "[-#{opt} value]") 398 | elsif meth.arity > 0 399 | v = []; meth.arity.times{ |i| v << 'value' + (i + 1).to_s } 400 | str << (opt.size > 1 ? "[--#{opt} #{v.join(' ')}]" : "[-#{opt} #{v.join(' ')}]") 401 | else 402 | str << (opt.size > 1 ? "[--#{opt} *values]" : "[-#{opt} *values]") 403 | end 404 | when /=$/ 405 | opt = meth.chomp('=') 406 | str << (opt.size > 1 ? "[--#{opt} value]" : "[-#{opt} value]") 407 | when /!$/ 408 | opt = meth.chomp('!') 409 | str << (opt.size > 1 ? "[--#{opt}]" : "[-#{opt}]") 410 | end 411 | end 412 | return str.join(" ") 413 | end 414 | 415 | # 416 | 417 | def self.usage_class(usage) 418 | c = Class.new(self) 419 | argv = Shellwords.shellwords(usage) 420 | argv.each_with_index do |name, i| 421 | if name =~ /^-/ 422 | if argv[i+1] =~ /^[(.*?)]/ 423 | c.class_eval %{ 424 | attr_accessor :#{name} 425 | } 426 | else 427 | c.class_eval %{ 428 | attr_reader :#{name} 429 | def #{name}! ; @#{name} = true ; end 430 | } 431 | end 432 | end 433 | end 434 | return c 435 | end 436 | 437 | end 438 | 439 | =end 440 | -------------------------------------------------------------------------------- /work/deprecated/command3/command.f3.rb: -------------------------------------------------------------------------------- 1 | # TITLE: 2 | # 3 | # Command 4 | # 5 | # COPYRIGHT: 6 | # 7 | # Copyright (c) 2005 Thomas Sawyer 8 | # 9 | # LICENSE: 10 | # 11 | # Ruby License 12 | # 13 | # This module is free software. You may use, modify, and/or 14 | # redistribute this software under the same terms as Ruby. 15 | # 16 | # This program is distributed in the hope that it will be 17 | # useful, but WITHOUT ANY WARRANTY; without even the implied 18 | # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 19 | # PURPOSE. 20 | # 21 | # AUTHORS: 22 | # 23 | # - Trans 24 | # 25 | # TODO: 26 | # 27 | # - Add global options to master command, or "all are master options" flag? 28 | # - Add usage/help/documentation/man features. 29 | 30 | #require 'facets/annotations' # for help ? 31 | #require 'facets/module/attr' 32 | #require 'facets/kernel/constant' 33 | #require 'shellwords' 34 | require 'facets/arguments' 35 | 36 | module Console 37 | 38 | # For CommandOptions, but defined external to it, so 39 | # that it is easy to access from user defined commands. 40 | # (This lookup issue should be fixed in Ruby 1.9+, and then 41 | # the class can be moved back into Command namespace.) 42 | 43 | class NoOptionError < NoMethodError 44 | def initialize(name, *arg) 45 | super("unknown option -- #{name}", name, *args) 46 | end 47 | end 48 | 49 | class NoCommandError < NoMethodError 50 | def initialize(name, *arg) 51 | super("unknown subcommand -- #{name}", name, *args) 52 | end 53 | end 54 | 55 | # Here is an example of usage: 56 | # 57 | # # General Options 58 | # 59 | # module GeneralOptions 60 | # attr_accessor :dryrun ; alias_accessor :n, :noharm, :dryrun 61 | # attr_accessor :quiet ; alias_accessor :q, :quiet 62 | # attr_accessor :force 63 | # attr_accessor :trace 64 | # end 65 | # 66 | # # Build Subcommand 67 | # 68 | # class BuildCommand < Console::Command 69 | # include GeneralOptions 70 | # 71 | # # metadata files 72 | # attr_accessor :file ; alias_accessor :f, :file 73 | # attr_accessor :manifest ; alias_accessor :m, :manifest 74 | # 75 | # def call 76 | # # do stuf here 77 | # end 78 | # end 79 | # 80 | # # Box Master Command 81 | # 82 | # class BoxCommand < Console::MasterCommand 83 | # subcommand :build, BuildCommand 84 | # end 85 | # 86 | # BoxCommand.start 87 | 88 | class MasterCommand 89 | 90 | # 91 | 92 | module UniversalOptions 93 | end 94 | 95 | # 96 | 97 | def self.option_arity(arity_hash=nil) 98 | if arity_hash 99 | (@option_arity ||= {}).merge!(arity_hash) 100 | end 101 | @option_arity 102 | end 103 | 104 | # 105 | 106 | def self.start(line=nil) 107 | cargs = Console::Arguments.new(line || ARGV, option_arity) 108 | pre = cargs.preoptions 109 | cmd, argv = *cargs.subcommand 110 | args, opts = *argv 111 | 112 | if is_a?(UniversalOptions) 113 | new(pre, opts).call(cmd, args, opts) 114 | else 115 | new(pre).call(cmd, args, opts) 116 | end 117 | end 118 | 119 | # 120 | 121 | def self.subcommand(name, command_class, options=nil) 122 | options ||= {} 123 | if options[:no_merge] 124 | file, line = __FILE__, __LINE__+1 125 | code = %{ 126 | def #{name}(args, opts) 127 | #{command_class}.new(args, opts).call 128 | end 129 | } 130 | else 131 | file, line = __FILE__, __LINE__+1 132 | code = %{ 133 | def #{name}(args, opts) 134 | opts.merge(master_options) 135 | #{command_class}.new(args, opts).call 136 | end 137 | } 138 | end 139 | class_eval(code, file, line) 140 | end 141 | 142 | private 143 | 144 | attr :master_options 145 | 146 | # 147 | 148 | def initialize(*options) 149 | @master_options = {} 150 | initialize_options(*options) 151 | end 152 | 153 | # 154 | 155 | def initialize_options(*options) 156 | options = options.inject{ |h,o| h.merge(o) } 157 | begin 158 | opt, val = nil, nil 159 | options.each do |opt, val| 160 | opt = opt.gsub('-','_') 161 | send("#{opt}=", val) 162 | end 163 | rescue NoMethodError 164 | option_missing(opt, val) 165 | end 166 | @master_options.update(options) 167 | end 168 | 169 | public 170 | 171 | # 172 | 173 | def call(cmd, args, opts) 174 | cmd = :default if cmd.nil? 175 | begin 176 | subcommand = method(cmd) 177 | parameters = [args, opts] 178 | rescue NameError 179 | subcommand = method(:subcommand_missing) 180 | parameters = [cmd, args, opts] 181 | end 182 | if subcommand.arity < 0 183 | subcommand.call(*parameters[0..subcommand.arity]) 184 | else 185 | subcommand.call(*parameters[0,subcommand.arity]) 186 | end 187 | end 188 | 189 | # 190 | 191 | def help; end 192 | 193 | def default ; help ; end 194 | 195 | private 196 | 197 | # 198 | 199 | def subcommand_missing(cmd, args, opt) 200 | help 201 | #raise NoCommandError.new(cmd, args << opt) 202 | end 203 | 204 | # 205 | 206 | def option_missing(opt, arg=nil) 207 | raise NoOptionError.new(opt) 208 | end 209 | 210 | end 211 | 212 | # = Command base class 213 | # 214 | # See MasterCommand for example. 215 | 216 | class Command 217 | 218 | def self.option_arity(arity_hash=nil) 219 | if arity_hash 220 | (@option_arity ||= {}).merge!(arity_hash) 221 | end 222 | @option_arity 223 | end 224 | 225 | def self.start(line=nil) 226 | cargs = Console::Argument.new(line || ARGV, option_arity) 227 | pre = cargs.preoptions 228 | args, opts = *cargs.parameters 229 | new(args, opts).call 230 | end 231 | 232 | attr :arguments 233 | attr :options 234 | 235 | # 236 | 237 | def call 238 | puts "Not implemented yet." 239 | end 240 | 241 | private 242 | 243 | # 244 | 245 | def initialize(arguments, options=nil) 246 | initialize_arguments(*arguments) 247 | initialize_options(options) 248 | end 249 | 250 | # 251 | 252 | def initialize_arguments(*arguments) 253 | @arguments = arguments 254 | end 255 | 256 | # 257 | 258 | def initialize_options(options) 259 | options = options || {} 260 | begin 261 | opt, val = nil, nil 262 | options.each do |opt, val| 263 | send("#{opt}=", val) 264 | end 265 | rescue NoMethodError 266 | option_missing(opt, val) 267 | end 268 | @options = options 269 | end 270 | 271 | # 272 | 273 | def option_missing(opt, arg=nil) 274 | raise NoOptionError.new(opt) 275 | end 276 | 277 | end 278 | 279 | end 280 | 281 | 282 | class Array 283 | 284 | # Not empty? 285 | 286 | def not_empty? 287 | !empty? 288 | end 289 | 290 | # Convert an array into command line parameters. 291 | # The array is accepted in the format of Ruby 292 | # method arguments --ie. [arg1, arg2, ..., hash] 293 | 294 | def to_console 295 | flags = (Hash===last ? pop : {}) 296 | flags = flags.to_console 297 | flags + ' ' + join(" ") 298 | end 299 | 300 | end 301 | 302 | 303 | class Hash 304 | 305 | # Convert an array into command line parameters. 306 | # The array is accepted in the format of Ruby 307 | # method arguments --ie. [arg1, arg2, ..., hash] 308 | 309 | def to_console 310 | flags = collect do |f,v| 311 | m = f.to_s.size == 1 ? '-' : '--' 312 | case v 313 | when Array 314 | v.collect{ |e| "#{m}#{f}='#{e}'" }.join(' ') 315 | when true 316 | "#{m}#{f}" 317 | when false, nil 318 | '' 319 | else 320 | "#{m}#{f}='#{v}'" 321 | end 322 | end 323 | flags.join(" ") 324 | end 325 | 326 | # Turn a hash into arguments. 327 | # 328 | # h = { :list => [1,2], :base => "HI" } 329 | # h.argumentize #=> [ [], { :list => [1,2], :base => "HI" } ] 330 | # h.argumentize(:list) #=> [ [1,2], { :base => "HI" } ] 331 | # 332 | def argumentize(args_field=nil) 333 | config = dup 334 | if args_field 335 | args = [config.delete(args_field)].flatten.compact 336 | else 337 | args = [] 338 | end 339 | args << config 340 | return args 341 | end 342 | 343 | end 344 | 345 | 346 | # SCRAP CODE FOR REFERENCE TO POSSIBLE ADD FUTURE FEATURES 347 | 348 | =begin 349 | 350 | # We include a module here so you can define your own help 351 | # command and call #super to utilize this one. 352 | 353 | module Help 354 | 355 | def help 356 | opts = help_options 357 | s = "" 358 | s << "#{File.basename($0)}\n\n" 359 | unless opts.empty? 360 | s << "OPTIONS\n" 361 | s << help_options 362 | s << "\n" 363 | end 364 | s << "COMMANDS\n" 365 | s << help_commands 366 | puts s 367 | end 368 | 369 | private 370 | 371 | def help_commands 372 | help = self.class.help 373 | bufs = help.keys.collect{ |a| a.to_s.size }.max + 3 374 | lines = [] 375 | help.each { |cmd, str| 376 | cmd = cmd.to_s 377 | if cmd !~ /^_/ 378 | lines << " " + cmd + (" " * (bufs - cmd.size)) + str 379 | end 380 | } 381 | lines.join("\n") 382 | end 383 | 384 | def help_options 385 | help = self.class.help 386 | bufs = help.keys.collect{ |a| a.to_s.size }.max + 3 387 | lines = [] 388 | help.each { |cmd, str| 389 | cmd = cmd.to_s 390 | if cmd =~ /^_/ 391 | lines << " " + cmd.gsub(/_/,'-') + (" " * (bufs - cmd.size)) + str 392 | end 393 | } 394 | lines.join("\n") 395 | end 396 | 397 | module ClassMethods 398 | 399 | def help( str=nil ) 400 | return (@help ||= {}) unless str 401 | @current_help = str 402 | end 403 | 404 | def method_added( meth ) 405 | if @current_help 406 | @help ||= {} 407 | @help[meth] = @current_help 408 | @current_help = nil 409 | end 410 | end 411 | 412 | end 413 | 414 | end 415 | 416 | include Help 417 | extend Help::ClassMethods 418 | 419 | =end 420 | 421 | =begin 422 | 423 | # Provides a very basic usage help string. 424 | # 425 | # TODO Add support for __options. 426 | def usage 427 | str = [] 428 | public_methods(false).sort.each do |meth| 429 | meth = meth.to_s 430 | case meth 431 | when /^_/ 432 | opt = meth.sub(/^_+/, '') 433 | meth = method(meth) 434 | if meth.arity == 0 435 | str << (opt.size > 1 ? "[--#{opt}]" : "[-#{opt}]") 436 | elsif meth.arity == 1 437 | str << (opt.size > 1 ? "[--#{opt} value]" : "[-#{opt} value]") 438 | elsif meth.arity > 0 439 | v = []; meth.arity.times{ |i| v << 'value' + (i + 1).to_s } 440 | str << (opt.size > 1 ? "[--#{opt} #{v.join(' ')}]" : "[-#{opt} #{v.join(' ')}]") 441 | else 442 | str << (opt.size > 1 ? "[--#{opt} *values]" : "[-#{opt} *values]") 443 | end 444 | when /=$/ 445 | opt = meth.chomp('=') 446 | str << (opt.size > 1 ? "[--#{opt} value]" : "[-#{opt} value]") 447 | when /!$/ 448 | opt = meth.chomp('!') 449 | str << (opt.size > 1 ? "[--#{opt}]" : "[-#{opt}]") 450 | end 451 | end 452 | return str.join(" ") 453 | end 454 | 455 | # 456 | 457 | def self.usage_class(usage) 458 | c = Class.new(self) 459 | argv = Shellwords.shellwords(usage) 460 | argv.each_with_index do |name, i| 461 | if name =~ /^-/ 462 | if argv[i+1] =~ /^[(.*?)]/ 463 | c.class_eval %{ 464 | attr_accessor :#{name} 465 | } 466 | else 467 | c.class_eval %{ 468 | attr_reader :#{name} 469 | def #{name}! ; @#{name} = true ; end 470 | } 471 | end 472 | end 473 | end 474 | return c 475 | end 476 | 477 | end 478 | 479 | =end 480 | -------------------------------------------------------------------------------- /work/deprecated/command2/command.f2.rb: -------------------------------------------------------------------------------- 1 | # TITLE: 2 | # 3 | # Command 4 | # 5 | # COPYRIGHT: 6 | # 7 | # Copyright (c) 2005 Thomas Sawyer 8 | # 9 | # LICENSE: 10 | # 11 | # Ruby License 12 | # 13 | # This module is free software. You may use, modify, and/or 14 | # redistribute this software under the same terms as Ruby. 15 | # 16 | # This program is distributed in the hope that it will be 17 | # useful, but WITHOUT ANY WARRANTY; without even the implied 18 | # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 19 | # PURPOSE. 20 | # 21 | # AUTHORS: 22 | # 23 | # - 7rans 24 | # - Tyler Rick 25 | # 26 | # TODOs: 27 | # 28 | # - Add help/documentation features. 29 | # - Problem with exit -1 when testing. See IMPORTANT!!! remark below. 30 | # 31 | # LOG: 32 | # 33 | # - 2007.10.31 TRANS 34 | # Re-added support for __option notation. 35 | 36 | require 'facets/module/attr' 37 | require 'facets/kernel/constant' 38 | require 'facets/arguments' 39 | require 'shellwords' 40 | #require 'facets/annotations' # for help ? 41 | 42 | module Console 43 | 44 | # For CommandOptions, but defined external to it, so 45 | # that it is easy to access from user defined commands. 46 | # (This lookup issue should be fixed in Ruby 1.9+, and then 47 | # the class can be moved back into Command namespace.) 48 | 49 | class NoOptionError < NoMethodError 50 | def initialize(name, *arg) 51 | super("unknown option -- #{name}", name, *args) 52 | end 53 | end 54 | 55 | class NoCommandError < NoMethodError 56 | def initialize(name, *arg) 57 | super("unknown subcommand -- #{name}", name, *args) 58 | end 59 | end 60 | 61 | # Here is an example of usage: 62 | # 63 | # # General Options 64 | # 65 | # module GeneralOptions 66 | # attr_accessor :dryrun ; alias_accessor :n, :noharm, :dryrun 67 | # attr_accessor :quiet ; alias_accessor :q, :quiet 68 | # attr_accessor :force 69 | # attr_accessor :trace 70 | # end 71 | # 72 | # # Build Subcommand 73 | # 74 | # class BuildCommand < Console::Command 75 | # include GeneralOptions 76 | # 77 | # # metadata files 78 | # attr_accessor :file ; alias_accessor :f, :file 79 | # attr_accessor :manifest ; alias_accessor :m, :manifest 80 | # 81 | # def call 82 | # # do stuf here 83 | # end 84 | # end 85 | # 86 | # # Box Master Command 87 | # 88 | # class BoxCommand < Console::MasterCommand 89 | # subcommand :build, BuildCommand 90 | # subcommand :install, InstallCommand 91 | # subcommand :uninstall, UninstallCommand 92 | # end 93 | 94 | class MasterCommand 95 | 96 | # 97 | 98 | def self.start(line=nil) 99 | cargs = Console::Arguments.new(line || ARGV) 100 | pre = cargs.preoptions 101 | cmd, argv = *cargs.subcommand 102 | args, opts = *argv 103 | new(pre).send(cmd, *(args << opts)) 104 | end 105 | 106 | # 107 | 108 | def self.subcommand(name, command_class, options=nil) 109 | options ||= {} 110 | if options[:no_merge] 111 | file, line = __FILE__, __LINE__+1 112 | code = %{ 113 | def #{name}(*args) 114 | #{command_class}.new(*args).call 115 | end 116 | } 117 | else 118 | file, line = __FILE__, __LINE__+1 119 | code = %{ 120 | def #{name}(*args) 121 | (Hash===args.last ? args.last.merge(master_options) : args << master_options) 122 | #{command_class}.new(*args).call 123 | end 124 | } 125 | end 126 | class_eval(code, file, line) 127 | end 128 | 129 | private 130 | 131 | attr :master_options 132 | 133 | # 134 | 135 | def initialize(master_options=nil) 136 | @master_options = master_options || {} 137 | end 138 | 139 | public 140 | 141 | # 142 | 143 | def send(cmd, *args) 144 | cmd = :default if cmd.nil? 145 | begin 146 | subcommand = method(cmd) 147 | parameters = args 148 | rescue NameError 149 | subcommand = method(:subcommand_missing) 150 | parameters = [cmd, *args] 151 | end 152 | if subcommand.arity < 0 153 | subcommand.call(*parameters[0..subcommand.arity]) 154 | else 155 | subcommand.call(*parameters[0,subcommand.arity]) 156 | end 157 | end 158 | 159 | # 160 | 161 | def help; end 162 | 163 | def default ; help ; end 164 | 165 | # 166 | 167 | def subcommand_missing(cmd, *args) 168 | help 169 | #raise NoCommandError.new(cmd, *args) 170 | end 171 | 172 | end 173 | 174 | 175 | # = Command base class 176 | # 177 | # See MasterCommand for example. 178 | 179 | class Command 180 | 181 | def self.start(line=nil) 182 | cargs = Console::Argument.new(line || ARGV) 183 | pre = cargs.preoptions 184 | args, opts = *cargs.parameters 185 | new(args, opts).call 186 | end 187 | 188 | attr :arguments 189 | attr :options 190 | 191 | # 192 | 193 | def call 194 | puts "Not implemented yet." 195 | end 196 | 197 | private 198 | 199 | # 200 | 201 | def initialize(*arguments) 202 | options = (Hash===arguments.last ? arguments.pop : nil) 203 | initialize_arguments(*arguments) 204 | initialize_options(options) 205 | end 206 | 207 | # 208 | 209 | def initialize_arguments(*arguments) 210 | @arguments = arguments 211 | end 212 | 213 | # 214 | 215 | def initialize_options(options) 216 | options = options || {} 217 | begin 218 | opt, val = nil, nil 219 | options.each do |opt, val| 220 | send("#{opt}=", val) 221 | end 222 | rescue NoMethodError 223 | option_missing(opt, val) 224 | end 225 | @options = options 226 | end 227 | 228 | # 229 | 230 | def option_missing(opt, arg=nil) 231 | raise NoOptionError.new(opt) 232 | end 233 | 234 | end 235 | 236 | end 237 | 238 | 239 | =begin 240 | 241 | 242 | class Command 243 | 244 | # Command Syntax DSL 245 | 246 | class << self 247 | 248 | # Starts the command execution. 249 | def execute(*args) 250 | cmd = new() 251 | #cmd.instance_variable_set("@global_options",global_options) 252 | cmd.execute(*args) 253 | end 254 | alias_method :start, :execute 255 | 256 | # Change the option mode. 257 | def global_option(*names) 258 | names.each{ |name| global_options << name.to_sym } 259 | end 260 | alias_method :global_options, :global_option 261 | 262 | # Define a set of options. This can be a Command::Options subclass, 263 | # or a block which will be used to create an Command::Options subclass. 264 | 265 | def options(name, klass=nil, &block) 266 | raise ArgumentError if klass && block 267 | if block 268 | command_options[name.to_sym] = Class.new(Options, &block) 269 | else 270 | command_options[name.to_sym] = klass 271 | end 272 | end 273 | alias_method :opts, :options 274 | alias_method :opt, :options 275 | 276 | # 277 | 278 | def command_options 279 | @_command_options ||= {} 280 | end 281 | 282 | # 283 | 284 | def global_options 285 | @_global_options ||= [] 286 | end 287 | end 288 | 289 | #def initialize #(global_options=nil) 290 | # #@global_options = global_options || [] 291 | #end 292 | 293 | # 294 | 295 | def execute(line=ARGV) 296 | argv = line 297 | 298 | g_opts = Command::Options.new(self) 299 | g_keys = self.class.global_options 300 | 301 | # Deal with global options. 302 | if g_keys && ! g_keys.empty? 303 | argv = g_opts.parse(argv, :only => g_keys) 304 | end 305 | 306 | # Sole main command or has subcommands? 307 | if respond_to?(:main) 308 | argv = g_opts.parse(argv, :pass => true) 309 | cmd = :main 310 | else 311 | argv = g_opts.parse(argv, :stop => true) 312 | cmd = argv.find{ |s| s !~ /^-/ } 313 | argv.delete_at(argv.index(cmd)) if cmd 314 | cmd = :default unless cmd 315 | cmd = cmd.to_sym 316 | end 317 | 318 | keys = self.class.command_options 319 | 320 | if keys.key?(cmd) 321 | opts = keys[cmd].new 322 | argv = opts.parse(argv) 323 | end 324 | 325 | argv = g_opts.parse_missing(argv) 326 | 327 | call(cmd, argv, opts) 328 | end 329 | 330 | # 331 | 332 | def call(subcmd, argv, opts) 333 | @options = opts # should we use this it fill in instance vars? 334 | 335 | # This is a little tricky. The method has to be defined by a subclass. 336 | if self.respond_to?(subcmd) and not Console::Command.public_instance_methods.include?(subcmd.to_s) 337 | puts "# call: #{subcmd}(*#{argv.inspect})" if $debug 338 | __send__(subcmd, *argv) 339 | else 340 | begin 341 | puts "# call: method_missing(#{subcmd.inspect}, #{argv.inspect})" if $debug 342 | method_missing(subcmd.to_sym, *argv) 343 | rescue NoMethodError => e 344 | #if self.private_methods.include?( "no_command_error" ) 345 | # no_command_error( *args ) 346 | #else 347 | $stderr << "Unrecognized subcommand -- #{subcmd}\n" 348 | exit -1 349 | #end 350 | end 351 | end 352 | end 353 | 354 | # 355 | 356 | def call(argv, opts) 357 | begin 358 | opts.each do |opt, val| 359 | send("#{opt}=", val) 360 | end 361 | rescue NoMethodError => e 362 | option_missing(opt, val) 363 | end 364 | end 365 | 366 | #def global_options 367 | # self.class.global_options 368 | #end 369 | 370 | def option_missing(opt, arg=nil) 371 | raise InvalidOptionError.new(opt) 372 | end 373 | 374 | end 375 | 376 | 377 | # We include a module here so you can define your own help 378 | # command and call #super to utilize this one. 379 | 380 | module Help 381 | 382 | def help 383 | opts = help_options 384 | s = "" 385 | s << "#{File.basename($0)}\n\n" 386 | unless opts.empty? 387 | s << "OPTIONS\n" 388 | s << help_options 389 | s << "\n" 390 | end 391 | s << "COMMANDS\n" 392 | s << help_commands 393 | puts s 394 | end 395 | 396 | private 397 | 398 | def help_commands 399 | help = self.class.help 400 | bufs = help.keys.collect{ |a| a.to_s.size }.max + 3 401 | lines = [] 402 | help.each { |cmd, str| 403 | cmd = cmd.to_s 404 | if cmd !~ /^_/ 405 | lines << " " + cmd + (" " * (bufs - cmd.size)) + str 406 | end 407 | } 408 | lines.join("\n") 409 | end 410 | 411 | def help_options 412 | help = self.class.help 413 | bufs = help.keys.collect{ |a| a.to_s.size }.max + 3 414 | lines = [] 415 | help.each { |cmd, str| 416 | cmd = cmd.to_s 417 | if cmd =~ /^_/ 418 | lines << " " + cmd.gsub(/_/,'-') + (" " * (bufs - cmd.size)) + str 419 | end 420 | } 421 | lines.join("\n") 422 | end 423 | 424 | module ClassMethods 425 | 426 | def help( str=nil ) 427 | return (@help ||= {}) unless str 428 | @current_help = str 429 | end 430 | 431 | def method_added( meth ) 432 | if @current_help 433 | @help ||= {} 434 | @help[meth] = @current_help 435 | @current_help = nil 436 | end 437 | end 438 | 439 | end 440 | 441 | end 442 | 443 | include Help 444 | extend Help::ClassMethods 445 | 446 | 447 | 448 | # = Command::Options 449 | # 450 | # CommandOptions provides the basis for Command to Object Mapping (COM). 451 | # It is an commandline options parser that uses method definitions 452 | # as means of interprting command arguments. 453 | # 454 | # == Synopsis 455 | # 456 | # Let's make an executable called 'mycmd'. 457 | # 458 | # #!/usr/bin/env ruby 459 | # 460 | # require 'facets/command_options' 461 | # 462 | # class MyOptions < CommandOptions 463 | # attr_accessor :file 464 | # 465 | # def v! 466 | # @verbose = true 467 | # end 468 | # end 469 | # 470 | # opts = MyOptions.parse("-v --file hello.rb") 471 | # 472 | # opts.verbose #=> true 473 | # opts.file #=> "hello.rb" 474 | # 475 | #-- 476 | # == Global Options 477 | # 478 | # You can define global options which are options that will be 479 | # processed no matter where they occur in the command line. In the above 480 | # examples only the options occuring before the subcommand are processed 481 | # globally. Anything occuring after the subcommand belonds strictly to 482 | # the subcommand. For instance, if we had added the following to the above 483 | # example: 484 | # 485 | # global_option :_v 486 | # 487 | # Then -v could appear anywhere in the command line, even on the end, 488 | # and still work as expected. 489 | # 490 | # % mycmd jump -h 3 -v 491 | #++ 492 | # 493 | # == Missing Options 494 | # 495 | # You can use #option_missing to catch any options that are not explicility 496 | # defined. 497 | # 498 | # The method signature should look like: 499 | # 500 | # option_missing(option_name, args) 501 | # 502 | # Example: 503 | # def option_missing(option_name, args) 504 | # p args if $debug 505 | # case option_name 506 | # when 'p' 507 | # @a = args[0].to_i 508 | # @b = args[1].to_i 509 | # 2 510 | # else 511 | # raise InvalidOptionError(option_name, args) 512 | # end 513 | # end 514 | # 515 | # Its return value should be the effective "arity" of that options -- that is, 516 | # how many arguments it consumed ("-p a b", for example, would consume 2 args: 517 | # "a" and "b"). An arity of 1 is assumed if nil or false is returned. 518 | 519 | class Command::Options 520 | 521 | def self.parse(*line_and_options) 522 | o = new 523 | o.parse(*line_and_options) 524 | o 525 | end 526 | 527 | def initialize(delegate=nil) 528 | @__self__ = delegate || self 529 | end 530 | 531 | # Parse line for options in the context self. 532 | # 533 | # Options: 534 | # 535 | # :pass => true || false 536 | # 537 | # Setting this to true prevents the parse_missing routine from running. 538 | # 539 | # :only => [ global options, ... ] 540 | # 541 | # When processing global options, we only want to parse selected options. 542 | # This also set +pass+ to true. 543 | # 544 | # :stop => true || false 545 | # 546 | # If we are parsing options for the *main* command and we are allowing 547 | # subcommands, then we want to stop as soon as we get to the first non-option, 548 | # because that non-option will be the name of our subcommand and all options that 549 | # follow should be parsed later when we handle the subcommand. 550 | # This also set +pass+ to true. 551 | 552 | def parse(*line_and_options) 553 | __self__ = @__self__ 554 | 555 | if Hash === line_and_options.last 556 | options = line_and_options.pop 557 | line = line_and_options.first 558 | else 559 | options = {} 560 | line = line_and_options.first 561 | end 562 | 563 | case line 564 | when String 565 | argv = Shellwords.shellwords(line) 566 | when Array 567 | argv = line.dup 568 | else 569 | argv = ARGV.dup 570 | end 571 | 572 | only = options[:only] # only parse these options 573 | stop = options[:stop] # stop at first non-option 574 | pass = options[:pass] || only || stop # don't run options_missing 575 | 576 | if $debug 577 | puts(only ? "\nGlobal parsing..." : "\nParsing...") 578 | end 579 | 580 | puts "# line: #{argv.inspect}" if $debug 581 | 582 | # Split single letter option groupings into separate options. 583 | # ie. -xyz => -x -y -z 584 | argv = argv.collect { |arg| 585 | if md = /^-(\w{2,})/.match( arg ) 586 | md[1].split(//).collect { |c| "-#{c}" } 587 | else 588 | arg 589 | end 590 | }.flatten 591 | 592 | index = 0 593 | 594 | until index >= argv.size 595 | arg = argv.at(index) 596 | break if arg == '--' # POSIX compliance 597 | if arg[0,1] == '-' 598 | puts "# option: #{arg}" if $debug 599 | cnt = (arg[0,2] == '--' ? 2 : 1) 600 | #opt = Option.new(arg) 601 | #name = opt.methodize 602 | name = arg.sub(/^-{1,2}/,'') 603 | skip = only && only.include?(name) 604 | unam = ('__'*cnt)+name 605 | if __self__.respond_to?(unam) 606 | puts "# method: #{uname}" if $debug 607 | meth = method(unam) 608 | arity = meth.arity 609 | if arity < 0 610 | meth.call(*argv.slice(index+1..-1)) unless skip 611 | arity[index..-1] = nil # Get rid of the *name* and values 612 | elsif arity == 0 613 | meth.call unless skip 614 | argv.delete_at(index) # Get rid of the *name* of the option 615 | else 616 | meth.call(*argv.slice(index+1, arity)) unless skip 617 | #argv.delete_at(index) # Get rid of the *name* of the option 618 | #arity.times{ argv.delete_at(index) } # Get rid of the *value* of the option 619 | arity[index,arity] = nil 620 | end 621 | elsif __self__.respond_to?(name+'=') 622 | puts "# method: #{name}=" if $debug 623 | __self__.send(name+'=', *argv.slice(index+1, 1)) unless skip 624 | argv.delete_at(index) # Get rid of the *name* of the option 625 | argv.delete_at(index) # Get rid of the *value* of the option 626 | elsif __self__.respond_to?(name+'!') 627 | puts "# method: #{name}!" if $debug 628 | __self__.send(name+'!') unless skip 629 | argv.delete_at(index) # Get rid of the *name* of the option 630 | else 631 | index += 1 632 | end 633 | else 634 | index += 1 635 | break if stop 636 | end 637 | end 638 | # parse missing ? 639 | argv = parse_missing(argv) unless pass 640 | # return the remaining argv 641 | puts "# return: #{argv.inspect}" if $debug 642 | return argv 643 | end 644 | 645 | # 646 | 647 | def parse_missing(argv) 648 | argv.each_with_index do |a,i| 649 | if a =~ /^-/ 650 | #raise InvalidOptionError.new(a) unless @__self__.respond_to?(:option_missing) 651 | kept = @__self__.option_missing(a, *argv[i+1,1]) 652 | argv.delete_at(i) if kept # delete if value kept 653 | argv.delete_at(i) # delete option 654 | end 655 | end 656 | return argv 657 | end 658 | 659 | # 660 | 661 | def option_missing(opt, arg=nil) 662 | raise InvalidOptionError.new(opt) 663 | # #$stderr << "Unknown option '#{arg}'.\n" 664 | # #exit -1 665 | end 666 | 667 | # 668 | 669 | def to_h 670 | opts = @__self__.public_methods(true).select{ |m| m =~ /^[A-Za-z0-9]+[=!]$/ || m =~ /^[_][A-Za-z0-9]+$/ } 671 | #opts.reject!{ |k| k =~ /_$/ } 672 | opts.collect!{ |m| m.chomp('=').chomp('!') } 673 | opts.inject({}) do |h, m| 674 | k = m.sub(/^_+/, '') 675 | v = @__self__.send(m) 676 | h[k] = v if v 677 | h 678 | end 679 | 680 | #@__self__.instance_variables.inject({}) do |h, v| 681 | # next h if v == "@__self__" 682 | # h[v[1..-1]] = @__self__.instance_variable_get(v); h 683 | #end 684 | end 685 | 686 | # Provides a very basic usage help string. 687 | # 688 | # TODO Add support for __options. 689 | def usage 690 | str = [] 691 | public_methods(false).sort.each do |meth| 692 | meth = meth.to_s 693 | case meth 694 | when /^_/ 695 | opt = meth.sub(/^_+/, '') 696 | meth = method(meth) 697 | if meth.arity == 0 698 | str << (opt.size > 1 ? "[--#{opt}]" : "[-#{opt}]") 699 | elsif meth.arity == 1 700 | str << (opt.size > 1 ? "[--#{opt} value]" : "[-#{opt} value]") 701 | elsif meth.arity > 0 702 | v = []; meth.arity.times{ |i| v << 'value' + (i + 1).to_s } 703 | str << (opt.size > 1 ? "[--#{opt} #{v.join(' ')}]" : "[-#{opt} #{v.join(' ')}]") 704 | else 705 | str << (opt.size > 1 ? "[--#{opt} *values]" : "[-#{opt} *values]") 706 | end 707 | when /=$/ 708 | opt = meth.chomp('=') 709 | str << (opt.size > 1 ? "[--#{opt} value]" : "[-#{opt} value]") 710 | when /!$/ 711 | opt = meth.chomp('!') 712 | str << (opt.size > 1 ? "[--#{opt}]" : "[-#{opt}]") 713 | end 714 | end 715 | return str.join(" ") 716 | end 717 | 718 | # 719 | 720 | def self.usage_class(usage) 721 | c = Class.new(self) 722 | argv = Shellwords.shellwords(usage) 723 | argv.each_with_index do |name, i| 724 | if name =~ /^-/ 725 | if argv[i+1] =~ /^[(.*?)]/ 726 | c.class_eval %{ 727 | attr_accessor :#{name} 728 | } 729 | else 730 | c.class_eval %{ 731 | attr_reader :#{name} 732 | def #{name}! ; @#{name} = true ; end 733 | } 734 | end 735 | end 736 | end 737 | return c 738 | end 739 | 740 | end 741 | 742 | =end 743 | --------------------------------------------------------------------------------