use std::fs;
use std::io::Read;
use std::path::PathBuf;
use std::process::ExitCode;
use chrono::NaiveDate;
use clap::{Parser, Subcommand};
use pactole_core::ReadableStorage;
use pactole_core::{RegisterFilter, TransactionStatus};
use pactole_storage_fs::PactoleFileStorage;
#[derive(Debug, Parser)]
#[command(
name = "pactole",
version,
about = "Tools around the Pactole ledger format"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
Parse {
file: PathBuf,
},
Fmt {
file: String,
#[arg(long)]
write: bool,
},
Check {
file: PathBuf,
},
Register {
file: PathBuf,
#[arg(short, long)]
account: Option<String>,
#[arg(long)]
from: Option<NaiveDate>,
#[arg(long)]
to: Option<NaiveDate>,
#[arg(long)]
payee: Option<String>,
#[arg(long)]
narration: Option<String>,
#[arg(long)]
tag: Option<String>,
#[arg(long, value_enum)]
status: Option<StatusArg>,
},
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
enum StatusArg {
Uncleared,
Pending,
Cleared,
}
impl From<StatusArg> for pactole_core::TransactionStatus {
fn from(value: StatusArg) -> Self {
match value {
StatusArg::Uncleared => pactole_core::TransactionStatus::Uncleared,
StatusArg::Pending => pactole_core::TransactionStatus::Pending,
StatusArg::Cleared => pactole_core::TransactionStatus::Cleared,
}
}
}
fn main() -> ExitCode {
let cli = Cli::parse();
match cli.command {
Command::Parse { file } => run_parse(file),
Command::Fmt { file, write } => run_fmt(file, write),
Command::Check { file } => run_check(file),
Command::Register {
file,
account,
from,
to,
payee,
narration,
tag,
status,
} => run_register(file, account, from, to, payee, narration, tag, status),
}
}
fn run_parse(file: PathBuf) -> ExitCode {
let mut storage = match PactoleFileStorage::try_from(file) {
Ok(storage) => storage,
Err(err) => {
eprintln!("error: {err}");
return ExitCode::FAILURE;
}
};
if let Err(err) = storage.parse() {
eprintln!("error: {err}");
return ExitCode::FAILURE;
}
let journal = match storage.journal() {
Ok(journal) => journal,
Err(err) => {
eprintln!("error: {err}");
return ExitCode::FAILURE;
}
};
for entry in journal.entries() {
println!("{entry:#?}");
}
ExitCode::SUCCESS
}
fn run_check(file: PathBuf) -> ExitCode {
let mut storage = match PactoleFileStorage::try_from(file) {
Ok(storage) => storage,
Err(err) => {
eprintln!("error: {err}");
return ExitCode::FAILURE;
}
};
if let Err(err) = storage.parse() {
eprintln!("error: {err}");
return ExitCode::FAILURE;
}
let journal = match storage.journal() {
Ok(journal) => journal,
Err(err) => {
eprintln!("error: {err}");
return ExitCode::FAILURE;
}
};
if let Err(err) = pactole_core::validate_journal(journal) {
eprintln!("error: {err}");
return ExitCode::FAILURE;
}
println!("OK");
ExitCode::SUCCESS
}
#[allow(clippy::too_many_arguments)]
fn run_register(
file: PathBuf,
account: Option<String>,
from: Option<NaiveDate>,
to: Option<NaiveDate>,
payee: Option<String>,
narration: Option<String>,
tag: Option<String>,
status: Option<StatusArg>,
) -> ExitCode {
let mut storage = match PactoleFileStorage::try_from(file) {
Ok(storage) => storage,
Err(err) => {
eprintln!("error: {err}");
return ExitCode::FAILURE;
}
};
if let Err(err) = storage.parse() {
eprintln!("error: {err}");
return ExitCode::FAILURE;
}
let journal = match storage.journal() {
Ok(journal) => journal,
Err(err) => {
eprintln!("error: {err}");
return ExitCode::FAILURE;
}
};
let journal = match pactole_core::validate_journal(journal) {
Ok(journal) => journal,
Err(err) => {
eprintln!("error: {err}");
return ExitCode::FAILURE;
}
};
let account = match account {
Some(account) => match pactole_core::AccountName::new(account) {
Ok(account) => Some(account),
Err(err) => {
eprintln!("error: {err}");
return ExitCode::FAILURE;
}
},
None => None,
};
let filter = RegisterFilter {
account,
from,
to,
payee,
narration,
tag,
status: status.map(TransactionStatus::from),
};
for entry in pactole_core::register(&journal, &filter) {
let status = match entry.status {
TransactionStatus::Uncleared => "?",
TransactionStatus::Pending => "!",
TransactionStatus::Cleared => "*",
};
let description = [entry.payee.as_deref(), entry.narration.as_deref()]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(" ");
let amount = match &entry.amount {
Some(amount) => format!("{} {}", amount.number, amount.commodity),
None => String::new(),
};
let balance = entry
.balance
.iter()
.map(|(commodity, sum)| format!("{sum} {commodity}"))
.collect::<Vec<_>>()
.join(", ");
println!(
"{} {} {:<40} {:<25} {:>15} {:>15}",
entry.date, status, description, entry.account, amount, balance
);
}
ExitCode::SUCCESS
}
fn run_fmt(file: String, write: bool) -> ExitCode {
let source = if file == "-" {
let mut buf = String::new();
if let Err(err) = std::io::stdin().read_to_string(&mut buf) {
eprintln!("error: {err}");
return ExitCode::FAILURE;
}
buf
} else {
match fs::read_to_string(&file) {
Ok(source) => source,
Err(err) => {
eprintln!("error: {err}");
return ExitCode::FAILURE;
}
}
};
let formatted = match pactole_storage_fs::format(&source) {
Ok(formatted) => formatted,
Err(err) => {
eprintln!("error: {err}");
return ExitCode::FAILURE;
}
};
if write && file != "-" {
if let Err(err) = fs::write(&file, formatted) {
eprintln!("error: {err}");
return ExitCode::FAILURE;
}
} else {
print!("{formatted}");
}
ExitCode::SUCCESS
}