Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move IO#readline to Ruby #8473

Merged
merged 1 commit into from Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/vm.h
Expand Up @@ -58,6 +58,8 @@ VALUE rb_yield_refine_block(VALUE refinement, VALUE refinements);
VALUE ruby_vm_special_exception_copy(VALUE);
PUREFUNC(st_table *rb_vm_fstring_table(void));

void rb_lastline_set_up(VALUE val, unsigned int up);

/* vm_eval.c */
VALUE rb_current_realfilepath(void);
VALUE rb_check_block_call(VALUE, ID, int, const VALUE *, rb_block_call_func_t, VALUE);
Expand Down
35 changes: 20 additions & 15 deletions io.c
Expand Up @@ -4357,22 +4357,28 @@ rb_io_set_lineno(VALUE io, VALUE lineno)
return lineno;
}

/*
* call-seq:
* readline(sep = $/, chomp: false) -> string
* readline(limit, chomp: false) -> string
* readline(sep, limit, chomp: false) -> string
*
* Reads a line as with IO#gets, but raises EOFError if already at end-of-stream.
*
* Optional keyword argument +chomp+ specifies whether line separators
* are to be omitted.
*/

/* :nodoc: */
static VALUE
rb_io_readline(int argc, VALUE *argv, VALUE io)
io_readline(rb_execution_context_t *ec, VALUE io, VALUE sep, VALUE lim, VALUE chomp)
{
VALUE line = rb_io_gets_m(argc, argv, io);
if (NIL_P(lim)) {
// If sep is specified, but it's not a string and not nil, then assume
// it's the limit (it should be an integer)
if (!NIL_P(sep) && NIL_P(rb_check_string_type(sep))) {
// If the user has specified a non-nil / non-string value
// for the separator, we assume it's the limit and set the
// separator to default: rb_rs.
lim = sep;
sep = rb_rs;
}
}

if (!NIL_P(sep)) {
StringValue(sep);
}

VALUE line = rb_io_getline_1(sep, NIL_P(lim) ? -1L : NUM2LONG(lim), RTEST(chomp), io);
rb_lastline_set_up(line, 1);

if (NIL_P(line)) {
rb_eof_error();
Expand Down Expand Up @@ -15420,7 +15426,6 @@ Init_IO(void)
rb_define_method(rb_cIO, "read", io_read, -1);
rb_define_method(rb_cIO, "write", io_write_m, -1);
rb_define_method(rb_cIO, "gets", rb_io_gets_m, -1);
rb_define_method(rb_cIO, "readline", rb_io_readline, -1);
rb_define_method(rb_cIO, "getc", rb_io_getc, 0);
rb_define_method(rb_cIO, "getbyte", rb_io_getbyte, 0);
rb_define_method(rb_cIO, "readchar", rb_io_readchar, 0);
Expand Down
13 changes: 13 additions & 0 deletions io.rb
Expand Up @@ -120,4 +120,17 @@ def read_nonblock(len, buf = nil, exception: true)
def write_nonblock(buf, exception: true)
Primitive.io_write_nonblock(buf, exception)
end

# call-seq:
# readline(sep = $/, chomp: false) -> string
# readline(limit, chomp: false) -> string
# readline(sep, limit, chomp: false) -> string
#
# Reads a line as with IO#gets, but raises EOFError if already at end-of-stream.
#
# Optional keyword argument +chomp+ specifies whether line separators
# are to be omitted.
def readline(sep = $/, limit = nil, chomp: false)
Primitive.io_readline(sep, limit, chomp)
end
end
104 changes: 104 additions & 0 deletions test/ruby/test_io.rb
Expand Up @@ -1898,6 +1898,110 @@ def test_set_lineno_gets
end)
end

def test_readline_bad_param_raises
File.open(__FILE__) do |f|
assert_raise(TypeError) do
f.readline Object.new
end
end

File.open(__FILE__) do |f|
assert_raise(TypeError) do
f.readline 1, 2
end
end
end

def test_readline_raises
File.open(__FILE__) do |f|
assert_equal File.read(__FILE__), f.readline(nil)
assert_raise(EOFError) do
f.readline
end
end
end

def test_readline_separators
File.open(__FILE__) do |f|
line = f.readline("def")
assert_equal File.read(__FILE__)[/\A.*?def/m], line
end

File.open(__FILE__) do |f|
line = f.readline("def", chomp: true)
assert_equal File.read(__FILE__)[/\A.*?(?=def)/m], line
end
end

def test_readline_separators_limits
t = Tempfile.open("readline_limit")
str = "#" * 50
sep = "def"

t.write str
t.write sep
t.write str
t.flush

# over limit
File.open(t.path) do |f|
line = f.readline sep, str.bytesize
assert_equal(str, line)
end

# under limit
File.open(t.path) do |f|
line = f.readline(sep, str.bytesize + 5)
assert_equal(str + sep, line)
end

# under limit + chomp
File.open(t.path) do |f|
line = f.readline(sep, str.bytesize + 5, chomp: true)
assert_equal(str, line)
end
ensure
t&.close!
end

def test_readline_limit_without_separator
t = Tempfile.open("readline_limit")
str = "#" * 50
sep = "\n"

t.write str
t.write sep
t.write str
t.flush

# over limit
File.open(t.path) do |f|
line = f.readline str.bytesize
assert_equal(str, line)
end

# under limit
File.open(t.path) do |f|
line = f.readline(str.bytesize + 5)
assert_equal(str + sep, line)
end

# under limit + chomp
File.open(t.path) do |f|
line = f.readline(str.bytesize + 5, chomp: true)
assert_equal(str, line)
end
ensure
t&.close!
end

def test_readline_chomp_true
File.open(__FILE__) do |f|
line = f.readline(chomp: true)
assert_equal File.readlines(__FILE__).first.chomp, line
end
end

def test_set_lineno_readline
pipe(proc do |w|
w.puts "foo"
Expand Down
11 changes: 11 additions & 0 deletions vm.c
Expand Up @@ -1750,6 +1750,17 @@ rb_lastline_set(VALUE val)
vm_svar_set(GET_EC(), VM_SVAR_LASTLINE, val);
}

void
rb_lastline_set_up(VALUE val, unsigned int up)
{
rb_control_frame_t * cfp = GET_EC()->cfp;

for(unsigned int i = 0; i < up; i++) {
cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
}
vm_cfp_svar_set(GET_EC(), cfp, VM_SVAR_LASTLINE, val);
}

/* misc */

const char *
Expand Down