drudge 0.4.0

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,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