-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathcommand.cr
244 lines (195 loc) · 10.2 KB
/
command.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
module Cling
abstract class Command
# The name of the command. This is the only required field of a command and cannot be empty or
# blank.
property name : String
# A set of aliases for the command.
getter aliases : Set(String)
# An array of usage strings to display in generated help templates.
getter usage : Array(String)
# A header message to display at the top of generated help templates.
property header : String?
# The summary of the command to show in generated help templates.
property summary : String?
# The description of the command to show for specific help with the command.
property description : String?
# A footer message to display at the bottom of generated help templates.
property footer : String?
# The parent of the command of which the current command inherits from.
property parent : Command?
# A hash of commands that belong and inherit from the parent command.
getter children : Hash(String, Command)
# A hash of arguments belonging to the command. These arguments are parsed at execution time
# and can be accessed in the `pre_run`, `run`, and `post_run` methods via `ArgumentsInput`.
getter arguments : Hash(String, Argument)
# A hash of flag options belonging to the command. These options are parsed at execution time
# and can be accessed in the `pre_run`, `run`, and `post_run` methods via `OptionsInput`.
getter options : Hash(String, Option)
# Whether the command should be hidden from generated help templates.
property? hidden : Bool
# Whether the command should inherit the `header` and `footer` strings from the parent command.
property? inherit_borders : Bool
# Whether the command should inherit the options from the parent command.
property? inherit_options : Bool
# Whether the command should inherit the IO streams from the parent command.
property? inherit_streams : Bool
# The standard input stream for commands (defaults to `STDIN`). This is a helper method for
# custom commands and is only used by the `MainCommand` helper class.
property stdin : IO
# The standard output stream for commands (defaults to `STDOUT`). This is a helper method for
# custom commands and is only used by the `MainCommand` helper class.
property stdout : IO
# The standard error stream for commands (defaults to `STDERR`). This is a helper method for
# custom commands and is only used by the `MainCommand` helper class.
property stderr : IO
def initialize(*, aliases : Set(String)? = nil, usage : Array(String)? = nil,
@header : String? = nil, @summary : String? = nil, @description : String? = nil,
@footer : String? = nil, @parent : Command? = nil, children : Array(Command)? = nil,
arguments : Hash(String, Argument)? = nil, options : Hash(String, Option)? = nil,
@hidden : Bool = false, @inherit_borders : Bool = false, @inherit_options : Bool = false,
@inherit_streams : Bool = false, @stdin : IO = STDIN, @stdout : IO = STDOUT, @stderr : IO = STDERR)
@name = ""
@aliases = aliases || Set(String).new
@usage = usage || [] of String
@children = children || {} of String => Command
@arguments = arguments || {} of String => Argument
@options = options || {} of String => Option
setup
raise CommandError.new "Command name cannot be empty" if @name.empty?
raise CommandError.new "Command name cannot be blank" if @name.blank?
end
# An abstract method that should define information about the command such as the name,
# aliases, arguments, options, etc. The command name is required for all commands, all other
# values are optional including the help message.
abstract def setup : Nil
# Returns `true` if the argument matches the command name or any aliases.
def is?(name : String) : Bool
@name == name || @aliases.includes? name
end
# Returns the help template for this command. By default, one is generated interally unless
# this method is overridden.
def help_template : String
Formatter.new.generate self
end
# Adds an alias to the command.
def add_alias(name : String) : Nil
@aliases << name
end
# Adds several aliases to the command.
def add_aliases(*names : String) : Nil
@aliases.concat names
end
# Adds a usage string to the command.
def add_usage(usage : String) : Nil
@usage << usage
end
# Adds a command as a subcommand to the parent. The command can then be referenced by
# specifying it as the first argument in the command line.
def add_command(command : Command) : Nil
raise CommandError.new "Duplicate command '#{command.name}'" if @children.has_key? command.name
command.aliases.each do |a|
raise CommandError.new "Duplicate command alias '#{a}'" if @children.values.any? &.is? a
end
command.parent = self
if command.inherit_borders?
command.header = @header
command.footer = @footer
end
command.options.merge! @options if command.inherit_options?
if command.inherit_streams?
command.stdin = @stdin
command.stdout = @stdout
command.stderr = @stderr
end
@children[command.name] = command
end
# Adds several commands as subcommands to the parent (see `add_command`).
def add_commands(*commands : Command) : Nil
commands.each { |c| add_command(c) }
end
# Adds an argument to the command.
def add_argument(name : String, *, description : String? = nil, required : Bool = false,
multiple : Bool = false, hidden : Bool = false) : Nil
raise CommandError.new "Duplicate argument '#{name}'" if @arguments.has_key? name
if multiple && @arguments.values.find &.multiple?
raise CommandError.new "Cannot have more than one argument with multiple values"
end
@arguments[name] = Argument.new(name, description, required, multiple, hidden)
end
# Adds a long flag option to the command.
def add_option(long : String, *, description : String? = nil, required : Bool = false,
hidden : Bool = false, type : Option::Type = :none, default : Value::Type = nil) : Nil
raise CommandError.new "Duplicate flag option '#{long}'" if @options.has_key? long
@options[long] = Option.new(long, nil, description, required, hidden, type, default)
end
# Adds a short flag option to the command.
def add_option(short : Char, long : String, *, description : String? = nil, required : Bool = false,
hidden : Bool = false, type : Option::Type = :none, default : Value::Type = nil) : Nil
raise CommandError.new "Duplicate flag option '#{long}'" if @options.has_key? long
if op = @options.values.find { |o| o.short == short }
raise CommandError.new "Flag '#{op.long}' already has the short option '#{short}'"
end
@options[long] = Option.new(long, short, description, required, hidden, type, default)
end
{% begin %}
# Executes the command with the given input and parser (see `Parser`). Returns the program exit
# status code, by default it is `0`.
{% if @top_level.has_constant?("Spec") %}
def execute(input : String | Array(String), *, parser : Parser? = nil, terminate : Bool = false) : Int32
{% else %}
def execute(input : String | Array(String), *, parser : Parser? = nil, terminate : Bool = true) : Int32
{% end %}
parser ||= Parser.new input
results = parser.parse
code = Executor.handle self, results
exit code if terminate
code
end
{% end %}
# A hook method to run once the command/subcommands, arguments and options have been parsed.
# This has access to the parsed arguments and options from the command line. This is useful if
# you want to implement checks for specific flags outside of the main `run` method, such as
# `-v`/`--version` flags or `-h`/`--help` flags.
def pre_run(arguments : Arguments, options : Options) : Bool?
end
# The main point of execution for the command, where arguments and options can be accessed.
abstract def run(arguments : Arguments, options : Options) : Nil
# A hook method to run once the `pre_run` and main `run` methods have been executed.
def post_run(arguments : Arguments, options : Options) : Nil
end
# Raises an `ExitProgram` exception to exit the program. By default it is `1`.
def exit_program(code : Int32 = 1) : NoReturn
raise ExitProgram.new code
end
# A hook method for when the command raises an exception during execution. By default, this
# raises the exception.
def on_error(ex : Exception)
raise ex
end
# A hook method for when the command receives missing arguments during execution. By default,
# this raises a `CommandError`.
def on_missing_arguments(arguments : Array(String))
raise CommandError.new %(Missing required argument#{"s" if arguments.size > 1}: #{arguments.join(", ")})
end
# A hook method for when the command receives unknown arguments during execution. By default,
# this raises a `CommandError`.
def on_unknown_arguments(arguments : Array(String))
raise CommandError.new %(Unknown argument#{"s" if arguments.size > 1}: #{arguments.join(", ")})
end
# A hook method for when the command receives an invalid option, for example, a value given to
# an option that takes no arguments. By default, this raises a `CommandError`.
def on_invalid_option(message : String)
raise CommandError.new message
end
# A hook method for when the command receives missing options that are required during
# execution. By default, this raises an `CommandError`.
def on_missing_options(options : Array(String))
raise CommandError.new %(Missing required option#{"s" if options.size > 1}: #{options.join(", ")})
end
# A hook method for when the command receives unknown options during execution. By default,
# this raises an `CommandError`.
def on_unknown_options(options : Array(String))
raise CommandError.new %(Unknown option#{"s" if options.size > 1}: #{options.join(", ")})
end
end
end