cp 0.0.1.pre1 → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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