use std::ffi::OsString;
use crate::error::IskraError;
#[derive(Debug, Clone)]
pub struct IskraCli {
pub timeout: u64,
pub no_decompress: bool,
pub fail: bool,
pub show_headers: bool,
pub verbose_errors: bool,
pub verbose: bool,
pub quiet: bool,
pub subcommand: IskraSubcommand,
}
#[derive(Debug, Clone)]
pub enum IskraSubcommand {
Get {
url: String,
header: Vec<(String, String)>,
query: Vec<(String, String)>,
output: Option<String>,
range: Option<(u64, Option<u64>)>,
resume: bool,
},
Post {
url: String,
body: String,
header: Vec<(String, String)>,
query: Vec<(String, String)>,
output: Option<String>,
range: Option<(u64, Option<u64>)>,
resume: bool,
},
Put {
url: String,
body: String,
header: Vec<(String, String)>,
query: Vec<(String, String)>,
output: Option<String>,
range: Option<(u64, Option<u64>)>,
resume: bool,
},
Delete {
url: String,
header: Vec<(String, String)>,
query: Vec<(String, String)>,
output: Option<String>,
range: Option<(u64, Option<u64>)>,
resume: bool,
},
Custom {
method: String,
url: String,
header: Vec<(String, String)>,
query: Vec<(String, String)>,
output: Option<String>,
body: Option<String>,
trailing_body: Vec<String>,
range: Option<(u64, Option<u64>)>,
resume: bool,
},
Burst {
input: String,
output_dir: Option<String>,
concurrency: usize,
throttle: Option<usize>,
retries: usize,
backoff: u64,
summary: bool,
},
}
pub fn parse_iskra_cli_args<I, T>(args: I) -> Result<IskraCli, IskraError>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let mut timeout = 30u64;
let mut no_decompress = false;
let mut fail = false;
let mut show_headers = false;
let mut verbose_errors = false;
let mut verbose = false;
let mut quiet = false;
let mut args_vec: Vec<String> = args.into_iter().map(|a| a.into().to_string_lossy().to_string()).collect();
if !args_vec.is_empty() {
args_vec.remove(0);
}
if args_vec.is_empty() {
print_iskra_help();
std::process::exit(0);
}
let mut subcmd = None;
let mut subcmd_args = Vec::new();
let mut i = 0;
while i < args_vec.len() {
let arg = &args_vec[i];
match arg.as_str() {
"get" | "post" | "put" | "delete" | "custom" | "burst" => {
subcmd = Some(arg.clone());
subcmd_args = args_vec[i + 1..].to_vec();
break;
}
s if s.starts_with("http://") || s.starts_with("https://") || s.starts_with("file://") => {
subcmd = Some("get".to_string());
subcmd_args = args_vec[i..].to_vec();
break;
}
_ => {}
}
i += 1;
}
let subcmd = subcmd.ok_or_else(|| IskraError::Config { msg: "Missing subcommand or URL. See --help.".to_string() })?;
let subcommand = match subcmd.as_str() {
"get" => {
let mut url = None;
let header = parse_kv_pairs(&subcmd_args, "--header");
let query = parse_query_pairs(&subcmd_args, "--query");
let mut output = None;
let mut range = None;
let mut resume = false;
let mut i = 0;
while i < subcmd_args.len() {
match subcmd_args[i].as_str() {
s if s.starts_with("http://") || s.starts_with("https://") || s.starts_with("file://") => url = Some(s.to_string()),
"--header" | "--query" => i += 1, "--output" => {
if let Some(val) = subcmd_args.get(i + 1) {
output = Some(val.clone());
i += 1;
}
}
"--range" => {
if let Some(r) = subcmd_args.get(i + 1) {
let parts: Vec<&str> = r.split('-').collect();
let start = parts.get(0).and_then(|s| s.trim().parse::<u64>().ok());
let end = if let Some(e) = parts.get(1) { if !e.trim().is_empty() { e.trim().parse::<u64>().ok() } else { None } } else { None };
if let Some(start) = start {
range = Some((start, end));
}
i += 1;
}
}
"--resume" => resume = true,
_ => {}
}
i += 1;
}
IskraSubcommand::Get {
url: url.ok_or_else(|| IskraError::Config { msg: "Missing URL for get command. Usage: iskra get <url> [options]".to_string() })?,
header,
query,
output,
range,
resume,
}
}
"post" => {
let mut url = None;
let mut body = None;
let header = parse_kv_pairs(&subcmd_args, "--header");
let query = parse_query_pairs(&subcmd_args, "--query");
let mut output = None;
let mut range = None;
let mut resume = false;
let mut i = 0;
while i < subcmd_args.len() {
match subcmd_args[i].as_str() {
s if s.starts_with("http://") || s.starts_with("https://") || s.starts_with("file://") => url = Some(s.to_string()),
s if url.is_some() && body.is_none() && !s.starts_with('-') => body = Some(s.to_string()),
"--header" | "--query" => i += 1,
"--output" => {
if let Some(val) = subcmd_args.get(i + 1) {
output = Some(val.clone());
i += 1;
}
}
"--range" => {
if let Some(r) = subcmd_args.get(i + 1) {
let parts: Vec<&str> = r.split('-').collect();
let start = parts.get(0).and_then(|s| s.trim().parse::<u64>().ok());
let end = if let Some(e) = parts.get(1) { if !e.trim().is_empty() { e.trim().parse::<u64>().ok() } else { None } } else { None };
if let Some(start) = start {
range = Some((start, end));
}
i += 1;
}
}
"--resume" => resume = true,
_ => {}
}
i += 1;
}
IskraSubcommand::Post {
url: url.ok_or_else(|| IskraError::Config { msg: "Missing URL for post command. Usage: iskra post <url> <body> [options]".to_string() })?,
body: body.ok_or_else(|| IskraError::Config { msg: "Missing body for post command. Usage: iskra post <url> <body> [options]".to_string() })?,
header,
query,
output,
range,
resume,
}
}
"put" => {
let mut url = None;
let mut body = None;
let header = parse_kv_pairs(&subcmd_args, "--header");
let query = parse_query_pairs(&subcmd_args, "--query");
let mut output = None;
let mut range = None;
let mut resume = false;
let mut i = 0;
while i < subcmd_args.len() {
match subcmd_args[i].as_str() {
s if s.starts_with("http://") || s.starts_with("https://") || s.starts_with("file://") => url = Some(s.to_string()),
s if url.is_some() && body.is_none() && !s.starts_with('-') => body = Some(s.to_string()),
"--header" | "--query" => i += 1,
"--output" => {
if let Some(val) = subcmd_args.get(i + 1) {
output = Some(val.clone());
i += 1;
}
}
"--range" => {
if let Some(r) = subcmd_args.get(i + 1) {
let parts: Vec<&str> = r.split('-').collect();
let start = parts.get(0).and_then(|s| s.trim().parse::<u64>().ok());
let end = if let Some(e) = parts.get(1) { if !e.trim().is_empty() { e.trim().parse::<u64>().ok() } else { None } } else { None };
if let Some(start) = start {
range = Some((start, end));
}
i += 1;
}
}
"--resume" => resume = true,
_ => {}
}
i += 1;
}
IskraSubcommand::Put {
url: url.ok_or_else(|| IskraError::Config { msg: "Missing URL for put command. Usage: iskra put <url> <body> [options]".to_string() })?,
body: body.ok_or_else(|| IskraError::Config { msg: "Missing body for put command. Usage: iskra put <url> <body> [options]".to_string() })?,
header,
query,
output,
range,
resume,
}
}
"delete" => {
let mut url = None;
let header = parse_kv_pairs(&subcmd_args, "--header");
let query = parse_query_pairs(&subcmd_args, "--query");
let mut output = None;
let mut range = None;
let mut resume = false;
let mut i = 0;
while i < subcmd_args.len() {
match subcmd_args[i].as_str() {
s if s.starts_with("http://") || s.starts_with("https://") || s.starts_with("file://") => url = Some(s.to_string()),
"--header" => i += 1,
"--query" => i += 1,
"--output" => {
if i + 1 < subcmd_args.len() {
output = Some(subcmd_args[i + 1].clone());
i += 1;
}
}
"--range" => {
if i + 1 < subcmd_args.len() {
let r = &subcmd_args[i + 1];
let parts: Vec<&str> = r.split('-').collect();
if !parts.is_empty() {
let start = parts[0].trim().parse::<u64>().ok();
let end = if parts.len() > 1 && !parts[1].trim().is_empty() {
parts[1].trim().parse::<u64>().ok()
} else {
None
};
if let Some(start) = start {
range = Some((start, end));
}
}
i += 1;
}
}
"--resume" => resume = true,
_ => {}
}
i += 1;
}
IskraSubcommand::Delete {
url: url.ok_or_else(|| IskraError::Config { msg: "Missing URL for delete command".to_string() })?,
header,
query,
output,
range,
resume,
}
}
"custom" => {
let mut method = None;
let mut url = None;
let header = parse_kv_pairs(&subcmd_args, "--header");
let query = parse_query_pairs(&subcmd_args, "--query");
let mut output = None;
let mut range = None;
let mut resume = false;
let mut body = None;
let mut trailing_body = Vec::new();
let mut i = 0;
while i < subcmd_args.len() {
match subcmd_args[i].as_str() {
s if method.is_none() => { method = Some(s.to_string()); },
s if url.is_none() && method.is_some() && (s.starts_with("http://") || s.starts_with("https://") || s.starts_with("file://")) => { url = Some(s.to_string()); },
"--header" | "--query" => i += 1,
"--output" => {
if let Some(val) = subcmd_args.get(i + 1) {
output = Some(val.clone());
i += 1;
}
}
"--range" => {
if let Some(r) = subcmd_args.get(i + 1) {
let parts: Vec<&str> = r.split('-').collect();
let start = parts.get(0).and_then(|s| s.trim().parse::<u64>().ok());
let end = if let Some(e) = parts.get(1) { if !e.trim().is_empty() { e.trim().parse::<u64>().ok() } else { None } } else { None };
if let Some(start) = start {
range = Some((start, end));
}
i += 1;
}
}
"--resume" => resume = true,
"--body" => {
if let Some(val) = subcmd_args.get(i + 1) {
body = Some(val.clone());
i += 1;
}
}
s if method.is_some() && url.is_some() && !s.starts_with('-') => {
trailing_body.push(s.to_string());
}
_ => {}
}
i += 1;
}
IskraSubcommand::Custom {
method: method.ok_or_else(|| IskraError::Config { msg: "Missing method for custom command. Usage: iskra custom <METHOD> <url> [options] [body]".to_string() })?,
url: url.ok_or_else(|| IskraError::Config { msg: "Missing URL for custom command. Usage: iskra custom <METHOD> <url> [options] [body]".to_string() })?,
header,
query,
output,
body,
trailing_body,
range,
resume,
}
}
"burst" => {
use std::path::Path;
if subcmd_args.iter().any(|a| a == "--help" || a == "-h") {
print_iskra_help();
std::process::exit(0);
}
let mut input = None;
let mut output_dir = None;
let mut concurrency = 4;
let mut throttle = None;
let mut retries = 0;
let mut backoff = 0u64;
let mut summary = false;
let mut i = 0;
while i < subcmd_args.len() {
match subcmd_args[i].as_str() {
"-i" | "--input" => {
if i + 1 < subcmd_args.len() {
input = Some(subcmd_args[i + 1].clone());
i += 1;
} else {
return Err(IskraError::Config { msg: "Missing value for --input (-i) in burst command. Usage: iskra burst -i <file> [options]".to_string() });
}
}
"-o" | "--output-dir" => {
if i + 1 < subcmd_args.len() {
output_dir = Some(subcmd_args[i + 1].clone());
i += 1;
} else {
return Err(IskraError::Config { msg: "Missing value for --output-dir (-o) in burst command.".to_string() });
}
}
"-c" | "--concurrency" => {
if i + 1 < subcmd_args.len() {
concurrency = subcmd_args[i + 1].parse().map_err(|_| IskraError::Config { msg: "Invalid value for --concurrency (must be a positive integer)".to_string() })?;
if concurrency == 0 {
return Err(IskraError::Config { msg: "Concurrency must be greater than 0 for burst command.".to_string() });
}
i += 1;
} else {
return Err(IskraError::Config { msg: "Missing value for --concurrency (-c) in burst command.".to_string() });
}
}
"--throttle" => {
if i + 1 < subcmd_args.len() {
throttle = Some(subcmd_args[i + 1].parse().map_err(|_| IskraError::Config { msg: "Invalid value for --throttle (must be a positive integer)".to_string() })?);
i += 1;
} else {
return Err(IskraError::Config { msg: "Missing value for --throttle in burst command.".to_string() });
}
}
"--retries" => {
if i + 1 < subcmd_args.len() {
retries = subcmd_args[i + 1].parse().map_err(|_| IskraError::Config { msg: "Invalid value for --retries (must be a non-negative integer)".to_string() })?;
i += 1;
} else {
return Err(IskraError::Config { msg: "Missing value for --retries in burst command.".to_string() });
}
}
"--backoff" => {
if i + 1 < subcmd_args.len() {
backoff = subcmd_args[i + 1].parse().map_err(|_| IskraError::Config { msg: "Invalid value for --backoff (must be a non-negative integer)".to_string() })?;
i += 1;
} else {
return Err(IskraError::Config { msg: "Missing value for --backoff in burst command.".to_string() });
}
}
"--summary" => summary = true,
_ => {}
}
i += 1;
}
let input_file = input.ok_or_else(|| IskraError::Config { msg: "Missing input for burst command. Usage: iskra burst -i <file> [options]".to_string() })?;
if !Path::new(&input_file).exists() {
return Err(IskraError::Config { msg: format!("Input file '{}' does not exist for burst command. Please provide a valid file.", input_file) });
}
IskraSubcommand::Burst {
input: input_file,
output_dir,
concurrency,
throttle,
retries,
backoff,
summary,
}
}
_ => return Err(IskraError::Config { msg: format!("Unsupported subcommand: {}", subcmd) }),
};
Ok(IskraCli {
timeout,
no_decompress,
fail,
show_headers,
verbose_errors,
verbose,
quiet,
subcommand,
})
}
pub fn print_iskra_help() {
println!(r#"Iskra: Modern, secure, and flexible CLI for HTTP(S) data transfer.
USAGE:
iskra [GLOBAL OPTIONS] <COMMAND> [ARGS]
COMMANDS:
get HTTP GET request
post HTTP POST request
put HTTP PUT request
delete HTTP DELETE request
custom HTTP request with arbitrary method (e.g. PATCH, OPTIONS, HEAD, TRACE, CONNECT)
burst High-performance parallel/batch HTTP requests
GLOBAL OPTIONS (can be placed anywhere):
--timeout <seconds> Timeout in seconds (default: 30)
--no-decompress Disable automatic response decompression (gzip, deflate, brotli)
--fail Exit with non-zero code if response is not 2xx
--show-headers Always print response headers in output
--verbose-errors Show detailed error reports (same as ISKRA_ERROR_VERBOSE=1)
--verbose, -v Show verbose output (per-request details, progress, etc.)
--quiet, -q Suppress all output except errors (quiet mode)
BURST OPTIONS:
-i, --input <file> Input file with batch requests (required)
-o, --output-dir <dir> Output directory for results (optional)
-c, --concurrency <n> Number of concurrent requests (default: 4, must be > 0)
--throttle <n> Throttle requests per second (optional)
--retries <n> Number of retries per request (default: 0)
--backoff <ms> Backoff in milliseconds between retries (default: 0)
--summary Print summary after completion
EXAMPLES:
iskra get https://example.com --timeout 10
iskra --timeout 10 get https://example.com
iskra post https://httpbin.org/post 'hello world' --fail --no-decompress
iskra custom PATCH https://httpbin.org/patch --timeout 5 --verbose-errors
iskra delete https://example.com/resource --fail
iskra burst -i batch.txt --concurrency 8 --summary
iskra https://example.com (GET shortcut)
TIPS:
- All global options can be placed before or after the subcommand.
- For burst, input file must exist and concurrency must be > 0.
- Use --fail to ensure non-2xx responses cause errors.
- Use --show-headers and --verbose-errors for debugging.
For more details, see: https://github.com/ParkBlake/iskra
"#);
}
fn parse_kv_pairs(args: &[String], flag: &str) -> Vec<(String, String)> {
let mut out = Vec::new();
let mut i = 0;
while i < args.len() {
if args[i] == flag && i + 1 < args.len() {
if let Some((k, v)) = args[i + 1].split_once('=') {
out.push((k.trim().to_string(), v.trim().to_string()));
}
i += 2;
} else {
i += 1;
}
}
out
}
fn parse_query_pairs(args: &[String], flag: &str) -> Vec<(String, String)> {
parse_kv_pairs(args, flag)
}