mod interactive;
mod output;
use crate::cmd::completions::ShellType;
use anyhow::{Context, Result};
use clap::Parser;
use rustledger_booking::merge_with_padding_spanned;
use rustledger_core::DisplayContext;
use rustledger_loader::LoadOptions;
use std::fs;
use std::io;
use std::path::PathBuf;
const SYSTEM_TABLES: &[&str] = &[
"#accounts",
"#balances",
"#commodities",
"#documents",
"#entries",
"#events",
"#notes",
"#postings",
"#prices",
"#transactions",
];
#[derive(Parser, Debug)]
#[command(name = "query")]
#[command(author, version, about, long_about = None)]
pub struct Args {
#[arg(value_name = "FILE")]
pub file: Option<PathBuf>,
#[arg(long, value_name = "SHELL", hide = true)]
pub generate_completions: Option<ShellType>,
#[arg(value_name = "QUERY", trailing_var_arg = true, num_args = 0..)]
pub query: Vec<String>,
#[arg(short = 'F', long = "query-file", value_name = "QUERY_FILE")]
pub query_file: Option<PathBuf>,
#[arg(short = 'o', long, value_name = "OUTPUT_FILE")]
pub output: Option<PathBuf>,
#[arg(short = 'f', long)]
pub format: Option<OutputFormat>,
#[arg(short = 'm', long)]
pub numberify: bool,
#[arg(short = 'q', long = "no-errors")]
pub no_errors: bool,
#[arg(short, long)]
pub verbose: bool,
#[arg(long = "no-cache")]
pub no_cache: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum OutputFormat {
Text,
Csv,
Json,
Beancount,
}
impl std::fmt::Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Text => write!(f, "text"),
Self::Csv => write!(f, "csv"),
Self::Json => write!(f, "json"),
Self::Beancount => write!(f, "beancount"),
}
}
}
pub fn run(args: &Args) -> Result<()> {
let mut stdout = io::stdout();
run_with_writer(args, &mut stdout)
}
pub fn run_with_writer<W: io::Write>(args: &Args, out: &mut W) -> Result<()> {
let Some(file) = args.file.as_ref() else {
anyhow::bail!("FILE is required");
};
if !file.exists() {
anyhow::bail!("file not found: {}", file.display());
}
let options = LoadOptions {
validate: false, ..Default::default()
};
let (raw, _from_cache) =
crate::cmd::loadcache::load_result_cached(file, args.no_cache, args.verbose)?;
let ledger = rustledger_loader::process(raw, &options)
.with_context(|| format!("failed to load {}", file.display()))?;
if !ledger.errors.is_empty() && !args.no_errors {
for err in &ledger.errors {
eprintln!("{}: {}", err.code, err.message);
}
eprintln!();
}
let directives = merge_with_padding_spanned(&ledger.directives);
let source_map = ledger.source_map;
let display_context = ledger.display_context;
if args.verbose {
eprintln!("Loaded {} directives", directives.len());
}
if let Some(flag) = args.query.iter().find(|t| {
t.starts_with("--")
|| (t.len() == 2 && t.starts_with('-') && t.as_bytes()[1].is_ascii_alphabetic())
}) {
anyhow::bail!(
"'{flag}' looks like a command-line flag but was parsed as part of the query.\n \
Flags must come before the query, e.g. `rledger query <file> {flag} … \"<query>\"`."
);
}
let query_str = if !args.query.is_empty() {
args.query.join(" ")
} else if let Some(ref query_file) = args.query_file {
fs::read_to_string(query_file)
.with_context(|| format!("failed to read query file {}", query_file.display()))?
} else {
return interactive::run_interactive(
file,
&directives,
&source_map,
&display_context,
args,
);
};
let settings = ShellSettings::from_args(args, display_context);
if let Some(ref output_path) = settings.output_file {
let mut file = fs::File::create(output_path)
.with_context(|| format!("failed to create output file {}", output_path.display()))?;
output::execute_query(&query_str, &directives, &source_map, &settings, &mut file)
} else {
output::execute_query(&query_str, &directives, &source_map, &settings, out)
}
}
struct ShellSettings {
format: OutputFormat,
numberify: bool,
pager: bool,
output_file: Option<PathBuf>,
display_context: DisplayContext,
}
impl ShellSettings {
fn from_args(args: &Args, display_context: DisplayContext) -> Self {
Self {
format: args.format.unwrap_or(OutputFormat::Text),
numberify: args.numberify,
pager: true,
output_file: args.output.clone(),
display_context,
}
}
}
impl OutputFormat {
#[must_use]
pub fn from_str_config(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"text" => Some(Self::Text),
"csv" => Some(Self::Csv),
"json" => Some(Self::Json),
"beancount" => Some(Self::Beancount),
_ => None,
}
}
}