claide 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+
3
+ module CLAide
4
+ class Command
5
+ # Loads a command instances from arguments.
6
+ #
7
+ module Parser
8
+ # @param [Array, ARGV] argv
9
+ # A list of (remaining) parameters.
10
+ #
11
+ # @return [Command] An instance of the command class that was matched by
12
+ # going through the arguments in the parameters and drilling down
13
+ # command classes.
14
+ #
15
+ def self.parse(command, argv)
16
+ argv = ARGV.coherce(argv)
17
+ cmd = argv.arguments.first
18
+ if cmd && subcommand = command.find_subcommand(cmd)
19
+ argv.shift_argument
20
+ parse(subcommand, argv)
21
+ elsif command.abstract_command? && command.default_subcommand
22
+ load_default_subcommand(command, argv)
23
+ else
24
+ command.new(argv)
25
+ end
26
+ end
27
+
28
+ # @param [Array, ARGV] argv
29
+ # A list of (remaining) parameters.#
30
+ #
31
+ # @return [Command] Returns the default subcommand initialized with the
32
+ # given arguments.
33
+ #
34
+ def self.load_default_subcommand(command, argv)
35
+ default_subcommand = command.default_subcommand
36
+ subcommand = command.find_subcommand(default_subcommand)
37
+ unless subcommand
38
+ raise 'Unable to find the default subcommand ' \
39
+ "`#{default_subcommand}` for command `#{self}`."
40
+ end
41
+ result = parse(subcommand, argv)
42
+ result.invoked_as_default = true
43
+ result
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,112 @@
1
+ # encoding: utf-8
2
+
3
+ module CLAide
4
+ class Command
5
+ module PluginsHelper
6
+ # Loads additional plugins via rubygems looking for files named after the
7
+ # `PLUGIN_PREFIX_plugin`.
8
+ #
9
+ def self.load_plugins(plugin_prefix)
10
+ paths = PluginsHelper.plugin_load_paths(plugin_prefix)
11
+ loaded_paths = []
12
+ paths.each do |path|
13
+ if PluginsHelper.safe_require(path)
14
+ loaded_paths << path
15
+ end
16
+ end
17
+ loaded_paths
18
+ end
19
+
20
+ # Returns the name and the version of the plugin with the given path.
21
+ #
22
+ # @param [String] path
23
+ # The load path of the plugin.
24
+ #
25
+ # @return [String] A string including the name and the version or a
26
+ # failure message.
27
+ #
28
+ def self.plugin_info(path)
29
+ if gemspec = find_gemspec(path)
30
+ spec = Gem::Specification.load(gemspec)
31
+ end
32
+
33
+ if spec
34
+ "#{spec.name}: #{spec.version}"
35
+ else
36
+ "[!] Unable to load a specification for `#{path}`"
37
+ end
38
+ end
39
+
40
+ # @return [String] Finds the path of the gemspec of a path. The path is
41
+ # iterated upwards until a dir with a single gemspec is found.
42
+ #
43
+ # @param [String] path
44
+ # The load path of a plugin.
45
+ #
46
+ def self.find_gemspec(path)
47
+ reverse_ascending_paths(path).find do |candidate_path|
48
+ glob = Dir.glob("#{candidate_path}/*.gemspec")
49
+ if glob.count == 1
50
+ return glob.first
51
+ end
52
+ end
53
+ nil
54
+ end
55
+
56
+ # @return [String] Returns the list of the parents paths of a path.
57
+ #
58
+ # @param [String] path
59
+ # The path for which the list is needed.
60
+ #
61
+ def self.reverse_ascending_paths(path)
62
+ components = path.split('/')[0...-1]
63
+ progress = nil
64
+ components.map do |component|
65
+ if progress
66
+ progress = progress + '/' + component
67
+ else
68
+ progress = component
69
+ end
70
+ end.reverse
71
+ end
72
+
73
+ # Returns the paths of the files to require to load the available
74
+ # plugins.
75
+ #
76
+ # @return [Array] The found plugins load paths.
77
+ #
78
+ def self.plugin_load_paths(plugin_prefix)
79
+ if plugin_prefix && !plugin_prefix.empty?
80
+ if Gem.respond_to? :find_latest_files
81
+ Gem.find_latest_files("#{plugin_prefix}_plugin")
82
+ else
83
+ Gem.find_files("#{plugin_prefix}_plugin")
84
+ end
85
+ else
86
+ []
87
+ end
88
+ end
89
+
90
+ # Loads the given path. If any exception occurs it is catched and an
91
+ # informative message is printed.
92
+ #
93
+ # @param [String] path
94
+ # The path to load
95
+ #
96
+ # rubocop:disable RescueException
97
+ def self.safe_require(path)
98
+ require path
99
+ true
100
+ rescue Exception => exception
101
+ message = "\n---------------------------------------------"
102
+ message << "\nError loading the plugin with path `#{path}`.\n"
103
+ message << "\n#{exception.class} - #{exception.message}"
104
+ message << "\n#{exception.backtrace.join("\n")}"
105
+ message << "\n---------------------------------------------\n"
106
+ puts message.ansi.yellow
107
+ false
108
+ end
109
+ # rubocop:enable RescueException
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,39 @@
1
+ # encoding: utf-8
2
+
3
+ require 'claide/command/shell_completion_helper/zsh_completion_generator'
4
+
5
+ module CLAide
6
+ class Command
7
+ module ShellCompletionHelper
8
+ # Returns the completion template generated for the given command for the
9
+ # given shell. If the shell is not provided it will be inferred by the
10
+ # environment.
11
+ #
12
+ def self.completion_template(command, shell = nil)
13
+ shell ||= ENV['SHELL'].split('/').last
14
+ case shell
15
+ when 'zsh'
16
+ ZSHCompletionGenerator.generate(command)
17
+ else
18
+ raise Help, "Auto-completion generator for `#{shell}` shell not" \
19
+ ' implemented.'
20
+ end
21
+ end
22
+
23
+ # Indents the lines of the given string except the first one to the given
24
+ # level. Uses two spaces per each level.
25
+ #
26
+ # @param [String] string
27
+ # The string to indent.
28
+ #
29
+ # @param [Fixnum] indentation
30
+ # The indentation amount.
31
+ #
32
+ # @return [String] An indented string.
33
+ #
34
+ def self.indent(string, indentation)
35
+ string.gsub("\n", "\n#{' ' * indentation * 2}")
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,191 @@
1
+ module CLAide
2
+ class Command
3
+ module ShellCompletionHelper
4
+ # Generates a completion script for the Z shell.
5
+ #
6
+ module ZSHCompletionGenerator
7
+ # @return [String] The completion script.
8
+ #
9
+ # @param [Class] command
10
+ # The command to generate the script for.
11
+ #
12
+ # rubocop:disable MethodLength
13
+ def self.generate(command)
14
+ result = <<-DOC.strip_margin('|')
15
+ |#compdef #{command.command}
16
+ |# setopt XTRACE VERBOSE
17
+ |# vim: ft=zsh sw=2 ts=2 et
18
+ |
19
+ |local -a _subcommands
20
+ |local -a _options
21
+ |
22
+ |#{case_statement_fragment(command)}
23
+ DOC
24
+
25
+ post_process(result)
26
+ end
27
+ # rubocop:enable MethodLength
28
+
29
+ # Returns a case statement for a given command with the given nesting
30
+ # level.
31
+ #
32
+ # @param [Class] command
33
+ # The command to generate the fragment for.
34
+ #
35
+ # @param [Fixnum] nesting_level
36
+ # The nesting level to detect the index of the words array.
37
+ #
38
+ # @return [String] the case statement fragment.
39
+ #
40
+ # @example
41
+ # case "$words[2]" in
42
+ # spec-file)
43
+ # [..snip..]
44
+ # ;;
45
+ # *) # bin
46
+ # _subcommands=(
47
+ # "spec-file:"
48
+ # )
49
+ # _describe -t commands "bin subcommands" _subcommands
50
+ # _options=(
51
+ # "--completion-script:Print the auto-completion script"
52
+ # "--help:Show help banner of specified command"
53
+ # "--verbose:Show more debugging information"
54
+ # "--version:Show the version of the tool"
55
+ # )
56
+ # _describe -t options "bin options" _options
57
+ # ;;
58
+ # esac
59
+ #
60
+ # rubocop:disable MethodLength
61
+ def self.case_statement_fragment(command, nest_level = 0)
62
+ entries = case_statement_entries_fragment(command, nest_level + 1)
63
+ subcommands = subcommands_fragment(command)
64
+ options = options_fragment(command)
65
+
66
+ result = <<-DOC.strip_margin('|')
67
+ |case "$words[#{nest_level + 2}]" in
68
+ | #{ShellCompletionHelper.indent(entries, 1)}
69
+ | *) # #{command.full_command}
70
+ | #{ShellCompletionHelper.indent(subcommands, 2)}
71
+ | #{ShellCompletionHelper.indent(options, 2)}
72
+ | ;;
73
+ |esac
74
+ DOC
75
+ result.gsub(/\n *\n/, "\n").chomp
76
+ end
77
+ # rubocop:enable MethodLength
78
+
79
+ # Returns a case statement for a given command with the given nesting
80
+ # level.
81
+ #
82
+ # @param [Class] command
83
+ # The command to generate the fragment for.
84
+ #
85
+ # @param [Fixnum] nesting_level
86
+ # The nesting level to detect the index of the words array.
87
+ #
88
+ # @return [String] the case statement fragment.
89
+ #
90
+ # @example
91
+ # repo)
92
+ # case "$words[5]" in
93
+ # *) # bin spec-file lint
94
+ # _options=(
95
+ # "--help:Show help banner of specified command"
96
+ # "--only-errors:Skip warnings"
97
+ # "--verbose:Show more debugging information"
98
+ # )
99
+ # _describe -t options "bin spec-file lint options" _options
100
+ # ;;
101
+ # esac
102
+ # ;;
103
+ #
104
+ def self.case_statement_entries_fragment(command, nest_level)
105
+ subcommands = command.subcommands_for_command_lookup
106
+ subcommands.sort_by(&:name).map do |subcommand|
107
+ subcase = case_statement_fragment(subcommand, nest_level)
108
+ <<-DOC.strip_margin('|')
109
+ |#{subcommand.command})
110
+ | #{ShellCompletionHelper.indent(subcase, 1)}
111
+ |;;
112
+ DOC
113
+ end.join("\n")
114
+ end
115
+
116
+ # Returns the fragment of the subcommands array.
117
+ #
118
+ # @param [Class] command
119
+ # The command to generate the fragment for.
120
+ #
121
+ # @return [String] The fragment.
122
+ #
123
+ def self.subcommands_fragment(command)
124
+ subcommands = command.subcommands_for_command_lookup
125
+ list = subcommands.sort_by(&:name).map do |subcommand|
126
+ "\"#{subcommand.command}:#{subcommand.summary}\""
127
+ end
128
+ describe_fragment(command, 'subcommands', 'commands', list)
129
+ end
130
+
131
+ # Returns the fragment of the options array.
132
+ #
133
+ # @param [Class] command
134
+ # The command to generate the fragment for.
135
+ #
136
+ # @return [String] The fragment.
137
+ #
138
+ def self.options_fragment(command)
139
+ list = command.options.sort_by(&:first).map do |option|
140
+ "\"#{option[0]}:#{option[1]}\""
141
+ end
142
+ describe_fragment(command, 'options', 'options', list)
143
+ end
144
+
145
+ # Returns the fragment for a list of completions and the ZSH
146
+ # `_describe` function.
147
+ #
148
+ # @param [Class] command
149
+ # The command to generate the fragment for.
150
+ #
151
+ # @param [String] name
152
+ # The name of the list.
153
+ #
154
+ # @param [Class] tag
155
+ # The ZSH tag to use (e.g. command or option).
156
+ #
157
+ # @param [Array<String>] list
158
+ # The list of the entries.
159
+ #
160
+ # @return [String] The fragment.
161
+ #
162
+ def self.describe_fragment(command, name, tag, list)
163
+ if list && !list.empty?
164
+ <<-DOC.strip_margin('|')
165
+ |_#{name}=(
166
+ | #{ShellCompletionHelper.indent(list.join("\n"), 1)}
167
+ |)
168
+ |_describe -t #{tag} "#{command.full_command} #{name}" _#{name}
169
+ DOC
170
+ else
171
+ ''
172
+ end
173
+ end
174
+
175
+ # Post processes a script to remove any artifact and escape any needed
176
+ # character.
177
+ #
178
+ # @param [String] string
179
+ # The string to post process.
180
+ #
181
+ # @return [String] The post processed script.
182
+ #
183
+ def self.post_process(string)
184
+ string.gsub!(/\n *\n/, "\n\n")
185
+ string.gsub!(/`/, '\\\`')
186
+ string
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,82 @@
1
+ # encoding: utf-8
2
+
3
+ module CLAide
4
+ class Command
5
+ module ValidationHelper
6
+ # @return [String] Returns a message including a suggestion for the given
7
+ # unrecognized arguments.
8
+ #
9
+ # @param [Array<String>] arguments
10
+ # The unrecognized arguments.
11
+ #
12
+ # @param [Class] command_class
13
+ # The class of the command which encountered the unrecognized
14
+ # arguments.
15
+ #
16
+ def self.argument_suggestion(arguments, command_class)
17
+ string = arguments.first
18
+ type = ARGV::Parser.argument_type(string)
19
+ list = suggestion_list(command_class, type)
20
+ suggestion = ValidationHelper.suggestion(string, list)
21
+ pretty_suggestion = prettify_validation_suggestion(suggestion, type)
22
+ string_type = type == :arg ? 'command' : 'option'
23
+ "Unknown #{string_type}: `#{string}`\n" \
24
+ "Did you mean: #{pretty_suggestion}"
25
+ end
26
+
27
+ # @return [Array<String>] The list of the valid arguments for a command
28
+ # according to the type of the argument.
29
+ #
30
+ # @param [Command] command_class
31
+ # The class of the command for which the list of arguments is
32
+ # needed.
33
+ #
34
+ # @param [Symbol] type
35
+ # The type of the argument.
36
+ #
37
+ def self.suggestion_list(command_class, type)
38
+ case type
39
+ when :option, :flag
40
+ command_class.options.map(&:first)
41
+ when :arg
42
+ command_class.subcommands_for_command_lookup.map(&:command)
43
+ end
44
+ end
45
+
46
+ # Returns a suggestion for a string from a list of possible elements.
47
+ #
48
+ # @return [String] string
49
+ # The string for which the suggestion is needed.
50
+ #
51
+ # @param [Array<String>] list
52
+ # The list of the valid elements
53
+ #
54
+ def self.suggestion(string, list)
55
+ sorted = list.sort_by do |element|
56
+ Helper.levenshtein_distance(string, element)
57
+ end
58
+ sorted.first
59
+ end
60
+
61
+ # Prettifies the given validation suggestion according to the type.
62
+ #
63
+ # @param [String] suggestion
64
+ # The suggestion to prettify.
65
+ #
66
+ # @param [Type]
67
+ # The type of the suggestion: either `:command` or `:option`.
68
+ #
69
+ # @return [String] A handsome suggestion.
70
+ #
71
+ def self.prettify_validation_suggestion(suggestion, type)
72
+ case type
73
+ when :option, :flag
74
+ suggestion = "#{suggestion}"
75
+ suggestion.ansi.blue
76
+ when :arg
77
+ suggestion.ansi.green
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end