Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 61 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ mod config;
mod output;
mod shell;

use std::process::ExitCode;
use std::{ffi::OsStr, process::ExitCode};

use clap::{Parser, Subcommand};
use clap::{Parser, Subcommand, error::ErrorKind};
use output::OutputFormat;

#[derive(Parser)]
Expand Down Expand Up @@ -68,7 +68,31 @@ enum Commands {

#[tokio::main]
async fn main() -> ExitCode {
let cli = Cli::parse();
let args: Vec<_> = std::env::args_os().collect();
let requested_output = requested_output_format(&args[1..]);
let cli = match Cli::try_parse_from(args) {
Ok(cli) => cli,
Err(error) => {
if matches!(
error.kind(),
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
) {
error.exit();
}

if matches!(requested_output, OutputFormat::Json) {
println!(
"{}",
serde_json::json!({
"error": error.to_string(),
})
);
return ExitCode::from(u8::try_from(error.exit_code()).unwrap_or(1));
}

error.exit();
}
};
let output = cli.output;

if let Err(e) = run(cli).await {
Expand All @@ -79,6 +103,40 @@ async fn main() -> ExitCode {
ExitCode::SUCCESS
}

fn requested_output_format(args: &[impl AsRef<OsStr>]) -> OutputFormat {
let mut requested = OutputFormat::Table;
let mut args = args.iter();

while let Some(arg) = args.next() {
let arg = arg.as_ref();
if arg == "--" {
break;
}

if arg == "--output" || arg == "-o" {
if args.next().is_some_and(|value| value.as_ref() == "json") {
requested = OutputFormat::Json;
}
continue;
}

let Some(arg) = arg.to_str() else {
continue;
};
if arg
.strip_prefix("--output=")
.is_some_and(|value| value == "json")
|| arg
.strip_prefix("-o")
.is_some_and(|value| !value.is_empty() && value == "json")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Equals short output flag ignored

Low Severity

requested_output_format recognizes -ojson and --output=json but not clap's valid short equals form -o=json. Parse failures using that flag still go through clap's plain-text stderr path instead of the JSON error object.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9702ab7. Configure here.

{
requested = OutputFormat::Json;
}
}

requested
}

#[allow(clippy::too_many_lines)]
pub(crate) async fn run(cli: Cli) -> anyhow::Result<()> {
// Lazy-init so we only pay for the client we actually use.
Expand Down
56 changes: 56 additions & 0 deletions tests/cli_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,67 @@ fn no_args_shows_usage() {
.stderr(predicate::str::contains("Usage"));
}

fn assert_json_parse_error(args: &[&str]) {
let output = polymarket().args(args).output().unwrap();

assert_eq!(output.status.code(), Some(2));
assert!(
output.stderr.is_empty(),
"stderr should be empty in JSON mode: {}",
String::from_utf8_lossy(&output.stderr)
);

let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("stdout not valid JSON: {e}\nstdout: {stdout}"));
assert!(
parsed
.get("error")
.is_some_and(serde_json::Value::is_string),
"missing string 'error' key: {parsed}"
);
}

#[test]
fn unknown_command_fails() {
polymarket().arg("nonexistent").assert().failure();
}

#[test]
fn json_mode_formats_unknown_command_parse_error() {
assert_json_parse_error(&["--output", "json", "nonexistent"]);
}

#[test]
fn json_mode_is_detected_after_invalid_command() {
assert_json_parse_error(&["nonexistent", "--output=json"]);
}

#[test]
fn json_short_form_formats_validation_error() {
assert_json_parse_error(&["markets", "get", "-ojson"]);
}

#[test]
fn table_mode_parse_error_keeps_clap_output() {
polymarket()
.arg("nonexistent")
.assert()
.code(2)
.stdout(predicate::str::is_empty())
.stderr(predicate::str::starts_with("error:").and(predicate::str::contains("Usage:")));
}

#[test]
fn json_flag_does_not_change_help_semantics() {
polymarket()
.args(["--output", "json", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Usage:"))
.stderr(predicate::str::is_empty());
}

#[test]
fn invalid_output_format_rejected() {
polymarket()
Expand Down