claide 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.markdown +4 -3
- data/lib/claide.rb +4 -3
- data/lib/claide/ansi.rb +126 -0
- data/lib/claide/ansi/cursor.rb +69 -0
- data/lib/claide/ansi/graphics.rb +72 -0
- data/lib/claide/ansi/string_escaper.rb +81 -0
- data/lib/claide/argv.rb +63 -108
- data/lib/claide/argv/parser.rb +83 -0
- data/lib/claide/command.rb +245 -300
- data/lib/claide/command/banner.rb +133 -117
- data/lib/claide/command/banner/prettifier.rb +59 -0
- data/lib/claide/command/options.rb +87 -0
- data/lib/claide/command/parser.rb +47 -0
- data/lib/claide/command/plugins_helper.rb +112 -0
- data/lib/claide/command/shell_completion_helper.rb +39 -0
- data/lib/claide/command/shell_completion_helper/zsh_completion_generator.rb +191 -0
- data/lib/claide/command/validation_helper.rb +82 -0
- data/lib/claide/help.rb +2 -18
- data/lib/claide/helper.rb +113 -0
- data/lib/claide/informative_error.rb +0 -2
- data/lib/claide/mixins.rb +25 -0
- metadata +16 -2
@@ -1,177 +1,193 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
require 'claide/command/banner/prettifier'
|
4
|
+
|
3
5
|
module CLAide
|
4
6
|
class Command
|
5
|
-
|
6
7
|
# Creates the formatted banner to present as help of the provided command
|
7
8
|
# class.
|
8
9
|
#
|
9
10
|
class Banner
|
10
|
-
|
11
|
-
# @return [Class]
|
11
|
+
# @return [Class] The command for which the banner should be created.
|
12
12
|
#
|
13
13
|
attr_accessor :command
|
14
14
|
|
15
|
-
# @return [Bool]
|
16
|
-
#
|
17
|
-
attr_accessor :ansi_output
|
18
|
-
alias_method :ansi_output?, :ansi_output
|
19
|
-
|
20
|
-
def colorize_output
|
21
|
-
warn "[!] The use of `CLAide::Command::Banner#colorize_output` has " \
|
22
|
-
"been deprecated. Use `CLAide::Command::Banner#ansi_output` " \
|
23
|
-
"instead. (Called from: #{caller.first})"
|
24
|
-
ansi_output
|
25
|
-
end
|
26
|
-
alias_method :colorize_output?, :colorize_output
|
27
|
-
|
28
|
-
def colorize_output=(flag)
|
29
|
-
warn "[!] The use of `CLAide::Command::Banner#colorize_output=` has " \
|
30
|
-
"been deprecated. Use `CLAide::Command::Banner#ansi_output=` " \
|
31
|
-
"instead. (Called from: #{caller.first})"
|
32
|
-
self.ansi_output = flag
|
33
|
-
end
|
34
|
-
|
35
15
|
# @param [Class] command @see command
|
36
|
-
# @param [Class] ansi_output @see ansi_output
|
37
16
|
#
|
38
|
-
def initialize(command
|
17
|
+
def initialize(command)
|
39
18
|
@command = command
|
40
|
-
@ansi_output = ansi_output
|
41
19
|
end
|
42
20
|
|
43
|
-
# @return [String]
|
21
|
+
# @return [String] The banner for the command.
|
44
22
|
#
|
45
23
|
def formatted_banner
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
banner << commands
|
56
|
-
end
|
57
|
-
banner << 'Options:'
|
58
|
-
banner << formatted_options_description
|
59
|
-
banner.join("\n\n")
|
24
|
+
sections = [
|
25
|
+
['Usage', formatted_usage_description],
|
26
|
+
['Commands', formatted_subcommand_summaries],
|
27
|
+
['Options', formatted_options_description]
|
28
|
+
]
|
29
|
+
banner = sections.map do |(title, body)|
|
30
|
+
["#{prettify_title(title)}:", body] unless body.empty?
|
31
|
+
end.compact.join("\n\n")
|
32
|
+
banner
|
60
33
|
end
|
61
34
|
|
62
35
|
private
|
63
36
|
|
64
37
|
# @!group Banner sections
|
65
|
-
|
66
38
|
#-----------------------------------------------------------------------#
|
67
39
|
|
68
|
-
# @return [String]
|
40
|
+
# @return [String] The indentation of the text.
|
69
41
|
#
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
42
|
+
TEXT_INDENT = 6
|
43
|
+
|
44
|
+
# @return [Fixnum] The maximum width of the text.
|
45
|
+
#
|
46
|
+
MAX_WIDTH = TEXT_INDENT + 80
|
47
|
+
|
48
|
+
# @return [Fixnum] The minimum between a name and its description.
|
49
|
+
#
|
50
|
+
DESCRIPTION_SPACES = 3
|
51
|
+
|
52
|
+
# @return [Fixnum] The minimum between a name and its description.
|
53
|
+
#
|
54
|
+
SUBCOMMAND_BULLET_SIZE = 2
|
55
|
+
|
56
|
+
# @return [String] The section describing the usage of the command.
|
57
|
+
#
|
58
|
+
def formatted_usage_description
|
59
|
+
message = command.description || command.summary || ''
|
60
|
+
message = Helper.format_markdown(message, TEXT_INDENT, MAX_WIDTH)
|
61
|
+
message = prettify_message(command, message)
|
62
|
+
"#{signature}\n\n#{message}"
|
87
63
|
end
|
88
64
|
|
89
|
-
# @return [String]
|
65
|
+
# @return [String] The signature of the command.
|
90
66
|
#
|
91
|
-
def
|
92
|
-
|
67
|
+
def signature
|
68
|
+
result = prettify_signature(
|
69
|
+
command.full_command, signature_sub_command, signature_arguments)
|
70
|
+
result.insert(0, '$ ')
|
71
|
+
result.insert(0, ' ' * (TEXT_INDENT - '$ '.size))
|
93
72
|
end
|
94
73
|
|
95
|
-
# @return [String]
|
74
|
+
# @return [String] The subcommand indicator of the signature.
|
96
75
|
#
|
97
|
-
def
|
98
|
-
if
|
99
|
-
|
100
|
-
message = message.split("\n").map { |line| " #{line}" }.join("\n")
|
101
|
-
args = " #{command.arguments}" if command.arguments
|
102
|
-
cmd = "$ #{command.full_command}#{args}"
|
103
|
-
" #{prettify_command_in_usage_description(cmd)}\n\n#{message}"
|
76
|
+
def signature_sub_command
|
77
|
+
if command.subcommands.any?
|
78
|
+
command.default_subcommand ? '[COMMAND]' : 'COMMAND'
|
104
79
|
end
|
80
|
+
''
|
105
81
|
end
|
106
82
|
|
107
|
-
# @return [String]
|
83
|
+
# @return [String] The arguments of the signature.
|
108
84
|
#
|
109
|
-
def
|
110
|
-
command
|
85
|
+
def signature_arguments
|
86
|
+
command.arguments.reduce('') do |memo, (name, type)|
|
87
|
+
name = "[#{name}]" if type == :optional
|
88
|
+
memo << ' ' << name
|
89
|
+
end.lstrip
|
111
90
|
end
|
112
91
|
|
113
|
-
# @return [String]
|
92
|
+
# @return [String] The section describing the subcommands of the command.
|
93
|
+
#
|
94
|
+
# @note The plus sign emphasizes the that the subcommands are added to
|
95
|
+
# the command. The square brackets conveys a sense of direction
|
96
|
+
# and indicates the gravitational force towards the default
|
97
|
+
# command.
|
114
98
|
#
|
115
99
|
def formatted_subcommand_summaries
|
116
|
-
subcommands =
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
is_default = subcommand.command == command.default_subcommand
|
125
|
-
if is_default
|
126
|
-
bullet_point = '-'
|
127
|
-
else
|
128
|
-
bullet_point = '*'
|
129
|
-
end
|
130
|
-
" #{bullet_point} #{subcommand_string} #{subcommand.summary}"
|
131
|
-
end.join("\n")
|
132
|
-
end
|
100
|
+
subcommands = subcommands_for_banner
|
101
|
+
subcommands.map do |subcommand|
|
102
|
+
name = subcommand.command
|
103
|
+
bullet = (name == command.default_subcommand) ? '>' : '+'
|
104
|
+
name = "#{bullet} #{name}"
|
105
|
+
pretty_name = prettify_subcommand(name)
|
106
|
+
entry_description(pretty_name, subcommand.summary, name.size)
|
107
|
+
end.join("\n")
|
133
108
|
end
|
134
109
|
|
135
|
-
# @return [String]
|
110
|
+
# @return [String] The section describing the options of the command.
|
136
111
|
#
|
137
|
-
def
|
138
|
-
|
112
|
+
def formatted_options_description
|
113
|
+
options = command.options
|
114
|
+
options.map do |name, description|
|
115
|
+
pretty_name = prettify_option_name(name)
|
116
|
+
entry_description(pretty_name, description, name.size)
|
117
|
+
end.join("\n")
|
139
118
|
end
|
140
119
|
|
141
|
-
|
142
|
-
|
143
|
-
#
|
120
|
+
# @return [String] The line describing a single entry (subcommand or
|
121
|
+
# option).
|
122
|
+
#
|
123
|
+
def entry_description(name, description, name_width)
|
124
|
+
max_name_width = compute_max_name_width
|
125
|
+
desc_start = max_name_width + (TEXT_INDENT - 2) + DESCRIPTION_SPACES
|
126
|
+
result = ' ' * (TEXT_INDENT - 2)
|
127
|
+
result << name
|
128
|
+
result << ' ' * DESCRIPTION_SPACES
|
129
|
+
result << ' ' * (max_name_width - name_width)
|
130
|
+
result << Helper.wrap_with_indent(description, desc_start, MAX_WIDTH)
|
131
|
+
end
|
144
132
|
|
133
|
+
# @!group Overrides
|
145
134
|
#-----------------------------------------------------------------------#
|
146
135
|
|
147
|
-
# @return [String]
|
136
|
+
# @return [String] A decorated title.
|
148
137
|
#
|
149
|
-
def
|
150
|
-
|
151
|
-
string.gsub(/^[ \t]{#{min.size}}/, '')
|
152
|
-
else
|
153
|
-
string
|
154
|
-
end
|
138
|
+
def prettify_title(title)
|
139
|
+
Prettifier.prettify_title(title)
|
155
140
|
end
|
156
141
|
|
157
|
-
# @return [String]
|
142
|
+
# @return [String] A decorated textual representation of the command.
|
158
143
|
#
|
159
|
-
def
|
160
|
-
|
144
|
+
def prettify_signature(command, subcommand, argument)
|
145
|
+
Prettifier.prettify_signature(command, subcommand, argument)
|
161
146
|
end
|
162
147
|
|
163
|
-
# @return [
|
148
|
+
# @return [String] A decorated command description.
|
164
149
|
#
|
165
|
-
def
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
150
|
+
def prettify_message(command, message)
|
151
|
+
Prettifier.prettify_message(command, message)
|
152
|
+
end
|
153
|
+
|
154
|
+
# @return [String] A decorated textual representation of the subcommand
|
155
|
+
# name.
|
156
|
+
#
|
157
|
+
def prettify_subcommand(name)
|
158
|
+
Prettifier.prettify_subcommand(name)
|
159
|
+
end
|
160
|
+
|
161
|
+
# @return [String] A decorated textual representation of the option name.
|
162
|
+
#
|
163
|
+
#
|
164
|
+
def prettify_option_name(name)
|
165
|
+
Prettifier.prettify_option_name(name)
|
166
|
+
end
|
167
|
+
|
168
|
+
# @!group Private helpers
|
169
|
+
#-----------------------------------------------------------------------#
|
170
|
+
|
171
|
+
# @return [Array<String>] The list of the subcommands to use in the
|
172
|
+
# banner.
|
173
|
+
#
|
174
|
+
def subcommands_for_banner
|
175
|
+
command.subcommands_for_command_lookup.reject do |subcommand|
|
176
|
+
subcommand.summary.nil?
|
177
|
+
end.sort_by(&:command)
|
173
178
|
end
|
174
179
|
|
180
|
+
# @return [Fixnum] The width of the largest command name or of the
|
181
|
+
# largest option name. Used to align all the descriptions.
|
182
|
+
#
|
183
|
+
def compute_max_name_width
|
184
|
+
widths = []
|
185
|
+
widths << command.options.map { |option| option.first.size }
|
186
|
+
widths << subcommands_for_banner.map do |cmd|
|
187
|
+
cmd.command.size + SUBCOMMAND_BULLET_SIZE
|
188
|
+
end.max
|
189
|
+
widths.flatten.compact.max || 1
|
190
|
+
end
|
175
191
|
end
|
176
192
|
end
|
177
193
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module CLAide
|
4
|
+
class Command
|
5
|
+
class Banner
|
6
|
+
# Implements the default logic to prettify the Banner.
|
7
|
+
#
|
8
|
+
module Prettifier
|
9
|
+
# @return [String] A decorated title.
|
10
|
+
#
|
11
|
+
def self.prettify_title(title)
|
12
|
+
title.ansi.underline
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [String] A decorated textual representation of the command.
|
16
|
+
#
|
17
|
+
def self.prettify_signature(command, subcommand, argument)
|
18
|
+
components = [
|
19
|
+
[command, :green],
|
20
|
+
[subcommand, :green],
|
21
|
+
[argument, :magenta]
|
22
|
+
]
|
23
|
+
components.reduce('') do |memo, (string, ansi_key)|
|
24
|
+
memo << ' ' << string.ansi.apply(ansi_key) unless string.empty?
|
25
|
+
memo
|
26
|
+
end.lstrip
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [String] A decorated command description.
|
30
|
+
#
|
31
|
+
def self.prettify_message(command, message)
|
32
|
+
message = message.dup
|
33
|
+
[[command.arguments, :magenta],
|
34
|
+
[command.options, :blue]].each do |(list, ansi_key)|
|
35
|
+
list.map(&:first).each do |name|
|
36
|
+
message.gsub!(/`#{name}`/, "`#{name}`".ansi.apply(ansi_key))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
message
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [String] A decorated textual representation of the subcommand
|
43
|
+
# name.
|
44
|
+
#
|
45
|
+
def self.prettify_subcommand(name)
|
46
|
+
name.chomp.ansi.green
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [String] A decorated textual representation of the option
|
50
|
+
# name.
|
51
|
+
#
|
52
|
+
#
|
53
|
+
def self.prettify_option_name(name)
|
54
|
+
name.chomp.ansi.blue
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module CLAide
|
4
|
+
class Command
|
5
|
+
# Provides support for the default options.
|
6
|
+
#
|
7
|
+
module Options
|
8
|
+
# @return [Array<Array<String, String>>] The default options for a root
|
9
|
+
# command implemented by CLAide.
|
10
|
+
#
|
11
|
+
DEFAULT_ROOT_OPTIONS = [
|
12
|
+
['--completion-script', 'Print the auto-completion script'],
|
13
|
+
['--version', 'Show the version of the tool']
|
14
|
+
]
|
15
|
+
|
16
|
+
# @return [Array<Array<String, String>>] The default options implemented
|
17
|
+
# by CLAide.
|
18
|
+
#
|
19
|
+
DEFAULT_OPTIONS = [
|
20
|
+
['--verbose', 'Show more debugging information'],
|
21
|
+
['--no-ansi', 'Show output without ANSI codes'],
|
22
|
+
['--help', 'Show help banner of specified command']
|
23
|
+
]
|
24
|
+
|
25
|
+
# @return [Array<Array<String, String>>] The list of the default
|
26
|
+
# options for the given command.
|
27
|
+
#
|
28
|
+
# @param [Class] command_class
|
29
|
+
# The command class for which the options are needed.
|
30
|
+
#
|
31
|
+
def self.default_options(command_class)
|
32
|
+
if command_class.root_command?
|
33
|
+
Options::DEFAULT_ROOT_OPTIONS + Options::DEFAULT_OPTIONS
|
34
|
+
else
|
35
|
+
Options::DEFAULT_OPTIONS
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Handles root commands options if appropriate.
|
40
|
+
#
|
41
|
+
# @param [Command] command
|
42
|
+
# The invoked command.
|
43
|
+
#
|
44
|
+
# @param [ARGV] argv
|
45
|
+
# The parameters of the command.
|
46
|
+
#
|
47
|
+
# @return [Bool] Whether any root command option was handled.
|
48
|
+
#
|
49
|
+
def self.handle_root_option(command, argv)
|
50
|
+
argv = ARGV.coherce(argv)
|
51
|
+
return false unless command.class.root_command?
|
52
|
+
if argv.flag?('version')
|
53
|
+
print_version(command)
|
54
|
+
return true
|
55
|
+
elsif argv.flag?('completion-script')
|
56
|
+
print_completion_template(command)
|
57
|
+
return true
|
58
|
+
end
|
59
|
+
false
|
60
|
+
end
|
61
|
+
|
62
|
+
# Prints the version of the command optionally including plugins.
|
63
|
+
#
|
64
|
+
# @param [Command] command
|
65
|
+
# The invoked command.
|
66
|
+
#
|
67
|
+
def self.print_version(command)
|
68
|
+
puts command.class.version
|
69
|
+
if command.verbose?
|
70
|
+
prefix = command.class.plugin_prefix
|
71
|
+
PluginsHelper.plugin_load_paths(prefix).each do |path|
|
72
|
+
puts PluginsHelper.plugin_info(path)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Prints an auto-completion script according to the user shell.
|
78
|
+
#
|
79
|
+
# @param [Command] command
|
80
|
+
# The invoked command.#
|
81
|
+
#
|
82
|
+
def self.print_completion_template(command)
|
83
|
+
puts ShellCompletionHelper.completion_template(command.class)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|