The semantics of this are a little bit tricky: we want, if we come across a transaction with exactly the given UUID, to set the transaction's payee to be the specified one. We need to set that payee before the first post in the transaction is parsed, otherwise that post will inherit the wrong payee; however, we need to do it after the transaction's tags have been parsed. The implementation as it is in this commit is potentially a little wasteful, if there are post-like (non-comment non-assertion) entries in the transaction that don't successfully parse as posts.
1971 lines
54 KiB
C++
1971 lines
54 KiB
C++
/*
|
|
* Copyright (c) 2003-2014, 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 "journal.h"
|
|
#include "context.h"
|
|
#include "xact.h"
|
|
#include "post.h"
|
|
#include "account.h"
|
|
#include "option.h"
|
|
#include "query.h"
|
|
#include "pstream.h"
|
|
#include "pool.h"
|
|
#if HAVE_BOOST_PYTHON
|
|
#include "pyinterp.h"
|
|
#endif
|
|
|
|
#define TIMELOG_SUPPORT 1
|
|
#if defined(TIMELOG_SUPPORT)
|
|
#include "timelog.h"
|
|
#endif
|
|
|
|
namespace ledger {
|
|
|
|
namespace {
|
|
typedef std::pair<commodity_t *, amount_t> fixed_rate_t;
|
|
|
|
struct application_t
|
|
{
|
|
string label;
|
|
variant<optional<datetime_t>, account_t *, string, fixed_rate_t> value;
|
|
|
|
application_t(string _label, optional<datetime_t> epoch)
|
|
: label(_label), value(epoch) {}
|
|
application_t(string _label, account_t * acct)
|
|
: label(_label), value(acct) {}
|
|
application_t(string _label, string tag)
|
|
: label(_label), value(tag) {}
|
|
application_t(string _label, fixed_rate_t rate)
|
|
: label(_label), value(rate) {}
|
|
};
|
|
|
|
class instance_t : public noncopyable, public scope_t
|
|
{
|
|
public:
|
|
parse_context_stack_t& context_stack;
|
|
parse_context_t& context;
|
|
std::istream& in;
|
|
instance_t * parent;
|
|
std::list<application_t> apply_stack;
|
|
bool no_assertions;
|
|
#if defined(TIMELOG_SUPPORT)
|
|
time_log_t timelog;
|
|
#endif
|
|
|
|
instance_t(parse_context_stack_t& _context_stack,
|
|
parse_context_t& _context,
|
|
instance_t * _parent = NULL,
|
|
const bool _no_assertions = false)
|
|
: context_stack(_context_stack), context(_context),
|
|
in(*context.stream.get()), parent(_parent),
|
|
no_assertions(_no_assertions), timelog(context) {}
|
|
|
|
virtual string description() {
|
|
return _("textual parser");
|
|
}
|
|
|
|
template <typename T>
|
|
void get_applications(std::vector<T>& result) {
|
|
foreach (application_t& state, apply_stack) {
|
|
if (state.value.type() == typeid(T))
|
|
result.push_back(boost::get<T>(state.value));
|
|
}
|
|
if (parent)
|
|
parent->get_applications<T>(result);
|
|
}
|
|
|
|
template <typename T>
|
|
optional<T> get_application() {
|
|
foreach (application_t& state, apply_stack) {
|
|
if (state.value.type() == typeid(T))
|
|
return boost::get<T>(state.value);
|
|
}
|
|
return parent ? parent->get_application<T>() : none;
|
|
}
|
|
|
|
account_t * top_account() {
|
|
if (optional<account_t *> acct = get_application<account_t *>())
|
|
return *acct;
|
|
else
|
|
return NULL;
|
|
}
|
|
|
|
void parse();
|
|
|
|
std::streamsize read_line(char *& line);
|
|
|
|
bool peek_whitespace_line() {
|
|
return (in.good() && ! in.eof() &&
|
|
(in.peek() == ' ' || in.peek() == '\t'));
|
|
}
|
|
#if HAVE_BOOST_PYTHON
|
|
bool peek_blank_line() {
|
|
return (in.good() && ! in.eof() &&
|
|
(in.peek() == '\n' || in.peek() == '\r'));
|
|
}
|
|
#endif
|
|
|
|
void read_next_directive(bool& error_flag);
|
|
|
|
#if defined(TIMELOG_SUPPORT)
|
|
void clock_in_directive(char * line, bool capitalized);
|
|
void clock_out_directive(char * line, bool capitalized);
|
|
#endif
|
|
|
|
bool general_directive(char * line);
|
|
|
|
void account_directive(char * line);
|
|
void account_alias_directive(account_t * account, string alias);
|
|
void account_payee_directive(account_t * account, string payee);
|
|
void account_value_directive(account_t * account, string expr_str);
|
|
void account_default_directive(account_t * account);
|
|
|
|
void default_account_directive(char * args);
|
|
void alias_directive(char * line);
|
|
|
|
void payee_directive(char * line);
|
|
void payee_alias_directive(const string& payee, string alias);
|
|
void payee_uuid_directive(const string& payee, string uuid);
|
|
|
|
void commodity_directive(char * line);
|
|
void commodity_alias_directive(commodity_t& comm, string alias);
|
|
void commodity_value_directive(commodity_t& comm, string expr_str);
|
|
void commodity_format_directive(commodity_t& comm, string format);
|
|
void commodity_nomarket_directive(commodity_t& comm);
|
|
void commodity_default_directive(commodity_t& comm);
|
|
|
|
void default_commodity_directive(char * line);
|
|
|
|
void tag_directive(char * line);
|
|
|
|
void apply_directive(char * line);
|
|
void apply_account_directive(char * line);
|
|
void apply_tag_directive(char * line);
|
|
void apply_rate_directive(char * line);
|
|
void apply_year_directive(char * line);
|
|
void end_apply_directive(char * line);
|
|
|
|
void xact_directive(char * line, std::streamsize len);
|
|
void period_xact_directive(char * line);
|
|
void automated_xact_directive(char * line);
|
|
void price_xact_directive(char * line);
|
|
void price_conversion_directive(char * line);
|
|
void nomarket_directive(char * line);
|
|
|
|
void include_directive(char * line);
|
|
void option_directive(char * line);
|
|
void comment_directive(char * line);
|
|
|
|
void eval_directive(char * line);
|
|
void assert_directive(char * line);
|
|
void check_directive(char * line);
|
|
void value_directive(char * line);
|
|
|
|
void import_directive(char * line);
|
|
void python_directive(char * line);
|
|
|
|
post_t * parse_post(char * line,
|
|
std::streamsize len,
|
|
account_t * account,
|
|
xact_t * xact,
|
|
bool defer_expr = false);
|
|
|
|
bool parse_posts(account_t * account,
|
|
xact_base_t& xact,
|
|
const bool defer_expr = false);
|
|
|
|
xact_t * parse_xact(char * line,
|
|
std::streamsize len,
|
|
account_t * account);
|
|
|
|
virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind,
|
|
const string& name);
|
|
};
|
|
|
|
void parse_amount_expr(std::istream& in,
|
|
scope_t& scope,
|
|
post_t& post,
|
|
amount_t& amount,
|
|
const parse_flags_t& flags = PARSE_DEFAULT,
|
|
const bool defer_expr = false,
|
|
optional<expr_t> * amount_expr = NULL)
|
|
{
|
|
expr_t expr(in, flags.plus_flags(PARSE_PARTIAL));
|
|
|
|
DEBUG("textual.parse", "Parsed an amount expression");
|
|
|
|
if (expr) {
|
|
if (amount_expr)
|
|
*amount_expr = expr;
|
|
if (! defer_expr)
|
|
amount = post.resolve_expr(scope, expr);
|
|
}
|
|
}
|
|
}
|
|
|
|
void instance_t::parse()
|
|
{
|
|
INFO("Parsing file " << context.pathname);
|
|
|
|
TRACE_START(instance_parse, 1, "Done parsing file " << context.pathname);
|
|
|
|
if (! in.good() || in.eof())
|
|
return;
|
|
|
|
context.linenum = 0;
|
|
context.curr_pos = in.tellg();
|
|
|
|
bool error_flag = false;
|
|
|
|
while (in.good() && ! in.eof()) {
|
|
try {
|
|
read_next_directive(error_flag);
|
|
}
|
|
catch (const std::exception& err) {
|
|
error_flag = true;
|
|
|
|
string current_context = error_context();
|
|
|
|
if (parent) {
|
|
std::list<instance_t *> instances;
|
|
|
|
for (instance_t * instance = parent;
|
|
instance;
|
|
instance = instance->parent)
|
|
instances.push_front(instance);
|
|
|
|
foreach (instance_t * instance, instances)
|
|
add_error_context(_f("In file included from %1%")
|
|
% instance->context.location());
|
|
}
|
|
add_error_context(_f("While parsing file %1%") % context.location());
|
|
|
|
if (caught_signal != NONE_CAUGHT)
|
|
throw;
|
|
|
|
string err_context = error_context();
|
|
if (! err_context.empty())
|
|
std::cerr << err_context << std::endl;
|
|
|
|
if (! current_context.empty())
|
|
std::cerr << current_context << std::endl;
|
|
|
|
std::cerr << _("Error: ") << err.what() << std::endl;
|
|
context.errors++;
|
|
}
|
|
}
|
|
|
|
#if defined(TIMELOG_SUPPORT)
|
|
timelog.close();
|
|
#endif // TIMELOG_SUPPORT
|
|
|
|
TRACE_STOP(instance_parse, 1);
|
|
}
|
|
|
|
std::streamsize instance_t::read_line(char *& line)
|
|
{
|
|
assert(in.good());
|
|
assert(! in.eof()); // no one should call us in that case
|
|
|
|
context.line_beg_pos = context.curr_pos;
|
|
|
|
check_for_signal();
|
|
|
|
in.getline(context.linebuf, parse_context_t::MAX_LINE);
|
|
std::streamsize len = in.gcount();
|
|
|
|
if (len > 0) {
|
|
context.linenum++;
|
|
|
|
context.curr_pos = context.line_beg_pos;
|
|
context.curr_pos += len;
|
|
|
|
if (context.linenum == 0 && utf8::is_bom(context.linebuf)) {
|
|
line = &context.linebuf[3];
|
|
len -= 3;
|
|
} else {
|
|
line = context.linebuf;
|
|
}
|
|
|
|
--len;
|
|
while (len > 0 && std::isspace(line[len - 1])) // strip trailing whitespace
|
|
line[--len] = '\0';
|
|
|
|
return len;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void instance_t::read_next_directive(bool& error_flag)
|
|
{
|
|
char * line;
|
|
std::streamsize len = read_line(line);
|
|
if (len == 0 || line == NULL)
|
|
return;
|
|
|
|
if (! std::isspace(line[0]))
|
|
error_flag = false;
|
|
|
|
switch (line[0]) {
|
|
case '\0':
|
|
assert(false); // shouldn't ever reach here
|
|
break;
|
|
|
|
case ' ':
|
|
case '\t':
|
|
if (! error_flag)
|
|
throw parse_error(_("Unexpected whitespace at beginning of line"));
|
|
break;
|
|
|
|
case ';': // comments
|
|
case '#':
|
|
case '*':
|
|
case '|':
|
|
break;
|
|
|
|
case '-': // option setting
|
|
option_directive(line);
|
|
break;
|
|
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
xact_directive(line, len);
|
|
break;
|
|
case '=': // automated xact
|
|
automated_xact_directive(line);
|
|
break;
|
|
case '~': // period xact
|
|
period_xact_directive(line);
|
|
break;
|
|
|
|
case '@':
|
|
case '!':
|
|
line++;
|
|
// fall through...
|
|
default: // some other directive
|
|
if (! general_directive(line)) {
|
|
switch (line[0]) {
|
|
#if defined(TIMELOG_SUPPORT)
|
|
case 'i':
|
|
clock_in_directive(line, false);
|
|
break;
|
|
case 'I':
|
|
clock_in_directive(line, true);
|
|
break;
|
|
|
|
case 'o':
|
|
clock_out_directive(line, false);
|
|
break;
|
|
case 'O':
|
|
clock_out_directive(line, true);
|
|
break;
|
|
|
|
case 'h':
|
|
case 'b':
|
|
break;
|
|
#endif // TIMELOG_SUPPORT
|
|
|
|
case 'A': // a default account for unbalanced posts
|
|
default_account_directive(line + 1);
|
|
break;
|
|
case 'C': // a set of conversions
|
|
price_conversion_directive(line);
|
|
break;
|
|
case 'D': // a default commodity for "xact"
|
|
default_commodity_directive(line);
|
|
break;
|
|
case 'N': // don't download prices
|
|
nomarket_directive(line);
|
|
break;
|
|
case 'P': // a pricing xact
|
|
price_xact_directive(line);
|
|
break;
|
|
case 'Y': // set the current year
|
|
apply_year_directive(line + 1);
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
#if defined(TIMELOG_SUPPORT)
|
|
|
|
void instance_t::clock_in_directive(char * line, bool capitalized)
|
|
{
|
|
string datetime(line, 2, 19);
|
|
|
|
char * p = skip_ws(line + 22);
|
|
char * n = next_element(p, true);
|
|
char * end = n ? next_element(n, true) : NULL;
|
|
|
|
if (end && *end == ';')
|
|
end = skip_ws(end + 1);
|
|
else
|
|
end = NULL;
|
|
|
|
position_t position;
|
|
position.pathname = context.pathname;
|
|
position.beg_pos = context.line_beg_pos;
|
|
position.beg_line = context.linenum;
|
|
position.end_pos = context.curr_pos;
|
|
position.end_line = context.linenum;
|
|
position.sequence = context.sequence++;
|
|
|
|
time_xact_t event(position, parse_datetime(datetime), capitalized,
|
|
p ? top_account()->find_account(p) : NULL,
|
|
n ? n : "",
|
|
end ? end : "");
|
|
|
|
timelog.clock_in(event);
|
|
}
|
|
|
|
void instance_t::clock_out_directive(char * line, bool capitalized)
|
|
{
|
|
string datetime(line, 2, 19);
|
|
|
|
char * p = skip_ws(line + 22);
|
|
char * n = next_element(p, true);
|
|
char * end = n ? next_element(n, true) : NULL;
|
|
|
|
if (end && *end == ';')
|
|
end = skip_ws(end + 1);
|
|
else
|
|
end = NULL;
|
|
|
|
position_t position;
|
|
position.pathname = context.pathname;
|
|
position.beg_pos = context.line_beg_pos;
|
|
position.beg_line = context.linenum;
|
|
position.end_pos = context.curr_pos;
|
|
position.end_line = context.linenum;
|
|
position.sequence = context.sequence++;
|
|
|
|
time_xact_t event(position, parse_datetime(datetime), capitalized,
|
|
p ? top_account()->find_account(p) : NULL,
|
|
n ? n : "",
|
|
end ? end : "");
|
|
|
|
context.count += timelog.clock_out(event);
|
|
}
|
|
|
|
#endif // TIMELOG_SUPPORT
|
|
|
|
void instance_t::default_commodity_directive(char * line)
|
|
{
|
|
amount_t amt(skip_ws(line + 1));
|
|
VERIFY(amt.valid());
|
|
commodity_pool_t::current_pool->default_commodity = &amt.commodity();
|
|
amt.commodity().add_flags(COMMODITY_KNOWN);
|
|
}
|
|
|
|
void instance_t::default_account_directive(char * line)
|
|
{
|
|
context.journal->bucket = top_account()->find_account(skip_ws(line));
|
|
context.journal->bucket->add_flags(ACCOUNT_KNOWN);
|
|
}
|
|
|
|
void instance_t::price_conversion_directive(char * line)
|
|
{
|
|
if (char * p = std::strchr(line + 1, '=')) {
|
|
*p++ = '\0';
|
|
amount_t::parse_conversion(line + 1, p);
|
|
}
|
|
}
|
|
|
|
void instance_t::price_xact_directive(char * line)
|
|
{
|
|
optional<std::pair<commodity_t *, price_point_t> > point =
|
|
commodity_pool_t::current_pool->parse_price_directive(skip_ws(line + 1));
|
|
if (! point)
|
|
throw parse_error(_("Pricing entry failed to parse"));
|
|
}
|
|
|
|
void instance_t::nomarket_directive(char * line)
|
|
{
|
|
char * p = skip_ws(line + 1);
|
|
string symbol;
|
|
commodity_t::parse_symbol(p, symbol);
|
|
|
|
if (commodity_t * commodity =
|
|
commodity_pool_t::current_pool->find_or_create(symbol))
|
|
commodity->add_flags(COMMODITY_NOMARKET | COMMODITY_KNOWN);
|
|
}
|
|
|
|
void instance_t::option_directive(char * line)
|
|
{
|
|
char * p = next_element(line);
|
|
if (! p) {
|
|
p = std::strchr(line, '=');
|
|
if (p)
|
|
*p++ = '\0';
|
|
}
|
|
|
|
if (! process_option(context.pathname.string(), line + 2, *context.scope,
|
|
p, line))
|
|
throw_(option_error, _f("Illegal option --%1%") % (line + 2));
|
|
}
|
|
|
|
void instance_t::automated_xact_directive(char * line)
|
|
{
|
|
istream_pos_type pos = context.line_beg_pos;
|
|
|
|
bool reveal_context = true;
|
|
|
|
try {
|
|
query_t query;
|
|
keep_details_t keeper(true, true, true);
|
|
expr_t::ptr_op_t expr =
|
|
query.parse_args(string_value(skip_ws(line + 1)).to_sequence(),
|
|
keeper, false, true);
|
|
|
|
unique_ptr<auto_xact_t> ae(new auto_xact_t(predicate_t(expr, keeper)));
|
|
ae->pos = position_t();
|
|
ae->pos->pathname = context.pathname;
|
|
ae->pos->beg_pos = context.line_beg_pos;
|
|
ae->pos->beg_line = context.linenum;
|
|
ae->pos->sequence = context.sequence++;
|
|
|
|
post_t * last_post = NULL;
|
|
|
|
while (peek_whitespace_line()) {
|
|
std::streamsize len = read_line(line);
|
|
char * p = skip_ws(line);
|
|
if (! *p)
|
|
break;
|
|
|
|
const std::size_t remlen = std::strlen(p);
|
|
|
|
if (*p == ';') {
|
|
item_t * item;
|
|
if (last_post)
|
|
item = last_post;
|
|
else
|
|
item = ae.get();
|
|
|
|
// This is a trailing note, and possibly a metadata info tag
|
|
ae->append_note(p + 1, *context.scope, true);
|
|
item->add_flags(ITEM_NOTE_ON_NEXT_LINE);
|
|
item->pos->end_pos = context.curr_pos;
|
|
item->pos->end_line++;
|
|
}
|
|
else if ((remlen > 7 && *p == 'a' &&
|
|
std::strncmp(p, "assert", 6) == 0 && std::isspace(p[6])) ||
|
|
(remlen > 6 && *p == 'c' &&
|
|
std::strncmp(p, "check", 5) == 0 && std::isspace(p[5])) ||
|
|
(remlen > 5 && *p == 'e' &&
|
|
((std::strncmp(p, "expr", 4) == 0 && std::isspace(p[4])) ||
|
|
(std::strncmp(p, "eval", 4) == 0 && std::isspace(p[4]))))) {
|
|
const char c = *p;
|
|
p = skip_ws(&p[*p == 'a' ? 6 : (*p == 'c' ? 5 : 4)]);
|
|
if (! ae->check_exprs)
|
|
ae->check_exprs = expr_t::check_expr_list();
|
|
ae->check_exprs->push_back
|
|
(expr_t::check_expr_pair(expr_t(p),
|
|
c == 'a' ?
|
|
expr_t::EXPR_ASSERTION :
|
|
(c == 'c' ?
|
|
expr_t::EXPR_CHECK :
|
|
expr_t::EXPR_GENERAL)));
|
|
}
|
|
else {
|
|
reveal_context = false;
|
|
|
|
if (post_t * post =
|
|
parse_post(p, len - (p - line), top_account(), NULL, true)) {
|
|
reveal_context = true;
|
|
ae->add_post(post);
|
|
ae->active_post = last_post = post;
|
|
}
|
|
reveal_context = true;
|
|
}
|
|
}
|
|
|
|
context.journal->auto_xacts.push_back(ae.get());
|
|
|
|
ae->journal = context.journal;
|
|
ae->pos->end_pos = context.curr_pos;
|
|
ae->pos->end_line = context.linenum;
|
|
|
|
ae.release();
|
|
}
|
|
catch (const std::exception&) {
|
|
if (reveal_context) {
|
|
add_error_context(_("While parsing automated transaction:"));
|
|
add_error_context(source_context(context.pathname, pos,
|
|
context.curr_pos, "> "));
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
void instance_t::period_xact_directive(char * line)
|
|
{
|
|
istream_pos_type pos = context.line_beg_pos;
|
|
|
|
bool reveal_context = true;
|
|
|
|
try {
|
|
|
|
unique_ptr<period_xact_t> pe(new period_xact_t(skip_ws(line + 1)));
|
|
pe->pos = position_t();
|
|
pe->pos->pathname = context.pathname;
|
|
pe->pos->beg_pos = context.line_beg_pos;
|
|
pe->pos->beg_line = context.linenum;
|
|
pe->pos->sequence = context.sequence++;
|
|
|
|
reveal_context = false;
|
|
|
|
if (parse_posts(top_account(), *pe.get())) {
|
|
reveal_context = true;
|
|
pe->journal = context.journal;
|
|
|
|
if (pe->finalize()) {
|
|
context.journal->extend_xact(pe.get());
|
|
context.journal->period_xacts.push_back(pe.get());
|
|
|
|
pe->pos->end_pos = context.curr_pos;
|
|
pe->pos->end_line = context.linenum;
|
|
|
|
pe.release();
|
|
} else {
|
|
reveal_context = true;
|
|
pe->journal = NULL;
|
|
throw parse_error(_("Period transaction failed to balance"));
|
|
}
|
|
}
|
|
|
|
}
|
|
catch (const std::exception&) {
|
|
if (reveal_context) {
|
|
add_error_context(_("While parsing periodic transaction:"));
|
|
add_error_context(source_context(context.pathname, pos,
|
|
context.curr_pos, "> "));
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
void instance_t::xact_directive(char * line, std::streamsize len)
|
|
{
|
|
TRACE_START(xacts, 1, "Time spent handling transactions:");
|
|
|
|
if (xact_t * xact = parse_xact(line, len, top_account())) {
|
|
unique_ptr<xact_t> manager(xact);
|
|
|
|
if (context.journal->add_xact(xact)) {
|
|
manager.release(); // it's owned by the journal now
|
|
context.count++;
|
|
}
|
|
// It's perfectly valid for the journal to reject the xact, which it
|
|
// will do if the xact has no substantive effect (for example, a
|
|
// checking xact, all of whose postings have null amounts).
|
|
} else {
|
|
throw parse_error(_("Failed to parse transaction"));
|
|
}
|
|
|
|
TRACE_STOP(xacts, 1);
|
|
}
|
|
|
|
void instance_t::include_directive(char * line)
|
|
{
|
|
path filename;
|
|
|
|
DEBUG("textual.include", "include: " << line);
|
|
|
|
if (line[0] != '/' && line[0] != '\\' && line[0] != '~') {
|
|
DEBUG("textual.include", "received a relative path");
|
|
DEBUG("textual.include", "parent file path: " << context.pathname);
|
|
string pathstr(context.pathname.string());
|
|
string::size_type pos = pathstr.rfind('/');
|
|
if (pos == string::npos)
|
|
pos = pathstr.rfind('\\');
|
|
if (pos != string::npos) {
|
|
filename = path(string(pathstr, 0, pos + 1)) / line;
|
|
DEBUG("textual.include", "normalized path: " << filename.string());
|
|
} else {
|
|
filename = path(string(".")) / line;
|
|
}
|
|
} else {
|
|
filename = line;
|
|
}
|
|
|
|
filename = resolve_path(filename);
|
|
DEBUG("textual.include", "resolved path: " << filename.string());
|
|
|
|
mask_t glob;
|
|
#if BOOST_VERSION >= 103700
|
|
path parent_path = filename.parent_path();
|
|
#if BOOST_VERSION >= 104600
|
|
glob.assign_glob('^' + filename.filename().string() + '$');
|
|
#else
|
|
glob.assign_glob('^' + filename.filename() + '$');
|
|
#endif
|
|
#else // BOOST_VERSION >= 103700
|
|
path parent_path = filename.branch_path();
|
|
glob.assign_glob('^' + filename.leaf() + '$');
|
|
#endif // BOOST_VERSION >= 103700
|
|
|
|
bool files_found = false;
|
|
if (exists(parent_path)) {
|
|
filesystem::directory_iterator end;
|
|
for (filesystem::directory_iterator iter(parent_path);
|
|
iter != end;
|
|
++iter) {
|
|
#if BOOST_VERSION <= 103500
|
|
if (is_regular(*iter))
|
|
#else
|
|
if (is_regular_file(*iter))
|
|
#endif
|
|
{
|
|
#if BOOST_VERSION >= 103700
|
|
#if BOOST_VERSION >= 104600
|
|
string base = (*iter).path().filename().string();
|
|
#else
|
|
string base = (*iter).filename();
|
|
#endif
|
|
#else // BOOST_VERSION >= 103700
|
|
string base = (*iter).leaf();
|
|
#endif // BOOST_VERSION >= 103700
|
|
if (glob.match(base)) {
|
|
journal_t * journal = context.journal;
|
|
account_t * master = top_account();
|
|
scope_t * scope = context.scope;
|
|
std::size_t& errors = context.errors;
|
|
std::size_t& count = context.count;
|
|
std::size_t& sequence = context.sequence;
|
|
|
|
DEBUG("textual.include", "Including: " << *iter);
|
|
DEBUG("textual.include", "Master account: " << master->fullname());
|
|
|
|
context_stack.push(*iter);
|
|
|
|
context_stack.get_current().journal = journal;
|
|
context_stack.get_current().master = master;
|
|
context_stack.get_current().scope = scope;
|
|
try {
|
|
instance_t instance(context_stack, context_stack.get_current(),
|
|
this, no_assertions);
|
|
instance.apply_stack.push_front(application_t("account", master));
|
|
instance.parse();
|
|
}
|
|
catch (...) {
|
|
errors += context_stack.get_current().errors;
|
|
count += context_stack.get_current().count;
|
|
sequence += context_stack.get_current().sequence;
|
|
|
|
context_stack.pop();
|
|
throw;
|
|
}
|
|
|
|
errors += context_stack.get_current().errors;
|
|
count += context_stack.get_current().count;
|
|
sequence += context_stack.get_current().sequence;
|
|
|
|
context_stack.pop();
|
|
|
|
files_found = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (! files_found)
|
|
throw_(std::runtime_error,
|
|
_f("File to include was not found: %1%") % filename);
|
|
|
|
}
|
|
|
|
void instance_t::apply_directive(char * line)
|
|
{
|
|
char * b = next_element(line);
|
|
string keyword(line);
|
|
if (keyword == "account")
|
|
apply_account_directive(b);
|
|
else if (keyword == "tag")
|
|
apply_tag_directive(b);
|
|
else if (keyword == "fixed" || keyword == "rate")
|
|
apply_rate_directive(b);
|
|
else if (keyword == "year")
|
|
apply_year_directive(b);
|
|
}
|
|
|
|
void instance_t::apply_account_directive(char * line)
|
|
{
|
|
if (account_t * acct = top_account()->find_account(line))
|
|
apply_stack.push_front(application_t("account", acct));
|
|
#if !NO_ASSERTS
|
|
else
|
|
assert("Failed to create account" == NULL);
|
|
#endif
|
|
}
|
|
|
|
void instance_t::apply_tag_directive(char * line)
|
|
{
|
|
string tag(trim_ws(line));
|
|
|
|
if (tag.find(':') == string::npos)
|
|
tag = string(":") + tag + ":";
|
|
|
|
apply_stack.push_front(application_t("tag", tag));
|
|
}
|
|
|
|
void instance_t::apply_rate_directive(char * line)
|
|
{
|
|
if (optional<std::pair<commodity_t *, price_point_t> > price_point =
|
|
commodity_pool_t::current_pool->parse_price_directive
|
|
(trim_ws(line), true, true)) {
|
|
apply_stack.push_front
|
|
(application_t("fixed", fixed_rate_t(price_point->first,
|
|
price_point->second.price)));
|
|
} else {
|
|
throw_(std::runtime_error, _("Error in fixed directive"));
|
|
}
|
|
}
|
|
|
|
void instance_t::apply_year_directive(char * line)
|
|
{
|
|
apply_stack.push_front(application_t("year", epoch));
|
|
|
|
// This must be set to the last day of the year, otherwise partial
|
|
// dates like "11/01" will refer to last year's november, not the
|
|
// current year.
|
|
unsigned short year(lexical_cast<unsigned short>(skip_ws(line)));
|
|
DEBUG("times.epoch", "Setting current year to " << year);
|
|
epoch = datetime_t(date_t(year, 12, 31));
|
|
}
|
|
|
|
void instance_t::end_apply_directive(char * kind)
|
|
{
|
|
char * b = kind ? next_element(kind) : NULL;
|
|
string name(b ? b : "");
|
|
|
|
if (apply_stack.size() <= 1) {
|
|
if (name.empty()) {
|
|
throw_(std::runtime_error,
|
|
_("'end' or 'end apply' found, but no enclosing 'apply' directive"));
|
|
} else {
|
|
throw_(std::runtime_error,
|
|
_f("'end apply %1%' found, but no enclosing 'apply' directive")
|
|
% name);
|
|
}
|
|
}
|
|
|
|
if (! name.empty() && name != apply_stack.front().label)
|
|
throw_(std::runtime_error,
|
|
_f("'end apply %1%' directive does not match 'apply %2%' directive")
|
|
% name % apply_stack.front().label);
|
|
|
|
if (apply_stack.front().value.type() == typeid(optional<datetime_t>))
|
|
epoch = boost::get<optional<datetime_t> >(apply_stack.front().value);
|
|
|
|
apply_stack.pop_front();
|
|
}
|
|
|
|
void instance_t::account_directive(char * line)
|
|
{
|
|
istream_pos_type beg_pos = context.line_beg_pos;
|
|
std::size_t beg_linenum = context.linenum;
|
|
|
|
char * p = skip_ws(line);
|
|
account_t * account =
|
|
context.journal->register_account(p, NULL, top_account());
|
|
unique_ptr<auto_xact_t> ae;
|
|
|
|
while (peek_whitespace_line()) {
|
|
read_line(line);
|
|
char * q = skip_ws(line);
|
|
if (! *q)
|
|
break;
|
|
|
|
char * b = next_element(q);
|
|
string keyword(q);
|
|
if (keyword == "alias") {
|
|
account_alias_directive(account, b);
|
|
}
|
|
else if (keyword == "payee") {
|
|
account_payee_directive(account, b);
|
|
}
|
|
else if (keyword == "value") {
|
|
account_value_directive(account, b);
|
|
}
|
|
else if (keyword == "default") {
|
|
account_default_directive(account);
|
|
}
|
|
else if (keyword == "assert" || keyword == "check") {
|
|
keep_details_t keeper(true, true, true);
|
|
expr_t expr(string("account == \"") + account->fullname() + "\"");
|
|
predicate_t pred(expr.get_op(), keeper);
|
|
|
|
if (! ae.get()) {
|
|
ae.reset(new auto_xact_t(pred));
|
|
|
|
ae->pos = position_t();
|
|
ae->pos->pathname = context.pathname;
|
|
ae->pos->beg_pos = beg_pos;
|
|
ae->pos->beg_line = beg_linenum;
|
|
ae->pos->sequence = context.sequence++;
|
|
ae->check_exprs = expr_t::check_expr_list();
|
|
}
|
|
|
|
ae->check_exprs->push_back
|
|
(expr_t::check_expr_pair(expr_t(b),
|
|
keyword == "assert" ?
|
|
expr_t::EXPR_ASSERTION :
|
|
expr_t::EXPR_CHECK));
|
|
}
|
|
else if (keyword == "eval" || keyword == "expr") {
|
|
// jww (2012-02-27): Make account into symbol scopes so that this
|
|
// can be used to override definitions within the account.
|
|
bind_scope_t bound_scope(*context.scope, *account);
|
|
expr_t(b).calc(bound_scope);
|
|
}
|
|
else if (keyword == "note") {
|
|
account->note = b;
|
|
}
|
|
}
|
|
|
|
if (ae.get()) {
|
|
context.journal->auto_xacts.push_back(ae.get());
|
|
|
|
ae->journal = context.journal;
|
|
ae->pos->end_pos = in.tellg();
|
|
ae->pos->end_line = context.linenum;
|
|
|
|
ae.release();
|
|
}
|
|
}
|
|
|
|
void instance_t::account_alias_directive(account_t * account, string alias)
|
|
{
|
|
// Once we have an alias name (alias) and the target account
|
|
// (account), add a reference to the account in the `account_aliases'
|
|
// map, which is used by the post parser to resolve alias references.
|
|
trim(alias);
|
|
// Ensure that no alias like "alias Foo=Foo" is registered.
|
|
if ( alias == account->fullname()) {
|
|
throw_(parse_error, _f("Illegal alias %1%=%2%")
|
|
% alias % account->fullname());
|
|
}
|
|
std::pair<accounts_map::iterator, bool> result =
|
|
context.journal->account_aliases.insert
|
|
(accounts_map::value_type(alias, account));
|
|
if (! result.second)
|
|
(*result.first).second = account;
|
|
}
|
|
|
|
void instance_t::alias_directive(char * line)
|
|
{
|
|
if (char * e = std::strchr(line, '=')) {
|
|
char * z = e - 1;
|
|
while (std::isspace(*z))
|
|
*z-- = '\0';
|
|
*e++ = '\0';
|
|
e = skip_ws(e);
|
|
|
|
account_alias_directive(top_account()->find_account(e), line);
|
|
}
|
|
}
|
|
|
|
void instance_t::account_payee_directive(account_t * account, string payee)
|
|
{
|
|
trim(payee);
|
|
context.journal->payees_for_unknown_accounts
|
|
.push_back(account_mapping_t(mask_t(payee), account));
|
|
}
|
|
|
|
void instance_t::account_default_directive(account_t * account)
|
|
{
|
|
context.journal->bucket = account;
|
|
}
|
|
|
|
void instance_t::account_value_directive(account_t * account, string expr_str)
|
|
{
|
|
account->value_expr = expr_t(expr_str);
|
|
}
|
|
|
|
void instance_t::payee_directive(char * line)
|
|
{
|
|
string payee = context.journal->register_payee(line, NULL);
|
|
|
|
while (peek_whitespace_line()) {
|
|
read_line(line);
|
|
char * p = skip_ws(line);
|
|
if (! *p)
|
|
break;
|
|
|
|
char * b = next_element(p);
|
|
string keyword(p);
|
|
if (keyword == "alias")
|
|
payee_alias_directive(payee, b);
|
|
if (keyword == "uuid")
|
|
payee_uuid_directive(payee, b);
|
|
}
|
|
}
|
|
|
|
void instance_t::payee_alias_directive(const string& payee, string alias)
|
|
{
|
|
trim(alias);
|
|
context.journal->payee_alias_mappings
|
|
.push_back(payee_alias_mapping_t(mask_t(alias), payee));
|
|
}
|
|
|
|
void instance_t::payee_uuid_directive(const string& payee, string uuid)
|
|
{
|
|
trim(uuid);
|
|
context.journal->payee_uuid_mappings
|
|
.push_back(payee_uuid_mapping_t(uuid, payee));
|
|
}
|
|
|
|
void instance_t::commodity_directive(char * line)
|
|
{
|
|
char * p = skip_ws(line);
|
|
string symbol;
|
|
commodity_t::parse_symbol(p, symbol);
|
|
|
|
if (commodity_t * commodity =
|
|
commodity_pool_t::current_pool->find_or_create(symbol)) {
|
|
context.journal->register_commodity(*commodity, 0);
|
|
|
|
while (peek_whitespace_line()) {
|
|
read_line(line);
|
|
char * q = skip_ws(line);
|
|
if (! *q)
|
|
break;
|
|
|
|
char * b = next_element(q);
|
|
string keyword(q);
|
|
if (keyword == "alias")
|
|
commodity_alias_directive(*commodity, b);
|
|
else if (keyword == "value")
|
|
commodity_value_directive(*commodity, b);
|
|
else if (keyword == "format")
|
|
commodity_format_directive(*commodity, b);
|
|
else if (keyword == "nomarket")
|
|
commodity_nomarket_directive(*commodity);
|
|
else if (keyword == "default")
|
|
commodity_default_directive(*commodity);
|
|
else if (keyword == "note")
|
|
commodity->set_note(string(b));
|
|
}
|
|
}
|
|
}
|
|
|
|
void instance_t::commodity_alias_directive(commodity_t& comm, string alias)
|
|
{
|
|
trim(alias);
|
|
commodity_pool_t::current_pool->alias(alias, comm);
|
|
}
|
|
|
|
void instance_t::commodity_value_directive(commodity_t& comm, string expr_str)
|
|
{
|
|
comm.set_value_expr(expr_t(expr_str));
|
|
}
|
|
|
|
void instance_t::commodity_format_directive(commodity_t&, string format)
|
|
{
|
|
// jww (2012-02-27): A format specified this way should turn off
|
|
// observational formatting.
|
|
trim(format);
|
|
amount_t amt;
|
|
amt.parse(format);
|
|
VERIFY(amt.valid());
|
|
}
|
|
|
|
void instance_t::commodity_nomarket_directive(commodity_t& comm)
|
|
{
|
|
comm.add_flags(COMMODITY_NOMARKET);
|
|
}
|
|
|
|
void instance_t::commodity_default_directive(commodity_t& comm)
|
|
{
|
|
commodity_pool_t::current_pool->default_commodity = &comm;
|
|
}
|
|
|
|
void instance_t::tag_directive(char * line)
|
|
{
|
|
char * p = skip_ws(line);
|
|
context.journal->register_metadata(p, NULL_VALUE, 0);
|
|
|
|
while (peek_whitespace_line()) {
|
|
read_line(line);
|
|
char * q = skip_ws(line);
|
|
if (! *q)
|
|
break;
|
|
|
|
char * b = next_element(q);
|
|
string keyword(q);
|
|
if (keyword == "assert" || keyword == "check") {
|
|
context.journal->tag_check_exprs.insert
|
|
(tag_check_exprs_map::value_type
|
|
(string(p), expr_t::check_expr_pair(expr_t(b),
|
|
keyword == "assert" ?
|
|
expr_t::EXPR_ASSERTION :
|
|
expr_t::EXPR_CHECK)));
|
|
}
|
|
}
|
|
}
|
|
|
|
void instance_t::eval_directive(char * line)
|
|
{
|
|
expr_t expr(line);
|
|
expr.calc(*context.scope);
|
|
}
|
|
|
|
void instance_t::assert_directive(char * line)
|
|
{
|
|
expr_t expr(line);
|
|
if (! expr.calc(*context.scope).to_boolean())
|
|
throw_(parse_error, _f("Assertion failed: %1%") % line);
|
|
}
|
|
|
|
void instance_t::check_directive(char * line)
|
|
{
|
|
expr_t expr(line);
|
|
if (! expr.calc(*context.scope).to_boolean())
|
|
context.warning(_f("Check failed: %1%") % line);
|
|
}
|
|
|
|
void instance_t::value_directive(char * line)
|
|
{
|
|
context.journal->value_expr = expr_t(line);
|
|
}
|
|
|
|
void instance_t::comment_directive(char * line)
|
|
{
|
|
while (in.good() && ! in.eof()) {
|
|
if (read_line(line) > 0) {
|
|
std::string buf(line);
|
|
if (starts_with(buf, "end comment") || starts_with(buf, "end test"))
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
#if HAVE_BOOST_PYTHON
|
|
|
|
void instance_t::import_directive(char * line)
|
|
{
|
|
string module_name(line);
|
|
trim(module_name);
|
|
python_session->import_option(module_name);
|
|
}
|
|
|
|
void instance_t::python_directive(char * line)
|
|
{
|
|
std::ostringstream script;
|
|
|
|
if (line)
|
|
script << skip_ws(line) << '\n';
|
|
|
|
std::size_t indent = 0;
|
|
|
|
while (peek_whitespace_line() || peek_blank_line()) {
|
|
if (read_line(line) > 0) {
|
|
if (! indent) {
|
|
const char * p = line;
|
|
while (*p && std::isspace(*p)) {
|
|
++indent;
|
|
++p;
|
|
}
|
|
}
|
|
|
|
const char * p = line;
|
|
for (std::size_t i = 0; i < indent; i++) {
|
|
if (std::isspace(*p))
|
|
++p;
|
|
else
|
|
break;
|
|
}
|
|
|
|
if (*p)
|
|
script << p << '\n';
|
|
}
|
|
}
|
|
|
|
if (! python_session->is_initialized)
|
|
python_session->initialize();
|
|
|
|
python_session->main_module->define_global
|
|
("journal", python::object(python::ptr(context.journal)));
|
|
python_session->eval(script.str(), python_interpreter_t::PY_EVAL_MULTI);
|
|
}
|
|
|
|
#else
|
|
|
|
void instance_t::import_directive(char *)
|
|
{
|
|
throw_(parse_error,
|
|
_("'python' directive seen, but Python support is missing"));
|
|
}
|
|
|
|
void instance_t::python_directive(char *)
|
|
{
|
|
throw_(parse_error,
|
|
_("'import' directive seen, but Python support is missing"));
|
|
}
|
|
|
|
#endif // HAVE_BOOST_PYTHON
|
|
|
|
bool instance_t::general_directive(char * line)
|
|
{
|
|
char buf[8192];
|
|
|
|
std::strcpy(buf, line);
|
|
|
|
char * p = buf;
|
|
char * arg = next_element(buf);
|
|
|
|
if (*p == '@' || *p == '!')
|
|
p++;
|
|
|
|
switch (*p) {
|
|
case 'a':
|
|
if (std::strcmp(p, "account") == 0) {
|
|
account_directive(arg);
|
|
return true;
|
|
}
|
|
else if (std::strcmp(p, "alias") == 0) {
|
|
alias_directive(arg);
|
|
return true;
|
|
}
|
|
else if (std::strcmp(p, "apply") == 0) {
|
|
apply_directive(arg);
|
|
return true;
|
|
}
|
|
else if (std::strcmp(p, "assert") == 0) {
|
|
assert_directive(arg);
|
|
return true;
|
|
}
|
|
break;
|
|
|
|
case 'b':
|
|
if (std::strcmp(p, "bucket") == 0) {
|
|
default_account_directive(arg);
|
|
return true;
|
|
}
|
|
break;
|
|
|
|
case 'c':
|
|
if (std::strcmp(p, "check") == 0) {
|
|
check_directive(arg);
|
|
return true;
|
|
}
|
|
else if (std::strcmp(p, "comment") == 0) {
|
|
comment_directive(arg);
|
|
return true;
|
|
}
|
|
else if (std::strcmp(p, "commodity") == 0) {
|
|
commodity_directive(arg);
|
|
return true;
|
|
}
|
|
break;
|
|
|
|
case 'd':
|
|
if (std::strcmp(p, "def") == 0 || std::strcmp(p, "define") == 0) {
|
|
eval_directive(arg);
|
|
return true;
|
|
}
|
|
break;
|
|
|
|
case 'e':
|
|
if (std::strcmp(p, "end") == 0) {
|
|
end_apply_directive(arg);
|
|
return true;
|
|
}
|
|
else if (std::strcmp(p, "expr") == 0 || std::strcmp(p, "eval") == 0) {
|
|
eval_directive(arg);
|
|
return true;
|
|
}
|
|
break;
|
|
|
|
case 'i':
|
|
if (std::strcmp(p, "include") == 0) {
|
|
include_directive(arg);
|
|
return true;
|
|
}
|
|
else if (std::strcmp(p, "import") == 0) {
|
|
import_directive(arg);
|
|
return true;
|
|
}
|
|
break;
|
|
|
|
case 'p':
|
|
if (std::strcmp(p, "payee") == 0) {
|
|
payee_directive(arg);
|
|
return true;
|
|
}
|
|
else if (std::strcmp(p, "python") == 0) {
|
|
python_directive(arg);
|
|
return true;
|
|
}
|
|
break;
|
|
|
|
case 't':
|
|
if (std::strcmp(p, "tag") == 0) {
|
|
tag_directive(arg);
|
|
return true;
|
|
}
|
|
else if (std::strcmp(p, "test") == 0) {
|
|
comment_directive(arg);
|
|
return true;
|
|
}
|
|
break;
|
|
|
|
case 'v':
|
|
if (std::strcmp(p, "value") == 0) {
|
|
value_directive(arg);
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (expr_t::ptr_op_t op = lookup(symbol_t::DIRECTIVE, p)) {
|
|
call_scope_t args(*this);
|
|
args.push_back(string_value(p));
|
|
op->as_function()(args);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
post_t * instance_t::parse_post(char * line,
|
|
std::streamsize len,
|
|
account_t * account,
|
|
xact_t * xact,
|
|
bool defer_expr)
|
|
{
|
|
TRACE_START(post_details, 1, "Time spent parsing postings:");
|
|
|
|
unique_ptr<post_t> post(new post_t);
|
|
|
|
post->xact = xact; // this could be NULL
|
|
post->pos = position_t();
|
|
post->pos->pathname = context.pathname;
|
|
post->pos->beg_pos = context.line_beg_pos;
|
|
post->pos->beg_line = context.linenum;
|
|
post->pos->sequence = context.sequence++;
|
|
|
|
char buf[parse_context_t::MAX_LINE + 1];
|
|
std::strcpy(buf, line);
|
|
std::streamsize beg = 0;
|
|
|
|
try {
|
|
|
|
// Parse the state flag
|
|
|
|
assert(line);
|
|
assert(*line);
|
|
|
|
char * p = skip_ws(line);
|
|
|
|
switch (*p) {
|
|
case '*':
|
|
post->set_state(item_t::CLEARED);
|
|
p = skip_ws(p + 1);
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "Parsed the CLEARED flag");
|
|
break;
|
|
|
|
case '!':
|
|
post->set_state(item_t::PENDING);
|
|
p = skip_ws(p + 1);
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "Parsed the PENDING flag");
|
|
break;
|
|
}
|
|
|
|
if (xact &&
|
|
((xact->_state == item_t::CLEARED && post->_state != item_t::CLEARED) ||
|
|
(xact->_state == item_t::PENDING && post->_state == item_t::UNCLEARED)))
|
|
post->set_state(xact->_state);
|
|
|
|
// Parse the account name
|
|
|
|
if (! *p)
|
|
throw parse_error(_("Posting has no account"));
|
|
|
|
char * next = next_element(p, true);
|
|
char * e = p + std::strlen(p);
|
|
|
|
while (e > p && std::isspace(*(e - 1)))
|
|
e--;
|
|
|
|
if ((*p == '[' && *(e - 1) == ']') || (*p == '(' && *(e - 1) == ')')) {
|
|
post->add_flags(POST_VIRTUAL);
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "Parsed a virtual account name");
|
|
|
|
if (*p == '[') {
|
|
post->add_flags(POST_MUST_BALANCE);
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "Posting must balance");
|
|
}
|
|
p++; e--;
|
|
}
|
|
else if (*p == '<' && *(e - 1) == '>') {
|
|
post->add_flags(POST_DEFERRED);
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "Parsed a deferred account name");
|
|
p++; e--;
|
|
}
|
|
|
|
string name(p, static_cast<string::size_type>(e - p));
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "Parsed account name " << name);
|
|
|
|
post->account =
|
|
context.journal->register_account(name, post.get(), account);
|
|
|
|
// Parse the optional amount
|
|
|
|
if (next && *next && (*next != ';' && *next != '=')) {
|
|
beg = static_cast<std::streamsize>(next - line);
|
|
ptristream stream(next, static_cast<std::size_t>(len - beg));
|
|
|
|
if (*next != '(') // indicates a value expression
|
|
post->amount.parse(stream, PARSE_NO_REDUCE);
|
|
else
|
|
parse_amount_expr(stream, *context.scope, *post.get(), post->amount,
|
|
PARSE_NO_REDUCE | PARSE_SINGLE | PARSE_NO_ASSIGN,
|
|
defer_expr, &post->amount_expr);
|
|
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "post amount = " << post->amount);
|
|
|
|
if (! post->amount.is_null() && post->amount.has_commodity()) {
|
|
context.journal->register_commodity(post->amount.commodity(), post.get());
|
|
|
|
if (! post->amount.has_annotation()) {
|
|
std::vector<fixed_rate_t> rates;
|
|
get_applications<fixed_rate_t>(rates);
|
|
foreach (fixed_rate_t& rate, rates) {
|
|
if (*rate.first == post->amount.commodity()) {
|
|
annotation_t details(rate.second);
|
|
details.add_flags(ANNOTATION_PRICE_FIXATED);
|
|
post->amount.annotate(details);
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "applied rate = " << post->amount);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (stream.eof()) {
|
|
next = NULL;
|
|
} else {
|
|
next = skip_ws(next + static_cast<std::ptrdiff_t>(stream.tellg()));
|
|
|
|
// Parse the optional cost (@ PER-UNIT-COST, @@ TOTAL-COST)
|
|
|
|
if (*next == '@' || (*next == '(' && *(next + 1) == '@')) {
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "Found a price indicator");
|
|
|
|
if (*next == '(') {
|
|
post->add_flags(POST_COST_VIRTUAL);
|
|
++next;
|
|
}
|
|
|
|
bool per_unit = true;
|
|
if (*++next == '@') {
|
|
per_unit = false;
|
|
post->add_flags(POST_COST_IN_FULL);
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "And it's for a total price");
|
|
}
|
|
|
|
if (post->has_flags(POST_COST_VIRTUAL) && *(next + 1) == ')')
|
|
++next;
|
|
|
|
beg = static_cast<std::streamsize>(++next - line);
|
|
|
|
p = skip_ws(next);
|
|
if (*p) {
|
|
post->cost = amount_t();
|
|
|
|
bool fixed_cost = false;
|
|
if (*p == '=') {
|
|
p++;
|
|
fixed_cost = true;
|
|
if (*p == '\0')
|
|
throw parse_error(_("Posting is missing a cost amount"));
|
|
}
|
|
|
|
beg = static_cast<std::streamsize>(p - line);
|
|
ptristream cstream(p, static_cast<std::size_t>(len - beg));
|
|
|
|
if (*p != '(') // indicates a value expression
|
|
post->cost->parse(cstream, PARSE_NO_MIGRATE);
|
|
else
|
|
parse_amount_expr(cstream, *context.scope, *post.get(), *post->cost,
|
|
PARSE_NO_MIGRATE | PARSE_SINGLE | PARSE_NO_ASSIGN);
|
|
|
|
if (post->cost->sign() < 0)
|
|
throw parse_error(_("A posting's cost may not be negative"));
|
|
|
|
post->cost->in_place_unround();
|
|
|
|
if (per_unit) {
|
|
// For the sole case where the cost might be uncommoditized,
|
|
// guarantee that the commodity of the cost after multiplication
|
|
// is the same as it was before.
|
|
commodity_t& cost_commodity(post->cost->commodity());
|
|
*post->cost *= post->amount;
|
|
post->cost->set_commodity(cost_commodity);
|
|
}
|
|
else if (post->amount.sign() < 0) {
|
|
post->cost->in_place_negate();
|
|
}
|
|
|
|
if (fixed_cost)
|
|
post->add_flags(POST_COST_FIXATED);
|
|
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "Total cost is " << *post->cost);
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "Annotated amount is " << post->amount);
|
|
|
|
if (cstream.eof())
|
|
next = NULL;
|
|
else
|
|
next = skip_ws(p + static_cast<std::ptrdiff_t>(cstream.tellg()));
|
|
} else {
|
|
throw parse_error(_("Expected a cost amount"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse the optional balance assignment
|
|
|
|
if (xact && next && *next == '=') {
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "Found a balance assignment indicator");
|
|
|
|
beg = static_cast<std::streamsize>(++next - line);
|
|
|
|
p = skip_ws(next);
|
|
if (*p) {
|
|
post->assigned_amount = amount_t();
|
|
|
|
beg = static_cast<std::streamsize>(p - line);
|
|
ptristream stream(p, static_cast<std::size_t>(len - beg));
|
|
|
|
if (*p != '(') // indicates a value expression
|
|
post->assigned_amount->parse(stream, PARSE_NO_MIGRATE);
|
|
else
|
|
parse_amount_expr(stream, *context.scope, *post.get(),
|
|
*post->assigned_amount,
|
|
PARSE_SINGLE | PARSE_NO_MIGRATE);
|
|
|
|
if (post->assigned_amount->is_null()) {
|
|
if (post->amount.is_null())
|
|
throw parse_error(_("Balance assignment must evaluate to a constant"));
|
|
else
|
|
throw parse_error(_("Balance assertion must evaluate to a constant"));
|
|
}
|
|
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "POST assign: parsed amt = " << *post->assigned_amount);
|
|
|
|
amount_t& amt(*post->assigned_amount);
|
|
value_t account_total
|
|
(post->account->amount().strip_annotations(keep_details_t()));
|
|
|
|
DEBUG("post.assign", "line " << context.linenum << ": "
|
|
<< "account balance = " << account_total);
|
|
DEBUG("post.assign",
|
|
"line " << context.linenum << ": " << "post amount = " << amt);
|
|
|
|
amount_t diff = amt;
|
|
amount_t tot;
|
|
|
|
switch (account_total.type()) {
|
|
case value_t::AMOUNT:
|
|
tot = account_total.as_amount();
|
|
break;
|
|
|
|
case value_t::BALANCE:
|
|
if (optional<amount_t> comm_bal =
|
|
account_total.as_balance().commodity_amount(amt.commodity()))
|
|
tot = *comm_bal;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
diff -= tot;
|
|
|
|
DEBUG("post.assign",
|
|
"line " << context.linenum << ": " << "diff = " << diff);
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "POST assign: diff = " << diff);
|
|
|
|
if (! diff.is_zero()) {
|
|
if (! post->amount.is_null()) {
|
|
diff -= post->amount;
|
|
if (! no_assertions && ! diff.is_zero())
|
|
throw_(parse_error,
|
|
_f("Balance assertion off by %1% (expected to see %2%)")
|
|
% diff % tot);
|
|
} else {
|
|
post->amount = diff;
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "Overwrite null posting");
|
|
}
|
|
}
|
|
|
|
if (stream.eof())
|
|
next = NULL;
|
|
else
|
|
next = skip_ws(p + static_cast<std::ptrdiff_t>(stream.tellg()));
|
|
} else {
|
|
throw parse_error(_("Expected an balance assignment/assertion amount"));
|
|
}
|
|
}
|
|
|
|
// Parse the optional note
|
|
|
|
if (next && *next == ';') {
|
|
post->append_note(++next, *context.scope, true);
|
|
next = line + len;
|
|
DEBUG("textual.parse", "line " << context.linenum << ": "
|
|
<< "Parsed a posting note");
|
|
}
|
|
|
|
// There should be nothing more to read
|
|
|
|
if (next && *next)
|
|
throw_(parse_error,
|
|
_f("Unexpected char '%1%' (Note: inline math requires parentheses)")
|
|
% *next);
|
|
|
|
post->pos->end_pos = context.curr_pos;
|
|
post->pos->end_line = context.linenum;
|
|
|
|
std::vector<string> tags;
|
|
get_applications<string>(tags);
|
|
foreach (string& tag, tags)
|
|
post->parse_tags(tag.c_str(), *context.scope, true);
|
|
|
|
TRACE_STOP(post_details, 1);
|
|
|
|
return post.release();
|
|
|
|
}
|
|
catch (const std::exception&) {
|
|
add_error_context(_("While parsing posting:"));
|
|
add_error_context(line_context(buf, static_cast<string::size_type>(beg),
|
|
static_cast<string::size_type>(len)));
|
|
throw;
|
|
}
|
|
}
|
|
|
|
bool instance_t::parse_posts(account_t * account,
|
|
xact_base_t& xact,
|
|
const bool defer_expr)
|
|
{
|
|
TRACE_START(xact_posts, 1, "Time spent parsing postings:");
|
|
|
|
bool added = false;
|
|
|
|
while (peek_whitespace_line()) {
|
|
char * line;
|
|
std::streamsize len = read_line(line);
|
|
char * p = skip_ws(line);
|
|
if (*p != ';') {
|
|
if (post_t * post = parse_post(line, len, account, NULL, defer_expr)) {
|
|
xact.add_post(post);
|
|
added = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
TRACE_STOP(xact_posts, 1);
|
|
|
|
return added;
|
|
}
|
|
|
|
xact_t * instance_t::parse_xact(char * line,
|
|
std::streamsize len,
|
|
account_t * account)
|
|
{
|
|
TRACE_START(xact_text, 1, "Time spent parsing transaction text:");
|
|
|
|
unique_ptr<xact_t> xact(new xact_t);
|
|
|
|
xact->pos = position_t();
|
|
xact->pos->pathname = context.pathname;
|
|
xact->pos->beg_pos = context.line_beg_pos;
|
|
xact->pos->beg_line = context.linenum;
|
|
xact->pos->sequence = context.sequence++;
|
|
|
|
bool reveal_context = true;
|
|
|
|
try {
|
|
|
|
// Parse the date
|
|
|
|
char * next = next_element(line);
|
|
|
|
if (char * p = std::strchr(line, '=')) {
|
|
*p++ = '\0';
|
|
xact->_date_aux = parse_date(p);
|
|
}
|
|
xact->_date = parse_date(line);
|
|
|
|
// Parse the optional cleared flag: *
|
|
|
|
if (next) {
|
|
switch (*next) {
|
|
case '*':
|
|
xact->_state = item_t::CLEARED;
|
|
next = skip_ws(++next);
|
|
break;
|
|
case '!':
|
|
xact->_state = item_t::PENDING;
|
|
next = skip_ws(++next);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Parse the optional code: (TEXT)
|
|
|
|
if (next && *next == '(') {
|
|
if (char * p = std::strchr(next++, ')')) {
|
|
*p++ = '\0';
|
|
xact->code = next;
|
|
next = skip_ws(p);
|
|
}
|
|
}
|
|
|
|
// Parse the description text
|
|
|
|
if (next && *next) {
|
|
char * p = next;
|
|
std::size_t spaces = 0;
|
|
std::size_t tabs = 0;
|
|
while (*p) {
|
|
if (*p == ' ') {
|
|
++spaces;
|
|
}
|
|
else if (*p == '\t') {
|
|
++tabs;
|
|
}
|
|
else if (*p == ';' && (tabs > 0 || spaces > 1)) {
|
|
char *q = p - 1;
|
|
while (q > next && std::isspace(*q))
|
|
--q;
|
|
if (q > next)
|
|
*(q + 1) = '\0';
|
|
break;
|
|
}
|
|
else {
|
|
spaces = 0;
|
|
tabs = 0;
|
|
}
|
|
++p;
|
|
}
|
|
xact->payee = context.journal->register_payee(next, xact.get());
|
|
next = p;
|
|
} else {
|
|
xact->payee = _("<Unspecified payee>");
|
|
}
|
|
|
|
// Parse the xact note
|
|
|
|
if (next && *next == ';')
|
|
xact->append_note(++next, *context.scope, false);
|
|
|
|
TRACE_STOP(xact_text, 1);
|
|
|
|
// Parse all of the posts associated with this xact
|
|
|
|
TRACE_START(xact_details, 1, "Time spent parsing transaction details:");
|
|
|
|
post_t * last_post = NULL;
|
|
|
|
while (peek_whitespace_line()) {
|
|
len = read_line(line);
|
|
char * p = skip_ws(line);
|
|
if (! *p)
|
|
break;
|
|
|
|
const std::size_t remlen = std::strlen(p);
|
|
|
|
item_t * item;
|
|
if (last_post)
|
|
item = last_post;
|
|
else
|
|
item = xact.get();
|
|
|
|
if (*p == ';') {
|
|
// This is a trailing note, and possibly a metadata info tag
|
|
item->append_note(p + 1, *context.scope, true);
|
|
item->add_flags(ITEM_NOTE_ON_NEXT_LINE);
|
|
item->pos->end_pos = context.curr_pos;
|
|
item->pos->end_line++;
|
|
}
|
|
else if ((remlen > 7 && *p == 'a' &&
|
|
std::strncmp(p, "assert", 6) == 0 && std::isspace(p[6])) ||
|
|
(remlen > 6 && *p == 'c' &&
|
|
std::strncmp(p, "check", 5) == 0 && std::isspace(p[5])) ||
|
|
(remlen > 5 && *p == 'e' &&
|
|
std::strncmp(p, "expr", 4) == 0 && std::isspace(p[4]))) {
|
|
const char c = *p;
|
|
p = skip_ws(&p[*p == 'a' ? 6 : (*p == 'c' ? 5 : 4)]);
|
|
expr_t expr(p);
|
|
bind_scope_t bound_scope(*context.scope, *item);
|
|
if (c == 'e') {
|
|
expr.calc(bound_scope);
|
|
}
|
|
else if (! expr.calc(bound_scope).to_boolean()) {
|
|
if (c == 'a') {
|
|
throw_(parse_error, _f("Transaction assertion failed: %1%") % p);
|
|
} else {
|
|
context.warning(_f("Transaction check failed: %1%") % p);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
reveal_context = false;
|
|
|
|
if (!last_post) {
|
|
if (xact->has_tag(_("UUID"))) {
|
|
string uuid = xact->get_tag(_("UUID"))->to_string();
|
|
foreach (payee_uuid_mapping_t value, context.journal->payee_uuid_mappings) {
|
|
if (value.first.compare(uuid) == 0) {
|
|
xact->payee = value.second;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (post_t * post =
|
|
parse_post(p, len - (p - line), account, xact.get())) {
|
|
reveal_context = true;
|
|
xact->add_post(post);
|
|
last_post = post;
|
|
}
|
|
reveal_context = true;
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
if (xact->_state == item_t::UNCLEARED) {
|
|
item_t::application_t result = item_t::CLEARED;
|
|
|
|
foreach (post_t * post, xact->posts) {
|
|
if (post->_state == item_t::UNCLEARED) {
|
|
result = item_t::UNCLEARED;
|
|
break;
|
|
}
|
|
else if (post->_state == item_t::PENDING) {
|
|
result = item_t::PENDING;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
xact->pos->end_pos = context.curr_pos;
|
|
xact->pos->end_line = context.linenum;
|
|
|
|
std::vector<string> tags;
|
|
get_applications<string>(tags);
|
|
foreach (string& tag, tags)
|
|
xact->parse_tags(tag.c_str(), *context.scope, false);
|
|
|
|
TRACE_STOP(xact_details, 1);
|
|
|
|
return xact.release();
|
|
|
|
}
|
|
catch (const std::exception&) {
|
|
if (reveal_context) {
|
|
add_error_context(_("While parsing transaction:"));
|
|
add_error_context(source_context(xact->pos->pathname,
|
|
xact->pos->beg_pos,
|
|
context.curr_pos, "> "));
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
expr_t::ptr_op_t instance_t::lookup(const symbol_t::kind_t kind,
|
|
const string& name)
|
|
{
|
|
return context.scope->lookup(kind, name);
|
|
}
|
|
|
|
std::size_t journal_t::read_textual(parse_context_stack_t& context_stack)
|
|
{
|
|
TRACE_START(parsing_total, 1, "Total time spent parsing text:");
|
|
{
|
|
instance_t instance(context_stack, context_stack.get_current(), NULL,
|
|
checking_style == journal_t::CHECK_PERMISSIVE);
|
|
instance.apply_stack.push_front
|
|
(application_t("account", context_stack.get_current().master));
|
|
instance.parse();
|
|
}
|
|
TRACE_STOP(parsing_total, 1);
|
|
|
|
// Apply any deferred postings at this time
|
|
master->apply_deferred_posts();
|
|
|
|
// These tracers were started in textual.cc
|
|
TRACE_FINISH(xact_text, 1);
|
|
TRACE_FINISH(xact_details, 1);
|
|
TRACE_FINISH(xact_posts, 1);
|
|
TRACE_FINISH(xacts, 1);
|
|
TRACE_FINISH(instance_parse, 1); // report per-instance timers
|
|
TRACE_FINISH(parsing_total, 1);
|
|
|
|
if (context_stack.get_current().errors > 0)
|
|
throw error_count(context_stack.get_current().errors);
|
|
|
|
return context_stack.get_current().count;
|
|
}
|
|
|
|
} // namespace ledger
|