yarsh 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/AGENTS.md +13 -0
- data/CHANGES.md +9 -0
- data/README.md +15 -1
- data/lib/yarsh/binding.rb +4 -0
- data/lib/yarsh/completion/bash_autocomplete.rb +139 -0
- data/lib/yarsh/completion/engine.rb +17 -0
- data/lib/yarsh/completion/gem_completion.rb +281 -0
- data/lib/yarsh/completion/help_completion.rb +105 -0
- data/lib/yarsh/completion/registry.rb +39 -0
- data/lib/yarsh/completion.rb +16 -0
- data/lib/yarsh/config.rb +3 -1
- data/lib/yarsh/help.rb +72 -0
- data/lib/yarsh/shell.rb +160 -15
- data/lib/yarsh/version.rb +1 -1
- data/lib/yarsh.rb +10 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2abe40eb2be33ef53eba4b99743447c9db93831dd1e8c00221b1934cd5bc264
|
|
4
|
+
data.tar.gz: 3f53d3e1a4a560b7c04982a7874899d8be8aaba714a3cc42dc4ecff6adae99b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d89b9258f28f13778ad03734cd98db71e09f0b55132631fb2e054ec37286cde8ba688db5dede69835f1ca2ce51fd2cce4a9759707ef5df27cfe8218e6305f6c3
|
|
7
|
+
data.tar.gz: 7ca6c547b1eb3aac0c4c8c1582e42b71007fe2181877aa2d74379c24a175ff48d6307dc978c13a752d4ca054e69a40fc94ee0f4c661e0ca4ed571e82653926de
|
data/AGENTS.md
CHANGED
|
@@ -139,6 +139,19 @@ error crashes the REPL:
|
|
|
139
139
|
`yarsh_eval` deciding. `__sh__`/`__sh_exec` still exist for potential
|
|
140
140
|
re-enabling.
|
|
141
141
|
|
|
142
|
+
## Bash autocomplete
|
|
143
|
+
|
|
144
|
+
`Yarsh::Completion::BashAutocomplete` provides tab-completion by invoking bash
|
|
145
|
+
completion functions:
|
|
146
|
+
|
|
147
|
+
- For commands supporting the `__complete` subcommand (docker, kubectl, etc.),
|
|
148
|
+
it calls `<command> __complete <word>` and parses tab-separated output.
|
|
149
|
+
- For all other commands, it sources the bash completion script and runs the
|
|
150
|
+
registered completion function with `COMP_WORDS`/`COMP_CWORD` set.
|
|
151
|
+
- Privileged commands (`sudo`, `doas`, `pkexec`, `su`) skip the `__complete`
|
|
152
|
+
path to avoid password prompts; they always use the bash completion function.
|
|
153
|
+
- Results are cached in a bounded in-memory `CACHE` (max 100 entries).
|
|
154
|
+
|
|
142
155
|
## Style
|
|
143
156
|
|
|
144
157
|
- `# frozen_string_literal: true` on every `.rb` file.
|
data/CHANGES.md
CHANGED
|
@@ -200,4 +200,13 @@ See commits: `bcc25b4`, `ea92742`, `51c9db8`, `2218f29`, `0fceeb8`. Tests in
|
|
|
200
200
|
transformation, heredoc handling, binding threading, integration with
|
|
201
201
|
`Yarsh::Shell`).
|
|
202
202
|
|
|
203
|
+
## 0.3.0
|
|
203
204
|
|
|
205
|
+
A completion engine for writing shell commands complete in ruby. It comes with a Shell completion engine using bash autocomplete when available.
|
|
206
|
+
|
|
207
|
+
It alos comes with completion for the gem command.
|
|
208
|
+
|
|
209
|
+
## 0.3.1
|
|
210
|
+
|
|
211
|
+
* Small bug fix for the REPL.
|
|
212
|
+
* Added the export reserved word to define global variables in the shell.
|
data/README.md
CHANGED
|
@@ -107,7 +107,21 @@ Known limitations:
|
|
|
107
107
|
|
|
108
108
|
### Completion
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
Tab completion is powered by a multi-engine system:
|
|
111
|
+
|
|
112
|
+
- **Command completion** — completes executables in PATH and shell builtins
|
|
113
|
+
- **Path completion** — completes file and directory paths (including `~` expansion)
|
|
114
|
+
- **Ruby completion** — completes Ruby methods and constants (can be disabled via `Config.instance.ruby_completion = false`)
|
|
115
|
+
- **Bash completion** — invokes bash completion functions for commands that have them (e.g. `git`, `docker`, `systemctl`), including multi-word commands like `systemctl restart <service>`
|
|
116
|
+
- **Gem completion** — provides subcommand and option completion for `gem`, plus installed gem names for subcommands that operate on installed gems (`info`, `uninstall`, `open`, `contents`, `check`, `cleanup`, `pristine`, `rdoc`, `specification`, `dependency`)
|
|
117
|
+
|
|
118
|
+
Completion is suppressed when the cursor is inside a quoted string, so typing
|
|
119
|
+
`git commit -m ""` won't have options inserted between the quotes.
|
|
120
|
+
|
|
121
|
+
Privileged commands (`sudo`, `doas`, `pkexec`, `su`) are handled transparently:
|
|
122
|
+
`sudo systemctl restart <Tab>` completes the same as `systemctl restart <Tab>`.
|
|
123
|
+
|
|
124
|
+
A base completion engine is provided in order to add more completions with ease. See `lib/yarsh/completion/base.rb` for more information and `lib/yarsh/completion/gem.rb` for an example of a completion for the gem command.
|
|
111
125
|
|
|
112
126
|
### Errors
|
|
113
127
|
|
data/lib/yarsh/binding.rb
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
require_relative 'engine'
|
|
6
|
+
|
|
7
|
+
module Yarsh
|
|
8
|
+
module Completion
|
|
9
|
+
class BashAutocomplete < Engine
|
|
10
|
+
BASH_COMPLETION_MAIN = '/usr/share/bash-completion/bash_completion'
|
|
11
|
+
COMPLETION_DIR = '/usr/share/bash-completion/completions'
|
|
12
|
+
CACHE = {}
|
|
13
|
+
|
|
14
|
+
def supports?(command)
|
|
15
|
+
return false if command.nil? || command.empty?
|
|
16
|
+
command = command.strip.split(/\s+/).first
|
|
17
|
+
File.exist?("#{COMPLETION_DIR}/#{command}") ||
|
|
18
|
+
File.exist?("#{COMPLETION_DIR}/#{command}.bash") ||
|
|
19
|
+
bash_completion_defined?(command)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def complete(command, word, args = [])
|
|
23
|
+
command = command.strip.split(/\s+/).first
|
|
24
|
+
return [] unless supports?(command)
|
|
25
|
+
return [] unless find_script(command)
|
|
26
|
+
|
|
27
|
+
# Try __complete subcommand first (works for docker and others)
|
|
28
|
+
sub_output = bash_invoke_complete_subcommand(command, word)
|
|
29
|
+
return sub_output unless sub_output.nil? || sub_output.empty?
|
|
30
|
+
|
|
31
|
+
# Fall back to bash completion function
|
|
32
|
+
func = completion_function(command)
|
|
33
|
+
return [] if func.nil? || func.empty?
|
|
34
|
+
|
|
35
|
+
output = bash_invoke_completion(func, command, word)
|
|
36
|
+
return [] if output.nil? || output.empty?
|
|
37
|
+
|
|
38
|
+
has_trailing_space = word.end_with?(' ')
|
|
39
|
+
prefix = has_trailing_space ? '' : word.strip.split(/\s+/).last
|
|
40
|
+
return output.split if prefix == command
|
|
41
|
+
|
|
42
|
+
output.split.select { |r| r.start_with?(prefix) }
|
|
43
|
+
rescue
|
|
44
|
+
[]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def find_script(command)
|
|
50
|
+
return "#{COMPLETION_DIR}/#{command}" if File.exist?("#{COMPLETION_DIR}/#{command}")
|
|
51
|
+
return "#{COMPLETION_DIR}/#{command}.bash" if File.exist?("#{COMPLETION_DIR}/#{command}.bash")
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def bash_completion_defined?(command)
|
|
56
|
+
func = completion_function(command)
|
|
57
|
+
!func.nil? && !func.empty?
|
|
58
|
+
rescue
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def completion_function(command)
|
|
63
|
+
return '' unless File.exist?(BASH_COMPLETION_MAIN)
|
|
64
|
+
script = find_script(command)
|
|
65
|
+
return '' unless script
|
|
66
|
+
|
|
67
|
+
`bash -c 'source #{Shellwords.shellescape(BASH_COMPLETION_MAIN)} 2>/dev/null; source #{Shellwords.shellescape(script)} 2>/dev/null; complete -p #{Shellwords.shellescape(command)} 2>/dev/null | grep -oP "(?<= -F )\\S+"' 2>/dev/null`.strip
|
|
68
|
+
rescue
|
|
69
|
+
''
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def bash_invoke_completion(func, command, partial)
|
|
73
|
+
has_trailing_space = partial.end_with?(' ')
|
|
74
|
+
words = if has_trailing_space
|
|
75
|
+
partial.strip.split(/\s+/) << ''
|
|
76
|
+
else
|
|
77
|
+
partial.strip.split(/\s+/)
|
|
78
|
+
end
|
|
79
|
+
cword = [words.size - 1, 0].max
|
|
80
|
+
comp_words = words.map { |w| Shellwords.shellescape(w) }.join(' ')
|
|
81
|
+
comp_words += ' ""' unless has_trailing_space
|
|
82
|
+
script = Shellwords.shellescape(find_script(command))
|
|
83
|
+
cmd = "source #{Shellwords.shellescape(BASH_COMPLETION_MAIN)} 2>/dev/null; source #{script} 2>/dev/null; COMP_WORDS=(#{comp_words}); COMP_CWORD=#{cword}; COMPREPLY=(); #{Shellwords.shellescape(func)} 2>/dev/null; IFS=; printf '%s\\n' \"\${COMPREPLY[@]}\""
|
|
84
|
+
return CACHE[cmd] if CACHE.key?(cmd)
|
|
85
|
+
raw = nil
|
|
86
|
+
Tempfile.create('yarsh_completion') do |f|
|
|
87
|
+
f.write(cmd)
|
|
88
|
+
f.close
|
|
89
|
+
raw = `timeout 4 bash #{f.path} 2>/dev/null`.strip
|
|
90
|
+
end
|
|
91
|
+
CACHE[cmd] = raw
|
|
92
|
+
CACHE.delete_if { |_, v| v.nil? || v.empty? } if CACHE.size > 100
|
|
93
|
+
raw
|
|
94
|
+
rescue
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def bash_invoke_complete_subcommand(command, partial)
|
|
99
|
+
has_trailing_space = partial.end_with?(' ')
|
|
100
|
+
words = partial.strip.split(/\s+/)
|
|
101
|
+
return nil if words.size > 2
|
|
102
|
+
|
|
103
|
+
# Don't invoke __complete for privileged commands (prompts for password)
|
|
104
|
+
return nil if PRIVILEGED_COMMANDS.include?(command)
|
|
105
|
+
|
|
106
|
+
# Don't invoke __complete when only the command + space is typed (no arg yet)
|
|
107
|
+
return nil if has_trailing_space && words.size == 1
|
|
108
|
+
|
|
109
|
+
last_word = has_trailing_space ? '' : (words.last || '')
|
|
110
|
+
raw = `timeout 4 #{Shellwords.shellescape(command)} __complete #{Shellwords.shellescape(last_word)} 2>/dev/null`.strip
|
|
111
|
+
return nil if raw.empty?
|
|
112
|
+
|
|
113
|
+
# Filter to only lines that contain tab-separated data
|
|
114
|
+
tab_lines = raw.each_line.select { |line| line.include?("\t") }
|
|
115
|
+
return nil if tab_lines.empty?
|
|
116
|
+
|
|
117
|
+
results = tab_lines.map { |line| line.split("\t").first }.reject(&:empty?)
|
|
118
|
+
return nil if results.empty?
|
|
119
|
+
|
|
120
|
+
# If the only result is the last_word itself, try subcommand completion
|
|
121
|
+
if results.size == 1 && results.first == last_word
|
|
122
|
+
sub_cmd = words[1..-1].join(' ')
|
|
123
|
+
if sub_cmd && !sub_cmd.empty?
|
|
124
|
+
sub_raw = `timeout 4 #{Shellwords.shellescape(command)} #{Shellwords.shellescape(sub_cmd)} __complete 2>/dev/null`.strip
|
|
125
|
+
return nil if sub_raw.empty? || !sub_raw.include?("\t")
|
|
126
|
+
sub_tab_lines = sub_raw.each_line.select { |line| line.include?("\t") }
|
|
127
|
+
return nil if sub_tab_lines.empty?
|
|
128
|
+
sub_results = sub_tab_lines.map { |line| line.split("\t").first }.reject(&:empty?)
|
|
129
|
+
return nil if sub_results.empty?
|
|
130
|
+
results = sub_results
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
results
|
|
134
|
+
rescue
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yarsh
|
|
4
|
+
module Completion
|
|
5
|
+
class Engine
|
|
6
|
+
PRIVILEGED_COMMANDS = %w[sudo doas pkexec su].freeze
|
|
7
|
+
|
|
8
|
+
def complete(command, word, args = [])
|
|
9
|
+
raise NotImplementedError, "Subclasses must implement #complete"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def supports?(command)
|
|
13
|
+
false
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -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
|
data/lib/yarsh/shell.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
3
5
|
module Yarsh
|
|
4
6
|
class Shell
|
|
5
7
|
include Yarsh
|
|
@@ -12,6 +14,8 @@ module Yarsh
|
|
|
12
14
|
local logout mapfile popd printf pushd pwd read readonly
|
|
13
15
|
return set shift shopt source suspend test times trap true
|
|
14
16
|
type typeset ulimit umask unalias unset wait].freeze
|
|
17
|
+
EXPORT = 'export'
|
|
18
|
+
DO_NOT_PRINT_RETURN_VALUE = :do_not_print_return_value80y0327y47123
|
|
15
19
|
|
|
16
20
|
def initialize(bind)
|
|
17
21
|
@bind = bind
|
|
@@ -19,10 +23,14 @@ module Yarsh
|
|
|
19
23
|
@history_file = File.expand_path(HISTORY_FILE)
|
|
20
24
|
@config_file = File.expand_path(CONFIG_FILE)
|
|
21
25
|
@cf = Yarsh::AnsiColorFormatter.new
|
|
26
|
+
@typing = false
|
|
22
27
|
Yarsh.current_shell = self
|
|
28
|
+
setup_completion_registry
|
|
23
29
|
setup_reline_history
|
|
24
30
|
setup_reline_completion
|
|
25
31
|
setup_reline_prompt
|
|
32
|
+
setup_output_modifier_proc
|
|
33
|
+
setup_completion_dialog
|
|
26
34
|
end
|
|
27
35
|
|
|
28
36
|
def shell
|
|
@@ -52,26 +60,49 @@ module Yarsh
|
|
|
52
60
|
puts @cf.fg_red("Error: #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}")
|
|
53
61
|
end
|
|
54
62
|
end
|
|
63
|
+
ensure
|
|
64
|
+
Config.instance.logger.close
|
|
55
65
|
end
|
|
56
66
|
|
|
57
67
|
private
|
|
58
68
|
|
|
59
69
|
def format_output(object)
|
|
60
|
-
pp object
|
|
70
|
+
pp object unless object == DO_NOT_PRINT_RETURN_VALUE
|
|
61
71
|
end
|
|
62
72
|
alias fp format_output
|
|
63
73
|
|
|
64
74
|
def read_input
|
|
65
|
-
@
|
|
75
|
+
@typing = true
|
|
76
|
+
input = @core.readmultiline(Config.instance.prompt, true) do |buffer|
|
|
66
77
|
!incomplete_line?(buffer) || buffer.match?(/\n[ \t]*\n\z/)
|
|
67
78
|
end
|
|
79
|
+
@typing = false
|
|
80
|
+
input
|
|
68
81
|
end
|
|
69
82
|
|
|
70
83
|
# A line is "incomplete" (multiline continues) only when it is not a
|
|
71
84
|
# shell command: `cd /tmp`, `grep foo /etc/passwd` etc. fail to compile
|
|
72
85
|
# as Ruby (unterminated regexp) but must stay single-line.
|
|
73
86
|
def incomplete_line?(line)
|
|
74
|
-
|
|
87
|
+
return true if shell_command?(line) && unclosed_quote?(line)
|
|
88
|
+
|
|
89
|
+
Yarsh::Multiline.incomplete?(line) && !shell_command?(line) && !line&.start_with?('cd')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def unclosed_quote?(line)
|
|
93
|
+
return false if line.nil?
|
|
94
|
+
|
|
95
|
+
double_quotes = line.scan(/(?<!\\)"/).size
|
|
96
|
+
single_quotes = line.scan(/(?<!\\)'/).size
|
|
97
|
+
double_quotes.odd? || single_quotes.odd?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def inside_quote?(pre)
|
|
101
|
+
return false if pre.nil? || pre.empty?
|
|
102
|
+
|
|
103
|
+
double_quotes = pre.scan(/(?<!\\)"/).size
|
|
104
|
+
single_quotes = pre.scan(/(?<!\\)'/).size
|
|
105
|
+
double_quotes.odd? || single_quotes.odd?
|
|
75
106
|
end
|
|
76
107
|
|
|
77
108
|
def execute_multiline(input)
|
|
@@ -89,7 +120,17 @@ module Yarsh
|
|
|
89
120
|
|
|
90
121
|
if input == 'cd' || input.start_with?('cd ')
|
|
91
122
|
cd(input.split(' ', 2)[1])
|
|
92
|
-
return
|
|
123
|
+
return DO_NOT_PRINT_RETURN_VALUE
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
if input.start_with?("#{EXPORT} ")
|
|
127
|
+
assignment = input[EXPORT.length + 1..].strip.split('=', 2)
|
|
128
|
+
if assignment.size == 2
|
|
129
|
+
ENV[assignment[0]] = assignment[1].gsub(/\A['"]|['"]\z/, '') # Remove surrounding quotes if present
|
|
130
|
+
return DO_NOT_PRINT_RETURN_VALUE
|
|
131
|
+
end
|
|
132
|
+
puts @cf.fg_red("Error: Invalid export syntax. Use 'export VAR=value'")
|
|
133
|
+
return DO_NOT_PRINT_RETURN_VALUE
|
|
93
134
|
end
|
|
94
135
|
|
|
95
136
|
begin
|
|
@@ -104,11 +145,13 @@ module Yarsh
|
|
|
104
145
|
begin
|
|
105
146
|
input = Yarsh.expand_aliases(input, Config.instance.aliases)
|
|
106
147
|
execute(input)
|
|
148
|
+
DO_NOT_PRINT_RETURN_VALUE
|
|
107
149
|
rescue Exception => e
|
|
108
150
|
$sher = e
|
|
109
151
|
info("#{e.class}: #{e.message}")
|
|
110
152
|
puts "#{@cf.bold.underline.fg_blue('Shell:')} '#{input}': #{e.message}"
|
|
111
153
|
puts "#{@cf.bold.underline.fg_red('Ruby:')} '#{input}': #{rb_error.message}"
|
|
154
|
+
DO_NOT_PRINT_RETURN_VALUE
|
|
112
155
|
end
|
|
113
156
|
end
|
|
114
157
|
end
|
|
@@ -137,6 +180,7 @@ module Yarsh
|
|
|
137
180
|
system(cmd, exception: true)
|
|
138
181
|
end
|
|
139
182
|
|
|
183
|
+
# TODO: Use exec_in_path?
|
|
140
184
|
def shell_command?(input)
|
|
141
185
|
first_token = input.strip.split(/\s+/, 2).first
|
|
142
186
|
return true if shell_path?(first_token)
|
|
@@ -147,7 +191,39 @@ module Yarsh
|
|
|
147
191
|
end
|
|
148
192
|
end
|
|
149
193
|
|
|
194
|
+
def exec_in_path?(input)
|
|
195
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).any? do |dir|
|
|
196
|
+
File.executable?(File.join(dir, input))
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
150
200
|
def complete_command(word)
|
|
201
|
+
registry = @completion_registry
|
|
202
|
+
return [] if word.nil?
|
|
203
|
+
|
|
204
|
+
if word.empty?
|
|
205
|
+
return ENV['PATH'].split(File::PATH_SEPARATOR)
|
|
206
|
+
.flat_map { |dir| Dir["#{dir}/*"] }
|
|
207
|
+
.map { |f| File.basename(f) }
|
|
208
|
+
.uniq
|
|
209
|
+
.sort
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
cmd = word.strip.split(/\s+/).first
|
|
213
|
+
return [] if cmd.nil? || cmd.empty?
|
|
214
|
+
|
|
215
|
+
# Strip privileged prefix (sudo, doas, etc.) — complete the real command
|
|
216
|
+
if Yarsh::Completion::Engine::PRIVILEGED_COMMANDS.include?(cmd) && cmd != word
|
|
217
|
+
remaining = word.sub(/\A#{Regexp.escape(cmd)}\s+/, '')
|
|
218
|
+
return complete_command(remaining)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
completions = registry.complete(cmd, word)
|
|
222
|
+
return completions unless completions.empty?
|
|
223
|
+
|
|
224
|
+
builtins = SHELL_BUILTINS.select { |b| b.start_with?(word) }
|
|
225
|
+
return builtins unless builtins.empty?
|
|
226
|
+
|
|
151
227
|
ENV['PATH'].split(File::PATH_SEPARATOR)
|
|
152
228
|
.flat_map { |dir| Dir["#{dir}/#{word}*"] }
|
|
153
229
|
.map { |f| File.basename(f) }
|
|
@@ -155,6 +231,62 @@ module Yarsh
|
|
|
155
231
|
.sort
|
|
156
232
|
end
|
|
157
233
|
|
|
234
|
+
def setup_completion_registry
|
|
235
|
+
@completion_registry = Yarsh::Completion::Registry.new
|
|
236
|
+
@completion_registry.register(Yarsh::Completion::BashAutocomplete.new)
|
|
237
|
+
@completion_registry.register(Yarsh::Completion::GemCompletion.new)
|
|
238
|
+
@completion_registry.register(Yarsh::Completion::HelpCompletion.new)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def complete_ruby(word)
|
|
242
|
+
IRB::InputCompletor.retrieve_completion_data(word, bind: @bind)
|
|
243
|
+
rescue StandardError
|
|
244
|
+
[]
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def setup_output_modifier_proc
|
|
248
|
+
@core.output_modifier_proc = proc do |data|
|
|
249
|
+
if @typing
|
|
250
|
+
IRB::Color.colorize_code(data)
|
|
251
|
+
else
|
|
252
|
+
data
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def setup_completion_dialog
|
|
258
|
+
Reline.add_dialog_proc(:docs, proc {
|
|
259
|
+
begin
|
|
260
|
+
dialog.trap_key = nil
|
|
261
|
+
|
|
262
|
+
next nil if just_cursor_moving && completion_journey_data.nil?
|
|
263
|
+
|
|
264
|
+
cursor_pos_to_render, result, pointer, autocomplete_dialog = context.pop(4)
|
|
265
|
+
next nil if result.nil? || pointer.nil? || pointer.negative?
|
|
266
|
+
|
|
267
|
+
selection = result[pointer]
|
|
268
|
+
help_command = Help::Command.new(screen_width, cursor_pos_to_render, autocomplete_dialog)
|
|
269
|
+
next nil unless help_command.info(result[pointer])
|
|
270
|
+
|
|
271
|
+
dialog.trap_key = Yarsh::ALT_D_SEQUENCES
|
|
272
|
+
|
|
273
|
+
if key.match?(dialog.name)
|
|
274
|
+
begin
|
|
275
|
+
print "\e[?1049h"
|
|
276
|
+
system(*Shellwords.shellsplit("man #{selection}"))
|
|
277
|
+
ensure
|
|
278
|
+
print "\e[?1049l"
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
help_command.render_dialog
|
|
282
|
+
rescue StandardError => e
|
|
283
|
+
extend Yarsh::InstanceMethods
|
|
284
|
+
debug("Error occurend for add help dialog: #{e.message}")
|
|
285
|
+
nil
|
|
286
|
+
end
|
|
287
|
+
}, Reline::DEFAULT_DIALOG_CONTEXT)
|
|
288
|
+
end
|
|
289
|
+
|
|
158
290
|
def complete_path(word)
|
|
159
291
|
word = './' if word.empty?
|
|
160
292
|
glob_word = word.start_with?('~') ? File.expand_path(word) : word
|
|
@@ -199,17 +331,30 @@ module Yarsh
|
|
|
199
331
|
end
|
|
200
332
|
|
|
201
333
|
def setup_reline_completion
|
|
202
|
-
Reline.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
334
|
+
Reline.autocompletion = Config.instance.autocompletion
|
|
335
|
+
Reline.completion_append_character = ' '
|
|
336
|
+
Reline.core.config.keyseq_timeout = 0.5
|
|
337
|
+
Reline.completion_proc = proc { |target, pre, _post|
|
|
338
|
+
next [] if inside_quote?(pre) || pre =~ /'|"\z/
|
|
339
|
+
|
|
340
|
+
full = pre.empty? ? target : pre.rstrip + ' ' + target
|
|
341
|
+
first_word = full.strip.split(/\s+/).first.to_s
|
|
342
|
+
ruby_completions = if !Config.instance.ruby_completion ||
|
|
343
|
+
Yarsh::Completion::Engine::PRIVILEGED_COMMANDS.include?(first_word)
|
|
344
|
+
[]
|
|
345
|
+
else
|
|
346
|
+
complete_ruby(target).compact
|
|
347
|
+
end
|
|
348
|
+
ruby_completions +
|
|
349
|
+
if target.match?(%r{\A\.\.?(/|\z)}) || target.start_with?('/', '~') || target.include?('/')
|
|
350
|
+
complete_path(target)
|
|
351
|
+
elsif target.empty? && pre.empty?
|
|
352
|
+
complete_command(target)
|
|
353
|
+
elsif pre.end_with?(' ')
|
|
354
|
+
complete_command(full)
|
|
355
|
+
else
|
|
356
|
+
complete_command(full)
|
|
357
|
+
end
|
|
213
358
|
}
|
|
214
359
|
end
|
|
215
360
|
|
data/lib/yarsh/version.rb
CHANGED
data/lib/yarsh.rb
CHANGED
|
@@ -4,6 +4,9 @@ require 'reline'
|
|
|
4
4
|
require 'open3'
|
|
5
5
|
require 'singleton'
|
|
6
6
|
require 'logger'
|
|
7
|
+
require 'irb/completion'
|
|
8
|
+
require_relative 'yarsh/completion/gem_completion'
|
|
9
|
+
require 'irb/color'
|
|
7
10
|
require 'ripper'
|
|
8
11
|
require 'prism'
|
|
9
12
|
|
|
@@ -19,6 +22,13 @@ module Yarsh
|
|
|
19
22
|
YARSH_DIR = '~/.yarsh'
|
|
20
23
|
HISTORY_FILE = File.join Yarsh::YARSH_DIR, 'history'
|
|
21
24
|
CONFIG_FILE = File.join Yarsh::YARSH_DIR, 'config'
|
|
25
|
+
ALT_D_SEQUENCES = [
|
|
26
|
+
[27, 100], # Normal Alt+d when convert-meta isn't used.
|
|
27
|
+
# When option/alt is not configured as a meta key in terminal emulator,
|
|
28
|
+
# option/alt + d will send a unicode character depend on OS keyboard setting.
|
|
29
|
+
[195, 164], # "ä" in somewhere (FIXME: environment information is unknown).
|
|
30
|
+
[226, 136, 130] # "∂" Alt+d on Mac keyboard.
|
|
31
|
+
].freeze
|
|
22
32
|
|
|
23
33
|
class ExecOutput < String
|
|
24
34
|
def initialize(string)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yarsh
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- yarsh4all
|
|
@@ -45,7 +45,14 @@ files:
|
|
|
45
45
|
- lib/yarsh.rb
|
|
46
46
|
- lib/yarsh/ansi_color_formatter.rb
|
|
47
47
|
- lib/yarsh/binding.rb
|
|
48
|
+
- lib/yarsh/completion.rb
|
|
49
|
+
- lib/yarsh/completion/bash_autocomplete.rb
|
|
50
|
+
- lib/yarsh/completion/engine.rb
|
|
51
|
+
- lib/yarsh/completion/gem_completion.rb
|
|
52
|
+
- lib/yarsh/completion/help_completion.rb
|
|
53
|
+
- lib/yarsh/completion/registry.rb
|
|
48
54
|
- lib/yarsh/config.rb
|
|
55
|
+
- lib/yarsh/help.rb
|
|
49
56
|
- lib/yarsh/instance_methods.rb
|
|
50
57
|
- lib/yarsh/multiline.rb
|
|
51
58
|
- lib/yarsh/prompt.rb
|