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

GothenburgBitFactory / taskwarrior / 12358478612

16 Dec 2024 05:59PM UTC coverage: 84.898% (-0.6%) from 85.522%
12358478612

push

github

web-flow
[pre-commit.ci] pre-commit autoupdate (#3725)

updates:
- [github.com/pre-commit/mirrors-clang-format: v19.1.4 → v19.1.5](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.4...v19.1.5)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

19276 of 22705 relevant lines covered (84.9%)

23265.72 hits per line

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

92.98
/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) {
57,185✔
52
  _lextype = lextype;
57,185✔
53
  attribute("raw", raw);
57,185✔
54
}
57,185✔
55

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

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

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

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

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

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

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

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

96
  return "";
24,766✔
97
}
98

99
////////////////////////////////////////////////////////////////////////////////
100
const std::string A2::getToken() const {
13,295✔
101
  auto i = _attributes.find("canonical");
26,590✔
102
  if (i == _attributes.end()) i = _attributes.find("raw");
38,167✔
103

104
  return i->second;
26,590✔
105
}
106

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

115
  else if (_lextype == Lexer::Type::substitution) {
57,762✔
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)) {
111✔
123
      attribute("from", from);
74✔
124
      attribute("to", to);
74✔
125
      attribute("flags", flags);
74✔
126
    }
127
  }
37✔
128

129
  else if (_lextype == Lexer::Type::pair) {
57,725✔
130
    std::string name;
3,155✔
131
    std::string mod;
3,155✔
132
    std::string sep;
3,155✔
133
    std::string value;
3,155✔
134
    if (Lexer::decomposePair(_attributes["raw"], name, mod, sep, value)) {
9,465✔
135
      attribute("name", name);
6,310✔
136
      attribute("modifier", mod);
6,310✔
137
      attribute("separator", sep);
6,310✔
138
      attribute("value", value);
3,155✔
139

140
      if (name == "rc") {
3,155✔
141
        if (mod != "")
746✔
142
          tag("CONFIG");
1,138✔
143
        else
144
          tag("RC");
354✔
145
      }
146
    }
147
  }
3,155✔
148

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

153
    std::string pattern;
110✔
154
    std::string flags;
110✔
155
    if (Lexer::decomposePattern(_attributes["raw"], pattern, flags)) {
330✔
156
      attribute("pattern", pattern);
220✔
157
      attribute("flags", flags);
220✔
158
    }
159
  }
110✔
160
}
58,743✔
161

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

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

170
  // Dump tags.
171
  std::string tags;
234✔
172
  for (const auto& tag : _tags) {
691✔
173
    if (tag == "BINARY")
457✔
174
      tags += "\033[1;37;44m" + tag + "\033[0m ";
34✔
175
    else if (tag == "CMD")
423✔
176
      tags += "\033[1;37;46m" + tag + "\033[0m ";
28✔
177
    else if (tag == "FILTER")
395✔
178
      tags += "\033[1;37;42m" + tag + "\033[0m ";
90✔
179
    else if (tag == "MODIFICATION")
305✔
180
      tags += "\033[1;37;43m" + tag + "\033[0m ";
×
181
    else if (tag == "MISCELLANEOUS")
305✔
182
      tags += "\033[1;37;45m" + tag + "\033[0m ";
×
183
    else if (tag == "RC")
305✔
184
      tags += "\033[1;37;41m" + tag + "\033[0m ";
12✔
185
    else if (tag == "CONFIG")
293✔
186
      tags += "\033[1;37;101m" + tag + "\033[0m ";
34✔
187
    else if (tag == "?")
259✔
188
      tags += "\033[38;5;255;48;5;232m" + tag + "\033[0m ";
×
189
    else
190
      tags += "\033[32m" + tag + "\033[0m ";
259✔
191
  }
192

193
  return output + ' ' + atts + tags;
468✔
194
}
234✔
195

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

211
////////////////////////////////////////////////////////////////////////////////
212
// Static method.
213
bool CLI2::getOverride(int argc, const char** argv, File& rc) {
4,496✔
214
  const char* value = getValue(argc, argv, "rc");
4,496✔
215
  if (value == nullptr) return false;
4,496✔
216
  rc = File(value);
149✔
217
  return true;
149✔
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,496✔
224
  const char* value = getValue(argc, argv, "rc.data.location");
4,496✔
225
  if (value == nullptr) {
4,496✔
226
    std::string location = Context::getContext().config.get("data.location");
8,990✔
227
    if (location != "") data = location;
4,495✔
228
    return false;
4,495✔
229
  }
4,495✔
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,496✔
237
  auto& context = Context::getContext();
4,496✔
238
  auto last = std::find(argv, argv + argc, std::string("--"));
4,496✔
239
  auto is_override = [](const std::string& s) { return s.compare(0, 3, "rc.") == 0; };
28,645✔
240
  auto get_sep = [&](const std::string& s) {
15,014✔
241
    if (is_override(s)) return s.find_first_of(":=", 3);
15,014✔
242
    return std::string::npos;
14,530✔
243
  };
4,496✔
244
  auto override_settings = [&](std::string raw) {
15,014✔
245
    auto sep = get_sep(raw);
15,014✔
246
    if (sep == std::string::npos) return;
15,014✔
247
    std::string name = raw.substr(3, sep - 3);
480✔
248
    std::string value = raw.substr(sep + 1);
480✔
249
    context.config.set(name, value);
480✔
250
  };
480✔
251
  auto display_overrides = [&](std::string raw) {
13,631✔
252
    if (is_override(raw)) context.footnote(format("Configuration override {1}", raw));
14,167✔
253
  };
13,631✔
254
  std::for_each(argv, last, override_settings);
4,496✔
255
  if (context.verbose("override")) std::for_each(argv, last, display_overrides);
8,992✔
256
}
4,496✔
257

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

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

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

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

279
  // Adding a new argument invalidates prior analysis.
280
  _args.clear();
17,326✔
281
}
17,326✔
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 */) {
581✔
287
  std::vector<A2> replacement{_original_args.begin(), _original_args.begin() + offset + 1};
1,162✔
288

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

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

294
  _original_args = replacement;
581✔
295

296
  // Adding a new argument invalidates prior analysis.
297
  _args.clear();
581✔
298
}
581✔
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() {
5,076✔
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");
10,152✔
311
  A2 a(raw, Lexer::Type::word);
5,076✔
312
  a.tag("BINARY");
10,152✔
313

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

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

322
    A2 cal("calendar", Lexer::Type::word);
×
323
    _args.push_back(cal);
×
324
  } else {
×
325
    _args.push_back(a);
5,076✔
326
  }
327
}
5,076✔
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() {
5,076✔
336
  // Note: Starts iterating at index 1, because ::handleArg0 has already
337
  //       processed it.
338
  bool terminated = false;
5,076✔
339
  for (unsigned int i = 1; i < _original_args.size(); ++i) {
21,559✔
340
    bool quoted = Lexer::wasQuoted(_original_args[i].attribute("raw"));
32,966✔
341

342
    // Process single-token arguments.
343
    std::string lexeme;
16,483✔
344
    Lexer::Type type;
345
    Lexer lex(_original_args[i].attribute("raw"));
32,966✔
346
    if (lex.token(lexeme, type) &&
32,911✔
347
        (lex.isEOS() ||                           // Token goes to EOS
16,428✔
348
         (quoted && type == Lexer::Type::pair)))  // Quoted pairs automatically go to EOS
180✔
349
    {
350
      if (!terminated && type == Lexer::Type::separator)
16,171✔
351
        terminated = true;
771✔
352
      else if (terminated)
15,400✔
353
        type = Lexer::Type::word;
1,384✔
354

355
      A2 a(_original_args[i].attribute("raw"), type);
32,342✔
356
      if (terminated) a.tag("TERMINATED");
20,481✔
357
      if (quoted) a.tag("QUOTED");
18,757✔
358

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

361
      _args.push_back(a);
16,171✔
362
    }
16,171✔
363

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

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

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

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

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

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

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

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

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

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

408
        _args.push_back(unknown);
×
409
      }
×
410
    }
312✔
411
  }
16,483✔
412

413
  if (Context::getContext().config.getInteger("debug.parser") >= 2)
15,228✔
414
    Context::getContext().debug(dump("CLI2::analyze lexArguments"));
18✔
415
}
5,076✔
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() {
5,075✔
422
  bool changes = false;
5,075✔
423
  std::vector<A2> replacement;
5,075✔
424

425
  std::string canonical;
5,075✔
426
  for (auto& a : _args) {
26,707✔
427
    if (a._lextype == Lexer::Type::tag && a.attribute("sign") == "-") {
23,586✔
428
      std::string command = getCommand();
579✔
429
      if (command == "add" || command == "log") {
579✔
430
        a._lextype = Lexer::Type::word;
1✔
431
        changes = true;
1✔
432
      }
433
    }
579✔
434

435
    else if (a._lextype == Lexer::Type::pair &&
27,295✔
436
             canonicalize(canonical, "pseudo", a.attribute("name"))) {
33,537✔
437
      Context::getContext().config.set(canonical, a.attribute("value"));
148✔
438
      changes = true;
74✔
439

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

444
    replacement.push_back(a);
21,558✔
445
  }
446

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

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

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

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

469
  demotion();
5,075✔
470
  canonicalizeNames();
5,075✔
471

472
  // Determine arg types: FILTER, MODIFICATION, MISCELLANEOUS.
473
  categorizeArgs();
5,075✔
474
  parenthesizeOriginalFilter();
5,074✔
475

476
  // Cache frequently looked up items
477
  _command = getCommand();
5,074✔
478
}
5,074✔
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) {
576✔
484
  if (arg.length()) {
576✔
485
    std::vector<std::string> filter;
576✔
486
    filter.push_back("(");
576✔
487

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

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

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

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

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

511
    while (lex.token(lexeme, type)) mods.push_back(lexeme);
10✔
512

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

521
    // Insert modifications after the command.
522
    add(mods, cmdIndex);
5✔
523
    analyze();
5✔
524
  }
5✔
525
}
5✔
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,151✔
532
  // Recursion block.
533
  if (_context_added) return;
6,263✔
534

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

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

548
  // For readable contexts: Detect if UUID or ID is set, and bail out
549
  if (readable)
28✔
550
    for (auto& a : _args) {
87✔
551
      if (a._lextype == Lexer::Type::uuid || a._lextype == Lexer::Type::number ||
76✔
552
          a._lextype == Lexer::Type::set) {
64✔
553
        Context::getContext().debug(
24✔
554
            format("UUID/ID argument found '{1}', not applying context.", a.attribute("raw")));
48✔
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;
16✔
562
  if (readable)
16✔
563
    addFilter(contextString);
11✔
564
  else if (writeable)
5✔
565
    addModifications(contextString);
5✔
566

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

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

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

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

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

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

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

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

622
  return words;
1,120✔
623
}
×
624

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

632
  return misc;
55✔
633
}
×
634

635
////////////////////////////////////////////////////////////////////////////////
636
// Search for 'value' in _entities category, return canonicalized value.
637
bool CLI2::canonicalize(std::string& canonicalized, const std::string& category,
25,651✔
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);
25,651✔
643
  auto cache_result = _canonical_cache.find(cache_key);
25,651✔
644
  if (cache_result != _canonical_cache.end()) {
25,651✔
645
    canonicalized = cache_result->second;
6,797✔
646
    return true;
6,797✔
647
  }
648

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

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

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

671
  return false;
15,557✔
672
}
18,854✔
673

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

678
  return "";
×
679
}
680

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

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

689
  return "";
×
690
}
691

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

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

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

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

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

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

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

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

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

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

736
  return out.str();
80✔
737
}
40✔
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() {
5,076✔
743
  bool changes = false;
5,076✔
744
  bool action;
745
  int counter = 0;
5,076✔
746
  do {
747
    action = false;
5,096✔
748
    std::vector<A2> reconstructed;
5,096✔
749

750
    std::string raw;
5,096✔
751
    for (const auto& i : _args) {
26,712✔
752
      raw = i.attribute("raw");
43,232✔
753
      if (i.hasTag("TERMINATED")) {
43,232✔
754
        reconstructed.push_back(i);
2,157✔
755
      } else if (_aliases.find(raw) != _aliases.end()) {
19,459✔
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);
19,439✔
765
      }
766
    }
767

768
    _args = reconstructed;
5,096✔
769

770
    std::vector<A2> reconstructedOriginals;
5,096✔
771
    bool terminated = false;
5,096✔
772
    for (const auto& i : _original_args) {
26,712✔
773
      if (i.attribute("raw") == "--") terminated = true;
43,232✔
774

775
      if (terminated) {
21,616✔
776
        reconstructedOriginals.push_back(i);
2,318✔
777
      } else if (_aliases.find(i.attribute("raw")) != _aliases.end()) {
57,894✔
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);
19,278✔
787
      }
788
    }
789

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

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

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

800
////////////////////////////////////////////////////////////////////////////////
801
// Scan all arguments and canonicalize names that need it.
802
void CLI2::canonicalizeNames() {
5,075✔
803
  bool changes = false;
5,075✔
804
  for (auto& a : _args) {
26,707✔
805
    if (a._lextype == Lexer::Type::pair) {
21,632✔
806
      std::string raw = a.attribute("raw");
3,121✔
807
      if (raw.substr(0, 3) != "rc:" && raw.substr(0, 3) != "rc.") {
3,121✔
808
        std::string name = a.attribute("name");
2,375✔
809
        std::string canonical;
2,375✔
810
        if (canonicalize(canonical, "pseudo", name) || canonicalize(canonical, "attribute", name)) {
11,727✔
811
          a.attribute("canonical", canonical);
4,748✔
812
        } else {
813
          a._lextype = Lexer::Type::word;
1✔
814
        }
815

816
        changes = true;
2,375✔
817
      }
2,375✔
818
    }
3,121✔
819
  }
820

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

825
////////////////////////////////////////////////////////////////////////////////
826
// Categorize FILTER, MODIFICATION and MISCELLANEOUS args, based on CMD DNA.
827
void CLI2::categorizeArgs() {
5,075✔
828
  // Context is only applied for commands that request it.
829
  std::string command = getCommand();
5,075✔
830
  Command* cmd = Context::getContext().commands[command];
5,075✔
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))
5,075✔
840
    uses_context = (dynamic_cast<CmdCustom*>(cmd))->uses_context();
1,180✔
841
  else if (dynamic_cast<CmdTimesheet*>(cmd))
3,895✔
842
    uses_context = (dynamic_cast<CmdTimesheet*>(cmd))->uses_context();
3✔
843
  else if (cmd)
3,892✔
844
    uses_context = cmd->uses_context();
3,892✔
845

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

849
  bool changes = false;
5,075✔
850
  bool afterCommand = false;
5,075✔
851
  for (auto& a : _args) {
26,748✔
852
    if (a._lextype == Lexer::Type::separator) continue;
21,674✔
853

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

859
    // Skip admin args.
860
    else if (a.hasTag("BINARY") || a.hasTag("RC") || a.hasTag("CONFIG")) {
90,142✔
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,524✔
876
             !cmd->accepts_miscellaneous()) {
2,517✔
877
      // No commands were expected --> error.
878
      throw format("The '{1}' command does not allow '{2}'.", command, a.attribute("raw"));
5✔
879
    } else if (cmd && !cmd->accepts_filter() && !cmd->accepts_modifications() &&
12,522✔
880
               cmd->accepts_miscellaneous()) {
2,516✔
881
      a.tag("MISCELLANEOUS");
2,516✔
882
      changes = true;
2,516✔
883
    } else if (cmd && !cmd->accepts_filter() && cmd->accepts_modifications() &&
10,781✔
884
               !cmd->accepts_miscellaneous()) {
3,291✔
885
      a.tag("MODIFICATION");
3,291✔
886
      changes = true;
3,291✔
887
    } else if (cmd && !cmd->accepts_filter() && cmd->accepts_modifications() &&
4,199✔
888
               cmd->accepts_miscellaneous()) {
×
889
      // Error: internally inconsistent.
890
      throw std::string("Unknown error. Please report.");
×
891
    } else if (cmd && cmd->accepts_filter() && !cmd->accepts_modifications() &&
7,645✔
892
               !cmd->accepts_miscellaneous()) {
3,446✔
893
      a.tag("FILTER");
3,350✔
894
      changes = true;
3,350✔
895
    } else if (cmd && cmd->accepts_filter() && !cmd->accepts_modifications() &&
945✔
896
               cmd->accepts_miscellaneous()) {
96✔
897
      if (!afterCommand)
96✔
898
        a.tag("FILTER");
168✔
899
      else
900
        a.tag("MISCELLANEOUS");
24✔
901

902
      changes = true;
96✔
903
    } else if (cmd && cmd->accepts_filter() && cmd->accepts_modifications() &&
1,506✔
904
               !cmd->accepts_miscellaneous()) {
753✔
905
      if (!afterCommand)
753✔
906
        a.tag("FILTER");
876✔
907
      else
908
        a.tag("MODIFICATION");
630✔
909

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

918
  if (changes && Context::getContext().config.getInteger("debug.parser") >= 2)
14,212✔
919
    Context::getContext().debug(dump("CLI2::analyze categorizeArgs"));
12✔
920
}
5,075✔
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() {
5,074✔
943
  // Locate the first and last ORIGINAL FILTER args.
944
  unsigned int firstOriginalFilter = 0;
5,074✔
945
  unsigned int lastOriginalFilter = 0;
5,074✔
946
  for (unsigned int i = 1; i < _args.size(); ++i) {
21,671✔
947
    if (_args[i].hasTag("FILTER") && _args[i].hasTag("ORIGINAL")) {
57,535✔
948
      if (firstOriginalFilter == 0) firstOriginalFilter = i;
1,682✔
949

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

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

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

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

975
    _args = reconstructed;
1,367✔
976

977
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
4,101✔
978
      Context::getContext().debug(dump("CLI2::analyze parenthesizeOriginalFilter"));
6✔
979
  }
1,367✔
980
}
5,074✔
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() {
5,138✔
987
  for (auto& a : _args) {
14,134✔
988
    std::string raw = a.attribute("raw");
14,071✔
989
    std::string canonical;
14,071✔
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))
28,142✔
997
      canonical = raw;
4,949✔
998
    else if (exactMatch("attribute", raw))
18,244✔
999
      continue;
2✔
1000
    else if (!canonicalize(canonical, "cmd", raw))
18,240✔
1001
      continue;
8,994✔
1002

1003
    a.attribute("canonical", canonical);
10,150✔
1004
    a.tag("CMD");
5,075✔
1005

1006
    // Apply command DNA as tags.
1007
    Command* command = Context::getContext().commands[canonical];
5,075✔
1008
    if (command->read_only()) a.tag("READONLY");
10,837✔
1009
    if (command->displays_id()) a.tag("SHOWSID");
7,761✔
1010
    if (command->needs_gc()) a.tag("RUNSGC");
7,943✔
1011
    if (command->uses_context()) a.tag("USESCONTEXT");
11,377✔
1012
    if (command->accepts_filter()) a.tag("ALLOWSFILTER");
9,009✔
1013
    if (command->accepts_modifications()) a.tag("ALLOWSMODIFICATIONS");
9,299✔
1014
    if (command->accepts_miscellaneous()) a.tag("ALLOWSMISC");
8,031✔
1015

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

1019
    // Stop and indicate command found.
1020
    return true;
5,075✔
1021
  }
28,142✔
1022

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

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

1035
  return false;
18,242✔
1036
}
1037

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

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

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

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

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

1065
  if (changes) {
3,170✔
1066
    _args = reconstructed;
605✔
1067

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

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

1077
  auto command = getCommand();
3,170✔
1078
  if (command == "add" || command == "log") {
3,170✔
1079
    for (auto& a : _args) {
8,359✔
1080
      if (a.hasTag("FILTER")) {
13,384✔
1081
        a.unTag("FILTER");
14✔
1082
        a.tag("MODIFICATION");
7✔
1083
        changes = true;
7✔
1084
      }
1085
    }
1086
  }
1087

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

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

1106
      // An unquoted string, while equivalent to an empty string, doesn't cause
1107
      // an operand shortage in eval.
1108
      if (value == "") value = "''";
844✔
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);
844✔
1122
      if (Context::getContext().config.getInteger("debug.parser") >= 2) {
2,532✔
1123
        Context::getContext().debug("CLI2::lexExpression " + name + ':' + value);
4✔
1124
        for (auto& v : values) Context::getContext().debug("  " + v.dump());
16✔
1125
        Context::getContext().debug(" ");
12✔
1126
      }
1127

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

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

1145
        A2 op("", Lexer::Type::op);
1,554✔
1146
        op.tag("FILTER");
777✔
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);
2,331✔
1151
        rhs.tag("FILTER");
777✔
1152

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

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

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

1228
        found = true;
777✔
1229
      }
777✔
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;
67✔
1236
      }
1237

1238
      if (found)
844✔
1239
        changes = true;
777✔
1240
      else
1241
        reconstructed.push_back(a);
67✔
1242
    }
844✔
1243
    // Not a FILTER pair.
1244
    else
1245
      reconstructed.push_back(a);
20,540✔
1246
  }
1247

1248
  if (changes) {
3,170✔
1249
    _args = reconstructed;
561✔
1250

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

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

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

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

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

1281
  if (changes) {
3,170✔
1282
    _args = reconstructed;
65✔
1283

1284
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
195✔
1285
      Context::getContext().debug(dump("CLI2::prepareFilter desugarFilterPatterns"));
×
1286
  }
1287
}
3,170✔
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,170✔
1299
  bool changes = false;
3,170✔
1300

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

1305
    for (const auto& a : _args) {
19,392✔
1306
      if (a.hasTag("FILTER")) {
32,448✔
1307
        ++filterCount;
5,506✔
1308

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

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

1330
        std::string raw = a.attribute("raw");
5,506✔
1331
        previousFilterArgWasAnOperator =
5,506✔
1332
            (a._lextype == Lexer::Type::op && raw != "(" && raw != ")") ? true : false;
5,506✔
1333
      }
5,506✔
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,168✔
1339

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

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

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

1360
            for (const auto& element : elements) {
×
1361
              changes = true;
×
1362
              auto hyphen = element.find('-');
×
1363
              if (hyphen != std::string::npos)
×
1364
                _id_ranges.emplace_back(element.substr(0, hyphen), element.substr(hyphen + 1));
×
1365
              else
1366
                _id_ranges.emplace_back(element, element);
×
1367
            }
1368
          }
×
1369
        }
6✔
1370
      }
1371
    }
1372
  }
3,168✔
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) {
48✔
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,170✔
1392
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
1,602✔
1393
      Context::getContext().debug(dump("CLI2::prepareFilter findIDs"));
×
1394
}
3,170✔
1395

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

1400
  if (Context::getContext().config.getBoolean("sugar")) {
9,510✔
1401
    for (const auto& a : _args) {
19,392✔
1402
      if (a._lextype == Lexer::Type::uuid && a.hasTag("FILTER")) {
16,356✔
1403
        changes = true;
65✔
1404
        _uuid_list.push_back(a.attribute("raw"));
195✔
1405
      }
1406
    }
1407

1408
    if (!_uuid_list.size()) {
3,168✔
1409
      for (auto& a : _args) {
18,871✔
1410
        if (a._lextype == Lexer::Type::uuid && a.hasTag("MODIFICATION")) {
15,767✔
1411
          changes = true;
1✔
1412
          a.unTag("MODIFICATION");
2✔
1413
          a.tag("FILTER");
1✔
1414
          _uuid_list.push_back(a.attribute("raw"));
3✔
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) {
48✔
1425
        changes = true;
×
1426
        A2 pair("uuid:" + a.attribute("raw"), Lexer::Type::pair);
×
1427
        pair.tag("FILTER");
×
1428
        pair.decompose();
×
1429
        reconstructed.push_back(pair);
×
1430
      } else
×
1431
        reconstructed.push_back(a);
16✔
1432
    }
1433

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

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

1442
////////////////////////////////////////////////////////////////////////////////
1443
void CLI2::insertIDExpr() {
3,170✔
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,170✔
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;
595✔
1451
  bool foundID = false;
595✔
1452
  std::vector<A2> reconstructed;
595✔
1453
  for (const auto& a : _args) {
4,169✔
1454
    if ((a._lextype == Lexer::Type::set || a._lextype == Lexer::Type::number ||
3,546✔
1455
         a._lextype == Lexer::Type::uuid) &&
7,214✔
1456
        a.hasTag("FILTER")) {
4,792✔
1457
      if (!foundID) {
609✔
1458
        foundID = true;
595✔
1459
        changes = true;
595✔
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,190✔
1478
        openParen.tag("FILTER");
1,190✔
1479
        A2 closeParen(")", Lexer::Type::op);
1,190✔
1480
        closeParen.tag("FILTER");
1,190✔
1481
        A2 opOr("or", Lexer::Type::op);
1,190✔
1482
        opOr.tag("FILTER");
1,190✔
1483
        A2 opAnd("and", Lexer::Type::op);
1,190✔
1484
        opAnd.tag("FILTER");
1,190✔
1485
        A2 opSimilar("=", Lexer::Type::op);
1,190✔
1486
        opSimilar.tag("FILTER");
1,190✔
1487
        A2 opEqual("==", Lexer::Type::op);
1,190✔
1488
        opEqual.tag("FILTER");
1,190✔
1489
        A2 opGTE(">=", Lexer::Type::op);
1,190✔
1490
        opGTE.tag("FILTER");
1,190✔
1491
        A2 opLTE("<=", Lexer::Type::op);
1,190✔
1492
        opLTE.tag("FILTER");
1,190✔
1493

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

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

1501
        reconstructed.push_back(openParen);
595✔
1502

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1575
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
1,785✔
1576
      Context::getContext().debug(dump("CLI2::prepareFilter insertIDExpr"));
×
1577
  }
1578
}
595✔
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,170✔
1584
  bool changes = false;
3,170✔
1585
  std::vector<A2> reconstructed;
3,170✔
1586
  for (const auto& a : _args) {
19,359✔
1587
    if (a._lextype == Lexer::Type::word && a.hasTag("FILTER")) {
22,919✔
1588
      changes = true;
21✔
1589

1590
      std::string lexeme;
21✔
1591
      Lexer::Type type;
1592
      Lexer lex(a.attribute("raw"));
21✔
1593
      while (lex.token(lexeme, type)) {
93✔
1594
        A2 extra(lexeme, type);
72✔
1595
        extra.tag("FILTER");
72✔
1596
        reconstructed.push_back(extra);
72✔
1597
      }
72✔
1598
    } else
21✔
1599
      reconstructed.push_back(a);
16,168✔
1600
  }
1601

1602
  if (changes) {
3,170✔
1603
    _args = reconstructed;
20✔
1604

1605
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
60✔
1606
      Context::getContext().debug(dump("CLI2::prepareFilter lexFilterArgs"));
×
1607
  }
1608
}
3,170✔
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,170✔
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,170✔
1626
  auto prev = &_args[0];
3,170✔
1627
  for (auto& a : _args) {
23,188✔
1628
    auto raw = a.attribute("raw");
20,018✔
1629
    auto praw = prev->attribute("raw");
40,036✔
1630
    auto ppraw = prevprev->attribute("raw");
40,036✔
1631

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

1635
        (prev->_lextype == Lexer::Type::identifier ||  // candidate
19,359✔
1636
         prev->_lextype == Lexer::Type::date ||        // candidate
16,176✔
1637
         prev->_lextype == Lexer::Type::word) &&       // candidate
16,151✔
1638

1639
        prev->hasTag("FILTER") &&  // candidate
59,228✔
1640

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

1646
    prevprev = prev;
20,018✔
1647
    prev = &a;
20,018✔
1648
  }
20,018✔
1649

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

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

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

1665
  // Walk the list again, upgrading PLAIN args.
1666
  bool changes = false;
3,170✔
1667
  std::vector<A2> reconstructed;
3,170✔
1668
  for (const auto& a : _args) {
23,188✔
1669
    if (a.hasTag("PLAIN")) {
40,036✔
1670
      changes = true;
15✔
1671

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

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

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

1693
  if (changes) {
3,170✔
1694
    _args = reconstructed;
15✔
1695

1696
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
45✔
1697
      Context::getContext().debug(dump("CLI2::prepareFilter desugarFilterPlainArgs"));
×
1698
  }
1699
}
3,170✔
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 {
639✔
1712
  int open = 0;
639✔
1713
  int closed = 0;
639✔
1714

1715
  for (auto a = it; a != (forward ? _args.end() : _args.begin()); (forward ? ++a : --a)) {
2,145✔
1716
    if (a->attribute("raw") == "(")
4,176✔
1717
      open++;
425✔
1718
    else if (a->attribute("raw") == ")")
2,901✔
1719
      closed++;
338✔
1720
    else
1721
      // Encountering a non-paren token means there is something between parenthees
1722
      return false;
639✔
1723

1724
    // Getting balanced parentheses means we have an empty paren expression
1725
    if (open == closed && open != 0) return true;
763✔
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,170✔
1742
  bool changes = false;
3,170✔
1743
  std::vector<A2> reconstructed;
3,170✔
1744
  auto prev = _args.begin();
3,170✔
1745

1746
  for (auto a = _args.begin(); a != _args.end(); ++a) {
26,272✔
1747
    if (a->hasTag("FILTER")) {
69,306✔
1748
      // The prev iterator should be the first FILTER arg.
1749
      if (prev == _args.begin()) prev = a;
12,376✔
1750

1751
      // Insert AND between terms.
1752
      else if (a != prev) {
11,118✔
1753
        if ((prev->_lextype != Lexer::Type::op && a->attribute("raw") == "(" &&
24,453✔
1754
             !isEmptyParenExpression(a, true)) ||
2✔
1755
            (prev->attribute("raw") == ")" && a->_lextype != Lexer::Type::op &&
44,472✔
1756
             !isEmptyParenExpression(prev, false)) ||
3✔
1757
            (prev->attribute("raw") == ")" && a->attribute("raw") == "(" &&
47,603✔
1758
             !isEmptyParenExpression(a, true) && !isEmptyParenExpression(prev, false)) ||
22,244✔
1759
            (prev->_lextype != Lexer::Type::op && a->_lextype != Lexer::Type::op)) {
10,802✔
1760
          A2 opOr("and", Lexer::Type::op);
1,838✔
1761
          opOr.tag("FILTER");
919✔
1762
          reconstructed.push_back(opOr);
919✔
1763
          changes = true;
919✔
1764
        }
919✔
1765
      }
1766

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

1771
    reconstructed.push_back(*a);
23,102✔
1772
  }
1773

1774
  if (changes) {
3,170✔
1775
    _args = reconstructed;
501✔
1776

1777
    if (Context::getContext().config.getInteger("debug.parser") >= 2)
1,503✔
1778
      Context::getContext().debug(dump("CLI2::prepareFilter insertJunctions"));
6✔
1779
  }
1780
}
3,170✔
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() {
62✔
1792
  // Scan the top-level branches for evidence of ID, UUID, overrides and other
1793
  // arguments.
1794
  bool changes = false;
62✔
1795
  bool found_command = false;
62✔
1796
  bool found_sequence = false;
62✔
1797

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

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

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

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

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

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

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

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

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

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

1836
        _original_args = reconstructedOriginals;
56✔
1837
        _args = reconstructed;
56✔
1838
        changes = true;
56✔
1839
      }
56✔
1840
    } else {
57✔
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)
184✔
1849
    Context::getContext().debug(dump("CLI2::analyze defaultCommand"));
×
1850
}
174✔
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) {
844✔
1864
  std::vector<A2> lexed;
844✔
1865
  std::string lexeme;
844✔
1866
  Lexer::Type type;
1867
  Lexer lex(expression);
844✔
1868
  while (lex.token(lexeme, type)) {
1,727✔
1869
    A2 token(lexeme, type);
883✔
1870
    token.tag("FILTER");
883✔
1871
    lexed.push_back(token);
883✔
1872
  }
883✔
1873

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

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

1886
  return lexed;
1,688✔
1887
}
844✔
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