From 5abb6384a08e112a07e94633ad8d4e2d97d72359 Mon Sep 17 00:00:00 2001 From: Gwen W Date: Tue, 25 Nov 2025 10:14:08 -0500 Subject: [PATCH] sed: Support strict POSIX regexes Adds command line flags to `sed` to require strict POSIX behavior. Extended regexes are still used by default but an ignored -E flag is added for compatibility with other sed versions. --- Tests/Utilities/TestSed.cpp | 7 ++++++ Userland/Utilities/sed.cpp | 43 ++++++++++++++++++++++++++----------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/Tests/Utilities/TestSed.cpp b/Tests/Utilities/TestSed.cpp index 825764b5bf9..d49f9190cf3 100644 --- a/Tests/Utilities/TestSed.cpp +++ b/Tests/Utilities/TestSed.cpp @@ -77,3 +77,10 @@ TEST_CASE(complex) { run_sed({ "h; x; s/./*/gp; x; h; p; x; s/./*/gp", "-n" }, "hello serenity"sv, "**************\nhello serenity\n**************\n"sv); } + +TEST_CASE(strict_posix) +{ + run_sed({ "--posix", "s/(.)/b/" }, "foo\n"sv, "foo\n"sv); + run_sed({ "--posix", "s/\\(.\\)/b/" }, "foo\n"sv, "boo\n"sv); + run_sed({ "--posix", "s/\\(.\\)\\(.*\\)/\\1\\2\\1/" }, "foo\n"sv, "foof\n"sv); +} diff --git a/Userland/Utilities/sed.cpp b/Userland/Utilities/sed.cpp index 5387210af22..5588e910b9c 100644 --- a/Userland/Utilities/sed.cpp +++ b/Userland/Utilities/sed.cpp @@ -242,13 +242,13 @@ struct RArguments : FilepathArgument { }; struct SArguments { - Regex regex; + Variant, Regex> regex; StringView replacement; PosixOptions options; bool print; Optional output_filepath; - static SedErrorOr parse(GenericLexer& lexer) + static SedErrorOr parse(GenericLexer& lexer, bool strict_posix) { auto generic_error_message = "Incomplete substitution command"sv; @@ -307,7 +307,11 @@ struct SArguments { } } - return SArguments { Regex { pattern }, replacement, options, print, output_filepath }; + if (strict_posix) { + return SArguments { Regex { pattern }, replacement, options, print, output_filepath }; + } else { + return SArguments { Regex { pattern }, replacement, options, print, output_filepath }; + } } private: @@ -503,7 +507,7 @@ static SedErrorOr verify_number_of_addresses(Command const& command) return {}; } -static SedErrorOr parse_command(GenericLexer& lexer) +static SedErrorOr parse_command(GenericLexer& lexer, bool strict_posix) { lexer.consume_while(is_ascii_blank); @@ -551,7 +555,7 @@ static SedErrorOr parse_command(GenericLexer& lexer) command.arguments = TRY(RArguments::parse(lexer)); break; case 's': - command.arguments = TRY(SArguments::parse(lexer)); + command.arguments = TRY(SArguments::parse(lexer, strict_posix)); break; case 't': command.arguments = TRY(TArguments::parse(lexer)); @@ -586,7 +590,7 @@ static SedErrorOr parse_command(GenericLexer& lexer) class Script { public: - [[nodiscard]] bool add_script_part(StringView data) + [[nodiscard]] bool add_script_part(StringView data, bool strict_posix) { auto last_pos = m_script.length(); m_script.append(data); @@ -594,7 +598,7 @@ public: while (!lexer.is_eof()) { if (lexer.is_eof()) break; - auto maybe_command = parse_command(lexer); + auto maybe_command = parse_command(lexer, strict_posix); if (maybe_command.is_error()) { warnln("Problem while parsing script part: {}", maybe_command.release_error().message()); return false; @@ -866,7 +870,7 @@ static ErrorOr apply(Command const& command, StringBuilder& patte case 's': { auto pattern_space_sv = pattern_space.string_view(); auto const& s_args = command.arguments->get(); - auto result = s_args.regex.replace(pattern_space_sv, s_args.replacement, s_args.options); + auto result = s_args.regex.visit([&](auto& re) { return re.replace(pattern_space_sv, s_args.replacement, s_args.options); }); auto replacement_made = result != pattern_space_sv; pattern_space.clear(); pattern_space.append(result); @@ -1001,9 +1005,11 @@ ErrorOr serenity_main(Main::Arguments args) bool suppress_default_output = false; bool edit_in_place = false; + bool strict_posix = false; Core::ArgsParser arg_parser; Script script; Vector pos_args; + Vector script_parts; arg_parser.set_general_help("The Stream EDitor"); arg_parser.add_option(suppress_default_output, "suppress default output", nullptr, 'n'); arg_parser.add_option(Core::ArgsParser::Option { @@ -1011,7 +1017,7 @@ ErrorOr serenity_main(Main::Arguments args) .help_string = "A file containing script commands", .short_name = 'f', .value_name = "script-file", - .accept_value = [&script](StringView script_file) { + .accept_value = [&script_parts](StringView script_file) { auto maybe_file = Core::File::open(script_file, Core::File::OpenMode::Read); if (maybe_file.is_error()) { warnln("Failed to open script file: {}", maybe_file.release_error()); @@ -1022,7 +1028,8 @@ ErrorOr serenity_main(Main::Arguments args) warnln("Failed to read contents of script file {}: {}", script_file, maybe_file_contents.release_error()); return false; } - return script.add_script_part(StringView { maybe_file_contents.release_value().bytes() }); + script_parts.append(StringView { maybe_file_contents.release_value().bytes() }); + return true; }, }); arg_parser.add_option(Core::ArgsParser::Option { @@ -1030,14 +1037,24 @@ ErrorOr serenity_main(Main::Arguments args) .help_string = "A script of commands", .short_name = 'e', .value_name = "script", - .accept_value = [&script](StringView script_argument) { - return script.add_script_part(script_argument); + .accept_value = [&script_parts](StringView script_argument) { + script_parts.append(script_argument); + return true; }, }); + arg_parser.add_ignored("regexp-extended", 'E'); + arg_parser.add_option(strict_posix, "Use POSIX regular expressions", "posix", 'p'); 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); + for (StringView part : script_parts) { + if (!script.add_script_part(part, strict_posix)) { + // Warning has already been emitted so just exit. + return 1; + } + } + // When editing in-place, there's also no default output. suppress_default_output |= edit_in_place; @@ -1050,7 +1067,7 @@ ErrorOr serenity_main(Main::Arguments args) warnln("No script specified, aborting"); return 1; } - if (!script.add_script_part(pos_args[0])) { + if (!script.add_script_part(pos_args[0], strict_posix)) { return 1; } pos_args.remove(0);