pub fn stderr_color_enabled(mode: ColorMode) -> bool {
match mode {
ColorMode::Never => false,
ColorMode::Always => true,
ColorMode::Auto => std::io::IsTerminal::is_terminal(&std::io::stderr()),
}
}
pub fn warn_stderr(_mode: ColorMode, message: impl AsRef<str>) {
tracing::warn!(
target: "atomwrite::warning",
warning = %message.as_ref(),
"command completed with a warning"
);
}
fn extract_clap_tip(msg: &str) -> Option<String> {
for line in msg.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("tip:") {
return Some(rest.trim().to_string());
}
}
None
}
pub fn enrich_clap_suggestion(msg: &str) -> Option<String> {
let clap_tip = extract_clap_tip(msg);
let msg_lower = msg.to_ascii_lowercase();
let mentions_edit_args = msg_lower.contains("--old")
|| msg_lower.contains("--new")
|| msg_lower.contains("--after-match")
|| msg_lower.contains("--before-match")
|| msg_lower.contains("--between");
let is_edit_subcommand =
msg.contains("Usage: atomwrite edit") || msg.contains("atomwrite edit ");
let hyphen_value_error = msg.contains("wasn't expected")
|| msg.contains("unexpected argument")
|| (msg.contains("tip:") && msg.contains("'--'"));
if mentions_edit_args || (hyphen_value_error && is_edit_subcommand) {
let base = "For content with special characters (hyphens, quotes, shell metacharacters), \
use --old-file <PATH> and --new-file <PATH> to read content from files \
instead of CLI arguments. This bypasses shell expansion and argument \
parsing entirely.";
return Some(match clap_tip {
Some(tip) => format!("{base} (original clap tip: {tip})"),
None => base.to_string(),
});
}
clap_tip
}
pub fn prescan_json_schema() -> Option<String> {
let args: Vec<String> = std::env::args_os()
.map(|a| a.to_string_lossy().into_owned())
.collect();
if !args.iter().any(|a| a == "--json-schema") {
return None;
}
for arg in &args[1..] {
if crate::SUBCOMMANDS.contains(&arg.as_str()) {
return Some(arg.clone());
}
}
None
}
pub fn prescan_print_schema(argv: &[String]) -> anyhow::Result<Option<ExitCode>> {
let name = match crate::plan_print_schema(argv) {
crate::PrintSchemaPlan::Defer => return Ok(None),
crate::PrintSchemaPlan::List => crate::SCHEMA_LIST_REQUEST.to_string(),
crate::PrintSchemaPlan::Print(name) => name,
};
let stdout = io::stdout();
let mut out = stdout.lock();
if crate::emit_schema_by_name(&name, &mut out)? {
return Ok(Some(ExitCode::from(0)));
}
let err = crate::error::AtomwriteError::InvalidInput {
reason: format!(
"unknown schema name {name:?}; run `--print-schema` with no name to list every \
schema this build publishes"
),
};
let ctx = crate::error::ErrorContext::default();
let shape = crate::output::OutputShape::default();
let written = crate::output::write_error_json_shaped(&mut out, &err, None, &ctx, &shape)
.and_then(|()| out.flush().map_err(anyhow::Error::from));
Ok(Some(ExitCode::from(
crate::best_effort::settle_error_report(written, err.exit_code()),
)))
}
pub fn find_io_error(err: &anyhow::Error) -> Option<std::io::ErrorKind> {
for cause in err.chain() {
if let Some(io_err) = cause.downcast_ref::<std::io::Error>() {
return Some(io_err.kind());
}
}
None
}
pub fn io_to_atomwrite_error(
kind: std::io::ErrorKind,
err: &anyhow::Error,
) -> crate::error::AtomwriteError {
let msg = format!("{err:#}");
match kind {
std::io::ErrorKind::PermissionDenied => crate::error::AtomwriteError::PermissionDenied {
path: extract_path_from_message(&msg),
},
std::io::ErrorKind::NotFound => crate::error::AtomwriteError::NotFound {
path: extract_path_from_message(&msg),
},
_ => crate::error::AtomwriteError::Io {
source: std::io::Error::new(kind, msg),
},
}
}
fn extract_path_from_message(msg: &str) -> PathBuf {
if let Some(start) = msg.find('/') {
let rest = &msg[start..];
let end = rest.find(':').unwrap_or(rest.len());
return PathBuf::from(&rest[..end]);
}
PathBuf::from("unknown")
}
pub fn handle_clap_parse_error(clap_err: clap::Error) -> ExitCode {
use clap::error::ErrorKind;
match clap_err.kind() {
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => {
let written = clap_err.print().map_err(anyhow::Error::from);
ExitCode::from(crate::best_effort::settle_error_report(
written,
clap_err.exit_code() as u8,
))
}
_ => {
let msg = clap_err.to_string();
let suggestion = enrich_clap_suggestion(&msg);
let ej = crate::error::ErrorJson {
error: true,
ok: false,
code: "ARGUMENT_PARSE_ERROR",
exit: 2,
message: msg,
path: None,
error_class: crate::error::ErrorClass::Permanent.as_str(),
retryable: false,
suggestion,
workspace: None,
failed_pair_index: None,
pairs_total: None,
pair_results: None,
best_candidate: None,
candidates: None,
match_count: None,
similar_paths: None,
conflict_count: None,
missing_flag: None,
pairs_unmatched: None,
pairs_applied: None,
suggestion_lang: crate::locale::resolved_language().as_str(),
};
let mut out = io::stdout().lock();
let written = serde_json::to_writer(&mut out, &ej)
.map_err(io::Error::from)
.and_then(|()| out.write_all(b"\n"))
.and_then(|()| out.flush())
.map_err(anyhow::Error::from);
ExitCode::from(crate::best_effort::settle_error_report(written, 2))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_log_level_matrix() {
assert_eq!(cli_log_level(0, 0), "warn");
assert_eq!(cli_log_level(1, 0), "info");
assert_eq!(cli_log_level(2, 0), "debug");
assert_eq!(cli_log_level(3, 0), "trace");
assert_eq!(cli_log_level(0, 1), "error");
assert_eq!(cli_log_level(0, 2), "off");
assert_eq!(cli_log_level(1, 1), "info");
}
#[test]
fn resolve_log_format_matrix() {
assert_eq!(resolve_log_format(false), LogFormat::Compact);
assert_eq!(resolve_log_format(true), LogFormat::Json);
}
#[test]
fn env_filter_parses_target_directive() {
let f = EnvFilter::builder()
.parse("warn,atomwrite=info")
.expect("parse directive");
let s = f.to_string();
assert!(
s.contains("warn") || s.contains("info") || !s.is_empty(),
"unexpected filter display: {s}"
);
}
}