Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions lib/reline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,14 @@ def readmultiline(_prompt = '', _add_history = false, prompt: _prompt, add_histo
raise ArgumentError.new('#readmultiline needs block to confirm multiline termination')
end

unless io_gate.both_tty?
whole_buffer = read_noninteractively(prompt, true, &confirm_multiline_termination)
if add_history and whole_buffer and whole_buffer.chomp("\n").size > 0
Reline::HISTORY << whole_buffer
end
return whole_buffer
end

io_gate.with_raw_input do
inner_readline(prompt, add_history, true, rprompt: rprompt, &confirm_multiline_termination)
end
Expand All @@ -275,6 +283,14 @@ def readmultiline(_prompt = '', _add_history = false, prompt: _prompt, add_histo

def readline(_prompt = '', _add_history = false, prompt: _prompt, add_history: _add_history, rprompt: nil)
@mutex.synchronize do
unless io_gate.both_tty?
line = read_noninteractively(prompt, false)
if add_history and line and line.chomp("\n").size > 0
Reline::HISTORY << line.chomp("\n")
end
return line
end

io_gate.with_raw_input do
inner_readline(prompt, add_history, false, rprompt: rprompt)
end
Expand All @@ -290,6 +306,42 @@ def readline(_prompt = '', _add_history = false, prompt: _prompt, add_history: _
end
end

# Line-by-line read used when input or output is not a tty. Editing keys
# in the input are not interpreted, and the output contains no escape
# sequences. Like GNU Readline, the prompt and the input are echoed to
# the output so that it forms a self-contained transcript.
# https://github.com/ruby/reline/issues/886
private def read_noninteractively(prompt, multiline, &confirm_multiline_termination)
input = io_gate.input
lines = []
loop do
output.write(noninteractive_prompt(prompt, multiline, lines))
output.flush
line = input.gets
unless line
# Close the prompt line even on EOF
output.write("\n")
break
end
chomped = line.chomp
# Control characters in the input are echoed in caret notation so
# that they cannot corrupt the output
output.write("#{Reline::Unicode.escape_for_print(chomped)}\n")
lines << chomped
break unless line.end_with?("\n") # EOF without a trailing newline
break unless multiline
break if confirm_multiline_termination.call(lines.join("\n") + "\n")
end
output.flush
lines.empty? ? nil : lines.join("\n")
end

private def noninteractive_prompt(prompt, multiline, lines)
return prompt unless multiline and prompt_proc
prompt_list = prompt_proc.call(lines + [''])
prompt_list[lines.size] || prompt_list[0] || prompt
end

private def inner_readline(prompt, add_history, multiline, rprompt: nil, &confirm_multiline_termination)
if ENV['RELINE_STDERR_TTY']
if io_gate.win?
Expand Down
5 changes: 5 additions & 0 deletions lib/reline/io.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ def dumb?
false
end

# Whether input and output are both connected to a terminal.
def both_tty?
true
end

def win?
false
end
Expand Down
2 changes: 1 addition & 1 deletion lib/reline/io/ansi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Reline::ANSI < Reline::IO
'H' => [:ed_move_to_beg, {}],
}

attr_writer :input, :output
attr_accessor :input, :output

def initialize
@input = STDIN
Expand Down
1 change: 1 addition & 0 deletions lib/reline/io/dumb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
class Reline::Dumb < Reline::IO
RESET_COLOR = '' # Do not send color reset sequence

attr_reader :input
attr_writer :output

def initialize(encoding: nil)
Expand Down
5 changes: 5 additions & 0 deletions lib/reline/io/windows.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class Reline::Windows < Reline::IO
const_set(name, console.const_get(name))
end

attr_reader :input
attr_writer :output

def initialize
Expand Down Expand Up @@ -93,6 +94,10 @@ def set_default_key_bindings(config)
false
end

def both_tty?
@input.tty? && @console_output.tty?
end

def msys_tty?
@input.tty?(:msys, :cygwin)
end
Expand Down
66 changes: 45 additions & 21 deletions test/reline/test_reline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -428,28 +428,48 @@ def test_readline_returns_nil_on_piped_stdin_eof
assert_include(out, { result: nil }.inspect)
end

def test_read_eof_returns_input
pend if win?
lib = File.expand_path("../../lib", __dir__)
code = "p result: Reline.readline"
out = IO.popen([Reline.test_rubybin, "-I#{lib}", "-rreline", "-e", code], "r+") do |io|
io.write "a\C-a"
io.close_write
io.read
end
assert_include(out, { result: 'a' }.inspect)
def test_readline_with_piped_stdin_emits_plain_transcript
out = readline_from_piped_stdin("input\n")
assert_include(out, ">input\n")
assert_not_include(out, "\e")
end

def test_read_eof_returns_nil_if_empty
pend if win?
lib = File.expand_path("../../lib", __dir__)
code = "p result: Reline.readline"
out = IO.popen([Reline.test_rubybin, "-I#{lib}", "-rreline", "-e", code], "r+") do |io|
io.write "a\C-h"
io.close_write
io.read
end
assert_include(out, { result: nil }.inspect)
def test_readline_with_piped_stdin_closes_prompt_line_on_eof
out = readline_from_piped_stdin("")
assert_include(out, ">\n")
end

def test_readline_does_not_interpret_editing_keys_in_piped_stdin
out = readline_from_piped_stdin("a\C-ab\n")
assert_include(out, ">a^Ab\n")
assert_include(out, { result: "a\C-ab" }.inspect)
end

def test_readline_adds_piped_stdin_to_history
code = <<~'RUBY'
require 'timeout'
Timeout.timeout(3) { Reline.readline('>', true) }
p history: Reline::HISTORY.to_a
RUBY
out = run_ruby_with_piped_stdin(code, "input\n")
assert_include(out, { history: ['input'] }.inspect)
end

def test_readmultiline_with_piped_stdin
code = <<~'RUBY'
require 'timeout'
Reline.prompt_proc = proc { |lines| lines.map.with_index { |_, i| "#{i}>" } }
p result: Timeout.timeout(3) { Reline.readmultiline('fallback>') { |code| code.include?('end') } }
RUBY
out = run_ruby_with_piped_stdin(code, "a\nend\n")
assert_include(out, "0>a\n")
assert_include(out, "1>end\n")
assert_include(out, { result: "a\nend" }.inspect)
end

def test_read_eof_returns_partial_line
out = readline_from_piped_stdin("a")
assert_include(out, { result: 'a' }.inspect)
end

def test_require_reline_should_not_trigger_winsize
Expand All @@ -471,7 +491,6 @@ def win?
end

def readline_from_piped_stdin(stdin)
lib = File.expand_path("../../lib", __dir__)
code = <<~'RUBY'
require 'timeout'
begin
Expand All @@ -481,6 +500,11 @@ def readline_from_piped_stdin(stdin)
end
RUBY

run_ruby_with_piped_stdin(code, stdin)
end

def run_ruby_with_piped_stdin(code, stdin)
lib = File.expand_path("../../lib", __dir__)
IO.popen([Reline.test_rubybin, "-I#{lib}", "-rreline", "-e", code], "r+") do |io|
io.write stdin
io.close_write
Expand Down
34 changes: 30 additions & 4 deletions test/reline/yamatanooroti/test_rendering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -965,8 +965,8 @@ def test_nontty
cmd = %Q{ruby -e 'puts(%Q{ello\C-ah\C-e})' | ruby -I#{@pwd}/lib -rreline -e 'p Reline.readline(%{> })' | ruby -e 'print STDIN.read'}
start_terminal(40, 50, ['bash', '-c', cmd])
assert_screen(<<~'EOC')
> hello
"hello"
> ello^Ah^E
"ello\u0001h\u0005"
EOC
close
end
Expand All @@ -976,8 +976,8 @@ def test_eof_with_newline
cmd = %Q{ruby -e 'print(%Q{abc def \\e\\r})' | ruby -I#{@pwd}/lib -rreline -e 'p Reline.readline(%{> })'}
start_terminal(40, 50, ['bash', '-c', cmd])
assert_screen(<<~'EOC')
> abc def
"abc def "
> abc def ^[
"abc def \e"
EOC
close
end
Expand All @@ -993,6 +993,32 @@ def test_eof_without_newline
close
end

def test_nontty_multiline_eof
omit if Reline.core.io_gate.win?
cmd = %Q{ruby -e 'puts(%{hello});print(%{world})' | ruby -I#{@pwd}/lib -rreline -e 'p Reline.readmultiline(%{> }){false}'}
start_terminal(40, 50, ['bash', '-c', cmd])
assert_screen(<<~'EOC')
> hello
> world
"hello\nworld"
EOC
close
end

def test_nontty_multiline
omit if Reline.core.io_gate.win?
cmd = %Q{ruby -e 'puts("def f", "42", "end", "hello")' | ruby -I#{@pwd}/lib -rreline -e 'p Reline.readmultiline(%{> }){|input| input.match?(/end/)}; p gets'}
start_terminal(40, 50, ['bash', '-c', cmd])
assert_screen(<<~'EOC')
> def f
> 42
> end
"def f\n42\nend"
"hello\n"
EOC
close
end

def test_em_set_mark_and_em_exchange_mark
start_terminal(10, 50, %W{ruby -I#{@pwd}/lib #{@pwd}/test/reline/yamatanooroti/multiline_repl}, startup_message: 'Multiline REPL.')
write("aaa bbb ccc ddd\eb\eb\e\x20\eb\C-x\C-xX\C-x\C-xY")
Expand Down