cli-template 3.1.0 → 3.2.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +44 -5
  4. data/lib/cli-template/helpers.rb +3 -0
  5. data/lib/cli-template/sequence.rb +5 -4
  6. data/lib/cli-template/version.rb +1 -1
  7. data/lib/templates/colon_namespaces/%project_name%.gemspec.tt +30 -0
  8. data/lib/templates/colon_namespaces/.gitignore +16 -0
  9. data/lib/templates/colon_namespaces/.rspec +2 -0
  10. data/lib/templates/colon_namespaces/CHANGELOG.md +7 -0
  11. data/lib/templates/colon_namespaces/Gemfile.tt +6 -0
  12. data/lib/templates/colon_namespaces/Guardfile +19 -0
  13. data/lib/templates/colon_namespaces/LICENSE.txt.tt +22 -0
  14. data/lib/templates/colon_namespaces/README.md.tt +47 -0
  15. data/lib/templates/colon_namespaces/Rakefile +6 -0
  16. data/lib/templates/colon_namespaces/exe/%project_name%.tt +14 -0
  17. data/lib/templates/colon_namespaces/lib/%project_name%.rb.tt +12 -0
  18. data/lib/templates/colon_namespaces/lib/%underscored_name%/cli.rb.tt +166 -0
  19. data/lib/templates/colon_namespaces/lib/%underscored_name%/command.rb.tt +186 -0
  20. data/lib/templates/colon_namespaces/lib/%underscored_name%/completer.rb.tt +145 -0
  21. data/lib/templates/colon_namespaces/lib/%underscored_name%/completer/script.rb.tt +6 -0
  22. data/lib/templates/colon_namespaces/lib/%underscored_name%/completer/script.sh.tt +16 -0
  23. data/lib/templates/{default → colon_namespaces}/lib/%underscored_name%/completions.rb.tt +0 -0
  24. data/lib/templates/colon_namespaces/lib/%underscored_name%/help.rb.tt +9 -0
  25. data/lib/templates/{default → colon_namespaces}/lib/%underscored_name%/help/completions.md.tt +0 -0
  26. data/lib/templates/{default → colon_namespaces}/lib/%underscored_name%/help/completions/script.md.tt +0 -0
  27. data/lib/templates/colon_namespaces/lib/%underscored_name%/help/hello.md.tt +5 -0
  28. data/lib/templates/colon_namespaces/lib/%underscored_name%/help/main.md.tt +5 -0
  29. data/lib/templates/colon_namespaces/lib/%underscored_name%/help/sub/goodbye.md.tt +5 -0
  30. data/lib/templates/{default → colon_namespaces}/lib/%underscored_name%/main.rb.tt +0 -0
  31. data/lib/templates/{default → colon_namespaces}/lib/%underscored_name%/rake_command.rb.tt +0 -0
  32. data/lib/templates/colon_namespaces/lib/%underscored_name%/sub.rb.tt +12 -0
  33. data/lib/templates/colon_namespaces/lib/%underscored_name%/version.rb.tt +3 -0
  34. data/lib/templates/colon_namespaces/spec/lib/cli_spec.rb.tt +37 -0
  35. data/lib/templates/colon_namespaces/spec/spec_helper.rb.tt +29 -0
  36. data/lib/templates/default/.gitignore +1 -1
  37. data/lib/templates/default/Gemfile.lock.tt +64 -0
  38. data/lib/templates/default/LICENSE.txt +1 -1
  39. data/lib/templates/default/lib/%project_name%.rb.tt +1 -2
  40. data/lib/templates/default/lib/%underscored_name%/cli.rb.tt +24 -165
  41. data/lib/templates/default/lib/%underscored_name%/command.rb.tt +21 -151
  42. data/lib/templates/default/lib/%underscored_name%/completer.rb.tt +58 -57
  43. data/lib/templates/default/lib/%underscored_name%/completer/script.sh.tt +4 -10
  44. data/lib/templates/default/lib/%underscored_name%/completion.rb.tt +15 -0
  45. data/lib/templates/default/lib/%underscored_name%/help/completion.md.tt +22 -0
  46. data/lib/templates/default/lib/%underscored_name%/help/completion_script.md.tt +3 -0
  47. data/lib/templates/default/spec/lib/cli_spec.rb.tt +18 -17
  48. data/lib/templates/default/spec/spec_helper.rb.tt +2 -2
  49. data/spec/lib/cli_spec.rb +49 -38
  50. metadata +35 -7
@@ -0,0 +1,186 @@
1
+ require "thor"
2
+ require "active_support" # for autoload
3
+ require "active_support/core_ext"
4
+
5
+ # Override thor's long_desc identation behavior
6
+ # https://github.com/erikhuda/thor/issues/398
7
+ class Thor
8
+ module Shell
9
+ class Basic
10
+ def print_wrapped(message, options = {})
11
+ message = "\n#{message}" unless message[0] == "\n"
12
+ stdout.puts message
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ module <%= project_class_name %>
19
+ class Command < Thor
20
+ class << self
21
+ # thor_args is an array of commands. Examples:
22
+ # ["help"]
23
+ # ["dynamodb:migrate"]
24
+ #
25
+ # Same signature as RakeCommand.perform. Signature is a little weird
26
+ # with some repetition. Examples:
27
+ #
28
+ # <%= project_class_name %>::Main.perform("hello", ["hello"])
29
+ # <%= project_class_name %>::Main.perform("dynamodb:migrate", ["migrate"])
30
+ #
31
+ def perform(full_command, thor_args)
32
+ config = {} # doesnt seem like config is used
33
+ dispatch(nil, thor_args, nil, config)
34
+ rescue Thor::InvocationError => e
35
+ puts e.message
36
+ puts " ufo #{full_command} -h # for more help"
37
+ exit 1
38
+ end
39
+
40
+ # Track all command subclasses.
41
+ def subclasses
42
+ @subclasses ||= []
43
+ end
44
+
45
+ def inherited(base)
46
+ super
47
+
48
+ if base.name
49
+ self.subclasses << base
50
+ end
51
+ end
52
+
53
+ # Useful for help menu when we need to have all the definitions loaded.
54
+ # Using constantize instead of require so we dont care about
55
+ # order. The eager load actually uses autoloading.
56
+ @@eager_loaded = false
57
+ def eager_load!
58
+ return if @@eager_loaded
59
+
60
+ path = File.expand_path("../../", __FILE__)
61
+ Dir.glob("#{path}/<%= underscored_name %>/**/*.rb").select do |path|
62
+ next if !File.file?(path)
63
+
64
+ class_name = path
65
+ .sub(/\.rb$/,'')
66
+ .sub(%r{.*/lib/},'')
67
+ .classify
68
+
69
+ if class_name.include?('-')
70
+ puts "WARN: Unable to autoload a class with a dash in the name" if debug?
71
+ next
72
+ end
73
+
74
+ class_name = class_map[class_name] || class_name
75
+
76
+ puts "eager_load! loading path: #{path} class_name: #{class_name}" if debug?
77
+ class_name.constantize # dont have to worry about order.
78
+ end
79
+
80
+ @@eager_loaded = true
81
+ end
82
+
83
+ # Special class mapping cases. This is because ActiveSupport's autoloading
84
+ # forces a specific naming convention.
85
+ def class_map
86
+ map = {
87
+ "<%= project_class_name %>::Cli" => "<%= project_class_name %>::CLI",
88
+ "<%= project_class_name %>::Version" => "<%= project_class_name %>::VERSION",
89
+ "<%= project_class_name %>::Completion" => "<%= project_class_name %>::Completions",
90
+ }
91
+ map.merge(additional_class_map)
92
+ map
93
+ end
94
+
95
+ # Override this if you need add addtional class mappings.
96
+ def additional_class_map
97
+ {}
98
+ end
99
+
100
+ # Fully qualifed task names. Examples:
101
+ # hello
102
+ # sub:goodbye
103
+ def namespaced_commands
104
+ eager_load!
105
+ subclasses.map do |klass|
106
+ klass.all_tasks.keys.map do |task_name|
107
+ klass = klass.to_s.sub('<%= project_class_name %>::','')
108
+ namespace = klass =~ /^Main/ ? nil : klass.underscore.gsub('/',':')
109
+ [namespace, task_name].compact.join(':')
110
+ end
111
+ end.flatten.sort
112
+ end
113
+
114
+ # Use <%= project_class_name %> banner instead of Thor to account for namespaces in commands.
115
+ def banner(command, namespace = nil, subcommand = false)
116
+ namespace = namespace_from_class(self)
117
+ command_name = command.usage # set with desc when defining tht Thor class
118
+ namespaced_command = [namespace, command_name].compact.join(':')
119
+
120
+ "<%= project_name %> #{namespaced_command}"
121
+ end
122
+
123
+ def namespace_from_class(klass)
124
+ namespace = klass.to_s.sub('<%= project_class_name %>::', '').underscore.gsub('/',':')
125
+ namespace unless namespace == "main"
126
+ end
127
+
128
+ def help_list(all=false)
129
+ # hack to show hidden comands when requested
130
+ Thor::HiddenCommand.class_eval do
131
+ def hidden?; false; end
132
+ end if all
133
+
134
+ list = []
135
+ eager_load!
136
+ subclasses.each do |klass|
137
+ commands = klass.printable_commands(true, false)
138
+ commands.reject! { |array| array[0].include?(':help') }
139
+ list += commands
140
+ end
141
+
142
+ list.sort_by! { |array| array[0] }
143
+ end
144
+
145
+ # Example:
146
+ # klass_from_namespace(nil) => Main
147
+ # klass_from_namespace("sub") => Sub
148
+ def klass_from_namespace(namespace)
149
+ if namespace.nil?
150
+ <%= project_class_name %>::Main
151
+ else
152
+ class_name = namespace.gsub(':','/')
153
+ class_name = "<%= project_class_name %>::#{class_name.classify}"
154
+ class_name = class_map[class_name] || class_name
155
+ class_name.constantize
156
+ end
157
+ end
158
+
159
+ # If this fails to match then it'l just return the original full command
160
+ def autocomplete(full_command)
161
+ return nil if full_command.nil? # <%= project_name %> help
162
+
163
+ eager_load!
164
+
165
+ words = full_command.split(':')
166
+ namespace = words[0..-2].join(':') if words.size > 1
167
+ command = words.last
168
+
169
+ # Thor's normalize_command_name autocompletes the command but then we need to add the namespace back
170
+ begin
171
+ thor_subclass = klass_from_namespace(namespace) # could NameError
172
+ command = thor_subclass.normalize_command_name(command) # could Thor::AmbiguousCommandError
173
+ [namespace, command].compact.join(':')
174
+ rescue NameError
175
+ full_command # return original full_command
176
+ rescue Thor::AmbiguousCommandError => e
177
+ full_command # return original full_command
178
+ end
179
+ end
180
+
181
+ def debug?
182
+ ENV['DEBUG'] && !ENV['TEST']
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,145 @@
1
+ # Code Explanation. This is mainly focused on the run method.
2
+ #
3
+ # There are 3 main branches of logic for completions:
4
+ #
5
+ # 1. top-level commands - when there are zero completed words
6
+ # 2. params completions - when a command has some required params
7
+ # 3. options completions - when we have finished auto-completing the top-level command and required params, the rest of the completion words will be options
8
+ #
9
+ # Terms:
10
+ #
11
+ # params - these are params in the command itself. Example: for the method `scale(service, count)` the params would be `service, count`.
12
+ # options - these are cli options flags. Examples: --noop, --verbose
13
+ #
14
+ # When we are done processing method params, the completions will be only options. When the detected params size is greater than the arity we are have finished auto-completing the parameters in the method declaration. For example, say you had a method for a CLI command with the following form:
15
+ #
16
+ # scale(service, count) = arity of 2
17
+ #
18
+ # <%= project_name %> scale service count [TAB] # there are 3 params including the "scale" command
19
+ #
20
+ # So the completions will be something like:
21
+ #
22
+ # --noop --verbose etc
23
+ #
24
+ # A note about artity values:
25
+ #
26
+ # We are using the arity of the command method to determine if we have finish auto-completing the params completions. When the ruby method has a splat param, it's arity will be negative. Here are some example methods and their arities.
27
+ #
28
+ # ship(service) = 1
29
+ # scale(service, count) = 2
30
+ # ships(*services) = -1
31
+ # foo(example, *rest) = -2
32
+ #
33
+ # Fortunately, negative and positive arity values are processed the same way. So we take simply take the abs of the arity.
34
+ #
35
+ # To test:
36
+ #
37
+ # <%= project_name %> completions
38
+ # <%= project_name %> completions hello
39
+ # <%= project_name %> completions hello name
40
+ # <%= project_name %> completions hello name --
41
+ # <%= project_name %> completions hello name --noop
42
+ #
43
+ # <%= project_name %> completions
44
+ # <%= project_name %> completions sub:goodbye
45
+ # <%= project_name %> completions sub:goodbye name
46
+ #
47
+ # Note when testing, the first top-level word must be an exact match
48
+ #
49
+ # <%= project_name %> completions hello # works fine
50
+ # <%= project_name %> completions he # incomplete, this will just break
51
+ #
52
+ # The completions assumes that the top-level word that is being passed in
53
+ # from completor/scripts.sh will always match exactly. This must be the
54
+ # case. For parameters, the word does not have to match exactly.
55
+ #
56
+ module <%= project_class_name %>
57
+ class Completer
58
+ autoload :Script, '<%= underscored_name %>/completer/script'
59
+
60
+ def initialize(*params)
61
+ @params = params
62
+ end
63
+
64
+ def run
65
+ if @params.size == 0
66
+ puts all_commands
67
+ return
68
+ end
69
+
70
+ # will only get to here if the top-level command has been fully auto-completed.
71
+ arity = command_class.instance_method(trailing_command).arity.abs
72
+ if @params.size <= arity
73
+ puts params_completions(current_command)
74
+ else
75
+ puts options_completions(current_command)
76
+ end
77
+ end
78
+
79
+ # all top-level commands
80
+ def all_commands
81
+ # Interesing, extra :help commands show up here but no whne using
82
+ # <%= project_class_name %>::Command.help_list in main_help -> thor_list
83
+ # We'll filter out :help for auto-completion.
84
+ commands = <%= project_class_name %>::Command.namespaced_commands
85
+ commands.reject { |c| c =~ /:help$/ }
86
+ end
87
+
88
+ def params_completions(current_command)
89
+ method_params = command_class.instance_method(trailing_command).parameters
90
+ # Example:
91
+ # >> Sub.instance_method(:goodbye).parameters
92
+ # => [[:req, :name]]
93
+ # >>
94
+ method_params.map!(&:last)
95
+
96
+ offset = @params.size - 1
97
+ offset_params = method_params[offset..-1]
98
+ method_params[offset..-1].first
99
+ end
100
+
101
+ def options_completions(current_command)
102
+ used = ARGV.select { |a| a.include?('--') } # so we can remove used options
103
+
104
+ method_options = command_class.all_commands[trailing_command].options.keys
105
+ class_options = command_class.class_options.keys
106
+
107
+ all_options = method_options + class_options + ['help']
108
+
109
+ all_options.map! { |o| "--#{o.to_s.dasherize}" }
110
+ filtered_options = all_options - used
111
+ filtered_options.uniq
112
+ end
113
+
114
+ def current_command
115
+ @params[0]
116
+ end
117
+
118
+ # Example: sub:goodbye => "sub"
119
+ def namespace
120
+ return nil unless current_command
121
+
122
+ if current_command.include?(':')
123
+ words = current_command.split(':')
124
+ words.pop
125
+ words.join(':')
126
+ end
127
+ end
128
+
129
+ # Example: sub:goodbye => "goodbye"
130
+ def trailing_command
131
+ current_command.split(':').last
132
+ end
133
+
134
+ def command_class
135
+ @command_class ||= <%= project_class_name %>::Command.klass_from_namespace(namespace)
136
+ end
137
+
138
+ # Useful for debugging. Using puts messes up completion.
139
+ def log(msg)
140
+ File.open("/tmp/complete.log", "a") do |file|
141
+ file.puts(msg)
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,6 @@
1
+ class <%= project_class_name %>::Completer::Script
2
+ def self.generate
3
+ bash_script = File.expand_path("script.sh", File.dirname(__FILE__))
4
+ puts "source #{bash_script}"
5
+ end
6
+ end
@@ -0,0 +1,16 @@
1
+ _<%= project_name %>() {
2
+ COMPREPLY=()
3
+ local word="${COMP_WORDS[COMP_CWORD]}"
4
+
5
+ if [ "$COMP_CWORD" -eq 1 ]; then
6
+ local completions=$(<%= project_name %> completions)
7
+ COMPREPLY=( $(compgen -W "$completions" -- "$word") )
8
+ else
9
+ local words=("${COMP_WORDS[@]}")
10
+ unset words[0]
11
+ local completions=$(<%= project_name %> completions ${words[@]})
12
+ COMPREPLY=( $(compgen -W "$completions" -- "$word") )
13
+ fi
14
+ }
15
+
16
+ complete -F _<%= project_name %> <%= project_name %>
@@ -0,0 +1,9 @@
1
+ module <%= project_class_name %>::Help
2
+ class << self
3
+ def text(namespaced_command)
4
+ path = namespaced_command.to_s.gsub(':','/')
5
+ path = File.expand_path("../help/#{path}.md", __FILE__)
6
+ IO.read(path) if File.exist?(path)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ Examples:
2
+
3
+ <%= project_name %> hello
4
+ <%= project_name %> hello NAME
5
+ <%= project_name %> hello NAME --from me
@@ -0,0 +1,5 @@
1
+ Add -h to any of the commands for more help. Examples:
2
+
3
+ <%= project_name %> hello -h
4
+ <%= project_name %> version
5
+ <%= project_name %> -h
@@ -0,0 +1,5 @@
1
+ Examples:
2
+
3
+ <%= project_name %> sub:goodbye
4
+ <%= project_name %> sub:goodbye NAME
5
+ <%= project_name %> sub:goodbye NAME --from me
@@ -0,0 +1,12 @@
1
+ module <%= project_class_name %>
2
+ class Sub < Command
3
+
4
+ desc "goodbye NAME", "say goodbye to NAME"
5
+ long_desc Help.text("sub:goodbye")
6
+ option :from, desc: "from person"
7
+ def goodbye(name="you")
8
+ puts "from: #{options[:from]}" if options[:from]
9
+ puts "Goodbye #{name}"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module <%= project_class_name %>
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,37 @@
1
+ require "spec_helper"
2
+
3
+ describe <%= project_class_name %>::CLI do
4
+ before(:all) do
5
+ @args = "--from Tung"
6
+ end
7
+
8
+ describe "<%= project_name %>" do
9
+ it "hello" do
10
+ out = execute("exe/<%= project_name %> hello world #{@args}")
11
+ expect(out).to include("from: Tung\nHello world")
12
+ end
13
+
14
+ it "goodbye" do
15
+ out = execute("exe/<%= project_name %> sub:goodbye world #{@args}")
16
+ expect(out).to include("from: Tung\nGoodbye world")
17
+ end
18
+
19
+ it "completions" do
20
+ out = execute("exe/<%= project_name %> completions")
21
+ expect(out).to include("hello")
22
+ expect(out).to include("sub:goodbye")
23
+
24
+ out = execute("exe/<%= project_name %> completions hello")
25
+ expect(out).to include("name")
26
+
27
+ out = execute("exe/<%= project_name %> completions hello name")
28
+ expect(out).to include("--from")
29
+
30
+ out = execute("exe/<%= project_name %> completions sub:goodbye")
31
+ expect(out).to include("name")
32
+
33
+ out = execute("exe/<%= project_name %> completions sub:goodbye name")
34
+ expect(out).to include("--from")
35
+ end
36
+ end
37
+ end