mod interactive;
mod output;
use crate::cmd::completions::ShellType;
use anyhow::{Context, Result};
use clap::Parser;
use rustledger_booking::expand_pads;
use rustledger_core::DisplayContext;
use rustledger_loader::{LoadOptions, load};
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..)]
query: Vec<String>,
#[arg(short = 'F', long = "query-file", value_name = "QUERY_FILE")]
query_file: Option<PathBuf>,
#[arg(short = 'o', long, value_name = "OUTPUT_FILE")]
output: Option<PathBuf>,
#[arg(short = 'f', long)]
pub format: Option<OutputFormat>,
#[arg(short = 'm', long)]
numberify: bool,
#[arg(short = 'q', long = "no-errors")]
no_errors: bool,
#[arg(short, long)]
verbose: 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 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 ledger =
load(file, &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 booked_directives: Vec<_> = ledger.directives.into_iter().map(|s| s.value).collect();
let directives = expand_pads(&booked_directives);
let display_context = ledger.display_context;
if args.verbose {
eprintln!("Loaded {} directives", directives.len());
}
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, &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, &settings, &mut file)
} else {
output::execute_query(&query_str, &directives, &settings, &mut io::stdout())
}
}
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,
}
}
}