completely 0.7.2 → 0.7.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e2d4ef0dc7d503f77387c199d75989c8aca7408f73421086219bfab91823dd0
4
- data.tar.gz: 4c5650a43490f9d35417a808c532391c9a0e7cf52d47b36ef5ffe56ab0d28ed2
3
+ metadata.gz: bc1feb2ea4f84471fa2e014aed467122163556f6f92c0ec5ad536065f3789438
4
+ data.tar.gz: 747d77ca9ce9f351bd914656423f92d8501aa2e0b19789f227bbcd1130de7cb2
5
5
  SHA512:
6
- metadata.gz: 655e9090c78451ffc8ba5f8b4f309e92756c02b769b4f60d9df2520f5ddc572b6eb7e761aeb8d0a2e1f3113bd1bcf05be186c75a95d833fc9938ad6bf706057c
7
- data.tar.gz: 81b519ae5db4f3eed6475b1a8b601c8bb873ed6cf0a67c2862b893e878342e031c60b3c58b600c2bc80b080f9c63a0a6ead68d37ef5b42924d5e687ac514feb1
6
+ metadata.gz: 9bd666b63af2f8a48a085d7025d39d5980eb93c796193144a29e3c730f80f9c409b99584ffbd6bbbd240819ba3af9013915ff090f04850e9bd09c01d928d056e
7
+ data.tar.gz: b2a6fa4704ba2c6004e4ceb4735a7cc92cfb24bc9e277779562180153102d1983f1716a59a829f2269042f2e02528af1d75de87196c8e86a17a043097e4c0d42
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Completely - Bash Completions Generator
2
2
 
3
+ ![repocard](https://repocard.dannyben.com/svg/completely.svg)
4
+
3
5
  Completely is a command line utility and a Ruby library that lets you generate
4
6
  bash completion scripts from simple YAML configuration.
5
7
 
@@ -167,6 +169,13 @@ mygit:
167
169
  The `2> /dev/null` is used so that if the command is executed in a directory
168
170
  without a git repository, it will still behave as expected.
169
171
 
172
+ ### Completion scope and limitations
173
+
174
+ - Completion words are treated as whitespace-delimited tokens.
175
+ - Literal completion phrases that contain spaces are not supported as a single completion item.
176
+ - Quotes and other special shell characters in literal completion words are not escaped automatically.
177
+ - Dynamic `$(...)` completion commands should output plain whitespace-delimited words.
178
+
170
179
  ### Suggesting flag arguments
171
180
 
172
181
  Adding a `*` wildcard in the middle of a pattern can be useful for suggesting
@@ -35,7 +35,13 @@ module Completely
35
35
  end
36
36
 
37
37
  def completions
38
- @completions ||= Completions.load(config_path, function_name: args['--function'])
38
+ @completions ||= if config_path == '-'
39
+ raise Error, 'Nothing is piped on stdin' if $stdin.tty?
40
+
41
+ Completions.read $stdin, function_name: args['--function']
42
+ else
43
+ Completions.load config_path, function_name: args['--function']
44
+ end
39
45
  end
40
46
 
41
47
  def config_path
@@ -43,7 +49,11 @@ module Completely
43
49
  end
44
50
 
45
51
  def output_path
46
- @output_path ||= args['OUTPUT_PATH'] || ENV['COMPLETELY_OUTPUT_PATH'] || "#{config_basename}.bash"
52
+ @output_path ||= args['OUTPUT_PATH'] || ENV['COMPLETELY_OUTPUT_PATH'] || stdout || "#{config_basename}.bash"
53
+ end
54
+
55
+ def stdout
56
+ @stdout ||= config_path == '-' ? '-' : nil
47
57
  end
48
58
 
49
59
  def config_basename
@@ -3,19 +3,31 @@ require 'completely/commands/base'
3
3
  module Completely
4
4
  module Commands
5
5
  class Generate < Base
6
- help 'Generate the bash completion script to a file'
6
+ help 'Generate the bash completion script to file or stdout'
7
7
 
8
8
  usage 'completely generate [CONFIG_PATH OUTPUT_PATH --function NAME --wrap NAME]'
9
+ usage 'completely generate [CONFIG_PATH --install PROGRAM --function NAME]'
9
10
  usage 'completely generate (-h|--help)'
10
11
 
11
12
  option_function
12
13
  option '-w --wrap NAME', 'Wrap the completion script inside a function that echos the ' \
13
14
  'script. This is useful if you wish to embed it directly in your script.'
14
15
 
15
- param_config_path
16
+ option '-i --install PROGRAM', 'Install the generated script as completions for PROGRAM.'
17
+
18
+ param 'CONFIG_PATH', <<~USAGE
19
+ Path to the YAML configuration file [default: completely.yaml].
20
+ Use '-' to read from stdin.
21
+
22
+ Can also be set by an environment variable.
23
+ USAGE
24
+
16
25
  param 'OUTPUT_PATH', <<~USAGE
17
26
  Path to the output bash script.
18
- When not provided, the name of the input file will be used with a .bash extension.
27
+ Use '-' for stdout.
28
+
29
+ When not provided, the name of the input file will be used with a .bash extension, unless the input is stdin - in this case the default will be to output to stdout.
30
+
19
31
  Can also be set by an environment variable.
20
32
  USAGE
21
33
 
@@ -26,13 +38,35 @@ module Completely
26
38
  def run
27
39
  wrap = args['--wrap']
28
40
  output = wrap ? wrapper_function(wrap) : script
29
- File.write output_path, output
30
- say "Saved m`#{output_path}`"
31
- syntax_warning unless completions.valid?
41
+
42
+ if args['--install']
43
+ install output
44
+ elsif output_path == '-'
45
+ show output
46
+ else
47
+ save output
48
+ end
32
49
  end
33
50
 
34
51
  private
35
52
 
53
+ def install(content)
54
+ installer = Installer.from_string program: args['--install'], string: content
55
+ success = installer.install force: true
56
+ raise InstallError, "Failed running command:\nnb`#{installer.install_command_string}`" unless success
57
+
58
+ say "Saved m`#{installer.target_path}`"
59
+ say 'You may need to restart your session to test it'
60
+ end
61
+
62
+ def show(content) = puts content
63
+
64
+ def save(content)
65
+ File.write output_path, content
66
+ say "Saved m`#{output_path}`"
67
+ syntax_warning unless completions.valid?
68
+ end
69
+
36
70
  def wrapper_function(wrapper_name)
37
71
  completions.wrapper_function wrapper_name
38
72
  end
@@ -17,7 +17,10 @@ module Completely
17
17
  option '-d --dry', 'Show the installation command but do not run it'
18
18
 
19
19
  param 'PROGRAM', 'Name of the program the completions are for.'
20
- param 'SCRIPT_PATH', 'Path to the source bash script [default: completely.bash].'
20
+ param 'SCRIPT_PATH', <<~USAGE
21
+ Path to the source bash script [default: completely.bash].
22
+ Use '-' to provide the script via stdin.
23
+ USAGE
21
24
 
22
25
  def run
23
26
  if args['--dry']
@@ -32,15 +35,19 @@ module Completely
32
35
  say 'You may need to restart your session to test it'
33
36
  end
34
37
 
35
- private
36
-
37
38
  def installer
38
- Installer.new program: args['PROGRAM'], script_path: script_path
39
+ @installer ||= if stdin?
40
+ Installer.from_io program:
41
+ else
42
+ Installer.new program:, script_path: input_script_path
43
+ end
39
44
  end
40
45
 
41
- def script_path
42
- args['SCRIPT_PATH'] || 'completely.bash'
43
- end
46
+ private
47
+
48
+ def program = args['PROGRAM']
49
+ def stdin? = input_script_path == '-'
50
+ def input_script_path = args['SCRIPT_PATH'] || 'completely.bash'
44
51
  end
45
52
  end
46
53
  end
@@ -3,7 +3,7 @@ require 'completely/commands/base'
3
3
  module Completely
4
4
  module Commands
5
5
  class Preview < Base
6
- help 'Generate the bash completion script to STDOUT'
6
+ help 'Generate the bash completion script to stdout'
7
7
 
8
8
  usage 'completely preview [CONFIG_PATH --function NAME]'
9
9
  usage 'completely preview (-h|--help)'
@@ -6,9 +6,12 @@ module Completely
6
6
  attr_reader :config
7
7
 
8
8
  class << self
9
- def load(config_path, function_name: nil)
10
- config = Config.load config_path
11
- new config, function_name: function_name
9
+ def load(path, function_name: nil)
10
+ new Config.load(path), function_name: function_name
11
+ end
12
+
13
+ def read(io, function_name: nil)
14
+ new Config.read(io), function_name: function_name
12
15
  end
13
16
  end
14
17
 
@@ -26,7 +29,7 @@ module Completely
26
29
  end
27
30
 
28
31
  def valid?
29
- pattern_prefixes.uniq.count == 1
32
+ pattern_prefixes.uniq.one?
30
33
  end
31
34
 
32
35
  def script
@@ -3,17 +3,14 @@ module Completely
3
3
  attr_reader :config, :options
4
4
 
5
5
  class << self
6
- def load(config_path)
7
- begin
8
- config = YAML.load_file config_path, aliases: true
9
- rescue ArgumentError
10
- # :nocov:
11
- config = YAML.load_file config_path
12
- # :nocov:
13
- end
14
-
15
- new config
6
+ def parse(str)
7
+ new YAML.load(str, aliases: true)
8
+ rescue Psych::Exception => e
9
+ raise ParseError, "Invalid YAML: #{e.message}"
16
10
  end
11
+
12
+ def load(path) = parse(File.read(path))
13
+ def read(io) = parse(io.read)
17
14
  end
18
15
 
19
16
  def initialize(config)
@@ -1,4 +1,5 @@
1
1
  module Completely
2
2
  class Error < StandardError; end
3
3
  class InstallError < Error; end
4
+ class ParseError < Error; end
4
5
  end
@@ -1,5 +1,38 @@
1
+ require 'fileutils'
2
+
1
3
  module Completely
2
4
  class Installer
5
+ class << self
6
+ def from_io(program:, io: nil)
7
+ io ||= $stdin
8
+
9
+ raise InstallError, 'io must respond to #read' unless io.respond_to?(:read)
10
+ raise InstallError, 'io is closed' if io.respond_to?(:closed?) && io.closed?
11
+
12
+ from_string program:, string: io.read
13
+ end
14
+
15
+ def from_string(program:, string:)
16
+ tempfile = create_tempfile
17
+ script_path = tempfile.path
18
+ begin
19
+ File.write script_path, string
20
+ ensure
21
+ tempfile.close
22
+ end
23
+
24
+ new program:, script_path:
25
+ end
26
+
27
+ def create_tempfile
28
+ tempfile = Tempfile.new ['completely-', '.bash']
29
+ tempfiles.push tempfile
30
+ tempfile
31
+ end
32
+
33
+ def tempfiles = @tempfiles ||= []
34
+ end
35
+
3
36
  attr_reader :program, :script_path
4
37
 
5
38
  def initialize(program:, script_path: nil)
@@ -7,18 +40,8 @@ module Completely
7
40
  @script_path = script_path
8
41
  end
9
42
 
10
- def target_directories
11
- @target_directories ||= %W[
12
- /usr/share/bash-completion/completions
13
- /usr/local/etc/bash_completion.d
14
- #{Dir.home}/.local/share/bash-completion/completions
15
- #{Dir.home}/.bash_completion.d
16
- ]
17
- end
18
-
19
43
  def install_command
20
- result = root_user? ? [] : %w[sudo]
21
- result + %W[cp #{script_path} #{target_path}]
44
+ %W[cp #{script_path} #{target_path}]
22
45
  end
23
46
 
24
47
  def install_command_string
@@ -26,8 +49,7 @@ module Completely
26
49
  end
27
50
 
28
51
  def uninstall_command
29
- result = root_user? ? [] : %w[sudo]
30
- result + %w[rm -f] + target_directories.map { |dir| "#{dir}/#{program}" }
52
+ %W[rm -f #{target_path}]
31
53
  end
32
54
 
33
55
  def uninstall_command_string
@@ -39,14 +61,12 @@ module Completely
39
61
  end
40
62
 
41
63
  def install(force: false)
42
- unless completions_path
43
- raise InstallError, 'Cannot determine system completions directory'
44
- end
45
-
46
64
  unless script_exist?
47
65
  raise InstallError, "Cannot find script: m`#{script_path}`"
48
66
  end
49
67
 
68
+ FileUtils.mkdir_p completions_path
69
+
50
70
  if target_exist? && !force
51
71
  raise InstallError, "File exists: m`#{target_path}`"
52
72
  end
@@ -68,20 +88,20 @@ module Completely
68
88
  File.exist? script_path
69
89
  end
70
90
 
71
- def root_user?
72
- Process.uid.zero?
91
+ def completions_path
92
+ @completions_path ||= "#{user_completions_base_dir}/completions"
73
93
  end
74
94
 
75
- def completions_path
76
- @completions_path ||= completions_path!
95
+ def user_completions_base_dir
96
+ @user_completions_base_dir ||= bash_completion_user_dir || "#{data_home}/bash-completion"
77
97
  end
78
98
 
79
- def completions_path!
80
- target_directories.each do |target|
81
- return target if Dir.exist? target
82
- end
99
+ def bash_completion_user_dir
100
+ ENV['BASH_COMPLETION_USER_DIR']&.split(':')&.find { |entry| !entry.empty? }
101
+ end
83
102
 
84
- nil
103
+ def data_home
104
+ ENV['XDG_DATA_HOME'] || "#{Dir.home}/.local/share"
85
105
  end
86
106
  end
87
107
  end
@@ -49,6 +49,10 @@ module Completely
49
49
  @compgen ||= compgen!
50
50
  end
51
51
 
52
+ def filename_action?
53
+ actions.include?('-A file') || actions.include?('-A directory')
54
+ end
55
+
52
56
  private
53
57
 
54
58
  def compgen!
@@ -9,24 +9,46 @@
9
9
  local cur=${COMP_WORDS[COMP_CWORD]}
10
10
  local result=()
11
11
 
12
+ # words the user already typed (excluding the command itself)
13
+ local used=()
14
+ if ((COMP_CWORD > 1)); then
15
+ used=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")
16
+ fi
17
+
12
18
  if [[ "${cur:0:1}" == "-" ]]; then
19
+ # Completing an option: offer everything (including options)
13
20
  echo "$words"
14
21
 
15
22
  else
23
+ # Completing a non-option: offer only non-options,
24
+ # and don't re-offer ones already used earlier in the line.
16
25
  for word in $words; do
17
- [[ "${word:0:1}" != "-" ]] && result+=("$word")
26
+ [[ "${word:0:1}" == "-" ]] && continue
27
+
28
+ local seen=0
29
+ for u in "${used[@]}"; do
30
+ if [[ "$u" == "$word" ]]; then
31
+ seen=1
32
+ break
33
+ fi
34
+ done
35
+ ((!seen)) && result+=("$word")
18
36
  done
19
37
 
20
38
  echo "${result[*]}"
21
-
22
39
  fi
23
40
  }
24
41
 
25
42
  <%= function_name %>() {
26
43
  local cur=${COMP_WORDS[COMP_CWORD]}
27
- local compwords=("${COMP_WORDS[@]:1:$COMP_CWORD-1}")
44
+ local compwords=()
45
+ if ((COMP_CWORD > 0)); then
46
+ compwords=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")
47
+ fi
28
48
  local compline="${compwords[*]}"
29
49
 
50
+ COMPREPLY=()
51
+
30
52
  % if ENV['COMPLETELY_DEBUG']
31
53
  if [[ -n "$COMPLETELY_DEBUG" ]]; then
32
54
  echo "compline: '$compline'" > 'completely-debug.txt'
@@ -38,6 +60,9 @@
38
60
  % patterns.each do |pattern|
39
61
  % next if pattern.empty?
40
62
  <%= pattern.case_string %>)
63
+ % if pattern.filename_action?
64
+ compopt -o filenames 2>/dev/null
65
+ % end
41
66
  while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen <%= pattern.compgen %> -- "$cur")
42
67
  ;;
43
68
 
@@ -1,3 +1,3 @@
1
1
  module Completely
2
- VERSION = '0.7.2'
2
+ VERSION = '0.7.5'
3
3
  end
data/lib/completely.rb CHANGED
@@ -4,3 +4,4 @@ require 'completely/pattern'
4
4
  require 'completely/completions'
5
5
  require 'completely/tester'
6
6
  require 'completely/installer'
7
+ require 'debug' if ENV['COMPLETELY_DEV']
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: completely
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.7.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danny Ben Shitrit
@@ -88,7 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
88
88
  - !ruby/object:Gem::Version
89
89
  version: '0'
90
90
  requirements: []
91
- rubygems_version: 3.6.9
91
+ rubygems_version: 4.0.6
92
92
  specification_version: 4
93
93
  summary: Bash Completions Generator
94
94
  test_files: []