ledger/src/global.cc
2009-10-25 05:01:47 -04:00

638 lines
20 KiB
C++

/*
* Copyright (c) 2003-2009, John Wiegley. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of New Artisans LLC nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <system.hh>
#include "global.h"
#if defined(HAVE_BOOST_PYTHON)
#include "pyinterp.h"
#else
#include "session.h"
#endif
#include "item.h"
#include "journal.h"
#include "pool.h"
namespace ledger {
static bool args_only = false;
global_scope_t::global_scope_t(char ** envp)
{
TRACE_CTOR(global_scope_t, "");
#if defined(HAVE_BOOST_PYTHON)
if (! python_session.get()) {
python_session.reset(new ledger::python_interpreter_t);
session_ptr = python_session;
}
#else
session_ptr.reset(new session_t);
#endif
set_session_context(session_ptr.get());
// Create the report object, which maintains state relating to each
// command invocation. Because we're running from main(), the
// distinction between session and report doesn't really matter, but if
// a GUI were calling into Ledger it would have one session object per
// open document, with a separate report_t object for each report it
// generated.
report_stack.push_front(new report_t(session()));
// Read the user's options, in the following order:
//
// 1. environment variables (LEDGER_<option>)
// 2. initialization file (~/.ledgerrc)
// 3. command-line (--option or -o)
//
// Before processing command-line options, we must notify the session object
// that such options are beginning, since options like -f cause a complete
// override of files found anywhere else.
if (! args_only) {
session().set_flush_on_next_data_file(true);
read_environment_settings(envp);
session().set_flush_on_next_data_file(true);
read_init();
} else {
session().HANDLER(price_db_).off();
}
}
global_scope_t::~global_scope_t()
{
TRACE_DTOR(global_scope_t);
// If memory verification is being performed (which can be very slow),
// clean up everything by closing the session and deleting the session
// object, and then shutting down the memory tracing subsystem.
// Otherwise, let it all leak because we're about to exit anyway.
IF_VERIFY() set_session_context(NULL);
#if defined(HAVE_BOOST_PYTHON)
python_session.reset();
#endif
}
void global_scope_t::read_init()
{
if (HANDLED(init_file_)) {
path init_file(HANDLER(init_file_).str());
if (exists(init_file)) {
TRACE_START(init, 1, "Read initialization file");
ifstream init(init_file);
if (session().read_journal(init_file) > 0 ||
session().journal->auto_xacts.size() > 0 ||
session().journal->period_xacts.size() > 0) {
throw_(parse_error, _("Transactions found in initialization file '%1'")
<< init_file);
}
TRACE_FINISH(init, 1);
}
}
}
char * global_scope_t::prompt_string()
{
static char prompt[32];
std::size_t i;
for (i = 0; i < report_stack.size(); i++)
prompt[i] = ']';
prompt[i++] = ' ';
prompt[i] = '\0';
return prompt;
}
void global_scope_t::report_error(const std::exception& err)
{
std::cout.flush(); // first display anything that was pending
if (caught_signal == NONE_CAUGHT) {
// Display any pending error context information
string context = error_context();
if (! context.empty())
std::cerr << context << std::endl;
std::cerr << _("Error: ") << err.what() << std::endl;
} else {
caught_signal = NONE_CAUGHT;
}
}
void global_scope_t::execute_command(strings_list args, bool at_repl)
{
session().set_flush_on_next_data_file(true);
// Process the command verb, arguments and options
if (at_repl) {
args = read_command_arguments(report(), args);
if (args.empty())
return;
}
strings_list::iterator arg = args.begin();
string verb = *arg++;
// Look for a precommand first, which is defined as any defined function
// whose name starts with "ledger_precmd_". The difference between a
// precommand and a regular command is that precommands ignore the journal
// data file completely, nor is the user's init file read.
//
// Here are some examples of pre-commands:
//
// parse STRING ; show how a value expression is parsed
// eval STRING ; simply evaluate a value expression
// format STRING ; show how a format string is parsed
//
// If such a command is found, create the output stream for the result and
// then invoke the command.
function_t command;
bool is_precommand = false;
bind_scope_t bound_scope(*this, report());
if (bool(command = look_for_precommand(bound_scope, verb)))
is_precommand = true;
// If it is not a pre-command, then parse the user's ledger data at this
// time if not done alreday (i.e., if not at a REPL). Then patch up the
// report options based on the command verb.
if (! is_precommand) {
if (! at_repl)
session().read_journal_files();
normalize_report_options(verb);
if (! bool(command = look_for_command(bound_scope, verb)))
throw_(std::logic_error, _("Unrecognized command '%1'") << verb);
}
// Create the output stream (it might be a file, the console or a PAGER
// subprocess) and invoke the report command. The output stream is closed
// by the caller of this function.
report().output_stream
.initialize(report().HANDLED(output_) ?
optional<path>(path(report().HANDLER(output_).str())) :
optional<path>(),
report().HANDLED(pager_) ?
optional<path>(path(report().HANDLER(pager_).str())) :
optional<path>());
// Now that the output stream is initialized, report the options that will
// participate in this report, if the user specified --options
if (HANDLED(options))
report_options(report(), report().output_stream);
// Create an argument scope containing the report command's arguments, and
// then invoke the command. The bound scope causes lookups to happen
// first in the global scope, and then in the report scope.
call_scope_t command_args(bound_scope);
for (strings_list::iterator i = arg; i != args.end(); i++)
command_args.push_back(string_value(*i));
INFO_START(command, "Finished executing command");
command(command_args);
INFO_FINISH(command);
}
int global_scope_t::execute_command_wrapper(strings_list args, bool at_repl)
{
int status = 1;
try {
if (at_repl) push_report();
execute_command(args, at_repl);
if (at_repl) pop_report();
// If we've reached this point, everything succeeded fine. Ledger uses
// exceptions to notify of error conditions, so if you're using gdb,
// just type "catch throw" to find the source point of any error.
status = 0;
}
catch (const std::exception& err) {
if (at_repl) pop_report();
report_error(err);
}
return status;
}
void global_scope_t::report_options(report_t& report, std::ostream& out)
{
out << "<=============================================================================>"
<< std::endl;
out << "[Global scope options]" << std::endl;
HANDLER(args_only).report(out);
HANDLER(debug_).report(out);
HANDLER(init_file_).report(out);
HANDLER(script_).report(out);
HANDLER(trace_).report(out);
HANDLER(verbose).report(out);
HANDLER(verify).report(out);
out << std::endl << "[Session scope options]" << std::endl;
report.session.report_options(out);
out << std::endl << "[Report scope options]" << std::endl;
report.report_options(out);
out << "<=============================================================================>"
<< std::endl;
}
option_t<global_scope_t> * global_scope_t::lookup_option(const char * p)
{
switch (*p) {
case 'a':
OPT(args_only);
break;
case 'd':
OPT(debug_);
break;
case 'f':
OPT(full_help);
break;
case 'h':
OPT_(help);
else OPT(help_calc);
else OPT(help_comm);
else OPT(help_disp);
break;
case 'i':
OPT(init_file_);
break;
case 'o':
OPT(options);
break;
case 's':
OPT(script_);
break;
case 't':
OPT(trace_);
break;
case 'v':
OPT_(verbose);
else OPT(verify);
else OPT(version);
break;
}
return NULL;
}
expr_t::ptr_op_t global_scope_t::lookup(const string& name)
{
const char * p = name.c_str();
switch (*p) {
case 'o':
if (WANT_OPT()) { p += OPT_PREFIX_LEN;
if (option_t<global_scope_t> * handler = lookup_option(p))
return MAKE_OPT_HANDLER(global_scope_t, handler);
}
break;
case 'p':
if (WANT_PRECMD()) { const char * q = p + PRECMD_PREFIX_LEN;
switch (*q) {
case 'p':
if (is_eq(q, "push"))
return MAKE_FUNCTOR(global_scope_t::push_command);
else if (is_eq(q, "pop"))
return MAKE_FUNCTOR(global_scope_t::pop_command);
break;
}
}
break;
}
// If you're wondering how symbols from report() will be found, it's
// because of the bind_scope_t object in execute_command() below.
return NULL;
}
void global_scope_t::read_environment_settings(char * envp[])
{
TRACE_START(environment, 1, "Processed environment variables");
process_environment(const_cast<const char **>(envp), "LEDGER_", report());
#if 1
// These are here for backwards compatability, but are deprecated.
if (const char * p = std::getenv("LEDGER")) {
if (! std::getenv("LEDGER_FILE"))
process_option("environ", "file", report(), p, "LEDGER");
}
if (const char * p = std::getenv("LEDGER_INIT")) {
if (! std::getenv("LEDGER_INIT_FILE"))
process_option("environ", "init-file", report(), p, "LEDGER_INIT");
}
if (const char * p = std::getenv("PRICE_HIST")) {
if (! std::getenv("LEDGER_PRICEDB"))
process_option("environ", "price-db", report(), p, "PRICE_HIST");
}
if (const char * p = std::getenv("PRICE_EXP"))
process_option("environ", "price-exp", report(), p, "PRICE_EXP");
#endif
TRACE_FINISH(environment, 1);
}
strings_list
global_scope_t::read_command_arguments(scope_t& scope, strings_list args)
{
TRACE_START(arguments, 1, "Processed command-line arguments");
strings_list remaining = process_arguments(args, scope);
TRACE_FINISH(arguments, 1);
return remaining;
}
void global_scope_t::normalize_session_options()
{
INFO("Initialization file is " << HANDLER(init_file_).str());
INFO("Price database is " << session().HANDLER(price_db_).str());
foreach (const path& pathname, session().HANDLER(file_).data_files)
INFO("Journal file is " << pathname.string());
}
function_t global_scope_t::look_for_precommand(scope_t& scope,
const string& verb)
{
if (expr_t::ptr_op_t def = scope.lookup(string(PRECMD_PREFIX) + verb))
return def->as_function();
else
return function_t();
}
function_t global_scope_t::look_for_command(scope_t& scope,
const string& verb)
{
if (expr_t::ptr_op_t def = scope.lookup(string(CMD_PREFIX) + verb))
return def->as_function();
else
return function_t();
}
void global_scope_t::normalize_report_options(const string& verb)
{
// Patch up some of the reporting options based on what kind of
// command it was.
report_t& rep(report());
// jww (2009-02-09): These globals are a hack, but hard to avoid.
item_t::use_effective_date = rep.HANDLED(effective);
rep.session.commodity_pool->keep_base = rep.HANDLED(base);
rep.session.commodity_pool->get_quotes = rep.session.HANDLED(download);
if (rep.session.HANDLED(price_exp_))
rep.session.commodity_pool->quote_leeway =
rep.session.HANDLER(price_exp_).value.as_long();
if (rep.session.HANDLED(price_db_))
rep.session.commodity_pool->price_db = rep.session.HANDLER(price_db_).str();
else
rep.session.commodity_pool->price_db = none;
if (rep.HANDLED(date_format_)) {
set_date_format(rep.HANDLER(date_format_).str().c_str());
}
if (rep.HANDLED(datetime_format_)) {
set_datetime_format(rep.HANDLER(datetime_format_).str().c_str());
}
if (rep.HANDLED(start_of_week_)) {
if (optional<date_time::weekdays> weekday =
string_to_day_of_week(rep.HANDLER(start_of_week_).str()))
start_of_week = *weekday;
}
// jww (2008-08-14): This code really needs to be rationalized away for 3.0.
// I might be able to do it with command objects, like register_t, which
// each know how to adjust the report based on its current option settings.
if (verb == "print" || verb == "xact" || verb == "dump") {
rep.HANDLER(related).on_only(string("?normalize"));
rep.HANDLER(related_all).on_only(string("?normalize"));
}
else if (verb == "equity") {
rep.HANDLER(equity).on_only(string("?normalize"));
}
else if (rep.HANDLED(related)) {
if (verb[0] == 'r') {
rep.HANDLER(invert).on_only(string("?normalize"));
} else {
rep.HANDLER(subtotal).on_only(string("?normalize"));
rep.HANDLER(related_all).on_only(string("?normalize"));
}
}
if (verb == "print")
rep.HANDLER(limit_).on(string("?normalize"), "actual");
if (! rep.HANDLED(empty))
rep.HANDLER(display_).on(string("?normalize"), "amount|(!post&total)");
if (verb[0] != 'b' && verb[0] != 'r')
rep.HANDLER(base).on_only(string("?normalize"));
// If a time period was specified with -p, check whether it also gave a
// begin and/or end to the report period (though these can be overridden
// using -b or -e). Then, if no _duration_ was specified (such as monthly),
// then ignore the period since the begin/end are the only interesting
// details.
if (rep.HANDLED(period_)) {
if (! rep.HANDLED(sort_all_))
rep.HANDLER(sort_xacts_).on_only(string("?normalize"));
date_interval_t interval(rep.HANDLER(period_).str());
if (! rep.HANDLED(begin_) && interval.start) {
string predicate =
"date>=[" + to_iso_extended_string(*interval.start) + "]";
rep.HANDLER(limit_).on(string("?normalize"), predicate);
}
if (! rep.HANDLED(end_) && interval.end) {
string predicate =
"date<[" + to_iso_extended_string(*interval.end) + "]";
rep.HANDLER(limit_).on(string("?normalize"), predicate);
}
if (! interval.duration)
rep.HANDLER(period_).off();
}
// If -j or -J were specified, set the appropriate format string now so as
// to avoid option ordering issues were we to have done it during the
// initial parsing of the options.
if (rep.HANDLED(amount_data)) {
rep.HANDLER(format_)
.on_with(string("?normalize"), rep.HANDLER(plot_amount_format_).value);
}
else if (rep.HANDLED(total_data)) {
rep.HANDLER(format_)
.on_with(string("?normalize"), rep.HANDLER(plot_total_format_).value);
}
// If the --exchange (-X) option was used, parse out any final price
// settings that may be there.
if (rep.HANDLED(exchange_) &&
rep.HANDLER(exchange_).str().find('=') != string::npos) {
value_t(0L).exchange_commodities(rep.HANDLER(exchange_).str(), true,
rep.terminus);
}
long cols = 0;
if (rep.HANDLED(columns_))
cols = rep.HANDLER(columns_).value.to_long();
else if (const char * columns = std::getenv("COLUMNS"))
cols = lexical_cast<long>(columns);
if (cols > 0) {
DEBUG("auto.columns", "cols = " << cols);
long date_width = rep.HANDLER(date_width_).value.to_long();
long payee_width = (rep.HANDLER(payee_width_).specified ?
rep.HANDLER(payee_width_).value.to_long() :
int(double(cols) * 0.263157));
long account_width = (rep.HANDLER(account_width_).specified ?
rep.HANDLER(account_width_).value.to_long() :
int(double(cols) * 0.302631));
long amount_width = (rep.HANDLER(amount_width_).specified ?
rep.HANDLER(amount_width_).value.to_long() :
int(double(cols) * 0.157894));
long total_width = (rep.HANDLER(total_width_).specified ?
rep.HANDLER(total_width_).value.to_long() :
amount_width);
DEBUG("auto.columns", "date_width = " << date_width);
DEBUG("auto.columns", "payee_width = " << payee_width);
DEBUG("auto.columns", "account_width = " << account_width);
DEBUG("auto.columns", "amount_width = " << amount_width);
DEBUG("auto.columns", "total_width = " << total_width);
if (! rep.HANDLER(date_width_).specified &&
! rep.HANDLER(payee_width_).specified &&
! rep.HANDLER(account_width_).specified &&
! rep.HANDLER(amount_width_).specified &&
! rep.HANDLER(total_width_).specified) {
long total = (4 /* the spaces between */ + date_width + payee_width +
account_width + amount_width + total_width);
if (total > cols) {
DEBUG("auto.columns", "adjusting account down");
account_width -= total - cols;
DEBUG("auto.columns", "account_width now = " << account_width);
}
}
if (! rep.HANDLER(date_width_).specified)
rep.HANDLER(date_width_).on_with(string("?normalize"), date_width);
if (! rep.HANDLER(payee_width_).specified)
rep.HANDLER(payee_width_).on_with(string("?normalize"), payee_width);
if (! rep.HANDLER(account_width_).specified)
rep.HANDLER(account_width_).on_with(string("?normalize"), account_width);
if (! rep.HANDLER(amount_width_).specified)
rep.HANDLER(amount_width_).on_with(string("?normalize"), amount_width);
if (! rep.HANDLER(total_width_).specified)
rep.HANDLER(total_width_).on_with(string("?normalize"), total_width);
}
}
void global_scope_t::visit_man_page() const
{
int pid = fork();
if (pid < 0) {
throw std::logic_error(_("Failed to fork child process"));
}
else if (pid == 0) { // child
execlp("man", "man", "1", "ledger", NULL);
// We should never, ever reach here
perror("execlp: man");
exit(1);
}
int status = -1;
wait(&status);
exit(0); // parent
}
void handle_debug_options(int argc, char * argv[])
{
for (int i = 1; i < argc; i++) {
if (argv[i][0] == '-') {
if (std::strcmp(argv[i], "--args-only") == 0) {
args_only = true;
}
else if (std::strcmp(argv[i], "--verify") == 0) {
#if defined(VERIFY_ON)
verify_enabled = true; // global in utils.h
#endif
}
else if (std::strcmp(argv[i], "--verbose") == 0 ||
std::strcmp(argv[i], "-v") == 0) {
#if defined(LOGGING_ON)
_log_level = LOG_INFO; // global in utils.h
#endif
}
else if (i + 1 < argc && std::strcmp(argv[i], "--debug") == 0) {
#if defined(DEBUG_ON)
_log_level = LOG_DEBUG; // global in utils.h
_log_category = argv[i + 1]; // global in utils.h
i++;
#endif
}
else if (i + 1 < argc && std::strcmp(argv[i], "--trace") == 0) {
#if defined(TRACING_ON)
_log_level = LOG_TRACE; // global in utils.h
try {
// global in utils.h
_trace_level = boost::lexical_cast<uint8_t>(argv[i + 1]);
}
catch (const boost::bad_lexical_cast& e) {
throw std::logic_error(_("Argument to --trace must be an integer"));
}
i++;
#endif
}
}
}
}
} // namespace ledger