#include "ledger.h" #include "balance.h" #include "error.h" #include "textual.h" #include "binary.h" #include "item.h" #include "expr.h" #include "format.h" #include #include #include #include namespace ledger { ////////////////////////////////////////////////////////////////////// // // The command-line balance report // static const std::string bal_fmt = "%20T%2_%-n\n"; unsigned int show_balances(std::ostream& out, items_deque& items, const node_t * predicate, const node_t * sort_order, const format_t& format, const bool show_expanded, const item_t * displayed_parent) { unsigned int headlines = 0; value_predicate pred_obj(predicate); for (items_deque::const_iterator i = items.begin(); i != items.end(); i++) { const item_t * parent = displayed_parent; if (pred_obj(*i) && ((*i)->subitems.size() != 1 || (*i)->total != (*i)->subitems[0]->total)) { format.format_elements(out, *i, parent); parent = *i; if (! displayed_parent->parent) headlines++; } if (sort_order) (*i)->sort(sort_order); if (show_expanded) headlines += show_balances(out, (*i)->subitems, predicate, sort_order, format, true, parent); } return headlines; } void balance_report(std::ostream& out, item_t * top, const node_t * predicate, const node_t * sort_order, const format_t& format, const bool show_expanded, const bool show_subtotals) { if (sort_order) top->sort(sort_order); unsigned int headlines = show_balances(out, top->subitems, predicate, sort_order, format, show_expanded, top); if (show_subtotals && headlines > 1 && top->total) { std::cout << "--------------------\n"; format.format_elements(std::cout, top); } } ////////////////////////////////////////////////////////////////////// // // The command-line register report // static const std::string reg_fmt = "%10d %-.20p %-.22N %12.66t %12.80T\n%/%22_ %-.22N %12.66t %12.80T\n"; static bool show_commodities_revalued = false; static bool show_commodities_revalued_only = false; static void report_value_change(std::ostream& out, const std::time_t date, const balance_pair_t& balance, const balance_pair_t& prev_balance, const node_t * predicate, const format_t& first_line_format, const format_t& next_lines_format) { static std::time_t prev_date = -1; if (prev_date == -1) { prev_date = date; return; } item_t temp; temp.date = prev_date; temp.total = prev_balance; balance_t prev_bal = format_t::compute_total(&temp); temp.date = date; temp.total = balance; balance_t cur_bal = format_t::compute_total(&temp); if (balance_t diff = cur_bal - prev_bal) { temp.value = diff; temp.total = balance; temp.payee = "Commodities revalued"; if (value_predicate(predicate)(&temp)) { first_line_format.format_elements(out, &temp); next_lines_format.format_elements(out, &temp); } } prev_date = date; } void register_report(std::ostream& out, item_t * top, const node_t * predicate, const node_t * sort_order, const format_t& first_line_format, const format_t& next_lines_format, const bool show_expanded) { if (sort_order) top->sort(sort_order); balance_pair_t balance; balance_pair_t last_reported; account_t splits(NULL, ""); value_predicate pred_obj(predicate); for (items_deque::const_iterator i = top->subitems.begin(); i != top->subitems.end(); i++) { bool first = true; if ((*i)->subitems.size() > 1 && ! show_expanded) { item_t summary; summary.date = (*i)->date; summary.parent = *i; summary.account = &splits; for (items_deque::const_iterator j = (*i)->subitems.begin(); j != (*i)->subitems.end(); j++) summary.value += (*j)->value; summary.total = balance + summary.value; bool show = pred_obj(&summary); if (show && show_commodities_revalued) report_value_change(out, summary.date, balance, last_reported, predicate, first_line_format, next_lines_format); balance += summary.value; if (show) { if (! show_commodities_revalued_only) first_line_format.format_elements(out, *i, top); if (show_commodities_revalued) last_reported = balance; } } else { for (items_deque::const_iterator j = (*i)->subitems.begin(); j != (*i)->subitems.end(); j++) { (*j)->total = balance + (*j)->value; bool show = pred_obj(*j); if (show && first && show_commodities_revalued) { report_value_change(out, (*i)->date, balance, last_reported, predicate, first_line_format, next_lines_format); if (show_commodities_revalued_only) first = false; } balance += (*j)->value; if (show) { if (! show_commodities_revalued_only) { if (first) { first = false; first_line_format.format_elements(out, *j, *i); } else { next_lines_format.format_elements(out, *j, *i); } } if (show_commodities_revalued) last_reported = balance; } } } } if (show_commodities_revalued) report_value_change(out, -1, balance, last_reported, predicate, first_line_format, next_lines_format); } bool add_new_entry(int index, int argc, char **argv, ledger_t * ledger) { masks_list regexps; entry_t added; entry_t * matching = NULL; added.state = entry_t::UNCLEARED; assert(index < argc); if (! parse_date(argv[index++], &added.date)) { std::cerr << "Error: Bad entry date: " << argv[index - 1] << std::endl; return false; } if (index == argc) { std::cerr << "Error: Too few arguments to 'entry'." << std::endl; return false; } regexps.push_back(mask_t(argv[index++])); for (entries_list::reverse_iterator i = ledger->entries.rbegin(); i != ledger->entries.rend(); i++) if (matches(regexps, (*i)->payee)) { matching = *i; break; } added.payee = matching ? matching->payee : regexps.front().pattern; if (index == argc) { std::cerr << "Error: Too few arguments to 'entry'." << std::endl; return false; } if (argv[index][0] == '-' || std::isdigit(argv[index][0])) { if (! matching) { std::cerr << "Error: Missing account name for non-matching entry." << std::endl; return false; } transaction_t * m_xact, * xact, * first; m_xact = matching->transactions.front(); amount_t amt(argv[index++]); first = xact = new transaction_t(&added, m_xact->account, amt, amt); if (xact->amount.commodity->symbol.empty()) { xact->amount.commodity = m_xact->amount.commodity; xact->cost.commodity = m_xact->amount.commodity; } added.add_transaction(xact); m_xact = matching->transactions.back(); xact = new transaction_t(&added, m_xact->account, - first->amount, - first->amount); added.add_transaction(xact); if ((index + 1) < argc && std::string(argv[index]) == "-from") if (account_t * acct = ledger->find_account(argv[++index])) added.transactions.back()->account = acct; } else { while (index < argc && std::string(argv[index]) != "-from") { mask_t acct_regex(argv[index++]); account_t * acct = NULL; commodity_t * cmdty = NULL; if (matching) { for (transactions_list::iterator x = matching->transactions.begin(); x != matching->transactions.end(); x++) { if (acct_regex.match((*x)->account->fullname())) { acct = (*x)->account; cmdty = (*x)->amount.commodity; break; } } } if (! acct) acct = ledger->find_account(acct_regex.pattern); if (! acct) { std::cerr << "Error: Could not find account name '" << acct_regex.pattern << "'." << std::endl; return false; } if (index == argc) { std::cerr << "Error: Too few arguments to 'entry'." << std::endl; return false; } amount_t amt(argv[index]++); transaction_t * xact = new transaction_t(&added, acct, amt, amt); if (! xact->amount.commodity) xact->amount.commodity = cmdty; added.add_transaction(xact); } if ((index + 1) < argc && std::string(argv[index]) == "-from") { if (account_t * acct = ledger->find_account(argv[++index])) { transaction_t * xact = new transaction_t(NULL, acct); added.add_transaction(xact); } } else { if (! matching) { std::cerr << "Error: Could not figure out the account to draw from." << std::endl; std::exit(1); } transaction_t * xact = new transaction_t(&added, matching->transactions.back()->account); added.add_transaction(xact); } } // if (added.finalize()) print_textual_entry(std::cout, &added); return true; } void set_price_conversion(const std::string& setting) { char buf[128]; std::strcpy(buf, setting.c_str()); assert(setting.length() < 128); char * c = buf; char * p = std::strchr(buf, '='); if (! p) { std::cerr << "Warning: Invalid price setting: " << setting << std::endl; } else { *p++ = '\0'; amount_t price; price.parse(p); commodity_t * commodity = commodity_t::find_commodity(c, true); commodity->set_conversion(price); } } static long pricing_leeway = 24 * 3600; static std::string price_db; static bool cache_dirty = false; void download_price_quote(commodity_t * commodity, const std::time_t age, const amount_t& price, const std::time_t moment) { std::time_t now = std::time(NULL); // the time of the query if (! (commodity->flags & COMMODITY_STYLE_CONSULTED) && std::difftime(now, moment) < pricing_leeway && (! price || std::difftime(moment, age) > pricing_leeway)) { using namespace std; // Only consult the Internet once for any commodity commodity->flags |= COMMODITY_STYLE_CONSULTED; cache_dirty = true; char buf[256]; buf[0] = '\0'; if (FILE * fp = popen((string("getquote ") + commodity->symbol).c_str(), "r")) { if (feof(fp) || ! fgets(buf, 255, fp)) { fclose(fp); return; } fclose(fp); } if (buf[0]) { char * p = strchr(buf, '\n'); if (p) *p = '\0'; amount_t current; current.parse(buf); commodity->add_price(now, current); if (! price_db.empty()) { char buf[128]; strftime(buf, 127, "%Y/%m/%d %H:%M:%S", localtime(&now)); ofstream database(price_db.c_str(), ios_base::out | ios_base::app); database << "P " << buf << " " << commodity->symbol << " " << current << endl; } } } } } // namespace ledger static void show_help(std::ostream& out) { std::cerr << "usage: ledger [options] COMMAND [options] [REGEXPS]" << std::endl << std::endl << "Basic options:" << std::endl << " -h display this help text" << std::endl << " -v display version information" << std::endl << " -f FILE specify pathname of ledger data file" << std::endl << std::endl << "Report filtering:" << std::endl << " -a REGEX specify an account regex for \"print\"" << std::endl << " -b DATE specify a beginning date" << std::endl << " -e DATE specify an ending date" << std::endl << " -c do not show future entries (same as -e TODAY)" << std::endl << " -d DATE specify a date mask ('-d mon', for all mondays)" << std::endl << " -C show only cleared transactions and balances" << std::endl << " -U show only uncleared transactions and balances" << std::endl << " -R do not consider virtual transactions: real only" << std::endl << " -l EXPR don't print entries for which EXPR yields 0" << std::endl << std::endl << "Customizing output:" << std::endl << " -n do not calculate parent account totals" << std::endl << " -s show sub-accounts in balance, and splits in register" << std::endl << " -M print register using monthly sub-totals" << std::endl << " -E show accounts that total to zero" << std::endl << " -S EXPR sort entry output based on EXPR" << std::endl << std::endl << "Commodity prices:" << std::endl << " -T report commodity totals, not their market value" << std::endl << " -B report cost basis of commodities" << std::endl << " -V report the market value of commodities" << std::endl << " -P FILE sets the price database, for reading/writing price info" << std::endl << " -Q download new price information (when needed) from the Internet" << std::endl << " (works by running \"getquote SYMBOL\")" << std::endl << " -L MINS with -Q, fetch quotes only if data is older than MINS" << std::endl << " -p STR specifies a direct commodity conversion: COMM=AMOUNT" << std::endl << std::endl << "commands:" << std::endl << " balance show balance totals" << std::endl << " register display a register for ACCOUNT" << std::endl << " print print all ledger entries" << std::endl << " equity generate equity ledger for all entries" << std::endl << " entry output a newly formed entry, based on arguments" << std::endl << " price show the last known price for matching commodities" << std::endl; } int main(int argc, char * argv[]) { std::list files; std::string predicate_string; ledger::node_t * predicate = NULL; std::string format_string; std::string sort_string; ledger::node_t * sort_order = NULL; std::string value_expr = "a"; std::string total_expr = "T"; ledger::ledger_t * journal = new ledger::ledger_t; bool show_subtotals = true; bool show_expanded = false; bool show_related = false; bool show_inverted = false; #ifdef DEBUG bool debug = false; #endif // Initialize some variables based on environment variable settings if (char * p = std::getenv("PRICE_HIST")) ledger::price_db = p; if (char * p = std::getenv("PRICE_EXP")) ledger::pricing_leeway = std::atol(p) * 60; // A ledger data file must be specified bool use_cache = std::getenv("LEDGER") != NULL; if (use_cache) { for (int i = 0; i < argc; i++) if (std::strcmp(argv[i], "-f") == 0) { use_cache = false; break; } ledger::cache_dirty = true; if (use_cache) if (const char * p = std::getenv("LEDGER_CACHE")) if (access(p, R_OK) != -1) { std::ifstream instr(p); if (! ledger::read_binary_ledger(instr, std::getenv("LEDGER"), journal)) { // We need to throw away what we've read, and create a new // ledger delete journal; journal = new ledger::ledger_t; } else { ledger::cache_dirty = false; } } } // Parse the command-line options int c, index; while (-1 != (c = getopt(argc, argv, "+ABb:Ccd:DEe:F:f:Ghi:L:l:MnoOP:p:QRS:st:T:UVvWXZz"))) { switch (char(c)) { // Basic options case 'h': show_help(std::cout); break; #ifdef DEBUG case 'z': debug = 1; break; #endif case 'v': std::cout << "Ledger " << ledger::version << ", the command-line accounting tool" << std::endl << " Copyright (c) 2003-2004, New Artisans LLC. All rights reserved." << std::endl << std::endl << "This program is made available under the terms of the BSD Public" << std::endl << "License. See the LICENSE file included with the distribution for" << std::endl << "details and disclaimer." << std::endl; return 0; case 'f': files.push_back(optarg); use_cache = false; break; case 'p': ledger::set_price_conversion(optarg); break; case 'b': if (! predicate_string.empty()) predicate_string += "&"; predicate_string += "(d>=["; predicate_string += optarg; predicate_string += "])"; break; case 'e': if (! predicate_string.empty()) predicate_string += "&"; predicate_string += "(d<["; predicate_string += optarg; predicate_string += "])"; break; case 'c': { if (! predicate_string.empty()) predicate_string += "&"; predicate_string += "(d<"; std::ostringstream now; now << std::time(NULL); predicate_string += now.str(); predicate_string += ")"; break; } case 'C': if (! predicate_string.empty()) predicate_string += "&"; predicate_string += "X"; break; case 'U': if (! predicate_string.empty()) predicate_string += "&"; predicate_string += "!X"; break; case 'R': if (! predicate_string.empty()) predicate_string += "&"; predicate_string += "R"; break; // Customizing output case 'F': format_string = optarg; break; #if 0 case 'E': show_empty = true; break; #endif case 'n': show_subtotals = false; break; case 's': show_expanded = true; break; case 'S': sort_string = optarg; break; case 'o': show_related = true; break; case 'l': if (! predicate_string.empty()) predicate_string += "&"; predicate_string += "("; predicate_string += optarg; predicate_string += ")"; break; // Commodity reporting case 'P': ledger::price_db = optarg; break; case 'L': ledger::pricing_leeway = std::atol(optarg) * 60; break; case 'Q': ledger::commodity_t::updater = ledger::download_price_quote; break; case 't': value_expr = optarg; break; case 'T': total_expr = optarg; break; case 'O': value_expr = "a"; total_expr = "T"; break; case 'B': value_expr = "c"; total_expr = "C"; break; case 'V': ledger::show_commodities_revalued = true; value_expr = "v"; total_expr = "V"; break; case 'G': ledger::show_commodities_revalued = ledger::show_commodities_revalued_only = true; value_expr = "c"; total_expr = "G"; break; case 'A': value_expr = "a"; total_expr = "MT"; break; case 'D': value_expr = "a"; total_expr = "DMT"; break; case 'Z': value_expr = "a"; total_expr = "MDMT"; break; #if 0 case 'W': value_expr = "a"; total_expr = "MD(MT*(d-b/e-b))"; break; case 'X': value_expr = "a"; total_expr = "a+MD(MT*(d-b/e-b))"; break; #endif default: assert(0); break; } } if (optind == argc) { show_help(std::cout); return 1; } index = optind; // Read the ledger file, unless we already read it from the cache if (! use_cache || ledger::cache_dirty) { int entry_count = 0; try { if (files.empty()) { if (char * p = std::getenv("LEDGER")) for (p = std::strtok(p, ":"); p; p = std::strtok(NULL, ":")) entry_count += parse_ledger_file(p, journal); } else { for (std::list::iterator i = files.begin(); i != files.end(); i++) { char buf[4096]; char * p = buf; std::strcpy(p, (*i).c_str()); entry_count += parse_ledger_file(p, journal); } } // Read prices from their own ledger file, after all others have // been read. if (! ledger::price_db.empty()) { const char * path = ledger::price_db.c_str(); std::ifstream db(path); journal->sources.push_back(path); entry_count += ledger::parse_textual_ledger(db, journal, journal->master); } } catch (ledger::error& err) { std::cerr << "Fatal: " << err.what() << std::endl; return 1; } if (entry_count == 0) { std::cerr << ("Please specify ledger file(s) using -f option " "or LEDGER environment variable.") << std::endl; return 1; } } // Read the command word, and handle the "entry" command specially, // without any other processing. const std::string command = argv[index++]; if (command == "entry") return add_new_entry(index, argc, argv, journal) ? 0 : 1; // Interpret the remaining arguments as regular expressions, used // for refining report results. for (; index < argc; index++) { if (std::strcmp(argv[index], "--") == 0) { index++; break; } show_expanded = true; if (! predicate_string.empty()) predicate_string += "&"; if (argv[index][0] == '-') { predicate_string += "(!/"; predicate_string += argv[index] + 1; } else { predicate_string += "(/"; predicate_string += argv[index]; } predicate_string += "/)"; } for (; index < argc; index++) { show_expanded = true; if (! predicate_string.empty()) predicate_string += "&"; if (argv[index][0] == '-') { predicate_string += "(!//"; predicate_string += argv[index] + 1; } else { predicate_string += "(//"; predicate_string += argv[index]; } predicate_string += "/)"; } // Compile the predicate if (! predicate_string.empty()) { #ifdef DEBUG if (debug) std::cerr << "predicate = " << predicate_string << std::endl; #endif predicate = ledger::parse_expr(predicate_string); } // Compile the sorting string if (! sort_string.empty()) sort_order = ledger::parse_expr(sort_string); // Setup the meaning of %t and %T encountered in format strings ledger::format_t::value_expr = ledger::parse_expr(value_expr); ledger::format_t::total_expr = ledger::parse_expr(total_expr); // Now handle the command that was identified above. if (command == "print") { #if 0 if (ledger::item_t * top = ledger::walk_entries(journal->entries.begin(), journal->entries.end(), predicate, show_related, show_inverted)) { ledger::format_t * format = new ledger::format_t(format_string); ledger::entry_report(std::cout, top, *format); #ifdef DEBUG delete top; delete format; #endif } #endif } else if (command == "equity") { #if 0 if (ledger::item_t * top = ledger::walk_accounts(journal->master, predicate, show_subtotals)) { ledger::format_t * format = new ledger::format_t(format_string); ledger::entry_report(std::cout, top, predicate, *format); #ifdef DEBUG delete top; delete format; #endif } #endif } else if (! sort_order && ! show_related && (command == "balance" || command == "bal")) { if (ledger::item_t * top = ledger::walk_accounts(journal->master, predicate, show_subtotals)) { ledger::format_t * format = new ledger::format_t(format_string.empty() ? ledger::bal_fmt : format_string); ledger::balance_report(std::cout, top, predicate, sort_order, *format, show_expanded, show_subtotals); #ifdef DEBUG delete format; delete top; #endif } } else if (command == "balance" || command == "bal") { if (ledger::item_t * list = ledger::walk_entries(journal->entries.begin(), journal->entries.end(), predicate, show_related, show_inverted)) if (ledger::item_t * top = ledger::walk_items(list, journal->master, predicate, show_subtotals)) { ledger::format_t * format = new ledger::format_t(format_string.empty() ? ledger::bal_fmt : format_string); ledger::balance_report(std::cout, top, predicate, sort_order, *format, show_expanded, show_subtotals); #ifdef DEBUG delete format; delete top; delete list; #endif } } else if (command == "register" || command == "reg") { if (show_related) show_inverted = true; if (ledger::item_t * top = ledger::walk_entries(journal->entries.begin(), journal->entries.end(), predicate, show_related, show_inverted)) { std::string first_line_format; std::string next_lines_format; const char * f = (format_string.empty() ? ledger::reg_fmt.c_str() : format_string.c_str()); if (const char * p = std::strstr(f, "%/")) { first_line_format = std::string(f, 0, p - f); next_lines_format = std::string(p + 2); } else { first_line_format = format_string; next_lines_format = format_string; } ledger::format_t * format = new ledger::format_t(first_line_format); ledger::format_t * nformat = new ledger::format_t(next_lines_format); ledger::register_report(std::cout, top, predicate, sort_order, *format, *nformat, show_expanded); #ifdef DEBUG delete nformat; delete format; delete top; #endif } } else { std::cerr << "Error: Unrecognized command '" << command << "'." << std::endl; return 1; } // Save the cache, if need be if (use_cache && ledger::cache_dirty) if (const char * p = std::getenv("LEDGER_CACHE")) { std::ofstream outstr(p); assert(std::getenv("LEDGER")); ledger::write_binary_ledger(outstr, journal, std::getenv("LEDGER")); } #ifdef DEBUG delete journal; if (predicate) delete predicate; if (sort_order) delete sort_order; if (ledger::format_t::value_expr) delete ledger::format_t::value_expr; if (ledger::format_t::total_expr) delete ledger::format_t::total_expr; // jww (2004-07-30): This should be moved into some kind of // "ledger::shutdown" function. for (ledger::commodities_map::iterator i = ledger::commodity_t::commodities.begin(); i != ledger::commodity_t::commodities.end(); i++) delete (*i).second; #endif return 0; } // main.cc ends here.