fix: make help and version successful discovery
Some checks failed
checks / flake (push) Has been cancelled

This commit is contained in:
hermes 2026-07-28 19:26:43 +00:00
commit 5c81b469e5
5 changed files with 88 additions and 1 deletions

6
PRD.md
View file

@ -813,6 +813,12 @@ JSON mode is a strict framing protocol:
- An error includes the path, byte size, and content digest of its Diagnostic
Log when one is available.
CLI discovery is the sole framing exception. `ghidr --help`, every
`ghidr <command> --help`, and `ghidr --version` are successful metadata
requests rather than operations: they emit plain UTF-8 text on stdout, leave
stderr empty, exit with status 0, and never initialize Ghidra, access the
Analysis Store, or execute a command.
Human-readable mode may present progress and concise diagnostics on stderr.
Every Ghidra/adapter invocation captures diagnostics into a private temporary

View file

@ -21,6 +21,17 @@ Each command will identify the sample by its content, reuse compatible cached
analysis, and produce versioned JSON by default. Human-readable presentation is
available explicitly through `--format human`.
Agents can discover the complete command surface without initializing Ghidra:
```console
ghidr --help
ghidr decompile --help
ghidr --version
```
These discovery requests return plain text on stdout with exit status 0 and
leave stderr empty.
## Documents
- [PRD.md](./PRD.md) defines the product requirements and initial delivery

View file

@ -11,3 +11,10 @@ whether stdout is a terminal or pipe; human-readable presentation requires
command has the same framing and semantics in every execution environment.
JSON mode never emits interactive prompts; required confirmation is represented
as a structured error and satisfied by an explicit flag.
Clap discovery requests are intentionally outside the operation-result
protocol. Root and subcommand `--help`, plus `--version`, return conventional
plain UTF-8 text on stdout with status 0 and no stderr. They do not initialize
the runtime or execute an operation. This narrow exception keeps the CLI
self-describing for both people and automation agents without weakening JSON
framing for analysis and cleanup commands.

View file

@ -5,7 +5,7 @@ use std::{
io::{self, Write},
};
use clap::Parser as _;
use clap::{Parser as _, error::ErrorKind};
use serde_json::Value;
use crate::{
@ -46,6 +46,15 @@ where
let arguments: Vec<OsString> = arguments.into_iter().map(Into::into).collect();
match Cli::try_parse_from(arguments.clone()) {
Ok(cli) => run_parsed(&cli, executor, stdout, stderr),
Err(error)
if matches!(
error.kind(),
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
) =>
{
stdout.write_all(error.to_string().as_bytes())?;
Ok(ExitStatus::Success)
}
Err(error) => {
if requests_human_format(&arguments) {
stderr.write_all(error.to_string().as_bytes())?;
@ -173,6 +182,35 @@ mod tests {
assert_eq!(document["error"]["code"], "invalid_arguments");
}
#[test]
fn discovery_requests_use_plain_stdout_and_succeed() {
for (arguments, expected) in [
(&["ghidr", "--help"][..], "Usage: ghidr"),
(
&["ghidr", "decompile", "--help"][..],
"Usage: ghidr decompile",
),
(&["ghidr", "--version"][..], "ghidr 0.1.0"),
] {
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let status = run_from(
arguments.iter().copied(),
&SuccessExecutor,
&mut stdout,
&mut stderr,
)
.expect("write succeeds");
assert_eq!(status, ExitStatus::Success);
assert!(stderr.is_empty());
assert!(
String::from_utf8(stdout)
.expect("help is UTF-8")
.contains(expected)
);
}
}
#[test]
fn oversized_executor_success_cannot_escape_inline_bound() {
let mut stdout = Vec::new();

View file

@ -10,6 +10,31 @@ use std::{
};
use rustix::process::{Pid, Signal, kill_process};
#[test]
fn help_and_version_are_successful_discovery_output() {
let cases = [
(vec!["--help"], "Usage: ghidr"),
(vec!["doctor", "--help"], "Usage: ghidr doctor"),
(vec!["inspect", "--help"], "Usage: ghidr inspect"),
(vec!["functions", "--help"], "Usage: ghidr functions"),
(vec!["decompile", "--help"], "Usage: ghidr decompile"),
(vec!["clean", "--help"], "Usage: ghidr clean"),
(vec!["--version"], "ghidr 0.1.0"),
];
for (arguments, expected) in cases {
let output = Command::new(env!("CARGO_BIN_EXE_ghidr"))
.args(arguments)
.output()
.expect("run discovery request");
assert_eq!(output.status.code(), Some(0), "{:?}", output.stderr);
assert!(output.stderr.is_empty());
let stdout = String::from_utf8(output.stdout).expect("discovery output is UTF-8");
assert!(stdout.contains(expected), "{stdout:?}");
}
}
#[test]
fn doctor_reports_ready_with_closed_runtime_paths() {
let temp = tempfile::tempdir().expect("temp");