use anyhow::Result;
use clap::{ArgMatches, CommandFactory, FromArgMatches};
use crate::cli::{Cli, OutputFormat, ShellCompletion};
use crate::config::MultilineJoin;
use crate::config_file::{ConfigExpansionInfo, ConfigFile};
use crate::help;
use crate::platform::{ExitCode, SafeStderr};
use crate::tty;
pub fn validate_cli_args(cli: &Cli) -> Result<()> {
if cli.no_input && !cli.files.is_empty() {
return Err(anyhow::anyhow!(
"--no-input cannot be used with input files. Remove --no-input to read files, or remove file arguments to run script-only stages."
));
}
let mut stdin_count = 0;
for file_path in &cli.files {
if file_path == "-" {
stdin_count += 1;
if stdin_count > 1 {
return Err(anyhow::anyhow!("stdin (\"-\") can only be specified once"));
}
}
}
for exec_file in &cli.exec_files {
if !std::path::Path::new(exec_file).exists() {
return Err(anyhow::anyhow!("Exec file not found: {}", exec_file));
}
}
if let Some(batch_size) = cli.batch_size {
if batch_size == 0 {
return Err(anyhow::anyhow!("Batch size must be greater than 0"));
}
}
if cli.threads > 1000 {
return Err(anyhow::anyhow!("Thread count too high (max 1000)"));
}
if cli.span_close.is_some() && cli.span.is_none() && cli.span_idle.is_none() {
return Err(anyhow::anyhow!(
"--span-close requires --span or --span-idle. Use --span N for fixed-size spans or --span-idle 30s for inactivity-based spans."
));
}
if cli.multiline.is_none() && cli.multiline_join != MultilineJoin::Space {
return Err(anyhow::anyhow!(
"--multiline-join requires --multiline. Start with --multiline indent, --multiline blank, or see --help-multiline for regex/timestamp strategies."
));
}
if cli.core {
match cli.output_format {
OutputFormat::Csv => {
return Err(anyhow::anyhow!(
"csv output format does not support --core. Use --keys to define column order, e.g. --keys ts,level,msg."
));
}
OutputFormat::Tsv => {
return Err(anyhow::anyhow!(
"tsv output format does not support --core. Use --keys to define column order, e.g. --keys ts,level,msg."
));
}
OutputFormat::Csvnh => {
return Err(anyhow::anyhow!(
"csvnh output format does not support --core. Use --keys to define column order, e.g. --keys ts,level,msg."
));
}
OutputFormat::Tsvnh => {
return Err(anyhow::anyhow!(
"tsvnh output format does not support --core. Use --keys to define column order, e.g. --keys ts,level,msg."
));
}
_ => {
}
}
}
let implies_parallel = cli.parallel || cli.threads > 0 || cli.batch_size.is_some();
if (cli.discover_fields.is_some() || cli.discover_final_fields.is_some()) && implies_parallel {
return Err(anyhow::anyhow!(
"--discover and --discover-final are not supported with --parallel or thread overrides. Rerun without --parallel to use field discovery."
));
}
if cli.drain.is_some() && implies_parallel {
return Err(anyhow::anyhow!(
"--drain summary is not supported with --parallel or thread overrides. Rerun without --parallel to use Drain template mining."
));
}
if cli.drain.is_some() {
let effective_keys: Vec<String> = cli
.keys
.iter()
.filter(|key| !cli.exclude_keys.contains(key))
.cloned()
.collect();
if effective_keys.len() != 1 {
return Err(anyhow::anyhow!(
"--drain requires exactly one effective field in --keys after exclusions, e.g. --keys msg. Use -s to inspect available fields."
));
}
}
Ok(())
}
pub fn extract_config_file_arg(args: &[String]) -> Option<String> {
for i in 0..args.len() {
if args[i] == "--config-file" && i + 1 < args.len() {
return Some(args[i + 1].clone());
}
}
None
}
pub fn extract_save_alias_arg(args: &[String]) -> Option<String> {
for i in 0..args.len() {
if args[i] == "--save-alias" && i + 1 < args.len() {
return Some(args[i + 1].clone());
}
}
None
}
fn extract_completions_arg(args: &[String]) -> Option<ShellCompletion> {
for i in 0..args.len() {
if args[i] == "--completions" && i + 1 < args.len() {
return match args[i + 1].to_lowercase().as_str() {
"bash" => Some(ShellCompletion::Bash),
"zsh" => Some(ShellCompletion::Zsh),
"fish" => Some(ShellCompletion::Fish),
"powershell" => Some(ShellCompletion::PowerShell),
"elvish" => Some(ShellCompletion::Elvish),
_ => None,
};
}
if let Some(shell) = args[i].strip_prefix("--completions=") {
return match shell.to_lowercase().as_str() {
"bash" => Some(ShellCompletion::Bash),
"zsh" => Some(ShellCompletion::Zsh),
"fish" => Some(ShellCompletion::Fish),
"powershell" => Some(ShellCompletion::PowerShell),
"elvish" => Some(ShellCompletion::Elvish),
_ => None,
};
}
}
None
}
fn generate_completions(shell: ShellCompletion) {
use clap_complete::{generate, Shell};
let mut cmd = Cli::command();
let name = cmd.get_name().to_string();
let shell = match shell {
ShellCompletion::Bash => Shell::Bash,
ShellCompletion::Zsh => Shell::Zsh,
ShellCompletion::Fish => Shell::Fish,
ShellCompletion::PowerShell => Shell::PowerShell,
ShellCompletion::Elvish => Shell::Elvish,
};
generate(shell, &mut cmd, name, &mut std::io::stdout());
}
pub fn should_resolve_alias_references(args: &[String], alias_name: &str) -> bool {
let mut i = 0;
while i < args.len() {
if (args[i] == "-a" || args[i] == "--alias") && i + 1 < args.len() {
if args[i + 1] == alias_name {
return true;
}
i += 2;
} else {
i += 1;
}
}
false
}
pub fn handle_save_alias(raw_args: &[String], alias_name: &str, use_emoji: bool) {
let mut config_file_path: Option<String> = None;
let mut command_args = Vec::new();
let mut i = 0;
while i < raw_args.len() {
if raw_args[i] == "--save-alias" {
i += 2;
} else if raw_args[i] == "--config-file" && i + 1 < raw_args.len() {
config_file_path = Some(raw_args[i + 1].clone());
i += 2;
} else {
command_args.push(raw_args[i].clone());
i += 1;
}
}
if !command_args.is_empty() {
command_args.remove(0);
}
if command_args.is_empty() {
let prefix = if use_emoji { "⚠️" } else { "kelora:" };
eprintln!("{} No command to save as alias '{}'", prefix, alias_name);
std::process::exit(2);
}
let should_resolve = should_resolve_alias_references(&command_args, alias_name);
let alias_value = if command_args
.iter()
.any(|arg| arg == "-a" || arg == "--alias")
{
let config_result = match config_file_path.as_ref() {
Some(path) => ConfigFile::load_with_custom_path(Some(path)),
None => ConfigFile::load_with_custom_path(None),
};
match config_result {
Ok((config, _path)) => {
if should_resolve {
match config.resolve_args_only(&command_args) {
Ok(resolved_args) => {
if resolved_args.is_empty() {
let prefix = if use_emoji { "⚠️" } else { "kelora:" };
eprintln!(
"{} Resolved command is empty for alias '{}'",
prefix, alias_name
);
std::process::exit(2);
}
shell_words::join(resolved_args)
}
Err(e) => {
let prefix = if use_emoji { "⚠️" } else { "kelora:" };
eprintln!("{} Failed to resolve aliases in command: {}", prefix, e);
std::process::exit(1);
}
}
} else {
if let Err(e) = config.validate_alias_references(&command_args) {
let prefix = if use_emoji { "⚠️" } else { "kelora:" };
eprintln!("{} {}", prefix, e);
eprintln!(
"{} Cannot save alias '{}' with reference to non-existent alias",
prefix, alias_name
);
std::process::exit(1);
}
shell_words::join(command_args)
}
}
Err(_) if should_resolve => {
let prefix = if use_emoji { "⚠️" } else { "kelora:" };
eprintln!(
"{} Cannot update alias '{}' - no config file found",
prefix, alias_name
);
eprintln!(
"{} To create a new alias, use a command without referencing itself",
prefix
);
std::process::exit(1);
}
Err(_) => {
let prefix = if use_emoji { "⚠️" } else { "kelora:" };
eprintln!(
"{} Cannot save alias '{}' with alias references - no config file found",
prefix, alias_name
);
eprintln!(
"{} Create the referenced aliases first, or use a command without alias references",
prefix
);
std::process::exit(1);
}
}
} else {
shell_words::join(command_args)
};
let target_path = config_file_path.as_ref().map(std::path::Path::new);
match ConfigFile::save_alias(alias_name, &alias_value, target_path) {
Ok((config_path, previous_value)) => {
let success_prefix = if use_emoji { "🔹" } else { "kelora:" };
println!(
"{} Alias '{}' saved to {}",
success_prefix,
alias_name,
config_path.display()
);
if let Some(prev) = previous_value {
let info_prefix = if use_emoji { "🔹" } else { "kelora:" };
println!("{} Replaced previous alias:", info_prefix);
println!(" {} = {}", alias_name, prev);
}
}
Err(e) => {
let error_prefix = if use_emoji { "⚠️" } else { "kelora:" };
eprintln!(
"{} Failed to save alias '{}': {}",
error_prefix, alias_name, e
);
std::process::exit(1);
}
}
}
pub fn process_args_with_config(stderr: &mut SafeStderr) -> (ArgMatches, Cli, ConfigExpansionInfo) {
let raw_args: Vec<String> = std::env::args().collect();
let config_file_path = extract_config_file_arg(&raw_args);
let has_show_config = raw_args.iter().any(|arg| arg == "--show-config");
let has_edit_config = raw_args.iter().any(|arg| arg == "--edit-config");
let has_ignore_config = raw_args.iter().any(|arg| arg == "--ignore-config");
if has_show_config && has_edit_config {
stderr
.writeln("kelora: Error: --show-config and --edit-config are mutually exclusive")
.unwrap_or(());
ExitCode::InvalidUsage.exit();
}
if has_ignore_config && has_edit_config {
stderr
.writeln("kelora: Error: --ignore-config and --edit-config are mutually exclusive")
.unwrap_or(());
ExitCode::InvalidUsage.exit();
}
if has_show_config {
ConfigFile::show_config();
std::process::exit(0);
}
if has_edit_config {
ConfigFile::edit_config(config_file_path.as_deref());
std::process::exit(0);
}
if raw_args.iter().any(|arg| arg == "--help-time") {
help::print_time_format_help();
std::process::exit(0);
}
if raw_args.iter().any(|arg| arg == "--help-functions") {
help::print_functions_help();
std::process::exit(0);
}
if raw_args.iter().any(|arg| arg == "-h") {
help::print_quick_help();
std::process::exit(0);
}
if raw_args.iter().any(|arg| arg == "--help-examples") {
help::print_examples_help();
std::process::exit(0);
}
if raw_args.iter().any(|arg| arg == "--help-rhai") {
help::print_rhai_help();
std::process::exit(0);
}
if raw_args.iter().any(|arg| arg == "--help-multiline") {
help::print_multiline_help();
std::process::exit(0);
}
if raw_args.iter().any(|arg| arg == "--help-regex") {
help::print_regex_help();
std::process::exit(0);
}
if raw_args.iter().any(|arg| arg == "--help-formats") {
help::print_formats_help();
std::process::exit(0);
}
if let Some(shell) = extract_completions_arg(&raw_args) {
generate_completions(shell);
std::process::exit(0);
}
if let Some(alias_name) = extract_save_alias_arg(&raw_args) {
let use_emoji = tty::should_use_emoji_for_stderr();
handle_save_alias(&raw_args, &alias_name, use_emoji);
std::process::exit(0);
}
let ignore_config = has_ignore_config;
let disable_auto_config = std::env::var_os("KELORA_IGNORE_CONFIG").is_some();
let (processed_args, expansion_info) = if ignore_config {
(raw_args, ConfigExpansionInfo::default())
} else if let Some(path) = config_file_path.as_deref() {
match ConfigFile::load_with_custom_path(Some(path)) {
Ok((config_file, loaded_path)) => match config_file.process_args(raw_args) {
Ok((processed, mut info)) => {
info.loaded_config_path = loaded_path;
info.explicit_config_path = true;
(processed, info)
}
Err(e) => {
stderr
.writeln(&format!("kelora: Config error: {}", e))
.unwrap_or(());
std::process::exit(1);
}
},
Err(e) => {
stderr
.writeln(&format!("kelora: Config file error: {}", e))
.unwrap_or(());
std::process::exit(1);
}
}
} else if disable_auto_config {
(raw_args, ConfigExpansionInfo::default())
} else {
match ConfigFile::load_with_custom_path(config_file_path.as_deref()) {
Ok((config_file, loaded_path)) => match config_file.process_args(raw_args) {
Ok((processed, mut info)) => {
info.loaded_config_path = loaded_path;
info.explicit_config_path = config_file_path.is_some();
(processed, info)
}
Err(e) => {
stderr
.writeln(&format!("kelora: Config error: {}", e))
.unwrap_or(());
std::process::exit(1);
}
},
Err(e) => {
stderr
.writeln(&format!("kelora: Config file error: {}", e))
.unwrap_or(());
std::process::exit(1);
}
}
};
let matches = Cli::command().get_matches_from(processed_args);
let mut cli = Cli::from_arg_matches(&matches).unwrap_or_else(|e| {
stderr
.writeln(&format!("kelora: Error: {}", e))
.unwrap_or(());
std::process::exit(1);
});
cli.resolve_boolean_flags();
if crate::tty::is_stdin_tty() && cli.files.is_empty() && !cli.no_input {
let raw_args: Vec<String> = std::env::args().collect();
if raw_args.len() == 1 {
if let Err(e) = crate::interactive::run_interactive_mode() {
eprintln!("Interactive mode error: {}", e);
std::process::exit(1);
}
std::process::exit(0);
}
eprintln!("error: no input files or stdin provided");
eprintln!();
eprintln!("{}", Cli::command().render_usage());
eprintln!();
eprintln!(
"Pass one or more files, pipe input on stdin, or run plain 'kelora' to enter interactive mode."
);
eprintln!("For a quick reference, try '-h'.");
std::process::exit(2);
}
(matches, cli, expansion_info)
}