Simplify manwhere to minimal version - remove interactive mode, descriptions, verbose flags. Keep only -s section filter. Output plain names for piping to fzf.
All checks were successful
Create Release / release (push) Successful in 44s
All checks were successful
Create Release / release (push) Successful in 44s
This commit is contained in:
parent
1d113eacf7
commit
f21598bdbb
7 changed files with 218 additions and 689 deletions
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Zig
|
||||||
|
.zig-cache/
|
||||||
|
zig-out/
|
||||||
|
|
||||||
|
# Compiled binaries
|
||||||
|
*.o
|
||||||
|
*.exe
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Project specific
|
||||||
|
manwhere
|
||||||
|
manwhere-debug
|
||||||
21
build.zig
21
build.zig
|
|
@ -106,12 +106,25 @@ pub fn build(b: *std.Build) void {
|
||||||
std.debug.print("Error: Failed to construct install path\n", .{});
|
std.debug.print("Error: Failed to construct install path\n", .{});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer b.allocator.free(install_path);
|
// Note: NOT freed - used by build system
|
||||||
|
|
||||||
const install_exe = b.addInstallArtifact(release_exe, .{
|
// Create install directory if it doesn't exist
|
||||||
.dest_dir = .{ .override = .{ .custom = install_path } },
|
const mkdir_cmd = b.addSystemCommand(&.{
|
||||||
|
"mkdir", "-p", install_path,
|
||||||
});
|
});
|
||||||
release_step.dependOn(&install_exe.step);
|
release_step.dependOn(&mkdir_cmd.step);
|
||||||
|
|
||||||
|
// Build the release executable
|
||||||
|
const release_build = b.addInstallArtifact(release_exe, .{});
|
||||||
|
release_build.step.dependOn(&mkdir_cmd.step);
|
||||||
|
release_step.dependOn(&release_build.step);
|
||||||
|
|
||||||
|
// Copy to final destination
|
||||||
|
const copy_cmd = b.addSystemCommand(&.{
|
||||||
|
"cp", "zig-out/bin/manwhere", install_path,
|
||||||
|
});
|
||||||
|
copy_cmd.step.dependOn(&release_build.step);
|
||||||
|
release_step.dependOn(©_cmd.step);
|
||||||
|
|
||||||
// Test step - run all unit tests
|
// Test step - run all unit tests
|
||||||
const run_unit_tests = b.addRunArtifact(unit_tests);
|
const run_unit_tests = b.addRunArtifact(unit_tests);
|
||||||
|
|
|
||||||
164
src/display.zig
164
src/display.zig
|
|
@ -1,164 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
const types = @import("types.zig");
|
|
||||||
|
|
||||||
// Following Zig 0.15.1 writergate changes - buffer goes in the interface
|
|
||||||
var stdout_buffer: [4096]u8 = undefined;
|
|
||||||
var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
|
|
||||||
const stdout = &stdout_writer.interface;
|
|
||||||
|
|
||||||
pub fn displayManEntry(entry: types.ManEntry) !void {
|
|
||||||
try stdout.print("{s} {s}({s}) - {s}\n", .{ entry.getSectionMarker(), entry.name, entry.section, entry.description });
|
|
||||||
if (entry.path) |path| {
|
|
||||||
try stdout.print(" -> {s}\n", .{path});
|
|
||||||
}
|
|
||||||
// Don't forget to flush! - per writergate guidelines
|
|
||||||
try stdout.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn displaySearchResults(
|
|
||||||
entries: types.ManEntryList,
|
|
||||||
config: types.SearchConfig,
|
|
||||||
duration_ms: f64,
|
|
||||||
) !void {
|
|
||||||
if (config.verbose) {
|
|
||||||
if (config.target_sections) |sections| {
|
|
||||||
// e.g., join sections for display
|
|
||||||
var joined: [32]u8 = undefined;
|
|
||||||
var pos: usize = 0;
|
|
||||||
for (sections, 0..) |s, i| {
|
|
||||||
if (i != 0) {
|
|
||||||
joined[pos] = ',';
|
|
||||||
pos += 1;
|
|
||||||
}
|
|
||||||
// Copy the string into the buffer
|
|
||||||
@memcpy(joined[pos .. pos + s.len], s);
|
|
||||||
pos += s.len;
|
|
||||||
}
|
|
||||||
try stdout.print(
|
|
||||||
"[=] Found {d} entries matching '{s}' in section(s) {s}\n",
|
|
||||||
.{ entries.items.len, config.keyword, joined[0..pos] },
|
|
||||||
);
|
|
||||||
try stdout.print("[=] Found {d} entries matching '{s}'\n", .{ entries.items.len, config.keyword });
|
|
||||||
}
|
|
||||||
try stdout.print("\n", .{});
|
|
||||||
} else {
|
|
||||||
if (config.target_sections) |sections| {
|
|
||||||
// Join sections for display
|
|
||||||
var joined: [32]u8 = undefined;
|
|
||||||
var pos: usize = 0;
|
|
||||||
for (sections, 0..) |s, i| {
|
|
||||||
if (i != 0) {
|
|
||||||
joined[pos] = ',';
|
|
||||||
pos += 1;
|
|
||||||
}
|
|
||||||
@memcpy(joined[pos .. pos + s.len], s);
|
|
||||||
pos += s.len;
|
|
||||||
}
|
|
||||||
try stdout.print("[=] Found {d} entries matching '{s}' in section {s}\n\n", .{ entries.items.len, config.keyword, joined[0..pos] });
|
|
||||||
} else {
|
|
||||||
try stdout.print("[=] Found {d} entries matching '{s}'\n\n", .{ entries.items.len, config.keyword });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try stdout.flush(); // Flush header
|
|
||||||
|
|
||||||
// Iterate over entries
|
|
||||||
for (entries.items) |entry| {
|
|
||||||
try displayManEntry(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.verbose) {
|
|
||||||
try stdout.print("───────────────────────────────────────────────\n", .{});
|
|
||||||
try stdout.print("[=] Completed in {d:.1}ms\n", .{duration_ms});
|
|
||||||
try stdout.flush(); // Flush footer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn displaySearchStart(config: types.SearchConfig) !void {
|
|
||||||
if (config.verbose) {
|
|
||||||
if (config.target_sections) |sections| {
|
|
||||||
// Join sections for display (same pattern as displaySearchResults)
|
|
||||||
var joined: [32]u8 = undefined;
|
|
||||||
var pos: usize = 0;
|
|
||||||
for (sections, 0..) |s, i| {
|
|
||||||
if (i != 0) {
|
|
||||||
joined[pos] = ',';
|
|
||||||
pos += 1;
|
|
||||||
}
|
|
||||||
@memcpy(joined[pos .. pos + s.len], s);
|
|
||||||
pos += s.len;
|
|
||||||
}
|
|
||||||
try stdout.print("[=] Searching for '{s}' in man pages database (section {s})...\n", .{ config.keyword, joined[0..pos] });
|
|
||||||
} else {
|
|
||||||
try stdout.print("[=] Searching for '{s}' in man pages database...\n", .{config.keyword});
|
|
||||||
}
|
|
||||||
try stdout.flush(); // Flush search start message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// TESTS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
test "displayManEntry - basic entry" {
|
|
||||||
const entry = types.ManEntry{
|
|
||||||
.name = "ls",
|
|
||||||
.section = "1",
|
|
||||||
.description = "list directory contents",
|
|
||||||
.path = null,
|
|
||||||
};
|
|
||||||
// Verify function doesn't panic
|
|
||||||
try displayManEntry(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "displaySearchResults - non-verbose no sections" {
|
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
||||||
defer _ = gpa.deinit();
|
|
||||||
const allocator = gpa.allocator();
|
|
||||||
|
|
||||||
var entries = types.ManEntryList.init(allocator);
|
|
||||||
defer {
|
|
||||||
for (entries.items) |entry| {
|
|
||||||
allocator.free(entry.name);
|
|
||||||
allocator.free(entry.section);
|
|
||||||
allocator.free(entry.description);
|
|
||||||
}
|
|
||||||
entries.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
try entries.append(types.ManEntry{
|
|
||||||
.name = try allocator.dupe(u8, "ls"),
|
|
||||||
.section = try allocator.dupe(u8, "1"),
|
|
||||||
.description = try allocator.dupe(u8, "list files"),
|
|
||||||
.path = null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const config = types.SearchConfig{
|
|
||||||
.keyword = "ls",
|
|
||||||
.target_sections = null,
|
|
||||||
.show_paths = false,
|
|
||||||
.verbose = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
try displaySearchResults(entries, config, 100.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "displaySearchStart - non-verbose" {
|
|
||||||
const config = types.SearchConfig{
|
|
||||||
.keyword = "test",
|
|
||||||
.target_sections = null,
|
|
||||||
.show_paths = false,
|
|
||||||
.verbose = false,
|
|
||||||
};
|
|
||||||
// Should not output anything in non-verbose mode
|
|
||||||
try displaySearchStart(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "displaySearchStart - verbose" {
|
|
||||||
const config = types.SearchConfig{
|
|
||||||
.keyword = "sleep",
|
|
||||||
.target_sections = null,
|
|
||||||
.show_paths = false,
|
|
||||||
.verbose = true,
|
|
||||||
};
|
|
||||||
try displaySearchStart(config);
|
|
||||||
}
|
|
||||||
31
src/main.zig
31
src/main.zig
|
|
@ -2,53 +2,44 @@ const std = @import("std");
|
||||||
const types = @import("types.zig");
|
const types = @import("types.zig");
|
||||||
const parser = @import("parser.zig");
|
const parser = @import("parser.zig");
|
||||||
const search = @import("search.zig");
|
const search = @import("search.zig");
|
||||||
const display = @import("display.zig");
|
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
defer _ = gpa.deinit();
|
defer _ = gpa.deinit();
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
// Following Zig 0.15.1 writergate changes - buffer goes in the interface
|
var stderr_buf: [1024]u8 = undefined;
|
||||||
var stderr_buffer: [1024]u8 = undefined;
|
var stderr_writer = std.fs.File.stderr().writer(&stderr_buf);
|
||||||
var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
|
|
||||||
const stderr = &stderr_writer.interface;
|
const stderr = &stderr_writer.interface;
|
||||||
|
|
||||||
const args = try std.process.argsAlloc(allocator);
|
const args = try std.process.argsAlloc(allocator);
|
||||||
defer std.process.argsFree(allocator, args);
|
defer std.process.argsFree(allocator, args);
|
||||||
|
|
||||||
// Parse command line arguments
|
|
||||||
const config = parser.parseArgs(allocator, args) catch |err| switch (err) {
|
const config = parser.parseArgs(allocator, args) catch |err| switch (err) {
|
||||||
error.HelpRequested => return,
|
error.HelpRequested => return,
|
||||||
error.InvalidOption, error.InvalidSection, error.MultipleKeywords, error.NoKeyword => std.process.exit(1),
|
error.InvalidOption, error.InvalidSection, error.MultipleKeywords, error.NoKeyword => std.process.exit(1),
|
||||||
else => return err,
|
else => return err,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Display search start message
|
var entries = search.searchManPages(config.keyword, config.target_sections, allocator, stderr) catch |err| {
|
||||||
try display.displaySearchStart(config);
|
|
||||||
|
|
||||||
// Perform the search
|
|
||||||
const start_time = std.time.nanoTimestamp();
|
|
||||||
var entries = search.searchManPages(config.keyword, config.target_sections, config.show_paths, config.verbose, allocator, stderr) catch |err| {
|
|
||||||
try stderr.print("[X] Search failed: {}\n", .{err});
|
try stderr.print("[X] Search failed: {}\n", .{err});
|
||||||
try stderr.flush(); // Don't forget to flush!
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
defer {
|
defer {
|
||||||
for (entries.items) |entry| {
|
for (entries.items) |entry| {
|
||||||
// Free all the allocated strings from parseManLine
|
|
||||||
allocator.free(entry.name);
|
allocator.free(entry.name);
|
||||||
allocator.free(entry.section);
|
allocator.free(entry.section);
|
||||||
allocator.free(entry.description);
|
allocator.free(entry.description);
|
||||||
if (entry.path) |path| allocator.free(path);
|
|
||||||
}
|
}
|
||||||
entries.deinit(allocator);
|
entries.deinit(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate duration
|
// Print results (simple name-only format for piping)
|
||||||
const end_time = std.time.nanoTimestamp();
|
var stdout_buf: [4096]u8 = undefined;
|
||||||
const duration_ms = @as(f64, @floatFromInt(end_time - start_time)) / 1_000_000.0;
|
var stdout_writer = std.fs.File.stdout().writer(&stdout_buf);
|
||||||
|
const stdout = &stdout_writer.interface;
|
||||||
// Display results
|
for (entries.items) |entry| {
|
||||||
try display.displaySearchResults(entries, config, duration_ms);
|
try stdout.print("{s}\n", .{entry.name});
|
||||||
|
}
|
||||||
|
try stdout.flush();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,12 @@ pub const HELP_TEXT =
|
||||||
\\
|
\\
|
||||||
\\OPTIONS:
|
\\OPTIONS:
|
||||||
\\ -h, --help Show this help message
|
\\ -h, --help Show this help message
|
||||||
\\ -v, --verbose Show timing and extra details
|
|
||||||
\\ --paths Show file paths (slower)
|
|
||||||
\\ -s, --section NUM Only show results from section NUM (1-9)
|
\\ -s, --section NUM Only show results from section NUM (1-9)
|
||||||
\\ Can be specified multiple times
|
|
||||||
\\
|
\\
|
||||||
\\EXAMPLES:
|
\\EXAMPLES:
|
||||||
\\ manwhere sleep # find all man pages mentioning "sleep"
|
\\ manwhere sleep # find all man pages mentioning "sleep"
|
||||||
\\ manwhere -s 1 sleep # find only commands (section 1) mentioning "sleep"
|
\\ manwhere sleep | fzf | xargs man # pipe to fzf for interactive selection
|
||||||
\\ manwhere -s 1 -s 3 sleep # find in sections 1 and 3
|
\\ manwhere -s 1 sleep # find only in section 1
|
||||||
\\ manwhere -v --paths ssl # detailed search with paths and timing
|
|
||||||
\\ manwhere --section 3 printf # find only library functions (section 3)
|
|
||||||
\\
|
\\
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -31,8 +26,6 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: [][:0]u8) !types.SearchConf
|
||||||
var keyword: []const u8 = "";
|
var keyword: []const u8 = "";
|
||||||
var sections_list = std.ArrayList([]const u8){};
|
var sections_list = std.ArrayList([]const u8){};
|
||||||
defer sections_list.deinit(allocator);
|
defer sections_list.deinit(allocator);
|
||||||
var show_paths = false;
|
|
||||||
var verbose = false;
|
|
||||||
|
|
||||||
var i: usize = 1;
|
var i: usize = 1;
|
||||||
while (i < args.len) : (i += 1) {
|
while (i < args.len) : (i += 1) {
|
||||||
|
|
@ -41,35 +34,20 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: [][:0]u8) !types.SearchConf
|
||||||
if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) {
|
if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) {
|
||||||
print("{s}", .{HELP_TEXT});
|
print("{s}", .{HELP_TEXT});
|
||||||
return error.HelpRequested;
|
return error.HelpRequested;
|
||||||
} else if (std.mem.eql(u8, arg, "--paths")) {
|
|
||||||
show_paths = true;
|
|
||||||
} else if (std.mem.eql(u8, arg, "-v") or std.mem.eql(u8, arg, "--verbose")) {
|
|
||||||
verbose = true;
|
|
||||||
} else if (std.mem.eql(u8, arg, "-s") or std.mem.eql(u8, arg, "--section")) {
|
} else if (std.mem.eql(u8, arg, "-s") or std.mem.eql(u8, arg, "--section")) {
|
||||||
// Next argument should be the section number
|
|
||||||
if (i + 1 >= args.len) {
|
if (i + 1 >= args.len) {
|
||||||
print("[X] Option {s} requires a section number (1-9)\n", .{arg});
|
print("[X] Option {s} requires a section number\n", .{arg});
|
||||||
return error.InvalidOption;
|
return error.InvalidOption;
|
||||||
}
|
}
|
||||||
i += 1;
|
i += 1;
|
||||||
const section_arg = args[i];
|
const section_arg = args[i];
|
||||||
|
if (section_arg.len == 0 or section_arg[0] < '1' or section_arg[0] > '9') {
|
||||||
// Validate section number
|
print("[X] Invalid section '{s}'\n", .{section_arg});
|
||||||
if (section_arg.len == 0 or section_arg.len > 4) {
|
|
||||||
print("[X] Invalid section '{s}'. Use 1-9 or subsections like 3ssl\n", .{section_arg});
|
|
||||||
return error.InvalidSection;
|
return error.InvalidSection;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic validation - should start with a digit 1-9
|
|
||||||
if (section_arg[0] < '1' or section_arg[0] > '9') {
|
|
||||||
print("[X] Invalid section '{s}'. Section must be 1-9\n", .{section_arg});
|
|
||||||
return error.InvalidSection;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add section to list
|
|
||||||
try sections_list.append(allocator, section_arg);
|
try sections_list.append(allocator, section_arg);
|
||||||
} else if (std.mem.startsWith(u8, arg, "-")) {
|
} else if (std.mem.startsWith(u8, arg, "-")) {
|
||||||
print("[X] Unknown option: {s}\nUse -h or --help for usage information\n", .{arg});
|
print("[X] Unknown option: {s}\n", .{arg});
|
||||||
return error.InvalidOption;
|
return error.InvalidOption;
|
||||||
} else if (keyword.len == 0) {
|
} else if (keyword.len == 0) {
|
||||||
keyword = arg;
|
keyword = arg;
|
||||||
|
|
@ -80,11 +58,10 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: [][:0]u8) !types.SearchConf
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyword.len == 0) {
|
if (keyword.len == 0) {
|
||||||
print("[X] No keyword provided\n{s}", .{HELP_TEXT});
|
print("[X] No keyword provided\n", .{});
|
||||||
return error.NoKeyword;
|
return error.NoKeyword;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert ArrayList to owned slice, or null if empty
|
|
||||||
const target_sections = if (sections_list.items.len > 0)
|
const target_sections = if (sections_list.items.len > 0)
|
||||||
try sections_list.toOwnedSlice(allocator)
|
try sections_list.toOwnedSlice(allocator)
|
||||||
else
|
else
|
||||||
|
|
@ -93,8 +70,6 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: [][:0]u8) !types.SearchConf
|
||||||
return types.SearchConfig{
|
return types.SearchConfig{
|
||||||
.keyword = keyword,
|
.keyword = keyword,
|
||||||
.target_sections = target_sections,
|
.target_sections = target_sections,
|
||||||
.show_paths = show_paths,
|
|
||||||
.verbose = verbose,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,32 +87,6 @@ test "parseArgs - basic keyword" {
|
||||||
|
|
||||||
try std.testing.expectEqualStrings("sleep", config.keyword);
|
try std.testing.expectEqualStrings("sleep", config.keyword);
|
||||||
try std.testing.expect(config.target_sections == null);
|
try std.testing.expect(config.target_sections == null);
|
||||||
try std.testing.expect(!config.show_paths);
|
|
||||||
try std.testing.expect(!config.verbose);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parseArgs - with verbose flag" {
|
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
||||||
defer _ = gpa.deinit();
|
|
||||||
const allocator = gpa.allocator();
|
|
||||||
|
|
||||||
const args = &[_][:0]u8{ "manwhere", "-v", "ls" };
|
|
||||||
const config = try parseArgs(allocator, args);
|
|
||||||
|
|
||||||
try std.testing.expectEqualStrings("ls", config.keyword);
|
|
||||||
try std.testing.expect(config.verbose);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parseArgs - with paths flag" {
|
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
||||||
defer _ = gpa.deinit();
|
|
||||||
const allocator = gpa.allocator();
|
|
||||||
|
|
||||||
const args = &[_][:0]u8{ "manwhere", "--paths", "printf" };
|
|
||||||
const config = try parseArgs(allocator, args);
|
|
||||||
|
|
||||||
try std.testing.expectEqualStrings("printf", config.keyword);
|
|
||||||
try std.testing.expect(config.show_paths);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parseArgs - with section flag" {
|
test "parseArgs - with section flag" {
|
||||||
|
|
|
||||||
599
src/search.zig
599
src/search.zig
|
|
@ -1,332 +1,118 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const types = @import("types.zig");
|
const types = @import("types.zig");
|
||||||
|
|
||||||
// Fast man page discovery by directly scanning filesystem
|
const MAN_DIRS = [_][]const u8{
|
||||||
const ManSearcher = struct {
|
"/usr/share/man",
|
||||||
|
"/usr/local/share/man",
|
||||||
|
"/opt/homebrew/share/man",
|
||||||
|
"/opt/local/share/man",
|
||||||
|
"/usr/X11R6/man",
|
||||||
|
"/usr/pkg/man",
|
||||||
|
};
|
||||||
|
|
||||||
|
fn parseManFilename(filename: []const u8) ?struct { name: []const u8, section: []const u8 } {
|
||||||
|
var base_name = filename;
|
||||||
|
for ([_][]const u8{ ".gz", ".bz2", ".xz", ".lz", ".Z" }) |ext| {
|
||||||
|
if (std.mem.endsWith(u8, base_name, ext)) {
|
||||||
|
base_name = base_name[0 .. base_name.len - ext.len];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.lastIndexOf(u8, base_name, ".")) |dot_pos| {
|
||||||
|
if (dot_pos > 0 and dot_pos < base_name.len - 1) {
|
||||||
|
const name = base_name[0..dot_pos];
|
||||||
|
const section = base_name[dot_pos + 1 ..];
|
||||||
|
if (section.len > 0 and section.len <= 4) {
|
||||||
|
return .{ .name = name, .section = section };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scanManSection(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
entries: types.ManEntryList,
|
entries: *types.ManEntryList,
|
||||||
|
base_dir: []const u8,
|
||||||
|
section: []const u8,
|
||||||
|
keyword: []const u8,
|
||||||
|
) !void {
|
||||||
|
var path_buffer: [512]u8 = undefined;
|
||||||
|
const section_path = try std.fmt.bufPrint(path_buffer[0..], "{s}/man{s}", .{ base_dir, section });
|
||||||
|
|
||||||
const Self = @This();
|
var dir = std.fs.cwd().openDir(section_path, .{ .iterate = true }) catch return;
|
||||||
|
defer dir.close();
|
||||||
|
|
||||||
// Common man page directories in priority order
|
var iterator = dir.iterate();
|
||||||
const MAN_DIRS = [_][]const u8{
|
while (try iterator.next()) |entry| {
|
||||||
"/usr/share/man", // Standard Linux/macOS
|
if (entry.kind != .file) continue;
|
||||||
"/usr/local/share/man", // Local installations
|
|
||||||
"/opt/homebrew/share/man", // Homebrew on macOS
|
|
||||||
"/opt/local/share/man", // MacPorts on macOS
|
|
||||||
"/usr/X11R6/man", // X11 on some systems
|
|
||||||
"/usr/pkg/man", // pkgsrc on NetBSD
|
|
||||||
};
|
|
||||||
|
|
||||||
// File extensions to look for (compressed and uncompressed)
|
if (parseManFilename(entry.name)) |parsed| {
|
||||||
const EXTENSIONS = [_][]const u8{ "", ".gz", ".bz2", ".xz", ".lz", ".Z" };
|
if (!std.mem.eql(u8, parsed.section, section)) continue;
|
||||||
|
if (!keywordMatches(parsed.name, keyword)) continue;
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator) Self {
|
try entries.append(allocator, .{
|
||||||
return Self{
|
.name = try allocator.dupe(u8, parsed.name),
|
||||||
.allocator = allocator,
|
.section = try allocator.dupe(u8, parsed.section),
|
||||||
.entries = .{},
|
.description = try allocator.dupe(u8, ""),
|
||||||
|
.path = null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn searchManPages(
|
||||||
|
keyword: []const u8,
|
||||||
|
target_sections: ?[]const []const u8,
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
stderr: *std.Io.Writer,
|
||||||
|
) !types.ManEntryList {
|
||||||
|
var result: types.ManEntryList = .{};
|
||||||
|
errdefer {
|
||||||
|
for (result.items) |entry| {
|
||||||
|
allocator.free(entry.name);
|
||||||
|
allocator.free(entry.section);
|
||||||
|
allocator.free(entry.description);
|
||||||
|
}
|
||||||
|
result.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sections = if (target_sections) |sects|
|
||||||
|
sects
|
||||||
|
else
|
||||||
|
&[_][]const u8{ "1", "2", "3", "4", "5", "6", "7", "8", "9" };
|
||||||
|
|
||||||
|
var path_buffer: [512]u8 = undefined;
|
||||||
|
|
||||||
|
for (MAN_DIRS) |man_dir| {
|
||||||
|
var base_dir = std.fs.cwd().openDir(man_dir, .{}) catch |err| switch (@as(anyerror, err)) {
|
||||||
|
error.FileNotFound, error.NotDir, error.AccessDenied => continue,
|
||||||
|
else => return err,
|
||||||
};
|
};
|
||||||
}
|
defer base_dir.close();
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
for (sections) |section| {
|
||||||
for (self.entries.items) |entry| {
|
const man_section_path = try std.fmt.bufPrint(path_buffer[0..], "man{s}", .{section});
|
||||||
self.allocator.free(entry.name);
|
_ = base_dir.openDir(man_section_path, .{}) catch |err| switch (@as(anyerror, err)) {
|
||||||
self.allocator.free(entry.section);
|
error.FileNotFound, error.NotDir => continue,
|
||||||
self.allocator.free(entry.description);
|
error.AccessDenied => continue,
|
||||||
if (entry.path) |path| self.allocator.free(path);
|
else => return err,
|
||||||
}
|
};
|
||||||
self.entries.deinit(self.allocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract man page name and section from filename
|
scanManSection(allocator, &result, man_dir, section, keyword) catch |err| switch (@as(anyerror, err)) {
|
||||||
fn parseManFilename(filename: []const u8) ?struct { name: []const u8, section: []const u8 } {
|
|
||||||
// Handle compressed extensions first
|
|
||||||
var base_name = filename;
|
|
||||||
for (EXTENSIONS[1..]) |ext| { // Skip empty extension
|
|
||||||
if (std.mem.endsWith(u8, base_name, ext)) {
|
|
||||||
base_name = base_name[0 .. base_name.len - ext.len];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the last dot to separate name from section
|
|
||||||
if (std.mem.lastIndexOf(u8, base_name, ".")) |dot_pos| {
|
|
||||||
if (dot_pos > 0 and dot_pos < base_name.len - 1) {
|
|
||||||
const name = base_name[0..dot_pos];
|
|
||||||
const section = base_name[dot_pos + 1 ..];
|
|
||||||
|
|
||||||
// Basic validation - section should be reasonable
|
|
||||||
if (section.len > 0 and section.len <= 4) {
|
|
||||||
return .{ .name = name, .section = section };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get description from man file header (first line parsing)
|
|
||||||
fn extractDescription(self: Self, file_path: []const u8) []const u8 {
|
|
||||||
// Try to read the first few lines to extract description
|
|
||||||
const file = std.fs.cwd().openFile(file_path, .{}) catch return "";
|
|
||||||
defer file.close();
|
|
||||||
|
|
||||||
var buffer: [1024]u8 = undefined;
|
|
||||||
const bytes_read = file.read(&buffer) catch return "";
|
|
||||||
|
|
||||||
if (bytes_read == 0) return "";
|
|
||||||
|
|
||||||
const content = buffer[0..bytes_read];
|
|
||||||
|
|
||||||
// For compressed files, we'd need to decompress first
|
|
||||||
// For now, return empty description for compressed files
|
|
||||||
if (std.mem.indexOf(u8, file_path, ".gz") != null or
|
|
||||||
std.mem.indexOf(u8, file_path, ".bz2") != null or
|
|
||||||
std.mem.indexOf(u8, file_path, ".xz") != null)
|
|
||||||
{
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for NAME section and extract description
|
|
||||||
var lines = std.mem.splitScalar(u8, content, '\n');
|
|
||||||
var in_name_section = false;
|
|
||||||
|
|
||||||
while (lines.next()) |line| {
|
|
||||||
const trimmed = std.mem.trim(u8, line, " \t\r\n");
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, trimmed, ".SH NAME") or
|
|
||||||
std.mem.eql(u8, trimmed, ".SH \"NAME\""))
|
|
||||||
{
|
|
||||||
in_name_section = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (in_name_section) {
|
|
||||||
if (std.mem.startsWith(u8, trimmed, ".SH")) {
|
|
||||||
break; // End of NAME section
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for the description line (usually contains \-)
|
|
||||||
if (std.mem.indexOf(u8, trimmed, "\\-")) |dash_pos| {
|
|
||||||
const desc_start = dash_pos + 2;
|
|
||||||
if (desc_start < trimmed.len) {
|
|
||||||
const desc = std.mem.trim(u8, trimmed[desc_start..], " \t");
|
|
||||||
return self.allocator.dupe(u8, desc) catch "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan a single man directory section (e.g., man1, man2, etc.)
|
|
||||||
fn scanManSection(self: *Self, base_dir: []const u8, section: []const u8, show_paths: bool) !void {
|
|
||||||
var path_buffer: [512]u8 = undefined;
|
|
||||||
const section_path = try std.fmt.bufPrint(path_buffer[0..], "{s}/man{s}", .{ base_dir, section });
|
|
||||||
|
|
||||||
var dir = std.fs.cwd().openDir(section_path, .{ .iterate = true }) catch return;
|
|
||||||
defer dir.close();
|
|
||||||
|
|
||||||
var iterator = dir.iterate();
|
|
||||||
while (try iterator.next()) |entry| {
|
|
||||||
if (entry.kind != .file) continue;
|
|
||||||
|
|
||||||
// Parse the filename to get name and section
|
|
||||||
if (parseManFilename(entry.name)) |parsed| {
|
|
||||||
// Verify section matches directory
|
|
||||||
if (!std.mem.eql(u8, parsed.section, section)) continue;
|
|
||||||
|
|
||||||
const full_path = if (show_paths) blk: {
|
|
||||||
var full_path_buf: [768]u8 = undefined;
|
|
||||||
const full_path_str = try std.fmt.bufPrint(full_path_buf[0..], "{s}/{s}", .{ section_path, entry.name });
|
|
||||||
break :blk try self.allocator.dupe(u8, full_path_str);
|
|
||||||
} else null;
|
|
||||||
|
|
||||||
// Extract description (simplified - just use empty for now)
|
|
||||||
// In a production implementation, you'd want to parse the man file header
|
|
||||||
const description = if (show_paths)
|
|
||||||
self.extractDescription(full_path.?)
|
|
||||||
else
|
|
||||||
try self.allocator.dupe(u8, "");
|
|
||||||
|
|
||||||
const man_entry = types.ManEntry{
|
|
||||||
.name = try self.allocator.dupe(u8, parsed.name),
|
|
||||||
.section = try self.allocator.dupe(u8, parsed.section),
|
|
||||||
.description = description,
|
|
||||||
.path = full_path,
|
|
||||||
};
|
|
||||||
|
|
||||||
try self.entries.append(self.allocator, man_entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main search function - much faster than man -k
|
|
||||||
// In the searchManPages function of ManSearcher:
|
|
||||||
pub fn searchManPages(
|
|
||||||
self: *Self,
|
|
||||||
keyword: []const u8,
|
|
||||||
target_sections: ?[]const []const u8,
|
|
||||||
show_paths: bool,
|
|
||||||
verbose: bool,
|
|
||||||
debug_writer: *std.Io.Writer,
|
|
||||||
) !void {
|
|
||||||
if (verbose) {
|
|
||||||
try debug_writer.print("[=] Scanning man page directories directly...\n", .{});
|
|
||||||
try debug_writer.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sections to scan - handle multiple sections
|
|
||||||
const sections = if (target_sections) |sects|
|
|
||||||
sects // Use provided sections array
|
|
||||||
else
|
|
||||||
&[_][]const u8{ "1", "2", "3", "4", "5", "6", "7", "8", "9", "1m", "3c", "3x", "n" };
|
|
||||||
|
|
||||||
for (MAN_DIRS) |man_dir| {
|
|
||||||
// Check if directory exists
|
|
||||||
var dir = std.fs.cwd().openDir(man_dir, .{}) catch |err| switch (@as(anyerror, err)) {
|
|
||||||
error.FileNotFound, error.NotDir, error.AccessDenied => continue,
|
error.FileNotFound, error.NotDir, error.AccessDenied => continue,
|
||||||
else => return err,
|
else => return err,
|
||||||
};
|
};
|
||||||
dir.close();
|
|
||||||
|
|
||||||
if (verbose) {
|
|
||||||
try debug_writer.print("[=] Scanning {s}...\n", .{man_dir});
|
|
||||||
try debug_writer.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (sections) |section| {
|
|
||||||
self.scanManSection(man_dir, section, show_paths) catch |err| switch (@as(anyerror, err)) {
|
|
||||||
error.FileNotFound, error.NotDir, error.AccessDenied => continue,
|
|
||||||
else => return err,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by keyword (case-insensitive search in name)
|
|
||||||
var filtered_entries = types.ManEntryList{};
|
|
||||||
defer {
|
|
||||||
// Move entries to self.entries and clean up filtered_entries
|
|
||||||
self.entries.deinit(self.allocator);
|
|
||||||
self.entries = filtered_entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (self.entries.items) |entry| {
|
|
||||||
// Simple case-insensitive substring search
|
|
||||||
var name_lower: [256]u8 = undefined;
|
|
||||||
var keyword_lower: [256]u8 = undefined;
|
|
||||||
|
|
||||||
if (entry.name.len < name_lower.len and keyword.len < keyword_lower.len) {
|
|
||||||
_ = std.ascii.lowerString(name_lower[0..entry.name.len], entry.name);
|
|
||||||
_ = std.ascii.lowerString(keyword_lower[0..keyword.len], keyword);
|
|
||||||
|
|
||||||
if (std.mem.indexOf(u8, name_lower[0..entry.name.len], keyword_lower[0..keyword.len]) != null) {
|
|
||||||
try filtered_entries.append(self.allocator, entry);
|
|
||||||
} else {
|
|
||||||
// Clean up unused entry
|
|
||||||
self.allocator.free(entry.name);
|
|
||||||
self.allocator.free(entry.section);
|
|
||||||
self.allocator.free(entry.description);
|
|
||||||
if (entry.path) |path| self.allocator.free(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Updated search function that uses direct filesystem scanning
|
if (result.items.len == 0) {
|
||||||
pub fn searchManPagesFast(
|
if (target_sections) |sects| {
|
||||||
keyword: []const u8,
|
|
||||||
target_section: ?[]const []const u8,
|
|
||||||
show_paths: bool,
|
|
||||||
verbose: bool,
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
stderr: *std.Io.Writer, // Post-writergate: concrete type
|
|
||||||
) !types.ManEntryList {
|
|
||||||
var searcher = ManSearcher.init(allocator);
|
|
||||||
defer searcher.deinit();
|
|
||||||
|
|
||||||
// Create debug writer for verbose output - using stderr for debug messages
|
|
||||||
try searcher.searchManPages(keyword, target_section, show_paths, verbose, stderr);
|
|
||||||
|
|
||||||
if (searcher.entries.items.len == 0) {
|
|
||||||
if (target_section) |sections| {
|
|
||||||
// Join sections for display
|
|
||||||
var joined: [32]u8 = undefined;
|
var joined: [32]u8 = undefined;
|
||||||
var pos: usize = 0;
|
var pos: usize = 0;
|
||||||
for (sections, 0..) |s, i| {
|
for (sects, 0..) |s, i| {
|
||||||
if (i != 0) {
|
|
||||||
joined[pos] = ',';
|
|
||||||
pos += 1;
|
|
||||||
}
|
|
||||||
@memcpy(joined[pos .. pos + s.len], s);
|
|
||||||
pos += s.len;
|
|
||||||
}
|
|
||||||
try stderr.print("[X] No matches found for '{s}' in section {s}\n", .{ keyword, joined[0..pos] });
|
|
||||||
} else {
|
|
||||||
try stderr.print("[X] No matches found for '{s}'\n", .{keyword});
|
|
||||||
}
|
|
||||||
try stderr.flush(); // Don't forget to flush!
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transfer ownership of entries
|
|
||||||
var result: types.ManEntryList = .{};
|
|
||||||
try result.appendSlice(allocator, searcher.entries.items);
|
|
||||||
|
|
||||||
// Clear the searcher entries so deinit doesn't free them
|
|
||||||
searcher.entries.clearRetainingCapacity();
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Original implementation as fallback using child process
|
|
||||||
fn searchManPagesOriginal(
|
|
||||||
keyword: []const u8,
|
|
||||||
target_section: ?[]const []const u8,
|
|
||||||
show_paths: bool,
|
|
||||||
verbose: bool,
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
stderr: *std.io.Writer,
|
|
||||||
) !types.ManEntryList {
|
|
||||||
_ = show_paths;
|
|
||||||
_ = verbose;
|
|
||||||
|
|
||||||
var child = std.process.Child.init(&[_][]const u8{ "man", "-k", keyword }, allocator);
|
|
||||||
child.stdout_behavior = .Pipe;
|
|
||||||
child.stderr_behavior = .Ignore;
|
|
||||||
|
|
||||||
try child.spawn();
|
|
||||||
|
|
||||||
// In 0.15.1, ArrayList is unmanaged
|
|
||||||
var output_list = std.ArrayList(u8){};
|
|
||||||
defer output_list.deinit(allocator);
|
|
||||||
|
|
||||||
// Read from child stdout using the new reader interface
|
|
||||||
var stdout_buffer: [4096]u8 = undefined;
|
|
||||||
var stdout_reader_wrapper = child.stdout.?.reader(&stdout_buffer);
|
|
||||||
const reader = &stdout_reader_wrapper.interface;
|
|
||||||
|
|
||||||
// Read all data - use readAll or read in a loop
|
|
||||||
var temp_buffer: [4096]u8 = undefined;
|
|
||||||
while (true) {
|
|
||||||
const bytes_read = reader.takeByte() catch |err| switch (err) {
|
|
||||||
error.EndOfStream => break,
|
|
||||||
else => return err,
|
|
||||||
};
|
|
||||||
if (bytes_read == 0) break;
|
|
||||||
try output_list.appendSlice(allocator, temp_buffer[0..bytes_read]);
|
|
||||||
}
|
|
||||||
|
|
||||||
std.debug.print("ouptut_list len: {d}\n", .{output_list.items.len});
|
|
||||||
|
|
||||||
const term = try child.wait();
|
|
||||||
if ((term != .Exited or term.Exited != 0) and output_list.items.len == 0) {
|
|
||||||
if (target_section) |sections| {
|
|
||||||
// Join sections for display
|
|
||||||
var joined: [32]u8 = undefined;
|
|
||||||
var pos: usize = 0;
|
|
||||||
for (sections, 0..) |s, i| {
|
|
||||||
if (i != 0) {
|
if (i != 0) {
|
||||||
joined[pos] = ',';
|
joined[pos] = ',';
|
||||||
pos += 1;
|
pos += 1;
|
||||||
|
|
@ -339,150 +125,81 @@ fn searchManPagesOriginal(
|
||||||
try stderr.print("[X] No matches found for '{s}'\n", .{keyword});
|
try stderr.print("[X] No matches found for '{s}'\n", .{keyword});
|
||||||
}
|
}
|
||||||
try stderr.flush();
|
try stderr.flush();
|
||||||
return .{};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var entries: types.ManEntryList = .{};
|
return result;
|
||||||
errdefer {
|
|
||||||
for (entries.items) |entry| {
|
|
||||||
allocator.free(entry.name);
|
|
||||||
allocator.free(entry.section);
|
|
||||||
allocator.free(entry.description);
|
|
||||||
if (entry.path) |path| allocator.free(path);
|
|
||||||
}
|
|
||||||
entries.deinit(allocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process output line by line
|
|
||||||
var line_iterator = std.mem.splitScalar(u8, output_list.items, '\n');
|
|
||||||
while (line_iterator.next()) |raw_line| {
|
|
||||||
if (raw_line.len == 0) continue;
|
|
||||||
|
|
||||||
var cleaned_buffer: [4096]u8 = undefined;
|
|
||||||
var cleaned_len: usize = 0;
|
|
||||||
|
|
||||||
for (raw_line) |byte| {
|
|
||||||
if (cleaned_len >= cleaned_buffer.len - 1) break;
|
|
||||||
if (byte >= 32 and byte <= 126) {
|
|
||||||
cleaned_buffer[cleaned_len] = byte;
|
|
||||||
cleaned_len += 1;
|
|
||||||
} else if (byte == '\t') {
|
|
||||||
cleaned_buffer[cleaned_len] = ' ';
|
|
||||||
cleaned_len += 1;
|
|
||||||
} else if (byte >= 128) {
|
|
||||||
cleaned_buffer[cleaned_len] = '?';
|
|
||||||
cleaned_len += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// const cleaned_line = cleaned_buffer[0..cleaned_len];
|
|
||||||
// if (cleaned_line.len == 0) continue;
|
|
||||||
//
|
|
||||||
// _ = cleaned_line; // Your parsing logic here
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main search function with fallback strategy
|
fn keywordMatches(name: []const u8, keyword: []const u8) bool {
|
||||||
pub fn searchManPages(
|
if (name.len > 256 or keyword.len > 256) return false;
|
||||||
keyword: []const u8,
|
|
||||||
target_section: ?[]const []const u8,
|
|
||||||
show_paths: bool,
|
|
||||||
verbose: bool,
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
stderr: *std.Io.Writer, // Post-writergate: concrete type
|
|
||||||
) !types.ManEntryList {
|
|
||||||
// Try fast method first, fallback to man -k if needed
|
|
||||||
return searchManPagesFast(keyword, target_section, show_paths, verbose, allocator, stderr) catch |err| {
|
|
||||||
if (verbose) {
|
|
||||||
try stderr.print("[W] Fast search failed ({}), falling back to man -k...\n", .{err});
|
|
||||||
try stderr.flush(); // Don't forget to flush!
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to original man -k implementation
|
var name_lower: [256]u8 = undefined;
|
||||||
return searchManPagesOriginal(keyword, target_section, show_paths, verbose, allocator, stderr);
|
var keyword_lower: [256]u8 = undefined;
|
||||||
};
|
|
||||||
|
_ = std.ascii.lowerString(name_lower[0..name.len], name);
|
||||||
|
_ = std.ascii.lowerString(keyword_lower[0..keyword.len], keyword);
|
||||||
|
|
||||||
|
return std.mem.indexOf(u8, name_lower[0..name.len], keyword_lower[0..keyword.len]) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TESTS
|
// TESTS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
test "ManSearcher - basic initialization" {
|
test "parseManFilename - basic" {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
const result = parseManFilename("ls.1");
|
||||||
defer _ = gpa.deinit();
|
try std.testing.expect(result != null);
|
||||||
const allocator = gpa.allocator();
|
try std.testing.expectEqualStrings("ls", result.?.name);
|
||||||
|
try std.testing.expectEqualStrings("1", result.?.section);
|
||||||
// Test the types ManEntryList uses
|
|
||||||
var entries = types.ManEntryList.init(allocator);
|
|
||||||
defer {
|
|
||||||
for (entries.items) |entry| {
|
|
||||||
allocator.free(entry.name);
|
|
||||||
allocator.free(entry.section);
|
|
||||||
allocator.free(entry.description);
|
|
||||||
}
|
|
||||||
entries.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
try std.testing.expectEqual(@as(usize, 0), entries.items.len);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "ManEntry - full lifecycle" {
|
test "parseManFilename - with gz extension" {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
const result = parseManFilename("ls.1.gz");
|
||||||
defer _ = gpa.deinit();
|
try std.testing.expect(result != null);
|
||||||
const allocator = gpa.allocator();
|
try std.testing.expectEqualStrings("ls", result.?.name);
|
||||||
|
try std.testing.expectEqualStrings("1", result.?.section);
|
||||||
const entry = types.ManEntry{
|
|
||||||
.name = try allocator.dupe(u8, "grep"),
|
|
||||||
.section = try allocator.dupe(u8, "1"),
|
|
||||||
.description = try allocator.dupe(u8, "global regular expression print"),
|
|
||||||
.path = try allocator.dupe(u8, "/usr/share/man/man1/grep.1.gz"),
|
|
||||||
};
|
|
||||||
defer {
|
|
||||||
allocator.free(entry.name);
|
|
||||||
allocator.free(entry.section);
|
|
||||||
allocator.free(entry.description);
|
|
||||||
if (entry.path) |path| allocator.free(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
try std.testing.expectEqualStrings("grep", entry.name);
|
|
||||||
try std.testing.expectEqualStrings("1", entry.section);
|
|
||||||
try std.testing.expectEqualStrings("global regular expression print", entry.description);
|
|
||||||
try std.testing.expect(entry.path != null);
|
|
||||||
try std.testing.expectEqualStrings("/usr/share/man/man1/grep.1.gz", entry.path.?);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "ManEntryList - append multiple" {
|
test "parseManFilename - library function" {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
const result = parseManFilename("printf.3");
|
||||||
defer _ = gpa.deinit();
|
try std.testing.expect(result != null);
|
||||||
const allocator = gpa.allocator();
|
try std.testing.expectEqualStrings("printf", result.?.name);
|
||||||
|
try std.testing.expectEqualStrings("3", result.?.section);
|
||||||
var entries = types.ManEntryList.init(allocator);
|
}
|
||||||
defer {
|
|
||||||
for (entries.items) |entry| {
|
test "parseManFilename - subsection" {
|
||||||
allocator.free(entry.name);
|
const result = parseManFilename("SSL_new.3ssl");
|
||||||
allocator.free(entry.section);
|
try std.testing.expect(result != null);
|
||||||
allocator.free(entry.description);
|
try std.testing.expectEqualStrings("SSL_new", result.?.name);
|
||||||
}
|
try std.testing.expectEqualStrings("3ssl", result.?.section);
|
||||||
entries.deinit();
|
}
|
||||||
}
|
|
||||||
|
test "parseManFilename - no extension" {
|
||||||
try entries.append(types.ManEntry{
|
const result = parseManFilename("ls");
|
||||||
.name = try allocator.dupe(u8, "ls"),
|
try std.testing.expect(result == null);
|
||||||
.section = try allocator.dupe(u8, "1"),
|
}
|
||||||
.description = try allocator.dupe(u8, "list"),
|
|
||||||
.path = null,
|
test "keywordMatches - exact match" {
|
||||||
});
|
try std.testing.expect(keywordMatches("ls", "ls"));
|
||||||
|
}
|
||||||
try entries.append(types.ManEntry{
|
|
||||||
.name = try allocator.dupe(u8, "cat"),
|
test "keywordMatches - case insensitive" {
|
||||||
.section = try allocator.dupe(u8, "1"),
|
try std.testing.expect(keywordMatches("LS", "ls"));
|
||||||
.description = try allocator.dupe(u8, "concatenate"),
|
try std.testing.expect(keywordMatches("ls", "LS"));
|
||||||
.path = null,
|
}
|
||||||
});
|
|
||||||
|
test "keywordMatches - partial match" {
|
||||||
try std.testing.expectEqual(@as(usize, 2), entries.items.len);
|
try std.testing.expect(keywordMatches("perlsec", "sec"));
|
||||||
try std.testing.expectEqualStrings("ls", entries.items[0].name);
|
try std.testing.expect(keywordMatches("ls", "l"));
|
||||||
try std.testing.expectEqualStrings("cat", entries.items[1].name);
|
}
|
||||||
|
|
||||||
|
test "keywordMatches - no match" {
|
||||||
|
try std.testing.expect(!keywordMatches("ls", "cat"));
|
||||||
|
try std.testing.expect(!keywordMatches("ls", "xyz"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "keywordMatches - too long strings" {
|
||||||
|
const long_name = "a" ** 300;
|
||||||
|
try std.testing.expect(!keywordMatches(long_name, "ls"));
|
||||||
|
try std.testing.expect(!keywordMatches("ls", long_name));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,6 @@ pub const ManEntryList = std.ArrayList(ManEntry);
|
||||||
pub const SearchConfig = struct {
|
pub const SearchConfig = struct {
|
||||||
keyword: []const u8,
|
keyword: []const u8,
|
||||||
target_sections: ?[]const []const u8,
|
target_sections: ?[]const []const u8,
|
||||||
show_paths: bool,
|
|
||||||
verbose: bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue