yap-shell 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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/Gemfile +5 -0
  4. data/WISHLIST.md +14 -0
  5. data/addons/history/Gemfile +2 -0
  6. data/addons/history/history.rb +101 -0
  7. data/addons/history/lib/history/buffer.rb +204 -0
  8. data/addons/history/lib/history/events.rb +13 -0
  9. data/addons/keyboard_macros/keyboard_macros.rb +295 -0
  10. data/addons/prompt/Gemfile +1 -0
  11. data/addons/prompt/right_prompt.rb +17 -0
  12. data/addons/prompt_updates/prompt_updates.rb +28 -0
  13. data/addons/tab_completion/Gemfile +0 -0
  14. data/addons/tab_completion/lib/tab_completion/completer.rb +62 -0
  15. data/addons/tab_completion/lib/tab_completion/custom_completion.rb +33 -0
  16. data/addons/tab_completion/lib/tab_completion/dsl_methods.rb +7 -0
  17. data/addons/tab_completion/lib/tab_completion/file_completion.rb +75 -0
  18. data/addons/tab_completion/tab_completion.rb +157 -0
  19. data/bin/yap +13 -4
  20. data/lib/tasks/addons.rake +51 -0
  21. data/lib/yap.rb +4 -55
  22. data/lib/yap/shell.rb +51 -10
  23. data/lib/yap/shell/builtins.rb +2 -2
  24. data/lib/yap/shell/builtins/alias.rb +2 -2
  25. data/lib/yap/shell/builtins/cd.rb +9 -11
  26. data/lib/yap/shell/builtins/env.rb +11 -0
  27. data/lib/yap/shell/commands.rb +29 -18
  28. data/lib/yap/shell/evaluation.rb +185 -68
  29. data/lib/yap/shell/evaluation/shell_expansions.rb +85 -0
  30. data/lib/yap/shell/event_emitter.rb +18 -0
  31. data/lib/yap/shell/execution/builtin_command_execution.rb +1 -1
  32. data/lib/yap/shell/execution/command_execution.rb +3 -3
  33. data/lib/yap/shell/execution/context.rb +32 -9
  34. data/lib/yap/shell/execution/file_system_command_execution.rb +12 -7
  35. data/lib/yap/shell/execution/ruby_command_execution.rb +6 -6
  36. data/lib/yap/shell/execution/shell_command_execution.rb +17 -2
  37. data/lib/yap/shell/prompt.rb +21 -0
  38. data/lib/yap/shell/repl.rb +179 -18
  39. data/lib/yap/shell/version.rb +1 -1
  40. data/lib/yap/world.rb +149 -15
  41. data/lib/yap/world/addons.rb +135 -0
  42. data/rcfiles/.yaprc +240 -10
  43. data/test.rb +206 -0
  44. data/update-rawline.sh +6 -0
  45. data/yap-shell.gemspec +11 -3
  46. metadata +101 -10
  47. data/addons/history.rb +0 -171
@@ -0,0 +1,85 @@
1
+ module Yap::Shell
2
+ class Evaluation
3
+ class ShellExpansions
4
+ attr_reader :aliases, :world
5
+
6
+ def initialize(world:, aliases: Aliases.instance)
7
+ @world = world
8
+ @aliases = aliases
9
+ end
10
+
11
+ def expand_aliases_in(input)
12
+ head, *tail = input.split(/\s/, 2).first
13
+ if new_head=aliases.fetch_alias(head)
14
+ [new_head].concat(tail).join(" ")
15
+ else
16
+ input
17
+ end
18
+ end
19
+
20
+ def expand_words_in(input, escape_directory_expansions: true)
21
+ [input].flatten.inject([]) do |results,str|
22
+ results << process_expansions(
23
+ word_expand(str),
24
+ escape_directory_expansions: escape_directory_expansions
25
+ )
26
+ end.flatten
27
+ end
28
+
29
+ def expand_variables_in(input)
30
+ env_expand(input)
31
+ end
32
+
33
+ private
34
+
35
+ def env_expand(str)
36
+ str.gsub(/\$(\S+)/) do |match,*args|
37
+ var_name = match[1..-1]
38
+ case var_name
39
+ when "?"
40
+ world.last_result ? world.last_result.status_code.to_s : '0'
41
+ else
42
+ world.env.fetch(var_name){ match }
43
+ end
44
+ end
45
+ end
46
+
47
+ def word_expand(str)
48
+ content = str.scan(/\{([^\}]+)\}/).flatten.first
49
+ if content
50
+ expansions = content.split(",", -1)
51
+
52
+ # Be compatible with Bash/Zsh which only do word-expansion if there
53
+ # at least one comma listed. E.g. "a_{1,2}" => "a_1 a_2" whereas
54
+ # "a_{1}" => "a_{1}"
55
+ if expansions.length > 1
56
+ return expansions.map { |expansion| str.sub(/\{([^\}]+)\}/, expansion) }
57
+ end
58
+ end
59
+ return [str]
60
+ end
61
+
62
+ def process_expansions(expansions, escape_directory_expansions: true)
63
+ expansions.map do |s|
64
+ # Basic bash-style tilde expansion
65
+ s.gsub!(/\A~(.*)/, world.env["HOME"] + '\1')
66
+
67
+ # Basic bash-style variable expansion
68
+ s = env_expand(s)
69
+
70
+ # Basic bash-style path-name expansion
71
+ expansions = Dir[s]
72
+ if expansions.any?
73
+ if escape_directory_expansions
74
+ expansions.map(&:shellescape)
75
+ else
76
+ expansions
77
+ end
78
+ else
79
+ s
80
+ end
81
+ end.flatten
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,18 @@
1
+ module Yap::Shell
2
+ module EventEmitter
3
+ def _callbacks
4
+ @_callbacks ||= Hash.new { |h, k| h[k] = [] }
5
+ end
6
+
7
+ def on(type, *args, &blk)
8
+ _callbacks[type] << blk
9
+ self
10
+ end
11
+
12
+ def emit(type, *args)
13
+ _callbacks[type].each do |blk|
14
+ blk.call(*args)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -2,7 +2,7 @@ require 'yap/shell/execution/result'
2
2
 
3
3
  module Yap::Shell::Execution
4
4
  class BuiltinCommandExecution < CommandExecution
5
- on_execute do |command:, n:, of:|
5
+ on_execute do |command:, n:, of:, wait:|
6
6
  status_code = command.execute(stdin:@stdin, stdout:@stdout, stderr:@stderr)
7
7
  if status_code == :resume
8
8
  ResumeExecution.new(status_code:0, directory:Dir.pwd, n:n, of:of)
@@ -10,14 +10,14 @@ module Yap::Shell::Execution
10
10
  end
11
11
  end
12
12
 
13
- def initialize(stdin:, stdout:, stderr:,world:)
13
+ def initialize(stdin:, stdout:, stderr:, world:)
14
14
  @stdin, @stdout, @stderr = stdin, stdout, stderr
15
15
  @world = world
16
16
  end
17
17
 
18
- def execute(command:, n:, of:)
18
+ def execute(command:, n:, of:, wait:true)
19
19
  if self.class.on_execute
20
- self.instance_exec(command:command, n:n, of:of, &self.class.on_execute)
20
+ self.instance_exec(command:command, n:n, of:of, wait:wait, &self.class.on_execute)
21
21
  else
22
22
  raise NotImplementedError, "on_execute block hasn't been implemented!"
23
23
  end
@@ -1,3 +1,5 @@
1
+ require 'uri'
2
+
1
3
  module Yap::Shell::Execution
2
4
  class Context
3
5
  def self.on(event=nil, &blk)
@@ -31,8 +33,8 @@ module Yap::Shell::Execution
31
33
  @suspended_execution_contexts = []
32
34
  end
33
35
 
34
- def add_command_to_run(command, stdin:, stdout:, stderr:)
35
- @command_queue << [command, stdin, stdout, stderr]
36
+ def add_command_to_run(command, stdin:, stdout:, stderr:, wait:)
37
+ @command_queue << [command, stdin, stdout, stderr, wait]
36
38
  end
37
39
 
38
40
  def clear_commands
@@ -41,9 +43,9 @@ module Yap::Shell::Execution
41
43
 
42
44
  def execute(world:)
43
45
  results = []
44
- @command_queue.each_with_index do |(command, stdin, stdout, stderr), reversed_i|
46
+ @command_queue.each_with_index do |(command, stdin, stdout, stderr, wait), reversed_i|
45
47
  of = @command_queue.length
46
- i = of - reversed_i
48
+ i = reversed_i + 1
47
49
  stdin = @stdin if stdin == :stdin
48
50
  stdout = @stdout if stdout == :stdout
49
51
  stderr = @stderr if stderr == :stderr
@@ -58,11 +60,32 @@ module Yap::Shell::Execution
58
60
  )
59
61
 
60
62
  @saved_tty_attrs = Termios.tcgetattr(STDIN)
61
- self.class.fire :before_execute, execution_context, command: command
62
- result = execution_context.execute(command:command, n:i, of:of)
63
- self.class.fire :after_execute, execution_context, command: command, result: result
63
+ self.class.fire :before_execute, world, command: command
64
+
65
+ begin
66
+ result = execution_context.execute(command:command, n:i, of:of, wait:wait)
67
+ rescue Exception => ex
68
+ raise(ex) if ex.is_a?(SystemExit)
69
+ puts <<-ERROR.gsub(/^\s*\|/, '')
70
+ |******************************
71
+ |\e[31mWhoops! An unexpected error has occurred\e[0m
72
+ |******************************
73
+ |
74
+ |The error was:
75
+ | #{ex.message}
76
+ |
77
+ |Backtrace:
78
+ |#{ex.backtrace.join("\n")}
79
+ |
80
+ |Report this to yap-shell on github:
81
+ | https://github.com/zdennis/yap-shell/issues/new?title=#{URI.escape(ex.message)}
82
+ |
83
+ ERROR
84
+ end
85
+
86
+ self.class.fire :after_execute, world, command: command, result: result
64
87
 
65
- results << process_execution_result(execution_context:execution_context, result: result)
88
+ results << process_execution_result(execution_context:execution_context, result:result)
66
89
  Termios.tcsetattr(STDIN, Termios::TCSANOW, @saved_tty_attrs)
67
90
  end
68
91
  end
@@ -86,7 +109,7 @@ module Yap::Shell::Execution
86
109
  nresult = execution_context.resume
87
110
  return process_execution_result execution_context: execution_context, result: nresult
88
111
  else
89
- stderr.puts "fg: No such job"
112
+ @stderr.puts "fg: No such job"
90
113
  end
91
114
  else
92
115
  return result
@@ -3,7 +3,7 @@ require 'termios'
3
3
 
4
4
  module Yap::Shell::Execution
5
5
  class FileSystemCommandExecution < CommandExecution
6
- on_execute do |command:, n:, of:, resume_blk:nil|
6
+ on_execute do |command:, n:, of:, wait:, resume_blk:nil|
7
7
  stdin, stdout, stderr, world = @stdin, @stdout, @stderr, @world
8
8
  result = nil
9
9
  if resume_blk
@@ -40,7 +40,13 @@ module Yap::Shell::Execution
40
40
  $stdout.reopen stdout
41
41
  $stderr.reopen stderr
42
42
 
43
- Kernel.exec command.to_executable_str
43
+ begin
44
+ before = ENV.to_h.dup
45
+ ENV.replace(@world.env)
46
+ Kernel.exec command.to_executable_str
47
+ ensure
48
+ ENV.replace(before)
49
+ end
44
50
  end
45
51
 
46
52
  # Put the child process into a process group of its own
@@ -54,10 +60,9 @@ module Yap::Shell::Execution
54
60
 
55
61
  # Set terminal's process group to that of the child process
56
62
  Termios.tcsetpgrp STDIN, pid
57
- pid, status = Process.wait2(-1, Process::WUNTRACED) unless of > 1
58
- puts "Process (#{pid}) stopped: #{status.inspect}" if ENV["DEBUG"]
63
+ pid, status = Process.wait2(pid, Process::WUNTRACED) if wait
59
64
 
60
- # If we're not printing to the terminal than close in/out/err. This
65
+ # If we're not printing to the terminal then close in/out/err. This
61
66
  # is so the next command in the pipeline can complete and don't hang waiting for
62
67
  # stdin after the command that's writing to its stdin has completed.
63
68
  if stdout != $stdout && stdout.is_a?(IO) && !stdout.closed? then
@@ -78,7 +83,7 @@ module Yap::Shell::Execution
78
83
  end
79
84
 
80
85
  # if the reason we stopped is from being suspended
81
- if status.stopsig == Signal.list["TSTP"]
86
+ if status && status.stopsig == Signal.list["TSTP"]
82
87
  puts "Process (#{pid}) suspended: #{status.stopsig}" if ENV["DEBUG"]
83
88
  suspended(command:command, n:n, of:of, pid: pid)
84
89
  result = Yap::Shell::Execution::SuspendExecution.new(status_code:nil, directory:Dir.pwd, n:n, of:of)
@@ -100,7 +105,7 @@ module Yap::Shell::Execution
100
105
  args[:pid]
101
106
  end
102
107
 
103
- self.instance_exec command:args[:command], n:args[:n], of:args[:of], resume_blk:resume_blk, &self.class.on_execute
108
+ self.instance_exec command:args[:command], n:args[:n], of:args[:of], resume_blk:resume_blk, wait:true, &self.class.on_execute
104
109
  end
105
110
 
106
111
  def suspended(command:, n:, of:, pid:)
@@ -1,9 +1,9 @@
1
1
  module Yap::Shell::Execution
2
2
  class RubyCommandExecution < CommandExecution
3
- on_execute do |command:, n:, of:|
3
+ on_execute do |command:, n:, of:, wait:|
4
4
  result = nil
5
5
  stdin, stdout, stderr, world = @stdin, @stdout, @stderr, @world
6
- t = Thread.new {
6
+ # t = Thread.new {
7
7
  exit_code = 0
8
8
  first_command = n == 1
9
9
 
@@ -72,13 +72,13 @@ module Yap::Shell::Execution
72
72
  # to be scheduled before we send back our status code. This could
73
73
  # probably use a more elaborate signal or message passing scheme,
74
74
  # but that's for another day.
75
- Thread.pass
75
+ # Thread.pass
76
76
 
77
77
  # Make up an exit code
78
78
  result = Result.new(status_code:exit_code, directory:Dir.pwd, n:n, of:of)
79
- }
80
- t.abort_on_exception = true
81
- t.join
79
+ # }
80
+ # t.abort_on_exception = true
81
+ # t.join
82
82
  result
83
83
  end
84
84
  end
@@ -1,8 +1,23 @@
1
1
  module Yap::Shell::Execution
2
2
  class ShellCommandExecution < CommandExecution
3
- on_execute do |command:, n:, of:|
3
+ on_execute do |command:, n:, of:, wait:|
4
+ possible_parameters = {
5
+ command: command.str,
6
+ args: command.args,
7
+ stdin: @stdin,
8
+ stdout: @stdout,
9
+ stderr: @stderr,
10
+ world: @world,
11
+ line: command.line
12
+ }
13
+
4
14
  func = command.to_proc
5
- command_result = func.call(args:command.args, stdin:@stdin, stdout:@stdout, stderr:@stderr)
15
+ params = func.parameters.reduce({}) do |h, (type, name)|
16
+ h[name] = possible_parameters[name]
17
+ h
18
+ end
19
+
20
+ command_result = func.call(**params)
6
21
  @stdout.close if @stdout != $stdout && !@stdout.closed?
7
22
  @stderr.close if @stderr != $stderr && !@stderr.closed?
8
23
  end
@@ -0,0 +1,21 @@
1
+ module Yap::Shell
2
+ class Prompt
3
+ attr_reader :text
4
+
5
+ def initialize(text:, &blk)
6
+ @text = text
7
+ @blk = blk
8
+ end
9
+
10
+ def text=(text)
11
+ @text = text
12
+ end
13
+
14
+ def update
15
+ if @blk
16
+ @text = @blk.call
17
+ end
18
+ self
19
+ end
20
+ end
21
+ end
@@ -1,38 +1,198 @@
1
- require 'readline'
1
+ require 'shellwords'
2
+ require 'term/ansicolor'
2
3
 
3
4
  module Yap::Shell
5
+ module Color
6
+ extend Term::ANSIColor
7
+ end
8
+
4
9
  class Repl
10
+ attr_reader :editor
11
+
5
12
  def initialize(world:nil)
6
13
  @world = world
14
+ @editor= world.editor
15
+ install_default_keybindings
16
+ install_default_tab_completion_proc
7
17
  end
8
18
 
9
- def loop_on_input(&blk)
19
+ def on_input(&blk)
10
20
  @blk = blk
11
21
 
12
- loop do
13
- heredoc = nil
14
- prompt = ""
15
-
22
+ @world.editor.on_read_line do |event|
23
+ # editor.history = true?
24
+ line = event[:payload][:line]
16
25
  begin
17
- prompt = @world ? @world.prompt : "> "
18
- input = Readline.readline("#{prompt}", true)
19
-
20
- next if input == ""
21
-
22
- input = process_heredoc(input)
23
-
24
- yield input
26
+ @blk.call(line)
27
+ @world.editor.redraw_prompt
25
28
  rescue ::Yap::Shell::CommandUnknownError => ex
26
29
  puts " CommandError: #{ex.message}"
27
- rescue Interrupt
28
- puts "^C"
29
- next
30
+ ensure
31
+ @world.editor.reset_line
30
32
  end
33
+
34
+ ensure_process_group_controls_the_tty
35
+ @world.refresh_prompt
31
36
  end
32
37
  end
33
38
 
34
39
  private
35
40
 
41
+ def kill_ring
42
+ @kill_ring ||= []
43
+ end
44
+
45
+ def install_default_keybindings
46
+ editor.terminal.keys.merge!(enter: [13])
47
+ editor.bind(:return){ editor.newline }
48
+
49
+ # Move to beginning of line
50
+ editor.bind(:ctrl_a) { editor.move_to_beginning_of_input }
51
+
52
+ # Move to end of line
53
+ editor.bind(:ctrl_e) { editor.move_to_end_of_input }
54
+
55
+ # Move backward one word at a time
56
+ editor.bind(:ctrl_b) {
57
+ text = editor.line.text[0...editor.line.position].reverse
58
+ position = text.index(/\s+/, 1)
59
+ position = position ? (text.length - position) : 0
60
+ editor.move_to_position position
61
+ }
62
+
63
+ # Move forward one word at a time
64
+ editor.bind(:ctrl_f) {
65
+ text = editor.line.text
66
+ position = text.index(/\s+/, editor.line.position)
67
+ position = position ? (position + 1) : text.length
68
+ editor.move_to_position position
69
+ }
70
+
71
+ # Yank text from the kill ring and insert it at the cursor position
72
+ editor.bind(:ctrl_y){
73
+ text = kill_ring[-1]
74
+ if text
75
+ editor.yank_forward text.without_ansi
76
+ end
77
+ }
78
+
79
+ # Backwards delete one word
80
+ editor.bind(:ctrl_w){
81
+ before_text = editor.line.text[0...editor.line.position]
82
+ after_text = editor.line.text[editor.line.position..-1]
83
+
84
+ have_only_seen_whitespace = true
85
+ position = 0
86
+
87
+ before_text.reverse.each_char.with_index do |ch, i|
88
+ if ch =~ /\s/ && !have_only_seen_whitespace
89
+ position = before_text.length - i
90
+ break
91
+ else
92
+ have_only_seen_whitespace = false
93
+ end
94
+ end
95
+
96
+ killed_text = before_text[position...editor.line.position]
97
+ kill_ring.push killed_text
98
+
99
+ text = [before_text.slice(0, position), after_text].join
100
+ editor.overwrite_line text
101
+ editor.move_to_position position
102
+ }
103
+
104
+ # History forward, but if at the end of the history then give user a
105
+ # blank line rather than remain on the last command
106
+ editor.bind(:down_arrow) {
107
+ if editor.history.searching? && !editor.history.end?
108
+ editor.history_forward
109
+ else
110
+ editor.overwrite_line ""
111
+ end
112
+ }
113
+
114
+ editor.bind(:enter) { editor.newline }
115
+ editor.bind(:tab) { editor.complete }
116
+ editor.bind(:backspace) { editor.delete_left_character }
117
+
118
+ # Delete to end of line from cursor position
119
+ editor.bind(:ctrl_k) {
120
+ kill_ring.push editor.kill_forward
121
+ }
122
+
123
+ # Delete to beginning of line from cursor position
124
+ editor.bind(:ctrl_u) {
125
+ kill_ring.push editor.line.text[0...editor.line.position]
126
+ editor.overwrite_line editor.line.text[editor.line.position..-1]
127
+ editor.move_to_position 0
128
+ }
129
+
130
+ # Forward delete a character, leaving the cursor in place
131
+ editor.bind("\e[3~") {
132
+ before_text = editor.line.text[0...editor.line.position]
133
+ after_text = editor.line.text[(editor.line.position+1)..-1]
134
+ text = [before_text, after_text].join
135
+ position = editor.line.position
136
+ editor.overwrite_line text
137
+ editor.move_to_position position
138
+ }
139
+
140
+ editor.bind(:ctrl_l){
141
+ editor.clear_screen
142
+ }
143
+
144
+ editor.bind(:ctrl_r) {
145
+ $r = $r ? false : true
146
+ # editor.redo
147
+ }
148
+ editor.bind(:left_arrow) { editor.move_left }
149
+ editor.bind(:right_arrow) { editor.move_right }
150
+ editor.bind(:up_arrow) { editor.history_back }
151
+ editor.bind(:down_arrow) { editor.history_forward }
152
+ editor.bind(:delete) { editor.delete_character }
153
+ editor.bind(:insert) { editor.toggle_mode }
154
+
155
+ editor.bind(:ctrl_g) { editor.clear_history }
156
+ # editor.bind(:ctrl_l) { editor.debug_line }
157
+ editor.bind(:ctrl_h) { editor.show_history }
158
+ editor.bind(:ctrl_d) { puts; puts "Exiting..."; exit }
159
+
160
+ # character-search; wraps around as necessary
161
+ editor.bind(:ctrl_n) {
162
+ line = editor.line
163
+ text, start_position = line.text, line.position
164
+ i, new_position = start_position, nil
165
+
166
+ break_on_bytes = [editor.terminal.keys[:ctrl_c]].flatten
167
+ byte = [editor.read_character].flatten.first
168
+
169
+ unless break_on_bytes.include?(byte)
170
+ loop do
171
+ i += 1
172
+ i = 0 if i >= text.length # wrap-around to the beginning
173
+ break if i == start_position # back to where we started
174
+ (editor.move_to_position(i) ; break) if text[i] == byte.chr # found a match; move and break
175
+ end
176
+ end
177
+ }
178
+ end
179
+
180
+ def install_default_tab_completion_proc
181
+ editor.completion_proc = lambda do |word|
182
+ Dir["#{word}*"].map{ |str| str.gsub(/ /, '\ ')}
183
+ end
184
+ end
185
+
186
+ # This is to prevent the Errno::EIO error from occurring by ensuring that
187
+ # if we haven't been made the process group controlling the TTY that we
188
+ # become so. This method intentionally blocks.
189
+ def ensure_process_group_controls_the_tty
190
+ while Process.pid != Termios.tcgetpgrp(STDIN)
191
+ Termios.tcsetpgrp(STDIN, Process.pid)
192
+ sleep 0.1
193
+ end
194
+ end
195
+
36
196
  def process_heredoc(_input)
37
197
  if _input =~ /<<-?([A-z0-9\-]+)\s*$/
38
198
  input = _input.dup
@@ -43,8 +203,9 @@ module Yap::Shell
43
203
  end
44
204
 
45
205
  puts "Beginning heredoc" if ENV["DEBUG"]
206
+
46
207
  loop do
47
- str = Readline.readline("> ", true)
208
+ str = editor.read(@world.secondary_prompt.update.text, false)
48
209
  input << "#{str}\n"
49
210
  if str =~ /^#{Regexp.escape(marker)}$/
50
211
  puts "Ending heredoc" if ENV["DEBUG"]