yap-shell 0.1.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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"]