claide 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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, ansi_output = false)
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
- banner = []
47
- if command.abstract_command?
48
- banner << command.description if command.description
49
- elsif usage = formatted_usage_description
50
- banner << 'Usage:'
51
- banner << usage
52
- end
53
- if commands = formatted_subcommand_summaries
54
- banner << 'Commands:'
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
- def formatted_options_description
71
- opts = command.options
72
- max_key_size = opts.map { |opt| opt.first.size }.max
73
-
74
- desc_start = max_key_size + 7 # fixed whitespace in `result` var
75
- desc_width = terminal_width - desc_start
76
-
77
- opts.map do |key, desc|
78
- space = ' ' * (max_key_size - key.size)
79
- result = " #{prettify_option_name(key)}#{space} "
80
- if terminal_width == 0
81
- result << desc
82
- else
83
- space = ' ' * desc_start
84
- result << word_wrap(desc, desc_width).split("\n").join("\n#{space}")
85
- end
86
- end.join("\n")
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 prettify_option_name(name)
92
- name
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 formatted_usage_description
98
- if message = command.description || command.summary
99
- message = strip_heredoc(message)
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 prettify_command_in_usage_description(command)
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 = command.subcommands_for_command_lookup.reject do |subcommand|
117
- subcommand.summary.nil?
118
- end.sort_by(&:command)
119
- unless subcommands.empty?
120
- command_size = subcommands.map { |cmd| cmd.command.size }.max
121
- subcommands.map do |subcommand|
122
- subcommand_string = subcommand.command.ljust(command_size)
123
- subcommand_string = prettify_subcommand_name(subcommand_string)
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 prettify_subcommand_name(name)
138
- ansi_output? ? name.green : name
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
- private
142
-
143
- # @!group Private helpers
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] Lifted straight from ActiveSupport. Thanks guys!
136
+ # @return [String] A decorated title.
148
137
  #
149
- def strip_heredoc(string)
150
- if min = string.scan(/^[ \t]*(?=\S)/).min
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] Lifted straight from ActionView. Thanks guys!
142
+ # @return [String] A decorated textual representation of the command.
158
143
  #
159
- def word_wrap(line, line_width)
160
- line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip
144
+ def prettify_signature(command, subcommand, argument)
145
+ Prettifier.prettify_signature(command, subcommand, argument)
161
146
  end
162
147
 
163
- # @return [Fixnum] The width of the current terminal, unless being piped.
148
+ # @return [String] A decorated command description.
164
149
  #
165
- def terminal_width
166
- @terminal_width ||= begin
167
- if STDOUT.tty? && system('which tput > /dev/null 2>&1')
168
- `tput cols`.to_i
169
- else
170
- 0
171
- end
172
- end
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