diff --git a/lib/elixir/lib/file/stream.ex b/lib/elixir/lib/file/stream.ex index 94b2ad20b5b..894055b9270 100644 --- a/lib/elixir/lib/file/stream.ex +++ b/lib/elixir/lib/file/stream.ex @@ -119,7 +119,7 @@ defmodule File.Stream do counter = fn device -> device = skip_bom_and_offset(device, raw, modes) - count_lines(device, path, pattern, read_function(stream), 0) + count_lines(device, path, pattern, read_function(stream), 0, :empty) end {:ok, open!(stream, modes, counter)} @@ -229,21 +229,28 @@ defmodule File.Stream do for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode end - defp count_lines(device, path, pattern, read, count) do + defp count_lines(device, path, pattern, read, count, last_byte) do case read.(device) do + data when is_binary(data) and byte_size(data) > 0 -> + newlines = length(:binary.matches(data, pattern)) + last = :binary.last(data) + count_lines(device, path, pattern, read, count + newlines, last) + data when is_binary(data) -> - count_lines(device, path, pattern, read, count + count_lines(data, pattern)) + count_lines(device, path, pattern, read, count, last_byte) :eof -> - count + case last_byte do + :empty -> 0 + ?\n -> count + _ -> count + 1 + end {:error, reason} -> raise File.Error, reason: reason, action: "stream", path: path end end - defp count_lines(data, pattern), do: length(:binary.matches(data, pattern)) - defp read_function(%{raw: true}), do: &IO.binread(&1, @read_ahead_size) defp read_function(%{raw: false}), do: &IO.read(&1, @read_ahead_size) end diff --git a/lib/elixir/test/elixir/file/stream_test.exs b/lib/elixir/test/elixir/file/stream_test.exs index cade78f0c85..98df2313515 100644 --- a/lib/elixir/test/elixir/file/stream_test.exs +++ b/lib/elixir/test/elixir/file/stream_test.exs @@ -65,6 +65,34 @@ defmodule File.StreamTest do assert Enum.count(stream) == 2 end + test "counts lines without trailing newline" do + no_trailing = tmp_path("no_trailing.txt") + single_line = tmp_path("single_line.txt") + empty_file = tmp_path("empty.txt") + + try do + File.write!(no_trailing, "line1\nline2\nline3") + File.write!(single_line, "hello") + File.write!(empty_file, "") + + # 3 lines, no trailing newline + stream = stream!(@node, no_trailing) + assert Enum.count(stream) == 3 + + # 1 line, no newline at all + stream = stream!(@node, single_line) + assert Enum.count(stream) == 1 + + # empty file + stream = stream!(@node, empty_file) + assert Enum.count(stream) == 0 + after + File.rm(no_trailing) + File.rm(single_line) + File.rm(empty_file) + end + end + test "reads and writes lines" do src = fixture_path("file.txt") dest = tmp_path("tmp_test.txt")