cp 0.0.1.pre1 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,55 @@
1
+ CP
2
+ ==
3
+
4
+ Write complex command line scripts simply.
5
+
6
+ CP provides an API to write command line scripts with nested subcommands
7
+ and switches.
8
+
9
+ Rather than reinventing the wheel, CP calls on the time tested [CmdParse][] gem
10
+ to do the heavy lifting, wrapping it in an updated API and adding a handful of
11
+ new features.
12
+
13
+ Roadmap
14
+ -------
15
+
16
+ CP aims to provide:
17
+
18
+ * Automatic help generation
19
+ * Any number of nested subcommands
20
+ * Global and command-specific switches
21
+
22
+ Credit
23
+ ------
24
+
25
+ CP's API was inspired by the Commander gem.
26
+
27
+ I decided I didn't personally like parts
28
+ of the Commander API. I forked Commander and began adding features, but found
29
+ it difficult to extend, and started work on CP instead.
30
+
31
+ [CmdParse]: http://cmdparse.rubyforge.org/
32
+
33
+ License
34
+ -------
35
+
36
+ (The MIT License)
37
+
38
+ Copyright © 2011 Rick Fletcher <fletch@pobox.com>
39
+
40
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
41
+ this software and associated documentation files (the "Software"), to deal in
42
+ the Software without restriction, including without limitation the rights to
43
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
44
+ the Software, and to permit persons to whom the Software is furnished to do so,
45
+ subject to the following conditions:
46
+
47
+ The above copyright notice and this permission notice shall be included in all
48
+ copies or substantial portions of the Software.
49
+
50
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
51
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
52
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
53
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
54
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
55
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/cp.gemspec CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |s|
8
8
  s.platform = Gem::Platform::RUBY
9
9
  s.authors = ["Rick Fletcher"]
10
10
  s.email = ["fletch@pobox.com"]
11
- s.homepage = ""
11
+ s.homepage = "https://github.com/baseballdb/cp"
12
12
  s.summary = %q{An alternative API for CmdParse}
13
13
  s.description = %q{CP provides a less verbose API for the CmdParse gem.}
14
14
 
@@ -18,5 +18,4 @@ Gem::Specification.new do |s|
18
18
  s.require_paths = ["lib"]
19
19
 
20
20
  s.add_runtime_dependency(%q<cmdparse>, [">= 2.0.2"])
21
- s.add_runtime_dependency(%q<cmdparse>, [">= 2.0.2"])
22
21
  end
@@ -1,13 +1,18 @@
1
1
  ##
2
2
  # A port of the CmdParse tutorial app, net.rb
3
+ #
4
+ # This app doesn't really do much of anything. It's meant as an example of
5
+ # the difference in API from CmdParse to CP.
6
+ #
3
7
  # original: http://cmdparse.rubyforge.org/tutorial.html
4
8
  #
5
9
 
6
10
  $: << File.join( ".", File.dirname( __FILE__ ), "..", "lib" )
7
11
 
12
+ require "yaml"
8
13
  require "cp"
9
14
 
10
- include CP
15
+ extend CP
11
16
 
12
17
  $ipaddrs = []
13
18
  $verbose = false
@@ -16,18 +21,18 @@ app :name, "net"
16
21
  app :version, "0.1.1"
17
22
 
18
23
  # global options
19
- option( "--verbose", "Be verbose when outputting info" ) { |t| $verbose = true }
24
+ option "--verbose", "Be verbose when outputting info", lambda { |v| $verbose = true }
20
25
 
21
26
  # add the top-level "ipaddr" command
22
27
  command :ipaddr do |c|
23
- # c.allow_partial! # partial commands aren't implemented
28
+ c.default = true
24
29
  c.summary = "Manage IP addresses"
25
30
 
26
31
  # add the "add" subcommand
27
32
  c.command( :add ) do |s|
28
33
  s.summary = "Add an IP address"
29
34
 
30
- s.execute do |*args|
35
+ s.execute do |opts, args|
31
36
  puts "Adding IP addresses: #{args.join(', ')}" if $verbose
32
37
  $ipaddrs += args
33
38
  end
@@ -39,7 +44,7 @@ command :ipaddr do |c|
39
44
 
40
45
  s.option( "-a", "--all", "Delete all IP addresses" ){ $deleteAll = true }
41
46
 
42
- s.execute do |*args|
47
+ s.execute do |opts, args|
43
48
  if $deleteAll
44
49
  $ipaddrs = []
45
50
  else
@@ -51,7 +56,7 @@ command :ipaddr do |c|
51
56
 
52
57
  # add the "list" subcommand
53
58
  c.command :list do |s|
54
- # s.default! # make :list the default subcommand
59
+ s.default = true
55
60
  s.summary = "List all IP addresses"
56
61
 
57
62
  s.execute do |*args|
@@ -65,7 +70,7 @@ command :ipaddr do |c|
65
70
  s.summary = "Show network statistics"
66
71
  s.description = "This command shows very useful network statistics - eye catching!!!"
67
72
 
68
- s.execute do |*args|
73
+ s.execute do |opts, args|
69
74
  puts "Showing network statistics" if $verbose
70
75
  puts
71
76
  puts "Yeah, I will do something now..."
@@ -77,5 +82,3 @@ command :ipaddr do |c|
77
82
  end
78
83
  end
79
84
  end
80
-
81
- run
data/lib/cp.rb CHANGED
@@ -1,22 +1,36 @@
1
1
  require "rubygems"
2
2
  require "cmdparse"
3
+ require "optparse"
3
4
 
4
- require "cp/runners/cmd_parse"
5
- require "cp/commands"
6
- require "cp/options"
5
+ require "cp/has/commands"
6
+ require "cp/has/options"
7
+
8
+ require "cp/app"
7
9
  require "cp/command"
8
- require "cp/version"
10
+ require "cp/errors"
9
11
  require "cp/option"
10
- require "cp/app"
12
+ require "cp/options_struct"
13
+ require "cp/runners/cmd_parse"
14
+ require "cp/version"
11
15
 
12
16
  module CP
13
- def self.included( scope )
14
- scope.extend( self )
17
+ def self.included( klass )
18
+ klass.extend( self )
19
+ klass.class_eval do
20
+ def self.run
21
+ CP::App.run
22
+ end
23
+ end
24
+ end
25
+
26
+ def self.extended( instance )
27
+ is_main = instance.class === Object && instance.inspect === "main"
28
+ at_exit { CP::App.run } if is_main
15
29
  end
16
30
 
17
- [:app, :command, :option, :run].each do |method|
31
+ [:app, :command, :option].each do |method|
18
32
  define_method( method ) { |*args, &block|
19
- App.instance.send( method, *args, &block )
33
+ CP::App.instance.send( method, *args, &block )
20
34
  }
21
35
  end
22
36
  end
@@ -4,12 +4,13 @@ module CP
4
4
  class App
5
5
  include ::Singleton
6
6
 
7
- include CP::Commands
8
- include CP::Options
7
+ include CP::Has::Commands
8
+ include CP::Has::Options
9
9
 
10
10
  attr_accessor :name, :version, :runner
11
11
 
12
12
  def initialize
13
+ self.name = File.basename( $0 )
13
14
  self.runner = CP::Runners::CmdParse
14
15
  end
15
16
 
@@ -21,8 +22,21 @@ module CP
21
22
  end
22
23
  end
23
24
 
24
- def run
25
- self.runner.new( self ).run
25
+ def fatal( msg )
26
+ self.error( msg )
27
+ exit 1
28
+ end
29
+
30
+ def error( msg )
31
+ $stderr.puts "#{self.name}: #{msg}. See `#{self.name} --help`."
32
+ end
33
+
34
+ def run( *args )
35
+ CP::App.instance.runner.new( CP::App.instance ).run( *args )
36
+ end
37
+
38
+ def self.run( *args )
39
+ CP::App.instance.run( *args )
26
40
  end
27
41
  end
28
42
  end
@@ -1,23 +1,54 @@
1
1
  module CP
2
2
  class Command
3
- include CP::Commands
4
- include CP::Options
3
+ include CP::Has::Commands
4
+ include CP::Has::Options
5
5
 
6
- attr_reader :block, :name
7
- attr_accessor :description, :summary
6
+ attr_reader :block, :name, :parent
7
+ attr_accessor :default, :description, :summary
8
8
 
9
- def initialize( name )
9
+ def initialize( name, parent=nil )
10
10
  unless name.respond_to?( :to_sym )
11
- raise ArgumentError.new( "parameter must be a Symbol (or respond to .to_sym)")
11
+ raise ArgumentError.new( "name must be a Symbol (or respond to .to_sym)")
12
12
  end
13
13
 
14
+ @parent = parent
14
15
  @name = name.to_sym
15
16
 
16
17
  yield self if block_given?
17
18
  end
18
19
 
19
20
  def execute( &block )
20
- @block = block
21
+ @block = lambda { |args|
22
+ begin
23
+ opts = gather_options
24
+ rescue CP::MissingOptionError => e
25
+ CP::App.instance.fatal( "'#{e}' is required" )
26
+ end
27
+
28
+ block.call( args, opts )
29
+ }
30
+ end
31
+
32
+ private
33
+
34
+ def gather_options
35
+ options = []
36
+
37
+ current = self
38
+ begin
39
+ options += current.options
40
+ current = current.respond_to?( :parent ) ? current.parent : nil
41
+ end while current && current.respond_to?( :options )
42
+
43
+ option_names = options.map { |o|
44
+ raise CP::MissingOptionError.new( o.switches.last ) if o.required? && o.value.nil?
45
+ o.name
46
+ }.sort.uniq
47
+
48
+ struct = CP::OptionsStruct.new( *option_names )
49
+
50
+ options.reverse.each { |o| struct[o.name] = o.value }
51
+ struct
21
52
  end
22
53
  end
23
54
  end
@@ -0,0 +1,4 @@
1
+ module CP
2
+ class CommandError < StandardError; end
3
+ class MissingOptionError < StandardError; end
4
+ end
@@ -0,0 +1,34 @@
1
+ module CP
2
+ module Has
3
+ module Commands
4
+ def commands
5
+ @commands ||= []
6
+ @commands
7
+ end
8
+
9
+ def command( name )
10
+ cmd = name.to_s.split( " " ).inject( self ) do |parent, name|
11
+ subcommand = parent.commands.find { |c| c.name === name.to_sym }
12
+
13
+ if subcommand.nil?
14
+ subcommand = CP::Command.new( name, parent )
15
+ parent.commands << subcommand
16
+ end
17
+
18
+ subcommand
19
+ end
20
+
21
+ yield cmd if block_given?
22
+
23
+ if cmd.default
24
+ default_cmd = commands.find{ |c| c.default }
25
+ if default_cmd
26
+ raise CP::CommandError.new( "only one default command is allowed: #{default_cmd.name}, #{cmd.name}" )
27
+ end
28
+ end
29
+
30
+ cmd
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,17 @@
1
+ module CP
2
+ module Has
3
+ module Options
4
+ def options
5
+ @options ||= []
6
+ @options
7
+ end
8
+
9
+ def option( *args )
10
+ opt = CP::Option.new( *args )
11
+ yield opt if block_given?
12
+ options << opt
13
+ opt
14
+ end
15
+ end
16
+ end
17
+ end
@@ -2,11 +2,12 @@ require "optparse"
2
2
 
3
3
  module CP
4
4
  class Option
5
- attr_accessor :allowed, :type, :block, :description, :required
6
- attr_reader :arg, :short, :long
5
+ attr_accessor :allowed, :default, :description, :type
6
+ attr_reader :arg, :block, :short, :long
7
7
 
8
8
  # see: OptionsParser#make_switch
9
9
  def initialize( *args )
10
+ @required = false
10
11
  parsed = parse_args( args )
11
12
 
12
13
  yield self if block_given?
@@ -19,6 +20,13 @@ module CP
19
20
  end
20
21
  end
21
22
 
23
+ def block=( block=nil )
24
+ @block = lambda { |value|
25
+ @value = value
26
+ block.call( *( block.arity === 0 ? [] : [value] ) ) if block
27
+ }
28
+ end
29
+
22
30
  def long=( val )
23
31
  set_switch( :long, val )
24
32
  end
@@ -32,6 +40,14 @@ module CP
32
40
  name.gsub( "-", "_" ).to_sym
33
41
  end
34
42
 
43
+ def required=( val )
44
+ @required = !!val
45
+ end
46
+
47
+ def required?
48
+ @required
49
+ end
50
+
35
51
  def short=( val )
36
52
  set_switch( :short, val )
37
53
  end
@@ -51,6 +67,10 @@ module CP
51
67
  args.find_all { |a| !a.nil? }
52
68
  end
53
69
 
70
+ def value
71
+ @value.nil? ? self.default : @value
72
+ end
73
+
54
74
  private
55
75
 
56
76
  def parse_args( args )
@@ -0,0 +1,7 @@
1
+ module CP
2
+ class OptionsStruct
3
+ def self.new( *properties )
4
+ Struct.new( "Options", *properties ).new
5
+ end
6
+ end
7
+ end
@@ -4,21 +4,42 @@ require "cmdparse"
4
4
  module CP
5
5
  module Runners
6
6
  class CmdParse
7
- attr_reader :runner
8
-
9
7
  def initialize( app )
10
8
  @app = app
11
9
  end
12
10
 
13
- def run
14
- @runner = ::CmdParse::CommandParser.new
15
- @runner.program_name = @app.name
16
- @runner.program_version = @app.version.split( "." )
11
+ def run( *args )
12
+ args = args.empty? ? args : [args]
13
+ command_name = ''
14
+
15
+ begin
16
+ runner.parse( *args ) { |l, c| command_name << "#{c} " if l > 0 }
17
+
18
+ rescue ::CmdParse::InvalidCommandError => e
19
+ command_name << e.message.gsub( /^.*: /, '' )
20
+ CP::App.instance.fatal( "'#{command_name}' is not a #{CP::App.instance.name} command." )
17
21
 
18
- add_options( @runner, @app.options )
19
- add_commands( @runner, @app.commands )
22
+ rescue ::CmdParse::InvalidOptionError,
23
+ ::OptionParser::InvalidOption => e
24
+ switch = e.message.gsub( /^.*: /, '' )
25
+ CP::App.instance.fatal( "'#{switch}' is not a valid option." )
26
+ end
27
+ end
28
+
29
+ def runner
30
+ runner = ::CmdParse::CommandParser.new
31
+ runner.program_name = @app.name
32
+ runner.program_version = @app.version ? @app.version.split( "." ) : nil
33
+
34
+ add_options( runner, @app.options )
35
+ add_commands( runner, @app.commands )
36
+
37
+ unless @app.commands.find { |c| c.name === :help }
38
+ has_default_command = @app.commands.find { |c| c.default }
39
+ runner.add_command( ::CmdParse::HelpCommand.new, !has_default_command )
40
+ end
20
41
 
21
- @runner.parse
42
+ runner
22
43
  end
23
44
 
24
45
  private
@@ -40,13 +61,13 @@ module CP
40
61
  cmd.short_desc = c.summary
41
62
 
42
63
  cmd.set_execution_block do |args|
43
- c.block.call( args ) if c.block
64
+ c.block.call( args )
44
65
  end
45
66
 
46
67
  add_options( cmd, c.options )
47
68
  add_commands( cmd, c.commands )
48
69
 
49
- target.add_command( cmd )
70
+ target.add_command( cmd, c.default )
50
71
  end
51
72
  end
52
73
  end