claide 0.5.0 → 0.6.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.
- 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
|