drudge 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ require 'fileutils'
2
+
3
+ Given(/^the file "(.+?)" is executable$/) do |file_name|
4
+ in_current_dir do
5
+ FileUtils.chmod "a+x", file_name
6
+ end
7
+ end
8
+
9
+ Given(/^a Ruby script called "(.+?)" with:$/) do |script_name, contents|
10
+ steps %Q{
11
+ Given a file named "#{script_name}" with:
12
+ """
13
+ #!/usr/bin/env ruby
14
+
15
+ #{contents}
16
+ """
17
+ And the file "#{script_name}" is executable
18
+ }
19
+ end
@@ -0,0 +1,5 @@
1
+ require 'cucumber'
2
+ require 'aruba/cucumber'
3
+ require 'rspec/expectations'
4
+
5
+ ENV['PATH'] = "#{File.expand_path '../../tmp/aruba', __dir__}#{File::PATH_SEPARATOR}#{ENV['PATH']}"
@@ -0,0 +1,111 @@
1
+ Feature: Splatt Arguments
2
+ Ruby 2.0 supports splatt (*args) arguments.
3
+ I want a close mapping between splatt arguements and the command line.
4
+
5
+ Scenario Outline: Splatt arguments at the end of the command
6
+ Given a Ruby script called "cli" with:
7
+ """
8
+ require 'drudge'
9
+
10
+ class Cli < Drudge
11
+
12
+ desc "Greets people"
13
+ def greet(from, *messages)
14
+ puts "#{from} says: #{messages.join(', ')}"
15
+ end
16
+
17
+ end
18
+
19
+ Cli.dispatch
20
+ """
21
+ When I run `<command>`
22
+ Then the output should contain "<output>"
23
+
24
+ Examples:
25
+ | command | output |
26
+ | cli greet | error: expected a value for <from>: |
27
+ | cli greet Santa Hi | Santa says: Hi |
28
+ | cli greet Santa Hi Aloha | Santa says: Hi, Aloha |
29
+ | cli greet Santa Hi Aloha 'Good Morning' | Santa says: Hi, Aloha, Good Morning |
30
+
31
+
32
+ Scenario Outline: Splatt arguments in the middle of the command
33
+ Given a Ruby script called "cli" with:
34
+ """
35
+ require 'drudge'
36
+
37
+ class Cli < Drudge
38
+
39
+ desc "Greets people"
40
+ def greet(from, *messages, to)
41
+ puts "#{from} says: #{messages.join(', ')} to #{to}"
42
+ end
43
+ end
44
+
45
+ Cli.dispatch
46
+ """
47
+ When I run `<command>`
48
+ Then the output should contain "<output>"
49
+
50
+ Examples:
51
+ | command | output |
52
+ | cli greet | error: expected a value for <from> |
53
+ | cli greet Santa | error: expected a value for <to> |
54
+ | cli greet Santa Joe | Santa says: to Joe |
55
+ | cli greet Santa Hi Joe | Santa says: Hi to Joe |
56
+ | cli greet Santa Hi Aloha Joe | Santa says: Hi, Aloha to Joe |
57
+
58
+ Scenario Outline: Splatt arguments at the beginning of the ocmmand
59
+ Given a Ruby script called "cli" with:
60
+ """
61
+ require 'drudge'
62
+
63
+ class Cli < Drudge
64
+
65
+ desc "Greets people"
66
+ def greet(*greeters, message, to)
67
+ puts "#{greeters.join(', ')} all say: #{message} to #{to}"
68
+ end
69
+ end
70
+
71
+ Cli.dispatch
72
+ """
73
+ When I run `<command>`
74
+ Then the output should contain "<output>"
75
+
76
+ Examples:
77
+ | command | output |
78
+ | cli greet | error: expected a value for <message> |
79
+ | cli greet Hi | error: expected a value for <to> |
80
+ | cli greet Hi Joe | all say: Hi to Joe |
81
+ | cli greet Santa Hi Joe | Santa all say: Hi to Joe |
82
+ | cli greet Santa Spiderman Hi Joe | Santa, Spiderman all say: Hi to Joe |
83
+
84
+
85
+ Scenario Outline: Splatt arguments combined with optional arguments
86
+ Given a Ruby script called "cli" with:
87
+ """
88
+ require 'drudge'
89
+
90
+ class Cli < Drudge
91
+
92
+ desc "Greets people"
93
+ def greet(from, first_message = 'Hi', *messages, to)
94
+ puts "#{from} says first #{first_message}, then #{messages.join(', ')} to #{to}"
95
+ end
96
+ end
97
+
98
+ Cli.dispatch
99
+ """
100
+ When I run `<command>`
101
+ Then the output should contain "<output>"
102
+
103
+ Examples:
104
+ | command | output |
105
+ | cli greet | error: expected a value for <from> |
106
+ | cli greet Santa | error: expected a value for <to> |
107
+ | cli greet Santa Joe | Santa says first Hi, then to Joe |
108
+ | cli greet Santa Hello Joe | Santa says first Hello, then to Joe |
109
+ | cli greet Santa Hello Aloha Joe | Santa says first Hello, then Aloha to Joe |
110
+ | cli greet Santa Hello Aloha 'Good Morning' Joe | Santa says first Hello, then Aloha, Good Morning to Joe |
111
+
@@ -0,0 +1,8 @@
1
+ require "drudge/version"
2
+ require "drudge/class_dsl"
3
+ require "drudge/dispatch"
4
+
5
+ class Drudge
6
+ include ClassDSL
7
+ include Dispatch
8
+ end
@@ -0,0 +1,106 @@
1
+ require 'drudge/kit'
2
+ require 'drudge/command'
3
+ require 'drudge/errors'
4
+
5
+ class Drudge
6
+
7
+ # A DSL that allows writing of a command line
8
+ # tool (kit) as a class
9
+ module ClassDSL
10
+
11
+
12
+ def self.included(cls)
13
+ cls.singleton_class.send :include, ClassMethods
14
+ end
15
+
16
+ # converts this into a (command) kit,
17
+ def to_kit(name = $0)
18
+ Kit.new name, build_commands(self.class.__commands)
19
+ end
20
+
21
+ private
22
+
23
+ def build_commands(commands)
24
+ commands.map do |c|
25
+ Command.new(c[:name], c[:params],
26
+ -> (*args) { self.send c[:name], *args },
27
+ **c[:meta])
28
+ end
29
+ end
30
+
31
+ module ClassMethods
32
+
33
+ # When found before a method definition, it marks the
34
+ # Provides a short description for the next command
35
+ def desc(description)
36
+ @__command_meta ||= {}
37
+ @__command_meta[:desc] = description
38
+ end
39
+
40
+ def method_added(m)
41
+ if @__command_meta and @__command_meta[:desc]
42
+ meth = instance_method(m)
43
+
44
+ @__commands ||= []
45
+ @__commands << { name: meth.name,
46
+ params: parse_command_parameters(meth.parameters),
47
+ meta: { desc: @__command_meta[:desc] } }
48
+
49
+ @__command_meta = {}
50
+ end
51
+
52
+ super
53
+ end
54
+
55
+ def __commands
56
+ merged_commands((@__commands || []), (superclass.__commands rescue []))
57
+ end
58
+
59
+ private
60
+
61
+ def merged_commands(newer, older)
62
+ # review: this method seems too complex. find a simpler implemetnation
63
+ case
64
+ when older.empty? then newer
65
+ when newer.empty? then older
66
+ else
67
+ deep_merger = -> (_, old, new) do
68
+ if Hash === old
69
+ old.merge(new, &deep_merger)
70
+ else
71
+ new
72
+ end
73
+ end
74
+
75
+ non_overriden = older.reject { |c| newer.any? { |cc| cc[:name] == c[:name] } }
76
+ newer_and_overriden = newer.map do |cmd|
77
+ overriden = older.find { |c| c[:name] == cmd[:name] }
78
+
79
+ if overriden
80
+ overriden.merge(cmd, &deep_merger)
81
+ else
82
+ cmd
83
+ end
84
+ end
85
+
86
+ non_overriden + newer_and_overriden
87
+ end
88
+
89
+ end
90
+
91
+ private
92
+
93
+ def parse_command_parameters(method_parameters)
94
+ method_parameters.map do |kind, name|
95
+ case kind
96
+ when :req then Param.any(name)
97
+ when :opt then Param.any(name, optional: true)
98
+ when :rest then Param.any(name, splatt: true)
99
+ else raise "Unsupported parameter type"
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ end
106
+ end
@@ -0,0 +1,100 @@
1
+ require 'drudge/errors'
2
+ require 'drudge/parsers'
3
+
4
+ class Drudge
5
+
6
+ # Describes a command and helps executing it
7
+ #
8
+ # The command is defined by a name and a list of arguments (see class Param).
9
+ # The body of the command is a lambda that accepts exactly the arguments
10
+
11
+ class Command
12
+ include Parsers
13
+
14
+ # The name of the command
15
+ attr_reader :name
16
+
17
+ # The list of parameters
18
+ attr_reader :params
19
+
20
+ # The command's body
21
+ attr_reader :body
22
+
23
+ # An optional short desicription of the command
24
+ attr_reader :desc
25
+
26
+ # Initializes a new command
27
+ def initialize(name, params = [], body, desc: "")
28
+ @name = name.to_sym
29
+ @params = params
30
+ @body = body
31
+
32
+ @desc = desc
33
+ end
34
+
35
+ # runs the command
36
+ def dispatch(*args)
37
+ @body.call(*args)
38
+ rescue ArgumentError => e
39
+ raise CommandArgumentError.new(name), e.message
40
+ end
41
+
42
+ # creates an argument parser for the command
43
+ def argument_parser
44
+ end_of_args = eos("extra command line arguments provided")
45
+
46
+ parser = params.reverse.reduce(end_of_args) do |rest, param|
47
+ p = param.argument_parser
48
+
49
+ case
50
+ when param.optional? then ((p > rest) | rest).describe("[#{p}] #{rest}")
51
+ when param.splatt? then (p.repeats(till: rest) > rest).describe("[#{p} ...] #{rest}")
52
+ else p > rest
53
+ end
54
+ end
55
+ end
56
+
57
+ end
58
+
59
+ # Represents a command parameter
60
+ class Param
61
+ include Parsers
62
+
63
+ TYPES = %i[any string]
64
+
65
+ # the argument's name
66
+ attr_reader :name
67
+
68
+ # the argument's type
69
+ attr_reader :type
70
+
71
+ attr_reader :optional
72
+ alias_method :optional?, :optional
73
+
74
+ attr_reader :splatt
75
+ alias_method :splatt?, :splatt
76
+
77
+ def initialize(name, type, optional: false, splatt: false)
78
+ @name = name.to_sym
79
+ @type = type.to_sym
80
+ @optional = !! optional
81
+ @splatt = !! splatt
82
+ end
83
+
84
+ # returns a parser that is able to parse arguments
85
+ # fitting this parameter
86
+ def argument_parser
87
+ arg(name, value(/.+/))
88
+ end
89
+
90
+ # factory methods for every type of parameter
91
+ class << self
92
+ TYPES.each do |type|
93
+ define_method type do |name, *rest|
94
+ new(name, type, *rest)
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ end
@@ -0,0 +1,41 @@
1
+ require 'drudge/ext'
2
+ require 'drudge/parsers/tokenizer'
3
+
4
+ class Drudge
5
+
6
+ module Dispatch
7
+
8
+ def self.included(cls)
9
+ cls.singleton_class.send :include, ClassMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ Tokenizer = Drudge::Parsers::Tokenizer
14
+
15
+ # Runs the CLI with the specified arguments
16
+ def dispatch(command_name = File.basename($0), args = ARGV)
17
+ cli_kit = self.new.to_kit(command_name)
18
+ complete_args = command_name, *args
19
+
20
+ argument_parser = cli_kit.argument_parser
21
+ _, *command_arguments = argument_parser.parse!(complete_args)[:args]
22
+
23
+ cli_kit.dispatch(*command_arguments)
24
+
25
+ rescue CliError => e
26
+ puts "#{e.command}: #{e.message}"
27
+ rescue ParseError => pe
28
+ $stderr.puts <<-EOS.undent
29
+ error: #{pe.message}:
30
+
31
+ #{Tokenizer.untokenize(pe.input)}
32
+ #{Tokenizer.underline_token(pe.input,
33
+ pe.remaining_input.first)}
34
+ EOS
35
+
36
+
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,30 @@
1
+ class Drudge
2
+ # a c
3
+ class CliError < StandardError
4
+ # the command that produced the error
5
+ attr_reader :command
6
+
7
+ def initialize(command)
8
+ @command = command.to_s
9
+ end
10
+ end
11
+
12
+ # Identifies a parse error
13
+ class ParseError < StandardError
14
+
15
+ attr_reader :remaining_input
16
+ attr_reader :input
17
+
18
+ def initialize(input, remaining_input)
19
+ @input, @remaining_input = input, remaining_input
20
+ end
21
+
22
+ end
23
+
24
+ # Identifies a problem with the arguments
25
+ class CommandArgumentError < CliError; end
26
+
27
+ # The user asked to execute a command that doesn't exist
28
+ class UnknownCommandError < CliError; end
29
+ end
30
+
@@ -0,0 +1,17 @@
1
+ # Extensions to standard classes
2
+
3
+ class String
4
+
5
+ # undents the string by removeing as much leading space
6
+ # as the first line has.
7
+ #
8
+ # Useful for cases like:
9
+ # puts <<-EOS.undent
10
+ # bla bla bla
11
+ # bla
12
+ # EOS
13
+ #
14
+ def undent
15
+ gsub(/^.{#{slice(/^ +/).length}}/, '')
16
+ end unless method_defined?(:undent)
17
+ end