diff --git a/Userland/Utilities/CMakeLists.txt b/Userland/Utilities/CMakeLists.txt index f672eaead5..3d87cac51d 100644 --- a/Userland/Utilities/CMakeLists.txt +++ b/Userland/Utilities/CMakeLists.txt @@ -125,7 +125,7 @@ target_link_libraries(pls PRIVATE LibCrypt) target_link_libraries(pro PRIVATE LibFileSystem LibProtocol LibHTTP) target_link_libraries(run-tests PRIVATE LibCoredump LibDebug LibFileSystem LibRegex) target_link_libraries(rm PRIVATE LibFileSystem) -target_link_libraries(sed PRIVATE LibRegex) +target_link_libraries(sed PRIVATE LibRegex LibFileSystem) target_link_libraries(shot PRIVATE LibGfx LibGUI LibIPC) target_link_libraries(sql PRIVATE LibFileSystem LibIPC LibLine LibSQL) target_link_libraries(su PRIVATE LibCrypt) diff --git a/Userland/Utilities/sed.cpp b/Userland/Utilities/sed.cpp index 3c37677263..e6a4e72d01 100644 --- a/Userland/Utilities/sed.cpp +++ b/Userland/Utilities/sed.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2022, Eli Youngs * Copyright (c) 2023, Rodrigo Tobar + * Copyright (c) 2023, kleines Filmröllchen * * SPDX-License-Identifier: BSD-2-Clause */ @@ -8,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +18,8 @@ #include #include #include +#include +#include #include #include #include @@ -605,28 +609,55 @@ enum class CycleDecision { Quit }; -class InputFile { - AK_MAKE_NONCOPYABLE(InputFile); +// In most cases, just an input to sed. However, files are also written to when the -i option is used. +class File { + AK_MAKE_NONCOPYABLE(File); - InputFile(NonnullOwnPtr&& file) - : m_file(move(file)) + File(LexicalPath input_file_path, NonnullOwnPtr&& file, OwnPtr&& output, OwnPtr&& temp_file) + : m_input_file_path(move(input_file_path)) + , m_file(move(file)) + , m_output(move(output)) + , m_output_temp_file(move(temp_file)) { } public: - static ErrorOr create(NonnullOwnPtr&& file) + // Used for -i mode. + static ErrorOr create_with_output_file(LexicalPath input_path, NonnullOwnPtr&& file) { auto buffered_file = TRY(Core::BufferedFile::create(move(file))); - return InputFile(move(buffered_file)); + auto temp_file = TRY(FileSystem::TempFile::create_temp_file()); + // Open the file as read-write, since we need to later copy its contents to the original file. + auto output_file = TRY(Core::File::open(temp_file->path(), Core::File::OpenMode::ReadWrite | Core::File::OpenMode::Truncate)); + return File { move(input_path), move(buffered_file), move(output_file), move(temp_file) }; } - static ErrorOr create_from_stdin() + // Used for non -i mode. + static ErrorOr create(LexicalPath input_path, NonnullOwnPtr&& file) { - return create(TRY(Core::File::standard_input())); + auto buffered_file = TRY(Core::BufferedFile::create(move(file))); + return File { move(input_path), move(buffered_file), nullptr, nullptr }; } - InputFile(InputFile&&) = default; - InputFile& operator=(InputFile&&) = default; + static ErrorOr create_from_stdin() + { + // While this path is correct, we don't ever use it since there's no output file to be copied over. + return create(LexicalPath { "/proc/self/fd/0" }, TRY(Core::File::standard_input())); + } + + static ErrorOr create_from_stdout() + { + // We hack standard output into `File` to avoid having two versions of `write_pattern_space`. + return File { + LexicalPath { "/proc/self/fd/1" }, + TRY(Core::BufferedFile::create(TRY(Core::File::standard_input()))), + TRY(Core::File::standard_output()), + nullptr, + }; + } + + File(File&&) = default; + File& operator=(File&&) = default; ErrorOr has_next() const { @@ -641,17 +672,44 @@ public: return m_current_line; } + ErrorOr write_until_depleted(ReadonlyBytes buffer) + { + // If we're not in -i mode, stdout, not us, is responsible for writing the output. + if (!m_output) + return {}; + return m_output->write_until_depleted(buffer); + } + size_t line_number() const { return m_line_number; } + ErrorOr copy_output_to_original_file() + { + if (!m_output) + return {}; + VERIFY(m_output->is_open()); + + TRY(m_output->seek(0, SeekMode::SetPosition)); + auto source_stat = TRY(Core::System::stat(m_output_temp_file->path())); + return FileSystem::copy_file( + m_input_file_path.string(), m_output_temp_file->path(), source_stat, *m_output, + FileSystem::PreserveMode::Ownership | FileSystem::PreserveMode::Permissions); + } + private: + LexicalPath m_input_file_path; NonnullOwnPtr m_file; + + // Only in use if we're editing in place. + OwnPtr m_output; + OwnPtr m_output_temp_file; + size_t m_line_number { 0 }; DeprecatedString m_current_line; constexpr static size_t MAX_SUPPORTED_LINE_SIZE = 4096; Array m_buffer; }; -static ErrorOr write_pattern_space(Core::File& output, StringBuilder& pattern_space) +static ErrorOr write_pattern_space(File& output, StringBuilder& pattern_space) { TRY(output.write_until_depleted(pattern_space.string_view().bytes())); TRY(output.write_until_depleted("\n"sv.bytes())); @@ -699,9 +757,8 @@ static void print_unambiguous(StringView pattern_space) outln("{}$", unambiguous_output.string_view()); } -static ErrorOr apply(Command const& command, StringBuilder& pattern_space, StringBuilder& hold_space, InputFile& input, bool suppress_default_output) +static ErrorOr apply(Command const& command, StringBuilder& pattern_space, StringBuilder& hold_space, File& input, File& stdout, bool suppress_default_output) { - auto stdout = TRY(Core::File::standard_output()); auto cycle_decision = CycleDecision::None; switch (command.function) { @@ -731,19 +788,20 @@ static ErrorOr apply(Command const& command, StringBuilder& patte break; case 'n': if (!suppress_default_output) - TRY(write_pattern_space(*stdout, pattern_space)); + TRY(write_pattern_space(stdout, pattern_space)); + TRY(write_pattern_space(input, pattern_space)); if (TRY(input.has_next())) { pattern_space.clear(); pattern_space.append(TRY(input.next())); } break; case 'p': - TRY(write_pattern_space(*stdout, pattern_space)); + TRY(write_pattern_space(stdout, pattern_space)); break; case 'P': { auto pattern_sv = pattern_space.string_view(); auto newline_position = pattern_sv.find('\n').value_or(pattern_sv.length() - 1); - TRY(stdout->write_until_depleted(pattern_sv.substring_view(0, newline_position + 1).bytes())); + TRY(stdout.write_until_depleted(pattern_sv.substring_view(0, newline_position + 1).bytes())); break; } case 'q': @@ -757,7 +815,8 @@ static ErrorOr apply(Command const& command, StringBuilder& patte pattern_space.clear(); pattern_space.append(result); if (replacement_made && s_args.print) - TRY(write_pattern_space(*stdout, pattern_space)); + TRY(write_pattern_space(stdout, pattern_space)); + TRY(write_pattern_space(input, pattern_space)); break; } case 'x': @@ -776,16 +835,17 @@ static ErrorOr apply(Command const& command, StringBuilder& patte return cycle_decision; } -static ErrorOr run(Vector& inputs, Script& script, bool suppress_default_output) +static ErrorOr run(Vector& inputs, Script& script, bool suppress_default_output) { // TODO: verify all commands are valid StringBuilder pattern_space; StringBuilder hold_space; - auto stdout = TRY(Core::File::standard_output()); // TODO: extend to multiple input files auto& input = inputs[0]; + auto stdout = TRY(File::create_from_stdout()); + // main loop while (TRY(input.has_next())) { @@ -805,7 +865,7 @@ static ErrorOr run(Vector& inputs, Script& script, bool suppres for (auto& command : script.commands()) { if (!command.is_enabled()) continue; - auto command_cycle_decision = TRY(apply(command, pattern_space, hold_space, input, suppress_default_output)); + auto command_cycle_decision = TRY(apply(command, pattern_space, hold_space, input, stdout, suppress_default_output)); if (command_cycle_decision == CycleDecision::Next || command_cycle_decision == CycleDecision::Quit) { cycle_decision = command_cycle_decision; break; @@ -818,7 +878,7 @@ static ErrorOr run(Vector& inputs, Script& script, bool suppres break; if (!suppress_default_output) - TRY(write_pattern_space(*stdout, pattern_space)); + TRY(write_pattern_space(stdout, pattern_space)); pattern_space.clear(); } return {}; @@ -826,9 +886,10 @@ static ErrorOr run(Vector& inputs, Script& script, bool suppres ErrorOr serenity_main(Main::Arguments args) { - TRY(Core::System::pledge("stdio cpath rpath wpath")); + TRY(Core::System::pledge("stdio cpath rpath wpath fattr chown")); bool suppress_default_output = false; + bool edit_in_place = false; Core::ArgsParser arg_parser; Script script; Vector pos_args; @@ -862,9 +923,17 @@ ErrorOr serenity_main(Main::Arguments args) return script.add_script_part(script_argument); }, }); + arg_parser.add_option(edit_in_place, "Edit file in place, implies -n", "in-place", 'i'); arg_parser.add_positional_argument(pos_args, "script and/or file", "...", Core::ArgsParser::Required::No); arg_parser.parse(args); + // When editing in-place, there's also no default output. + suppress_default_output |= edit_in_place; + + // We only need fattr and chown for in-place editing. + if (!edit_in_place) + TRY(Core::System::pledge("stdio cpath rpath wpath")); + if (script.commands().is_empty()) { if (pos_args.is_empty()) { warnln("No script specified, aborting"); @@ -877,28 +946,36 @@ ErrorOr serenity_main(Main::Arguments args) } for (auto const& input_filename : TRY(script.input_filenames())) { - TRY(Core::System::unveil(TRY(FileSystem::absolute_path(input_filename)), "r"sv)); + TRY(Core::System::unveil(TRY(FileSystem::absolute_path(input_filename)), edit_in_place ? "rwc"sv : "r"sv)); } for (auto const& output_filename : TRY(script.output_filenames())) { TRY(Core::System::unveil(TRY(FileSystem::absolute_path(output_filename)), "w"sv)); } + TRY(Core::System::unveil("/tmp"sv, "rwc"sv)); - Vector inputs; + Vector inputs; for (auto const& filename : pos_args) { if (filename == "-"sv) { - inputs.empend(TRY(InputFile::create_from_stdin())); + inputs.empend(TRY(File::create_from_stdin())); } else { TRY(Core::System::unveil(TRY(FileSystem::absolute_path(filename)), edit_in_place ? "rwc"sv : "r"sv)); auto file = TRY(Core::File::open(filename, Core::File::OpenMode::Read)); - inputs.empend(TRY(InputFile::create(move(file)))); + if (edit_in_place) + inputs.empend(TRY(File::create_with_output_file(LexicalPath { filename }, move(file)))); + else + inputs.empend(TRY(File::create(LexicalPath { filename }, move(file)))); } } TRY(Core::System::unveil(nullptr, nullptr)); if (inputs.is_empty()) { - inputs.empend(TRY(InputFile::create_from_stdin())); + inputs.empend(TRY(File::create_from_stdin())); } TRY(run(inputs, script, suppress_default_output)); + + for (auto& input : inputs) + TRY(input.copy_output_to_original_file()); + return 0; }