Repository navigation
Expand file tree
/
Copy pathfiles.zig
More file actions
147 lines (128 loc) · 6.41 KB
/
Copy pathfiles.zig
File metadata and controls
147 lines (128 loc) · 6.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
const std = @import("std");
const is_windows = @import("builtin").os.tag == .windows;
const windows_glob = @import("glob_windows.zig");
const c = if (is_windows) struct {} else @cImport({
@cInclude("fnmatch.h");
});
pub const Selection = struct {
allocator: std.mem.Allocator,
io: std.Io,
paths: std.StringHashMap(void),
pub fn init(allocator: std.mem.Allocator, io: std.Io) Selection {
return .{ .allocator = allocator, .io = io, .paths = std.StringHashMap(void).init(allocator) };
}
pub fn addFile(self: *Selection, path: []const u8) !void {
const stat = try std.Io.Dir.cwd().statFile(self.io, path, .{ .follow_symlinks = false });
if (stat.kind != .file or stat.nlink != 1) return error.WriteRequiresSingleLinkRegularFile;
const absolute = try std.Io.Dir.cwd().realPathFileAlloc(self.io, path, self.allocator);
if (self.paths.contains(absolute)) {
self.allocator.free(absolute);
return;
}
try self.paths.put(absolute, {});
}
pub fn directory(self: *Selection, path: []const u8, recursive: bool) !void {
var dir = try std.Io.Dir.cwd().openDir(self.io, path, .{ .iterate = true, .follow_symlinks = false });
defer dir.close(self.io);
var iterator = dir.iterate();
while (try iterator.next(self.io)) |entry| {
if (entry.kind == .file and isCode(entry.name)) {
const child = try std.fs.path.join(self.allocator, &.{ path, entry.name });
defer self.allocator.free(child);
try self.addFile(child);
} else if (recursive and entry.kind == .directory and !skipDirectory(entry.name)) {
const child = try std.fs.path.join(self.allocator, &.{ path, entry.name });
defer self.allocator.free(child);
try self.directory(child, true);
}
}
}
pub fn pattern(self: *Selection, value: []const u8) !void {
if (!hasPattern(value)) return self.addFile(value);
if (is_windows) {
// Backslashes are separators; use bracket expressions for literal metacharacters.
const normalized = try self.allocator.dupe(u8, value);
defer self.allocator.free(normalized);
std.mem.replaceScalar(u8, normalized, '\\', '/');
const parsed = std.fs.path.parsePathWindows(u8, normalized);
if (parsed.kind == .drive_relative) return error.DriveRelativeGlobUnsupported;
const base = if (parsed.root.len == 0) "." else parsed.root;
const matched = try self.expand(base, std.mem.trimStart(u8, normalized[parsed.root.len..], "/"));
if (!matched) return error.NoFilesMatched;
return;
}
const matched = try self.expand(if (std.fs.path.isAbsolute(value)) "/" else ".", std.mem.trimStart(u8, value, "/"));
if (!matched) return error.NoFilesMatched;
}
fn expand(self: *Selection, base: []const u8, pattern_text: []const u8) !bool {
const separator = std.mem.indexOfScalar(u8, pattern_text, '/');
const segment = if (separator) |index| pattern_text[0..index] else pattern_text;
const rest = if (separator) |index| pattern_text[index + 1 ..] else null;
const recursive = std.mem.eql(u8, segment, "**");
var matched = false;
if (recursive) {
if (rest) |remaining| matched = try self.expand(base, remaining);
}
var dir = std.Io.Dir.cwd().openDir(self.io, base, .{ .iterate = true }) catch |err| switch (err) {
error.FileNotFound, error.NotDir => return false,
else => return err,
};
defer dir.close(self.io);
const segment_z: ?[:0]u8 = if (is_windows) null else try self.allocator.dupeZ(u8, segment);
defer if (segment_z) |bytes| self.allocator.free(bytes);
var iterator = dir.iterate();
// Literal path segments include . and .., which directory iteration omits.
if (!recursive and !hasPattern(segment) and rest != null) {
const child = try std.fs.path.join(self.allocator, &.{ base, segment });
defer self.allocator.free(child);
return self.expand(child, rest.?);
}
while (try iterator.next(self.io)) |entry| {
if (is_windows) {
if (!windows_glob.matches(segment, entry.name)) continue;
} else {
const name_z = try self.allocator.dupeZ(u8, entry.name);
defer self.allocator.free(name_z);
if (c.fnmatch(segment_z.?.ptr, name_z.ptr, c.FNM_PERIOD) != 0) continue;
}
const child = try std.fs.path.join(self.allocator, &.{ base, entry.name });
defer self.allocator.free(child);
if (entry.kind == .directory) {
if (recursive and !skipDirectory(entry.name)) {
matched = try self.expand(child, pattern_text) or matched;
} else if (!recursive) {
if (rest) |remaining| matched = try self.expand(child, remaining) or matched;
}
} else if (entry.kind == .file and rest == null) {
try self.addFile(child);
matched = true;
}
}
return matched;
}
};
fn hasPattern(value: []const u8) bool {
return std.mem.indexOfAny(u8, value, if (is_windows) "*?[" else "*?[\\") != null;
}
fn skipDirectory(name: []const u8) bool {
if (std.mem.startsWith(u8, name, ".")) return true;
for ([_][]const u8{ "node_modules", "vendor", "dist", "build", "target", "zig-out", "__pycache__" }) |excluded| {
if (std.mem.eql(u8, name, excluded)) return true;
}
return false;
}
fn isCode(name: []const u8) bool {
const extension = std.fs.path.extension(name);
for ([_][]const u8{
".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs",
".py", ".pyi", ".java", ".rs", ".zig", ".go", ".c", ".h",
".cc", ".cpp", ".cxx", ".hpp", ".hxx", ".cs", ".rb", ".php",
".kt", ".kts", ".swift", ".m", ".mm", ".scala", ".sc", ".dart",
".lua", ".ml", ".mli", ".ex", ".exs", ".erl", ".hrl", ".hs",
".sh", ".bash", ".zsh", ".fish", ".pl", ".pm", ".r", ".R",
".sql", ".vue", ".svelte", ".html", ".css", ".scss", ".less",
}) |suffix| {
if (std.mem.eql(u8, extension, suffix)) return true;
}
return false;
}