diff --git a/Userland/Utilities/tail.cpp b/Userland/Utilities/tail.cpp index e4084447f89..0fb55602597 100644 --- a/Userland/Utilities/tail.cpp +++ b/Userland/Utilities/tail.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2018-2020, Andreas Kling + * Copyright (c) 2024, Fabian Dellwing * * SPDX-License-Identifier: BSD-2-Clause */ @@ -20,27 +21,39 @@ static ErrorOr tail_from_pos(Core::File& file, off_t startline) return {}; } -static ErrorOr find_seek_pos(Core::File& file, int wanted_lines) +static ErrorOr find_seek_pos(Core::File& file, int wanted_lines, bool start_from_end) { - // Rather than reading the whole file, start at the end and work backwards, - // stopping when we've found the number of lines we want. - off_t pos = TRY(file.seek(0, SeekMode::FromEndPosition)); - - off_t end = pos; int lines = 0; - for (; pos >= 1; pos--) { - TRY(file.seek(pos - 1, SeekMode::SetPosition)); + if (start_from_end) { + off_t pos = TRY(file.seek(0, SeekMode::FromEndPosition)); + off_t end = pos; + for (; pos >= 1; pos--) { + TRY(file.seek(pos - 1, SeekMode::SetPosition)); + auto ch = TRY(file.read_value()); + if (ch == '\n' && (end - pos) > 0) { + lines++; + if (lines == wanted_lines) + break; + } + } + return pos; + } + + off_t file_size = TRY(file.size()); + off_t pos = 0; + + for (; pos < file_size; pos++) { auto ch = TRY(file.read_value()); - if (ch == '\n' && (end - pos) > 0) { + if (ch == '\n') { lines++; if (lines == wanted_lines) break; } } - return pos; + return pos + 1; } ErrorOr serenity_main(Main::Arguments arguments) @@ -49,12 +62,35 @@ ErrorOr serenity_main(Main::Arguments arguments) bool follow = false; size_t wanted_line_count = DEFAULT_LINE_COUNT; + bool start_from_end = true; StringView file; Core::ArgsParser args_parser; args_parser.set_general_help("Print the end ('tail') of a file."); args_parser.add_option(follow, "Output data as it is written to the file", "follow", 'f'); - args_parser.add_option(wanted_line_count, "Fetch the specified number of lines", "lines", 'n', "number"); + args_parser.add_option(Core::ArgsParser::Option { + .argument_mode = Core::ArgsParser::OptionArgumentMode::Required, + .help_string = "output the last NUM lines, instead of the last 10;" + " or use -n +NUM to output starting with line NUM", + .long_name = "lines", + .short_name = 'n', + .value_name = "[+]NUM", + .accept_value = [&](StringView lines) -> ErrorOr { + Optional value; + if (lines.starts_with('+')) { + value = lines.substring_view(1, lines.length() - 1).to_number(); + start_from_end = false; + } else { + value = lines.to_number(); + } + if (!value.has_value()) { + warnln("Invalid number: {}", lines); + return false; + } + wanted_line_count = value.value(); + return true; + }, + }); args_parser.add_positional_argument(file, "File path", "file", Core::ArgsParser::Required::No); args_parser.parse(arguments); @@ -81,6 +117,26 @@ ErrorOr serenity_main(Main::Arguments arguments) continue; } + if (!start_from_end) { + if (wanted_line_count > line_count) { + continue; + } + if (wanted_line_count == 0) { + out("{}", StringView { bytes }); + continue; + } + for (size_t i = 0; i < bytes.size(); i++) { + auto ch = bytes.at(i); + if (ch == '\n') { + line_index++; + } + if (line_index >= wanted_line_count) + line.append(ch); + } + out("{}", line.to_byte_string().substring_view(1, line.length() - 1)); + continue; + } + for (size_t i = 0; i < bytes.size(); i++) { auto ch = bytes.at(i); line.append(ch); @@ -98,7 +154,7 @@ ErrorOr serenity_main(Main::Arguments arguments) return 0; } - auto pos = TRY(find_seek_pos(*f, wanted_line_count)); + auto pos = TRY(find_seek_pos(*f, wanted_line_count, start_from_end)); TRY(tail_from_pos(*f, pos)); if (follow) {