mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
2081 lines
56 KiB
C++
2081 lines
56 KiB
C++
////////////////////////////////////////////////////////////////////////////////
|
|
// taskwarrior - a command line task list manager.
|
|
//
|
|
// Copyright 2006-2011, Paul Beckingham, Federico Hernandez.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included
|
|
// in all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
//
|
|
// http://www.opensource.org/licenses/mit-license.php
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#define L10N // Localization complete.
|
|
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <algorithm>
|
|
#include <stdlib.h>
|
|
#include <sys/select.h>
|
|
#include <Context.h>
|
|
#include <Directory.h>
|
|
#include <Date.h>
|
|
#include <Duration.h>
|
|
#include <ViewText.h>
|
|
#include <text.h>
|
|
#include <util.h>
|
|
#include <i18n.h>
|
|
#include <A3.h>
|
|
|
|
extern Context context;
|
|
|
|
// Supported modifiers, synonyms on the same line.
|
|
static const char* modifierNames[] =
|
|
{
|
|
"before", "under", "below",
|
|
"after", "over", "above",
|
|
"none",
|
|
"any",
|
|
"is", "equals",
|
|
"isnt", "not",
|
|
"has", "contains",
|
|
"hasnt",
|
|
"startswith", "left",
|
|
"endswith", "right",
|
|
"word",
|
|
"noword"
|
|
};
|
|
|
|
// Supported operators, borrowed from C++, particularly the precedence.
|
|
// Note: table is sorted by length of operator string, so searches match
|
|
// longest first.
|
|
static struct
|
|
{
|
|
std::string op;
|
|
int precedence;
|
|
char type;
|
|
int symbol;
|
|
char associativity;
|
|
} operators[] =
|
|
{
|
|
// Operator Precedence Type Symbol Associativity
|
|
{ "and", 5, 'b', 0, 'l' }, // Conjunction
|
|
{ "xor", 4, 'b', 0, 'l' }, // Disjunction
|
|
|
|
{ "or", 3, 'b', 0, 'l' }, // Disjunction
|
|
{ "<=", 10, 'b', 1, 'l' }, // Less than or equal
|
|
{ ">=", 10, 'b', 1, 'l' }, // Greater than or equal
|
|
{ "!~", 9, 'b', 1, 'l' }, // Regex non-match
|
|
{ "!=", 9, 'b', 1, 'l' }, // Inequal
|
|
|
|
{ "=", 9, 'b', 1, 'l' }, // Equal
|
|
// { "^", 16, 'b', 1, 'r' }, // Exponent
|
|
{ ">", 10, 'b', 1, 'l' }, // Greater than
|
|
{ "~", 9, 'b', 1, 'l' }, // Regex match
|
|
{ "!", 15, 'u', 1, 'r' }, // Not
|
|
// { "-", 15, 'u', 1, 'r' }, // Unary minus
|
|
{ "*", 13, 'b', 1, 'l' }, // Multiplication
|
|
{ "/", 13, 'b', 1, 'l' }, // Division
|
|
// { "%", 13, 'b', 1, 'l' }, // Modulus
|
|
{ "+", 12, 'b', 1, 'l' }, // Addition
|
|
{ "-", 12, 'b', 1, 'l' }, // Subtraction
|
|
{ "<", 10, 'b', 1, 'l' }, // Less than
|
|
{ "(", 0, 'b', 1, 'l' }, // Precedence start
|
|
{ ")", 0, 'b', 1, 'l' }, // Precedence end
|
|
};
|
|
|
|
#define NUM_MODIFIER_NAMES (sizeof (modifierNames) / sizeof (modifierNames[0]))
|
|
#define NUM_OPERATORS (sizeof (operators) / sizeof (operators[0]))
|
|
|
|
//static const char* non_word_chars = " +-*/%()=<>!~";
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
A3::A3 ()
|
|
: _read_only_command (true)
|
|
, _limit ("")
|
|
{
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
A3::A3 (const A3& other)
|
|
{
|
|
std::vector <Arg>::operator= (other);
|
|
_read_only_command = other._read_only_command;
|
|
_limit = other._limit;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
A3& A3::operator= (const A3& other)
|
|
{
|
|
std::vector <Arg>::operator= (other);
|
|
_read_only_command = other._read_only_command;
|
|
_limit = other._limit;
|
|
|
|
return *this;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
A3::~A3 ()
|
|
{
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Add an Arg with a blank category for every argv.
|
|
void A3::capture (int argc, const char** argv)
|
|
{
|
|
for (int i = 0; i < argc; ++i)
|
|
this->push_back (Arg (argv[i]));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Append an Arg with a blank category.
|
|
void A3::capture (const std::string& arg)
|
|
{
|
|
std::vector <std::string> parts;
|
|
this->push_back (Arg (arg));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Prepend a Arg with a blank category.
|
|
void A3::capture_first (const std::string& arg)
|
|
{
|
|
// Break the new argument into parts that comprise a series.
|
|
std::vector <Arg> series;
|
|
|
|
std::vector <std::string> separated;
|
|
splitq (separated, arg, ' ');
|
|
std::vector <std::string>::iterator sep;
|
|
for (sep = separated.begin (); sep != separated.end (); ++sep)
|
|
series.push_back (Arg (*sep));
|
|
|
|
// Locate an appropriate place to insert the series. This would be
|
|
// immediately after the program and command arguments.
|
|
std::vector <Arg>::iterator position;
|
|
for (position = this->begin (); position != this->end (); ++position)
|
|
if (position->_category != Arg::cat_program &&
|
|
position->_category != Arg::cat_command)
|
|
break;
|
|
|
|
this->insert (position, series.begin (), series.end ());
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Scan all arguments and categorize them as:
|
|
// program
|
|
// rc
|
|
// override
|
|
// command
|
|
// terminator
|
|
// word
|
|
//
|
|
void A3::categorize ()
|
|
{
|
|
bool terminated = false;
|
|
bool found_command = false;
|
|
|
|
// Generate a vector of command keywords against which autoComplete can run.
|
|
std::vector <std::string> keywords = context.getCommands ();
|
|
|
|
// Now categorize every argument.
|
|
std::vector <Arg>::iterator arg;
|
|
for (arg = this->begin (); arg != this->end (); ++arg)
|
|
{
|
|
if (!terminated)
|
|
{
|
|
// Nothing after -- is to be interpreted in any way.
|
|
if (arg->_raw == "--")
|
|
{
|
|
terminated = true;
|
|
arg->_category = Arg::cat_terminator;
|
|
}
|
|
|
|
// program
|
|
else if (arg == this->begin ())
|
|
{
|
|
arg->_category = Arg::cat_program;
|
|
|
|
if ((arg->_raw.length () >= 3 &&
|
|
arg->_raw.substr (arg->_raw.length () - 3) == "cal") ||
|
|
(arg->_raw.length () >= 8 &&
|
|
arg->_raw.substr (arg->_raw.length () - 8) == "calendar"))
|
|
{
|
|
arg->_raw = "calendar";
|
|
arg->_category = Arg::cat_command;
|
|
found_command = true;
|
|
}
|
|
|
|
// Context needs a copy.
|
|
context.program = arg->_raw;
|
|
}
|
|
|
|
// command
|
|
else if (!found_command &&
|
|
is_command (keywords, arg->_raw))
|
|
{
|
|
found_command = true;
|
|
arg->_category = Arg::cat_command;
|
|
_read_only_command = context.commands[arg->_raw]->read_only ();
|
|
}
|
|
|
|
// rc:<file>
|
|
// Note: This doesn't break a sequence chain.
|
|
else if (arg->_raw.substr (0, 3) == "rc:")
|
|
arg->_category = Arg::cat_rc;
|
|
|
|
// rc.<name>:<value>
|
|
// Note: This doesn't break a sequence chain.
|
|
else if (arg->_raw.substr (0, 3) == "rc.")
|
|
arg->_category = Arg::cat_override;
|
|
|
|
// If the type is not known, it is treated as a generic word.
|
|
}
|
|
|
|
// All post-termination arguments are simply words.
|
|
else
|
|
arg->_category = Arg::cat_literal;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
bool A3::is_command (
|
|
const std::vector <std::string>& keywords,
|
|
std::string& command)
|
|
{
|
|
std::vector <std::string> matches;
|
|
if (autoComplete (command,
|
|
keywords,
|
|
matches,
|
|
context.config.getInteger ("abbreviation.minimum")) == 1)
|
|
{
|
|
command = matches[0];
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Add an Arg for every word from std::cin.
|
|
void A3::append_stdin ()
|
|
{
|
|
// Use 'select' to determine whether there is any std::cin content buffered
|
|
// before trying to read it, to prevent blocking.
|
|
struct timeval tv;
|
|
fd_set fds;
|
|
tv.tv_sec = 0;
|
|
tv.tv_usec = 0;
|
|
FD_ZERO (&fds);
|
|
FD_SET (STDIN_FILENO, &fds);
|
|
select (STDIN_FILENO + 1, &fds, NULL, NULL, &tv);
|
|
if (FD_ISSET (0, &fds))
|
|
{
|
|
std::string arg;
|
|
while (std::cin >> arg)
|
|
{
|
|
// It the terminator token is found, stop reading.
|
|
if (arg == "--")
|
|
break;
|
|
|
|
this->push_back (Arg (arg));
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
void A3::rc_override (
|
|
std::string& home,
|
|
File& rc)
|
|
{
|
|
// Is there an override for rc:<file>?
|
|
std::vector <Arg>::iterator arg;
|
|
for (arg = this->begin (); arg != this->end (); ++arg)
|
|
{
|
|
if (arg->_category == Arg::cat_rc)
|
|
{
|
|
rc = File (arg->_raw.substr (3));
|
|
home = rc;
|
|
|
|
std::string::size_type last_slash = rc._data.rfind ("/");
|
|
if (last_slash != std::string::npos)
|
|
home = rc._data.substr (0, last_slash);
|
|
else
|
|
home = ".";
|
|
|
|
context.header (format (STRING_A3_ALTERNATE_RC, rc._data));
|
|
|
|
// Keep looping, because if there are multiple rc:file arguments, we
|
|
// want the last one to dominate.
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
void A3::get_data_location (std::string& data)
|
|
{
|
|
std::string location = context.config.get ("data.location");
|
|
if (location != "")
|
|
data = location;
|
|
|
|
// Are there any overrides for data.location?
|
|
std::vector <Arg>::iterator arg;
|
|
for (arg = this->begin (); arg != this->end (); ++arg)
|
|
{
|
|
if (arg->_category == Arg::cat_override)
|
|
{
|
|
if (arg->_raw.substr (0, 16) == "rc.data.location" &&
|
|
(arg->_raw[16] == ':' || arg->_raw[16] == '='))
|
|
{
|
|
data = arg->_raw.substr (17);
|
|
context.header (format (STRING_A3_ALTERNATE_DATA, data));
|
|
}
|
|
}
|
|
|
|
// Keep scanning, because if there are multiple overrides, we want the last
|
|
// one to dominate.
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// An alias must be a distinct word on the command line.
|
|
// Aliases may not recurse.
|
|
void A3::resolve_aliases ()
|
|
{
|
|
std::vector <std::string> expanded;
|
|
bool something;
|
|
int safety_valve = 10;
|
|
|
|
do
|
|
{
|
|
something = false;
|
|
std::vector <Arg>::iterator arg;
|
|
for (arg = this->begin (); arg != this->end (); ++arg)
|
|
{
|
|
std::map <std::string, std::string>::iterator match =
|
|
context.aliases.find (arg->_raw);
|
|
|
|
if (match != context.aliases.end ())
|
|
{
|
|
context.debug (std::string ("A3::resolve_aliases '")
|
|
+ arg->_raw
|
|
+ "' --> '"
|
|
+ context.aliases[arg->_raw]
|
|
+ "'");
|
|
|
|
std::vector <std::string> words;
|
|
splitq (words, context.aliases[arg->_raw], ' ');
|
|
|
|
std::vector <std::string>::iterator word;
|
|
for (word = words.begin (); word != words.end (); ++word)
|
|
expanded.push_back (*word);
|
|
|
|
something = true;
|
|
}
|
|
else
|
|
expanded.push_back (arg->_raw);
|
|
}
|
|
|
|
// Only overwrite if something happened.
|
|
if (something)
|
|
{
|
|
this->clear ();
|
|
std::vector <std::string>::iterator e;
|
|
for (e = expanded.begin (); e != expanded.end (); ++e)
|
|
this->push_back (Arg (*e));
|
|
|
|
expanded.clear ();
|
|
}
|
|
}
|
|
while (something && --safety_valve > 0);
|
|
|
|
if (safety_valve <= 0)
|
|
context.debug ("Nested alias limit of 10 reached.");
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Extracts any rc.name:value args and sets the name/value in context.config,
|
|
// leaving only the plain args.
|
|
void A3::apply_overrides ()
|
|
{
|
|
std::vector <Arg>::iterator arg;
|
|
for (arg = this->begin (); arg != this->end (); ++arg)
|
|
{
|
|
if (arg->_category == Arg::cat_override)
|
|
{
|
|
std::string name;
|
|
std::string value;
|
|
Nibbler n (arg->_raw);
|
|
if (n.getLiteral ("rc.") && // rc.
|
|
n.getUntilOneOf (":=", name) && // xxx
|
|
(n.skip (':') || n.skip ('='))) // [:=]
|
|
{
|
|
n.getUntilEOS (value); // May be blank.
|
|
|
|
context.config.set (name, value);
|
|
context.footnote (format (STRING_A3_OVERRIDE_RC, name, value));
|
|
}
|
|
else
|
|
context.footnote (format (STRING_A3_OVERRIDE_PROBLEM, arg->_raw));
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// These are some delicate heuristics here. Tread lightly.
|
|
void A3::inject_defaults ()
|
|
{
|
|
// Scan the arguments and detect what is present.
|
|
bool found_command = false;
|
|
bool found_sequence = false;
|
|
bool found_other = false;
|
|
|
|
std::vector <Arg>::iterator arg;
|
|
for (arg = this->begin (); arg != this->end (); ++arg)
|
|
{
|
|
if (arg->_category == Arg::cat_command)
|
|
found_command = true;
|
|
|
|
/* TODO no "id" or "uuid" categories exist at this time. Hmm.
|
|
else if (arg->_category == Arg::cat_id ||
|
|
arg->_category == Arg::cat_uuid)
|
|
found_sequence = true;
|
|
*/
|
|
|
|
else if (arg->_category != Arg::cat_program &&
|
|
arg->_category != Arg::cat_override &&
|
|
arg->_category != Arg::cat_rc)
|
|
found_other = true;
|
|
}
|
|
|
|
// If no command was specified, then a command will be inserted.
|
|
if (!found_command)
|
|
{
|
|
// Default command.
|
|
if (!found_sequence)
|
|
{
|
|
// Apply overrides, if any.
|
|
std::string defaultCommand = context.config.get ("default.command");
|
|
if (defaultCommand != "")
|
|
{
|
|
context.debug ("No command or sequence found - assuming default.command.");
|
|
capture_first (defaultCommand);
|
|
context.header ("[" + combine () + "]");
|
|
}
|
|
else
|
|
throw std::string (STRING_TRIVIAL_INPUT);
|
|
}
|
|
else
|
|
{
|
|
// Modify command.
|
|
if (found_other)
|
|
{
|
|
context.debug ("Sequence and filter, but no command found - assuming 'modify' command.");
|
|
capture_first ("modify");
|
|
}
|
|
|
|
// Information command.
|
|
else
|
|
{
|
|
context.debug ("Sequence but no command found - assuming 'information' command.");
|
|
context.header (STRING_ASSUME_INFO);
|
|
capture_first ("information");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
const std::string A3::combine () const
|
|
{
|
|
std::string combined;
|
|
|
|
std::vector <Arg>::const_iterator arg;
|
|
for (arg = this->begin (); arg != this->end (); ++arg)
|
|
{
|
|
if (arg != this->begin ())
|
|
combined += " ";
|
|
|
|
combined += arg->_raw;
|
|
}
|
|
|
|
return combined;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
const std::vector <std::string> A3::list () const
|
|
{
|
|
std::vector <std::string> all;
|
|
std::vector <Arg>::const_iterator arg;
|
|
for (arg = this->begin (); arg != this->end (); ++arg)
|
|
all.push_back (arg->_raw);
|
|
|
|
return all;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
bool A3::find_command (std::string& command) const
|
|
{
|
|
std::vector <Arg>::const_iterator arg;
|
|
for (arg = this->begin (); arg != this->end (); ++arg)
|
|
{
|
|
if (arg->_category == Arg::cat_command)
|
|
{
|
|
command = arg->_raw;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
const std::string A3::find_limit () const
|
|
{
|
|
return _limit;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
const std::vector <std::string> A3::operator_list ()
|
|
{
|
|
std::vector <std::string> all;
|
|
for (unsigned int i = 0; i < NUM_OPERATORS; ++i)
|
|
all.push_back (operators[i].op);
|
|
|
|
return all;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
const A3 A3::extract_filter () const
|
|
{
|
|
A3 filter;
|
|
bool before_command = true;
|
|
std::vector <Arg>::const_iterator arg;
|
|
for (arg = this->begin (); arg != this->end (); ++arg)
|
|
{
|
|
if (arg->_category == Arg::cat_command)
|
|
before_command = false;
|
|
|
|
if (arg->_category == Arg::cat_program ||
|
|
arg->_category == Arg::cat_rc ||
|
|
arg->_category == Arg::cat_override ||
|
|
arg->_category == Arg::cat_command ||
|
|
arg->_category == Arg::cat_terminator)
|
|
;
|
|
|
|
else if (before_command || _read_only_command)
|
|
filter.push_back (*arg);
|
|
}
|
|
|
|
filter = postfix (infix (sequence (expand (tokenize (filter)))));
|
|
context.a3._limit = filter._limit;
|
|
return filter;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
const A3 A3::extract_modifications () const
|
|
{
|
|
A3 mods;
|
|
mods._limit = _limit;
|
|
|
|
bool before_command = true;
|
|
std::vector <Arg>::const_iterator arg;
|
|
for (arg = this->begin (); arg != this->end (); ++arg)
|
|
{
|
|
if (arg->_category == Arg::cat_command)
|
|
before_command = false;
|
|
|
|
else if (! before_command)
|
|
{
|
|
// rc and override categories are not included.
|
|
if (arg->_category == Arg::cat_rc ||
|
|
arg->_category == Arg::cat_override)
|
|
{
|
|
;
|
|
}
|
|
|
|
// Any arguments identified as "id" or "uuid" are downgraded to "word".
|
|
// This is because any true "id" or "uuid" values have already been
|
|
// removed in the filter. What remains are numeric arguments in command
|
|
// lines that do not otherwise include an id, such as:
|
|
//
|
|
// task add Read the article on page 2
|
|
else if (arg->_category == Arg::cat_id ||
|
|
arg->_category == Arg::cat_uuid)
|
|
{
|
|
Arg downgrade (*arg);
|
|
downgrade._type = Arg::type_string;
|
|
downgrade._category = Arg::cat_literal;
|
|
mods.push_back (downgrade);
|
|
}
|
|
|
|
// Everything else is included.
|
|
else
|
|
mods.push_back (*arg);
|
|
}
|
|
}
|
|
|
|
mods = tokenize (mods);
|
|
context.a3._limit = mods._limit;
|
|
return mods;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
const std::vector <std::string> A3::extract_words () const
|
|
{
|
|
std::vector <std::string> words;
|
|
std::vector <Arg>::const_iterator arg;
|
|
for (arg = this->begin (); arg != this->end (); ++arg)
|
|
{
|
|
if (arg->_category == Arg::cat_program ||
|
|
arg->_category == Arg::cat_rc ||
|
|
arg->_category == Arg::cat_override ||
|
|
arg->_category == Arg::cat_command ||
|
|
arg->_category == Arg::cat_terminator)
|
|
;
|
|
|
|
else
|
|
words.push_back (arg->_raw);
|
|
}
|
|
|
|
return words;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
const A3 A3::tokenize (const A3& input) const
|
|
{
|
|
// Join all the arguments together.
|
|
std::string combined;
|
|
std::vector <Arg>::const_iterator arg;
|
|
for (arg = input.begin (); arg != input.end (); ++arg)
|
|
{
|
|
if (arg != input.begin ())
|
|
combined += " ";
|
|
|
|
combined += arg->_raw;
|
|
}
|
|
|
|
// List of operators for recognition.
|
|
std::vector <std::string> operators = A3::operator_list ();
|
|
|
|
// Date format, for both parsing and rendering.
|
|
std::string date_format = context.config.get ("dateformat");
|
|
|
|
// Nibble them apart.
|
|
A3 output;
|
|
Nibbler n (combined);
|
|
n.skipWS ();
|
|
|
|
// For identifying sequence versus non-sequence.
|
|
bool terminated = false;
|
|
bool found_sequence = false;
|
|
bool found_something_after_sequence = false;
|
|
|
|
std::string s;
|
|
int i;
|
|
double d;
|
|
time_t t;
|
|
while (! n.depleted ())
|
|
{
|
|
if (!terminated)
|
|
{
|
|
Arg new_arg;
|
|
|
|
if (n.getLiteral ("--"))
|
|
terminated = true;
|
|
|
|
else if (n.getQuoted ('"', s, true) ||
|
|
n.getQuoted ('\'', s, true))
|
|
{
|
|
output.push_back (Arg (s, Arg::type_string, Arg::cat_literal));
|
|
if (found_sequence)
|
|
found_something_after_sequence = true;
|
|
}
|
|
|
|
else if (is_subst (n, s))
|
|
{
|
|
output.push_back (Arg (s, Arg::cat_subst));
|
|
if (found_sequence)
|
|
found_something_after_sequence = true;
|
|
}
|
|
|
|
else if (is_pattern (n, s))
|
|
{
|
|
output.push_back (Arg (s, Arg::cat_pattern));
|
|
if (found_sequence)
|
|
found_something_after_sequence = true;
|
|
}
|
|
|
|
// Must be higher than number.
|
|
// Must be higher than operator.
|
|
// Note that Nibbler::getDate does not read durations.
|
|
else if (n.getDate (date_format, t))
|
|
{
|
|
output.push_back (Arg (Date (t).toString (date_format), Arg::type_date, Arg::cat_literal));
|
|
if (found_sequence)
|
|
found_something_after_sequence = true;
|
|
}
|
|
|
|
// Must be higher than number.
|
|
// Must be higher than operator.
|
|
else if (is_duration (n, s))
|
|
{
|
|
output.push_back (Arg (s, Arg::type_duration, Arg::cat_literal));
|
|
if (found_sequence)
|
|
found_something_after_sequence = true;
|
|
}
|
|
|
|
else if (is_tag (n, s))
|
|
{
|
|
output.push_back (Arg (s, Arg::cat_tag));
|
|
if (found_sequence)
|
|
found_something_after_sequence = true;
|
|
}
|
|
|
|
else if (is_operator (operators, n, s))
|
|
{
|
|
output.push_back (Arg (s, Arg::cat_op));
|
|
if (found_sequence)
|
|
found_something_after_sequence = true;
|
|
}
|
|
|
|
else if (is_attr (n, new_arg))
|
|
{
|
|
// The "limit:xxx" attribute is not stored, but the value is retained.
|
|
if (new_arg._raw.length () > 6 &&
|
|
new_arg._raw.substr (0, 6) == "limit:")
|
|
{
|
|
output._limit = new_arg._raw.substr (6);
|
|
}
|
|
else
|
|
{
|
|
output.push_back (new_arg);
|
|
if (found_sequence)
|
|
found_something_after_sequence = true;
|
|
}
|
|
}
|
|
|
|
else if (is_attmod (n, new_arg))
|
|
{
|
|
output.push_back (new_arg);
|
|
if (found_sequence)
|
|
found_something_after_sequence = true;
|
|
}
|
|
|
|
else if (is_dom (n, new_arg))
|
|
{
|
|
output.push_back (new_arg);
|
|
if (found_sequence)
|
|
found_something_after_sequence = true;
|
|
}
|
|
|
|
else if (n.getDateISO (t))
|
|
{
|
|
output.push_back (Arg (Date (t).toISO (), Arg::type_date, Arg::cat_literal));
|
|
if (found_sequence)
|
|
found_something_after_sequence = true;
|
|
}
|
|
|
|
else if (is_id (n, s))
|
|
{
|
|
if (found_something_after_sequence)
|
|
{
|
|
output.push_back (Arg (s, Arg::type_number, Arg::cat_literal));
|
|
}
|
|
else
|
|
{
|
|
output.push_back (Arg (s, Arg::type_number, Arg::cat_id));
|
|
found_sequence = true;
|
|
}
|
|
}
|
|
|
|
else if (is_uuid (n, s))
|
|
{
|
|
if (found_something_after_sequence)
|
|
{
|
|
output.push_back (Arg (s, Arg::type_string, Arg::cat_literal));
|
|
}
|
|
else
|
|
{
|
|
output.push_back (Arg (s, Arg::type_string, Arg::cat_uuid));
|
|
found_sequence = true;
|
|
}
|
|
}
|
|
|
|
else if (is_number (n, d))
|
|
{
|
|
output.push_back (Arg (format (d), Arg::type_number, Arg::cat_literal));
|
|
if (found_sequence)
|
|
found_something_after_sequence = true;
|
|
}
|
|
|
|
else if (is_integer (n, i))
|
|
{
|
|
output.push_back (Arg (format (i), Arg::type_number, Arg::cat_literal));
|
|
if (found_sequence)
|
|
found_something_after_sequence = true;
|
|
}
|
|
|
|
else
|
|
{
|
|
if (! n.getUntilWS (s))
|
|
n.getUntilEOS (s);
|
|
|
|
if (Date::valid (s))
|
|
output.push_back (Arg (s, Arg::type_date, Arg::cat_literal));
|
|
else
|
|
output.push_back (Arg (s, Arg::type_string, Arg::cat_literal));
|
|
|
|
if (found_sequence)
|
|
found_something_after_sequence = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (n.getUntilEOS (s))
|
|
{
|
|
output.push_back (Arg (s, Arg::type_string, Arg::cat_literal));
|
|
if (found_sequence)
|
|
found_something_after_sequence = true;
|
|
}
|
|
}
|
|
|
|
n.skipWS ();
|
|
}
|
|
|
|
output.dump ("A3::tokenize");
|
|
return output;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Insert 'and' operators between adjacent non-operators.
|
|
//
|
|
// ) <non-op> --> ) and <non-op>
|
|
// <non-op> ( --> <non-op> <and> (
|
|
// ) ( --> ) and (
|
|
// <non-op> <non-op> --> <non-op> and <non-op>
|
|
//
|
|
const A3 A3::infix (const A3& input) const
|
|
{
|
|
Arg previous ("?", Arg::cat_op);
|
|
|
|
A3 modified;
|
|
modified._limit = input._limit;
|
|
|
|
std::vector <Arg>::const_iterator arg;
|
|
for (arg = input.begin (); arg != input.end (); ++arg)
|
|
{
|
|
// Old-style filters need 'and' conjunctions.
|
|
if ((previous._category != Arg::cat_op || previous._raw == ")") &&
|
|
(arg->_category != Arg::cat_op || arg->_raw == "("))
|
|
{
|
|
modified.push_back (Arg ("and", Arg::cat_op));
|
|
}
|
|
|
|
// Now insert the adjacent non-operator.
|
|
modified.push_back (*arg);
|
|
previous = *arg;
|
|
}
|
|
|
|
modified.dump ("A3::infix");
|
|
return modified;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
const A3 A3::expand (const A3& input) const
|
|
{
|
|
A3 expanded;
|
|
expanded._limit = input._limit;
|
|
|
|
std::vector <Arg>::const_iterator arg;
|
|
std::vector <Arg>::const_iterator previous = input.begin ();
|
|
for (arg = input.begin (); arg != input.end (); ++arg)
|
|
{
|
|
// When expanded, the value retains the original type.
|
|
Arg::type implied = arg->_type;
|
|
|
|
// name:value --> name = value
|
|
if (arg->_category == Arg::cat_attr)
|
|
{
|
|
std::string name;
|
|
std::string value;
|
|
A3::extract_attr (arg->_raw, name, value);
|
|
|
|
expanded.push_back (Arg (name, Arg::type_string, Arg::cat_dom));
|
|
expanded.push_back (Arg ("=", Arg::cat_op));
|
|
expanded.push_back (Arg (value, implied, Arg::cat_literal));
|
|
}
|
|
|
|
// name.mod:value --> name <op sub mod> value
|
|
else if (arg->_category == Arg::cat_attmod)
|
|
{
|
|
std::string name;
|
|
std::string mod;
|
|
std::string value;
|
|
std::string sense;
|
|
extract_attmod (arg->_raw, name, mod, value, sense);
|
|
|
|
// name.before:value --> name < value
|
|
if (mod == "before" || mod == "under" || mod == "below")
|
|
{
|
|
expanded.push_back (Arg (name, Arg::type_string, Arg::cat_dom));
|
|
expanded.push_back (Arg ("<", Arg::cat_op));
|
|
expanded.push_back (Arg (value, implied, Arg::cat_literal));
|
|
}
|
|
|
|
// name.after:value --> name > value
|
|
else if (mod == "after" || mod == "over" || mod == "above")
|
|
{
|
|
expanded.push_back (Arg (name, Arg::type_string, Arg::cat_dom));
|
|
expanded.push_back (Arg (">", Arg::cat_op));
|
|
expanded.push_back (Arg (value, implied, Arg::cat_literal));
|
|
}
|
|
|
|
// name.none: --> name == ""
|
|
else if (mod == "none")
|
|
{
|
|
expanded.push_back (Arg (name, Arg::type_string, Arg::cat_dom));
|
|
expanded.push_back (Arg ("=", Arg::cat_op));
|
|
expanded.push_back (Arg ("", implied, Arg::cat_none));
|
|
}
|
|
|
|
// name.any: --> name != ""
|
|
else if (mod == "any")
|
|
{
|
|
expanded.push_back (Arg (name, Arg::type_string, Arg::cat_dom));
|
|
expanded.push_back (Arg ("!=", Arg::cat_op));
|
|
expanded.push_back (Arg ("", implied, Arg::cat_none));
|
|
}
|
|
|
|
// name.is:value --> name = value
|
|
else if (mod == "is" || mod == "equals")
|
|
{
|
|
expanded.push_back (Arg (name, Arg::type_string, Arg::cat_dom));
|
|
expanded.push_back (Arg ("=", Arg::cat_op));
|
|
expanded.push_back (Arg (value, implied, Arg::cat_none));
|
|
}
|
|
|
|
// name.isnt:value --> name != value
|
|
else if (mod == "isnt" || mod == "not")
|
|
{
|
|
expanded.push_back (Arg (name, Arg::type_string, Arg::cat_dom));
|
|
expanded.push_back (Arg ("!=", Arg::cat_op));
|
|
expanded.push_back (Arg (value, implied, Arg::cat_none));
|
|
}
|
|
|
|
// name.has:value --> name ~ value
|
|
else if (mod == "has" || mod == "contains")
|
|
{
|
|
expanded.push_back (Arg (name, Arg::type_string, Arg::cat_dom));
|
|
expanded.push_back (Arg ("~", Arg::cat_op));
|
|
expanded.push_back (Arg (value, Arg::type_string, Arg::cat_rx));
|
|
}
|
|
|
|
// name.hasnt:value --> name !~ value
|
|
else if (mod == "hasnt")
|
|
{
|
|
expanded.push_back (Arg (name, Arg::type_string, Arg::cat_dom));
|
|
expanded.push_back (Arg ("!~", Arg::cat_op));
|
|
expanded.push_back (Arg (value, Arg::type_string, Arg::cat_rx));
|
|
}
|
|
|
|
// name.startswith:value --> name ~ ^value
|
|
else if (mod == "startswith" || mod == "left")
|
|
{
|
|
expanded.push_back (Arg (name, Arg::type_string, Arg::cat_dom));
|
|
expanded.push_back (Arg ("~", Arg::cat_op));
|
|
expanded.push_back (Arg ("^" + value, Arg::type_string, Arg::cat_rx));
|
|
}
|
|
|
|
// name.endswith:value --> name ~ value$
|
|
else if (mod == "endswith" || mod == "right")
|
|
{
|
|
expanded.push_back (Arg (name, Arg::type_string, Arg::cat_dom));
|
|
expanded.push_back (Arg ("~", Arg::cat_op));
|
|
expanded.push_back (Arg (value + "$", Arg::type_string, Arg::cat_rx));
|
|
}
|
|
|
|
// name.word:value --> name ~ \bvalue\b
|
|
else if (mod == "word")
|
|
{
|
|
expanded.push_back (Arg (name, Arg::type_string, Arg::cat_dom));
|
|
expanded.push_back (Arg ("~", Arg::cat_op));
|
|
expanded.push_back (Arg ("\\b" + value + "\\b", Arg::type_string, Arg::cat_rx));
|
|
}
|
|
|
|
// name.noword:value --> name !~ \bvalue\n
|
|
else if (mod == "noword")
|
|
{
|
|
expanded.push_back (Arg (name, Arg::type_string, Arg::cat_dom));
|
|
expanded.push_back (Arg ("!~", Arg::cat_op));
|
|
expanded.push_back (Arg ("\\b" + value + "\\b", Arg::type_string, Arg::cat_rx));
|
|
}
|
|
else
|
|
throw format (STRING_A3_UNKNOWN_ATTMOD, mod);
|
|
}
|
|
|
|
// [+-]value --> tags ~/!~ value
|
|
else if (arg->_category == Arg::cat_tag)
|
|
{
|
|
char type;
|
|
std::string value;
|
|
extract_tag (arg->_raw, type, value);
|
|
|
|
expanded.push_back (Arg ("tags", Arg::type_string, Arg::cat_dom));
|
|
expanded.push_back (Arg (type == '+' ? "~" : "!~", Arg::cat_op));
|
|
expanded.push_back (Arg (value, Arg::type_string, Arg::cat_literal));
|
|
}
|
|
|
|
// word --> description ~ word
|
|
// Note: use of previous prevents desc~foo --> desc~desc~foo
|
|
else if (arg->_category == Arg::cat_literal &&
|
|
previous->_category != Arg::cat_op)
|
|
{
|
|
expanded.push_back (Arg ("description", Arg::type_string, Arg::cat_dom));
|
|
expanded.push_back (Arg ("~", Arg::cat_op));
|
|
expanded.push_back (Arg (arg->_raw, Arg::type_string, Arg::cat_literal));
|
|
}
|
|
|
|
// /pattern/ --> description ~ pattern
|
|
else if (arg->_category == Arg::cat_pattern)
|
|
{
|
|
std::string value;
|
|
extract_pattern (arg->_raw, value);
|
|
|
|
expanded.push_back (Arg ("description", Arg::type_string, Arg::cat_dom));
|
|
expanded.push_back (Arg ("~", Arg::cat_op));
|
|
expanded.push_back (Arg (value, Arg::type_string, Arg::cat_rx));
|
|
}
|
|
|
|
// Default --> preserve
|
|
else
|
|
expanded.push_back (*arg);
|
|
|
|
previous = arg;
|
|
}
|
|
|
|
expanded.dump ("A3::expand");
|
|
return expanded;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Convert: 1-3,5 7
|
|
// To: (id=1 or id=2 or id=3 or id=5 or id=7)
|
|
const A3 A3::sequence (const A3& input) const
|
|
{
|
|
A3 sequenced;
|
|
sequenced._limit = input._limit;
|
|
|
|
// Extract all the components of a sequence.
|
|
std::vector <int> ids;
|
|
std::vector <std::string> uuids;
|
|
std::vector <Arg>::const_iterator arg;
|
|
for (arg = input.begin (); arg != input.end (); ++arg)
|
|
{
|
|
if (arg->_category == Arg::cat_id)
|
|
extract_id (arg->_raw, ids);
|
|
|
|
else if (arg->_category == Arg::cat_uuid)
|
|
extract_uuid (arg->_raw, uuids);
|
|
}
|
|
|
|
// If there is no sequence, we're done.
|
|
if (ids.size () == 0 && uuids.size () == 0)
|
|
return input;
|
|
|
|
// Copy everything up to the first id/uuid.
|
|
for (arg = input.begin (); arg != input.end (); ++arg)
|
|
{
|
|
if (arg->_category == Arg::cat_id || arg->_category == Arg::cat_uuid)
|
|
break;
|
|
|
|
sequenced.push_back (*arg);
|
|
}
|
|
|
|
// Insert the algebraic form.
|
|
sequenced.push_back (Arg ("(", Arg::cat_op));
|
|
|
|
for (unsigned int i = 0; i < ids.size (); ++i)
|
|
{
|
|
if (i)
|
|
sequenced.push_back (Arg ("or", Arg::cat_op));
|
|
|
|
sequenced.push_back (Arg ("id", Arg::type_number, Arg::cat_dom));
|
|
sequenced.push_back (Arg ("=", Arg::cat_op));
|
|
sequenced.push_back (Arg (format(ids[i]), Arg::type_number, Arg::cat_literal));
|
|
}
|
|
|
|
for (unsigned int i = 0; i < uuids.size (); ++i)
|
|
{
|
|
if (ids.size ())
|
|
sequenced.push_back (Arg ("or", Arg::cat_op));
|
|
|
|
sequenced.push_back (Arg ("uuid", Arg::type_string, Arg::cat_dom));
|
|
sequenced.push_back (Arg ("=", Arg::cat_op));
|
|
sequenced.push_back (Arg (uuids[i], Arg::type_string, Arg::cat_literal));
|
|
}
|
|
|
|
sequenced.push_back (Arg (")", Arg::cat_op));
|
|
|
|
// Now copy everything after the last id/uuid.
|
|
bool found_id = false;
|
|
for (arg = input.begin (); arg != input.end (); ++arg)
|
|
{
|
|
if (arg->_category == Arg::cat_id || arg->_category == Arg::cat_uuid)
|
|
found_id = true;
|
|
|
|
else if (found_id)
|
|
sequenced.push_back (*arg);
|
|
}
|
|
|
|
sequenced.dump ("A3::sequence");
|
|
return sequenced;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Dijkstra Shunting Algorithm.
|
|
// http://en.wikipedia.org/wiki/Shunting-yard_algorithm
|
|
//
|
|
// While there are tokens to be read:
|
|
// Read a token.
|
|
// If the token is an operator, o1, then:
|
|
// while there is an operator token, o2, at the top of the stack, and
|
|
// either o1 is left-associative and its precedence is less than or
|
|
// equal to that of o2,
|
|
// or o1 is right-associative and its precedence is less than that
|
|
// of o2,
|
|
// pop o2 off the stack, onto the output queue;
|
|
// push o1 onto the stack.
|
|
// If the token is a left parenthesis, then push it onto the stack.
|
|
// If the token is a right parenthesis:
|
|
// Until the token at the top of the stack is a left parenthesis, pop
|
|
// operators off the stack onto the output queue.
|
|
// Pop the left parenthesis from the stack, but not onto the output queue.
|
|
// If the token at the top of the stack is a function token, pop it onto
|
|
// the output queue.
|
|
// If the stack runs out without finding a left parenthesis, then there
|
|
// are mismatched parentheses.
|
|
// If the token is a number, then add it to the output queue.
|
|
//
|
|
// When there are no more tokens to read:
|
|
// While there are still operator tokens in the stack:
|
|
// If the operator token on the top of the stack is a parenthesis, then
|
|
// there are mismatched parentheses.
|
|
// Pop the operator onto the output queue.
|
|
// Exit.
|
|
//
|
|
const A3 A3::postfix (const A3& input) const
|
|
{
|
|
A3 converted;
|
|
converted._limit = input._limit;
|
|
|
|
A3 op_stack;
|
|
char type;
|
|
int precedence;
|
|
char associativity;
|
|
|
|
std::vector <Arg>::const_iterator arg;
|
|
for (arg = input.begin (); arg != input.end (); ++arg)
|
|
{
|
|
if (arg->_raw == "(")
|
|
{
|
|
op_stack.push_back (*arg);
|
|
}
|
|
else if (arg->_raw == ")")
|
|
{
|
|
while (op_stack.size () > 0 &&
|
|
op_stack.back ()._raw != "(")
|
|
{
|
|
converted.push_back (op_stack.back ());
|
|
op_stack.pop_back ();
|
|
}
|
|
|
|
if (op_stack.size ())
|
|
op_stack.pop_back ();
|
|
else
|
|
throw std::string (STRING_A3_MISMATCHED_PARENS);
|
|
}
|
|
else if (which_operator (arg->_raw, type, precedence, associativity))
|
|
{
|
|
char type2;
|
|
int precedence2;
|
|
char associativity2;
|
|
while (op_stack.size () > 0 &&
|
|
which_operator (op_stack.back ()._raw, type2, precedence2, associativity2) &&
|
|
((associativity == 'l' && precedence <= precedence2) ||
|
|
(associativity == 'r' && precedence < precedence2)))
|
|
{
|
|
converted.push_back (op_stack.back ());
|
|
op_stack.pop_back ();
|
|
}
|
|
|
|
op_stack.push_back (*arg);
|
|
}
|
|
else
|
|
{
|
|
converted.push_back (*arg);
|
|
}
|
|
}
|
|
|
|
while (op_stack.size () != 0)
|
|
{
|
|
if (op_stack.back ()._raw == "(" ||
|
|
op_stack.back ()._raw == ")")
|
|
throw std::string (STRING_A3_MISMATCHED_PARENS);
|
|
|
|
converted.push_back (op_stack.back ());
|
|
op_stack.pop_back ();
|
|
}
|
|
|
|
converted.dump ("A3::postfix");
|
|
return converted;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// <name>:['"][<value>]['"]
|
|
bool A3::is_attr (Nibbler& n, Arg& arg)
|
|
{
|
|
n.save ();
|
|
std::string name;
|
|
std::string value;
|
|
|
|
// If there is a valid attribute name.
|
|
if (n.getName (name) &&
|
|
name.length () &&
|
|
is_attribute (name, name))
|
|
{
|
|
if (n.skip (':'))
|
|
{
|
|
// Both quoted and unquoted Att's are accepted.
|
|
// Consider removing this for a stricter parse.
|
|
if (n.getQuoted ('"', value) ||
|
|
n.getQuoted ('\'', value) ||
|
|
n.getUntilOneOf (" \t)(", value) ||
|
|
n.getUntilEOS (value) ||
|
|
n.depleted ())
|
|
{
|
|
/*
|
|
// TODO Reject anything that looks like a URL.
|
|
// Exclude certain URLs, that look like attrs.
|
|
if (value.find ('@') <= n.cursor () ||
|
|
value.find ('/') <= n.cursor ())
|
|
return false;
|
|
*/
|
|
|
|
arg._raw = name + ':' + value;
|
|
arg._category = Arg::cat_attr;
|
|
|
|
// Most attributes are standard, some are pseudo-attributes, such as
|
|
// 'limit:page', which is not represented by a column object, and
|
|
// therefore not stored.
|
|
Column* col = context.columns[name];
|
|
if (col)
|
|
arg._type = Arg::type_id (col->type ());
|
|
else
|
|
arg._type = Arg::type_pseudo;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
n.restore ();
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// <name>.<mod>[:=]['"]<value>['"]
|
|
bool A3::is_attmod (Nibbler& n, Arg& arg)
|
|
{
|
|
n.save ();
|
|
std::string name;
|
|
std::string modifier;
|
|
std::string value;
|
|
// time_t date;
|
|
|
|
// If there is a valid attribute name.
|
|
if (n.getName (name) &&
|
|
name.length () &&
|
|
is_attribute (name, name))
|
|
{
|
|
if (n.skip ('.'))
|
|
{
|
|
// Skip the negation character.
|
|
n.skip ('~');
|
|
|
|
// If there is a valid modifier name.
|
|
if (n.getName (modifier) &&
|
|
modifier.length () &&
|
|
is_modifier (modifier, modifier))
|
|
{
|
|
if (n.skip (':') ||
|
|
n.skip ('='))
|
|
{
|
|
// Both quoted and unquoted Att's are accepted.
|
|
// Consider removing this for a stricter parse.
|
|
if (n.getQuoted ('"', value) ||
|
|
n.getQuoted ('\'', value) ||
|
|
// TODO Need more things recognized before it falls through to getUntilEOS.
|
|
// n.getDate (context.config.get ("dateformat"), date) ||
|
|
// need Duration too.
|
|
n.getName (value) ||
|
|
n.getUntilWS (value) ||
|
|
n.getUntilEOS (value) || // Redundant?
|
|
n.depleted ())
|
|
{
|
|
/*
|
|
TODO Eliminate anything that looks like a URL.
|
|
// Exclude certain URLs, that look like attrs.
|
|
if (value.find ('@') <= n.cursor () ||
|
|
value.find ('/') <= n.cursor ())
|
|
return false;
|
|
*/
|
|
|
|
arg._raw = name + '.' + modifier + ':' + value;
|
|
arg._type = Arg::type_id (context.columns[name]->type ());
|
|
arg._category = Arg::cat_attmod;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
n.restore ();
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Canonicalize attribute names.
|
|
bool A3::is_attribute (const std::string& input, std::string& canonical)
|
|
{
|
|
std::vector <std::string> columns = context.getColumns ();
|
|
columns.push_back ("limit"); // Special case.
|
|
|
|
std::vector <std::string> matches;
|
|
autoComplete (input,
|
|
columns,
|
|
matches,
|
|
context.config.getInteger ("abbreviation.minimum"));
|
|
|
|
if (matches.size () == 1)
|
|
{
|
|
canonical = matches[0];
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Canonicalize modifier names.
|
|
bool A3::is_modifier (const std::string& input, std::string& canonical)
|
|
{
|
|
std::vector <std::string> candidates;
|
|
for (unsigned int i = 0; i < NUM_MODIFIER_NAMES; ++i)
|
|
candidates.push_back (modifierNames[i]);
|
|
|
|
std::vector <std::string> matches;
|
|
autoComplete (input,
|
|
candidates,
|
|
matches,
|
|
context.config.getInteger ("abbreviation.minimum"));
|
|
|
|
if (matches.size () == 1)
|
|
{
|
|
canonical = matches[0];
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// DOM references are one of the following:
|
|
//
|
|
// 1. Fixed string
|
|
// DOM::get_references
|
|
// 2. Attribute
|
|
// <attr>
|
|
// 3. Task-specific attribute
|
|
// <id>.<attr>
|
|
// <uuid>.<attr>
|
|
// 4. Configuration value
|
|
// rc.<name>
|
|
//
|
|
bool A3::is_dom (Nibbler& n, Arg& arg)
|
|
{
|
|
n.save ();
|
|
std::string name;
|
|
int id;
|
|
std::string uuid;
|
|
|
|
// Fixed string reference.
|
|
std::vector <std::string> refs = context.dom.get_references ();
|
|
std::string result;
|
|
if (n.getOneOf (refs, result))
|
|
return true;
|
|
|
|
// Configuration.
|
|
if (n.getLiteral ("rc."))
|
|
{
|
|
result = "rc.";
|
|
while (n.getWord (name))
|
|
{
|
|
result += name;
|
|
|
|
if (n.skip ('.'))
|
|
result += '.';
|
|
}
|
|
|
|
arg._raw = result;
|
|
arg._category = Arg::cat_dom;
|
|
return true;
|
|
}
|
|
|
|
n.restore ();
|
|
|
|
// <id>.<attr>
|
|
if (n.getInt (id) &&
|
|
n.skip ('.') &&
|
|
n.getName (name) &&
|
|
name.length () &&
|
|
is_attribute (name, name))
|
|
{
|
|
result = format (id) + '.' + name;
|
|
arg._raw = result;
|
|
arg._type = Arg::type_id (context.columns[name]->type ());
|
|
arg._category = Arg::cat_dom;
|
|
return true;
|
|
}
|
|
|
|
n.restore ();
|
|
|
|
// <uuid>.<attr>
|
|
if (n.getUUID (uuid) &&
|
|
n.skip ('.') &&
|
|
n.getName (name) &&
|
|
name.length () &&
|
|
is_attribute (name, name))
|
|
{
|
|
arg._raw = uuid + '.' + name;
|
|
arg._type = Arg::type_id (context.columns[name]->type ());
|
|
arg._category = Arg::cat_dom;
|
|
return true;
|
|
}
|
|
|
|
n.restore ();
|
|
|
|
// Attribute.
|
|
if (n.getName (name) &&
|
|
name.length () &&
|
|
is_attribute (name, name))
|
|
{
|
|
arg._raw = name;
|
|
arg._type = Arg::type_id (context.columns[name]->type ());
|
|
arg._category = Arg::cat_dom;
|
|
return true;
|
|
}
|
|
|
|
n.restore ();
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// A duration may only be followed by \0, ), +, -, *, / or ' '.
|
|
//
|
|
// This prevents the interpretation of '31st' as a duration ('31s').
|
|
bool A3::is_duration (Nibbler& n, std::string& result)
|
|
{
|
|
std::string::size_type start = n.save ();
|
|
|
|
double d;
|
|
std::string unit;
|
|
std::vector <std::string> units = Duration::get_units ();
|
|
|
|
if (n.getUnsignedNumber (d) &&
|
|
n.getOneOf (units, unit))
|
|
{
|
|
char next = n.next ();
|
|
if (next == '\0' ||
|
|
next == ')' ||
|
|
next == '+' ||
|
|
next == '-' ||
|
|
next == '*' ||
|
|
next == '/' ||
|
|
next == ' ')
|
|
{
|
|
result = n.str ().substr (start, n.cursor () - start);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
n.restore ();
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// /<pattern>/
|
|
bool A3::is_pattern (Nibbler& n, std::string& result)
|
|
{
|
|
n.save ();
|
|
std::string pattern;
|
|
if (n.getQuoted ('/', pattern) &&
|
|
pattern.length () > 0)
|
|
{
|
|
result = '/' + pattern + '/';
|
|
return true;
|
|
}
|
|
|
|
n.restore ();
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// /<from>/<to>/[g]
|
|
//
|
|
// Note: one problem with this is that substitutions start with a /, and so any
|
|
// two-directory absolute path, (or three-level, if the third directory is
|
|
// named 'g') can be misinterpreted. To help (but not solve) this, if a
|
|
// substition exists on the local disk, it is not considered a subst.
|
|
// This needs to be changed to a better solution. When I think of one.
|
|
bool A3::is_subst (Nibbler& n, std::string& result)
|
|
{
|
|
n.save ();
|
|
|
|
std::string from;
|
|
std::string to;
|
|
bool global = false;
|
|
if (n.skip ('/') &&
|
|
n.getUntil ('/', from) &&
|
|
from.length () &&
|
|
n.skip ('/') &&
|
|
n.getUntil ('/', to) &&
|
|
n.skip ('/'))
|
|
{
|
|
if (n.skip ('g'))
|
|
global = true;
|
|
|
|
result = '/' + from + '/' + to + '/';
|
|
if (global)
|
|
result += 'g';
|
|
|
|
if (! Directory (result).exists ()) // Ouch - expensive call.
|
|
return true;
|
|
}
|
|
|
|
n.restore ();
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// <id>[-<id>][,<id>[-<id>]]
|
|
bool A3::is_id (Nibbler& n, std::string& result)
|
|
{
|
|
n.save ();
|
|
std::string::size_type start = n.cursor ();
|
|
int id;
|
|
|
|
if (n.getUnsignedInt (id))
|
|
{
|
|
if (n.skip ('-') &&
|
|
!n.getUnsignedInt (id))
|
|
{
|
|
n.restore ();
|
|
return false;
|
|
}
|
|
|
|
while (n.skip (','))
|
|
{
|
|
if (n.getUnsignedInt (id))
|
|
{
|
|
if (n.skip ('-'))
|
|
{
|
|
if (!n.getUnsignedInt (id))
|
|
{
|
|
n.restore ();
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
n.restore ();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
char next = n.next ();
|
|
if (next == '\0' ||
|
|
next == ')' ||
|
|
next == ' ' ||
|
|
next == '-')
|
|
{
|
|
std::string::size_type end = n.cursor ();
|
|
n.restore ();
|
|
if (n.getN (end - start, result))
|
|
return true;
|
|
}
|
|
}
|
|
|
|
n.restore ();
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// <uuid>[,...]
|
|
bool A3::is_uuid (Nibbler& n, std::string& result)
|
|
{
|
|
n.save ();
|
|
result = "";
|
|
std::string uuid;
|
|
if (n.getUUID (uuid))
|
|
{
|
|
result += uuid;
|
|
while (n.skip (',') &&
|
|
n.getUUID (uuid))
|
|
{
|
|
result += ',' + uuid;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
n.restore ();
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// [+-]<tag>
|
|
bool A3::is_tag (Nibbler& n, std::string& result)
|
|
{
|
|
n.save ();
|
|
std::string::size_type start = n.cursor ();
|
|
|
|
if (n.skipAllOneOf ("+-"))
|
|
{
|
|
if (!isdigit (n.next ()))
|
|
{
|
|
std::string name;
|
|
if (n.getUntilOneOf (" \t()+-*/", name) &&
|
|
name.length ())
|
|
{
|
|
std::string::size_type end = n.cursor ();
|
|
n.restore ();
|
|
if (n.getN (end - start, result))
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
n.restore ();
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// <number> followed by either: \0, ), +, -, *, /, ' '.
|
|
//
|
|
// This prevents the interpretation of '3M' as a number.
|
|
bool A3::is_number (Nibbler& n, double& d)
|
|
{
|
|
n.save ();
|
|
if (n.getNumber (d))
|
|
{
|
|
char next = n.next ();
|
|
if (next == '\0' ||
|
|
next == ')' ||
|
|
next == '+' ||
|
|
next == '-' ||
|
|
next == '*' ||
|
|
next == '/' ||
|
|
next == ' ')
|
|
{
|
|
return true;
|
|
}
|
|
|
|
n.restore ();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// <number> followed by either: \0, ), +, -, *, /, ' '.
|
|
//
|
|
// This prevents the interpretation of '3M' as a number.
|
|
bool A3::is_integer (Nibbler& n, int& i)
|
|
{
|
|
n.save ();
|
|
if (n.getInt (i))
|
|
{
|
|
char next = n.next ();
|
|
if (next == '\0' ||
|
|
next == ')' ||
|
|
next == '+' ||
|
|
next == '-' ||
|
|
next == '*' ||
|
|
next == '/' ||
|
|
next == ' ')
|
|
{
|
|
return true;
|
|
}
|
|
|
|
n.restore ();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
bool A3::is_operator (
|
|
std::vector <std::string>& operators,
|
|
Nibbler& n,
|
|
std::string& result)
|
|
{
|
|
n.save ();
|
|
|
|
if (n.getOneOf (operators, result) &&
|
|
isTokenEnd (n.str (), n.cursor () - 1))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
n.restore ();
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
bool A3::extract_pattern (const std::string& input, std::string& pattern)
|
|
{
|
|
Nibbler n (input);
|
|
if (n.skip ('/') &&
|
|
n.getUntil ('/', pattern) &&
|
|
n.skip ('/'))
|
|
{
|
|
if (!n.depleted ())
|
|
throw std::string (STRING_A3_PATTERN_GARBAGE);
|
|
|
|
return true;
|
|
}
|
|
else
|
|
throw std::string (STRING_A3_MALFORMED_PATTERN);
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
bool A3::extract_tag (
|
|
const std::string& input,
|
|
char& type,
|
|
std::string& tag)
|
|
{
|
|
if (input.length () > 1)
|
|
{
|
|
type = input[0];
|
|
tag = input.substr (1);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// <name>:['"]<value>['"]
|
|
bool A3::extract_attr (
|
|
const std::string& input,
|
|
std::string& name,
|
|
std::string& value)
|
|
{
|
|
Nibbler n (input);
|
|
|
|
name = "";
|
|
value = "";
|
|
|
|
if (n.getUntil (':', name))
|
|
{
|
|
if (n.skip (':'))
|
|
{
|
|
if (n.getQuoted ('"', value) ||
|
|
n.getQuoted ('\'', value) ||
|
|
n.getUntilEOS (value))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// <name>.<mod>[:=]['"]<value>['"]
|
|
bool A3::extract_attmod (
|
|
const std::string& input,
|
|
std::string& name,
|
|
std::string& modifier,
|
|
std::string& value,
|
|
std::string& sense)
|
|
{
|
|
Nibbler n (input);
|
|
|
|
name = "";
|
|
value = "";
|
|
modifier = "";
|
|
sense = "positive";
|
|
|
|
if (n.getUntil (".", name))
|
|
{
|
|
if (n.skip ('.'))
|
|
{
|
|
if (n.skip ('~'))
|
|
sense = "negative";
|
|
|
|
n.getUntil (':', modifier);
|
|
}
|
|
|
|
if (n.skip (':') ||
|
|
n.skip ('='))
|
|
{
|
|
if (n.getQuoted ('"', value) ||
|
|
n.getQuoted ('\'', value) ||
|
|
n.getUntilEOS (value))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
bool A3::extract_subst (
|
|
const std::string& input,
|
|
std::string& from,
|
|
std::string& to,
|
|
bool& global)
|
|
{
|
|
Nibbler n (input);
|
|
if (n.skip ('/') &&
|
|
n.getUntil ('/', from) &&
|
|
n.skip ('/') &&
|
|
n.getUntil ('/', to) &&
|
|
n.skip ('/'))
|
|
{
|
|
global = n.skip ('g');
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// A sequence can be:
|
|
//
|
|
// a single ID: 1
|
|
// a list of IDs: 1,3,5
|
|
// a list of IDs: 1 3 5
|
|
// a range: 5-10
|
|
// or a combination: 1,3,5-10 12
|
|
//
|
|
// If a sequence is followed by a non-number, then subsequent numbers are not
|
|
// interpreted as IDs. For example:
|
|
//
|
|
// 1 2 three 4
|
|
//
|
|
// The sequence is "1 2".
|
|
//
|
|
// The _first number found in the command line is assumed to be a sequence. If
|
|
// there are two sequences, only the _first is recognized, for example:
|
|
//
|
|
// 1,2 three 4,5
|
|
//
|
|
// The sequence is "1,2".
|
|
//
|
|
bool A3::extract_id (const std::string& input, std::vector <int>& sequence)
|
|
{
|
|
Nibbler n (input);
|
|
|
|
int id;
|
|
|
|
if (n.getUnsignedInt (id))
|
|
{
|
|
sequence.push_back (id);
|
|
|
|
if (n.skip ('-'))
|
|
{
|
|
int end;
|
|
if (!n.getUnsignedInt (end))
|
|
throw std::string (STRING_A3_ID_AFTER_HYPHEN);
|
|
|
|
if (id > end)
|
|
throw std::string (STRING_A3_RANGE_INVERTED);
|
|
|
|
for (int n = id + 1; n <= end; ++n)
|
|
sequence.push_back (n);
|
|
}
|
|
|
|
while (n.skip (','))
|
|
{
|
|
if (n.getUnsignedInt (id))
|
|
{
|
|
sequence.push_back (id);
|
|
|
|
if (n.skip ('-'))
|
|
{
|
|
int end;
|
|
if (!n.getUnsignedInt (end))
|
|
throw std::string (STRING_A3_ID_AFTER_HYPHEN);
|
|
|
|
if (id > end)
|
|
throw std::string (STRING_A3_RANGE_INVERTED);
|
|
|
|
for (int n = id + 1; n <= end; ++n)
|
|
sequence.push_back (n);
|
|
}
|
|
}
|
|
else
|
|
throw std::string (STRING_A3_MALFORMED_ID);
|
|
}
|
|
}
|
|
else
|
|
throw std::string (STRING_A3_MALFORMED_ID);
|
|
|
|
return n.depleted ();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
bool A3::extract_uuid (
|
|
const std::string& input,
|
|
std::vector <std::string>& sequence)
|
|
{
|
|
Nibbler n (input);
|
|
|
|
std::string uuid;
|
|
if (n.getUUID (uuid))
|
|
{
|
|
sequence.push_back (uuid);
|
|
|
|
while (n.skip (','))
|
|
{
|
|
if (!n.getUUID (uuid))
|
|
throw std::string (STRING_A3_UUID_AFTER_COMMA);
|
|
|
|
sequence.push_back (uuid);
|
|
}
|
|
}
|
|
else
|
|
throw std::string (STRING_A3_MALFORMED_UUID);
|
|
|
|
if (!n.depleted ())
|
|
throw std::string (STRING_A3_PATTERN_GARBAGE);
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
bool A3::which_operator (
|
|
const std::string& input,
|
|
char& type,
|
|
int& precedence,
|
|
char& associativity)
|
|
{
|
|
for (unsigned int i = 0; i < NUM_OPERATORS; ++i)
|
|
{
|
|
if (operators[i].op == input)
|
|
{
|
|
type = operators[i].type;
|
|
precedence = operators[i].precedence;
|
|
associativity = operators[i].associativity;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
void A3::dump (const std::string& label)
|
|
{
|
|
if (context.config.getBoolean ("debug"))
|
|
{
|
|
// Set up a color mapping.
|
|
std::map <int, Color> color_map;
|
|
color_map[Arg::cat_program] = Color ("bold blue on blue");
|
|
color_map[Arg::cat_command] = Color ("bold cyan on cyan");
|
|
color_map[Arg::cat_rc] = Color ("bold red on red");
|
|
color_map[Arg::cat_override] = Color ("bold red on red");
|
|
color_map[Arg::cat_terminator] = Color ("bold yellow on yellow");
|
|
color_map[Arg::cat_literal] = Color ("white on gray4");
|
|
|
|
// Filter colors.
|
|
color_map[Arg::cat_attr] = Color ("bold red on gray4");
|
|
color_map[Arg::cat_attmod] = Color ("bold red on gray4");
|
|
color_map[Arg::cat_pattern] = Color ("cyan on gray4");
|
|
color_map[Arg::cat_subst] = Color ("bold cyan on gray4");
|
|
color_map[Arg::cat_op] = Color ("green on gray4");
|
|
color_map[Arg::type_string] = Color ("bold yellow on gray4");
|
|
color_map[Arg::cat_rx] = Color ("bold yellow on gray4");
|
|
color_map[Arg::type_date] = Color ("bold yellow on gray4");
|
|
color_map[Arg::cat_dom] = Color ("bold white on gray4");
|
|
color_map[Arg::type_duration] = Color ("magenta on gray4");
|
|
color_map[Arg::cat_id] = Color ("white on gray4");
|
|
color_map[Arg::cat_uuid] = Color ("white on gray4");
|
|
|
|
// Default.
|
|
color_map[Arg::cat_none] = Color ("black on white");
|
|
|
|
Color color_debug (context.config.get ("color.debug"));
|
|
std::stringstream out;
|
|
out << color_debug.colorize (label)
|
|
<< "\n";
|
|
|
|
ViewText view;
|
|
view.width (context.getWidth ());
|
|
view.leftMargin (2);
|
|
for (unsigned int i = 0; i < this->size (); ++i)
|
|
view.add (Column::factory ("string", ""));
|
|
|
|
view.addRow ();
|
|
view.addRow ();
|
|
view.addRow ();
|
|
view.addRow ();
|
|
|
|
for (unsigned int i = 0; i < this->size (); ++i)
|
|
{
|
|
std::string value = (*this)[i]._value;
|
|
std::string raw = (*this)[i]._raw;
|
|
Arg::type type = (*this)[i]._type;
|
|
Arg::category category = (*this)[i]._category;
|
|
|
|
Color c;
|
|
if (color_map[category].nontrivial ())
|
|
c = color_map[category];
|
|
else
|
|
c = color_map[Arg::cat_none];
|
|
|
|
view.set (0, i, value, c);
|
|
view.set (1, i, raw, c);
|
|
view.set (2, i, Arg::type_name (type), c);
|
|
view.set (3, i, Arg::category_name (category), c);
|
|
}
|
|
|
|
out << view.render ();
|
|
context.debug (out.str ());
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|