mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
update output format
This commit is contained in:
@@ -1 +1,29 @@
|
||||
See @AGENTS.md
|
||||
# ghidra-cli Navigation Index
|
||||
|
||||
See @AGENTS.md for agent-specific instructions.
|
||||
|
||||
## Key Files
|
||||
|
||||
| What | When |
|
||||
|------|------|
|
||||
| `src/main.rs` | Modifying CLI entry point, daemon lifecycle, or output format detection |
|
||||
| `src/cli.rs` | Adding/modifying CLI arguments and subcommands |
|
||||
| `src/format/mod.rs` | Implementing new output formats or changing format detection logic |
|
||||
| `src/daemon/handlers/*.rs` | Implementing daemon command handlers |
|
||||
| `PLAN.md` | Understanding current implementation plan or reviewing decision rationale |
|
||||
| `README.md` | Understanding project architecture or user-facing command documentation |
|
||||
|
||||
## Modules
|
||||
|
||||
| What | When |
|
||||
|------|------|
|
||||
| `src/daemon/` | Working with persistent Ghidra daemon or IPC communication |
|
||||
| `src/format/` | Handling output format conversion (Table, Compact, JSON, CSV, etc.) |
|
||||
| `tests/` | Writing integration or unit tests |
|
||||
|
||||
## Documentation
|
||||
|
||||
| What | When |
|
||||
|------|------|
|
||||
| `src/daemon/README.md` | Understanding daemon architecture and IPC protocol |
|
||||
| `tests/README.md` | Understanding test structure and conventions |
|
||||
|
||||
@@ -196,20 +196,26 @@ ghidra daemon restart --project myproject --program otherbinary
|
||||
|
||||
## Output Formats
|
||||
|
||||
```bash
|
||||
# Human-readable (default)
|
||||
ghidra function list
|
||||
Default output adapts to context:
|
||||
- **Interactive (TTY)**: Compact human-readable format
|
||||
- **Piped/scripted**: Compact JSON for machine parsing
|
||||
|
||||
# JSON output
|
||||
Override with flags:
|
||||
```bash
|
||||
# Force JSON output (compact, single-line)
|
||||
ghidra function list --json
|
||||
|
||||
# Pretty JSON
|
||||
# Force pretty JSON (indented, multi-line)
|
||||
ghidra function list --pretty
|
||||
|
||||
# Select specific fields
|
||||
ghidra function list --fields "name,address,size"
|
||||
```
|
||||
|
||||
### Output Format Design
|
||||
|
||||
Format detection occurs at the CLI boundary rather than in daemon handlers. Handlers always return compact JSON for IPC efficiency and caching stability. The CLI applies format transformation (human-readable, pretty JSON) at the output boundary based on TTY detection or explicit flags. This design maintains a stable IPC protocol with a single format decision point, preventing daemon cache invalidation from format variations.
|
||||
|
||||
## Filtering
|
||||
|
||||
Use expressions to filter results:
|
||||
|
||||
@@ -15,6 +15,14 @@ pub struct Cli {
|
||||
/// Suppress non-essential output
|
||||
#[arg(short, long, global = true)]
|
||||
pub quiet: bool,
|
||||
|
||||
/// Output as JSON
|
||||
#[arg(long, global = true)]
|
||||
pub json: bool,
|
||||
|
||||
/// Output JSON with pretty formatting
|
||||
#[arg(long, global = true)]
|
||||
pub pretty: bool,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Clone, Serialize, Deserialize, Debug)]
|
||||
|
||||
@@ -40,7 +40,7 @@ pub async fn handle_batch(file_path: &str) -> Result<String> {
|
||||
"results": results
|
||||
});
|
||||
|
||||
serde_json::to_string_pretty(&response)
|
||||
serde_json::to_string(&response)
|
||||
.context("Failed to serialize batch results")
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ pub async fn handle_comment_list(bridge: &mut GhidraBridge) -> Result<String> {
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to list comments".to_string());
|
||||
@@ -31,7 +31,7 @@ pub async fn handle_comment_get(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to get comment".to_string());
|
||||
|
||||
@@ -16,7 +16,7 @@ pub async fn handle_diff_programs(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to diff programs".to_string());
|
||||
@@ -36,7 +36,7 @@ pub async fn handle_diff_functions(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to diff functions".to_string());
|
||||
|
||||
@@ -22,7 +22,7 @@ pub async fn handle_disasm(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to disassemble".to_string());
|
||||
|
||||
@@ -15,7 +15,7 @@ pub async fn handle_find_string(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to find string".to_string());
|
||||
@@ -34,7 +34,7 @@ pub async fn handle_find_bytes(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to find bytes".to_string());
|
||||
@@ -53,7 +53,7 @@ pub async fn handle_find_function(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to find function".to_string());
|
||||
@@ -72,7 +72,7 @@ pub async fn handle_find_calls(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to find calls".to_string());
|
||||
@@ -88,7 +88,7 @@ pub async fn handle_find_crypto(bridge: &mut GhidraBridge) -> Result<String> {
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to find crypto constants".to_string());
|
||||
@@ -104,7 +104,7 @@ pub async fn handle_find_interesting(bridge: &mut GhidraBridge) -> Result<String
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to find interesting functions".to_string());
|
||||
|
||||
@@ -21,7 +21,7 @@ pub async fn handle_graph_calls(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to get call graph".to_string());
|
||||
@@ -46,7 +46,7 @@ pub async fn handle_graph_callers(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to get callers".to_string());
|
||||
@@ -71,7 +71,7 @@ pub async fn handle_graph_callees(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to get callees".to_string());
|
||||
@@ -90,7 +90,7 @@ pub async fn handle_graph_export(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to export graph".to_string());
|
||||
|
||||
@@ -19,7 +19,7 @@ pub async fn handle_patch_bytes(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to patch bytes".to_string());
|
||||
@@ -38,7 +38,7 @@ pub async fn handle_patch_nop(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to patch NOP".to_string());
|
||||
@@ -57,7 +57,7 @@ pub async fn handle_patch_export(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to export patched binary".to_string());
|
||||
|
||||
@@ -45,7 +45,7 @@ pub async fn handle_program_info(bridge: &mut GhidraBridge) -> Result<String> {
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to get program info".to_string());
|
||||
@@ -73,7 +73,7 @@ pub async fn handle_program_export(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to export program".to_string());
|
||||
|
||||
@@ -16,7 +16,7 @@ pub async fn handle_script_run(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to run script".to_string());
|
||||
@@ -35,7 +35,7 @@ pub async fn handle_script_python(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to execute Python code".to_string());
|
||||
@@ -54,7 +54,7 @@ pub async fn handle_script_java(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to execute Java code".to_string());
|
||||
@@ -70,7 +70,7 @@ pub async fn handle_script_list(bridge: &mut GhidraBridge) -> Result<String> {
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to list scripts".to_string());
|
||||
|
||||
@@ -11,7 +11,7 @@ pub async fn handle_stats(bridge: &mut GhidraBridge) -> Result<String> {
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(serde_json::json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to get program statistics".to_string());
|
||||
|
||||
@@ -21,7 +21,7 @@ pub async fn handle_symbol_list(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to list symbols".to_string());
|
||||
@@ -40,7 +40,7 @@ pub async fn handle_symbol_get(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to get symbol".to_string());
|
||||
|
||||
@@ -12,7 +12,7 @@ pub async fn handle_type_list(bridge: &mut GhidraBridge) -> Result<String> {
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to list types".to_string());
|
||||
@@ -31,7 +31,7 @@ pub async fn handle_type_get(
|
||||
|
||||
if response.status == "success" {
|
||||
let data = response.data.unwrap_or(json!({}));
|
||||
serde_json::to_string_pretty(&data)
|
||||
serde_json::to_string(&data)
|
||||
.context("Failed to serialize response")
|
||||
} else {
|
||||
let message = response.message.unwrap_or_else(|| "Failed to get type".to_string());
|
||||
|
||||
+2
-1
@@ -91,6 +91,7 @@ impl Formatter for DefaultFormatter {
|
||||
OutputFormat::Tsv => {
|
||||
format_csv(data, '\t')
|
||||
}
|
||||
OutputFormat::Compact => format_minimal(data),
|
||||
OutputFormat::Minimal | OutputFormat::Ids => {
|
||||
format_minimal(data)
|
||||
}
|
||||
@@ -232,7 +233,7 @@ fn format_json_value(value: &JsonValue) -> String {
|
||||
|
||||
pub fn auto_detect_format(is_tty: bool) -> OutputFormat {
|
||||
if is_tty {
|
||||
OutputFormat::Table
|
||||
OutputFormat::Compact
|
||||
} else {
|
||||
OutputFormat::JsonCompact
|
||||
}
|
||||
|
||||
+28
-18
@@ -17,6 +17,8 @@ use error::{GhidraError, Result};
|
||||
use ghidra::GhidraClient;
|
||||
use std::path::PathBuf;
|
||||
use tracing::info;
|
||||
use atty;
|
||||
use format::{auto_detect_format, DefaultFormatter, Formatter, OutputFormat};
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
@@ -121,27 +123,18 @@ async fn run_with_daemon_check(cli: Cli) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
let config = Config::load()?;
|
||||
let project_path = match &cli.command {
|
||||
Commands::Import(args) => {
|
||||
resolve_project_path(&args.project, &config)?
|
||||
}
|
||||
Commands::Analyze(args) => {
|
||||
resolve_project_path(&args.project, &config)?
|
||||
}
|
||||
Commands::Quick(args) => {
|
||||
resolve_project_path(&args.project, &config)?
|
||||
}
|
||||
_ => {
|
||||
resolve_project_path(&None, &config)?
|
||||
}
|
||||
};
|
||||
|
||||
ensure_daemon_running(&project_path).await?;
|
||||
// Extract project and program from command arguments
|
||||
let (project_opt, program_opt) = extract_project_program(&cli.command);
|
||||
let project_path = resolve_project_path(&project_opt, &config)?;
|
||||
let program_name = program_opt.or(config.default_program.clone());
|
||||
|
||||
ensure_daemon_running(&project_path, program_name.as_deref()).await?;
|
||||
|
||||
match ipc::client::DaemonClient::connect().await {
|
||||
Ok(mut client) => {
|
||||
info!("Connected to daemon via IPC");
|
||||
let output = execute_via_daemon(&mut client, &cli.command).await?;
|
||||
let output = execute_via_daemon(&mut client, &cli.command, cli.json, cli.pretty).await?;
|
||||
if !output.is_empty() {
|
||||
println!("{}", output);
|
||||
}
|
||||
@@ -193,6 +186,8 @@ async fn ensure_daemon_running(project_path: &PathBuf) -> anyhow::Result<()> {
|
||||
async fn execute_via_daemon(
|
||||
client: &mut ipc::client::DaemonClient,
|
||||
command: &Commands,
|
||||
json_flag: bool,
|
||||
pretty_flag: bool,
|
||||
) -> anyhow::Result<String> {
|
||||
let result = match command {
|
||||
Commands::Import(args) => {
|
||||
@@ -339,8 +334,23 @@ async fn execute_via_daemon(
|
||||
_ => anyhow::bail!("Command not supported via daemon"),
|
||||
};
|
||||
|
||||
// Format the JSON output nicely
|
||||
serde_json::to_string_pretty(&result).map_err(Into::into)
|
||||
// Determine output format based on flags and TTY detection
|
||||
let format = if pretty_flag {
|
||||
OutputFormat::Json
|
||||
} else if json_flag {
|
||||
OutputFormat::JsonCompact
|
||||
} else {
|
||||
auto_detect_format(atty::is(atty::Stream::Stdout))
|
||||
};
|
||||
|
||||
// Detect if result is already an array before wrapping
|
||||
let values = match result {
|
||||
serde_json::Value::Array(arr) => arr,
|
||||
single => vec![single],
|
||||
};
|
||||
|
||||
let formatter = DefaultFormatter;
|
||||
formatter.format(&values, format).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Handle daemon management commands.
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
//! Integration tests for output format
|
||||
//! Tests require real Ghidra installation
|
||||
|
||||
use assert_cmd::Command;
|
||||
|
||||
/// Helper to verify Ghidra is installed before running tests
|
||||
fn require_ghidra() {
|
||||
let output = Command::cargo_bin("ghidra")
|
||||
.unwrap()
|
||||
.arg("doctor")
|
||||
.output()
|
||||
.expect("Failed to run ghidra doctor");
|
||||
|
||||
if !output.status.success() {
|
||||
panic!("Ghidra is not installed. Tests require Ghidra installation per AGENTS.md");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_detection_tty() {
|
||||
require_ghidra();
|
||||
|
||||
// Test that --help shows both --json and --pretty flags
|
||||
let mut cmd = Command::cargo_bin("ghidra").unwrap();
|
||||
cmd.arg("--help");
|
||||
cmd.assert().success();
|
||||
|
||||
let output = cmd.output().unwrap();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("--json"), "Help should show --json flag");
|
||||
assert!(stdout.contains("--pretty"), "Help should show --pretty flag");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_flag() {
|
||||
require_ghidra();
|
||||
|
||||
// Test --json flag is recognized
|
||||
let mut cmd = Command::cargo_bin("ghidra").unwrap();
|
||||
cmd.arg("--json").arg("--help");
|
||||
cmd.assert().success();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pretty_flag() {
|
||||
require_ghidra();
|
||||
|
||||
// Test --pretty flag is recognized
|
||||
let mut cmd = Command::cargo_bin("ghidra").unwrap();
|
||||
cmd.arg("--pretty").arg("--help");
|
||||
cmd.assert().success();
|
||||
}
|
||||
Reference in New Issue
Block a user