• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

GothenburgBitFactory / taskwarrior / 10152339701

29 Jul 2024 09:45PM UTC coverage: 84.437% (+0.07%) from 84.372%
10152339701

push

github

web-flow
Merge pull request #3566 from felixschurk/add-clang-format

Add clang-format to enforce style guide

12359 of 13760 new or added lines in 147 files covered. (89.82%)

123 existing lines in 42 files now uncovered.

19070 of 22585 relevant lines covered (84.44%)

19724.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.32
/src/CLI2.cpp
1
////////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2006 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez.
4
//
5
// Permission is hereby granted, free of charge, to any person obtaining a copy
6
// of this software and associated documentation files (the "Software"), to deal
7
// in the Software without restriction, including without limitation the rights
8
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
// copies of the Software, and to permit persons to whom the Software is
10
// furnished to do so, subject to the following conditions:
11
//
12
// The above copyright notice and this permission notice shall be included
13
// in all copies or substantial portions of the Software.
14
//
15
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
// SOFTWARE.
22
//
23
// https://www.opensource.org/licenses/mit-license.php
24
//
25
////////////////////////////////////////////////////////////////////////////////
26

27
#include <cmake.h>
28
// cmake.h include header must come first
29

30
#include <CLI2.h>
31
#include <CmdCustom.h>
32
#include <CmdTimesheet.h>
33
#include <Color.h>
34
#include <Context.h>
35
#include <Lexer.h>
36
#include <format.h>
37
#include <shared.h>
38
#include <stdlib.h>
39
#include <utf8.h>
40

41
#include <algorithm>
42
#include <sstream>
43

44
// Overridden by rc.abbreviation.minimum.
45
int CLI2::minimumMatchLength = 3;
46

47
// Alias expansion limit. Any more indicates some kind of error.
48
static int safetyValveDefault = 10;
49

50
////////////////////////////////////////////////////////////////////////////////
51
A2::A2(const std::string& raw, Lexer::Type lextype) {
55,406✔
52
  _lextype = lextype;
55,406✔
53
  attribute("raw", raw);
55,406✔
54
}
55,406✔
55

56
////////////////////////////////////////////////////////////////////////////////
57
A2::A2(const A2& other) = default;
553,927✔
58

59
////////////////////////////////////////////////////////////////////////////////
60
A2& A2::operator=(const A2& other) = default;
47,196✔
61

62
////////////////////////////////////////////////////////////////////////////////
63
bool A2::hasTag(const std::string& tag) const {
393,559✔
64
  return std::find(_tags.begin(), _tags.end(), tag) != _tags.end();
393,559✔
65
}
66

67
////////////////////////////////////////////////////////////////////////////////
68
void A2::tag(const std::string& tag) {
86,177✔
69
  if (!hasTag(tag)) _tags.push_back(tag);
86,177✔
70
}
86,177✔
71

72
////////////////////////////////////////////////////////////////////////////////
NEW
73
void A2::unTag(const std::string& tag) {
×
NEW
74
  for (auto i = _tags.begin(); i != _tags.end(); ++i)
×
NEW
75
    if (*i == tag) {
×
NEW
76
      _tags.erase(i);
×
UNCOV
77
      break;
×
78
    }
79
}
80

81
////////////////////////////////////////////////////////////////////////////////
82
// Accessor for attributes.
83
void A2::attribute(const std::string& name, const std::string& value) {
84,774✔
84
  _attributes[name] = value;
84,774✔
85

86
  if (name == "raw") decompose();
84,774✔
87
}
84,774✔
88

89
////////////////////////////////////////////////////////////////////////////////
90
// Accessor for attributes.
91
const std::string A2::attribute(const std::string& name) const {
314,545✔
92
  // Prevent autovivification.
93
  auto i = _attributes.find(name);
314,545✔
94
  if (i != _attributes.end()) return i->second;
314,545✔
95

96
  return "";
12,004✔
97
}
98

99
////////////////////////////////////////////////////////////////////////////////
100
const std::string A2::getToken() const {
12,867✔
101
  auto i = _attributes.find("canonical");
12,867✔
102
  if (i == _attributes.end()) i = _attributes.find("raw");
12,867✔
103

104
  return i->second;
25,734✔
105
}
106

107
////////////////////////////////////////////////////////////////////////////////
108
void A2::decompose() {
56,912✔
109
  if (_lextype == Lexer::Type::tag) {
56,912✔
110
    std::string raw = _attributes["raw"];
1,892✔
111
    attribute("name", raw.substr(1));
946✔
112
    attribute("sign", raw.substr(0, 1));
946✔
113
  }
946✔
114

115
  else if (_lextype == Lexer::Type::substitution) {
55,966✔
116
    // if (Directory (raw).exists ())
117
    //   return;
118

119
    std::string from;
37✔
120
    std::string to;
37✔
121
    std::string flags;
37✔
122
    if (Lexer::decomposeSubstitution(_attributes["raw"], from, to, flags)) {
37✔
123
      attribute("from", from);
37✔
124
      attribute("to", to);
37✔
125
      attribute("flags", flags);
37✔
126
    }
127
  }
37✔
128

129
  else if (_lextype == Lexer::Type::pair) {
55,929✔
130
    std::string name;
2,964✔
131
    std::string mod;
2,964✔
132
    std::string sep;
2,964✔
133
    std::string value;
2,964✔
134
    if (Lexer::decomposePair(_attributes["raw"], name, mod, sep, value)) {
2,964✔
135
      attribute("name", name);
2,964✔
136
      attribute("modifier", mod);
2,964✔
137
      attribute("separator", sep);
2,964✔
138
      attribute("value", value);
2,964✔
139

140
      if (name == "rc") {
2,964✔
141
        if (mod != "")
608✔
142
          tag("CONFIG");
565✔
143
        else
144
          tag("RC");
43✔
145
      }
146
    }
147
  }
2,964✔
148

149
  else if (_lextype == Lexer::Type::pattern) {
52,965✔
150
    // if (Directory (raw).exists ())
151
    //   return;
152

153
    std::string pattern;
109✔
154
    std::string flags;
109✔
155
    if (Lexer::decomposePattern(_attributes["raw"], pattern, flags)) {
109✔
156
      attribute("pattern", pattern);
109✔
157
      attribute("flags", flags);
109✔
158
    }
159
  }
109✔
160
}
56,912✔
161

162
////////////////////////////////////////////////////////////////////////////////
163
const std::string A2::dump() const {
152✔
164
  auto output = Lexer::typeToString(_lextype);
152✔
165

166
  // Dump attributes.
167
  std::string atts;
152✔
168
  for (const auto& a : _attributes) atts += a.first + "='\033[33m" + a.second + "\033[0m' ";
506✔
169

170
  // Dump tags.
171
  std::string tags;
152✔
172
  for (const auto& tag : _tags) {
441✔
173
    if (tag == "BINARY")
289✔
174
      tags += "\033[1;37;44m" + tag + "\033[0m ";
22✔
175
    else if (tag == "CMD")
267✔
176
      tags += "\033[1;37;46m" + tag + "\033[0m ";
18✔
177
    else if (tag == "FILTER")
249✔
178
      tags += "\033[1;37;42m" + tag + "\033[0m ";
62✔
179
    else if (tag == "MODIFICATION")
187✔
NEW
180
      tags += "\033[1;37;43m" + tag + "\033[0m ";
×
181
    else if (tag == "MISCELLANEOUS")
187✔
NEW
182
      tags += "\033[1;37;45m" + tag + "\033[0m ";
×
183
    else if (tag == "RC")
187✔
NEW
184
      tags += "\033[1;37;41m" + tag + "\033[0m ";
×
185
    else if (tag == "CONFIG")
187✔
186
      tags += "\033[1;37;101m" + tag + "\033[0m ";
22✔
187
    else if (tag == "?")
165✔
NEW
188
      tags += "\033[38;5;255;48;5;232m" + tag + "\033[0m ";
×
189
    else
190
      tags += "\033[32m" + tag + "\033[0m ";
165✔
191
  }
192

193
  return output + ' ' + atts + tags;
456✔
194
}
152✔
195

196
////////////////////////////////////////////////////////////////////////////////
197
static const char* getValue(int argc, const char** argv, std::string arg) {
8,780✔
198
  const auto is_arg = [&](std::string s) {
28,893✔
199
    return s.size() > arg.size() + 1 && (s[arg.size()] == ':' || s[arg.size()] == '=') &&
28,973✔
200
           s.compare(0, arg.size(), arg) == 0;
28,973✔
201
  };
8,780✔
202
  // find last argument before --
203
  auto last = std::make_reverse_iterator(argv);
8,780✔
204
  auto first = std::make_reverse_iterator(std::find(argv, argv + argc, std::string("--")));
8,780✔
205
  auto it = std::find_if(first, last, is_arg);
8,780✔
206
  if (it == last) return nullptr;
8,780✔
207
  // return the string after : or =
208
  return *it + arg.size() + 1;
42✔
209
}
210

211
////////////////////////////////////////////////////////////////////////////////
212
// Static method.
213
bool CLI2::getOverride(int argc, const char** argv, File& rc) {
4,390✔
214
  const char* value = getValue(argc, argv, "rc");
4,390✔
215
  if (value == nullptr) return false;
4,390✔
216
  rc = File(value);
41✔
217
  return true;
41✔
218
}
219

220
////////////////////////////////////////////////////////////////////////////////
221
// Look for CONFIG data.location and initialize a Path object.
222
// Static method.
223
bool CLI2::getDataLocation(int argc, const char** argv, Path& data) {
4,390✔
224
  const char* value = getValue(argc, argv, "rc.data.location");
4,390✔
225
  if (value == nullptr) {
4,390✔
226
    std::string location = Context::getContext().config.get("data.location");
8,778✔
227
    if (location != "") data = location;
4,389✔
228
    return false;
4,389✔
229
  }
4,389✔
230
  data = Directory(value);
1✔
231
  return true;
1✔
232
}
233

234
////////////////////////////////////////////////////////////////////////////////
235
// Static method.
236
void CLI2::applyOverrides(int argc, const char** argv) {
4,390✔
237
  auto& context = Context::getContext();
4,390✔
238
  auto last = std::find(argv, argv + argc, std::string("--"));
4,390✔
239
  auto is_override = [](const std::string& s) { return s.compare(0, 3, "rc.") == 0; };
27,661✔
240
  auto get_sep = [&](const std::string& s) {
14,522✔
241
    if (is_override(s)) return s.find_first_of(":=", 3);
14,522✔
242
    return std::string::npos;
14,042✔
243
  };
4,390✔
244
  auto override_settings = [&](std::string raw) {
14,522✔
245
    auto sep = get_sep(raw);
14,522✔
246
    if (sep == std::string::npos) return;
14,522✔
247
    std::string name = raw.substr(3, sep - 3);
476✔
248
    std::string value = raw.substr(sep + 1);
476✔
249
    context.config.set(name, value);
476✔
250
  };
476✔
251
  auto display_overrides = [&](std::string raw) {
13,139✔
252
    if (is_override(raw)) context.footnote(format("Configuration override {1}", raw));
13,139✔
253
  };
13,139✔
254
  std::for_each(argv, last, override_settings);
4,390✔
255
  if (context.verbose("override")) std::for_each(argv, last, display_overrides);
4,390✔
256
}
4,390✔
257

258
////////////////////////////////////////////////////////////////////////////////
259
void CLI2::alias(const std::string& name, const std::string& value) { _aliases[name] = value; }
17,674✔
260

261
////////////////////////////////////////////////////////////////////////////////
262
void CLI2::entity(const std::string& category, const std::string& name) {
808,699✔
263
  // Walk the list of entities for category.
264
  auto c = _entities.equal_range(category);
808,699✔
265
  for (auto e = c.first; e != c.second; ++e)
23,015,766✔
266
    if (e->second == name) return;
22,207,067✔
267

268
  // The category/name pair was not found, therefore add it.
269
  _entities.emplace(category, name);
808,699✔
270
}
271

272
////////////////////////////////////////////////////////////////////////////////
273
// Capture a single argument.
274
void CLI2::add(const std::string& argument) {
16,840✔
275
  A2 arg(Lexer::trim(argument), Lexer::Type::word);
33,680✔
276
  arg.tag("ORIGINAL");
16,840✔
277
  _original_args.push_back(arg);
16,840✔
278

279
  // Adding a new argument invalidates prior analysis.
280
  _args.clear();
16,840✔
281
}
16,840✔
282

283
////////////////////////////////////////////////////////////////////////////////
284
// Capture a set of arguments, inserted immediately after <offset> arguments
285
// after the binary..
286
void CLI2::add(const std::vector<std::string>& arguments, int offset /* = 0 */) {
555✔
287
  std::vector<A2> replacement{_original_args.begin(), _original_args.begin() + offset + 1};
555✔
288

289
  for (const auto& arg : arguments) replacement.emplace_back(arg, Lexer::Type::word);
2,759✔
290

291
  for (unsigned int i = 1 + offset; i < _original_args.size(); ++i)
1,812✔
292
    replacement.push_back(_original_args[i]);
1,257✔
293

294
  _original_args = replacement;
555✔
295

296
  // Adding a new argument invalidates prior analysis.
297
  _args.clear();
555✔
298
}
555✔
299

300
////////////////////////////////////////////////////////////////////////////////
301
// Arg0 is the first argument, which is the name and potentially a relative or
302
// absolute path to the invoked binary.
303
//
304
// The binary name is 'task', but if the binary is reported as 'cal' or
305
// 'calendar' then it was invoked via symbolic link, in which case capture the
306
// first argument as 'calendar'.
307
void CLI2::handleArg0() {
4,944✔
308
  // Capture arg0 separately, because it is the command that was run, and could
309
  // need special handling.
310
  auto raw = _original_args[0].attribute("raw");
9,888✔
311
  A2 a(raw, Lexer::Type::word);
4,944✔
312
  a.tag("BINARY");
4,944✔
313

314
  std::string basename = "task";
4,944✔
315
  auto slash = raw.rfind('/');
4,944✔
316
  if (slash != std::string::npos) basename = raw.substr(slash + 1);
4,944✔
317

318
  a.attribute("basename", basename);
4,944✔
319
  if (basename == "cal" || basename == "calendar") {
4,944✔
NEW
320
    _args.push_back(a);
×
321

NEW
322
    A2 cal("calendar", Lexer::Type::word);
×
NEW
323
    _args.push_back(cal);
×
NEW
324
  } else {
×
325
    _args.push_back(a);
4,944✔
326
  }
327
}
4,944✔
328

329
////////////////////////////////////////////////////////////////////////////////
330
// All arguments must be individually and wholly recognized by the Lexer. Any
331
// argument not recognized is considered a Lexer::Type::word.
332
//
333
// As a side effect, tags all arguments after a terminator ('--') with
334
// TERMINATED.
335
void CLI2::lexArguments() {
4,944✔
336
  // Note: Starts iterating at index 1, because ::handleArg0 has already
337
  //       processed it.
338
  bool terminated = false;
4,944✔
339
  for (unsigned int i = 1; i < _original_args.size(); ++i) {
20,856✔
340
    bool quoted = Lexer::wasQuoted(_original_args[i].attribute("raw"));
15,912✔
341

342
    // Process single-token arguments.
343
    std::string lexeme;
15,912✔
344
    Lexer::Type type;
345
    Lexer lex(_original_args[i].attribute("raw"));
31,824✔
346
    if (lex.token(lexeme, type) &&
31,769✔
347
        (lex.isEOS() ||                           // Token goes to EOS
15,857✔
348
         (quoted && type == Lexer::Type::pair)))  // Quoted pairs automatically go to EOS
176✔
349
    {
350
      if (!terminated && type == Lexer::Type::separator)
15,607✔
351
        terminated = true;
773✔
352
      else if (terminated)
14,834✔
353
        type = Lexer::Type::word;
1,388✔
354

355
      A2 a(_original_args[i].attribute("raw"), type);
31,214✔
356
      if (terminated) a.tag("TERMINATED");
15,607✔
357
      if (quoted) a.tag("QUOTED");
15,607✔
358

359
      if (_original_args[i].hasTag("ORIGINAL")) a.tag("ORIGINAL");
15,607✔
360

361
      _args.push_back(a);
15,607✔
362
    }
15,607✔
363

364
    // Process multiple-token arguments.
365
    else {
366
      const std::string quote = "'";
305✔
367

368
      // Escape unescaped single quotes
369
      std::string escaped = "";
305✔
370

371
      // For performance reasons. The escaped string is as long as the original.
372
      escaped.reserve(_original_args[i].attribute("raw").size());
305✔
373

374
      std::string::size_type cursor = 0;
305✔
375
      bool nextEscaped = false;
305✔
376
      while (int num = utf8_next_char(_original_args[i].attribute("raw"), cursor)) {
3,221✔
377
        std::string character = utf8_character(num);
2,916✔
378
        if (!nextEscaped && (character == "\\"))
2,916✔
379
          nextEscaped = true;
2✔
380
        else {
381
          if (character == quote && !nextEscaped) escaped += "\\";
2,914✔
382
          nextEscaped = false;
2,914✔
383
        }
384
        escaped += character;
2,916✔
385
      }
2,916✔
386

387
      cursor = 0;
305✔
388
      std::string word;
305✔
389
      if (Lexer::readWord(quote + escaped + quote, quote, cursor, word)) {
305✔
390
        Lexer::dequote(word);
305✔
391
        A2 unknown(word, Lexer::Type::word);
305✔
392
        if (lex.wasQuoted(_original_args[i].attribute("raw"))) unknown.tag("QUOTED");
305✔
393

394
        if (_original_args[i].hasTag("ORIGINAL")) unknown.tag("ORIGINAL");
305✔
395

396
        _args.push_back(unknown);
305✔
397
      }
305✔
398

399
      // This branch may have no use-case.
400
      else {
NEW
401
        A2 unknown(_original_args[i].attribute("raw"), Lexer::Type::word);
×
NEW
402
        unknown.tag("UNKNOWN");
×
403

NEW
404
        if (lex.wasQuoted(_original_args[i].attribute("raw"))) unknown.tag("QUOTED");
×
405

NEW
406
        if (_original_args[i].hasTag("ORIGINAL")) unknown.tag("ORIGINAL");
×
407

NEW
408
        _args.push_back(unknown);
×
409
      }
410
    }
305✔
411
  }
15,912✔
412

413
  if (Context::getContext().config.getInteger("debug.parser") >= 2)
4,944✔
414
    Context::getContext().debug(dump("CLI2::analyze lexArguments"));
4✔
415
}
4,944✔
416

417
////////////////////////////////////////////////////////////////////////////////
418
// [1] Scan all args for the 'add' and 'log' commands, and demote any
419
//     Lexer::Type::Tag args with sign '-' to Lexer::Type::word.
420
// [2] Convert any pseudo args name:value into config settings, and erase.
421
void CLI2::demotion() {
4,943✔
422
  bool changes = false;
4,943✔
423
  std::vector<A2> replacement;
4,943✔
424

425
  std::string canonical;
4,943✔
426
  for (auto& a : _args) {
25,862✔
427
    if (a._lextype == Lexer::Type::tag && a.attribute("sign") == "-") {
20,919✔
428
      std::string command = getCommand();
563✔
429
      if (command == "add" || command == "log") {
563✔
430
        a._lextype = Lexer::Type::word;
1✔
431
        changes = true;
1✔
432
      }
433
    }
563✔
434

435
    else if (a._lextype == Lexer::Type::pair &&
23,286✔
436
             canonicalize(canonical, "pseudo", a.attribute("name"))) {
23,286✔
437
      Context::getContext().config.set(canonical, a.attribute("value"));
64✔
438
      changes = true;
64✔
439

440
      // Equivalent to erasing 'a'.
441
      continue;
64✔
442
    }
443

444
    replacement.push_back(a);
20,855✔
445
  }
446

447
  if (changes && Context::getContext().config.getInteger("debug.parser") >= 2)
4,943✔
NEW
448
    Context::getContext().debug(dump("CLI2::analyze demotion"));
×
449
}
4,943✔
450

451
////////////////////////////////////////////////////////////////////////////////
452
// Intended to be called after ::add() to perform the final analysis.
453
void CLI2::analyze() {
4,944✔
454
  if (Context::getContext().config.getInteger("debug.parser") >= 2)
4,944✔
455
    Context::getContext().debug(dump("CLI2::analyze"));
4✔
456

457
  // Process _original_args.
458
  _args.clear();
4,944✔
459
  handleArg0();
4,944✔
460
  lexArguments();
4,944✔
461

462
  // Process _args.
463
  aliasExpansion();
4,944✔
464
  if (!findCommand()) {
4,944✔
465
    defaultCommand();
52✔
466
    if (!findCommand()) throw std::string("You must specify a command or a task to modify.");
52✔
467
  }
468

469
  demotion();
4,943✔
470
  canonicalizeNames();
4,943✔
471

472
  // Determine arg types: FILTER, MODIFICATION, MISCELLANEOUS.
473
  categorizeArgs();
4,943✔
474
  parenthesizeOriginalFilter();
4,942✔
475

476
  // Cache frequently looked up items
477
  _command = getCommand();
4,942✔
478
}
4,942✔
479

480
////////////////////////////////////////////////////////////////////////////////
481
// Process raw filter string.
482
// Insert filter arguments (wrapped in parentheses) immediatelly after the binary.
483
void CLI2::addFilter(const std::string& arg) {
555✔
484
  if (arg.length()) {
555✔
485
    std::vector<std::string> filter;
555✔
486
    filter.push_back("(");
555✔
487

488
    std::string lexeme;
555✔
489
    Lexer::Type type;
490
    Lexer lex(arg);
555✔
491

492
    while (lex.token(lexeme, type)) filter.push_back(lexeme);
1,649✔
493

494
    filter.push_back(")");
555✔
495
    add(filter);
555✔
496
    analyze();
555✔
497
  }
555✔
498
}
555✔
499

500
////////////////////////////////////////////////////////////////////////////////
501
// Process raw modification string.
502
// Insert modification arguments immediatelly after the command (i.e. 'add')
NEW
503
void CLI2::addModifications(const std::string& arg) {
×
NEW
504
  if (arg.length()) {
×
NEW
505
    std::vector<std::string> mods;
×
506

507
    std::string lexeme;
×
508
    Lexer::Type type;
NEW
509
    Lexer lex(arg);
×
510

NEW
511
    while (lex.token(lexeme, type)) mods.push_back(lexeme);
×
512

513
    // Determine at which argument index does the task modification command
514
    // reside
515
    unsigned int cmdIndex = 0;
×
NEW
516
    for (; cmdIndex < _args.size(); ++cmdIndex) {
×
517
      // Command found, stop iterating.
NEW
518
      if (_args[cmdIndex].hasTag("CMD")) break;
×
519
    }
520

521
    // Insert modifications after the command.
NEW
522
    add(mods, cmdIndex);
×
NEW
523
    analyze();
×
524
  }
525
}
526

527
////////////////////////////////////////////////////////////////////////////////
528
// There are situations where a context filter is applied. This method
529
// determines whether one applies, and if so, applies it. Disqualifiers include:
530
//   - filter contains ID or UUID
531
void CLI2::addContext(bool readable, bool writeable) {
3,050✔
532
  // Recursion block.
533
  if (_context_added) return;
6,079✔
534

535
  // Detect if any context is set, and bail out if not
536
  std::string contextString;
3,036✔
537
  if (readable)
3,036✔
538
    // Empty string is treated as "currently selected context"
539
    contextString = Context::getContext().getTaskContext("read", "");
1,408✔
540
  else if (writeable)
1,628✔
541
    contextString = Context::getContext().getTaskContext("write", "");
1,628✔
542
  else
543
    return;
×
544

545
  // If context is empty, bail out too
546
  if (contextString.empty()) return;
3,036✔
547

548
  // For readable contexts: Detect if UUID or ID is set, and bail out
549
  if (readable)
19✔
550
    for (auto& a : _args) {
71✔
551
      if (a._lextype == Lexer::Type::uuid || a._lextype == Lexer::Type::number ||
64✔
552
          a._lextype == Lexer::Type::set) {
52✔
553
        Context::getContext().debug(
24✔
554
            format("UUID/ID argument found '{1}', not applying context.", a.attribute("raw")));
24✔
555
        return;
12✔
556
      }
557
    }
558

559
  // Apply the context. Readable (filtering) takes precedence. Also set the
560
  // block now, since addFilter calls analyze(), which calls addContext().
561
  _context_added = true;
7✔
562
  if (readable)
7✔
563
    addFilter(contextString);
7✔
564
  else if (writeable)
×
NEW
565
    addModifications(contextString);
×
566

567
  // Inform the user about the application of context
568
  if (Context::getContext().verbose("context"))
7✔
569
    Context::getContext().footnote(format("Context '{1}' set. Use 'task context none' to remove.",
14✔
570
                                          Context::getContext().config.get("context")));
14✔
571
}
3,036✔
572

573
////////////////////////////////////////////////////////////////////////////////
574
// Parse the command line, identifiying filter components, expanding syntactic
575
// sugar as necessary.
576
void CLI2::prepareFilter() {
3,078✔
577
  // Clear and re-populate.
578
  _id_ranges.clear();
3,078✔
579
  _uuid_list.clear();
3,078✔
580
  _context_added = false;
3,078✔
581

582
  // Remove all the syntactic sugar for FILTERs.
583
  lexFilterArgs();
3,078✔
584
  findIDs();
3,078✔
585
  findUUIDs();
3,078✔
586
  insertIDExpr();
3,078✔
587
  desugarFilterPlainArgs();
3,078✔
588
  findStrayModifications();
3,078✔
589
  desugarFilterTags();
3,078✔
590
  desugarFilterAttributes();
3,078✔
591
  desugarFilterPatterns();
3,078✔
592
  insertJunctions();  // Deliberately after all desugar calls.
3,078✔
593

594
  if (Context::getContext().verbose("filter")) {
3,078✔
595
    std::string combined;
7✔
596
    for (const auto& a : _args) {
70✔
597
      if (a.hasTag("FILTER")) {
63✔
598
        if (combined != "") combined += ' ';
42✔
599

600
        combined += a.attribute("raw");
42✔
601
      }
602
    }
603

604
    if (combined.size()) Context::getContext().footnote(std::string("Filter: ") + combined);
7✔
605
  }
7✔
606
}
3,078✔
607

608
////////////////////////////////////////////////////////////////////////////////
609
// Return all the MISCELLANEOUS args as strings.
610
const std::vector<std::string> CLI2::getWords() {
1,108✔
611
  std::vector<std::string> words;
1,108✔
612
  for (const auto& a : _args)
6,307✔
613
    if (a.hasTag("MISCELLANEOUS")) words.push_back(a.attribute("raw"));
5,199✔
614

615
  if (Context::getContext().config.getInteger("debug.parser") >= 2) {
1,108✔
NEW
616
    Color colorOrigArgs("gray10 on gray4");
×
617
    std::string message = " ";
×
NEW
618
    for (const auto& word : words) message += colorOrigArgs.colorize(word) + ' ';
×
NEW
619
    Context::getContext().debug("CLI2::getWords" + message);
×
620
  }
621

622
  return words;
1,108✔
623
}
624

625
////////////////////////////////////////////////////////////////////////////////
626
// Return all the MISCELLANEOUS args.
627
const std::vector<A2> CLI2::getMiscellaneous() {
54✔
628
  std::vector<A2> misc;
54✔
629
  for (const auto& a : _args)
327✔
630
    if (a.hasTag("MISCELLANEOUS")) misc.push_back(a);
273✔
631

632
  return misc;
54✔
633
}
634

635
////////////////////////////////////////////////////////////////////////////////
636
// Search for 'value' in _entities category, return canonicalized value.
637
bool CLI2::canonicalize(std::string& canonicalized, const std::string& category,
24,775✔
638
                        const std::string& value) {
639
  // Utilize a cache mapping of (category, value) -> canonicalized value.
640
  // This cache does not need to be invalidated, because entities are defined
641
  // only once per initialization of the Context object.
642
  int cache_key = 31 * std::hash<std::string>{}(category) + std::hash<std::string>{}(value);
24,775✔
643
  auto cache_result = _canonical_cache.find(cache_key);
24,775✔
644
  if (cache_result != _canonical_cache.end()) {
24,775✔
645
    canonicalized = cache_result->second;
6,689✔
646
    return true;
6,689✔
647
  }
648

649
  // Extract a list of entities for category.
650
  std::vector<std::string> options;
18,086✔
651
  auto c = _entities.equal_range(category);
18,086✔
652
  for (auto e = c.first; e != c.second; ++e) {
876,641✔
653
    // Shortcut: if an exact match is found, success.
654
    if (value == e->second) {
861,456✔
655
      canonicalized = value;
2,901✔
656
      _canonical_cache[cache_key] = value;
2,901✔
657
      return true;
2,901✔
658
    }
659

660
    options.push_back(e->second);
858,555✔
661
  }
662

663
  // Match against the options, throw away results.
664
  std::vector<std::string> matches;
15,185✔
665
  if (autoComplete(value, options, matches, minimumMatchLength) == 1) {
15,185✔
666
    canonicalized = matches[0];
301✔
667
    _canonical_cache[cache_key] = matches[0];
301✔
668
    return true;
301✔
669
  }
670

671
  return false;
14,884✔
672
}
18,086✔
673

674
////////////////////////////////////////////////////////////////////////////////
675
std::string CLI2::getBinary() const {
3✔
676
  if (_args.size()) return _args[0].attribute("raw");
3✔
677

678
  return "";
×
679
}
680

681
////////////////////////////////////////////////////////////////////////////////
682
std::string CLI2::getCommand(bool canonical) const {
25,399✔
683
  // Shortcut if analysis has been finalized
684
  if (_command != "") return _command;
25,399✔
685

686
  for (const auto& a : _args)
21,642✔
687
    if (a.hasTag("CMD")) return a.attribute(canonical ? "canonical" : "raw");
21,642✔
688

689
  return "";
×
690
}
691

692
////////////////////////////////////////////////////////////////////////////////
693
const std::string CLI2::dump(const std::string& title) const {
26✔
694
  std::stringstream out;
26✔
695

696
  out << "\033[1m" << title << "\033[0m\n"
697
      << "  _original_args\n    ";
26✔
698

699
  Color colorArgs("gray10 on gray4");
26✔
700
  Color colorFilter("black on rgb311");
26✔
701
  for (auto i = _original_args.begin(); i != _original_args.end(); ++i) {
170✔
702
    if (i != _original_args.begin()) out << ' ';
144✔
703

704
    if (i->hasTag("ORIGINAL"))
144✔
705
      out << colorArgs.colorize(i->attribute("raw"));
79✔
706
    else
707
      out << colorFilter.colorize(i->attribute("raw"));
65✔
708
  }
709

710
  out << '\n';
26✔
711

712
  if (_args.size()) {
26✔
713
    out << "  _args\n";
22✔
714
    for (const auto& a : _args) out << "    " << a.dump() << '\n';
172✔
715
  }
716

717
  if (_id_ranges.size()) {
26✔
UNCOV
718
    out << "  _id_ranges\n    ";
×
NEW
719
    for (const auto& range : _id_ranges) {
×
UNCOV
720
      if (range.first != range.second)
×
NEW
721
        out << colorArgs.colorize(range.first + "-" + range.second) << ' ';
×
722
      else
NEW
723
        out << colorArgs.colorize(range.first) << ' ';
×
724
    }
725

726
    out << '\n';
×
727
  }
728

729
  if (_uuid_list.size()) {
26✔
UNCOV
730
    out << "  _uuid_list\n    ";
×
NEW
731
    for (const auto& uuid : _uuid_list) out << colorArgs.colorize(uuid) << ' ';
×
732

733
    out << '\n';
×
734
  }
735

736
  return out.str();
52✔
737
}
26✔
738

739
////////////////////////////////////////////////////////////////////////////////
740
// If any aliases are found in un-TERMINATED arguments, replace the alias with
741
// a set of Lexed tokens from the configuration.
742
void CLI2::aliasExpansion() {
4,944✔
743
  bool changes = false;
4,944✔
744
  bool action;
745
  int counter = 0;
4,944✔
746
  do {
747
    action = false;
4,964✔
748
    std::vector<A2> reconstructed;
4,964✔
749

750
    std::string raw;
4,964✔
751
    for (const auto& i : _args) {
25,877✔
752
      raw = i.attribute("raw");
20,913✔
753
      if (i.hasTag("TERMINATED")) {
20,913✔
754
        reconstructed.push_back(i);
2,163✔
755
      } else if (_aliases.find(raw) != _aliases.end()) {
18,750✔
756
        std::string lexeme;
20✔
757
        Lexer::Type type;
758
        Lexer lex(_aliases[raw]);
20✔
759
        while (lex.token(lexeme, type)) reconstructed.emplace_back(lexeme, type);
50✔
760

761
        action = true;
20✔
762
        changes = true;
20✔
763
      } else {
20✔
764
        reconstructed.push_back(i);
18,730✔
765
      }
766
    }
767

768
    _args = reconstructed;
4,964✔
769

770
    std::vector<A2> reconstructedOriginals;
4,964✔
771
    bool terminated = false;
4,964✔
772
    for (const auto& i : _original_args) {
25,877✔
773
      if (i.attribute("raw") == "--") terminated = true;
20,913✔
774

775
      if (terminated) {
20,913✔
776
        reconstructedOriginals.push_back(i);
2,324✔
777
      } else if (_aliases.find(i.attribute("raw")) != _aliases.end()) {
18,589✔
778
        std::string lexeme;
20✔
779
        Lexer::Type type;
780
        Lexer lex(_aliases[i.attribute("raw")]);
40✔
781
        while (lex.token(lexeme, type)) reconstructedOriginals.emplace_back(lexeme, type);
50✔
782

783
        action = true;
20✔
784
        changes = true;
20✔
785
      } else {
20✔
786
        reconstructedOriginals.push_back(i);
18,569✔
787
      }
788
    }
789

790
    _original_args = reconstructedOriginals;
4,964✔
791
  } while (action && counter++ < safetyValveDefault);
4,964✔
792

793
  if (counter >= safetyValveDefault)
4,944✔
NEW
794
    Context::getContext().debug(format("Nested alias limit of {1} reached.", safetyValveDefault));
×
795

796
  if (changes && Context::getContext().config.getInteger("debug.parser") >= 2)
4,944✔
NEW
797
    Context::getContext().debug(dump("CLI2::analyze aliasExpansion"));
×
798
}
4,944✔
799

800
////////////////////////////////////////////////////////////////////////////////
801
// Scan all arguments and canonicalize names that need it.
802
void CLI2::canonicalizeNames() {
4,943✔
803
  bool changes = false;
4,943✔
804
  for (auto& a : _args) {
25,862✔
805
    if (a._lextype == Lexer::Type::pair) {
20,919✔
806
      std::string raw = a.attribute("raw");
5,860✔
807
      if (raw.substr(0, 3) != "rc:" && raw.substr(0, 3) != "rc.") {
2,930✔
808
        std::string name = a.attribute("name");
4,644✔
809
        std::string canonical;
2,322✔
810
        if (canonicalize(canonical, "pseudo", name) || canonicalize(canonical, "attribute", name)) {
2,322✔
811
          a.attribute("canonical", canonical);
2,321✔
812
        } else {
813
          a._lextype = Lexer::Type::word;
1✔
814
        }
815

816
        changes = true;
2,322✔
817
      }
2,322✔
818
    }
2,930✔
819
  }
820

821
  if (changes && Context::getContext().config.getInteger("debug.parser") >= 2)
4,943✔
822
    Context::getContext().debug(dump("CLI2::analyze canonicalizeNames"));
2✔
823
}
4,943✔
824

825
////////////////////////////////////////////////////////////////////////////////
826
// Categorize FILTER, MODIFICATION and MISCELLANEOUS args, based on CMD DNA.
827
void CLI2::categorizeArgs() {
4,943✔
828
  // Context is only applied for commands that request it.
829
  std::string command = getCommand();
4,943✔
830
  Command* cmd = Context::getContext().commands[command];
4,943✔
831

832
  // Determine if the command uses Context. CmdCustom and CmdTimesheet need to
833
  // be handled separately, as they override the parent Command::use_context
834
  // method, and this is a pointer to Command class.
835
  //
836
  // All Command classes overriding uses_context () getter need to be specified
837
  // here.
838
  bool uses_context;
839
  if (dynamic_cast<CmdCustom*>(cmd))
4,943✔
840
    uses_context = (dynamic_cast<CmdCustom*>(cmd))->uses_context();
1,141✔
841
  else if (dynamic_cast<CmdTimesheet*>(cmd))
3,802✔
842
    uses_context = (dynamic_cast<CmdTimesheet*>(cmd))->uses_context();
3✔
843
  else if (cmd)
3,799✔
844
    uses_context = cmd->uses_context();
3,799✔
845

846
  // Apply the context, if applicable
847
  if (cmd && uses_context) addContext(cmd->accepts_filter(), cmd->accepts_modifications());
4,943✔
848

849
  bool changes = false;
4,943✔
850
  bool afterCommand = false;
4,943✔
851
  for (auto& a : _args) {
25,886✔
852
    if (a._lextype == Lexer::Type::separator) continue;
20,944✔
853

854
    // Record that the command has been found, it affects behavior.
855
    if (a.hasTag("CMD")) {
20,171✔
856
      afterCommand = true;
4,943✔
857
    }
858

859
    // Skip admin args.
860
    else if (a.hasTag("BINARY") || a.hasTag("RC") || a.hasTag("CONFIG")) {
15,228✔
861
      // NOP.
862
    }
863

864
    // All combinations, with all 8 cases handled below.:
865
    //
866
    //   -- -- --   Error: found an arg, but none expected
867
    //   -- -- Mi   task [Mi] <cmd> [Mi]
868
    //   -- Mo --   task [Mo] <cmd> [Mo]
869
    //   -- Mo Mi   Internally inconsistent
870
    //   Fi -- --   task [Fi] <cmd> [Fi]
871
    //   Fi -- Mi   task [Fi] <cmd> [Mi]
872
    //   Fi Mo --   task [Fi] <cmd> [Mo]
873
    //   Fi Mo Mi   Internally inconsistent
874
    //
875
    else if (cmd && !cmd->accepts_filter() && !cmd->accepts_modifications() &&
12,168✔
876
             !cmd->accepts_miscellaneous()) {
2,491✔
877
      // No commands were expected --> error.
878
      throw format("The '{1}' command does not allow '{2}'.", command, a.attribute("raw"));
1✔
879
    } else if (cmd && !cmd->accepts_filter() && !cmd->accepts_modifications() &&
12,166✔
880
               cmd->accepts_miscellaneous()) {
2,490✔
881
      a.tag("MISCELLANEOUS");
2,490✔
882
      changes = true;
2,490✔
883
    } else if (cmd && !cmd->accepts_filter() && cmd->accepts_modifications() &&
10,364✔
884
               !cmd->accepts_miscellaneous()) {
3,178✔
885
      a.tag("MODIFICATION");
3,178✔
886
      changes = true;
3,178✔
887
    } else if (cmd && !cmd->accepts_filter() && cmd->accepts_modifications() &&
4,008✔
NEW
888
               cmd->accepts_miscellaneous()) {
×
889
      // Error: internally inconsistent.
NEW
890
      throw std::string("Unknown error. Please report.");
×
891
    } else if (cmd && cmd->accepts_filter() && !cmd->accepts_modifications() &&
7,273✔
892
               !cmd->accepts_miscellaneous()) {
3,265✔
893
      a.tag("FILTER");
3,170✔
894
      changes = true;
3,170✔
895
    } else if (cmd && cmd->accepts_filter() && !cmd->accepts_modifications() &&
933✔
896
               cmd->accepts_miscellaneous()) {
95✔
897
      if (!afterCommand)
95✔
898
        a.tag("FILTER");
83✔
899
      else
900
        a.tag("MISCELLANEOUS");
12✔
901

902
      changes = true;
95✔
903
    } else if (cmd && cmd->accepts_filter() && cmd->accepts_modifications() &&
1,486✔
904
               !cmd->accepts_miscellaneous()) {
743✔
905
      if (!afterCommand)
743✔
906
        a.tag("FILTER");
431✔
907
      else
908
        a.tag("MODIFICATION");
312✔
909

910
      changes = true;
743✔
NEW
911
    } else if (cmd && cmd->accepts_filter() && cmd->accepts_modifications() &&
×
NEW
912
               cmd->accepts_miscellaneous()) {
×
913
      // Error: internally inconsistent.
NEW
914
      throw std::string("Unknown error. Please report.");
×
915
    }
916
  }
917

918
  if (changes && Context::getContext().config.getInteger("debug.parser") >= 2)
4,942✔
919
    Context::getContext().debug(dump("CLI2::analyze categorizeArgs"));
2✔
920
}
4,943✔
921

922
////////////////////////////////////////////////////////////////////////////////
923
// The following command:
924
//
925
//    task +home or +work list
926
//
927
// Is reasonable, and does not work unless the filter is parenthesized. Ignoring
928
// context, the 'list' report has a filter, which is inserted at the beginning
929
// like this:
930
//
931
//   task ( status:pending ) +home or +work list
932
//
933
// Parenthesizing the user-provided (original) filter yields this:
934
//
935
//   task ( status:pending ) ( +home or +work ) list
936
//
937
// And when the conjunction is added:
938
//
939
//   task ( status:pending ) and ( +home or +work ) list
940
//
941
// the query is correct.
942
void CLI2::parenthesizeOriginalFilter() {
4,942✔
943
  // Locate the first and last ORIGINAL FILTER args.
944
  unsigned int firstOriginalFilter = 0;
4,942✔
945
  unsigned int lastOriginalFilter = 0;
4,942✔
946
  for (unsigned int i = 1; i < _args.size(); ++i) {
20,941✔
947
    if (_args[i].hasTag("FILTER") && _args[i].hasTag("ORIGINAL")) {
15,999✔
948
      if (firstOriginalFilter == 0) firstOriginalFilter = i;
1,592✔
949

950
      lastOriginalFilter = i;
1,592✔
951
    }
952
  }
953

954
  // If found, parenthesize the arg list accordingly.
955
  if (firstOriginalFilter && lastOriginalFilter) {
4,942✔
956
    std::vector<A2> reconstructed;
1,321✔
957
    for (unsigned int i = 0; i < _args.size(); ++i) {
7,353✔
958
      if (i == firstOriginalFilter) {
6,032✔
959
        A2 openParen("(", Lexer::Type::op);
2,642✔
960
        openParen.tag("ORIGINAL");
1,321✔
961
        openParen.tag("FILTER");
1,321✔
962
        reconstructed.push_back(openParen);
1,321✔
963
      }
1,321✔
964

965
      reconstructed.push_back(_args[i]);
6,032✔
966

967
      if (i == lastOriginalFilter) {
6,032✔
968
        A2 closeParen(")", Lexer::Type::op);
2,642✔
969
        closeParen.tag("ORIGINAL");
1,321✔
970
        closeParen.tag("FILTER");
1,321✔
971
        reconstructed.push_back(closeParen);
1,321✔
972
      }
1,321✔
973
    }
974

975
    _args = reconstructed;
1,321✔
976

977
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
1,321✔
NEW
978
      Context::getContext().debug(dump("CLI2::analyze parenthesizeOriginalFilter"));
×
979
  }
1,321✔
980
}
4,942✔
981

982
////////////////////////////////////////////////////////////////////////////////
983
// Scan all arguments and if any are an exact match for a command name, then
984
// tag as CMD. If an argument is an exact match for an attribute, despite being
985
// an inexact match for a command, then it is not a command.
986
bool CLI2::findCommand() {
4,996✔
987
  for (auto& a : _args) {
13,575✔
988
    std::string raw = a.attribute("raw");
27,044✔
989
    std::string canonical;
13,522✔
990

991
    // If the arg canonicalized to a 'cmd', but is also not an exact match
992
    // for an 'attribute', proceed. Example:
993
    //   task project=foo list
994
    //        ^cmd        ^cmd
995
    //        ^attribute
996
    if (exactMatch("cmd", raw))
13,522✔
997
      canonical = raw;
4,826✔
998
    else if (exactMatch("attribute", raw))
8,696✔
999
      continue;
2✔
1000
    else if (!canonicalize(canonical, "cmd", raw))
8,694✔
1001
      continue;
8,577✔
1002

1003
    a.attribute("canonical", canonical);
4,943✔
1004
    a.tag("CMD");
4,943✔
1005

1006
    // Apply command DNA as tags.
1007
    Command* command = Context::getContext().commands[canonical];
4,943✔
1008
    if (command->read_only()) a.tag("READONLY");
4,943✔
1009
    if (command->displays_id()) a.tag("SHOWSID");
4,943✔
1010
    if (command->needs_gc()) a.tag("RUNSGC");
4,943✔
1011
    if (command->uses_context()) a.tag("USESCONTEXT");
4,943✔
1012
    if (command->accepts_filter()) a.tag("ALLOWSFILTER");
4,943✔
1013
    if (command->accepts_modifications()) a.tag("ALLOWSMODIFICATIONS");
4,943✔
1014
    if (command->accepts_miscellaneous()) a.tag("ALLOWSMISC");
4,943✔
1015

1016
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
4,943✔
1017
      Context::getContext().debug(dump("CLI2::analyze findCommand"));
4✔
1018

1019
    // Stop and indicate command found.
1020
    return true;
4,943✔
1021
  }
27,044✔
1022

1023
  // Indicate command not found.
1024
  return false;
53✔
1025
}
1026

1027
////////////////////////////////////////////////////////////////////////////////
1028
// Search for exact 'value' in _entities category.
1029
bool CLI2::exactMatch(const std::string& category, const std::string& value) const {
22,218✔
1030
  // Extract a list of entities for category.
1031
  auto c = _entities.equal_range(category);
22,218✔
1032
  for (auto e = c.first; e != c.second; ++e)
1,187,614✔
1033
    if (value == e->second) return true;
1,170,224✔
1034

1035
  return false;
17,390✔
1036
}
1037

1038
////////////////////////////////////////////////////////////////////////////////
1039
// +tag --> tags _hastag_ tag
1040
// -tag --> tags _notag_ tag
1041
void CLI2::desugarFilterTags() {
3,078✔
1042
  bool changes = false;
3,078✔
1043
  std::vector<A2> reconstructed;
3,078✔
1044
  for (const auto& a : _args) {
22,441✔
1045
    if (a._lextype == Lexer::Type::tag && a.hasTag("FILTER")) {
19,363✔
1046
      changes = true;
644✔
1047

1048
      A2 left("tags", Lexer::Type::dom);
1,288✔
1049
      left.tag("FILTER");
644✔
1050
      reconstructed.push_back(left);
644✔
1051

1052
      std::string raw = a.attribute("raw");
1,288✔
1053

1054
      A2 op(raw[0] == '+' ? "_hastag_" : "_notag_", Lexer::Type::op);
1,288✔
1055
      op.tag("FILTER");
644✔
1056
      reconstructed.push_back(op);
644✔
1057

1058
      A2 right("" + raw.substr(1) + "", Lexer::Type::string);
1,288✔
1059
      right.tag("FILTER");
644✔
1060
      reconstructed.push_back(right);
644✔
1061
    } else
644✔
1062
      reconstructed.push_back(a);
18,719✔
1063
  }
1064

1065
  if (changes) {
3,078✔
1066
    _args = reconstructed;
583✔
1067

1068
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
583✔
1069
      Context::getContext().debug(dump("CLI2::prepareFilter desugarFilterTags"));
2✔
1070
  }
1071
}
3,078✔
1072

1073
////////////////////////////////////////////////////////////////////////////////
1074
void CLI2::findStrayModifications() {
3,078✔
1075
  bool changes = false;
3,078✔
1076

1077
  auto command = getCommand();
3,078✔
1078
  if (command == "add" || command == "log") {
3,078✔
1079
    for (auto& a : _args) {
8,097✔
1080
      if (a.hasTag("FILTER")) {
6,469✔
NEW
1081
        a.unTag("FILTER");
×
NEW
1082
        a.tag("MODIFICATION");
×
UNCOV
1083
        changes = true;
×
1084
      }
1085
    }
1086
  }
1087

1088
  if (changes)
3,078✔
NEW
1089
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
×
NEW
1090
      Context::getContext().debug(dump("CLI2::prepareFilter findStrayModifications"));
×
1091
}
3,078✔
1092

1093
////////////////////////////////////////////////////////////////////////////////
1094
// <name>[.<mod>]:['"][<value>]['"] --> name <op> value
1095
void CLI2::desugarFilterAttributes() {
3,078✔
1096
  bool changes = false;
3,078✔
1097
  std::vector<A2> reconstructed;
3,078✔
1098
  for (auto& a : _args) {
23,729✔
1099
    if (a._lextype == Lexer::Type::pair && a.hasTag("FILTER")) {
20,651✔
1100
      std::string raw = a.attribute("raw");
1,616✔
1101
      std::string name = a.attribute("name");
1,616✔
1102
      std::string mod = a.attribute("modifier");
1,616✔
1103
      std::string sep = a.attribute("separator");
1,616✔
1104
      std::string value = a.attribute("value");
1,616✔
1105

1106
      // An unquoted string, while equivalent to an empty string, doesn't cause
1107
      // an operand shortage in eval.
1108
      if (value == "") value = "''";
808✔
1109

1110
      // Some values are expressions, which need to be lexed. The best way to
1111
      // determine whether an expression is either a single value, or needs to
1112
      // be lexed, is to lex it and count the tokens. For example:
1113
      //    now+1d
1114
      // This should be lexed and surrounded by parentheses:
1115
      //    (
1116
      //    now
1117
      //    +
1118
      //    1d
1119
      //    )
1120
      // Use this sequence in place of a single value.
1121
      std::vector<A2> values = lexExpression(value);
808✔
1122
      if (Context::getContext().config.getInteger("debug.parser") >= 2) {
808✔
1123
        Context::getContext().debug("CLI2::lexExpression " + name + ':' + value);
2✔
1124
        for (auto& v : values) Context::getContext().debug("  " + v.dump());
4✔
1125
        Context::getContext().debug(" ");
2✔
1126
      }
1127

1128
      bool found = false;
808✔
1129
      std::string canonical;
808✔
1130
      if (canonicalize(canonical, "attribute", name)) {
808✔
1131
        // Certain attribute types do not suport math.
1132
        //   string   --> no
1133
        //   numeric  --> yes
1134
        //   date     --> yes
1135
        //   duration --> yes
1136
        bool evalSupported = true;
751✔
1137
        Column* col = Context::getContext().columns[canonical];
751✔
1138
        if (col && col->type() == "string") evalSupported = false;
751✔
1139

1140
        A2 lhs(name, Lexer::Type::dom);
751✔
1141
        lhs.tag("FILTER");
751✔
1142
        lhs.attribute("canonical", canonical);
751✔
1143
        lhs.attribute("modifier", mod);
751✔
1144

1145
        A2 op("", Lexer::Type::op);
1,502✔
1146
        op.tag("FILTER");
751✔
1147

1148
        // Attribute types that do not support evaluation should be interpreted
1149
        // as strings (currently this means that string attributes are not evaluated)
1150
        A2 rhs("", evalSupported ? values[0]._lextype : Lexer::Type::string);
1,502✔
1151
        rhs.tag("FILTER");
751✔
1152

1153
        // Special case for '<name>:<value>'.
1154
        if (mod == "") {
751✔
1155
          op.attribute("raw", "=");
630✔
1156
          rhs.attribute("raw", value);
630✔
1157
        } else if (mod == "before" || mod == "under" || mod == "below") {
121✔
1158
          op.attribute("raw", "<");
17✔
1159
          rhs.attribute("raw", value);
17✔
1160
        } else if (mod == "after" || mod == "over" || mod == "above") {
104✔
1161
          op.attribute("raw", ">");
18✔
1162
          rhs.attribute("raw", value);
18✔
1163
        } else if (mod == "by") {
86✔
1164
          op.attribute("raw", "<=");
3✔
1165
          rhs.attribute("raw", value);
3✔
1166
        } else if (mod == "none") {
83✔
1167
          op.attribute("raw", "==");
2✔
1168
          rhs.attribute("raw", "''");
2✔
1169
        } else if (mod == "any") {
81✔
NEW
1170
          op.attribute("raw", "!==");
×
NEW
1171
          rhs.attribute("raw", "''");
×
1172
        } else if (mod == "is" || mod == "equals") {
81✔
1173
          op.attribute("raw", "==");
1✔
1174
          rhs.attribute("raw", value);
1✔
1175
        } else if (mod == "not") {
80✔
1176
          op.attribute("raw", "!=");
9✔
1177
          rhs.attribute("raw", value);
9✔
1178
        } else if (mod == "isnt") {
71✔
1179
          op.attribute("raw", "!==");
13✔
1180
          rhs.attribute("raw", value);
13✔
1181
        } else if (mod == "has" || mod == "contains") {
58✔
1182
          op.attribute("raw", "~");
50✔
1183
          rhs.attribute("raw", value);
50✔
1184
        } else if (mod == "hasnt") {
8✔
1185
          op.attribute("raw", "!~");
3✔
1186
          rhs.attribute("raw", value);
3✔
1187
        } else if (mod == "startswith" || mod == "left") {
5✔
1188
          op.attribute("raw", "~");
3✔
1189
          rhs.attribute("raw", "^" + value);
3✔
1190
        } else if (mod == "endswith" || mod == "right") {
2✔
1191
          op.attribute("raw", "~");
2✔
1192
          rhs.attribute("raw", value + "$");
2✔
NEW
1193
        } else if (mod == "word") {
×
NEW
1194
          op.attribute("raw", "~");
×
1195
#if defined(DARWIN)
1196
          rhs.attribute("raw", value);
1197
#elif defined(SOLARIS)
1198
          rhs.attribute("raw", "\\<" + value + "\\>");
1199
#else
NEW
1200
          rhs.attribute("raw", "\\b" + value + "\\b");
×
1201
#endif
NEW
1202
        } else if (mod == "noword") {
×
NEW
1203
          op.attribute("raw", "!~");
×
1204
#if defined(DARWIN)
1205
          rhs.attribute("raw", value);
1206
#elif defined(SOLARIS)
1207
          rhs.attribute("raw", "\\<" + value + "\\>");
1208
#else
NEW
1209
          rhs.attribute("raw", "\\b" + value + "\\b");
×
1210
#endif
1211
        } else
NEW
1212
          throw format("Error: unrecognized attribute modifier '{1}'.", mod);
×
1213

1214
        reconstructed.push_back(lhs);
751✔
1215
        reconstructed.push_back(op);
751✔
1216

1217
        // Do not modify this construct without full understanding.
1218
        // Getting this wrong breaks a whole lot of filtering tests.
1219
        if (evalSupported) {
751✔
1220
          for (auto& v : values) reconstructed.push_back(v);
116✔
1221
        } else if (Lexer::isDOM(rhs.attribute("raw"))) {
706✔
1222
          rhs._lextype = Lexer::Type::dom;
4✔
1223
          reconstructed.push_back(rhs);
4✔
1224
        } else {
1225
          reconstructed.push_back(rhs);
702✔
1226
        }
1227

1228
        found = true;
751✔
1229
      }
751✔
1230

1231
      // If the name does not canonicalize to either an attribute or a UDA
1232
      // then it is not a recognized Lexer::Type::pair, so downgrade it to
1233
      // Lexer::Type::word.
1234
      else {
1235
        a._lextype = Lexer::Type::word;
57✔
1236
      }
1237

1238
      if (found)
808✔
1239
        changes = true;
751✔
1240
      else
1241
        reconstructed.push_back(a);
57✔
1242
    }
808✔
1243
    // Not a FILTER pair.
1244
    else
1245
      reconstructed.push_back(a);
19,843✔
1246
  }
1247

1248
  if (changes) {
3,078✔
1249
    _args = reconstructed;
536✔
1250

1251
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
536✔
1252
      Context::getContext().debug(dump("CLI2::prepareFilter desugarFilterAttributes"));
2✔
1253
  }
1254
}
3,078✔
1255

1256
////////////////////////////////////////////////////////////////////////////////
1257
// /pattern/ --> description ~ 'pattern'
1258
void CLI2::desugarFilterPatterns() {
3,078✔
1259
  bool changes = false;
3,078✔
1260
  std::vector<A2> reconstructed;
3,078✔
1261
  for (const auto& a : _args) {
25,257✔
1262
    if (a._lextype == Lexer::Type::pattern && a.hasTag("FILTER")) {
22,179✔
1263
      changes = true;
68✔
1264

1265
      A2 lhs("description", Lexer::Type::dom);
136✔
1266
      lhs.tag("FILTER");
68✔
1267
      reconstructed.push_back(lhs);
68✔
1268

1269
      A2 op("~", Lexer::Type::op);
136✔
1270
      op.tag("FILTER");
68✔
1271
      reconstructed.push_back(op);
68✔
1272

1273
      A2 rhs(a.attribute("pattern"), Lexer::Type::string);
136✔
1274
      rhs.attribute("flags", a.attribute("flags"));
68✔
1275
      rhs.tag("FILTER");
68✔
1276
      reconstructed.push_back(rhs);
68✔
1277
    } else
68✔
1278
      reconstructed.push_back(a);
22,111✔
1279
  }
1280

1281
  if (changes) {
3,078✔
1282
    _args = reconstructed;
64✔
1283

1284
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
64✔
NEW
1285
      Context::getContext().debug(dump("CLI2::prepareFilter desugarFilterPatterns"));
×
1286
  }
1287
}
3,078✔
1288

1289
////////////////////////////////////////////////////////////////////////////////
1290
// An ID sequence can be:
1291
//
1292
//   a single ID:          1
1293
//   a list of IDs:        1,3,5
1294
//   a list of IDs:        1 3 5
1295
//   a range:              5-10
1296
//   or a combination:     1,3,5-10 12
1297
//
1298
void CLI2::findIDs() {
3,078✔
1299
  bool changes = false;
3,078✔
1300

1301
  if (Context::getContext().config.getBoolean("sugar")) {
3,078✔
1302
    bool previousFilterArgWasAnOperator = false;
3,076✔
1303
    int filterCount = 0;
3,076✔
1304

1305
    for (const auto& a : _args) {
18,699✔
1306
      if (a.hasTag("FILTER")) {
15,623✔
1307
        ++filterCount;
5,291✔
1308

1309
        if (a._lextype == Lexer::Type::number) {
5,291✔
1310
          // Skip any number that was preceded by an operator.
1311
          if (!previousFilterArgWasAnOperator) {
510✔
1312
            changes = true;
505✔
1313
            std::string number = a.attribute("raw");
1,010✔
1314
            _id_ranges.emplace_back(number, number);
505✔
1315
          }
505✔
1316
        } else if (a._lextype == Lexer::Type::set) {
4,781✔
1317
          // Split the ID list into elements.
1318
          auto elements = split(a.attribute("raw"), ',');
54✔
1319

1320
          for (auto& element : elements) {
65✔
1321
            changes = true;
38✔
1322
            auto hyphen = element.find('-');
38✔
1323
            if (hyphen != std::string::npos)
38✔
1324
              _id_ranges.emplace_back(element.substr(0, hyphen), element.substr(hyphen + 1));
17✔
1325
            else
1326
              _id_ranges.emplace_back(element, element);
21✔
1327
          }
1328
        }
27✔
1329

1330
        std::string raw = a.attribute("raw");
10,582✔
1331
        previousFilterArgWasAnOperator =
5,291✔
1332
            (a._lextype == Lexer::Type::op && raw != "(" && raw != ")") ? true : false;
5,291✔
1333
      }
5,291✔
1334
    }
1335

1336
    // If no IDs were found, and no filter was specified, look for number/set
1337
    // listed as a MODIFICATION.
1338
    std::string command = getCommand();
3,076✔
1339

1340
    if (!_id_ranges.size() && filterCount == 0 && command != "add" && command != "log") {
3,076✔
1341
      for (auto& a : _args) {
1,144✔
1342
        if (a.hasTag("MODIFICATION")) {
906✔
1343
          std::string raw = a.attribute("raw");
8✔
1344

1345
          // For a number to be an ID, it must not contain any sign or floating
1346
          // point elements.
NEW
1347
          if (a._lextype == Lexer::Type::number && raw.find('.') == std::string::npos &&
×
1348
              raw.find('e') == std::string::npos && raw.find('-') == std::string::npos) {
4✔
1349
            changes = true;
×
NEW
1350
            a.unTag("MODIFICATION");
×
NEW
1351
            a.tag("FILTER");
×
NEW
1352
            _id_ranges.emplace_back(raw, raw);
×
1353
          } else if (a._lextype == Lexer::Type::set) {
4✔
NEW
1354
            a.unTag("MODIFICATION");
×
NEW
1355
            a.tag("FILTER");
×
1356

1357
            // Split the ID list into elements.
NEW
1358
            auto elements = split(raw, ',');
×
1359

NEW
1360
            for (const auto& element : elements) {
×
UNCOV
1361
              changes = true;
×
NEW
1362
              auto hyphen = element.find('-');
×
1363
              if (hyphen != std::string::npos)
×
NEW
1364
                _id_ranges.emplace_back(element.substr(0, hyphen), element.substr(hyphen + 1));
×
1365
              else
NEW
1366
                _id_ranges.emplace_back(element, element);
×
1367
            }
1368
          }
1369
        }
4✔
1370
      }
1371
    }
1372
  }
3,076✔
1373

1374
  // Sugar-free.
1375
  else {
1376
    std::vector<A2> reconstructed;
2✔
1377
    for (const auto& a : _args) {
18✔
1378
      if (a.hasTag("FILTER") && a._lextype == Lexer::Type::number) {
16✔
1379
        changes = true;
4✔
1380
        A2 pair("id:" + a.attribute("raw"), Lexer::Type::pair);
8✔
1381
        pair.tag("FILTER");
4✔
1382
        pair.decompose();
4✔
1383
        reconstructed.push_back(pair);
4✔
1384
      } else
4✔
1385
        reconstructed.push_back(a);
12✔
1386
    }
1387

1388
    if (changes) _args = reconstructed;
2✔
1389
  }
2✔
1390

1391
  if (changes)
3,078✔
1392
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
523✔
NEW
1393
      Context::getContext().debug(dump("CLI2::prepareFilter findIDs"));
×
1394
}
3,078✔
1395

1396
////////////////////////////////////////////////////////////////////////////////
1397
void CLI2::findUUIDs() {
3,078✔
1398
  bool changes = false;
3,078✔
1399

1400
  if (Context::getContext().config.getBoolean("sugar")) {
3,078✔
1401
    for (const auto& a : _args) {
18,699✔
1402
      if (a._lextype == Lexer::Type::uuid && a.hasTag("FILTER")) {
15,623✔
1403
        changes = true;
65✔
1404
        _uuid_list.push_back(a.attribute("raw"));
65✔
1405
      }
1406
    }
1407

1408
    if (!_uuid_list.size()) {
3,076✔
1409
      for (auto& a : _args) {
18,178✔
1410
        if (a._lextype == Lexer::Type::uuid && a.hasTag("MODIFICATION")) {
15,164✔
1411
          changes = true;
×
NEW
1412
          a.unTag("MODIFICATION");
×
NEW
1413
          a.tag("FILTER");
×
NEW
1414
          _uuid_list.push_back(a.attribute("raw"));
×
1415
        }
1416
      }
1417
    }
1418
  }
1419

1420
  // Sugar-free.
1421
  else {
1422
    std::vector<A2> reconstructed;
2✔
1423
    for (const auto& a : _args) {
18✔
1424
      if (a.hasTag("FILTER") && a._lextype == Lexer::Type::uuid) {
16✔
1425
        changes = true;
×
NEW
1426
        A2 pair("uuid:" + a.attribute("raw"), Lexer::Type::pair);
×
NEW
1427
        pair.tag("FILTER");
×
NEW
1428
        pair.decompose();
×
NEW
1429
        reconstructed.push_back(pair);
×
NEW
1430
      } else
×
1431
        reconstructed.push_back(a);
16✔
1432
    }
1433

1434
    if (changes) _args = reconstructed;
2✔
1435
  }
2✔
1436

1437
  if (changes)
3,078✔
1438
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
62✔
NEW
1439
      Context::getContext().debug(dump("CLI2::prepareFilter findUUIDs"));
×
1440
}
3,078✔
1441

1442
////////////////////////////////////////////////////////////////////////////////
1443
void CLI2::insertIDExpr() {
3,078✔
1444
  // Skip completely if no ID/UUID was found. This is because below, '(' and ')'
1445
  // are inserted regardless of list size.
1446
  if (!_id_ranges.size() && !_uuid_list.size()) return;
3,078✔
1447

1448
  // Find the *first* occurence of lexer type set/number/uuid, and replace it
1449
  // with a synthesized expression. All other occurences are eaten.
1450
  bool changes = false;
583✔
1451
  bool foundID = false;
583✔
1452
  std::vector<A2> reconstructed;
583✔
1453
  for (const auto& a : _args) {
4,090✔
1454
    if ((a._lextype == Lexer::Type::set || a._lextype == Lexer::Type::number ||
3,480✔
1455
         a._lextype == Lexer::Type::uuid) &&
7,079✔
1456
        a.hasTag("FILTER")) {
4,104✔
1457
      if (!foundID) {
597✔
1458
        foundID = true;
583✔
1459
        changes = true;
583✔
1460

1461
        // Construct a single sequence that represents all _id_ranges and
1462
        // _uuid_list in one clause. This is essentially converting this:
1463
        //
1464
        //   1,2-3 uuid,uuid uuid 4
1465
        //
1466
        // into:
1467
        //
1468
        //   (
1469
        //        ( id == 1 )
1470
        //     or ( ( id >= 2 ) and ( id <= 3 ) )
1471
        //     or ( id == 4 )
1472
        //     or ( uuid = $UUID )
1473
        //     or ( uuid = $UUID )
1474
        //   )
1475

1476
        // Building block operators.
1477
        A2 openParen("(", Lexer::Type::op);
1,166✔
1478
        openParen.tag("FILTER");
583✔
1479
        A2 closeParen(")", Lexer::Type::op);
1,166✔
1480
        closeParen.tag("FILTER");
583✔
1481
        A2 opOr("or", Lexer::Type::op);
1,166✔
1482
        opOr.tag("FILTER");
583✔
1483
        A2 opAnd("and", Lexer::Type::op);
1,166✔
1484
        opAnd.tag("FILTER");
583✔
1485
        A2 opSimilar("=", Lexer::Type::op);
1,166✔
1486
        opSimilar.tag("FILTER");
583✔
1487
        A2 opEqual("==", Lexer::Type::op);
1,166✔
1488
        opEqual.tag("FILTER");
583✔
1489
        A2 opGTE(">=", Lexer::Type::op);
1,166✔
1490
        opGTE.tag("FILTER");
583✔
1491
        A2 opLTE("<=", Lexer::Type::op);
1,166✔
1492
        opLTE.tag("FILTER");
583✔
1493

1494
        // Building block attributes.
1495
        A2 argID("id", Lexer::Type::dom);
1,166✔
1496
        argID.tag("FILTER");
583✔
1497

1498
        A2 argUUID("uuid", Lexer::Type::dom);
1,166✔
1499
        argUUID.tag("FILTER");
583✔
1500

1501
        reconstructed.push_back(openParen);
583✔
1502

1503
        // Add all ID ranges.
1504
        for (auto r = _id_ranges.begin(); r != _id_ranges.end(); ++r) {
1,126✔
1505
          if (r != _id_ranges.begin()) reconstructed.push_back(opOr);
543✔
1506

1507
          if (r->first == r->second) {
543✔
1508
            reconstructed.push_back(openParen);
526✔
1509
            reconstructed.push_back(argID);
526✔
1510
            reconstructed.push_back(opEqual);
526✔
1511

1512
            A2 value(r->first, Lexer::Type::number);
526✔
1513
            value.tag("FILTER");
526✔
1514
            reconstructed.push_back(value);
526✔
1515

1516
            reconstructed.push_back(closeParen);
526✔
1517
          } else {
526✔
1518
            bool ascending = true;
17✔
1519
            int low = strtol(r->first.c_str(), nullptr, 10);
17✔
1520
            int high = strtol(r->second.c_str(), nullptr, 10);
17✔
1521
            if (low <= high)
17✔
1522
              ascending = true;
17✔
1523
            else
1524
              ascending = false;
×
1525

1526
            reconstructed.push_back(openParen);
17✔
1527
            reconstructed.push_back(argID);
17✔
1528
            reconstructed.push_back(opGTE);
17✔
1529

1530
            A2 startValue((ascending ? r->first : r->second), Lexer::Type::number);
17✔
1531
            startValue.tag("FILTER");
17✔
1532
            reconstructed.push_back(startValue);
17✔
1533

1534
            reconstructed.push_back(opAnd);
17✔
1535
            reconstructed.push_back(argID);
17✔
1536
            reconstructed.push_back(opLTE);
17✔
1537

1538
            A2 endValue((ascending ? r->second : r->first), Lexer::Type::number);
17✔
1539
            endValue.tag("FILTER");
17✔
1540
            reconstructed.push_back(endValue);
17✔
1541

1542
            reconstructed.push_back(closeParen);
17✔
1543
          }
17✔
1544
        }
1545

1546
        // Combine the ID and UUID sections with 'or'.
1547
        if (_id_ranges.size() && _uuid_list.size()) reconstructed.push_back(opOr);
583✔
1548

1549
        // Add all UUID list items.
1550
        for (auto u = _uuid_list.begin(); u != _uuid_list.end(); ++u) {
648✔
1551
          if (u != _uuid_list.begin()) reconstructed.push_back(opOr);
65✔
1552

1553
          reconstructed.push_back(openParen);
65✔
1554
          reconstructed.push_back(argUUID);
65✔
1555
          reconstructed.push_back(opSimilar);
65✔
1556

1557
          A2 value(*u, Lexer::Type::string);
65✔
1558
          value.tag("FILTER");
65✔
1559
          reconstructed.push_back(value);
65✔
1560

1561
          reconstructed.push_back(closeParen);
65✔
1562
        }
65✔
1563

1564
        reconstructed.push_back(closeParen);
583✔
1565
      }
583✔
1566

1567
      // No 'else' because all set/number/uuid args but the first are removed.
1568
    } else
1569
      reconstructed.push_back(a);
2,910✔
1570
  }
1571

1572
  if (changes) {
583✔
1573
    _args = reconstructed;
583✔
1574

1575
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
583✔
NEW
1576
      Context::getContext().debug(dump("CLI2::prepareFilter insertIDExpr"));
×
1577
  }
1578
}
583✔
1579

1580
////////////////////////////////////////////////////////////////////////////////
1581
// FILTER Lexer::Type::word args will become part of an expression, and so they
1582
// need to be Lexed.
1583
void CLI2::lexFilterArgs() {
3,078✔
1584
  bool changes = false;
3,078✔
1585
  std::vector<A2> reconstructed;
3,078✔
1586
  for (const auto& a : _args) {
18,668✔
1587
    if (a._lextype == Lexer::Type::word && a.hasTag("FILTER")) {
15,590✔
1588
      changes = true;
20✔
1589

1590
      std::string lexeme;
20✔
1591
      Lexer::Type type;
1592
      Lexer lex(a.attribute("raw"));
40✔
1593
      while (lex.token(lexeme, type)) {
89✔
1594
        A2 extra(lexeme, type);
69✔
1595
        extra.tag("FILTER");
69✔
1596
        reconstructed.push_back(extra);
69✔
1597
      }
69✔
1598
    } else
20✔
1599
      reconstructed.push_back(a);
15,570✔
1600
  }
1601

1602
  if (changes) {
3,078✔
1603
    _args = reconstructed;
19✔
1604

1605
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
19✔
NEW
1606
      Context::getContext().debug(dump("CLI2::prepareFilter lexFilterArgs"));
×
1607
  }
1608
}
3,078✔
1609

1610
////////////////////////////////////////////////////////////////////////////////
1611
// FILTER, Lexer::Type::word args are treated as search terms.
1612
//
1613
// Algorithm:
1614
//   Given:
1615
//     - task ... argX candidate argY
1616
//   Where:
1617
//     - neither argX nor argY are an operator, except (, ), and, or, xor
1618
//     - candidate is one of: Lexer::Type::word
1619
//                            Lexer::Type::identifier
1620
//                            Lexer::Type::date
1621
//
1622
void CLI2::desugarFilterPlainArgs() {
3,078✔
1623
  // First walk the arg list looking for plain words that are not part of an
1624
  // existing expression.
1625
  auto prevprev = &_args[0];
3,078✔
1626
  auto prev = &_args[0];
3,078✔
1627
  for (auto& a : _args) {
22,419✔
1628
    auto raw = a.attribute("raw");
38,682✔
1629
    auto praw = prev->attribute("raw");
38,682✔
1630
    auto ppraw = prevprev->attribute("raw");
38,682✔
1631

1632
    if ((prevprev->_lextype != Lexer::Type::op ||  // argX
5,422✔
1633
         ppraw == "(" || ppraw == ")" || ppraw == "and" || ppraw == "or" || ppraw == "xor") &&
8,059✔
1634

1635
        (prev->_lextype == Lexer::Type::identifier ||  // candidate
18,696✔
1636
         prev->_lextype == Lexer::Type::date ||        // candidate
15,620✔
1637
         prev->_lextype == Lexer::Type::word) &&       // candidate
15,598✔
1638

1639
        prev->hasTag("FILTER") &&  // candidate
38,693✔
1640

1641
        (a._lextype != Lexer::Type::op ||  // argY
20✔
1642
         raw == "(" || raw == ")" || raw == "and" || raw == "or" || raw == "xor")) {
18✔
1643
      prev->tag("PLAIN");
11✔
1644
    }
1645

1646
    prevprev = prev;
19,341✔
1647
    prev = &a;
19,341✔
1648
  }
19,341✔
1649

1650
  // Cover the case where the *last* argument is a plain arg.
1651
  auto& penultimate = _args[_args.size() - 2];
3,078✔
1652
  auto praw = penultimate.attribute("raw");
6,156✔
1653
  auto& last = _args[_args.size() - 1];
3,078✔
1654
  if ((penultimate._lextype != Lexer::Type::op ||  // argX
641✔
1655
       praw == "(" || praw == ")" || praw == "and" || praw == "or" || praw == "xor") &&
1,282✔
1656

1657
      (last._lextype == Lexer::Type::identifier ||  // candidate
3,078✔
1658
       last._lextype == Lexer::Type::word) &&       // candidate
6,286✔
1659

1660
      last.hasTag("FILTER"))  // candidate
5,082✔
1661
  {
NEW
1662
    last.tag("PLAIN");
×
1663
  }
1664

1665
  // Walk the list again, upgrading PLAIN args.
1666
  bool changes = false;
3,078✔
1667
  std::vector<A2> reconstructed;
3,078✔
1668
  for (const auto& a : _args) {
22,419✔
1669
    if (a.hasTag("PLAIN")) {
19,341✔
1670
      changes = true;
11✔
1671

1672
      A2 lhs("description", Lexer::Type::dom);
22✔
1673
      lhs.attribute("canonical", "description");
11✔
1674
      lhs.tag("FILTER");
11✔
1675
      lhs.tag("PLAIN");
11✔
1676
      reconstructed.push_back(lhs);
11✔
1677

1678
      A2 op("~", Lexer::Type::op);
22✔
1679
      op.tag("FILTER");
11✔
1680
      op.tag("PLAIN");
11✔
1681
      reconstructed.push_back(op);
11✔
1682

1683
      std::string word = a.attribute("raw");
22✔
1684
      Lexer::dequote(word);
11✔
1685
      A2 rhs(word, Lexer::Type::string);
11✔
1686
      rhs.tag("FILTER");
11✔
1687
      rhs.tag("PLAIN");
11✔
1688
      reconstructed.push_back(rhs);
11✔
1689
    } else
11✔
1690
      reconstructed.push_back(a);
19,330✔
1691
  }
1692

1693
  if (changes) {
3,078✔
1694
    _args = reconstructed;
11✔
1695

1696
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
11✔
NEW
1697
      Context::getContext().debug(dump("CLI2::prepareFilter desugarFilterPlainArgs"));
×
1698
  }
1699
}
3,078✔
1700

1701
////////////////////////////////////////////////////////////////////////////////
1702
// Detects if the bracket at iterator it is a start or end of an empty paren expression
1703
// Examples:
1704
// ( status = pending ) ( )
1705
//                      ^
1706
//              it -----|         => true
1707
//
1708
// ( status = pending ) ( project = Home )
1709
//                      ^
1710
//              it -----|         => false
1711
bool CLI2::isEmptyParenExpression(std::vector<A2>::iterator it, bool forward /* = true */) const {
619✔
1712
  int open = 0;
619✔
1713
  int closed = 0;
619✔
1714

1715
  for (auto a = it; a != (forward ? _args.end() : _args.begin()); (forward ? ++a : --a)) {
1,328✔
1716
    if (a->attribute("raw") == "(")
1,328✔
1717
      open++;
398✔
1718
    else if (a->attribute("raw") == ")")
930✔
1719
      closed++;
311✔
1720
    else
1721
      // Encountering a non-paren token means there is something between parenthees
1722
      return false;
619✔
1723

1724
    // Getting balanced parentheses means we have an empty paren expression
1725
    if (open == closed && open != 0) return true;
709✔
1726
  }
1727

1728
  // Should not end here.
1729
  return false;
×
1730
}
1731

1732
////////////////////////////////////////////////////////////////////////////////
1733
// Two consecutive FILTER, non-OP arguments that are not "(" or ")" need an
1734
// "and" operator inserted between them.
1735
//
1736
//   ) <non-op>         -->  ) and <non-op>
1737
//   <non-op> (         -->  <non-op> <and> (
1738
//   ) (                -->  ) and (
1739
//   <non-op> <non-op>  -->  <non-op> and <non-op>
1740
//
1741
void CLI2::insertJunctions() {
3,078✔
1742
  bool changes = false;
3,078✔
1743
  std::vector<A2> reconstructed;
3,078✔
1744
  auto prev = _args.begin();
3,078✔
1745

1746
  for (auto a = _args.begin(); a != _args.end(); ++a) {
25,393✔
1747
    if (a->hasTag("FILTER")) {
22,315✔
1748
      // The prev iterator should be the first FILTER arg.
1749
      if (prev == _args.begin()) prev = a;
11,979✔
1750

1751
      // Insert AND between terms.
1752
      else if (a != prev) {
10,767✔
1753
        if ((prev->_lextype != Lexer::Type::op && a->attribute("raw") == "(" &&
15,066✔
1754
             !isEmptyParenExpression(a, true)) ||
10,767✔
1755
            (prev->attribute("raw") == ")" && a->_lextype != Lexer::Type::op &&
23,062✔
1756
             !isEmptyParenExpression(prev, false)) ||
10,767✔
1757
            (prev->attribute("raw") == ")" && a->attribute("raw") == "(" &&
23,056✔
1758
             !isEmptyParenExpression(a, true) && !isEmptyParenExpression(prev, false)) ||
21,534✔
1759
            (prev->_lextype != Lexer::Type::op && a->_lextype != Lexer::Type::op)) {
10,456✔
1760
          A2 opOr("and", Lexer::Type::op);
1,776✔
1761
          opOr.tag("FILTER");
888✔
1762
          reconstructed.push_back(opOr);
888✔
1763
          changes = true;
888✔
1764
        }
888✔
1765
      }
1766

1767
      // Previous FILTER arg.
1768
      prev = a;
11,979✔
1769
    }
1770

1771
    reconstructed.push_back(*a);
22,315✔
1772
  }
1773

1774
  if (changes) {
3,078✔
1775
    _args = reconstructed;
485✔
1776

1777
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
485✔
1778
      Context::getContext().debug(dump("CLI2::prepareFilter insertJunctions"));
2✔
1779
  }
1780
}
3,078✔
1781

1782
////////////////////////////////////////////////////////////////////////////////
1783
// Look for situations that require defaults:
1784
//
1785
// 1. If no command was found, and no ID/UUID, and if rc.default.command is
1786
//    configured, inject the lexed tokens from rc.default.command.
1787
//
1788
// 2. If no command was found, but an ID/UUID was found, then assume a command
1789
//    of 'information'.
1790
//
1791
void CLI2::defaultCommand() {
52✔
1792
  // Scan the top-level branches for evidence of ID, UUID, overrides and other
1793
  // arguments.
1794
  bool changes = false;
52✔
1795
  bool found_command = false;
52✔
1796
  bool found_sequence = false;
52✔
1797

1798
  for (const auto& a : _args) {
141✔
1799
    std::string raw = a.attribute("raw");
178✔
1800

1801
    if (a.hasTag("CMD")) found_command = true;
89✔
1802

1803
    if (a._lextype == Lexer::Type::uuid || a._lextype == Lexer::Type::number) found_sequence = true;
89✔
1804
  }
89✔
1805

1806
  // If no command was specified, then a command will be inserted.
1807
  if (!found_command) {
52✔
1808
    // Default command.
1809
    if (!found_sequence) {
52✔
1810
      // Apply overrides, if any.
1811
      std::string defaultCommand = Context::getContext().config.get("default.command");
94✔
1812
      if (defaultCommand != "") {
47✔
1813
        // Modify _args, _original_args to be:
1814
        //   <args0> [<def0> ...] <args1> [...]
1815

1816
        std::vector<A2> reconstructedOriginals{_original_args[0]};
138✔
1817
        std::vector<A2> reconstructed{_args[0]};
138✔
1818

1819
        std::string lexeme;
46✔
1820
        Lexer::Type type;
1821
        Lexer lex(defaultCommand);
46✔
1822

1823
        while (lex.token(lexeme, type)) {
95✔
1824
          reconstructedOriginals.emplace_back(lexeme, type);
49✔
1825

1826
          A2 cmd(lexeme, type);
49✔
1827
          cmd.tag("DEFAULT");
49✔
1828
          reconstructed.push_back(cmd);
49✔
1829
        }
49✔
1830

1831
        for (unsigned int i = 1; i < _original_args.size(); ++i)
78✔
1832
          reconstructedOriginals.push_back(_original_args[i]);
32✔
1833

1834
        for (unsigned int i = 1; i < _args.size(); ++i) reconstructed.push_back(_args[i]);
78✔
1835

1836
        _original_args = reconstructedOriginals;
46✔
1837
        _args = reconstructed;
46✔
1838
        changes = true;
46✔
1839
      }
46✔
1840
    } else {
47✔
1841
      A2 info("information", Lexer::Type::word);
10✔
1842
      info.tag("ASSUMED");
5✔
1843
      _args.push_back(info);
5✔
1844
      changes = true;
5✔
1845
    }
5✔
1846
  }
1847

1848
  if (changes && Context::getContext().config.getInteger("debug.parser") >= 2)
52✔
NEW
1849
    Context::getContext().debug(dump("CLI2::analyze defaultCommand"));
×
1850
}
52✔
1851

1852
////////////////////////////////////////////////////////////////////////////////
1853
// Some values are expressions, which need to be lexed. The best way to
1854
// determine whether an expression is either a single value, or needs to be
1855
// lexed, is to lex it and count the tokens. For example:
1856
//    now+1d
1857
// This should be lexed and surrounded by parentheses:
1858
//    (
1859
//    now
1860
//    +
1861
//    1d
1862
//    )
1863
std::vector<A2> CLI2::lexExpression(const std::string& expression) {
808✔
1864
  std::vector<A2> lexed;
808✔
1865
  std::string lexeme;
808✔
1866
  Lexer::Type type;
1867
  Lexer lex(expression);
808✔
1868
  while (lex.token(lexeme, type)) {
1,647✔
1869
    A2 token(lexeme, type);
839✔
1870
    token.tag("FILTER");
839✔
1871
    lexed.push_back(token);
839✔
1872
  }
839✔
1873

1874
  // If there were multiple tokens, parenthesize, because this expression will
1875
  // be used as a value.
1876
  if (lexed.size() > 1) {
808✔
1877
    A2 openParen("(", Lexer::Type::op);
24✔
1878
    openParen.tag("FILTER");
12✔
1879
    A2 closeParen(")", Lexer::Type::op);
24✔
1880
    closeParen.tag("FILTER");
12✔
1881

1882
    lexed.insert(lexed.begin(), openParen);
12✔
1883
    lexed.push_back(closeParen);
12✔
1884
  }
12✔
1885

1886
  return lexed;
1,616✔
1887
}
808✔
1888

1889
////////////////////////////////////////////////////////////////////////////////
1890

1891
// vim: ts=2:sw=2
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc