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.
@@ -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