Skip to content

Commit d98ca95

Browse files
committed
command/unlink: implement unlink command
1 parent a77a8f4 commit d98ca95

File tree

3 files changed

+261
-1
lines changed

3 files changed

+261
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Any tools not in GNU coreutils are acceptable as well.
2121
* touch
2222
* true
2323
* uname
24+
* unlink
2425
* whoami
2526
* yes
2627

@@ -122,7 +123,6 @@ Any tools not in GNU coreutils are acceptable as well.
122123
* tty
123124
* unexpand
124125
* uniq
125-
* unlink
126126
* uptime
127127
* users
128128
* vdir

src/commands/listing.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub const commands: []const type = &.{
1212
@import("touch.zig"),
1313
@import("true.zig"),
1414
@import("uname.zig"),
15+
@import("unlink.zig"),
1516
@import("whoami.zig"),
1617
@import("yes.zig"),
1718
};

src/commands/unlink.zig

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
// SPDX-License-Identifier: MIT
2+
// SPDX-FileCopyrightText: 2025 Lee Cannon <leecannon@leecannon.xyz>
3+
4+
/// Is this command enabled for the current target?
5+
pub const enabled: bool = true;
6+
7+
pub const command: Command = .{
8+
.name = "unlink",
9+
10+
.short_help =
11+
\\Usage: {NAME} FILE...
12+
\\ or: {NAME} OPTION
13+
\\
14+
\\Call the unlink function to remove each FILE.
15+
\\
16+
\\ -h display the short help and exit
17+
\\ --help display the full help and exit
18+
\\ --version output version information and exit
19+
\\
20+
,
21+
22+
.extended_help =
23+
\\Examples:
24+
\\ unlink FILE
25+
\\ unlink FILE1 FILE2
26+
\\
27+
,
28+
29+
.execute = impl.execute,
30+
};
31+
32+
// namespace required to prevent tests of disabled commands from being analyzed
33+
const impl = struct {
34+
fn execute(
35+
allocator: std.mem.Allocator,
36+
io: IO,
37+
args: *Arg.Iterator,
38+
system: System,
39+
exe_path: []const u8,
40+
) Command.Error!void {
41+
const z: tracy.Zone = .begin(.{ .src = @src(), .name = command.name });
42+
defer z.end();
43+
44+
var opt_arg: ?Arg = (try args.nextWithHelpOrVersion(true)) orelse
45+
return command.printInvalidUsage(
46+
io,
47+
exe_path,
48+
"missing file operand",
49+
);
50+
51+
var cwd = system.cwd();
52+
53+
while (opt_arg) |file_arg| : (opt_arg = args.next()) {
54+
const file_path = file_arg.raw;
55+
56+
const file_zone: tracy.Zone = .begin(.{ .src = @src(), .name = "unlink file" });
57+
defer file_zone.end();
58+
file_zone.text(file_path);
59+
60+
log.debug("unlinking file '{s}'", .{file_path});
61+
62+
cwd.unlinkFile(file_path) catch |err| return command.printErrorAlloc(
63+
allocator,
64+
io,
65+
"failed to unlink '{s}': {s}",
66+
.{ file_path, @errorName(err) },
67+
);
68+
}
69+
}
70+
71+
test "unlink no args" {
72+
try command.testError(
73+
&.{},
74+
.{},
75+
"missing file operand",
76+
);
77+
}
78+
79+
test "unlink no file" {
80+
const fs_description = try System.TestBackend.Description.FileSystemDescription.create(
81+
std.testing.allocator,
82+
);
83+
defer fs_description.destroy();
84+
85+
try command.testError(
86+
&.{"non-existent"},
87+
.{
88+
.system_description = .{ .file_system = fs_description },
89+
},
90+
"failed to unlink 'non-existent': FileNotFound",
91+
);
92+
}
93+
94+
test "unlink directory" {
95+
const fs_description = try System.TestBackend.Description.FileSystemDescription.create(
96+
std.testing.allocator,
97+
);
98+
defer fs_description.destroy();
99+
100+
_ = try fs_description.root.addDirectory("dir");
101+
102+
try command.testError(
103+
&.{"dir"},
104+
.{
105+
.system_description = .{ .file_system = fs_description },
106+
},
107+
"failed to unlink 'dir': IsDirectory",
108+
);
109+
}
110+
111+
test "unlink single file" {
112+
const fs_description = try System.TestBackend.Description.FileSystemDescription.create(
113+
std.testing.allocator,
114+
);
115+
defer fs_description.destroy();
116+
117+
_ = try fs_description.root.addFile("file1", "contents");
118+
_ = try fs_description.root.addFile("file2", "contents");
119+
120+
var system: System = undefined;
121+
defer system._backend.destroy();
122+
123+
try command.testExecute(
124+
&.{"file1"},
125+
.{
126+
.system_description = .{ .file_system = fs_description },
127+
.test_backend_behaviour = .{ .provide = &system },
128+
},
129+
);
130+
131+
const test_backend: *System.TestBackend = system._backend;
132+
const file_system = test_backend.file_system.?;
133+
134+
try std.testing.expect(file_system.root.subdata.dir.entries.get("file1") == null);
135+
try std.testing.expect(file_system.root.subdata.dir.entries.get("file2") != null);
136+
try shared.customExpectEqual(file_system.entries.count(), 2); // root and file2
137+
}
138+
139+
test "unlink multiple files" {
140+
const fs_description = try System.TestBackend.Description.FileSystemDescription.create(
141+
std.testing.allocator,
142+
);
143+
defer fs_description.destroy();
144+
145+
_ = try fs_description.root.addFile("file1", "contents");
146+
_ = try fs_description.root.addFile("file2", "contents");
147+
148+
var system: System = undefined;
149+
defer system._backend.destroy();
150+
151+
try command.testExecute(
152+
&.{ "file1", "file2" },
153+
.{
154+
.system_description = .{ .file_system = fs_description },
155+
.test_backend_behaviour = .{ .provide = &system },
156+
},
157+
);
158+
159+
const test_backend: *System.TestBackend = system._backend;
160+
const file_system = test_backend.file_system.?;
161+
162+
try std.testing.expect(file_system.root.subdata.dir.entries.get("file1") == null);
163+
try std.testing.expect(file_system.root.subdata.dir.entries.get("file2") == null);
164+
try shared.customExpectEqual(file_system.entries.count(), 1); // root
165+
}
166+
167+
test "complex paths" {
168+
const fs_description = try System.TestBackend.Description.FileSystemDescription.create(
169+
std.testing.allocator,
170+
);
171+
defer fs_description.destroy();
172+
173+
{
174+
const dir1 = try fs_description.root.addDirectory("dir1");
175+
_ = try dir1.addFile("file1", "contents");
176+
_ = try dir1.addFile("file2", "contents");
177+
178+
const dir2 = try dir1.addDirectory("dir2");
179+
_ = try dir2.addFile("file3", "contents");
180+
_ = try dir2.addFile("file4", "contents");
181+
}
182+
183+
var system: System = undefined;
184+
defer system._backend.destroy();
185+
186+
try command.testExecute(
187+
&.{
188+
"dir1/file1", // relative to cwd
189+
"dir1/dir2/../file2", // .. traversal
190+
"dir1/dir2/file3",
191+
"/dir1/dir2/file4", // absolute path
192+
},
193+
.{
194+
.system_description = .{ .file_system = fs_description },
195+
.test_backend_behaviour = .{ .provide = &system },
196+
},
197+
);
198+
199+
const test_backend: *System.TestBackend = system._backend;
200+
const file_system = test_backend.file_system.?;
201+
202+
const dir1 = file_system.root.subdata.dir.entries.get("dir1").?.subdata.dir;
203+
try std.testing.expect(dir1.entries.get("file1") == null);
204+
try std.testing.expect(dir1.entries.get("file2") == null);
205+
206+
const dir2 = dir1.entries.get("dir2").?.subdata.dir;
207+
try std.testing.expect(dir2.entries.get("file3") == null);
208+
try std.testing.expect(dir2.entries.get("file4") == null);
209+
210+
try shared.customExpectEqual(file_system.entries.count(), 3); // root, dir1 and dir2
211+
}
212+
213+
test "unlink help" {
214+
try command.testHelp(true);
215+
}
216+
217+
test "unlink version" {
218+
try command.testVersion();
219+
}
220+
221+
test "unlink fuzz" {
222+
const fs_description = try System.TestBackend.Description.FileSystemDescription.create(
223+
std.testing.allocator,
224+
);
225+
defer fs_description.destroy();
226+
227+
{
228+
const dir1 = try fs_description.root.addDirectory("dir1");
229+
_ = try dir1.addFile("file1", "contents");
230+
_ = try dir1.addFile("file2", "contents");
231+
232+
const dir2 = try dir1.addDirectory("dir2");
233+
_ = try dir2.addFile("file3", "contents");
234+
_ = try dir2.addFile("file4", "contents");
235+
}
236+
237+
try command.testFuzz(.{
238+
.expect_stdout_output_on_success = false,
239+
.system_description = .{ .file_system = fs_description },
240+
.corpus = &.{
241+
"dir1/file1",
242+
"dir1/dir2/../file2",
243+
"dir1/dir2/file3",
244+
"/dir1/dir2/file4",
245+
},
246+
});
247+
}
248+
};
249+
250+
const Arg = @import("../Arg.zig");
251+
const Command = @import("../Command.zig");
252+
const IO = @import("../IO.zig");
253+
const shared = @import("../shared.zig");
254+
const System = @import("../system/System.zig");
255+
256+
const log = std.log.scoped(.unlink);
257+
258+
const std = @import("std");
259+
const tracy = @import("tracy");

0 commit comments

Comments
 (0)