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,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
- git_branch = `git status --porcelain -b`.split("\n")&.first&.gsub('## ', '') || '' if File.directory?('.git')
12
- git_branch = ' ' + git_branch unless git_branch.empty?
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
- SHELL_BUILTINS = %w[alias bg bind break builtin cd command echo eval exec exit
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
- @core.readline(Config.instance.prompt, true)
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
- if input.include?('-->')
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.completion_proc = proc { |target, pre|
165
- # puts "target='#{target}' pre='#{pre}'"
166
- if target.match?(%r{\A\.\.?(/|\z)}) || target.start_with?('/', '~') || target.include?('/')
167
- complete_path(target)
168
- elsif target.empty? && pre.empty?
169
- complete_command(target)
170
- elsif pre.end_with?(' ')
171
- complete_path(target)
172
- else
173
- complete_command(target)
174
- end
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
- File.readlines(@history_file, chomp: true).each { |line| Reline::HISTORY << line } if File.exist?(@history_file)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yarsh
4
- VERSION = '0.1.1'
4
+ VERSION = '0.3.0'
5
5
  end
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['lib/yarsh/**/*.rb'].each do |f|
13
- require_relative File.join('..', f)
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.1.1
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