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.
- checksums.yaml +4 -4
- data/AGENTS.md +105 -20
- data/CHANGES.md +101 -12
- data/README.md +66 -2
- data/lib/yarsh/binding.rb +14 -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/instance_methods.rb +23 -0
- data/lib/yarsh/multiline.rb +147 -0
- data/lib/yarsh/prompt.rb +9 -2
- data/lib/yarsh/shell.rb +243 -50
- data/lib/yarsh/version.rb +1 -1
- data/lib/yarsh.rb +22 -2
- metadata +10 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yarsh
|
|
4
|
+
# Multiline input support: decides when a line is syntactically incomplete
|
|
5
|
+
# (keep reading) and transforms a finished buffer so shell lines can live
|
|
6
|
+
# inside Ruby blocks/defs/classes: shell lines are rewritten into __sh__ calls.
|
|
7
|
+
module Multiline
|
|
8
|
+
INCOMPLETE_MESSAGES = [
|
|
9
|
+
/unexpected end-of-input/,
|
|
10
|
+
/unterminated string/,
|
|
11
|
+
/unterminated heredoc/,
|
|
12
|
+
/unterminated regexp/,
|
|
13
|
+
/can't find string ".*" anywhere before EOF/,
|
|
14
|
+
/expected a `when` or `in` clause after `case`/,
|
|
15
|
+
/expected an `end` to close the/
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
# Keywords that must never be treated as shell, even though some of them
|
|
19
|
+
# (break, next, return, times, ...) are also bash builtins.
|
|
20
|
+
STRUCTURAL_KEYWORDS = %w[do end if else elsif when rescue ensure then begin
|
|
21
|
+
case class def module while until for unless
|
|
22
|
+
return break next yield].freeze
|
|
23
|
+
KEYWORD_REGEX = Regexp.new("\\A(?:#{STRUCTURAL_KEYWORDS.join('|')})\\b")
|
|
24
|
+
|
|
25
|
+
class IdentifierSurround
|
|
26
|
+
def initialize
|
|
27
|
+
@local_vars = {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# detect if the line assign a new local variable
|
|
31
|
+
# if yes, save it's name
|
|
32
|
+
def save_local_vars(line)
|
|
33
|
+
tokens = Prism.lex(line).value.map { |token, _| token }
|
|
34
|
+
return unless tokens.any? { |token| token.type == :EQUAL }
|
|
35
|
+
|
|
36
|
+
tokens
|
|
37
|
+
.take_while { |token| token.type != :EQUAL }
|
|
38
|
+
.filter { |token| token.type == :IDENTIFIER }
|
|
39
|
+
.each do |token|
|
|
40
|
+
@local_vars[token.value] =
|
|
41
|
+
token
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# surroud the local variable with 'bind.eval()'
|
|
46
|
+
# a local variable is an identifier who was registred
|
|
47
|
+
# in a precedent line
|
|
48
|
+
def surrond_with_bind(line, bind_name)
|
|
49
|
+
Prism.lex(line).value
|
|
50
|
+
.map { |token, _| token }
|
|
51
|
+
.filter { |token| token.type == :IDENTIFIER }
|
|
52
|
+
.each do |token|
|
|
53
|
+
next unless @local_vars[token.value]
|
|
54
|
+
|
|
55
|
+
location = token.location
|
|
56
|
+
line[location.start_column..(location.end_column - 1)] =
|
|
57
|
+
"__yarsh__(\"#{token.value}\", #{bind_name})"
|
|
58
|
+
end
|
|
59
|
+
line
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.incomplete?(src)
|
|
64
|
+
RubyVM::InstructionSequence.compile(src)
|
|
65
|
+
false
|
|
66
|
+
rescue SyntaxError => e
|
|
67
|
+
INCOMPLETE_MESSAGES.any? { |re| e.message =~ re }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# :ruby - keep the line verbatim
|
|
71
|
+
# :shell - rewrite the line into a __sh__ call
|
|
72
|
+
# :skip - drop the line (blank)
|
|
73
|
+
def self.classify_line(line)
|
|
74
|
+
stripped = line.strip
|
|
75
|
+
return :skip if stripped.empty?
|
|
76
|
+
# return :ruby if structural?(stripped)
|
|
77
|
+
# return :shell if stripped.include?('-->') #FIXME: Should not be treated as a shell --> should allow multiline too
|
|
78
|
+
# return :shell if shell_command && shell_command.call(stripped) #FIXME: pwd = Dir.pwd is evaluated as a shell command instead of a ruby
|
|
79
|
+
# return :ruby if incomplete?(stripped)
|
|
80
|
+
# return :ruby if compiles?(stripped)
|
|
81
|
+
# #TODO: check what happend when it do not compile with another error message catched by incomplete? ?
|
|
82
|
+
|
|
83
|
+
# :shell
|
|
84
|
+
return :structural if structural?(stripped)
|
|
85
|
+
|
|
86
|
+
# return :shell if !compiles?(stripped) && shell_command&.call(stripped)
|
|
87
|
+
|
|
88
|
+
:yarsh
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.transform(buffer, &shell_command)
|
|
92
|
+
heredoc_delim = nil
|
|
93
|
+
is_structural = false
|
|
94
|
+
print_binding = 'bind = binding'
|
|
95
|
+
ident_surround = IdentifierSurround.new
|
|
96
|
+
buffer.split("\n").map do |line|
|
|
97
|
+
if heredoc_delim
|
|
98
|
+
heredoc_delim = nil if line.strip == heredoc_delim
|
|
99
|
+
next line
|
|
100
|
+
end
|
|
101
|
+
if (delim = heredoc_delimiter(line))
|
|
102
|
+
heredoc_delim = delim
|
|
103
|
+
next line
|
|
104
|
+
end
|
|
105
|
+
case classify_line(line, &shell_command)
|
|
106
|
+
when :skip then nil
|
|
107
|
+
when :structural
|
|
108
|
+
next line if line.include? 'end'
|
|
109
|
+
|
|
110
|
+
if is_structural
|
|
111
|
+
line = ident_surround.surrond_with_bind(line, 'bind')
|
|
112
|
+
print_binding = 'bind = binding + bind'
|
|
113
|
+
end
|
|
114
|
+
is_structural = true
|
|
115
|
+
line + "\n" + print_binding
|
|
116
|
+
when :shell then "__sh__(#{line.strip.inspect})"
|
|
117
|
+
when :yarsh
|
|
118
|
+
ident_surround.save_local_vars(line)
|
|
119
|
+
"__yarsh__(#{line.strip.inspect}, #{is_structural ? 'bind' : 'nil'})"
|
|
120
|
+
end
|
|
121
|
+
end.join("\n")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def self.structural?(line)
|
|
125
|
+
# line.strip.match?(KEYWORD_REGEX)
|
|
126
|
+
Ripper.lex(line).any? { |token| STRUCTURAL_KEYWORDS.include?(token[2]) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def self.compiles?(src)
|
|
130
|
+
RubyVM::InstructionSequence.compile(src)
|
|
131
|
+
true
|
|
132
|
+
rescue SyntaxError
|
|
133
|
+
false
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def self.heredoc_delimiter(line)
|
|
137
|
+
return nil if line.include?('-->')
|
|
138
|
+
return nil unless line.include?('<<')
|
|
139
|
+
|
|
140
|
+
m = line.match(/<<(-|~)?(['"])([^'"]+)\2(?:\s*\.\w+)?\s*\z/)
|
|
141
|
+
return m[3] if m
|
|
142
|
+
|
|
143
|
+
m = line.match(/<<(-|~)?([A-Za-z_]\w*)(?:\s*\.\w+)?\s*\z/)
|
|
144
|
+
m && m[2]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
data/lib/yarsh/prompt.rb
CHANGED
|
@@ -8,8 +8,15 @@ module Yarsh
|
|
|
8
8
|
seg = Dir.pwd.gsub(/^#{Dir.home}/, '~').split('/')
|
|
9
9
|
seg = (seg.size > 3 ? [' …'] + seg[-3..-1] : seg)
|
|
10
10
|
git_branch = ''
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
if File.directory?('.git')
|
|
12
|
+
git_branch = `git status --porcelain -b`
|
|
13
|
+
.split("\n")
|
|
14
|
+
&.first
|
|
15
|
+
&.gsub('## ', '')
|
|
16
|
+
&.split('...', 2)
|
|
17
|
+
&.first || ''
|
|
18
|
+
end
|
|
19
|
+
git_branch = ' ' + git_branch unless git_branch.empty?
|
|
13
20
|
c.bg_color256(base_color)
|
|
14
21
|
.fg_white("#{seg.join(' ')}#{git_branch}")
|
|
15
22
|
.reset
|
data/lib/yarsh/shell.rb
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
2
5
|
module Yarsh
|
|
3
6
|
class Shell
|
|
4
7
|
include Yarsh
|
|
5
8
|
include InstanceMethods
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
attr_accessor :bind
|
|
11
|
+
|
|
12
|
+
SHELL_BUILTINS = %w[alias bg bind break builtin command echo eval exec exit
|
|
8
13
|
export false fc fg getopts hash help history jobs kill let
|
|
9
14
|
local logout mapfile popd printf pushd pwd read readonly
|
|
10
15
|
return set shift shopt source suspend test times trap true
|
|
@@ -16,8 +21,14 @@ module Yarsh
|
|
|
16
21
|
@history_file = File.expand_path(HISTORY_FILE)
|
|
17
22
|
@config_file = File.expand_path(CONFIG_FILE)
|
|
18
23
|
@cf = Yarsh::AnsiColorFormatter.new
|
|
24
|
+
@typing = false
|
|
25
|
+
Yarsh.current_shell = self
|
|
26
|
+
setup_completion_registry
|
|
19
27
|
setup_reline_history
|
|
20
28
|
setup_reline_completion
|
|
29
|
+
setup_reline_prompt
|
|
30
|
+
setup_output_modifier_proc
|
|
31
|
+
setup_completion_dialog
|
|
21
32
|
end
|
|
22
33
|
|
|
23
34
|
def shell
|
|
@@ -25,7 +36,7 @@ module Yarsh
|
|
|
25
36
|
load @config_file, InstanceMethods if File.exist? @config_file
|
|
26
37
|
loop do
|
|
27
38
|
input = begin
|
|
28
|
-
|
|
39
|
+
read_input
|
|
29
40
|
rescue Interrupt
|
|
30
41
|
next
|
|
31
42
|
end
|
|
@@ -37,42 +48,7 @@ module Yarsh
|
|
|
37
48
|
append_history(input, @history_file)
|
|
38
49
|
|
|
39
50
|
begin
|
|
40
|
-
|
|
41
|
-
fp execute_pipe(input, @bind)
|
|
42
|
-
next
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
if shell_path?(input)
|
|
46
|
-
execute(input)
|
|
47
|
-
next
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
if input == 'cd' || input.start_with?('cd ')
|
|
51
|
-
cd(input.split(' ', 2)[1])
|
|
52
|
-
next
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
begin
|
|
56
|
-
result = @bind.eval(input)
|
|
57
|
-
fp result
|
|
58
|
-
rescue NameError, SyntaxError, ArgumentError => rb_error
|
|
59
|
-
unless shell_command?(input) || rb_error.is_a?(NameError) || rb_error.is_a?(SyntaxError)
|
|
60
|
-
$rber = rb_error
|
|
61
|
-
puts @cf.fg_red("Error: ArgumentError: #{rb_error.message}")
|
|
62
|
-
next
|
|
63
|
-
end
|
|
64
|
-
info("Assuming it is not a ruby expression, the error: #{rb_error.message}")
|
|
65
|
-
debug("The detail: #{rb_error.backtrace.join("\n")}")
|
|
66
|
-
begin
|
|
67
|
-
input = Yarsh.expand_aliases(input, Config.instance.aliases)
|
|
68
|
-
execute(input)
|
|
69
|
-
rescue Exception => e
|
|
70
|
-
$sher = e
|
|
71
|
-
info("#{e.class}: #{e.message}")
|
|
72
|
-
puts "#{@cf.bold.underline.fg_blue('Shell:')} '#{input}': #{e.message}"
|
|
73
|
-
puts "#{@cf.bold.underline.fg_red('Ruby:')} '#{input}': #{rb_error.message}"
|
|
74
|
-
end
|
|
75
|
-
end
|
|
51
|
+
fp execute_multiline(input)
|
|
76
52
|
rescue Interrupt
|
|
77
53
|
next
|
|
78
54
|
rescue SystemExit
|
|
@@ -82,6 +58,8 @@ module Yarsh
|
|
|
82
58
|
puts @cf.fg_red("Error: #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}")
|
|
83
59
|
end
|
|
84
60
|
end
|
|
61
|
+
ensure
|
|
62
|
+
Config.instance.logger.close
|
|
85
63
|
end
|
|
86
64
|
|
|
87
65
|
private
|
|
@@ -91,14 +69,106 @@ module Yarsh
|
|
|
91
69
|
end
|
|
92
70
|
alias fp format_output
|
|
93
71
|
|
|
72
|
+
def read_input
|
|
73
|
+
@typing = true
|
|
74
|
+
input = @core.readmultiline(Config.instance.prompt, true) do |buffer|
|
|
75
|
+
!incomplete_line?(buffer) || buffer.match?(/\n[ \t]*\n\z/)
|
|
76
|
+
end
|
|
77
|
+
@typing = false
|
|
78
|
+
input
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A line is "incomplete" (multiline continues) only when it is not a
|
|
82
|
+
# shell command: `cd /tmp`, `grep foo /etc/passwd` etc. fail to compile
|
|
83
|
+
# as Ruby (unterminated regexp) but must stay single-line.
|
|
84
|
+
def incomplete_line?(line)
|
|
85
|
+
return true if shell_command?(line) && unclosed_quote?(line)
|
|
86
|
+
|
|
87
|
+
Yarsh::Multiline.incomplete?(line) && !shell_command?(line) && !line&.start_with?('cd')
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def unclosed_quote?(line)
|
|
91
|
+
return false if line.nil?
|
|
92
|
+
double_quotes = line.scan(/(?<!\\)"/).size
|
|
93
|
+
single_quotes = line.scan(/(?<!\\)'/).size
|
|
94
|
+
double_quotes.odd? || single_quotes.odd?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def inside_quote?(pre)
|
|
98
|
+
return false if pre.nil? || pre.empty?
|
|
99
|
+
double_quotes = pre.scan(/(?<!\\)"/).size
|
|
100
|
+
single_quotes = pre.scan(/(?<!\\)'/).size
|
|
101
|
+
double_quotes.odd? || single_quotes.odd?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def execute_multiline(input)
|
|
105
|
+
source = Yarsh::Multiline.transform(input) { |line| shell_command?(line) }
|
|
106
|
+
debug("What I will evaluate as multiline:\n#{@cf.bold.fg_blue(source)}")
|
|
107
|
+
@bind.eval(source)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Evaluate a yarsh line. A yarsh line is a ruby expression or a shell syntax
|
|
111
|
+
def yarsh_eval(input)
|
|
112
|
+
debug("I want to evaluate the input: #{input}")
|
|
113
|
+
return execute_pipe(input, @bind) if input.include?('-->')
|
|
114
|
+
|
|
115
|
+
return execute(input) if shell_path?(input)
|
|
116
|
+
|
|
117
|
+
if input == 'cd' || input.start_with?('cd ')
|
|
118
|
+
cd(input.split(' ', 2)[1])
|
|
119
|
+
return nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
begin
|
|
123
|
+
@bind.eval(input)
|
|
124
|
+
rescue NameError, SyntaxError, ArgumentError => rb_error
|
|
125
|
+
unless shell_command?(input) || rb_error.is_a?(NameError) || rb_error.is_a?(SyntaxError)
|
|
126
|
+
$rber = rb_error
|
|
127
|
+
return @cf.fg_red("Error: ArgumentError: #{rb_error.message}")
|
|
128
|
+
end
|
|
129
|
+
info("Assuming it is not a ruby expression, the error: #{rb_error.message}")
|
|
130
|
+
debug("The detail: #{rb_error.backtrace.join("\n")}")
|
|
131
|
+
begin
|
|
132
|
+
input = Yarsh.expand_aliases(input, Config.instance.aliases)
|
|
133
|
+
execute(input)
|
|
134
|
+
rescue Exception => e
|
|
135
|
+
$sher = e
|
|
136
|
+
info("#{e.class}: #{e.message}")
|
|
137
|
+
puts "#{@cf.bold.underline.fg_blue('Shell:')} '#{input}': #{e.message}"
|
|
138
|
+
puts "#{@cf.bold.underline.fg_red('Ruby:')} '#{input}': #{rb_error.message}"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Runs a shell line from inside a multiline Ruby buffer (via __sh__).
|
|
144
|
+
def __sh_exec(command)
|
|
145
|
+
command = Yarsh.expand_aliases(command, Config.instance.aliases)
|
|
146
|
+
if command.include?('-->')
|
|
147
|
+
puts execute_pipe(command, @bind)
|
|
148
|
+
else
|
|
149
|
+
execute(command)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def setup_reline_prompt
|
|
154
|
+
Reline.prompt_proc = proc do |lines|
|
|
155
|
+
[Config.instance.prompt] + Array.new([lines.size - 1, 0].max, ' > ')
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
94
159
|
def cd(path = '~')
|
|
95
160
|
Dir.chdir(File.expand_path(path || '~'))
|
|
96
161
|
end
|
|
97
162
|
|
|
98
163
|
def execute(cmd)
|
|
164
|
+
return system(cmd, exception: true) if cmd.match?(/[|;&`$(){}\[\]<>]/)
|
|
165
|
+
args = Shellwords.shellsplit(cmd)
|
|
166
|
+
system(*args, exception: true)
|
|
167
|
+
rescue ArgumentError
|
|
99
168
|
system(cmd, exception: true)
|
|
100
169
|
end
|
|
101
170
|
|
|
171
|
+
# TODO: Use exec_in_path?
|
|
102
172
|
def shell_command?(input)
|
|
103
173
|
first_token = input.strip.split(/\s+/, 2).first
|
|
104
174
|
return true if shell_path?(first_token)
|
|
@@ -109,7 +179,39 @@ module Yarsh
|
|
|
109
179
|
end
|
|
110
180
|
end
|
|
111
181
|
|
|
182
|
+
def exec_in_path?(input)
|
|
183
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).any? do |dir|
|
|
184
|
+
File.executable?(File.join(dir, input))
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
112
188
|
def complete_command(word)
|
|
189
|
+
registry = @completion_registry
|
|
190
|
+
return [] if word.nil?
|
|
191
|
+
|
|
192
|
+
if word.empty?
|
|
193
|
+
return ENV['PATH'].split(File::PATH_SEPARATOR)
|
|
194
|
+
.flat_map { |dir| Dir["#{dir}/*"] }
|
|
195
|
+
.map { |f| File.basename(f) }
|
|
196
|
+
.uniq
|
|
197
|
+
.sort
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
cmd = word.strip.split(/\s+/).first
|
|
201
|
+
return [] if cmd.nil? || cmd.empty?
|
|
202
|
+
|
|
203
|
+
# Strip privileged prefix (sudo, doas, etc.) — complete the real command
|
|
204
|
+
if Yarsh::Completion::Engine::PRIVILEGED_COMMANDS.include?(cmd)
|
|
205
|
+
remaining = word.sub(/\A#{Regexp.escape(cmd)}\s+/, '')
|
|
206
|
+
return complete_command(remaining)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
completions = registry.complete(cmd, word)
|
|
210
|
+
return completions unless completions.empty?
|
|
211
|
+
|
|
212
|
+
builtins = SHELL_BUILTINS.select { |b| b.start_with?(word) }
|
|
213
|
+
return builtins unless builtins.empty?
|
|
214
|
+
|
|
113
215
|
ENV['PATH'].split(File::PATH_SEPARATOR)
|
|
114
216
|
.flat_map { |dir| Dir["#{dir}/#{word}*"] }
|
|
115
217
|
.map { |f| File.basename(f) }
|
|
@@ -117,6 +219,62 @@ module Yarsh
|
|
|
117
219
|
.sort
|
|
118
220
|
end
|
|
119
221
|
|
|
222
|
+
def setup_completion_registry
|
|
223
|
+
@completion_registry = Yarsh::Completion::Registry.new
|
|
224
|
+
@completion_registry.register(Yarsh::Completion::BashAutocomplete.new)
|
|
225
|
+
@completion_registry.register(Yarsh::Completion::GemCompletion.new)
|
|
226
|
+
@completion_registry.register(Yarsh::Completion::HelpCompletion.new)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def complete_ruby(word)
|
|
230
|
+
IRB::InputCompletor.retrieve_completion_data(word, bind: @bind)
|
|
231
|
+
rescue
|
|
232
|
+
[]
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def setup_output_modifier_proc
|
|
236
|
+
@core.output_modifier_proc = proc do |data|
|
|
237
|
+
if @typing
|
|
238
|
+
IRB::Color.colorize_code(data)
|
|
239
|
+
else
|
|
240
|
+
data
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def setup_completion_dialog
|
|
246
|
+
Reline.add_dialog_proc(:docs, proc {
|
|
247
|
+
begin
|
|
248
|
+
dialog.trap_key = nil
|
|
249
|
+
|
|
250
|
+
next nil if just_cursor_moving && completion_journey_data.nil?
|
|
251
|
+
|
|
252
|
+
cursor_pos_to_render, result, pointer, autocomplete_dialog = context.pop(4)
|
|
253
|
+
next nil if result.nil? || pointer.nil? || pointer.negative?
|
|
254
|
+
|
|
255
|
+
selection = result[pointer]
|
|
256
|
+
help_command = Help::Command.new(screen_width, cursor_pos_to_render, autocomplete_dialog)
|
|
257
|
+
next nil unless help_command.info(result[pointer])
|
|
258
|
+
|
|
259
|
+
dialog.trap_key = Yarsh::ALT_D_SEQUENCES
|
|
260
|
+
|
|
261
|
+
if key.match?(dialog.name)
|
|
262
|
+
begin
|
|
263
|
+
print "\e[?1049h"
|
|
264
|
+
system(*Shellwords.shellsplit("man #{selection}"))
|
|
265
|
+
ensure
|
|
266
|
+
print "\e[?1049l"
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
help_command.render_dialog
|
|
270
|
+
rescue StandardError => e
|
|
271
|
+
extend Yarsh::InstanceMethods
|
|
272
|
+
debug("Error occurend for add help dialog: #{e.message}")
|
|
273
|
+
nil
|
|
274
|
+
end
|
|
275
|
+
}, Reline::DEFAULT_DIALOG_CONTEXT)
|
|
276
|
+
end
|
|
277
|
+
|
|
120
278
|
def complete_path(word)
|
|
121
279
|
word = './' if word.empty?
|
|
122
280
|
glob_word = word.start_with?('~') ? File.expand_path(word) : word
|
|
@@ -161,23 +319,58 @@ module Yarsh
|
|
|
161
319
|
end
|
|
162
320
|
|
|
163
321
|
def setup_reline_completion
|
|
164
|
-
Reline.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
322
|
+
Reline.autocompletion = Config.instance.autocompletion
|
|
323
|
+
Reline.completion_append_character = ' '
|
|
324
|
+
Reline.core.config.keyseq_timeout = 0.5
|
|
325
|
+
Reline.completion_proc = proc { |target, pre, post|
|
|
326
|
+
next [] if inside_quote?(pre)
|
|
327
|
+
full = pre.empty? ? target : pre.rstrip + ' ' + target
|
|
328
|
+
first_word = full.strip.split(/\s+/).first.to_s
|
|
329
|
+
ruby_completions = if !Config.instance.ruby_completion ||
|
|
330
|
+
Yarsh::Completion::Engine::PRIVILEGED_COMMANDS.include?(first_word)
|
|
331
|
+
[]
|
|
332
|
+
else
|
|
333
|
+
complete_ruby(target).compact
|
|
334
|
+
end
|
|
335
|
+
ruby_completions +
|
|
336
|
+
if target.match?(%r{\A\.\.?(/|\z)}) || target.start_with?('/', '~') || target.include?('/')
|
|
337
|
+
complete_path(target)
|
|
338
|
+
elsif target.empty? && pre.empty?
|
|
339
|
+
complete_command(target)
|
|
340
|
+
elsif pre.end_with?(' ')
|
|
341
|
+
complete_command(full)
|
|
342
|
+
else
|
|
343
|
+
complete_command(full)
|
|
344
|
+
end
|
|
175
345
|
}
|
|
176
346
|
end
|
|
177
347
|
|
|
178
348
|
def setup_reline_history
|
|
179
349
|
Dir.mkdir(File.dirname(@history_file)) unless Dir.exist?(File.dirname(@history_file))
|
|
180
|
-
|
|
350
|
+
return unless File.exist?(@history_file)
|
|
351
|
+
|
|
352
|
+
pending = nil
|
|
353
|
+
File.readlines(@history_file, chomp: true).each do |line|
|
|
354
|
+
next if line.strip.empty?
|
|
355
|
+
|
|
356
|
+
unless pending
|
|
357
|
+
pending = line
|
|
358
|
+
next
|
|
359
|
+
end
|
|
360
|
+
if incomplete_line?(pending)
|
|
361
|
+
candidate = "#{pending}\n#{line}"
|
|
362
|
+
if Yarsh::Multiline.incomplete?(candidate)
|
|
363
|
+
pending = candidate
|
|
364
|
+
else
|
|
365
|
+
Reline::HISTORY << candidate
|
|
366
|
+
pending = nil
|
|
367
|
+
end
|
|
368
|
+
else
|
|
369
|
+
Reline::HISTORY << pending
|
|
370
|
+
pending = line
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
Reline::HISTORY << pending if pending
|
|
181
374
|
end
|
|
182
375
|
end
|
|
183
376
|
end
|
data/lib/yarsh/version.rb
CHANGED
data/lib/yarsh.rb
CHANGED
|
@@ -4,19 +4,31 @@ 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'
|
|
10
|
+
require 'ripper'
|
|
11
|
+
require 'prism'
|
|
7
12
|
|
|
8
13
|
module Yarsh
|
|
9
14
|
class Error < StandardError; end
|
|
10
15
|
end
|
|
11
16
|
|
|
12
|
-
Dir['
|
|
13
|
-
require_relative
|
|
17
|
+
Dir[File.expand_path('**/*.rb', __dir__)].each do |f|
|
|
18
|
+
require_relative f
|
|
14
19
|
end
|
|
15
20
|
|
|
16
21
|
module Yarsh
|
|
17
22
|
YARSH_DIR = '~/.yarsh'
|
|
18
23
|
HISTORY_FILE = File.join Yarsh::YARSH_DIR, 'history'
|
|
19
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
|
|
20
32
|
|
|
21
33
|
class ExecOutput < String
|
|
22
34
|
def initialize(string)
|
|
@@ -34,6 +46,14 @@ module Yarsh
|
|
|
34
46
|
|
|
35
47
|
class Error < StandardError; end
|
|
36
48
|
|
|
49
|
+
def self.current_shell
|
|
50
|
+
@current_shell
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.current_shell=(shell)
|
|
54
|
+
@current_shell = shell
|
|
55
|
+
end
|
|
56
|
+
|
|
37
57
|
# in a Shell input, find all command matching the keys of aliases and replace them with
|
|
38
58
|
# the values. Leading and trailing whitespace around the match is preserved.
|
|
39
59
|
def self.expand_aliases(input, aliases)
|
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.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- yarsh4all
|
|
@@ -44,8 +44,17 @@ files:
|
|
|
44
44
|
- exe/yarsh
|
|
45
45
|
- lib/yarsh.rb
|
|
46
46
|
- lib/yarsh/ansi_color_formatter.rb
|
|
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
|
|
47
54
|
- lib/yarsh/config.rb
|
|
55
|
+
- lib/yarsh/help.rb
|
|
48
56
|
- lib/yarsh/instance_methods.rb
|
|
57
|
+
- lib/yarsh/multiline.rb
|
|
49
58
|
- lib/yarsh/prompt.rb
|
|
50
59
|
- lib/yarsh/shell.rb
|
|
51
60
|
- lib/yarsh/version.rb
|