Convert manwhere to submodule
This commit is contained in:
parent
3c104f7ced
commit
50244b7eb9
9 changed files with 4 additions and 1378 deletions
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "scripts/manwhere"]
|
||||
path = scripts/manwhere
|
||||
url = https://git.jfraeys.com/jfraeysd/manwhere.git
|
||||
1
scripts/manwhere
Submodule
1
scripts/manwhere
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 26eb50d38f31ad0f791242e84481f57c4d2326e9
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
# manwhere
|
||||
|
||||
A fast, simple command-line tool to search man pages by keyword, written in Zig.
|
||||
|
||||
## Features
|
||||
|
||||
- **Fast keyword search** using `man -k` (apropos)
|
||||
- **Section filtering** to search specific man page sections (1-9)
|
||||
- **Path resolution** to show actual file locations
|
||||
- **Verbose mode** with timing information
|
||||
- **POSIX compatible** - works on Linux and macOS
|
||||
- **Simple and reliable** - no complex dependencies
|
||||
|
||||
## Project Structure
|
||||
|
||||
The project is organized into logical modules for maintainability:
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.zig # Main program entry point
|
||||
├── types.zig # Data structures and types
|
||||
├── parser.zig # Command line argument parsing
|
||||
├── search.zig # Man page search functionality
|
||||
├── display.zig # Output formatting and display
|
||||
└── performance.zig # Essential performance optimizations
|
||||
```
|
||||
|
||||
### Module Responsibilities
|
||||
|
||||
- **`types.zig`**: Contains `ManEntry`, `ManEntryList`, and `SearchConfig` structures
|
||||
- **`parser.zig`**: Handles command line argument parsing, validation, and help text
|
||||
- **`search.zig`**: Manages man page searching, parsing, and path resolution
|
||||
- **`display.zig`**: Formats and displays search results and progress information
|
||||
- **`performance.zig`**: Essential optimizations for fast string operations and I/O
|
||||
- **`main.zig`**: Orchestrates the program flow and coordinates between modules
|
||||
|
||||
## Performance Features
|
||||
|
||||
The tool is optimized for speed while maintaining simplicity:
|
||||
|
||||
- **Early filtering**: Section filtering happens during parsing for better performance
|
||||
- **Efficient I/O**: Optimized buffer sizes and reading strategies
|
||||
- **Fast string operations**: Optimized string comparisons for common cases
|
||||
- **Memory efficiency**: Exponential growth strategy for collections
|
||||
- **POSIX optimization**: Common man page directories checked first
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Search
|
||||
```bash
|
||||
manwhere sleep # Find all man pages mentioning "sleep"
|
||||
```
|
||||
|
||||
### Section Filtering
|
||||
```bash
|
||||
manwhere -s 1 sleep # Find only commands (section 1)
|
||||
manwhere --section 3 sleep # Find only library functions (section 3)
|
||||
```
|
||||
|
||||
### Verbose Mode
|
||||
```bash
|
||||
manwhere -v sleep # Show timing and processing details
|
||||
manwhere --verbose sleep # Same as above
|
||||
```
|
||||
|
||||
### Path Resolution
|
||||
```bash
|
||||
manwhere --paths sleep # Show file paths (slower but more detailed)
|
||||
```
|
||||
|
||||
### Combined Options
|
||||
```bash
|
||||
manwhere -v --paths -s 1 sleep # Verbose, with paths, section 1 only
|
||||
```
|
||||
|
||||
### Help
|
||||
```bash
|
||||
manwhere -h # Show help message
|
||||
manwhere --help # Same as above
|
||||
```
|
||||
|
||||
## Man Page Sections
|
||||
|
||||
- **1**: Commands (user commands)
|
||||
- **2**: System calls
|
||||
- **3**: Library functions
|
||||
- **4**: Device files
|
||||
- **5**: File formats
|
||||
- **6**: Games
|
||||
- **7**: Miscellaneous
|
||||
- **8**: System administration
|
||||
- **9**: Kernel routines
|
||||
|
||||
## Building
|
||||
|
||||
### Prerequisites
|
||||
- Zig 0.15.1 or later
|
||||
|
||||
### Build Commands
|
||||
```bash
|
||||
# Debug build (good for development)
|
||||
zig build
|
||||
|
||||
# Release build (maximum performance)
|
||||
zig build -Doptimize=ReleaseFast
|
||||
|
||||
# Clean build
|
||||
zig build clean
|
||||
```
|
||||
|
||||
### Install to ~/.local/bin
|
||||
```bash
|
||||
zig build release
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
The tool is designed to be fast and efficient:
|
||||
|
||||
- **Typical search time**: 700-800ms for common keywords
|
||||
- **Section filtering**: 20-30% faster than searching all sections
|
||||
- **Path resolution**: Adds ~200-300ms for detailed results
|
||||
- **Memory usage**: ~8-12MB for typical searches
|
||||
- **Scalability**: Handles large result sets efficiently
|
||||
|
||||
## POSIX Compatibility
|
||||
|
||||
Works reliably on:
|
||||
- **Linux**: Standard `/usr/share/man` and `/usr/local/share/man`
|
||||
- **macOS**: Standard paths plus Homebrew (`/opt/homebrew/share/man`) and MacPorts (`/opt/local/share/man`)
|
||||
- **Other Unix-like systems**: Standard man page directory structures
|
||||
|
||||
## Error Handling
|
||||
|
||||
The program provides clear error messages for:
|
||||
- Invalid command line options
|
||||
- Missing required arguments
|
||||
- Invalid section numbers
|
||||
- Multiple keywords (not supported)
|
||||
- Search failures
|
||||
|
||||
## Why Simple and Fast?
|
||||
|
||||
This tool prioritizes:
|
||||
1. **Reliability** - Works consistently across POSIX systems
|
||||
2. **Speed** - Essential optimizations without complexity
|
||||
3. **Simplicity** - Easy to understand, maintain, and debug
|
||||
4. **POSIX compatibility** - No platform-specific dependencies
|
||||
5. **Performance** - Fast enough for daily use without over-engineering
|
||||
|
||||
## Contributing
|
||||
|
||||
The simple structure makes it easy to:
|
||||
- Add new search features
|
||||
- Implement different output formats
|
||||
- Add support for additional platforms
|
||||
- Extend the man page section detection
|
||||
- Optimize performance further
|
||||
|
||||
## License
|
||||
|
||||
This project is open source. See the source code for license details.
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
// ============================================================================
|
||||
// MODULES
|
||||
// ============================================================================
|
||||
|
||||
// Main module (uses standard optimize option)
|
||||
const root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// Debug module (always Debug optimization)
|
||||
const debug_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = .Debug,
|
||||
});
|
||||
|
||||
// Release module (always ReleaseFast optimization)
|
||||
const release_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = .ReleaseFast,
|
||||
});
|
||||
|
||||
// Test module (uses standard optimize option)
|
||||
const test_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// EXECUTABLES
|
||||
// ============================================================================
|
||||
|
||||
// Main executable
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "manwhere",
|
||||
.root_module = root_module,
|
||||
});
|
||||
b.installArtifact(exe);
|
||||
|
||||
// Debug executable
|
||||
const debug_exe = b.addExecutable(.{
|
||||
.name = "manwhere-debug",
|
||||
.root_module = debug_module,
|
||||
});
|
||||
|
||||
// Release executable
|
||||
const release_exe = b.addExecutable(.{
|
||||
.name = "manwhere",
|
||||
.root_module = release_module,
|
||||
});
|
||||
|
||||
// Check executable (for compilation check)
|
||||
const check_exe = b.addExecutable(.{
|
||||
.name = "check-compile",
|
||||
.root_module = root_module,
|
||||
});
|
||||
|
||||
// Test executable
|
||||
const unit_tests = b.addTest(.{
|
||||
.root_module = test_module,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// BUILD STEPS
|
||||
// ============================================================================
|
||||
|
||||
// Default run step
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
if (b.args) |args| run_cmd.addArgs(args);
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
// Debug build step
|
||||
const debug_step = b.step("debug", "Build with Debug optimization");
|
||||
b.installArtifact(debug_exe);
|
||||
debug_step.dependOn(b.getInstallStep());
|
||||
|
||||
// Debug run step
|
||||
const debug_run_cmd = b.addRunArtifact(debug_exe);
|
||||
if (b.args) |args| debug_run_cmd.addArgs(args);
|
||||
debug_run_cmd.step.dependOn(&debug_exe.step);
|
||||
const debug_run_step = b.step("debug-run", "Run the app with Debug optimization");
|
||||
debug_run_step.dependOn(&debug_run_cmd.step);
|
||||
|
||||
// Release step - build optimized binary and install to ~/.local/bin
|
||||
const release_step = b.step("release", "Build optimized binary and install to ~/.local/bin/");
|
||||
|
||||
// Get home directory from environment
|
||||
const home = std.process.getEnvVarOwned(b.allocator, "HOME") catch {
|
||||
std.debug.print("Error: HOME environment variable not set\n", .{});
|
||||
return;
|
||||
};
|
||||
defer b.allocator.free(home);
|
||||
|
||||
const install_path = std.fs.path.join(b.allocator, &.{ home, ".local", "bin" }) catch {
|
||||
std.debug.print("Error: Failed to construct install path\n", .{});
|
||||
return;
|
||||
};
|
||||
defer b.allocator.free(install_path);
|
||||
|
||||
const install_exe = b.addInstallArtifact(release_exe, .{
|
||||
.dest_dir = .{ .override = .{ .custom = install_path } },
|
||||
});
|
||||
release_step.dependOn(&install_exe.step);
|
||||
|
||||
// Test step - run all unit tests
|
||||
const run_unit_tests = b.addRunArtifact(unit_tests);
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&run_unit_tests.step);
|
||||
|
||||
// Check step - compilation check without output
|
||||
const check_step = b.step("check", "Check code formatting and compilation");
|
||||
check_step.dependOn(&check_exe.step);
|
||||
|
||||
// Format step - format source code
|
||||
const fmt_step = b.step("fmt", "Format source code");
|
||||
const fmt_cmd = b.addFmt(.{
|
||||
.paths = &.{ "src", "build.zig" },
|
||||
.check = false,
|
||||
});
|
||||
fmt_step.dependOn(&fmt_cmd.step);
|
||||
|
||||
// Clean step - remove build artifacts
|
||||
const clean_step = b.step("clean", "Remove build artifacts");
|
||||
const clean_cmd = b.addSystemCommand(&.{
|
||||
"rm", "-rf", ".zig-cache", "zig-out",
|
||||
});
|
||||
clean_step.dependOn(&clean_cmd.step);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
const std = @import("std");
|
||||
const types = @import("types.zig");
|
||||
const parser = @import("parser.zig");
|
||||
const search = @import("search.zig");
|
||||
const display = @import("display.zig");
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
// Following Zig 0.15.1 writergate changes - buffer goes in the interface
|
||||
var stderr_buffer: [1024]u8 = undefined;
|
||||
var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
|
||||
const stderr = &stderr_writer.interface;
|
||||
|
||||
const args = try std.process.argsAlloc(allocator);
|
||||
defer std.process.argsFree(allocator, args);
|
||||
|
||||
// Parse command line arguments
|
||||
const config = parser.parseArgs(allocator, args) catch |err| switch (err) {
|
||||
error.HelpRequested => return,
|
||||
error.InvalidOption, error.InvalidSection, error.MultipleKeywords, error.NoKeyword => std.process.exit(1),
|
||||
else => return err,
|
||||
};
|
||||
|
||||
// Display search start message
|
||||
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.flush(); // Don't forget to flush!
|
||||
return;
|
||||
};
|
||||
defer {
|
||||
for (entries.items) |entry| {
|
||||
// Free all the allocated strings from parseManLine
|
||||
allocator.free(entry.name);
|
||||
allocator.free(entry.section);
|
||||
allocator.free(entry.description);
|
||||
if (entry.path) |path| allocator.free(path);
|
||||
}
|
||||
entries.deinit(allocator);
|
||||
}
|
||||
|
||||
// Calculate duration
|
||||
const end_time = std.time.nanoTimestamp();
|
||||
const duration_ms = @as(f64, @floatFromInt(end_time - start_time)) / 1_000_000.0;
|
||||
|
||||
// Display results
|
||||
try display.displaySearchResults(entries, config, duration_ms);
|
||||
}
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
const std = @import("std");
|
||||
const types = @import("types.zig");
|
||||
const print = std.debug.print;
|
||||
|
||||
pub const HELP_TEXT =
|
||||
\\Usage: manwhere [OPTIONS] <keyword>
|
||||
\\Find man pages containing a keyword.
|
||||
\\
|
||||
\\OPTIONS:
|
||||
\\ -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)
|
||||
\\ Can be specified multiple times
|
||||
\\
|
||||
\\EXAMPLES:
|
||||
\\ manwhere sleep # find all man pages mentioning "sleep"
|
||||
\\ manwhere -s 1 sleep # find only commands (section 1) mentioning "sleep"
|
||||
\\ manwhere -s 1 -s 3 sleep # find in sections 1 and 3
|
||||
\\ manwhere -v --paths ssl # detailed search with paths and timing
|
||||
\\ manwhere --section 3 printf # find only library functions (section 3)
|
||||
\\
|
||||
;
|
||||
|
||||
pub fn parseArgs(allocator: std.mem.Allocator, args: [][:0]u8) !types.SearchConfig {
|
||||
if (args.len < 2) {
|
||||
print("{s}", .{HELP_TEXT});
|
||||
return error.NoKeyword;
|
||||
}
|
||||
|
||||
var keyword: []const u8 = "";
|
||||
var sections_list = std.ArrayList([]const u8){};
|
||||
defer sections_list.deinit(allocator);
|
||||
var show_paths = false;
|
||||
var verbose = false;
|
||||
|
||||
var i: usize = 1;
|
||||
while (i < args.len) : (i += 1) {
|
||||
const arg = args[i];
|
||||
|
||||
if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) {
|
||||
print("{s}", .{HELP_TEXT});
|
||||
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")) {
|
||||
// Next argument should be the section number
|
||||
if (i + 1 >= args.len) {
|
||||
print("[X] Option {s} requires a section number (1-9)\n", .{arg});
|
||||
return error.InvalidOption;
|
||||
}
|
||||
i += 1;
|
||||
const section_arg = args[i];
|
||||
|
||||
// Validate section number
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
} else if (std.mem.startsWith(u8, arg, "-")) {
|
||||
print("[X] Unknown option: {s}\nUse -h or --help for usage information\n", .{arg});
|
||||
return error.InvalidOption;
|
||||
} else if (keyword.len == 0) {
|
||||
keyword = arg;
|
||||
} else {
|
||||
print("[X] Multiple keywords not supported\n", .{});
|
||||
return error.MultipleKeywords;
|
||||
}
|
||||
}
|
||||
|
||||
if (keyword.len == 0) {
|
||||
print("[X] No keyword provided\n{s}", .{HELP_TEXT});
|
||||
return error.NoKeyword;
|
||||
}
|
||||
|
||||
// Convert ArrayList to owned slice, or null if empty
|
||||
const target_sections = if (sections_list.items.len > 0)
|
||||
try sections_list.toOwnedSlice(allocator)
|
||||
else
|
||||
null;
|
||||
|
||||
return types.SearchConfig{
|
||||
.keyword = keyword,
|
||||
.target_sections = target_sections,
|
||||
.show_paths = show_paths,
|
||||
.verbose = verbose,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TESTS
|
||||
// ============================================================================
|
||||
|
||||
test "parseArgs - basic keyword" {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const args = &[_][:0]u8{ "manwhere", "sleep" };
|
||||
const config = try parseArgs(allocator, args);
|
||||
|
||||
try std.testing.expectEqualStrings("sleep", config.keyword);
|
||||
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" {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const args = &[_][:0]u8{ "manwhere", "-s", "1", "ls" };
|
||||
const config = try parseArgs(allocator, args);
|
||||
|
||||
try std.testing.expectEqualStrings("ls", config.keyword);
|
||||
try std.testing.expect(config.target_sections != null);
|
||||
try std.testing.expectEqual(@as(usize, 1), config.target_sections.?.len);
|
||||
try std.testing.expectEqualStrings("1", config.target_sections.?[0]);
|
||||
}
|
||||
|
||||
test "parseArgs - with multiple sections" {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const args = &[_][:0]u8{ "manwhere", "-s", "1", "-s", "3", "test" };
|
||||
const config = try parseArgs(allocator, args);
|
||||
|
||||
try std.testing.expectEqualStrings("test", config.keyword);
|
||||
try std.testing.expect(config.target_sections != null);
|
||||
try std.testing.expectEqual(@as(usize, 2), config.target_sections.?.len);
|
||||
try std.testing.expectEqualStrings("1", config.target_sections.?[0]);
|
||||
try std.testing.expectEqualStrings("3", config.target_sections.?[1]);
|
||||
}
|
||||
|
||||
test "parseArgs - error no keyword" {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const args = &[_][:0]u8{"manwhere"};
|
||||
const result = parseArgs(allocator, args);
|
||||
|
||||
try std.testing.expectError(error.NoKeyword, result);
|
||||
}
|
||||
|
||||
test "parseArgs - error invalid option" {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const args = &[_][:0]u8{ "manwhere", "--invalid", "ls" };
|
||||
const result = parseArgs(allocator, args);
|
||||
|
||||
try std.testing.expectError(error.InvalidOption, result);
|
||||
}
|
||||
|
||||
test "parseArgs - error multiple keywords" {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const args = &[_][:0]u8{ "manwhere", "ls", "cat" };
|
||||
const result = parseArgs(allocator, args);
|
||||
|
||||
try std.testing.expectError(error.MultipleKeywords, result);
|
||||
}
|
||||
|
|
@ -1,488 +0,0 @@
|
|||
const std = @import("std");
|
||||
const types = @import("types.zig");
|
||||
|
||||
// Fast man page discovery by directly scanning filesystem
|
||||
const ManSearcher = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
entries: types.ManEntryList,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
// Common man page directories in priority order
|
||||
const MAN_DIRS = [_][]const u8{
|
||||
"/usr/share/man", // Standard Linux/macOS
|
||||
"/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)
|
||||
const EXTENSIONS = [_][]const u8{ "", ".gz", ".bz2", ".xz", ".lz", ".Z" };
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) Self {
|
||||
return Self{
|
||||
.allocator = allocator,
|
||||
.entries = .{},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
for (self.entries.items) |entry| {
|
||||
self.allocator.free(entry.name);
|
||||
self.allocator.free(entry.section);
|
||||
self.allocator.free(entry.description);
|
||||
if (entry.path) |path| self.allocator.free(path);
|
||||
}
|
||||
self.entries.deinit(self.allocator);
|
||||
}
|
||||
|
||||
// Extract man page name and section from filename
|
||||
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,
|
||||
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
|
||||
pub fn searchManPagesFast(
|
||||
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 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 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) {
|
||||
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();
|
||||
return .{};
|
||||
}
|
||||
|
||||
var entries: types.ManEntryList = .{};
|
||||
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
|
||||
pub fn searchManPages(
|
||||
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
|
||||
return searchManPagesOriginal(keyword, target_section, show_paths, verbose, allocator, stderr);
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TESTS
|
||||
// ============================================================================
|
||||
|
||||
test "ManSearcher - basic initialization" {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
// 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" {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
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" {
|
||||
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"),
|
||||
.path = null,
|
||||
});
|
||||
|
||||
try entries.append(types.ManEntry{
|
||||
.name = try allocator.dupe(u8, "cat"),
|
||||
.section = try allocator.dupe(u8, "1"),
|
||||
.description = try allocator.dupe(u8, "concatenate"),
|
||||
.path = null,
|
||||
});
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 2), entries.items.len);
|
||||
try std.testing.expectEqualStrings("ls", entries.items[0].name);
|
||||
try std.testing.expectEqualStrings("cat", entries.items[1].name);
|
||||
}
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const ManEntry = struct {
|
||||
name: []const u8,
|
||||
section: []const u8,
|
||||
description: []const u8,
|
||||
path: ?[]const u8 = null,
|
||||
|
||||
pub fn getSectionMarker(self: ManEntry) []const u8 {
|
||||
if (self.section.len == 0) return "[?] Unknown";
|
||||
// Fast path for common sections
|
||||
return switch (self.section[0]) {
|
||||
'1' => "[*] Command",
|
||||
'2' => "[S] Syscall",
|
||||
'3' => blk: {
|
||||
// Check for specific 3xxx subsections (order matters - longer first)
|
||||
if (std.mem.startsWith(u8, self.section, "3ssl")) break :blk "[L] OpenSSL";
|
||||
if (std.mem.startsWith(u8, self.section, "3tcl")) break :blk "[L] Tcl API";
|
||||
if (std.mem.startsWith(u8, self.section, "3pm")) break :blk "[L] Perl";
|
||||
if (std.mem.startsWith(u8, self.section, "3p")) break :blk "[L] POSIX";
|
||||
if (std.mem.startsWith(u8, self.section, "3x")) break :blk "[L] X11/ncurses";
|
||||
break :blk "[L] C Library";
|
||||
},
|
||||
'4' => "[D] Device",
|
||||
'5' => "[F] Format",
|
||||
'6' => "[G] Game",
|
||||
'7' => "[M] Misc",
|
||||
'8' => "[A] Admin",
|
||||
'9' => "[K] Kernel",
|
||||
else => "[?] Unknown",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn matchesSection(self: ManEntry, target_sections: ?[]const []const u8) bool {
|
||||
if (target_sections == null) return true;
|
||||
for (target_sections.?) |section| {
|
||||
if (self.section.len == 0) continue;
|
||||
if (std.mem.eql(u8, self.section, section)) return true;
|
||||
if (section.len == 1 and std.mem.startsWith(u8, self.section, section)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Use modern ArrayList instead of custom implementation
|
||||
pub const ManEntryList = std.ArrayList(ManEntry);
|
||||
|
||||
pub const SearchConfig = struct {
|
||||
keyword: []const u8,
|
||||
target_sections: ?[]const []const u8,
|
||||
show_paths: bool,
|
||||
verbose: bool,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// TESTS
|
||||
// ============================================================================
|
||||
|
||||
test "ManEntry.getSectionMarker - commands" {
|
||||
const entry = ManEntry{
|
||||
.name = "ls",
|
||||
.section = "1",
|
||||
.description = "list directory contents",
|
||||
.path = null,
|
||||
};
|
||||
const marker = entry.getSectionMarker();
|
||||
try std.testing.expectEqualStrings("[*] Command", marker);
|
||||
}
|
||||
|
||||
test "ManEntry.getSectionMarker - syscalls" {
|
||||
const entry = ManEntry{
|
||||
.name = "open",
|
||||
.section = "2",
|
||||
.description = "open file",
|
||||
.path = null,
|
||||
};
|
||||
const marker = entry.getSectionMarker();
|
||||
try std.testing.expectEqualStrings("[S] Syscall", marker);
|
||||
}
|
||||
|
||||
test "ManEntry.getSectionMarker - C library" {
|
||||
const entry = ManEntry{
|
||||
.name = "printf",
|
||||
.section = "3",
|
||||
.description = "format and print data",
|
||||
.path = null,
|
||||
};
|
||||
const marker = entry.getSectionMarker();
|
||||
try std.testing.expectEqualStrings("[L] C Library", marker);
|
||||
}
|
||||
|
||||
test "ManEntry.getSectionMarker - OpenSSL" {
|
||||
const entry = ManEntry{
|
||||
.name = "SSL_new",
|
||||
.section = "3ssl",
|
||||
.description = "create SSL structure",
|
||||
.path = null,
|
||||
};
|
||||
const marker = entry.getSectionMarker();
|
||||
try std.testing.expectEqualStrings("[L] OpenSSL", marker);
|
||||
}
|
||||
|
||||
test "ManEntry.getSectionMarker - POSIX" {
|
||||
const entry = ManEntry{
|
||||
.name = "pthread_create",
|
||||
.section = "3p",
|
||||
.description = "create thread",
|
||||
.path = null,
|
||||
};
|
||||
const marker = entry.getSectionMarker();
|
||||
try std.testing.expectEqualStrings("[L] POSIX", marker);
|
||||
}
|
||||
|
||||
test "ManEntry.getSectionMarker - kernel" {
|
||||
const entry = ManEntry{
|
||||
.name = "kmalloc",
|
||||
.section = "9",
|
||||
.description = "kernel memory alloc",
|
||||
.path = null,
|
||||
};
|
||||
const marker = entry.getSectionMarker();
|
||||
try std.testing.expectEqualStrings("[K] Kernel", marker);
|
||||
}
|
||||
|
||||
test "ManEntry.getSectionMarker - unknown" {
|
||||
const entry = ManEntry{
|
||||
.name = "unknown",
|
||||
.section = "0",
|
||||
.description = "invalid section",
|
||||
.path = null,
|
||||
};
|
||||
const marker = entry.getSectionMarker();
|
||||
try std.testing.expectEqualStrings("[?] Unknown", marker);
|
||||
}
|
||||
|
||||
test "ManEntry.matchesSection - no target" {
|
||||
const entry = ManEntry{
|
||||
.name = "ls",
|
||||
.section = "1",
|
||||
.description = "list",
|
||||
.path = null,
|
||||
};
|
||||
try std.testing.expect(entry.matchesSection(null));
|
||||
}
|
||||
|
||||
test "ManEntry.matchesSection - exact match" {
|
||||
const entry = ManEntry{
|
||||
.name = "ls",
|
||||
.section = "1",
|
||||
.description = "list",
|
||||
.path = null,
|
||||
};
|
||||
const targets = &[_][]const u8{"1"};
|
||||
try std.testing.expect(entry.matchesSection(targets));
|
||||
}
|
||||
|
||||
test "ManEntry.matchesSection - no match" {
|
||||
const entry = ManEntry{
|
||||
.name = "ls",
|
||||
.section = "1",
|
||||
.description = "list",
|
||||
.path = null,
|
||||
};
|
||||
const targets = &[_][]const u8{"3"};
|
||||
try std.testing.expect(!entry.matchesSection(targets));
|
||||
}
|
||||
Loading…
Reference in a new issue