diff --git a/DOCS/gnu-coreutils/README.md b/DOCS/gnu-coreutils/README.md index fb35aa0..608319c 100644 --- a/DOCS/gnu-coreutils/README.md +++ b/DOCS/gnu-coreutils/README.md @@ -75,7 +75,6 @@ - [shred](shred.md) - 安全删除文件 ### 特殊文件类型 (Special file types) -- [link](link.md) - 通过 link 系统调用创建硬链接 - [ln](ln.md) - 创建文件链接 - [mkdir](mkdir.md) - 创建目录 - [readlink](readlink.md) - 打印符号链接值或规范文件名 diff --git a/DOCS/gnu-coreutils/SUMMARY.md b/DOCS/gnu-coreutils/SUMMARY.md index c9dfe9c..4d1e0cf 100644 --- a/DOCS/gnu-coreutils/SUMMARY.md +++ b/DOCS/gnu-coreutils/SUMMARY.md @@ -4,7 +4,7 @@ ## 文档统计 -- **总文档数**: 102 个命令文档 + 1 个 README;文档数量不等于行为兼容数量 +- **总文档数**: 101 个命令文档 + 1 个 README;文档数量不等于行为兼容数量 - **文档格式**: Markdown - **参考版本**: GNU Coreutils 9.11 @@ -124,7 +124,6 @@ ### 特殊文件类型 (Special file types) | 命令 | 参数数 | 状态 | 文档 | |------|--------|------|------| -| link | 0 | 已实现 | [link.md](link.md) | | ln | 12 | 已实现 | [ln.md](ln.md) | | mkdir | 3 | 已实现 | [mkdir.md](mkdir.md) | | readlink | 7 | 已实现 | [readlink.md](readlink.md) | diff --git a/DOCS/gnu-coreutils/link.md b/DOCS/gnu-coreutils/link.md deleted file mode 100644 index 8ff74cf..0000000 --- a/DOCS/gnu-coreutils/link.md +++ /dev/null @@ -1,57 +0,0 @@ -# link 命令 - -## 命令概述 - -`link` 命令用于创建硬链接。它调用 `link()` 系统调用,创建一个名为 `FILE2` 的新条目(硬链接),指向现有文件 `FILE1`。 - -**命令格式:** -``` -link FILE1 FILE2 -``` - -## 完整参数列表 - -| 短选项 | 长选项 | 说明 | -|--------|--------|------| -| | `--help` | 显示帮助信息并退出 | -| | `--version` | 显示版本信息并退出 | - -## 参数详细说明 - -### `FILE1` -要创建硬链接的源文件路径。必须是一个已存在的文件。 - -### `FILE2` -要创建的新硬链接的路径。如果文件已存在,操作将失败。 - -### `--help` -显示帮助信息,包括命令用法和选项说明,然后退出。 - -### `--version` -显示版本信息,包括版权和许可证信息,然后退出。 - -## 使用示例 - -1. **创建硬链接** - ```bash - link original.txt hardlink.txt - ``` - -2. **创建指向系统文件的硬链接** - ```bash - link /etc/hosts hosts_backup - ``` - -3. **查看帮助信息** - ```bash - link --help - ``` - -4. **查看版本信息** - ```bash - link --version - ``` - -## WinuxCmd 实现状态 - -**待实现** \ No newline at end of file diff --git a/scripts/audit-command-compatibility.py b/scripts/audit-command-compatibility.py index e42c903..c9677e2 100644 --- a/scripts/audit-command-compatibility.py +++ b/scripts/audit-command-compatibility.py @@ -61,7 +61,6 @@ "install", "join", "kill", - "link", "ln", "logname", "ls", diff --git a/scripts/benchmark-command-parity.py b/scripts/benchmark-command-parity.py index 7bef62d..c80dbd9 100644 --- a/scripts/benchmark-command-parity.py +++ b/scripts/benchmark-command-parity.py @@ -724,7 +724,6 @@ class ProbeResult: Probe("du", "apparent bytes single file", ["-b", "du-file.txt"], "fileops", isolated=True), Probe("du", "files0 total bytes", ["-b", "-c", "--files0-from", "du-list0.bin"], "fileops", isolated=True), Probe("truncate", "shrink existing file state", ["-s", "1024", "du-file.txt"], "fileops", compare_stdout=False, isolated=True), - Probe("link", "hard link file state", ["link-src.txt", "link-created.txt"], "fileops", isolated=True), Probe("cp", "recursive directory copy", ["-R", "copy-src", "copy-out"], "fileops", isolated=True), Probe("cp", "no-clobber keeps destination", ["-n", "copy-new.txt", "copy-existing.txt"], "fileops", isolated=True), Probe("cp", "update skips older source", ["-u", "copy-old-src.txt", "copy-newer-dst.txt"], "fileops", isolated=True), diff --git a/src/commands/link.cpp b/src/commands/link.cpp deleted file mode 100644 index 9dda647..0000000 --- a/src/commands/link.cpp +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright © 2026 [caomengxuan666] - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - * - * - File: link.cpp - * - Username: Administrator - * - CopyrightYear: 2026 - */ -/// @contributors: -/// - caomengxuan666 <2507560089@qq.com> -/// @Description: Implementation for link command. -/// @Version: 0.1.0 -/// @License: MIT -/// @Copyright: Copyright © 2026 WinuxCmd - -#include "pch/pch.h" -// include other header after pch.h -#include "core/command_macros.h" - -import std; -import core; -import utils; - -using cmd::meta::OptionMeta; -using cmd::meta::OptionType; - -auto constexpr LINK_OPTIONS = - std::array{OPTION("", "", "create link to file", STRING_TYPE)}; - -namespace { - -auto emit_link_operand_count_error() -> int { - safeErrorPrintLn("link: 2 values required"); - return 1; -} - -auto link_windows_error_text(DWORD error) -> std::string { - Win32ErrorTextOptions options; - options.file_exists = true; - return win32_posix_error_text(error, options); -} - -auto resolve_source_operand(const std::string& source) - -> std::expected { - if (!contains_wildcard(source)) { - return source; - } - - auto glob_result = glob_expand(source); - if (glob_result.expanded && !glob_result.files.empty()) { - if (glob_result.files.size() != 1) { - return std::unexpected("2 values required"); - } - return wstring_to_utf8(glob_result.files[0]); - } - - return source; -} - -} // namespace - -REGISTER_COMMAND( - link, - /* name */ - "link", - - /* synopsis */ - "link [OPTION]... FILE LINKNAME", - "Create a hard link to FILE named LINKNAME.\n" - "\n" - "On Windows, this creates a hard link using CreateHardLink API.\n" - "Note: Hard links only work on NTFS file systems.\n" - "\n" - "This command has no options other than --help and --version.", - " link file.txt link_to_file.txt\n" - " link existing new_link", - - /* see also */ - "ln(1), unlink(1)", "WinuxCmd", "Copyright © 2026 WinuxCmd", LINK_OPTIONS) { - namespace cp = core::pipeline; - - if (ctx.positionals.size() < 2) { - return emit_link_operand_count_error(); - } - if (ctx.positionals.size() > 2) { - return emit_link_operand_count_error(); - } - - std::string source_arg = std::string(ctx.positionals[0]); - auto source_result = resolve_source_operand(source_arg); - if (!source_result) { - if (source_result.error() == "2 values required") { - return emit_link_operand_count_error(); - } - safeErrorPrintLn("link: " + source_result.error()); - return 1; - } - - std::string file = *source_result; - std::string linkname = std::string(ctx.positionals[1]); - - std::wstring wfile = utf8_to_wstring(file); - std::wstring wlinkname = utf8_to_wstring(linkname); - - BOOL result = CreateHardLinkW(wlinkname.c_str(), wfile.c_str(), nullptr); - if (!result) { - DWORD error = GetLastError(); - std::string error_text = contains_wildcard(source_arg) && file == source_arg - ? "No such file or directory" - : link_windows_error_text(error); - safeErrorPrintLn("link: cannot create link '" + linkname + "' to '" + file + - "': " + error_text); - return 1; - } - - return 0; -} diff --git a/src/commands/wpm.cpp b/src/commands/wpm.cpp index 859b9c5..956143d 100644 --- a/src/commands/wpm.cpp +++ b/src/commands/wpm.cpp @@ -456,8 +456,9 @@ auto command_names() -> std::vector { auto legacy_link_names() -> std::vector { // Removed built-in commands that may still exist as hardlinks in old - // installs. - return {"jq"}; + // installs. 'link' was removed because its hardlink shadows the MSVC + // toolchain linker link.exe on PATH. + return {"jq", "link"}; } auto is_legacy_link_name(std::string_view name) -> bool { diff --git a/tests/unit/link/link_unit_test.cpp b/tests/unit/link/link_unit_test.cpp deleted file mode 100644 index 4491996..0000000 --- a/tests/unit/link/link_unit_test.cpp +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright © 2026 [caomengxuan666] - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software", to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - * - * - File: link_unit_test.cpp - * - Username: Administrator - * - CopyrightYear: 2026 - */ -#include "framework/winuxtest.h" - -TEST(link, link_basic) { - TempDir tmp; - tmp.write("original.txt", "hello world"); - - Pipeline p; - p.set_cwd(tmp.wpath()); - p.add(L"link.exe", {L"original.txt", L"link.txt"}); - - TEST_LOG_CMD_LIST("link.exe", L"original.txt", L"link.txt"); - - auto r = p.run(); - - TEST_LOG_EXIT_CODE(r); - - EXPECT_EQ(r.exit_code, 0); -} - -TEST(link, link_missing_operand) { - Pipeline p; - p.add(L"link.exe", {}); - - TEST_LOG_CMD_LIST("link.exe"); - - auto r = p.run(); - - TEST_LOG_EXIT_CODE(r); - TEST_LOG("link stderr", r.stderr_text); - - EXPECT_EQ(r.exit_code, 1); - EXPECT_EQ_TEXT(r.stderr_text, "link: 2 values required\n"); -} - -TEST(link, link_extra_operand) { - Pipeline p; - p.add(L"link.exe", {L"a", L"b", L"c"}); - - TEST_LOG_CMD_LIST("link.exe", L"a", L"b", L"c"); - - auto r = p.run(); - - TEST_LOG_EXIT_CODE(r); - TEST_LOG("link stderr", r.stderr_text); - - EXPECT_EQ(r.exit_code, 1); - EXPECT_EQ_TEXT(r.stderr_text, "link: 2 values required\n"); -} - -TEST(link, link_unknown_option_reports_gnu_style_parse_error) { - Pipeline p; - p.add(L"link.exe", {L"--bogus", L"a", L"b"}); - - auto r = p.run(); - - EXPECT_EQ(r.exit_code, 1); - EXPECT_TRUE(r.stdout_text.empty()); - EXPECT_EQ_TEXT(r.stderr_text, - "link: unrecognized option '--bogus'\n" - "Try 'link --help' for more information.\n"); -} - -TEST(link, link_version_succeeds) { - Pipeline p; - p.add(L"link.exe", {L"--version"}); - - auto r = p.run(); - - EXPECT_EQ(r.exit_code, 0); - EXPECT_NE(r.stdout_text.find("link (WinuxCmd)"), std::string::npos); - EXPECT_TRUE(r.stderr_text.empty()); -} - -TEST(link, link_nonexistent_source_reports_gnu_style_error) { - TempDir tmp; - - Pipeline p; - p.set_cwd(tmp.wpath()); - p.add(L"link.exe", {L"missing.txt", L"copy.txt"}); - - auto r = p.run(); - - EXPECT_EQ(r.exit_code, 1); - EXPECT_TRUE(r.stdout_text.empty()); - EXPECT_EQ_TEXT( - r.stderr_text, - "link: cannot create link 'copy.txt' to 'missing.txt': No such file or " - "directory\n"); - EXPECT_FALSE(std::filesystem::exists(tmp.path / "copy.txt")); -} - -TEST(link, link_same_missing_path_reports_gnu_style_error) { - TempDir tmp; - - Pipeline p; - p.set_cwd(tmp.wpath()); - p.add(L"link.exe", {L"same.txt", L"same.txt"}); - - auto r = p.run(); - - EXPECT_EQ(r.exit_code, 1); - EXPECT_TRUE(r.stdout_text.empty()); - EXPECT_EQ_TEXT( - r.stderr_text, - "link: cannot create link 'same.txt' to 'same.txt': No such file or " - "directory\n"); - EXPECT_FALSE(std::filesystem::exists(tmp.path / "same.txt")); -} - -TEST(link, link_existing_destination_reports_gnu_style_error) { - TempDir tmp; - tmp.write("source.txt", "hello world"); - tmp.write("linked.txt", "existing"); - - Pipeline p; - p.set_cwd(tmp.wpath()); - p.add(L"link.exe", {L"source.txt", L"linked.txt"}); - - auto r = p.run(); - - EXPECT_EQ(r.exit_code, 1); - EXPECT_TRUE(r.stdout_text.empty()); - EXPECT_EQ_TEXT( - r.stderr_text, - "link: cannot create link 'linked.txt' to 'source.txt': File exists\n"); - EXPECT_EQ_TEXT(tmp.read("linked.txt"), "existing"); -} - -TEST(link, link_wildcard_source_single_match_is_expanded) { - TempDir tmp; - tmp.write("source.txt", "hello world"); - - Pipeline p; - p.set_cwd(tmp.wpath()); - p.add(L"link.exe", {L"*.txt", L"linked.txt"}); - - auto r = p.run(); - - EXPECT_EQ(r.exit_code, 0); - EXPECT_TRUE(std::filesystem::exists(tmp.path / "linked.txt")); - EXPECT_EQ_TEXT(tmp.read("linked.txt"), "hello world"); -} - -TEST(link, link_wildcard_source_multiple_matches_are_rejected) { - TempDir tmp; - tmp.write("a.txt", "a"); - tmp.write("b.txt", "b"); - - Pipeline p; - p.set_cwd(tmp.wpath()); - p.add(L"link.exe", {L"*.txt", L"linked.txt"}); - - auto r = p.run(); - - EXPECT_EQ(r.exit_code, 1); - EXPECT_TRUE(r.stdout_text.empty()); - EXPECT_EQ_TEXT(r.stderr_text, "link: 2 values required\n"); - EXPECT_FALSE(std::filesystem::exists(tmp.path / "linked.txt")); -} - -TEST(link, link_wildcard_source_zero_match_falls_back_to_literal_path) { - TempDir tmp; - - Pipeline p; - p.set_cwd(tmp.wpath()); - p.add(L"link.exe", {L"*.txt", L"linked.txt"}); - - auto r = p.run(); - - EXPECT_EQ(r.exit_code, 1); - EXPECT_TRUE(r.stdout_text.empty()); - EXPECT_EQ_TEXT( - r.stderr_text, - "link: cannot create link 'linked.txt' to '*.txt': No such file or " - "directory\n"); - EXPECT_FALSE(std::filesystem::exists(tmp.path / "linked.txt")); -} diff --git a/tests/unit/wpm/wpm_unit_test.cpp b/tests/unit/wpm/wpm_unit_test.cpp index 3f23e86..673560a 100644 --- a/tests/unit/wpm/wpm_unit_test.cpp +++ b/tests/unit/wpm/wpm_unit_test.cpp @@ -544,6 +544,28 @@ TEST(wpm, wpm_links_rebuild_removes_legacy_jq_hardlink) { EXPECT_TRUE(std::filesystem::exists(canonical_exe(tmp.path, L"wpm.exe"))); } +TEST(wpm, wpm_links_rebuild_removes_legacy_link_hardlink) { + TempDir tmp; + auto root_exe = canonical_exe(tmp.path, L"winuxcmd.exe"); + std::filesystem::create_directories(root_exe.parent_path()); + auto legacy_link = tmp.path / L"link.exe"; + std::filesystem::copy_file(build_winuxcmd_path(), root_exe, + std::filesystem::copy_options::overwrite_existing); + bool linked = CreateHardLinkW(legacy_link.wstring().c_str(), + root_exe.wstring().c_str(), nullptr) != 0; + EXPECT_TRUE(linked); + if (!linked) return; + + Pipeline p; + p.add(L"winuxcmd.exe", + {L"wpm", L"links", L"rebuild", L"--root", tmp.wpath()}); + auto r = p.run(); + + EXPECT_EQ(r.exit_code, 0); + EXPECT_FALSE(std::filesystem::exists(legacy_link)); + EXPECT_TRUE(std::filesystem::exists(canonical_exe(tmp.path, L"wpm.exe"))); +} + TEST(wpm, wpm_apply_update_replaces_root_and_rebuilds_links) { TempDir tmp; auto root_exe = canonical_exe(tmp.path, L"winuxcmd.exe");