yarsh 0.1.1 → 0.3.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,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'engine'
4
+
5
+ module Yarsh
6
+ module Completion
7
+ class GemCompletion < Engine
8
+ SUBCOMMANDS = %w[
9
+ build cert check cleanup contents dependency environment exec fetch
10
+ generate_index help info install list lock mirror open outdated owner
11
+ pristine push rdoc rebuild search server signin signout sources
12
+ specification stale uninstall unpack update which yank
13
+ ].freeze
14
+
15
+ OPTIONS = {
16
+ 'build' => %w[
17
+ --platform --force --strict -o --output
18
+ -h --help -V --verbose -q --quiet --silent --config-file
19
+ --backtrace --debug --norc
20
+ ].freeze,
21
+ 'cert' => %w[
22
+ -a --add -l --list -r --remove -b --build -C --certificate
23
+ -K --private-key -A --key-algorithm -s --sign -d --days -R --re-sign
24
+ -h --help -V --verbose -q --quiet --silent --config-file
25
+ --backtrace --debug --norc
26
+ ].freeze,
27
+ 'check' => %w[
28
+ -a --alien --doctor --dry-run --gems -v --version
29
+ -h --help -V --verbose -q --quiet --silent --config-file
30
+ --backtrace --debug --norc
31
+ ].freeze,
32
+ 'cleanup' => %w[
33
+ -n -d --dry-run -D --check-development --user-install --dryrun
34
+ -h --help -V --verbose -q --quiet --silent --config-file
35
+ --backtrace --debug --norc
36
+ ].freeze,
37
+ 'contents' => %w[
38
+ -v --version --all -s --spec-dir -l --lib-only --prefix --show-install-dir
39
+ -h --help -V --verbose -q --quiet --silent --config-file
40
+ --backtrace --debug --norc
41
+ ].freeze,
42
+ 'dependency' => %w[
43
+ -v --version --platform --prerelease -R --reverse-dependencies --pipe
44
+ -u --update-sources
45
+ -l --local -r --remote -b --both -B --bulk-threshold --clear-sources
46
+ -s --source -p --http-proxy
47
+ -h --help -V --verbose -q --quiet --silent --config-file
48
+ --backtrace --debug --norc
49
+ ].freeze,
50
+ 'environment' => %w[
51
+ -h --help -V --verbose -q --quiet --silent --config-file
52
+ --backtrace --debug --norc
53
+ ].freeze,
54
+ 'exec' => %w[
55
+ -v --version --prerelease -g --gem --conservative
56
+ -h --help -V --verbose -q --quiet --silent --config-file
57
+ --backtrace --debug --norc
58
+ ].freeze,
59
+ 'fetch' => %w[
60
+ -v --version --platform --prerelease --suggestions
61
+ -B --bulk-threshold -p --http-proxy -s --source --clear-sources
62
+ -h --help -V --verbose -q --quiet --silent --config-file
63
+ --backtrace --debug --norc
64
+ ].freeze,
65
+ 'generate_index' => %w[
66
+ -d --directory --modern --compact --update
67
+ -h --help -V --verbose -q --quiet --silent --config-file
68
+ --backtrace --debug --norc
69
+ ].freeze,
70
+ 'help' => %w[
71
+ -h --help -V --verbose -q --quiet --silent --config-file
72
+ --backtrace --debug --norc
73
+ ].freeze,
74
+ 'info' => %w[
75
+ -i --installed -I -v --version --versions -a --all -e --exact --prerelease
76
+ -u --update-sources
77
+ -l --local -r --remote -b --both -B --bulk-threshold --clear-sources
78
+ -s --source -p --http-proxy
79
+ -h --help -V --verbose -q --quiet --silent --config-file
80
+ --backtrace --debug --norc
81
+ ].freeze,
82
+ 'install' => %w[
83
+ --platform -v --version --prerelease
84
+ --default -u --update-sources
85
+ -i --install-dir -n --bindir -j --build-jobs --document --build-root
86
+ --vendor -N --no-document -E --env-shebang -f --force -w --wrappers
87
+ -P --trust-policy --ignore-dependencies --format-executable --user-install
88
+ --development --development-all --conservative --minimal-deps
89
+ --post-install-message -g --file --without --explain --lock
90
+ --suggestions --target-rbconfig
91
+ -l --local -r --remote -b --both -B --bulk-threshold --clear-sources
92
+ -s --source -p --http-proxy
93
+ -h --help -V --verbose -q --quiet --silent --config-file
94
+ --backtrace --debug --norc
95
+ ].freeze,
96
+ 'list' => %w[
97
+ -i --installed -I -v --version -d --details --versions -a --all -e --exact --prerelease
98
+ -u --update-sources
99
+ -l --local -r --remote -b --both -B --bulk-threshold --clear-sources
100
+ -s --source -p --http-proxy
101
+ -h --help -V --verbose -q --quiet --silent --config-file
102
+ --backtrace --debug --norc
103
+ ].freeze,
104
+ 'lock' => %w[
105
+ -s --strict
106
+ -h --help -V --verbose -q --quiet --silent --config-file
107
+ --backtrace --debug --norc
108
+ ].freeze,
109
+ 'mirror' => %w[
110
+ -h --help -V --verbose -q --quiet --silent --config-file
111
+ --backtrace --debug --norc
112
+ ].freeze,
113
+ 'open' => %w[
114
+ -e --editor -v --version
115
+ -h --help -V --verbose -q --quiet --silent --config-file
116
+ --backtrace --debug --norc
117
+ ].freeze,
118
+ 'outdated' => %w[
119
+ --platform
120
+ -u --update-sources
121
+ -l --local -r --remote -b --both -B --bulk-threshold --clear-sources
122
+ -s --source -p --http-proxy
123
+ -h --help -V --verbose -q --quiet --silent --config-file
124
+ --backtrace --debug --norc
125
+ ].freeze,
126
+ 'owner' => %w[
127
+ -k --key --otp -a --add -r --remove --host
128
+ -p --http-proxy
129
+ -h --help -V --verbose -q --quiet --silent --config-file
130
+ --backtrace --debug --norc
131
+ ].freeze,
132
+ 'pristine' => %w[
133
+ --all --skip --extensions --only-missing-extensions --only-executables
134
+ --only-plugins -E --env-shebang -i --install-dir -n --bindir -v --version
135
+ -h --help -V --verbose -q --quiet --silent --config-file
136
+ --backtrace --debug --norc
137
+ ].freeze,
138
+ 'push' => %w[
139
+ -k --key --otp --host --attestation
140
+ -p --http-proxy
141
+ -h --help -V --verbose -q --quiet --silent --config-file
142
+ --backtrace --debug --norc
143
+ ].freeze,
144
+ 'rdoc' => %w[
145
+ --all --rdoc --ri --overwrite -v --version
146
+ -h --help -V --verbose -q --quiet --silent --config-file
147
+ --backtrace --debug --norc
148
+ ].freeze,
149
+ 'rebuild' => %w[
150
+ --diff --force --strict --source --original --gemspec -C
151
+ -h --help -V --verbose -q --quiet --silent --config-file
152
+ --backtrace --debug --norc
153
+ ].freeze,
154
+ 'search' => %w[
155
+ -i --installed -I -v --version -d --details --versions -a --all -e --exact --prerelease
156
+ -u --update-sources
157
+ -l --local -r --remote -b --both -B --bulk-threshold --clear-sources
158
+ -s --source -p --http-proxy
159
+ -h --help -V --verbose -q --quiet --silent --config-file
160
+ --backtrace --debug --norc
161
+ ].freeze,
162
+ 'server' => %w[
163
+ -h --help -V --verbose -q --quiet --silent --config-file
164
+ --backtrace --debug --norc
165
+ ].freeze,
166
+ 'signin' => %w[
167
+ --host --otp
168
+ -h --help -V --verbose -q --quiet --silent --config-file
169
+ --backtrace --debug --norc
170
+ ].freeze,
171
+ 'signout' => %w[
172
+ -h --help -V --verbose -q --quiet --silent --config-file
173
+ --backtrace --debug --norc
174
+ ].freeze,
175
+ 'sources' => %w[
176
+ -a --add --append --prepend -l --list -r --remove -c --clear-all
177
+ -u --update -f --force
178
+ -p --http-proxy
179
+ -h --help -V --verbose -q --quiet --silent --config-file
180
+ --backtrace --debug --norc
181
+ ].freeze,
182
+ 'specification' => %w[
183
+ -v --version --platform --prerelease --all --ruby --yaml --marshal
184
+ -u --update-sources
185
+ -l --local -r --remote -b --both -B --bulk-threshold --clear-sources
186
+ -s --source -p --http-proxy
187
+ -h --help -V --verbose -q --quiet --silent --config-file
188
+ --backtrace --debug --norc
189
+ ].freeze,
190
+ 'stale' => %w[
191
+ -h --help -V --verbose -q --quiet --silent --config-file
192
+ --backtrace --debug --norc
193
+ ].freeze,
194
+ 'uninstall' => %w[
195
+ -a --all -I --ignore-dependencies -D --check-development -x --executables
196
+ -i --install-dir -n --bindir --user-install --format-executable --force
197
+ --abort-on-dependent -v --version --platform --vendor
198
+ -h --help -V --verbose -q --quiet --silent --config-file
199
+ --backtrace --debug --norc
200
+ ].freeze,
201
+ 'unpack' => %w[
202
+ --target --spec -v --version
203
+ -P --trust-policy
204
+ -h --help -V --verbose -q --quiet --silent --config-file
205
+ --backtrace --debug --norc
206
+ ].freeze,
207
+ 'update' => %w[
208
+ --system --platform --prerelease
209
+ --default -u --update-sources
210
+ -i --install-dir -n --bindir -j --build-jobs --document --build-root
211
+ --vendor -N --no-document -E --env-shebang -f --force -w --wrappers
212
+ -P --trust-policy --ignore-dependencies --format-executable --user-install
213
+ --development --development-all --conservative --minimal-deps
214
+ --post-install-message -g --file --without --explain --lock
215
+ --suggestions --target-rbconfig
216
+ -l --local -r --remote -b --both -B --bulk-threshold --clear-sources
217
+ -s --source -p --http-proxy
218
+ -h --help -V --verbose -q --quiet --silent --config-file
219
+ --backtrace --debug --norc
220
+ ].freeze,
221
+ 'which' => %w[
222
+ -a --all -g --gems-first
223
+ -h --help -V --verbose -q --quiet --silent --config-file
224
+ --backtrace --debug --norc
225
+ ].freeze,
226
+ 'yank' => %w[
227
+ -v --version --platform --otp --host -k --key
228
+ -h --help -V --verbose -q --quiet --silent --config-file
229
+ --backtrace --debug --norc
230
+ ].freeze
231
+ }.freeze
232
+
233
+ GEM_NAME_SUBCOMMANDS = %w[
234
+ info uninstall open contents check cleanup pristine rdoc specification dependency
235
+ ].freeze
236
+
237
+ def supports?(command)
238
+ command.to_s.strip.split(/\s+/).first == 'gem'
239
+ end
240
+
241
+ def complete(command, word, args = [])
242
+ return [] unless supports?(command)
243
+
244
+ words = word.strip.split(/\s+/)
245
+ words.shift if words.first == 'gem'
246
+
247
+ has_trailing_space = word.end_with?(' ')
248
+
249
+ if words.empty?
250
+ return SUBCOMMANDS.dup
251
+ end
252
+
253
+ if words.size == 1 && !has_trailing_space
254
+ prefix = words.first
255
+ return SUBCOMMANDS.select { |s| s.start_with?(prefix) }
256
+ end
257
+
258
+ subcommand = words.first
259
+ return [] unless OPTIONS.key?(subcommand)
260
+
261
+ prefix = has_trailing_space ? '' : words.last
262
+
263
+ if prefix.start_with?('-')
264
+ OPTIONS[subcommand].select { |o| o.start_with?(prefix) }
265
+ elsif GEM_NAME_SUBCOMMANDS.include?(subcommand)
266
+ installed_gems.select { |g| g.start_with?(prefix) }
267
+ else
268
+ OPTIONS[subcommand].select { |o| o.start_with?(prefix) }
269
+ end
270
+ end
271
+
272
+ private
273
+
274
+ def installed_gems
275
+ Gem::Specification.map(&:name).uniq.sort
276
+ rescue
277
+ []
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shellwords'
4
+ require_relative 'engine'
5
+
6
+ module Yarsh
7
+ module Completion
8
+ class HelpCompletion < Engine
9
+ attr_reader :cache
10
+
11
+ def initialize
12
+ @cache = {}
13
+ end
14
+
15
+ def complete(command, word, args = [])
16
+ command = command.strip
17
+ return [] if command.nil? || command.empty?
18
+ cmd = command.split(/\s+/).first
19
+ return [] if cmd.nil? || cmd.empty?
20
+ return [] unless command_exists?(cmd)
21
+
22
+ # Skip privileged commands — running --help would prompt for password
23
+ return [] if PRIVILEGED_COMMANDS.include?(cmd)
24
+
25
+ # Use full word for parse_help only if it starts with the command
26
+ # (e.g., word="docker compose u" starts with command="docker")
27
+ help_cmd = if word.strip.start_with?(cmd)
28
+ word.strip
29
+ else
30
+ command
31
+ end
32
+ options = parse_help(help_cmd)
33
+ return [] if options.nil? || options.empty?
34
+
35
+ prefix = word.strip.split(/\s+/).last
36
+ return options if prefix == help_cmd || prefix == cmd
37
+
38
+ options.select { |opt| opt.start_with?(prefix) }
39
+ end
40
+
41
+ def supports?(command)
42
+ return false if command.nil? || command.empty?
43
+ cmd = command.strip.split(/\s+/).first
44
+ command_exists?(cmd)
45
+ end
46
+
47
+ def parse_help(command)
48
+ return @cache[command][:options] if @cache.key?(command) && !expired?(@cache[command])
49
+
50
+ options = do_parse_help(command)
51
+ @cache[command] = { options: options, fetched_at: Time.now.to_i }
52
+ options
53
+ rescue
54
+ []
55
+ end
56
+
57
+ def cache_info(command)
58
+ @cache[command]
59
+ end
60
+
61
+ def clear_cache
62
+ @cache.clear
63
+ end
64
+
65
+ private
66
+
67
+ def command_exists?(command)
68
+ cmd = command.split(/\s+/).first
69
+ !`which #{Shellwords.shellescape(cmd)} 2>/dev/null`.strip.empty?
70
+ rescue
71
+ false
72
+ end
73
+
74
+ def expired?(entry)
75
+ return true unless entry[:fetched_at]
76
+ (Time.now.to_i - entry[:fetched_at]) > CACHE_TTL
77
+ end
78
+
79
+ def do_parse_help(command)
80
+ escaped = command.split(/\s+/).map { |w| Shellwords.shellescape(w) }.join(' ')
81
+ output = `#{escaped} --help 2>&1`.strip
82
+ return [] if output.empty? || output.include?("command not found") || output.include?("No such file")
83
+
84
+ options = []
85
+ output.each_line do |line|
86
+ cmd_match = line.match(/^\s{2}([a-zA-Z][a-zA-Z0-9-]*)/)
87
+ options << cmd_match[1] if cmd_match && !options.include?(cmd_match[1])
88
+
89
+ line.strip!
90
+ next if line.empty?
91
+
92
+ matches = line.scan(/(?:^|[\s|\]\[])(-{1,2}[a-zA-Z][a-zA-Z0-9-]*)/)
93
+ matches.each do |m|
94
+ opt = m.first
95
+ next if opt.empty?
96
+ options << opt unless options.include?(opt)
97
+ end
98
+ end
99
+ options.uniq.sort
100
+ rescue
101
+ []
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'engine'
4
+
5
+ module Yarsh
6
+ module Completion
7
+ class Registry
8
+ def initialize
9
+ @engines = []
10
+ end
11
+
12
+ def register(engine)
13
+ @engines << engine
14
+ end
15
+
16
+ def complete(command, word, args = [])
17
+ return [] if command.nil? || command.empty?
18
+ return [] if word.nil?
19
+
20
+ @engines.each do |engine|
21
+ next unless engine.supports?(command)
22
+ result = engine.complete(command, word, args)
23
+ return result unless result.nil? || result.empty?
24
+ end
25
+
26
+ []
27
+ rescue
28
+ []
29
+ end
30
+
31
+ def find_engine(command)
32
+ @engines.each do |engine|
33
+ return engine if engine.supports?(command)
34
+ end
35
+ nil
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Yarsh
6
+ module Completion
7
+ BASE_DIR = '/usr/share/bash-completion/completions'
8
+ CACHE_DIR = File.expand_path('~/.yarsh/completion_cache')
9
+ CACHE_TTL = 3600
10
+
11
+ def self.cache_dir
12
+ FileUtils.mkdir_p(CACHE_DIR) unless File.exist?(CACHE_DIR)
13
+ CACHE_DIR
14
+ end
15
+ end
16
+ end
data/lib/yarsh/config.rb CHANGED
@@ -13,12 +13,14 @@ module Yarsh
13
13
  include Singleton
14
14
 
15
15
  attr_reader :aliases, :log_level
16
- attr_accessor :logger
16
+ attr_accessor :logger, :autocompletion, :ruby_completion
17
17
 
18
18
  def initialize
19
19
  @aliases = {}
20
20
  @prompt = nil
21
21
  @logger = Logger.new($stdout, level: Logger::UNKNOWN)
22
+ @autocompletion = true
23
+ @ruby_completion = true
22
24
  end
23
25
 
24
26
  def log_level=(level)
data/lib/yarsh/help.rb ADDED
@@ -0,0 +1,72 @@
1
+ module Yarsh
2
+ module Help
3
+ class Command
4
+ MINIMAL_WIDTH = 5
5
+
6
+ def initialize(screen_width, cursor_pos_to_render, autocomplete_dialog)
7
+ @x = cursor_pos_to_render.x + autocomplete_dialog.width + 1
8
+ @y = cursor_pos_to_render.y
9
+ @width = screen_width - @x
10
+ @whatis = nil
11
+ end
12
+
13
+ def info(command)
14
+ @info = `whatis #{command} 2>&1`
15
+ return false unless $?.success?
16
+
17
+ @info = @info.split('- ', 2).last.strip
18
+ rescue StandardError => e
19
+ debug("Error during whatis: #{e.message}")
20
+ false
21
+ end
22
+
23
+ def render_dialog
24
+ return nil unless info_and_width?
25
+
26
+ Reline::DialogRenderInfo.new(
27
+ contents: format_output,
28
+ pos: Reline::CursorPos.new(@x, @y - 1),
29
+ width: @width,
30
+ height: height + 1,
31
+ face: :completion_dialog
32
+ )
33
+ end
34
+
35
+ private
36
+
37
+ def format_output
38
+ return nil unless info_and_width?
39
+
40
+ dialog_line = []
41
+ line_size = 0
42
+ dialog_content = []
43
+ words = @info.split(' ')
44
+ words.each_with_index do |word, i|
45
+ dialog_line << word
46
+ line_size += word.size + 1
47
+ if i + 1 == words.size
48
+ if line_size >= @width
49
+ dialog_content << dialog_line[0..-2].join(' ').strip << dialog_line.last
50
+ next
51
+ end
52
+ dialog_content << dialog_line.join(' ').strip && next
53
+ end
54
+ next unless line_size >= @width
55
+
56
+ dialog_content << dialog_line[0..-2].join(' ').strip
57
+ line_size = dialog_line.last.size
58
+ dialog_line = [dialog_line.last]
59
+ end
60
+ dialog_content
61
+ end
62
+
63
+ def height
64
+ @height ||= !info_and_width? ? nil : @info.size / @width
65
+ end
66
+
67
+ def info_and_width?
68
+ @info && @width && @width > MINIMAL_WIDTH
69
+ end
70
+ end
71
+ end
72
+ end
@@ -8,6 +8,29 @@ module Yarsh
8
8
  Config.instance.add_alias(new_name, command)
9
9
  end
10
10
 
11
+ # Executes a shell line from inside a multiline Ruby buffer. Shell lines
12
+ # are rewritten to __sh__(...) by Yarsh::Multiline.transform.
13
+ def __sh__(command)
14
+ shell = Yarsh.current_shell
15
+ raise Error, 'No active Yarsh shell' if shell.nil?
16
+
17
+ shell.send(:__sh_exec, command)
18
+ end
19
+
20
+ def __yarsh__(exp, binding = nil)
21
+ shell = Yarsh.current_shell
22
+ raise Error, 'No active Yarsh shell' if shell.nil?
23
+
24
+ current_bind = nil
25
+ if binding
26
+ current_bind = shell.bind
27
+ shell.bind = binding
28
+ end
29
+ result = shell.send(:yarsh_eval, exp)
30
+ shell.bind = current_bind if binding
31
+ result
32
+ end
33
+
11
34
  def source(filename)
12
35
  load(filename, Yarsh::InstanceMethods)
13
36
  end