Skip to content

CLI: Implement output format selection (--top, --json flags) #31

Closed

Description

@unclesp1d3r

Summary

Implement CLI arguments for output format selection, enabling users to control result limiting and choose between human-readable and JSONL output formats.

Context

The Stringy CLI currently accepts only an input file path. The output system needs to support multiple formats to serve different use cases:

  • Human-readable: Interactive analysis with formatted tables
  • JSONL: Machine-readable for automation and pipelines
  • YARA: Security rule generation (future enhancement)

The core types (FoundString, Encoding, Tag, etc.) are fully defined in src/types.rs, and the output module skeleton exists at src/output/mod.rs.

Requirements

From the specification:

  • 7.5: Implement --top N argument to limit results to top N strings by score
  • 7.6: Add --json flag for JSONL output format (one JSON object per line)
  • 7.7: Default to human-readable table format when no format flag is specified

Proposed Solution

1. CLI Argument Structure

Extend the existing Cli struct in src/main.rs:

use clap::{Parser, ValueEnum};
use std::path::PathBuf;

#[derive(Parser)]
#[command(name = "stringy")]
#[command(about = "Extract meaningful strings from binary files")]
#[command(version)]
struct Cli {
    /// Input binary file to analyze
    #[arg(value_name = "FILE")]
    input: PathBuf,
    
    /// Output format
    #[arg(short = 'f', long, value_enum, default_value = "human")]
    format: OutputFormat,
    
    /// Shorthand for --format json
    #[arg(long, conflicts_with = "format")]
    json: bool,
    
    /// Limit to top N results by score
    #[arg(short = 't', long, value_name = "N")]
    top: Option<usize>,
}

#[derive(Clone, Copy, ValueEnum)]
enum OutputFormat {
    Human,
    Json,
}

2. Output Module Implementation

Create output formatters in src/output/:

src/output/mod.rs:

mod human;
mod json;

pub use human::HumanFormatter;
pub use json::JsonFormatter;

use crate::types::{FoundString, Result};

pub trait OutputFormatter {
    fn format(&self, strings: &[FoundString]) -> Result<String>;
}

src/output/human.rs:

use crate::output::OutputFormatter;
use crate::types::{FoundString, Result};

pub struct HumanFormatter;

impl OutputFormatter for HumanFormatter {
    fn format(&self, strings: &[FoundString]) -> Result<String> {
        let mut output = String::new();
        output.push_str("Score  Offset    Section      Tags           String\n");
        output.push_str("-----  ------    -------      ----           ------\n");
        
        for s in strings {
            let tags_str = s.tags.iter()
                .map(|t| format!("{:?}", t).to_lowercase())
                .collect::<Vec<_>>()
                .join(",");
            
            output.push_str(&format!(
                "{:5}  0x{:06x}  {:12} {:14} {}\n",
                s.score,
                s.offset,
                s.section.as_ref().unwrap_or(&String::from("<unknown>")),
                tags_str,
                s.text.chars().take(50).collect::<String>()
            ));
        }
        
        Ok(output)
    }
}

src/output/json.rs:

use crate::output::OutputFormatter;
use crate::types::{FoundString, Result};
use serde_json;

pub struct JsonFormatter;

impl OutputFormatter for JsonFormatter {
    fn format(&self, strings: &[FoundString]) -> Result<String> {
        let mut output = String::new();
        for s in strings {
            output.push_str(&serde_json::to_string(s)?);
            output.push('\n');
        }
        Ok(output)
    }
}

3. Main Pipeline Integration

Update src/main.rs:

use stringy::output::{HumanFormatter, JsonFormatter, OutputFormatter};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cli = Cli::parse();
    
    // Determine output format
    let format = if cli.json {
        OutputFormat::Json
    } else {
        cli.format
    };
    
    // TODO: Extract strings from binary (future PR)
    // let strings = extract_strings(&cli.input)?;
    
    // For now, use mock data
    let mut strings = vec![]; // Will be populated by extraction pipeline
    
    // Apply top limit if specified
    if let Some(top) = cli.top {
        strings.truncate(top);
    }
    
    // Format and output
    let formatter: Box<dyn OutputFormatter> = match format {
        OutputFormat::Human => Box::new(HumanFormatter),
        OutputFormat::Json => Box::new(JsonFormatter),
    };
    
    let output = formatter.format(&strings)?;
    println!("{}", output);
    
    Ok(())
}

4. Integration Tests

Create tests/cli_output_formats.rs:

use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::TempDir;

#[test]
fn test_default_human_readable_output() {
    let mut cmd = Command::cargo_bin("stringy").unwrap();
    cmd.arg("tests/fixtures/elf/simple_hello_world")
        .assert()
        .success()
        .stdout(predicate::str::contains("Score"))
        .stdout(predicate::str::contains("Offset"));
}

#[test]
fn test_json_flag() {
    let mut cmd = Command::cargo_bin("stringy").unwrap();
    cmd.arg("--json")
        .arg("tests/fixtures/elf/simple_hello_world")
        .assert()
        .success()
        .stdout(predicate::str::is_json());
}

#[test]
fn test_format_json() {
    let mut cmd = Command::cargo_bin("stringy").unwrap();
    cmd.arg("--format").arg("json")
        .arg("tests/fixtures/elf/simple_hello_world")
        .assert()
        .success()
        .stdout(predicate::str::is_json());
}

#[test]
fn test_top_limit() {
    let mut cmd = Command::cargo_bin("stringy").unwrap();
    let output = cmd.arg("--json")
        .arg("--top").arg("5")
        .arg("tests/fixtures/elf/simple_hello_world")
        .output()
        .unwrap();
    
    let line_count = String::from_utf8_lossy(&output.stdout)
        .lines()
        .filter(|l| !l.is_empty())
        .count();
    
    assert!(line_count <= 5, "Expected at most 5 results, got {}", line_count);
}

#[test]
fn test_json_and_format_conflict() {
    let mut cmd = Command::cargo_bin("stringy").unwrap();
    cmd.arg("--json")
        .arg("--format").arg("human")
        .arg("tests/fixtures/elf/simple_hello_world")
        .assert()
        .failure(); // Should fail due to conflicting arguments
}

Add test dependencies to Cargo.toml:

[dev-dependencies]
assert_cmd = "2.0"
predicates = "3.0"

Implementation Checklist

  • Update Cli struct in src/main.rs with new arguments
  • Implement OutputFormatter trait in src/output/mod.rs
  • Create HumanFormatter in src/output/human.rs
  • Create JsonFormatter in src/output/json.rs
  • Integrate formatters into main pipeline
  • Handle --top limiting in main
  • Add assert_cmd and predicates dev dependencies
  • Create tests/cli_output_formats.rs with integration tests
  • Ensure all tests pass: cargo test
  • Verify CLI works: cargo run -- --help
  • Update documentation if needed

Testing Strategy

  1. Unit tests: Test formatters individually with mock FoundString data
  2. Integration tests: Test CLI argument parsing and output format selection
  3. Manual testing: Verify output appearance and usability

Dependencies

  • Blocked by: Basic CLI Structure ✅ (Complete - basic clap structure exists)
  • Blocks: Future output features (YARA format, filtering options)

Related Files

  • src/main.rs: CLI argument parsing
  • src/output/mod.rs: Output module (currently empty)
  • src/types.rs: Core types (FoundString, Encoding, Tag)
  • tests/fixtures/: Test binary files for integration tests

References

  • Documentation: docs/src/output-formats.md
  • Documentation: docs/src/cli.md
  • Requirements: 7.5, 7.6, 7.7
  • Task ID: stringy-analyzer/output-format-cli-arguments

Activity

  1. added this to the v0.1 milestone on Sep 30, 2025
  2. self-assigned this
    on Sep 30, 2025
  3. added theissue type on Nov 10, 2025
  4. unclesp1d3r commented on Nov 10, 2025

    @unclesp1d3r
    MemberAuthor

    @coderabbitai Enhance this issue body with additional context and a proposed solution. Improve the issue title and assign the proper labels and other metadata.

  5. changed the title [-]Output Format CLI Arguments[/-] [+]CLI: Implement output format selection (--top, --json flags)[/+] on Nov 10, 2025
  6. traycerai commented on Nov 10, 2025

    @traycerai

    Warning

    Rate limit exceeded. Please try after 1229 seconds.

  7. coderabbitai commented on Nov 10, 2025

    @coderabbitai
    Contributor

    Tip

    For best results, initiate chat on the files or code changes.

    🧩 Analysis chain

    🏁 Script executed:

    #!/bin/bash
    
    # Fetch the current issue details
    gh issue view 31 --json title,body,labels,assignees,milestone,state --repo EvilBit-Labs/StringyMcStringFace
    

    Length of output: 1340


    🏁 Script executed:

    #!/bin/bash
    
    # Find CLI-related files
    fd -e py -e rs -e go cli
    
    # Find main entry point or CLI argument parsing
    fd -e py -e rs -e go main
    
    # Search for existing output format code
    rg -n -C3 --type=py --type=rust --type=go -i "output.*format|format.*output|json|human.readable"
    
    # Search for argument parsing (argparse, clap, cobra, etc.)
    rg -n -C3 --type=py --type=rust --type=go "argparse|clap|cobra|flag" 
    

    Length of output: 6456


    🏁 Script executed:

    #!/bin/bash
    
    # Look for project structure and language
    ls -la
    cat README.md 2>/dev/null || echo "No README found"
    
    # Check for requirements or dependencies
    cat requirements.txt 2>/dev/null || echo "No requirements.txt"
    cat Cargo.toml 2>/dev/null || echo "No Cargo.toml"
    cat go.mod 2>/dev/null || echo "No go.mod"
    

    Length of output: 9975


    🏁 Script executed:

    #!/bin/bash
    
    # Read the main.rs to see current CLI implementation
    cat src/main.rs
    
    # Read the output module
    cat src/output/mod.rs
    
    # Check if there are any other output-related files
    fd -e rs . src/output
    
    # Look for existing tests
    fd -t f test src/
    fd -t f integration tests/
    

    Length of output: 796


    🏁 Script executed:

    #!/bin/bash
    
    # Look for any existing CLI argument definitions
    rg -n -A10 "struct.*Cli|struct.*Args|Parser" src/main.rs
    
    # Check for existing output format implementations
    rg -n -C5 "enum.*Output|Format|Formatter" src/output/
    
    # Look at the types module to understand data structures
    cat src/types.rs 2>/dev/null | head -100
    

    Length of output: 3066


    🏁 Script executed:

    #!/bin/bash
    
    # Get the full types.rs to understand ExtractedString
    cat src/types.rs
    
    # Check if there's any implementation in output module
    find src/output -name "*.rs" -exec echo "=== {} ===" \; -exec cat {} \;
    
    # Look for task specifications
    cat .kiro/specs/stringy-binary-analyzer/tasks.md 2>/dev/null | grep -A30 "output-format-cli-arguments" || echo "Task spec not found"
    

    Length of output: 5189


    🏁 Script executed:

    #!/bin/bash
    
    # Check for related requirements in docs
    fd -e md . docs/ -x cat {}
    
    # Look for any existing JSON or output examples
    rg -n -C3 "json|jsonl|JSONL" src/ tests/
    

    Length of output: 125950


    🏁 Script executed:

    #!/bin/bash
    
    # Update the issue with enhanced content
    gh issue edit 31 --repo EvilBit-Labs/StringyMcStringFace --title "CLI: Implement output format selection (--top, --json flags)" --body "## Summary
    
    Implement CLI arguments for output format selection, enabling users to control result limiting and choose between human-readable and JSONL output formats.
    
    ## Context
    
    The Stringy CLI currently accepts only an input file path. The output system needs to support multiple formats to serve different use cases:
    - **Human-readable**: Interactive analysis with formatted tables
    - **JSONL**: Machine-readable for automation and pipelines
    - **YARA**: Security rule generation (future enhancement)
    
    The core types (\`FoundString\`, \`Encoding\`, \`Tag\`, etc.) are fully defined in \`src/types.rs\`, and the output module skeleton exists at \`src/output/mod.rs\`.
    
    ## Requirements
    
    From the specification:
    - **7.5**: Implement \`--top N\` argument to limit results to top N strings by score
    - **7.6**: Add \`--json\` flag for JSONL output format (one JSON object per line)
    - **7.7**: Default to human-readable table format when no format flag is specified
    
    ## Proposed Solution
    
    ### 1. CLI Argument Structure
    
    Extend the existing \`Cli\` struct in \`src/main.rs\`:
    
    \`\`\`rust
    use clap::{Parser, ValueEnum};
    use std::path::PathBuf;
    
    #[derive(Parser)]
    #[command(name = \"stringy\")]
    #[command(about = \"Extract meaningful strings from binary files\")]
    #[command(version)]
    struct Cli {
        /// Input binary file to analyze
        #[arg(value_name = \"FILE\")]
        input: PathBuf,
        
        /// Output format
        #[arg(short = 'f', long, value_enum, default_value = \"human\")]
        format: OutputFormat,
        
        /// Shorthand for --format json
        #[arg(long, conflicts_with = \"format\")]
        json: bool,
        
        /// Limit to top N results by score
        #[arg(short = 't', long, value_name = \"N\")]
        top: Option<usize>,
    }
    
    #[derive(Clone, Copy, ValueEnum)]
    enum OutputFormat {
        Human,
        Json,
    }
    \`\`\`
    
    ### 2. Output Module Implementation
    
    Create output formatters in \`src/output/\`:
    
    **\`src/output/mod.rs\`**:
    \`\`\`rust
    mod human;
    mod json;
    
    pub use human::HumanFormatter;
    pub use json::JsonFormatter;
    
    use crate::types::{FoundString, Result};
    
    pub trait OutputFormatter {
        fn format(&self, strings: &[FoundString]) -> Result<String>;
    }
    \`\`\`
    
    **\`src/output/human.rs\`**:
    \`\`\`rust
    use crate::output::OutputFormatter;
    use crate::types::{FoundString, Result};
    
    pub struct HumanFormatter;
    
    impl OutputFormatter for HumanFormatter {
        fn format(&self, strings: &[FoundString]) -> Result<String> {
            let mut output = String::new();
            output.push_str(\"Score  Offset    Section      Tags           String\\n\");
            output.push_str(\"-----  ------    -------      ----           ------\\n\");
            
            for s in strings {
                let tags_str = s.tags.iter()
                    .map(|t| format!(\"{:?}\", t).to_lowercase())
                    .collect::<Vec<_>>()
                    .join(\",\");
                
                output.push_str(&format!(
                    \"{:5}  0x{:06x}  {:12} {:14} {}\\n\",
                    s.score,
                    s.offset,
                    s.section.as_ref().unwrap_or(&String::from(\"<unknown>\")),
                    tags_str,
                    s.text.chars().take(50).collect::<String>()
                ));
            }
            
            Ok(output)
        }
    }
    \`\`\`
    
    **\`src/output/json.rs\`**:
    \`\`\`rust
    use crate::output::OutputFormatter;
    use crate::types::{FoundString, Result};
    use serde_json;
    
    pub struct JsonFormatter;
    
    impl OutputFormatter for JsonFormatter {
        fn format(&self, strings: &[FoundString]) -> Result<String> {
            let mut output = String::new();
            for s in strings {
                output.push_str(&serde_json::to_string(s)?);
                output.push('\\n');
            }
            Ok(output)
        }
    }
    \`\`\`
    
    ### 3. Main Pipeline Integration
    
    Update \`src/main.rs\`:
    
    \`\`\`rust
    use stringy::output::{HumanFormatter, JsonFormatter, OutputFormatter};
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let cli = Cli::parse();
        
        // Determine output format
        let format = if cli.json {
            OutputFormat::Json
        } else {
            cli.format
        };
        
        // TODO: Extract strings from binary (future PR)
        // let strings = extract_strings(&cli.input)?;
        
        // For now, use mock data
        let mut strings = vec![]; // Will be populated by extraction pipeline
        
        // Apply top limit if specified
        if let Some(top) = cli.top {
            strings.truncate(top);
        }
        
        // Format and output
        let formatter: Box<dyn OutputFormatter> = match format {
            OutputFormat::Human => Box::new(HumanFormatter),
            OutputFormat::Json => Box::new(JsonFormatter),
        };
        
        let output = formatter.format(&strings)?;
        println!(\"{}\", output);
        
        Ok(())
    }
    \`\`\`
    
    ### 4. Integration Tests
    
    Create \`tests/cli_output_formats.rs\`:
    
    \`\`\`rust
    use assert_cmd::Command;
    use predicates::prelude::*;
    use std::fs;
    use tempfile::TempDir;
    
    #[test]
    fn test_default_human_readable_output() {
        let mut cmd = Command::cargo_bin(\"stringy\").unwrap();
        cmd.arg(\"tests/fixtures/elf/simple_hello_world\")
            .assert()
            .success()
            .stdout(predicate::str::contains(\"Score\"))
            .stdout(predicate::str::contains(\"Offset\"));
    }
    
    #[test]
    fn test_json_flag() {
        let mut cmd = Command::cargo_bin(\"stringy\").unwrap();
        cmd.arg(\"--json\")
            .arg(\"tests/fixtures/elf/simple_hello_world\")
            .assert()
            .success()
            .stdout(predicate::str::is_json());
    }
    
    #[test]
    fn test_format_json() {
        let mut cmd = Command::cargo_bin(\"stringy\").unwrap();
        cmd.arg(\"--format\").arg(\"json\")
            .arg(\"tests/fixtures/elf/simple_hello_world\")
            .assert()
            .success()
            .stdout(predicate::str::is_json());
    }
    
    #[test]
    fn test_top_limit() {
        let mut cmd = Command::cargo_bin(\"stringy\").unwrap();
        let output = cmd.arg(\"--json\")
            .arg(\"--top\").arg(\"5\")
            .arg(\"tests/fixtures/elf/simple_hello_world\")
            .output()
            .unwrap();
        
        let line_count = String::from_utf8_lossy(&output.stdout)
            .lines()
            .filter(|l| !l.is_empty())
            .count();
        
        assert!(line_count <= 5, \"Expected at most 5 results, got {}\", line_count);
    }
    
    #[test]
    fn test_json_and_format_conflict() {
        let mut cmd = Command::cargo_bin(\"stringy\").unwrap();
        cmd.arg(\"--json\")
            .arg(\"--format\").arg(\"human\")
            .arg(\"tests/fixtures/elf/simple_hello_world\")
            .assert()
            .failure(); // Should fail due to conflicting arguments
    }
    \`\`\`
    
    Add test dependencies to \`Cargo.toml\`:
    \`\`\`toml
    [dev-dependencies]
    assert_cmd = \"2.0\"
    predicates = \"3.0\"
    \`\`\`
    
    ## Implementation Checklist
    
    - [ ] Update \`Cli\` struct in \`src/main.rs\` with new arguments
    - [ ] Implement \`OutputFormatter\` trait in \`src/output/mod.rs\`
    - [ ] Create \`HumanFormatter\` in \`src/output/human.rs\`
    - [ ] Create \`JsonFormatter\` in \`src/output/json.rs\`
    - [ ] Integrate formatters into main pipeline
    - [ ] Handle \`--top\` limiting in main
    - [ ] Add \`assert_cmd\` and \`predicates\` dev dependencies
    - [ ] Create \`tests/cli_output_formats.rs\` with integration tests
    - [ ] Ensure all tests pass: \`cargo test\`
    - [ ] Verify CLI works: \`cargo run -- --help\`
    - [ ] Update documentation if needed
    
    ## Testing Strategy
    
    1. **Unit tests**: Test formatters individually with mock \`FoundString\` data
    2. **Integration tests**: Test CLI argument parsing and output format selection
    3. **Manual testing**: Verify output appearance and usability
    
    ## Dependencies
    
    - **Blocked by**: Basic CLI Structure ✅ (Complete - basic clap structure exists)
    - **Blocks**: Future output features (YARA format, filtering options)
    
    ## Related Files
    
    - \`src/main.rs\`: CLI argument parsing
    - \`src/output/mod.rs\`: Output module (currently empty)
    - \`src/types.rs\`: Core types (\`FoundString\`, \`Encoding\`, \`Tag\`)
    - \`tests/fixtures/\`: Test binary files for integration tests
    
    ## References
    
    - Documentation: \`docs/src/output-formats.md\`
    - Documentation: \`docs/src/cli.md\`
    - Requirements: 7.5, 7.6, 7.7
    - Task ID: stringy-analyzer/output-format-cli-arguments"
    

    Length of output: 61


    🏁 Script executed:

    #!/bin/bash
    
    # Add priority label
    gh issue edit 31 --repo EvilBit-Labs/StringyMcStringFace --add-label "priority:high"
    
    # Confirm the update
    echo "=== Updated Issue ==="
    gh issue view 31 --repo EvilBit-Labs/StringyMcStringFace
    

    Length of output: 8245


    @unclesp1d3r I've successfully enhanced issue #31 with comprehensive details! Here's what was updated:

    Changes Made

    ✅ Improved Title

    Changed from "Output Format CLI Arguments" to "CLI: Implement output format selection (--top, --json flags)" - more descriptive and specific.

    ✅ Enhanced Issue Body

    Added detailed sections including:

    1. Context - Current state of the CLI and rationale for the feature
    2. Requirements - Clear mapping to requirements 7.5, 7.6, and 7.7
    3. Proposed Solution - Complete implementation guidance with:
      • CLI argument structure using clap
      • OutputFormatter trait design
      • HumanFormatter and JsonFormatter implementations
      • Main pipeline integration code
      • Full integration test suite with 5 test cases
    4. Implementation Checklist - Step-by-step task breakdown
    5. Testing Strategy - Unit, integration, and manual testing approach
    6. Dependencies - Confirmed Basic CLI Structure is complete (blocking dependency resolved)
    7. Related Files - Quick reference to affected files
    8. References - Links to relevant documentation

    ✅ Added Metadata

    • Added priority:high label (appropriate for v0.1 milestone deliverable)
    • All existing labels preserved: area:analyzer, type:enhancement, status:backlog, lang:rust, needs:tests
    • Assignment and milestone maintained

    Key Technical Details in the Solution

    The proposed implementation:

    • Uses clap with ValueEnum for type-safe format selection
    • Implements conflict detection between --json and --format flags
    • Provides trait-based design for extensibility (future YARA formatter)
    • Includes comprehensive integration tests using assert_cmd and predicates
    • Leverages existing FoundString types from src/types.rs
    • Follows Rust best practices with proper error handling

    The issue is now ready for implementation with clear guidance on structure, testing, and acceptance criteria! 🚀

  8. unclesp1d3r commented on Jul 4, 2026

    @unclesp1d3r
    MemberAuthor

    Implemented: --top and --json output-selection flags in src/main.rs. Closing as completed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions