open Inshellah.Parser open Inshellah.Manpage open Inshellah.Nushell let failures = ref 0 let passes = ref 0 let check name condition = if condition then begin incr passes; Printf.printf " PASS: %s\n" name end else begin incr failures; Printf.printf " FAIL: %s\n" name end let parse txt = match parse_help txt with | Ok r -> r | Error msg -> failwith (Printf.sprintf "parse_help failed: %s" msg) (* --- Help parser tests --- *) let test_gnu_basic () = Printf.printf "\n== GNU basic flags ==\n"; let r = parse " -a, --all do not ignore entries starting with .\n" in check "one entry" (List.length r.entries = 1); let e = List.hd r.entries in check "both switch" (e.switch = Both ('a', "all")); check "no param" (e.param = None); check "desc" (String.length e.desc > 0) let test_gnu_eq_param () = Printf.printf "\n== GNU = param ==\n"; let r = parse " --block-size=SIZE scale sizes by SIZE\n" in check "one entry" (List.length r.entries = 1); let e = List.hd r.entries in check "long switch" (e.switch = Long "block-size"); check "mandatory param" (e.param = Some (Mandatory "SIZE")) let test_gnu_opt_param () = Printf.printf "\n== GNU optional param ==\n"; let r = parse " --color[=WHEN] color the output WHEN\n" in check "one entry" (List.length r.entries = 1); let e = List.hd r.entries in check "long switch" (e.switch = Long "color"); check "optional param" (e.param = Some (Optional "WHEN")) let test_underscore_param () = Printf.printf "\n== Underscore in param (TIME_STYLE) ==\n"; let r = parse " --time-style=TIME_STYLE time/date format\n" in check "one entry" (List.length r.entries = 1); let e = List.hd r.entries in check "param with underscore" (e.param = Some (Mandatory "TIME_STYLE")) let test_short_only () = Printf.printf "\n== Short-only flag ==\n"; let r = parse " -v verbose output\n" in check "one entry" (List.length r.entries = 1); check "short switch" ((List.hd r.entries).switch = Short 'v') let test_long_only () = Printf.printf "\n== Long-only flag ==\n"; let r = parse " --help display help\n" in check "one entry" (List.length r.entries = 1); check "long switch" ((List.hd r.entries).switch = Long "help") let test_multiline_desc () = Printf.printf "\n== Multi-line description ==\n"; let r = parse {| --block-size=SIZE with -l, scale sizes by SIZE when printing them; e.g., '--block-size=M'; see SIZE format below |} in check "one entry" (List.length r.entries = 1); let e = List.hd r.entries in check "desc includes continuation" (String.length e.desc > 50) let test_multiple_entries () = Printf.printf "\n== Multiple entries ==\n"; let r = parse {| -a, --all do not ignore entries starting with . -A, --almost-all do not list implied . and .. --author with -l, print the author of each file |} in check "three entries" (List.length r.entries = 3) let test_clap_short_sections () = Printf.printf "\n== Clap short with section headers ==\n"; let r = parse {|INPUT OPTIONS: -e, --regexp=PATTERN A pattern to search for. -f, --file=PATTERNFILE Search for patterns from the given file. SEARCH OPTIONS: -s, --case-sensitive Search case sensitively. |} in check "three entries" (List.length r.entries = 3); let e = List.hd r.entries in check "first is regexp" (e.switch = Both ('e', "regexp")); check "first has param" (e.param = Some (Mandatory "PATTERN")) let test_clap_long_style () = Printf.printf "\n== Clap long style (desc below flag) ==\n"; let r = parse {| -H, --hidden Include hidden directories and files. --no-ignore Do not respect ignore files. |} in check "two entries" (List.length r.entries = 2); let e = List.hd r.entries in check "hidden switch" (e.switch = Both ('H', "hidden")); check "desc below" (String.length e.desc > 0) let test_clap_long_angle_param () = Printf.printf "\n== Clap long angle bracket param ==\n"; let r = parse {| --nonprintable-notation Set notation for non-printable characters. |} in check "one entry" (List.length r.entries = 1); let e = List.hd r.entries in check "long switch" (e.switch = Long "nonprintable-notation"); check "angle param" (e.param = Some (Mandatory "notation")) let test_space_upper_param () = Printf.printf "\n== Space-separated ALL_CAPS param ==\n"; let r = parse " -f, --foo FOO foo help\n" in check "one entry" (List.length r.entries = 1); let e = List.hd r.entries in check "switch" (e.switch = Both ('f', "foo")); check "space param" (e.param = Some (Mandatory "FOO")) let test_go_cobra_flags () = Printf.printf "\n== Go/Cobra flags ==\n"; let r = parse {|Flags: -D, --debug Enable debug mode -H, --host string Daemon socket to connect to -v, --version Print version information |} in check "three flag entries" (List.length r.entries = 3); (* Check the host flag has a type param *) let host = List.nth r.entries 1 in check "host switch" (host.switch = Both ('H', "host")); check "host type param" (host.param = Some (Mandatory "string")) let test_go_cobra_subcommands () = Printf.printf "\n== Go/Cobra subcommands ==\n"; let r = parse {|Common Commands: run Create and run a new container from an image exec Execute a command in a running container build Build an image from a Dockerfile |} in check "has subcommands" (List.length r.subcommands > 0) let test_busybox_tab () = Printf.printf "\n== Busybox tab-indented ==\n"; let r = parse "\t-1\tOne column output\n\t-a\tInclude names starting with .\n" in check "two entries" (List.length r.entries = 2); check "first is -1" ((List.hd r.entries).switch = Short '1') let test_no_debug_prints () = Printf.printf "\n== No debug side effects ==\n"; (* The old parser had print_endline at module load time. If we got here without "opt param is running" on stdout, we're good. *) check "no debug prints" true (* --- Manpage parser tests --- *) let test_manpage_tp_style () = Printf.printf "\n== Manpage .TP style ==\n"; let groff = {|.SH OPTIONS .TP \fB\-a\fR, \fB\-\-all\fR do not ignore entries starting with . .TP \fB\-A\fR, \fB\-\-almost\-all\fR do not list implied . and .. .TP \fB\-\-block\-size\fR=\fISIZE\fR with \fB\-l\fR, scale sizes by SIZE .SH AUTHOR Written by someone. |} in let result = parse_manpage_string groff in check "three entries" (List.length result.entries = 3); if List.length result.entries >= 1 then begin let e = List.hd result.entries in check "first is -a/--all" (e.switch = Both ('a', "all")); check "first desc" (String.length e.desc > 0) end; if List.length result.entries >= 3 then begin let e = List.nth result.entries 2 in check "block-size switch" (e.switch = Long "block-size"); check "block-size param" (e.param = Some (Mandatory "SIZE")) end let test_manpage_ip_style () = Printf.printf "\n== Manpage .IP style ==\n"; let groff = {|.SH OPTIONS .IP "\fB\-k\fR, \fB\-\-insecure\fR" Allow insecure connections. .IP "\fB\-o\fR, \fB\-\-output\fR \fIfile\fR" Write output to file. .SH SEE ALSO |} in let result = parse_manpage_string groff in check "two entries" (List.length result.entries = 2); if List.length result.entries >= 1 then begin let e = List.hd result.entries in check "first is -k/--insecure" (e.switch = Both ('k', "insecure")) end let test_manpage_groff_stripping () = Printf.printf "\n== Groff escape stripping ==\n"; let s = strip_groff_escapes {|\fB\-\-color\fR[=\fIWHEN\fR]|} in check "font escapes removed" (not (String.contains s 'f' && String.contains s 'B')); check "dashes converted" (String.contains s '-'); let s2 = strip_groff_escapes {|\(aqhello\(aq|} in check "aq -> quote" (String.contains s2 '\'') let test_manpage_empty_options () = Printf.printf "\n== Manpage with no OPTIONS section ==\n"; let groff = {|.SH NAME foo \- does stuff .SH DESCRIPTION Does stuff. |} in let result = parse_manpage_string groff in check "no entries" (List.length result.entries = 0) let test_slash_switch_separator () = Printf.printf "\n== Slash switch separator (--long / -s) ==\n"; let r = parse " --verbose / -v Increase verbosity\n" in check "one entry" (List.length r.entries = 1); let e = List.hd r.entries in check "both switch" (e.switch = Both ('v', "verbose")); check "no param" (e.param = None); check "desc" (e.desc = "Increase verbosity") let test_manpage_nix3_style () = Printf.printf "\n== Manpage nix3 style ==\n"; let groff = {|.SH Options .SS Logging-related options .IP "\(bu" 3 .UR #opt-verbose \f(CR--verbose\fR .UE / \f(CR-v\fR .IP Increase the logging verbosity level. .IP "\(bu" 3 .UR #opt-quiet \f(CR--quiet\fR .UE .IP Decrease the logging verbosity level. .SH SEE ALSO |} in let result = parse_manpage_string groff in check "two entries" (List.length result.entries = 2); if List.length result.entries >= 1 then begin let e = List.hd result.entries in check "verbose is Both" (e.switch = Both ('v', "verbose")); check "verbose desc" (String.length e.desc > 0) end; if List.length result.entries >= 2 then begin let e = List.nth result.entries 1 in check "quiet is Long" (e.switch = Long "quiet"); check "quiet desc" (String.length e.desc > 0) end let test_manpage_nix3_with_params () = Printf.printf "\n== Manpage nix3 with params ==\n"; let groff = {|.SH Options .IP "\(bu" 3 .UR #opt-arg \f(CR--arg\fR .UE \fIname\fR \fIexpr\fR .IP Pass the value as the argument name to Nix functions. .IP "\(bu" 3 .UR #opt-include \f(CR--include\fR .UE / \f(CR-I\fR \fIpath\fR .IP Add path to search path entries. .IP This option may be given multiple times. .SH SEE ALSO |} in let result = parse_manpage_string groff in check "two entries" (List.length result.entries = 2); if List.length result.entries >= 1 then begin let e = List.hd result.entries in check "arg is Long" (e.switch = Long "arg"); check "arg has param" (e.param <> None) end; if List.length result.entries >= 2 then begin let e = List.nth result.entries 1 in check "include is Both" (e.switch = Both ('I', "include")); check "include has path param" (e.param = Some (Mandatory "path")) end let test_synopsis_subcommand () = Printf.printf "\n== SYNOPSIS subcommand detection ==\n"; let groff = {|.SH "SYNOPSIS" .sp .nf \fBgit\fR \fBcommit\fR [\fB\-a\fR | \fB\-\-interactive\fR] .fi .SH "DESCRIPTION" |} in let cmd = extract_synopsis_command groff in check "detected git commit" (cmd = Some "git commit") let test_synopsis_standalone () = Printf.printf "\n== SYNOPSIS standalone command ==\n"; let groff = {|.SH Synopsis .LP \f(CRnix-build\fR [\fIpaths\fR] .SH Description |} in let cmd = extract_synopsis_command groff in check "detected nix-build" (cmd = Some "nix-build") let test_synopsis_nix3 () = Printf.printf "\n== SYNOPSIS nix3 subcommand ==\n"; let groff = {|.SH Synopsis .LP \f(CRnix run\fR [\fIoption\fR] \fIinstallable\fR .SH Description |} in let cmd = extract_synopsis_command groff in check "detected nix run" (cmd = Some "nix run") (* --- Nushell generation tests --- *) let contains s sub = try let _ = Str.search_forward (Str.regexp_string sub) s 0 in true with Not_found -> false let test_nushell_basic () = Printf.printf "\n== Nushell basic extern ==\n"; let r = parse " -a, --all do not ignore entries starting with .\n" in let nu = generate_extern "ls" r in check "has extern" (contains nu "export extern \"ls\""); check "has --all(-a)" (contains nu "--all(-a)"); check "has comment" (contains nu "# do not ignore") let test_nushell_param_types () = Printf.printf "\n== Nushell param type mapping ==\n"; let r = parse {| -w, --width=COLS set output width --block-size=SIZE scale sizes -o, --output FILE output file |} in let nu = generate_extern "ls" r in check "COLS -> int" (contains nu "--width(-w): int"); check "SIZE -> string" (contains nu "--block-size: string"); check "FILE -> path" (contains nu "--output(-o): path") let test_nushell_subcommands () = Printf.printf "\n== Nushell subcommands ==\n"; let r = parse {|Common Commands: run Create and run a new container exec Execute a command Flags: -D, --debug Enable debug mode |} in let nu = generate_extern "docker" r in check "has main extern" (contains nu "export extern \"docker\""); check "has --debug" (contains nu "--debug(-D)"); check "has run subcommand" (contains nu "export extern \"docker run\""); check "has exec subcommand" (contains nu "export extern \"docker exec\"") let test_nushell_from_manpage () = Printf.printf "\n== Nushell from manpage ==\n"; let groff = {|.SH OPTIONS .TP \fB\-a\fR, \fB\-\-all\fR do not ignore entries starting with . .TP \fB\-\-block\-size\fR=\fISIZE\fR scale sizes by SIZE .SH AUTHOR |} in let result = parse_manpage_string groff in let nu = generate_extern "ls" result in check "has extern" (contains nu "export extern \"ls\""); check "has --all(-a)" (contains nu "--all(-a)"); check "has --block-size" (contains nu "--block-size: string") let test_nushell_module () = Printf.printf "\n== Nushell module wrapper ==\n"; let r = parse " -v, --verbose verbose output\n" in let nu = generate_module "myapp" r in check "has module" (contains nu "module myapp-completions"); check "has extern inside" (contains nu "export extern \"myapp\""); check "has flag" (contains nu "--verbose(-v)") let test_dedup_entries () = Printf.printf "\n== Deduplication ==\n"; let r = parse {| -v, --verbose verbose output --verbose verbose mode -v be verbose |} in let nu = generate_extern "test" r in (* Count occurrences of --verbose *) let count = let re = Str.regexp_string "--verbose" in let n = ref 0 in let i = ref 0 in (try while true do let _ = Str.search_forward re nu !i in incr n; i := Str.match_end () done with Not_found -> ()); !n in check "verbose appears once" (count = 1); check "best version kept (Both)" (contains nu "--verbose(-v)") let test_dedup_manpage () = Printf.printf "\n== Dedup from manpage ==\n"; let groff = {|.SH OPTIONS .TP \fB\-v\fR, \fB\-\-verbose\fR Be verbose. .SH DESCRIPTION Use \fB\-v\fR for verbose output. Use \fB\-\-verbose\fR to see more. |} in let result = parse_manpage_string groff in let nu = generate_extern "test" result in check "has --verbose(-v)" (contains nu "--verbose(-v)"); (* Should not have standalone -v or duplicate --verbose *) let lines = String.split_on_char '\n' nu in let verbose_lines = List.filter (fun l -> contains l "verbose") lines in check "only one verbose line" (List.length verbose_lines = 1) let test_commands_section_subcommands () = Printf.printf "\n== COMMANDS section subcommand extraction ==\n"; (* manpages like systemctl have a COMMANDS section with bold command names * inside .PP + .RS/.RE blocks. these should be extracted as subcommands * and treated as leaf nodes (no entries of their own). *) let groff = {|.SH OPTIONS .TP \fB\-\-user\fR Talk to the service manager of the calling user. .TP \fB\-\-system\fR Talk to the service manager of the system. .SH COMMANDS .PP \fBstart\fR \fIUNIT\fR\&... .RS 4 Start (activate) one or more units. .RE .PP \fBstop\fR \fIUNIT\fR\&... .RS 4 Stop (deactivate) one or more units. .RE .PP \fBreload\fR \fIUNIT\fR\&... .RS 4 Asks all units to reload their configuration. .RE .SH SEE ALSO |} in let result = parse_manpage_string groff in check "has options entries" (List.length result.entries = 2); check "has subcommands" (List.length result.subcommands = 3); let sc_names = List.map (fun (sc : subcommand) -> sc.name) result.subcommands in check "has start" (List.mem "start" sc_names); check "has stop" (List.mem "stop" sc_names); check "has reload" (List.mem "reload" sc_names); (* verify subcommand descriptions are extracted *) let start_sc = List.find (fun (sc : subcommand) -> sc.name = "start") result.subcommands in check "start has desc" (String.length start_sc.desc > 0) let test_self_listing_detection () = Printf.printf "\n== Self-listing subcommand detection ==\n"; (* when a subcommand's --help shows the parent's help text, * the subcommand name appears in its own subcommand list. * the parser should detect this — tested via parse_help. *) let help_text = {|systemctl [OPTIONS...] COMMAND ... Unit Commands: start UNIT... Start (activate) one or more units stop UNIT... Stop (deactivate) one or more units status [PATTERN...] Show runtime status Options: --user Talk to the user service manager --system Talk to the system service manager |} in let r = parse help_text in let has_start = List.exists (fun (sc : subcommand) -> sc.name = "start") r.subcommands in check "detected start as subcommand" has_start; (* the self-listing logic (in main.ml) would check: is "start" in r.subcommands? * here we just verify the parser extracts it correctly. *) check "has entries too" (List.length r.entries >= 2) let test_nu_file_parsing () = Printf.printf "\n== .nu file parsing ==\n"; let nu_source = {|module completions { # Unofficial CLI tool export extern mytool [ --help(-h) # Print help --version(-V) # Print version ] # List all items export extern "mytool list" [ --raw # Output as JSON --format(-f): string # Output format --help(-h) # Print help name?: string # Filter by name ] } use completions * |} in let r = Inshellah.Store.parse_nu_completions "mytool" nu_source in check "has entries" (List.length r.entries = 2); check "has subcommands" (List.length r.subcommands >= 1); let list_sc = List.find_opt (fun (sc : subcommand) -> sc.name = "list") r.subcommands in check "has list subcommand" (list_sc <> None); check "description" (r.description = "Unofficial CLI tool"); (* test subcommand lookup *) let r2 = Inshellah.Store.parse_nu_completions "mytool list" nu_source in check "list has entries" (List.length r2.entries = 3); let has_format = List.exists (fun (e : entry) -> e.switch = Both ('f', "format")) r2.entries in check "list has --format(-f)" has_format; check "list has positional" (List.length r2.positionals >= 1) let test_italic_synopsis () = Printf.printf "\n== Italic in SYNOPSIS ==\n"; let groff = {|.SH Synopsis .LP \f(CRnix-env\fR \fIoperation\fR [\fIoptions\fR] [\fIarguments…\fR] .SH Description |} in let cmd = extract_synopsis_command groff in check "no phantom operation" (cmd = Some "nix-env") let test_font_boundary_spacing () = Printf.printf "\n== Font boundary spacing ==\n"; (* \fB--max-results\fR\fIcount\fR should become "--max-results count" *) let s = strip_groff_escapes {|\fB\-\-max\-results\fR\fIcount\fR|} in check "has space before param" (contains s "--max-results count"); (* \fB--color\fR[=\fIWHEN\fR] should NOT insert space before = *) let s2 = strip_groff_escapes {|\fB\-\-color\fR[=\fIWHEN\fR]|} in check "no space before =" (contains s2 "--color[=WHEN]") let () = Printf.printf "Running help parser tests...\n"; test_gnu_basic (); test_gnu_eq_param (); test_gnu_opt_param (); test_underscore_param (); test_short_only (); test_long_only (); test_multiline_desc (); test_multiple_entries (); test_clap_short_sections (); test_clap_long_style (); test_clap_long_angle_param (); test_space_upper_param (); test_go_cobra_flags (); test_go_cobra_subcommands (); test_busybox_tab (); test_no_debug_prints (); Printf.printf "\nRunning manpage parser tests...\n"; test_manpage_tp_style (); test_manpage_ip_style (); test_manpage_groff_stripping (); test_manpage_empty_options (); test_slash_switch_separator (); test_manpage_nix3_style (); test_manpage_nix3_with_params (); test_synopsis_subcommand (); test_synopsis_standalone (); test_synopsis_nix3 (); Printf.printf "\nRunning nushell generation tests...\n"; test_nushell_basic (); test_nushell_param_types (); test_nushell_subcommands (); test_nushell_from_manpage (); test_nushell_module (); Printf.printf "\nRunning dedup and font tests...\n"; test_dedup_entries (); test_dedup_manpage (); test_font_boundary_spacing (); Printf.printf "\nRunning COMMANDS section tests...\n"; test_commands_section_subcommands (); test_self_listing_detection (); Printf.printf "\nRunning .nu and synopsis tests...\n"; test_nu_file_parsing (); test_italic_synopsis (); Printf.printf "\n=== Results: %d passed, %d failed ===\n" !passes !failures; if !failures > 0 then exit 1