diff --git a/cmake/import_std.cmake b/cmake/import_std.cmake index a1225fe..fcb381a 100644 --- a/cmake/import_std.cmake +++ b/cmake/import_std.cmake @@ -4,4 +4,5 @@ set(_IMPORT_STD_UUID_3_31 "0e5b6991-d74f-4b3d-a41c-cf096e0b2508") set(_IMPORT_STD_UUID_4_0 "a9e1cf81-9932-4810-974b-6eccaf14e457") set(_IMPORT_STD_UUID_4_1 "d0edc3af-4c50-42ea-a356-e2862fe7a444") set(_IMPORT_STD_UUID_4_2 "d0edc3af-4c50-42ea-a356-e2862fe7a444") -set(_IMPORT_STD_UUID_4_3 "451f2fe2-a8a2-47c3-bc32-94786d8fc91b") \ No newline at end of file +set(_IMPORT_STD_UUID_4_3 "451f2fe2-a8a2-47c3-bc32-94786d8fc91b") +set(_IMPORT_STD_UUID_4_4 "f35a9ac6-8463-4d38-8eec-5d6008153e7d") diff --git a/src/commands/find.cpp b/src/commands/find.cpp index 592476b..26c0dbe 100644 --- a/src/commands/find.cpp +++ b/src/commands/find.cpp @@ -121,6 +121,9 @@ auto constexpr FIND_OPTIONS = std::array{ OPTION("-path", "", "file name matches shell pattern PATTERN", STRING_TYPE), OPTION("-ipath", "", "like -path, but the match is case insensitive", STRING_TYPE), + OPTION("-wholename", "", "same as -path", STRING_TYPE), + OPTION("-iwholename", "", "same as -ipath, but case insensitive", + STRING_TYPE), OPTION("-type", "", "file is of type c: b,d,p,f,l,s,D [only d,f,l are supported]", STRING_TYPE), @@ -146,6 +149,9 @@ auto constexpr FIND_OPTIONS = std::array{ "do not follow symbolic links, except while processing command line " "arguments"), OPTION("-P", "", "never follow symbolic links (default)"), + OPTION("-follow", "", "dereference symbolic links"), + OPTION("-O", "", "set optimization level (0-3, currently ignored)", + INT_TYPE), OPTION("-delete", "", "delete files"), OPTION("-exec", "", "execute command", TERMINATED_STRING_TYPE), OPTION("-ok", "", "execute command after confirmation", @@ -165,6 +171,13 @@ auto constexpr FIND_OPTIONS = std::array{ STRING_TYPE), OPTION("-depth", "", "process each directory's contents before the directory itself"), + OPTION("-d", "", "same as -depth"), + OPTION("-daystart", "", "measure times from start of today"), + OPTION("-mount", "", "do not descend into other file systems"), + OPTION("-xdev", "", "same as -mount"), + OPTION("-noleaf", "", "do not optimize by assuming 2+ hard links"), + OPTION("-regextype", "", "set regex syntax (currently ignored)", + STRING_TYPE), OPTION("!", "", "negate expression"), OPTION("-not", "", "negate expression"), OPTION("-a", "", "and expression"), @@ -409,22 +422,46 @@ auto is_unsupported_used(const CommandContext& ctx) auto is_path_option(std::string_view arg) -> bool { return arg == "-name" || arg == "-iname" || arg == "-path" || - arg == "-ipath" || arg == "-regex" || arg == "-iregex" || + arg == "-ipath" || arg == "-wholename" || arg == "-iwholename" || + arg == "-regex" || arg == "-iregex" || arg == "-type" || arg == "-size" || arg == "-empty" || arg == "-mtime" || arg == "-mmin" || arg == "-newer" || arg == "-mindepth" || arg == "-maxdepth" || arg == "-print" || arg == "-print0" || arg == "-delete" || arg == "-exec" || arg == "-ok" || arg == "-printf" || arg == "-prune" || arg == "-quit" || arg == "-true" || arg == "-false" || - arg == "-depth" || arg == "!" || arg == "-not" || arg == "-a" || - arg == "-and" || arg == "-o" || arg == "-or"; + arg == "-depth" || arg == "-d" || arg == "-follow" || + arg == "-mount" || arg == "-xdev" || arg == "-noleaf" || + arg == "-daystart" || arg == "-regextype" || arg == "-O" || + arg == "!" || arg == "-not" || arg == "-a" || arg == "-and" || + arg == "-o" || arg == "-or"; } auto parse_roots(std::span args) -> SmallVector { SmallVector roots; - for (auto arg : args) { - if (arg == "-L" || arg == "-P" || arg == "-H") continue; + for (size_t i = 0; i < args.size(); ++i) { + auto arg = args[i]; + if (roots.empty() && + (arg == "-L" || arg == "-P" || arg == "-H" || arg == "-follow" || + arg == "-mount" || arg == "-xdev" || arg == "-noleaf" || + arg == "-daystart" || arg == "-d")) { + continue; + } + if (roots.empty() && arg == "-O") { + ++i; + continue; + } + if (roots.empty() && arg == "-regextype") { + ++i; + continue; + } + if (roots.empty() && arg.size() > 2 && arg[0] == '-' && arg[1] == 'O' && + std::all_of(arg.begin() + 2, arg.end(), [](char ch) { + return std::isdigit(static_cast(ch)) != 0; + })) { + continue; + } if (is_path_option(arg) || arg == "!" || arg == "(" || arg == ")" || arg == ",") { break; @@ -489,7 +526,7 @@ auto resolve_symlink_mode(const CommandContext& ctx) if (occurrence.index >= FIND_OPTIONS.size()) continue; const auto& meta = FIND_OPTIONS[occurrence.index]; - if (meta.short_name == "-L") { + if (meta.short_name == "-L" || meta.short_name == "-follow") { mode = SymlinkMode::All; } else if (meta.short_name == "-H") { mode = SymlinkMode::CommandLineOnly; @@ -502,12 +539,33 @@ auto resolve_symlink_mode(const CommandContext& ctx) } auto expression_start_index(std::span args) -> size_t { + bool roots_seen = false; for (size_t i = 0; i < args.size(); ++i) { auto arg = args[i]; - if (arg == "-L" || arg == "-P" || arg == "-H") continue; + if (!roots_seen && + (arg == "-L" || arg == "-P" || arg == "-H" || arg == "-follow" || + arg == "-mount" || arg == "-xdev" || arg == "-noleaf" || + arg == "-daystart" || arg == "-d")) { + continue; + } + if (!roots_seen && arg == "-O" && i + 1 < args.size()) { + ++i; + continue; + } + if (!roots_seen && arg == "-regextype" && i + 1 < args.size()) { + ++i; + continue; + } + if (!roots_seen && arg.size() > 2 && arg[0] == '-' && arg[1] == 'O' && + std::all_of(arg.begin() + 2, arg.end(), [](char ch) { + return std::isdigit(static_cast(ch)) != 0; + })) { + continue; + } if (is_path_option(arg) || arg == "(" || arg == ")" || arg == ",") { return i; } + roots_seen = true; } return args.size(); } @@ -572,8 +630,11 @@ class ExpressionParser { token == "-mindepth" || token == "-maxdepth" || token == "-print" || token == "-print0" || token == "-printf" || token == "-prune" || token == "-quit" || token == "-true" || token == "-false" || - token == "-depth" || token == "-delete" || token == "-exec" || - token == "-ok" || token == "-L" || token == "-P" || token == "-H"; + token == "-depth" || token == "-d" || token == "-follow" || + token == "-mount" || token == "-xdev" || token == "-noleaf" || + token == "-daystart" || token == "-regextype" || token == "-O" || + token == "-delete" || token == "-exec" || token == "-ok" || + token == "-L" || token == "-P" || token == "-H"; } auto require_value(std::string_view option) -> cp::Result { @@ -664,13 +725,16 @@ class ExpressionParser { auto option = consume(); if (option == "-name" || option == "-iname" || option == "-path" || - option == "-ipath" || option == "-type") { + option == "-ipath" || option == "-wholename" || + option == "-iwholename" || option == "-type") { auto value = require_value(option); if (!value) return std::unexpected(value.error()); ExprKind kind = ExprKind::Name; if (option == "-iname") kind = ExprKind::IName; if (option == "-path") kind = ExprKind::Path; if (option == "-ipath") kind = ExprKind::IPath; + if (option == "-wholename") kind = ExprKind::Path; + if (option == "-iwholename") kind = ExprKind::IPath; if (option == "-type") { if (*value != "f" && *value != "d" && *value != "l") { return std::unexpected("-type currently supports only f,d,l"); @@ -730,13 +794,17 @@ class ExpressionParser { return node; } - if (option == "-mindepth" || option == "-maxdepth") { + if (option == "-mindepth" || option == "-maxdepth" || option == "-O" || + option == "-regextype") { auto value = require_value(option); if (!value) return std::unexpected(value.error()); return make_expr(ExprKind::Always); } - if (option == "-true" || option == "-depth") { + if (option == "-true" || option == "-depth" || option == "-d" || + option == "-follow" || option == "-mount" || option == "-xdev" || + option == "-noleaf" || option == "-daystart" || + option == "-regextype") { return make_expr(ExprKind::Always); } @@ -826,7 +894,7 @@ auto build_config(const CommandContext& ctx) cfg.maxdepth = ctx.get("-maxdepth", std::numeric_limits::max()); cfg.delete_action = ctx.get("-delete", false); - cfg.depth_first = ctx.get("-depth", false); + cfg.depth_first = ctx.get("-depth", false) || ctx.get("-d", false); if (cfg.delete_action && ctx.get("-prune", false) && !cfg.depth_first) { return std::unexpected( "The -delete action automatically turns on -depth, but -prune does " diff --git a/src/commands/grep.cpp b/src/commands/grep.cpp index 63c18f3..347a57f 100644 --- a/src/commands/grep.cpp +++ b/src/commands/grep.cpp @@ -45,7 +45,33 @@ using cmd::meta::OptionType; static auto grep_is_terminal(FILE* stream) -> bool { int fd = _fileno(stream); - return fd >= 0 && _isatty(fd) != 0; + if (fd < 0) return false; + + DWORD mode = 0; + auto handle = reinterpret_cast(_get_osfhandle(fd)); + if (handle == INVALID_HANDLE_VALUE) return false; + if (GetConsoleMode(handle, &mode) != 0) return true; + + if (GetFileType(handle) != FILE_TYPE_PIPE) return false; + + constexpr DWORD kPipeNameBufferBytes = + sizeof(FILE_NAME_INFO) + (MAX_PATH * sizeof(wchar_t)); + std::vector buffer(kPipeNameBufferBytes); + auto* info = reinterpret_cast(buffer.data()); + if (!GetFileInformationByHandleEx(handle, FileNameInfo, info, + static_cast(buffer.size()))) { + return false; + } + + std::wstring name(info->FileName, + info->FileNameLength / sizeof(wchar_t)); + std::ranges::transform(name, name.begin(), [](wchar_t ch) { + return static_cast(std::towlower(ch)); + }); + + return name.find(L"pty") != std::wstring::npos && + (name.find(L"msys") != std::wstring::npos || + name.find(L"cygwin") != std::wstring::npos); } /** @@ -226,6 +252,16 @@ struct FileSelectionRule { std::string pattern; }; +struct ColorConfig { + std::string matched_selected = "01;31"; + std::string matched_context = "01;31"; + std::string filename = "35"; + std::string line_number = "32"; + std::string byte_offset = "32"; + std::string separator = "36"; + bool no_erase = false; +}; + struct Config { PatternMode mode = PatternMode::BasicRegex; bool ignore_case = false; @@ -258,6 +294,7 @@ struct Config { int after_context = 0; bool context_requested = false; bool color = false; + ColorConfig color_config; BinaryMode binary_mode = BinaryMode::Binary; SmallVector file_selection_rules; SmallVector exclude_dir_patterns; @@ -266,6 +303,85 @@ struct Config { bool initial_tab = false; }; +auto getenv_string(const char* name) -> std::string { + if (const char* value = std::getenv(name); value != nullptr) { + return std::string(value); + } + return {}; +} + +auto build_color_config(std::string_view grep_color, + std::string_view grep_colors) -> ColorConfig { + ColorConfig config; + if (!grep_color.empty()) { + config.matched_selected = std::string(grep_color); + config.matched_context = std::string(grep_color); + } + + size_t start = 0; + while (start <= grep_colors.size()) { + size_t end = grep_colors.find(':', start); + if (end == std::string_view::npos) end = grep_colors.size(); + std::string_view item = grep_colors.substr(start, end - start); + if (item == "ne") { + config.no_erase = true; + } else if (auto pos = item.find('='); pos != std::string_view::npos) { + std::string_view key = item.substr(0, pos); + std::string value(item.substr(pos + 1)); + if (key == "mt") { + config.matched_selected = value; + config.matched_context = value; + } else if (key == "ms") { + config.matched_selected = value; + } else if (key == "mc") { + config.matched_context = value; + } else if (key == "fn") { + config.filename = value; + } else if (key == "ln") { + config.line_number = value; + } else if (key == "bn") { + config.byte_offset = value; + } else if (key == "se") { + config.separator = value; + } + } + if (end == grep_colors.size()) break; + start = end + 1; + } + + return config; +} + +auto append_sgr_prefix(std::string& out, const ColorConfig& cfg, + std::string_view sgr) -> void { + if (sgr.empty()) return; + out.append("\033["); + out.append(sgr); + out.append("m"); + if (!cfg.no_erase) out.append("\033[K"); +} + +auto append_sgr_suffix(std::string& out, const ColorConfig& cfg, + std::string_view sgr) -> void { + if (sgr.empty()) return; + out.append("\033[m"); + if (!cfg.no_erase) out.append("\033[K"); +} + +auto append_colored(std::string& out, const ColorConfig& cfg, + std::string_view sgr, std::string_view text) -> void { + append_sgr_prefix(out, cfg, sgr); + out.append(text); + append_sgr_suffix(out, cfg, sgr); +} + +auto append_colored_char(std::string& out, const ColorConfig& cfg, + std::string_view sgr, char ch) -> void { + append_sgr_prefix(out, cfg, sgr); + out.push_back(ch); + append_sgr_suffix(out, cfg, sgr); +} + auto to_lower_ascii(std::string_view s) -> std::string { std::string out; out.reserve(s.size()); @@ -548,16 +664,26 @@ auto build_config(const CommandContext& ctx) ctx.string_occurrences({"--color", "--colour"})) { color_opt = occurrence.value.empty() ? "auto" : occurrence.value; } - if (color_opt.has_value()) { - if (*color_opt == "always") { - cfg.color = true; - } else if (*color_opt == "auto") { - cfg.color = grep_is_terminal(stdout); - } else if (*color_opt == "never") { - cfg.color = false; - } else { - return std::unexpected("invalid color mode '" + *color_opt + "'"); - } + std::string color_mode = color_opt.value_or("auto"); + if (color_mode == "always") { + cfg.color = true; + } else if (color_mode == "auto") { + cfg.color = grep_is_terminal(stdout); + } else if (color_mode == "never") { + cfg.color = false; + } else { + return std::unexpected("invalid color mode '" + color_mode + "'"); + } + + std::string grep_color = getenv_string("GREP_COLOR"); + std::string grep_colors = getenv_string("GREP_COLORS"); + cfg.color_config = build_color_config(grep_color, grep_colors); + if (cfg.color && !grep_color.empty()) { + safeErrorPrint("grep: warning: GREP_COLOR='"); + safeErrorPrint(grep_color); + safeErrorPrint("' is deprecated; use GREP_COLORS='mt="); + safeErrorPrint(grep_color); + safeErrorPrint("'\n"); } for (const auto& occurrence : ctx.string_occurrences( @@ -750,37 +876,54 @@ auto append_prefix(std::string& out, const Config& cfg, bool show_filename, char separator = ':') -> void { if (show_filename) { if (cfg.color) { - out.append("\033[1;35m"); - out.append(display_name); - out.append("\033[0m"); + append_colored(out, cfg.color_config, cfg.color_config.filename, + display_name); } else { out.append(display_name); } - out.push_back(separator); + if (cfg.color) { + append_colored_char(out, cfg.color_config, cfg.color_config.separator, + separator); + } else { + out.push_back(separator); + } } if (cfg.line_number) { if (cfg.color) { - out.append("\033[1;32m"); auto s = std::to_string(line_no); - out.append(s); - out.append("\033[0m"); + append_colored(out, cfg.color_config, cfg.color_config.line_number, s); } else { auto s = std::to_string(line_no); out.append(s); } - out.push_back(separator); + if (cfg.color) { + append_colored_char(out, cfg.color_config, cfg.color_config.separator, + separator); + } else { + out.push_back(separator); + } } if (cfg.byte_offset) { auto s = std::to_string(offset); - out.append(s); - out.push_back(separator); + if (cfg.color) { + append_colored(out, cfg.color_config, cfg.color_config.byte_offset, s); + } else { + out.append(s); + } + if (cfg.color) { + append_colored_char(out, cfg.color_config, cfg.color_config.separator, + separator); + } else { + out.push_back(separator); + } } } auto append_line_with_color(std::string& out, std::string_view line, - const std::vector& matches, bool color) + const std::vector& matches, + const Config& cfg) -> void { - if (!color || matches.empty()) { + if (!cfg.color || matches.empty()) { out.append(line); return; } @@ -789,9 +932,8 @@ auto append_line_with_color(std::string& out, std::string_view line, if (m.begin > pos) { out.append(line.substr(pos, m.begin - pos)); } - out.append("\033[1;31m"); - out.append(line.substr(m.begin, m.end - m.begin)); - out.append("\033[0m"); + append_colored(out, cfg.color_config, cfg.color_config.matched_selected, + line.substr(m.begin, m.end - m.begin)); pos = m.end; } if (pos < line.size()) { @@ -826,9 +968,9 @@ auto process_selected_record(std::string_view line, bool had_delim, offset + m.begin); if (cfg.initial_tab) output_buf.push_back('\t'); if (cfg.color) { - output_buf.append("\033[1;31m"); - output_buf.append(line.substr(m.begin, m.end - m.begin)); - output_buf.append("\033[0m"); + append_colored(output_buf, cfg.color_config, + cfg.color_config.matched_selected, + line.substr(m.begin, m.end - m.begin)); } else { output_buf.append(line.substr(m.begin, m.end - m.begin)); } @@ -840,7 +982,7 @@ auto process_selected_record(std::string_view line, bool had_delim, append_prefix(output_buf, cfg, show_filename, display_name, line_no, offset); if (cfg.initial_tab) output_buf.push_back('\t'); - append_line_with_color(output_buf, line, matches, cfg.color); + append_line_with_color(output_buf, line, matches, cfg); if (had_delim) { output_buf.append(1, delim); } else { @@ -960,7 +1102,7 @@ auto scan_text(const std::string& text, std::string_view display_name, line_buf.reserve(line.size() + 128); append_prefix(line_buf, cfg, show_filename, display_name, i + 1, b); if (cfg.initial_tab) line_buf.push_back('\t'); - append_line_with_color(line_buf, line, matches, cfg.color); + append_line_with_color(line_buf, line, matches, cfg); if (had_delim) { line_buf.append(1, delim); } else { diff --git a/src/commands/rm.cpp b/src/commands/rm.cpp index 6422020..1a4db34 100644 --- a/src/commands/rm.cpp +++ b/src/commands/rm.cpp @@ -214,18 +214,93 @@ auto confirm_bulk_remove(size_t path_count, bool recursive) -> bool { auto parse_interactive_mode(std::string_view value) -> std::optional { - if (value.empty() || value == "always") { + if (value.empty() || value == "always" || value == "yes") { return InteractiveMode::always; } if (value == "once") { return InteractiveMode::once; } - if (value == "never") { + if (value == "never" || value == "no" || value == "none") { return InteractiveMode::never; } return std::nullopt; } +auto strip_trailing_separators(std::wstring_view path) -> std::wstring_view { + while (path.size() > 1 && (path.back() == L'\\' || path.back() == L'/')) { + if (path.size() == 3 && path[1] == L':') break; + path.remove_suffix(1); + } + return path; +} + +auto path_is_current_or_parent_directory(std::wstring_view path) -> bool { + path = strip_trailing_separators(path); + size_t pos = path.find_last_of(L"\\/"); + auto name = pos == std::wstring_view::npos ? path : path.substr(pos + 1); + if (name.empty() && pos != std::wstring_view::npos) { + auto parent = strip_trailing_separators(path.substr(0, pos)); + pos = parent.find_last_of(L"\\/"); + name = pos == std::wstring_view::npos ? parent : parent.substr(pos + 1); + } + return name == L"." || name == L".."; +} + +auto clear_readonly_attribute(const std::wstring& path, DWORD attr) -> void { + if (attr == INVALID_FILE_ATTRIBUTES || + (attr & FILE_ATTRIBUTE_READONLY) == 0) { + return; + } + SetFileAttributesW(path.c_str(), attr & ~FILE_ATTRIBUTE_READONLY); +} + +auto remove_empty_directory_path(const std::wstring& wpath, + std::string_view display_path, + const RmConfig& cfg) -> bool { + DWORD attr = GetFileAttributesW(wpath.c_str()); + clear_readonly_attribute(wpath, attr); + if (!RemoveDirectoryW(wpath.c_str())) { + DWORD error = GetLastError(); + std::wstring errorMsg = get_system_error_message(error); + safeErrorPrint("rm: cannot remove directory '"); + safeErrorPrint(display_path); + safeErrorPrint("': "); + safeErrorPrint(errorMsg); + safeErrorPrint("\n"); + return false; + } + + if (cfg.verbose) { + safePrint("removed '"); + safePrint(display_path); + safePrint("'\n"); + } + return true; +} + +auto remove_file_path(const std::wstring& wpath, std::string_view display_path, + const RmConfig& cfg) -> bool { + DWORD attr = GetFileAttributesW(wpath.c_str()); + clear_readonly_attribute(wpath, attr); + if (!DeleteFileW(wpath.c_str())) { + DWORD error = GetLastError(); + std::wstring errorMsg = get_system_error_message(error); + safeErrorPrint("rm: cannot remove file '"); + safeErrorPrint(display_path); + safeErrorPrint("': "); + safeErrorPrint(errorMsg); + safeErrorPrint("\n"); + return false; + } + + if (cfg.verbose) { + safePrint("removed '"); + safePrint(display_path); + safePrint("'\n"); + } + return true; +} + auto build_config(const CommandContext& ctx) -> cp::Result { RmConfig cfg; @@ -294,6 +369,13 @@ auto remove_path(const std::string& path, const RmConfig& cfg) -> bool { std::wstring wpath = to_extended_path(utf8_to_wstring(path)); DWORD attr = GetFileAttributesW(wpath.c_str()); + if (cfg.recursive && path_is_current_or_parent_directory(utf8_to_wstring(path))) { + safeErrorPrint("rm: refusing to remove '.' or '..' directory: skipping '"); + safeErrorPrint(path); + safeErrorPrint("'\n"); + return false; + } + if (cfg.preserve_root && is_root_path(path)) { safeErrorPrint("rm: it is dangerous to operate recursively on root '"); safeErrorPrint(path); @@ -358,26 +440,14 @@ auto remove_path(const std::string& path, const RmConfig& cfg) -> bool { return false; } - if (!RemoveDirectoryW(wpath.c_str())) { - DWORD error = GetLastError(); - std::wstring errorMsg = get_system_error_message(error); - safeErrorPrint("rm: cannot remove directory '"); - safeErrorPrint(path); - safeErrorPrint("': "); - safeErrorPrint(errorMsg); - safeErrorPrint("\n"); - return false; - } - - if (cfg.verbose) { - safePrint("removed '"); - safePrint(path); - safePrint("'\n"); - } - return true; + return remove_empty_directory_path(wpath, path, cfg); } if (attr & FILE_ATTRIBUTE_DIRECTORY) { + if (attr & FILE_ATTRIBUTE_REPARSE_POINT) { + return remove_empty_directory_path(wpath, path, cfg); + } + // Recursive function to delete directory with post-order traversal std::function remove_directory_recursive; std::wstring root_volume = @@ -431,23 +501,8 @@ auto remove_path(const std::string& path, const RmConfig& cfg) -> bool { // Store subdirectory for later recursive deletion subdirs.push_back(itemPath); } else { - // Delete file - if (!DeleteFileW(itemPath.c_str())) { - DWORD error = GetLastError(); - std::string itemPathStr = wstring_to_utf8(itemPath); - std::wstring errorMsg = get_system_error_message(error); - safeErrorPrint("rm: cannot remove file '"); - safeErrorPrint(itemPathStr); - safeErrorPrint("': "); - safeErrorPrint(errorMsg); - safeErrorPrint("\n"); + if (!remove_file_path(itemPath, wstring_to_utf8(itemPath), cfg)) { success = false; - } else if (cfg.verbose) { - // OPTIMIZED: Direct conversion - std::string itemPathStr = wstring_to_utf8(itemPath); - safePrint("removed '"); - safePrint(itemPathStr); - safePrint("'\n"); } } } @@ -462,58 +517,33 @@ auto remove_path(const std::string& path, const RmConfig& cfg) -> bool { // Recursively delete all subdirectories (post-order traversal) for (const auto& subdir : subdirs) { - if (!remove_directory_recursive(subdir)) { - return false; + DWORD sub_attr = GetFileAttributesW(subdir.c_str()); + bool sub_success = false; + if (sub_attr != INVALID_FILE_ATTRIBUTES && + (sub_attr & FILE_ATTRIBUTE_REPARSE_POINT)) { + sub_success = + remove_empty_directory_path(subdir, wstring_to_utf8(subdir), cfg); + } else { + sub_success = remove_directory_recursive(subdir); + } + if (!sub_success) { + success = false; } } - // Finally, remove the directory itself - if (!RemoveDirectoryW(dirPath.c_str())) { - DWORD error = GetLastError(); - std::string dirPathStr = wstring_to_utf8(dirPath); - std::wstring errorMsg = get_system_error_message(error); - safeErrorPrint("rm: cannot remove directory '"); - safeErrorPrint(dirPathStr); - safeErrorPrint("': "); - safeErrorPrint(errorMsg); - safeErrorPrint("\n"); + if (!success) { return false; } - if (cfg.verbose) { - // OPTIMIZED: Direct conversion - std::string dirPathStr = wstring_to_utf8(dirPath); - safePrint("removed '"); - safePrint(dirPathStr); - safePrint("'\n"); - } - - return true; + // Finally, remove the directory itself + return remove_empty_directory_path(dirPath, wstring_to_utf8(dirPath), + cfg); }; // Start recursive directory deletion return remove_directory_recursive(wpath); } else { - // Delete regular file - BOOL success = DeleteFileW(wpath.c_str()); - if (!success) { - DWORD error = GetLastError(); - std::wstring errorMsg = get_system_error_message(error); - // OPTIMIZED: Avoid redundant conversions - safeErrorPrint("rm: cannot remove file '"); - safeErrorPrint(path); - safeErrorPrint("': "); - safeErrorPrint(errorMsg); - safeErrorPrint("\n"); - return false; - } - - if (cfg.verbose) { - // OPTIMIZED: Avoid wstring conversion - safePrint("removed '"); - safePrint(path); - safePrint("'\n"); - } + return remove_file_path(wpath, path, cfg); } return true; diff --git a/tests/unit/find/find_unit_test.cpp b/tests/unit/find/find_unit_test.cpp index a757933..50a6e67 100644 --- a/tests/unit/find/find_unit_test.cpp +++ b/tests/unit/find/find_unit_test.cpp @@ -463,6 +463,54 @@ TEST(find, find_path_pattern_matches_full_path) { EXPECT_TRUE(r.stdout_text.find("src/nested/skip.log") == std::string::npos); } +TEST(find, find_wholename_alias_matches_path_pattern) { + TempDir tmp; + std::filesystem::create_directories(tmp.path / "src" / "nested"); + tmp.write("src/nested/match.txt", ""); + tmp.write("src/nested/skip.log", ""); + + Pipeline p; + p.set_cwd(tmp.wpath()); + p.add(L"find.exe", {L"src", L"-wholename", L"src/*/*.txt"}); + + auto r = p.run(); + EXPECT_EQ(r.exit_code, 0); + EXPECT_TRUE(r.stdout_text.find("src/nested/match.txt") != std::string::npos); + EXPECT_TRUE(r.stdout_text.find("src/nested/skip.log") == std::string::npos); +} + +TEST(find, find_iwholename_alias_is_case_insensitive) { + TempDir tmp; + std::filesystem::create_directories(tmp.path / "Case" / "Inner"); + tmp.write("Case/Inner/ReadMe.TXT", ""); + tmp.write("Case/Inner/Other.log", ""); + + Pipeline p; + p.set_cwd(tmp.wpath()); + p.add(L"find.exe", {L"Case", L"-iwholename", L"case/*/*.txt"}); + + auto r = p.run(); + EXPECT_EQ(r.exit_code, 0); + EXPECT_TRUE(r.stdout_text.find("Case/Inner/ReadMe.TXT") != std::string::npos); + EXPECT_TRUE(r.stdout_text.find("Case/Inner/Other.log") == std::string::npos); +} + +TEST(find, find_follow_and_optimization_flags_are_accepted_before_roots) { + TempDir tmp; + std::filesystem::create_directories(tmp.path / "src"); + tmp.write("src/a.txt", ""); + tmp.write("src/b.log", ""); + + Pipeline p; + p.set_cwd(tmp.wpath()); + p.add(L"find.exe", {L"-follow", L"-O2", L"src", L"-name", L"*.txt"}); + + auto r = p.run(); + EXPECT_EQ(r.exit_code, 0); + EXPECT_TRUE(r.stdout_text.find("src/a.txt") != std::string::npos); + EXPECT_TRUE(r.stdout_text.find("src/b.log") == std::string::npos); +} + TEST(find, find_ipath_pattern_is_case_insensitive) { TempDir tmp; std::filesystem::create_directories(tmp.path / "Case" / "Inner"); diff --git a/tests/unit/grep/grep_unit_test.cpp b/tests/unit/grep/grep_unit_test.cpp index 662add9..d821966 100644 --- a/tests/unit/grep/grep_unit_test.cpp +++ b/tests/unit/grep/grep_unit_test.cpp @@ -116,6 +116,73 @@ TEST(grep, grep_color_never_and_colour_alias) { EXPECT_NE(alias_result.stdout_text.find("\x1b["), std::string::npos); } +TEST(grep, grep_color_always_uses_gnu_sgr_erase_sequence) { + TempDir tmp; + tmp.write("a.txt", "alpha\n"); + + Pipeline p; + p.set_cwd(tmp.wpath()); + p.set_env(L"GREP_COLOR", L""); + p.set_env(L"GREP_COLORS", L""); + p.add(L"grep.exe", {L"--color=always", L"alpha", L"a.txt"}); + auto r = p.run(); + + EXPECT_EQ(r.exit_code, 0); + EXPECT_EQ_TEXT(r.stdout_text, "\x1b[01;31m\x1b[Kalpha\x1b[m\x1b[K\n"); +} + +TEST(grep, grep_colors_mt_overrides_match_color) { + TempDir tmp; + tmp.write("a.txt", "alpha\n"); + + Pipeline p; + p.set_cwd(tmp.wpath()); + p.set_env(L"GREP_COLOR", L""); + p.set_env(L"GREP_COLORS", L"mt=36:ne"); + p.add(L"grep.exe", {L"--color=always", L"alpha", L"a.txt"}); + auto r = p.run(); + + EXPECT_EQ(r.exit_code, 0); + EXPECT_EQ_TEXT(r.stdout_text, "\x1b[36malpha\x1b[m\n"); +} + +TEST(grep, grep_colors_controls_prefix_components) { + TempDir tmp; + tmp.write("a.txt", "alpha\n"); + + Pipeline p; + p.set_cwd(tmp.wpath()); + p.set_env(L"GREP_COLOR", L""); + p.set_env(L"GREP_COLORS", L"fn=33:ln=34:se=35:mt=36:ne"); + p.add(L"grep.exe", {L"--color=always", L"-Hn", L"alpha", L"a.txt"}); + auto r = p.run(); + + EXPECT_EQ(r.exit_code, 0); + EXPECT_EQ_TEXT(r.stdout_text, + "\x1b[33ma.txt\x1b[m" + "\x1b[35m:\x1b[m" + "\x1b[34m1\x1b[m" + "\x1b[35m:\x1b[m" + "\x1b[36malpha\x1b[m\n"); +} + +TEST(grep, grep_color_env_is_deprecated_but_applied) { + TempDir tmp; + tmp.write("a.txt", "alpha\n"); + + Pipeline p; + p.set_cwd(tmp.wpath()); + p.set_env(L"GREP_COLOR", L"36"); + p.set_env(L"GREP_COLORS", L""); + p.add(L"grep.exe", {L"--color=always", L"alpha", L"a.txt"}); + auto r = p.run(); + + EXPECT_EQ(r.exit_code, 0); + EXPECT_EQ_TEXT(r.stdout_text, "\x1b[36m\x1b[Kalpha\x1b[m\x1b[K\n"); + EXPECT_TRUE(r.stderr_text.find("GREP_COLOR='36' is deprecated") != + std::string::npos); +} + TEST(grep, grep_color_rejects_invalid_when) { TempDir tmp; tmp.write("a.txt", "alpha\n"); diff --git a/tests/unit/rm/rm_unit_test.cpp b/tests/unit/rm/rm_unit_test.cpp index 3ec66cd..5d57b36 100644 --- a/tests/unit/rm/rm_unit_test.cpp +++ b/tests/unit/rm/rm_unit_test.cpp @@ -24,6 +24,26 @@ */ #include "framework/winuxtest.h" +namespace { + +bool create_directory_junction(const std::filesystem::path& link, + const std::filesystem::path& target) { + std::wstring command = L"cmd /d /c mklink /j \"" + link.wstring() + L"\" \"" + + target.wstring() + L"\" >nul"; + return _wsystem(command.c_str()) == 0; +} + +bool set_readonly(const std::filesystem::path& path) { + DWORD attrs = GetFileAttributesW(path.wstring().c_str()); + if (attrs == INVALID_FILE_ATTRIBUTES) { + return false; + } + return SetFileAttributesW(path.wstring().c_str(), + attrs | FILE_ATTRIBUTE_READONLY) != FALSE; +} + +} // namespace + TEST(rm, rm_basic) { TempDir tmp; tmp.write("file.txt", "content"); @@ -73,6 +93,88 @@ TEST(rm, rm_recursive) { EXPECT_TRUE(!dir_exists); } +TEST(rm, rm_recursive_removes_readonly_file) { + TempDir tmp; + std::filesystem::create_directory(tmp.path / "dir1"); + tmp.write("dir1/file.txt", "content"); + bool readonly_set = set_readonly(tmp.path / "dir1" / "file.txt"); + EXPECT_TRUE(readonly_set); + if (!readonly_set) return; + + Pipeline p; + p.set_cwd(tmp.wpath()); + p.add(L"rm.exe", {L"-r", L"-f", L"dir1"}); + auto r = p.run(); + + EXPECT_EQ(r.exit_code, 0); + EXPECT_FALSE(std::filesystem::exists(tmp.path / "dir1")); +} + +TEST(rm, rm_recursive_removes_readonly_directory) { + TempDir tmp; + std::filesystem::create_directories(tmp.path / "dir1" / "subdir"); + tmp.write("dir1/subdir/file.txt", "content"); + bool readonly_set = set_readonly(tmp.path / "dir1" / "subdir"); + EXPECT_TRUE(readonly_set); + if (!readonly_set) return; + + Pipeline p; + p.set_cwd(tmp.wpath()); + p.add(L"rm.exe", {L"-r", L"-f", L"dir1"}); + auto r = p.run(); + + EXPECT_EQ(r.exit_code, 0); + EXPECT_FALSE(std::filesystem::exists(tmp.path / "dir1")); +} + +TEST(rm, rm_recursive_removes_directory_junction_without_deleting_target) { + TempDir tmp; + std::filesystem::create_directories(tmp.path / "tree" / "target"); + tmp.write("tree/target/keep.txt", "content"); + bool created = create_directory_junction(tmp.path / "tree" / "link", + tmp.path / "tree" / "target"); + EXPECT_TRUE(created); + if (!created) return; + + Pipeline p; + p.set_cwd(tmp.wpath()); + p.add(L"rm.exe", {L"-r", L"-f", L"tree\\link"}); + auto r = p.run(); + + EXPECT_EQ(r.exit_code, 0); + EXPECT_FALSE(std::filesystem::exists(tmp.path / "tree" / "link")); + EXPECT_TRUE(std::filesystem::exists(tmp.path / "tree" / "target" / + "keep.txt")); +} + +TEST(rm, rm_recursive_refuses_current_or_parent_directory_operands) { + TempDir current_dir; + current_dir.write("keep.txt", "content"); + + Pipeline current_pipeline; + current_pipeline.set_cwd(current_dir.wpath()); + current_pipeline.add(L"rm.exe", {L"-r", L"-f", L"."}); + auto current_result = current_pipeline.run(); + + EXPECT_EQ(current_result.exit_code, 1); + EXPECT_TRUE(current_result.stderr_text.find("refusing to remove") != + std::string::npos); + EXPECT_TRUE(std::filesystem::exists(current_dir.path / "keep.txt")); + + TempDir parent_dir; + parent_dir.write("keep.txt", "content"); + + Pipeline parent_pipeline; + parent_pipeline.set_cwd(parent_dir.wpath()); + parent_pipeline.add(L"rm.exe", {L"-r", L"-f", L".."}); + auto parent_result = parent_pipeline.run(); + + EXPECT_EQ(parent_result.exit_code, 1); + EXPECT_TRUE(parent_result.stderr_text.find("refusing to remove") != + std::string::npos); + EXPECT_TRUE(std::filesystem::exists(parent_dir.path / "keep.txt")); +} + TEST(rm, rm_force) { TempDir tmp; tmp.write("file.txt", "content"); @@ -251,6 +353,47 @@ TEST(rm, rm_interactive_never_does_not_prompt) { EXPECT_FALSE(std::filesystem::exists(tmp.path / "file.txt")); } +TEST(rm, rm_interactive_accepts_gnu_aliases) { + TempDir no_alias; + no_alias.write("file.txt", "content"); + + Pipeline no_pipeline; + no_pipeline.set_cwd(no_alias.wpath()); + no_pipeline.set_stdin("y\n"); + no_pipeline.add(L"rm.exe", {L"--interactive=no", L"file.txt"}); + auto no_result = no_pipeline.run(); + + EXPECT_EQ(no_result.exit_code, 0); + EXPECT_TRUE(no_result.stderr_text.find("remove 'file.txt'?") == + std::string::npos); + EXPECT_FALSE(std::filesystem::exists(no_alias.path / "file.txt")); + + TempDir yes_alias; + yes_alias.write("file.txt", "content"); + + Pipeline yes_pipeline; + yes_pipeline.set_cwd(yes_alias.wpath()); + yes_pipeline.set_stdin("n\n"); + yes_pipeline.add(L"rm.exe", {L"--interactive=yes", L"file.txt"}); + auto yes_result = yes_pipeline.run(); + + EXPECT_EQ(yes_result.exit_code, 0); + EXPECT_TRUE(yes_result.stderr_text.find("remove 'file.txt'?") != + std::string::npos); + EXPECT_TRUE(std::filesystem::exists(yes_alias.path / "file.txt")); + + TempDir none_alias; + none_alias.write("file.txt", "content"); + + Pipeline none_pipeline; + none_pipeline.set_cwd(none_alias.wpath()); + none_pipeline.add(L"rm.exe", {L"--interactive=none", L"file.txt"}); + auto none_result = none_pipeline.run(); + + EXPECT_EQ(none_result.exit_code, 0); + EXPECT_FALSE(std::filesystem::exists(none_alias.path / "file.txt")); +} + TEST(rm, rm_interactive_always_declines_single_remove) { TempDir tmp; tmp.write("file.txt", "content");