clamp 1.4.0 → 1.5.1

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,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "clamp/command"
4
+ require "clamp/completion/bash_generator"
5
+ require "clamp/completion/fish_generator"
6
+ require "clamp/completion/zsh_generator"
7
+
8
+ module Clamp
9
+
10
+ # Shell completion script generation.
11
+ #
12
+ module Completion
13
+
14
+ GENERATORS = {
15
+ bash: Clamp::Completion::BashGenerator,
16
+ fish: Clamp::Completion::FishGenerator,
17
+ zsh: Clamp::Completion::ZshGenerator
18
+ }.freeze
19
+
20
+ # Raised when --shell-completions is used; caught by Command.run.
21
+ #
22
+ class Wanted < StandardError
23
+
24
+ def initialize(command, shell)
25
+ super("completion requested")
26
+ @command = command
27
+ @shell = shell
28
+ end
29
+
30
+ attr_reader :command, :shell
31
+
32
+ end
33
+
34
+ module_function
35
+
36
+ # Encode a name for use as a shell function identifier.
37
+ # Special characters are replaced with _XX hex codes.
38
+ def encode_name(name)
39
+ name.gsub(/[^a-zA-Z0-9_]/) { |c| format("_%02x", c.ord) }
40
+ end
41
+
42
+ def generate(command_class, shell, executable_name)
43
+ generator_class = GENERATORS.fetch(shell) do
44
+ raise ArgumentError, "unsupported shell: #{shell.inspect}"
45
+ end
46
+ generator_class.new(command_class, executable_name).generate
47
+ end
48
+
49
+ # Return switches with --[no-]foo expanded to --foo and --no-foo.
50
+ def expanded_switches(option)
51
+ option.switches.flat_map do |switch|
52
+ if switch =~ /^--\[no-\](.*)/
53
+ ["--#{Regexp.last_match(1)}", "--no-#{Regexp.last_match(1)}"]
54
+ else
55
+ switch
56
+ end
57
+ end
58
+ end
59
+
60
+ # Options visible in completion (excludes hidden).
61
+ def visible_options(command_class)
62
+ command_class.recognised_options.reject(&:hidden?)
63
+ end
64
+
65
+ # Walk the command tree depth-first, yielding (command_class, path, has_children).
66
+ # Path is an array of Subcommand::Definition objects.
67
+ # Always yields, even for revisited classes (with has_children=false).
68
+ def walk_command_tree(command_class, path = [], visited = Set.new, &block)
69
+ fresh = !visited.include?(command_class)
70
+ visited |= [command_class]
71
+ has_children = command_class.has_subcommands? && fresh
72
+ yield command_class, path, has_children
73
+ return unless has_children
74
+
75
+ command_class.recognised_subcommands.each do |sub|
76
+ walk_command_tree(sub.subcommand_class, path + [sub], visited, &block)
77
+ end
78
+ end
79
+
80
+ # Count required, non-multivalued parameters for a command.
81
+ def required_parameter_count(command_class)
82
+ command_class.parameters.count { |p| p.required? && !p.multivalued? }
83
+ end
84
+
85
+ # Return fish argparse optspecs for an option.
86
+ def argparse_specs_for(option)
87
+ switches = expanded_switches(option)
88
+ suffix = option.flag? ? "" : "="
89
+ short = switches.find { |s| s.match?(/^-[^-]$/) }
90
+ longs = switches.select { |s| s.start_with?("--") }
91
+ if short && longs.length == 1
92
+ ["#{short.delete_prefix('-')}/#{longs.first.delete_prefix('--')}#{suffix}"]
93
+ else
94
+ longs.map { |l| "#{l.delete_prefix('--')}#{suffix}" }
95
+ end
96
+ end
97
+
98
+ # Collect all subcommand names across the command tree.
99
+ def collect_subcommand_names(command_class)
100
+ names = []
101
+ walk_command_tree(command_class) do |cmd, _path, has_children|
102
+ cmd.recognised_subcommands.each { |sub| names.concat(sub.names) } if has_children
103
+ end
104
+ names.uniq
105
+ end
106
+
107
+ end
108
+ end
109
+
110
+ module Clamp
111
+
112
+ # Reopened to add completion support.
113
+ #
114
+ class Command
115
+
116
+ def self.generate_completion(shell, executable_name)
117
+ Clamp::Completion.generate(self, shell, executable_name)
118
+ end
119
+
120
+ # Adds --shell-completions option and handles the Wanted exception.
121
+ #
122
+ module RunWithCompletion
123
+
124
+ def run(invocation_path = File.basename($PROGRAM_NAME), arguments = ARGV, context = {})
125
+ context[:root_command_class] ||= self
126
+ super
127
+ rescue Clamp::Completion::Wanted => e
128
+ shell_name = File.basename(e.shell).to_sym
129
+ begin
130
+ puts generate_completion(shell_name, invocation_path)
131
+ rescue ArgumentError => ex
132
+ $stderr.puts "ERROR: #{ex.message}"
133
+ exit(1)
134
+ end
135
+ end
136
+
137
+ end
138
+
139
+ class << self
140
+
141
+ prepend RunWithCompletion
142
+
143
+ end
144
+
145
+ end
146
+
147
+ end
148
+
149
+ module Clamp
150
+ module Option
151
+
152
+ # Adds implicit --shell-completions option to all commands.
153
+ #
154
+ module Declaration
155
+
156
+ # Declares --shell-completions alongside other implicit options.
157
+ #
158
+ module WithCompletionOption
159
+
160
+ def recognised_options
161
+ unless @implicit_completion_option_declared
162
+ @implicit_completion_option_declared = true
163
+ declare_implicit_completion_option
164
+ end
165
+ super
166
+ end
167
+
168
+ private
169
+
170
+ def declare_implicit_completion_option
171
+ return if effective_options.find { |o| o.handles?("--shell-completions") }
172
+
173
+ option "--shell-completions", "SHELL",
174
+ "generate shell completion script",
175
+ hidden: true do |shell|
176
+ raise Clamp::Completion::Wanted.new(self, shell)
177
+ end
178
+ end
179
+
180
+ end
181
+
182
+ prepend WithCompletionOption
183
+
184
+ end
185
+
186
+ end
187
+ end
data/lib/clamp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clamp
4
- VERSION = "1.4.0"
4
+ VERSION = "1.5.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clamp
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Williams
@@ -17,20 +17,9 @@ executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
- - ".autotest"
21
- - ".editorconfig"
22
- - ".github/workflows/ci.yml"
23
- - ".gitignore"
24
- - ".rspec"
25
- - ".rubocop.yml"
26
20
  - CHANGES.md
27
- - CODEOWNERS
28
- - Gemfile
29
- - Guardfile
30
21
  - LICENSE
31
22
  - README.md
32
- - Rakefile
33
- - clamp.gemspec
34
23
  - examples/admin
35
24
  - examples/defaulted
36
25
  - examples/flipflop
@@ -45,6 +34,10 @@ files:
45
34
  - lib/clamp/attribute/definition.rb
46
35
  - lib/clamp/attribute/instance.rb
47
36
  - lib/clamp/command.rb
37
+ - lib/clamp/completion.rb
38
+ - lib/clamp/completion/bash_generator.rb
39
+ - lib/clamp/completion/fish_generator.rb
40
+ - lib/clamp/completion/zsh_generator.rb
48
41
  - lib/clamp/errors.rb
49
42
  - lib/clamp/help.rb
50
43
  - lib/clamp/messages.rb
@@ -60,15 +53,6 @@ files:
60
53
  - lib/clamp/subcommand/parsing.rb
61
54
  - lib/clamp/truthy.rb
62
55
  - lib/clamp/version.rb
63
- - spec/clamp/command_group_spec.rb
64
- - spec/clamp/command_option_module_spec.rb
65
- - spec/clamp/command_option_reordering_spec.rb
66
- - spec/clamp/command_spec.rb
67
- - spec/clamp/help/builder_spec.rb
68
- - spec/clamp/messages_spec.rb
69
- - spec/clamp/option/definition_spec.rb
70
- - spec/clamp/parameter/definition_spec.rb
71
- - spec/spec_helper.rb
72
56
  homepage: https://github.com/mdub/clamp
73
57
  licenses:
74
58
  - MIT
data/.autotest DELETED
@@ -1,11 +0,0 @@
1
- require "autotest/bundler"
2
-
3
- Autotest.add_hook :initialize do |at|
4
-
5
- at.add_exception ".git"
6
-
7
- at.add_mapping(%r{^lib/(.*)\.rb$}, :prepend) do |_, match|
8
- ["spec/unit/#{match[1]}_spec.rb"] + Dir['spec/clamp/command*_spec.rb']
9
- end
10
-
11
- end
data/.editorconfig DELETED
@@ -1,10 +0,0 @@
1
- root = true
2
-
3
- [*]
4
- indent_style = space
5
- indent_size = 2
6
- end_of_line = lf
7
- charset = utf-8
8
- trim_trailing_whitespace = true
9
- insert_final_newline = true
10
- max_line_length = 120
@@ -1,31 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [master]
6
- pull_request:
7
- branches: [master]
8
-
9
- jobs:
10
- test:
11
- runs-on: ubuntu-latest
12
-
13
- strategy:
14
- fail-fast: false
15
- matrix:
16
- ruby-version:
17
- - "3.1"
18
- - "3.4"
19
- - "4.0"
20
-
21
- steps:
22
- - uses: actions/checkout@v4
23
-
24
- - name: Set up Ruby ${{ matrix.ruby-version }}
25
- uses: ruby/setup-ruby@v1
26
- with:
27
- ruby-version: ${{ matrix.ruby-version }}
28
- bundler-cache: true
29
-
30
- - name: Run tests and linter
31
- run: bundle exec rake
data/.gitignore DELETED
@@ -1,9 +0,0 @@
1
- *.gem
2
- .bundle
3
- .markdownlint*
4
- .rvmrc
5
- .ruby-version
6
- .yardoc
7
- doc
8
- pkg/*
9
- Gemfile.lock
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --color
2
- --warnings
data/.rubocop.yml DELETED
@@ -1,74 +0,0 @@
1
- plugins:
2
- - rubocop-rake
3
- - rubocop-rspec
4
-
5
- AllCops:
6
- TargetRubyVersion: 2.5
7
- NewCops: enable
8
-
9
- Layout/LineLength:
10
- Max: 120
11
-
12
- Layout/EmptyLinesAroundBlockBody:
13
- Enabled: false
14
-
15
- Layout/EmptyLinesAroundClassBody:
16
- EnforcedStyle: empty_lines
17
-
18
- Layout/EmptyLinesAroundModuleBody:
19
- Enabled: false
20
-
21
- Metrics/AbcSize:
22
- Enabled: false
23
-
24
- Metrics/BlockLength:
25
- Exclude:
26
- - "spec/**/*"
27
-
28
- Metrics/MethodLength:
29
- Max: 30
30
-
31
- Naming/AccessorMethodName:
32
- Enabled: false
33
-
34
- Naming/FileName:
35
- Exclude:
36
- - "bin/*"
37
-
38
- Naming/PredicatePrefix:
39
- Enabled: false
40
-
41
- Style/ClassAndModuleChildren:
42
- EnforcedStyle: nested
43
- Exclude:
44
- - "spec/**/*"
45
-
46
- Style/Documentation:
47
- Exclude:
48
- - "lib/**/version.rb"
49
- - "examples/*"
50
- - "spec/**/*"
51
-
52
- Style/Encoding:
53
- Enabled: true
54
-
55
- Style/Lambda:
56
- Enabled: false
57
-
58
- Style/NumericLiterals:
59
- Enabled: false
60
-
61
- Style/StderrPuts:
62
- Enabled: false
63
-
64
- Style/StringLiterals:
65
- EnforcedStyle: double_quotes
66
-
67
- Style/WordArray:
68
- Enabled: false
69
-
70
- RSpec/NestedGroups:
71
- Enabled: false
72
-
73
- RSpec/Output:
74
- Enabled: false
data/CODEOWNERS DELETED
@@ -1 +0,0 @@
1
- * @mdub
data/Gemfile DELETED
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
-
5
- gemspec
6
-
7
- group :development do
8
- gem "guard-rspec", "~> 4.7", require: false
9
- gem "highline"
10
- gem "listen", "~> 3.9"
11
- gem "pry-byebug", "~> 3.11"
12
- gem "rake", "~> 13.3"
13
- gem "rubocop", "~> 1.84.1", require: false
14
- gem "rubocop-rake", "~> 0.7.1", require: false
15
- gem "rubocop-rspec", "~> 3.9.0", require: false
16
- end
17
-
18
- group :test do
19
- gem "rspec", "~> 3.13"
20
- end
data/Guardfile DELETED
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # A sample Guardfile
4
- # More info at https://github.com/guard/guard#readme
5
-
6
- ## Uncomment and set this to only include directories you want to watch
7
- # directories %w(app lib config test spec features) \
8
- # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
9
-
10
- ## NOTE: if you are using the `directories` clause above and you are not
11
- ## watching the project directory ('.'), then you will want to move
12
- ## the Guardfile to a watched dir and symlink it back, e.g.
13
- #
14
- # $ mkdir config
15
- # $ mv Guardfile config/
16
- # $ ln -s config/Guardfile .
17
- #
18
- # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
19
-
20
- # NOTE: The cmd option is now required due to the increasing number of ways
21
- # rspec may be run, below are examples of the most common uses.
22
- # * bundler: 'bundle exec rspec'
23
- # * bundler binstubs: 'bin/rspec'
24
- # * spring: 'bin/rspec' (This will use spring if running and you have
25
- # installed the spring binstubs per the docs)
26
- # * zeus: 'zeus rspec' (requires the server to be started separately)
27
- # * 'just' rspec: 'rspec'
28
-
29
- guard :rspec, cmd: "bundle exec rspec" do
30
- require "guard/rspec/dsl"
31
- dsl = Guard::RSpec::Dsl.new(self)
32
-
33
- # Feel free to open issues for suggestions and improvements
34
-
35
- # RSpec files
36
- rspec = dsl.rspec
37
- watch(rspec.spec_helper) { rspec.spec_dir }
38
- watch(rspec.spec_support) { rspec.spec_dir }
39
- watch(rspec.spec_files)
40
-
41
- # Ruby files
42
- ruby = dsl.ruby
43
- dsl.watch_spec_files_for(ruby.lib_files)
44
-
45
- end
data/Rakefile DELETED
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler"
4
-
5
- Bundler::GemHelper.install_tasks
6
-
7
- require "rspec/core/rake_task"
8
-
9
- RSpec::Core::RakeTask.new do |t|
10
- t.pattern = "spec/**/*_spec.rb"
11
- t.rspec_opts = ["--colour", "--format", "documentation"]
12
- end
13
-
14
- require "rubocop/rake_task"
15
-
16
- RuboCop::RakeTask.new
17
-
18
- task "default" => ["spec", "rubocop"]
data/clamp.gemspec DELETED
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- $LOAD_PATH.push File.expand_path("lib", __dir__)
4
- require "clamp/version"
5
-
6
- Gem::Specification.new do |s|
7
-
8
- s.name = "clamp"
9
- s.version = Clamp::VERSION.dup
10
- s.platform = Gem::Platform::RUBY
11
- s.authors = ["Mike Williams"]
12
- s.email = "mdub@dogbiscuit.org"
13
- s.homepage = "https://github.com/mdub/clamp"
14
-
15
- s.license = "MIT"
16
-
17
- s.summary = "a minimal framework for command-line utilities"
18
- s.description = <<-TEXT.gsub(/^\s+/, "")
19
- Clamp provides an object-model for command-line utilities.
20
- It handles parsing of command-line options, and generation of usage help.
21
- TEXT
22
-
23
- s.files = `git ls-files`.split("\n")
24
- s.require_paths = ["lib"]
25
-
26
- s.required_ruby_version = ">= 2.5", "< 5"
27
- s.metadata["rubygems_mfa_required"] = "true"
28
- end