excavator 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Rakefile +10 -0
- data/bin/excavator +3 -0
- data/excavator.gemspec +25 -0
- data/lib/excavator.rb +80 -0
- data/lib/excavator/command.rb +79 -0
- data/lib/excavator/dsl.rb +33 -0
- data/lib/excavator/environment.rb +24 -0
- data/lib/excavator/namespace.rb +56 -0
- data/lib/excavator/param.rb +25 -0
- data/lib/excavator/param_parser.rb +135 -0
- data/lib/excavator/runner.rb +107 -0
- data/lib/excavator/table_view.rb +90 -0
- data/lib/excavator/version.rb +3 -0
- data/test/fixtures/commands/test.rb +18 -0
- data/test/test_helper.rb +32 -0
- data/test/unit/command_test.rb +75 -0
- data/test/unit/dsl_test.rb +49 -0
- data/test/unit/environment_test.rb +64 -0
- data/test/unit/namespace_test.rb +61 -0
- data/test/unit/param_parser_test.rb +128 -0
- data/test/unit/runner_test.rb +107 -0
- data/test/unit/table_view_test.rb +62 -0
- metadata +118 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
data/bin/excavator
ADDED
data/excavator.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "excavator/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "excavator"
|
7
|
+
s.version = Excavator::VERSION
|
8
|
+
s.authors = ["Peter Bui"]
|
9
|
+
s.email = ["peter@paydrotalks.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{A scripting framework for *nix systems.}
|
12
|
+
s.description = <<DESC
|
13
|
+
Excavator is a scripting framework for writing multi-command executables for the
|
14
|
+
unix environment.
|
15
|
+
DESC
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_development_dependency "minitest", ">= 2.10.0"
|
23
|
+
s.add_development_dependency "ruby-debug19"
|
24
|
+
s.add_development_dependency "rake"
|
25
|
+
end
|
data/lib/excavator.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Excavator
|
4
|
+
class ExcavatorError < ::StandardError; end
|
5
|
+
|
6
|
+
class MissingParamsError < ExcavatorError
|
7
|
+
attr_reader :params
|
8
|
+
def initialize(params)
|
9
|
+
@params = params
|
10
|
+
super "Missing parameters: #{params.join(", ")}."
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.config(name, default)
|
15
|
+
@config ||= {}
|
16
|
+
@defaults ||= {}
|
17
|
+
@defaults[name.to_sym] = default
|
18
|
+
module_eval <<-MOD, __FILE__, __LINE__ + 1
|
19
|
+
def self.#{name}
|
20
|
+
@config[:#{name}] || @defaults[:#{name}]
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.#{name}=(val)
|
24
|
+
@config[:#{name}] = val
|
25
|
+
end
|
26
|
+
MOD
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.cwd
|
30
|
+
@cwd ||= Pathname.new(Dir.pwd).expand_path
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.reset!
|
34
|
+
self.runner = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.runner
|
38
|
+
@runner ||= runner_class.new
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.runner=(runner)
|
42
|
+
@runner = runner
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.run(params)
|
46
|
+
begin
|
47
|
+
runner.run(params)
|
48
|
+
|
49
|
+
rescue => e
|
50
|
+
$stderr.puts e.message
|
51
|
+
if ENV['DEBUG'] == '1'
|
52
|
+
e.backtrace.each { |line| $stderr.puts line }
|
53
|
+
end
|
54
|
+
|
55
|
+
exit 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
require 'excavator/version'
|
62
|
+
require 'excavator/dsl'
|
63
|
+
require 'excavator/namespace'
|
64
|
+
require 'excavator/param'
|
65
|
+
require 'excavator/param_parser'
|
66
|
+
require 'excavator/command'
|
67
|
+
require 'excavator/environment'
|
68
|
+
require 'excavator/runner'
|
69
|
+
require 'excavator/table_view'
|
70
|
+
|
71
|
+
module Excavator
|
72
|
+
# Setup defaults classes
|
73
|
+
config :command_paths, []
|
74
|
+
config :runner_class, Runner
|
75
|
+
config :namespace_class, Namespace
|
76
|
+
config :param_parser_class, ParamParser
|
77
|
+
config :param_class, Param
|
78
|
+
config :command_class, Command
|
79
|
+
config :environment_class, Environment
|
80
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
module Excavator
|
3
|
+
class Command
|
4
|
+
# Descriptors
|
5
|
+
attr_accessor :name, :desc, :namespace
|
6
|
+
|
7
|
+
# Block to run when command is executed
|
8
|
+
attr_accessor :block
|
9
|
+
|
10
|
+
# A list of Param objects
|
11
|
+
attr_reader :param_definitions
|
12
|
+
|
13
|
+
attr_reader :params
|
14
|
+
attr_reader :raw_params
|
15
|
+
attr_reader :unparsed_params
|
16
|
+
|
17
|
+
attr_reader :runner
|
18
|
+
|
19
|
+
def initialize(runner, options = {})
|
20
|
+
@runner = runner
|
21
|
+
@name = options[:name]
|
22
|
+
@block = options[:block]
|
23
|
+
@param_definitions = options[:param_definitions] || []
|
24
|
+
@namespace = options[:namespace]
|
25
|
+
@param_parser = options[:param_parser] ||
|
26
|
+
Excavator.param_parser_class.new
|
27
|
+
@params = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_param(param)
|
31
|
+
self.param_definitions << param
|
32
|
+
end
|
33
|
+
|
34
|
+
def execute(*inputs)
|
35
|
+
inputs.flatten!
|
36
|
+
parse_params inputs
|
37
|
+
run
|
38
|
+
end
|
39
|
+
|
40
|
+
def execute_with_params(parsed_params = {})
|
41
|
+
parse_params [parsed_params]
|
42
|
+
run
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
# Internal
|
48
|
+
def run
|
49
|
+
env = Excavator.environment_class.new(
|
50
|
+
:runner => runner,
|
51
|
+
:params => params,
|
52
|
+
:unparsed_params => unparsed_params,
|
53
|
+
:raw_params => raw_params
|
54
|
+
)
|
55
|
+
env.instance_eval(&block)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Internal
|
59
|
+
def parse_params(inputs)
|
60
|
+
build_parser
|
61
|
+
@raw_params = inputs.dup
|
62
|
+
@params = @param_parser.parse!(inputs) if param_definitions.size > 0
|
63
|
+
@unparsed_params = inputs
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_parser
|
67
|
+
return if @parser_built
|
68
|
+
command_name = ""
|
69
|
+
command_name << "#{namespace.full_name}:" if namespace
|
70
|
+
command_name << name.to_s
|
71
|
+
@param_parser.build(
|
72
|
+
:name => command_name,
|
73
|
+
:desc => desc,
|
74
|
+
:params => param_definitions
|
75
|
+
)
|
76
|
+
@parser_built = true
|
77
|
+
end
|
78
|
+
end # Command
|
79
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Excavator
|
2
|
+
# Needs to depend on the runner
|
3
|
+
module DSL
|
4
|
+
def namespace(name)
|
5
|
+
Excavator.runner.in_namespace(name) do
|
6
|
+
yield
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def desc(description)
|
11
|
+
Excavator.runner.last_command.desc = description
|
12
|
+
end
|
13
|
+
|
14
|
+
def param(name, options = {})
|
15
|
+
param = Excavator.param_class.new(name, options)
|
16
|
+
Excavator.runner.last_command.add_param(param)
|
17
|
+
end
|
18
|
+
|
19
|
+
def command(name, &block)
|
20
|
+
cmd = Excavator.runner.last_command
|
21
|
+
cmd.name = name
|
22
|
+
cmd.block = block
|
23
|
+
Excavator.runner.current_namespace << cmd
|
24
|
+
Excavator.runner.clear_last_command!
|
25
|
+
cmd
|
26
|
+
end
|
27
|
+
|
28
|
+
end # DSL
|
29
|
+
end # Excavator
|
30
|
+
|
31
|
+
self.extend Excavator::DSL
|
32
|
+
|
33
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Excavator
|
2
|
+
class Environment
|
3
|
+
|
4
|
+
def self.modify(*mods, &block)
|
5
|
+
mods.each { |m| include m }
|
6
|
+
instance_eval &block if block
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :runner, :params, :unparsed_params, :raw_params
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@runner = options[:runner]
|
13
|
+
@params = options[:params]
|
14
|
+
@raw_params = options[:raw_params]
|
15
|
+
@unparsed_params = options[:unparsed_params]
|
16
|
+
end
|
17
|
+
|
18
|
+
# Execute another command
|
19
|
+
def execute(command, params = {})
|
20
|
+
command = runner.find_command(command)
|
21
|
+
command.execute(params)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Excavator
|
2
|
+
class Namespace
|
3
|
+
attr_reader :name
|
4
|
+
attr_accessor :parent
|
5
|
+
|
6
|
+
def initialize(name = nil, options = {})
|
7
|
+
@name = name.to_sym if name
|
8
|
+
@namespaces = {}
|
9
|
+
@commands = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def <<(obj)
|
13
|
+
if self.class === obj
|
14
|
+
obj.parent = self
|
15
|
+
@namespaces[obj.name] = obj
|
16
|
+
else
|
17
|
+
@commands[obj.name] = obj
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def command(name)
|
22
|
+
@commands[name.to_sym]
|
23
|
+
end
|
24
|
+
|
25
|
+
def namespace(name)
|
26
|
+
@namespaces[name.to_sym]
|
27
|
+
end
|
28
|
+
|
29
|
+
def full_name(command_name = nil)
|
30
|
+
return "#{command_name}" if parent.nil?
|
31
|
+
|
32
|
+
parts = []
|
33
|
+
parts << parent.full_name if parent.full_name != ""
|
34
|
+
parts << name.to_s
|
35
|
+
parts << command_name.to_s if command_name
|
36
|
+
|
37
|
+
parts.compact.join(":")
|
38
|
+
end
|
39
|
+
|
40
|
+
def commands_and_descriptions
|
41
|
+
items = []
|
42
|
+
@commands.each do |cmd_name, cmd|
|
43
|
+
items << [
|
44
|
+
full_name(cmd_name),
|
45
|
+
cmd.desc
|
46
|
+
]
|
47
|
+
end
|
48
|
+
|
49
|
+
@namespaces.each do |ns_name, ns|
|
50
|
+
items.push(*ns.commands_and_descriptions)
|
51
|
+
end
|
52
|
+
|
53
|
+
items.sort
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Excavator
|
2
|
+
class Param
|
3
|
+
attr_reader :name
|
4
|
+
attr_reader :default
|
5
|
+
attr_reader :desc
|
6
|
+
attr_accessor :short # Set by the ParamParser
|
7
|
+
|
8
|
+
def initialize(name, options = {})
|
9
|
+
@name = name
|
10
|
+
@default = options[:default]
|
11
|
+
@optional = options[:optional]
|
12
|
+
@desc = options[:desc]
|
13
|
+
@short = options[:short]
|
14
|
+
end
|
15
|
+
|
16
|
+
def required?
|
17
|
+
!optional?
|
18
|
+
end
|
19
|
+
|
20
|
+
def optional?
|
21
|
+
@default || (@default.nil? && @optional)
|
22
|
+
end
|
23
|
+
|
24
|
+
end # Param
|
25
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
module Excavator
|
3
|
+
class ParamParser
|
4
|
+
|
5
|
+
attr_accessor :name
|
6
|
+
attr_accessor :banner
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@parser = OptionParser.new
|
10
|
+
@parsed_params = {}
|
11
|
+
@params = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def build(options = {})
|
15
|
+
@name = options[:name] if options[:name]
|
16
|
+
@params = options[:params] if options[:params]
|
17
|
+
@desc = options[:desc] if options[:desc]
|
18
|
+
|
19
|
+
required_params = []
|
20
|
+
optional_params = []
|
21
|
+
|
22
|
+
@parser.banner = @desc
|
23
|
+
@parser.separator ""
|
24
|
+
@parser.separator "USAGE: #{@name} [options]"
|
25
|
+
@params.each do |param|
|
26
|
+
opts = []
|
27
|
+
|
28
|
+
# Long option
|
29
|
+
opts << "--#{param.name}"
|
30
|
+
|
31
|
+
# params require an argument (for now)
|
32
|
+
opts << "=#{param.name.to_s.upcase}"
|
33
|
+
|
34
|
+
# Short option
|
35
|
+
opts << short_switch(param)
|
36
|
+
|
37
|
+
opts << param.desc if param.desc
|
38
|
+
opts << "Defaults to: #{param.default}" if param.default
|
39
|
+
|
40
|
+
opts << Proc.new do |val|
|
41
|
+
@parsed_params[param.name] = val
|
42
|
+
end
|
43
|
+
|
44
|
+
opts.compact!
|
45
|
+
|
46
|
+
if param.required?
|
47
|
+
required_params << opts
|
48
|
+
else
|
49
|
+
optional_params << opts
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
if required_params.size > 0
|
54
|
+
@parser.separator ""
|
55
|
+
@parser.separator "REQUIRED:"
|
56
|
+
required_params.each { |opts| @parser.on(*opts) }
|
57
|
+
end
|
58
|
+
|
59
|
+
if optional_params.size > 0
|
60
|
+
@parser.separator ""
|
61
|
+
@parser.separator "OPTIONAL:"
|
62
|
+
optional_params.each { |opts| @parser.on(*opts) }
|
63
|
+
end
|
64
|
+
|
65
|
+
@parser.separator ""
|
66
|
+
@parser.on_tail("-h", "--help", "This message.") do
|
67
|
+
puts usage
|
68
|
+
exit 1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def parse!(args)
|
73
|
+
@parsed_params = args.last.is_a?(Hash) ? args.pop : {}
|
74
|
+
|
75
|
+
@parser.parse!(args)
|
76
|
+
set_default_params
|
77
|
+
detect_missing_params!
|
78
|
+
|
79
|
+
@parsed_params
|
80
|
+
end
|
81
|
+
|
82
|
+
def detect_missing_params!
|
83
|
+
missing_params = []
|
84
|
+
@params.each do |p|
|
85
|
+
if p.required? && !@parsed_params.has_key?(p.name)
|
86
|
+
missing_params << p.name
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
raise MissingParamsError.new(missing_params) if missing_params.size > 0
|
91
|
+
end
|
92
|
+
|
93
|
+
def usage
|
94
|
+
@parser.to_s
|
95
|
+
end
|
96
|
+
|
97
|
+
protected
|
98
|
+
|
99
|
+
def set_default_params
|
100
|
+
@params.each do |param|
|
101
|
+
next unless param.default
|
102
|
+
unless @parsed_params.has_key?(param.name)
|
103
|
+
@parsed_params[param.name] = param.default
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def short_switch(param)
|
109
|
+
# TODO Raise error for params with specified short switches that collide.
|
110
|
+
# For now, we'll assume the user knows if they have the same
|
111
|
+
# short switches when specifying them.
|
112
|
+
return "-#{param.short}" if param.short
|
113
|
+
|
114
|
+
param_name = param.name.to_s
|
115
|
+
short = nil
|
116
|
+
param_name.each_char do |c|
|
117
|
+
short = c
|
118
|
+
if @params.detect { |p| p != param && p.short == c }
|
119
|
+
short = nil if c == param_name[-1]
|
120
|
+
next
|
121
|
+
else
|
122
|
+
break
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
return nil unless short
|
127
|
+
|
128
|
+
# Set the param's short var so that later auto short switch creation
|
129
|
+
# can determine a collision
|
130
|
+
param.short = short
|
131
|
+
|
132
|
+
"-#{short}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|