-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMain.cpp
More file actions
2168 lines (1983 loc) · 99.1 KB
/
Copy pathMain.cpp
File metadata and controls
2168 lines (1983 loc) · 99.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* File: Main.cpp
Project: Kanabo
Purpose: Processes launch arguments and acts on them by issuing instructions to GameDataManager.
Notes: Some basic terminology for understanding the argument handling code: an argument is any space-separated term passed to Kanabo on the
command line. An option is an argument preceded by "--". An option value is an argument not preceded by "--" that follows an option.
A task is a job requested by an option. Terminology related to game data is in GameData.cpp's header comment. The overall hierarchy of
this program is as follows, in descending order: Main -> GameDataManager (GDM) -> GameData -> Template -> Field -> DataIO -> DiskItem.
Margin Guide: |----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- -----|
Copyright 2026 Iritscen */
#include "GameDataManager.hpp" // also brings in DataIO.hpp, DiskItem.hpp, Field.hpp and Template.hpp
#include <algorithm> // needed on Windows for std::replace
#include <charconv> // for from_chars()
#include <cstdlib> // for getenv()
#include <format> // needed on Windows for std::format
#include <functional> // needed on Windows for std::function
#include <iomanip> // needed on Windows for std::quoted
#include <iostream> // for cout, cerr
#include <optional> // for optional
#include <sstream> // for istringstream
#if MAC
#include <unistd.h> // for STDOUT_FILENO
#endif
#if MAC
#include <sys/ioctl.h> // for ioctl() and TIOCGWINSZ
#elif WIN
#include <io.h> // for _isatty() and _fileno()
#define WIN32_LEAN_AND_MEAN
#include <windows.h> // for GetConsoleScreenBufferInfo()
#pragma comment(lib, "user32.lib") // for GetDpiForSystem()
#endif
using std::cerr;
using std::cout;
using std::errc;
using std::from_chars;
using std::find_if;
using std::find_if_not;
using std::format;
using std::function;
using std::initializer_list;
using std::istringstream;
using std::locale;
using std::map;
using std::numpunct;
using std::nullopt;
using std::optional;
using std::pair;
using std::quoted;
using std::stoi;
using std::streambuf;
using std::streamsize;
using std::string;
using std::string_view;
using std::unordered_map;
using std::vector;
/********************** Enums, Structs & Tables **********************/
enum class RunMode : uint8_t
{
Unset,
CLI, // user invoked Kanabo from command line
Batch, // Kanabo has been invoked on command line to run a batch file
GUI // Kanabo was double-clicked as desktop application
};
enum class HelpCategory : uint8_t
{
About,
IO,
Task,
Filter,
Modifier
};
using HC = HelpCategory;
struct HelpCategoryEntry
{
HelpCategory hceCat;
string hceName;
string hceDesc;
};
// Header text used by the help command to delineate categories
namespace {
vector<HelpCategoryEntry> HelpCategoryEntryTable = // NOLINT(cert-err58-cpp)
{
{HC::About, "[About]", "Information about Kanabo."},
{HC::IO, "[Input/Output]", "How to specify source data and destinations for tasks."},
{HC::Task, "[Tasks]", "Operations you can perform on Oni game data."},
{HC::Filter, "[Filters]", "How to limit the scope of a task."},
{HC::Modifier, "[Modifiers]", "Additional settings for I/O, tasks, filters or other modifiers."}
};
} // anonymous namespace
// TypeCodes of value that an option can require
enum class OptionValueTypeReq : uint8_t
{
Nothing,
File,
Folder,
FileOrFolder,
DataPacket // arbitrary text meant for a specific command
};
using OVT = OptionValueTypeReq;
// Quantities of value that an option can require
enum class OptionValueQuantityReq : uint8_t
{
None,
ZeroOrOne,
One,
Some
};
using OVQ = OptionValueQuantityReq;
struct OptionValueEntry
{
OptionValueQuantityReq oveQuan; // required quantity of type
OptionValueTypeReq oveType; // required type
string usageHelpText; // human-readable needs of this option, for the help page
string errorHelpText; // human-readable needs of this option, for error message
};
// Human-readable strings used by the help command when explaining option value requirements
namespace {
vector<OptionValueEntry> OptionValueTable = // NOLINT(cert-err58-cpp)
{
{OVQ::None, OVT::Nothing, "N/A",
"N/A"},
{OVQ::ZeroOrOne, OVT::DataPacket, "[option name]",
"an option's name (optional)"},
{OVQ::One, OVT::File, "[path/to/file]",
"a path to a file"},
{OVQ::One, OVT::Folder, "[path/to/folder]",
"a path to a folder"},
{OVQ::One, OVT::FileOrFolder, "[path/to/file_or_folder]",
"a path to a file or folder"},
{OVQ::One, OVT::DataPacket, "[data for this option]",
"one value with option-specific data"},
{OVQ::Some, OVT::File, "[path/to/file1] [path/to/file2] (...)",
"a path to at least one file (or more, separated by spaces)"},
{OVQ::Some, OVT::Folder, "[path/to/folder1] [path/to/folder2] (...)",
"a path to at least one folder (or more, separated by spaces)"},
{OVQ::Some, OVT::FileOrFolder, "[path/to/file_or_folder1] [path/to/file_or_folder2] (...)",
"a path to at least one file or folder (or more, separated by spaces)"},
{OVQ::Some, OVT::DataPacket, "[data 1 for this option] [data 2 for this option] (...)",
"one or more arguments with option-specific data, separated by spaces"}
};
} // anonymous namespace
// Every option that can be supplied to the program, defined with unique bits so we add them to a bitset for use in OptionEntry's "goesWith"
enum class Option : uint64_t
{
Unset = 0x0000000000,
Alias = 0x0000000001,
FilterName = 0x0000000002,
FilterIndex = 0x0000000004,
FilterTag = 0x0000000008,
FilterField = 0x0000000010,
FilterValue = 0x0000000020,
FilterSize = 0x0000000040,
Source = 0x0000000080,
Destination = 0x0000000100,
DestName = 0x0000000200,
OverwritePolicy = 0x0000000400,
SanitizePolicy = 0x0000000800,
DisplayScale = 0x0000001000,
OnceLine_ = 0x0000002000, // above this line are options which must not be used more than once
FilterNamePartial = 0x0000004000,
FilterValueWhole = 0x0000008000,
FilterCaseInsens = 0x0000010000,
ListFields = 0x0000020000,
ShowFieldOffsets = 0x0000040000,
ShowLinkers = 0x0000080000,
ShowLinkees = 0x0000100000,
ShowPlaceholders = 0x0000200000,
ShowUnnamed = 0x0000400000,
ShowOrphans = 0x0000800000,
TaskLine_ = 0x0001000000, // above this line are non-task options, i.e. options which fall under HC::IO, HC::Filter and HC::Modifier
Batch = 0x0002000000,
Help = 0x0004000000,
Version = 0x0008000000,
Compliance = 0x0010000000,
Knowledge = 0x0020000000,
Flags = 0x0040000000,
TypeCodes = 0x0080000000,
FileLine_ = 0x0100000000, // below this line are options which operate on source files
Info = 0x0200000000,
ListInstances = 0x0400000000,
ListTables = 0x0800000000,
ListTemplates = 0x1000000000,
Export = 0x2000000000, // maps to either Operation::ExportBinary or ::ExportGraphics depending on argument received
Display = 0x4000000000, // below this are convenience bitsets for use with the "goesWith" safeguard in OptionTable
Filterable = (ListInstances + Export + Display), // these accept any filter
TagFilterable = (Knowledge + ListInstances + ListTables + ListTemplates + Export + Display), // these accept a tag filter
MustFilter = (Export + Display), // these require a filter to limit output
WritesOutput = (Export)
};
// Which I/O-related options are required for the task this option performs?
enum class OptionIORequirement : uint8_t
{
None = 0x0, // explicit values defined so that we can safely use bit tests to check option requirements in an efficient manner
Source = 0x1,
Destination = 0x2,
SrcAndDst = (Source + Destination),
TaskData = 0x4,
All = (TaskData + SrcAndDst)
};
using IOReq = OptionIORequirement;
struct OptionEntry
{
string fullName; // launch argument name that user types after "--"
string shortName; // short version for convenience, which follows a "-"
Option optCode; // internal code for this option
Operation operCode; // which GDM operation this option maps to, if any
OptionIORequirement ioReq; // what I/O options must be used along with this option
OptionValueQuantityReq valQReq; // how many values this option requires
OptionValueTypeReq valTReq; // what type of values this option requires
HelpCategory helpCat; // category where this option will appear on the help page
uint64_t goesWith; // what Option(s) this modifier argument is intended for
string longHelp; // full usage description, shown by --help <option>
string shortHelp; // brief one-liner, shown in the default --help listing
};
// If changing the name of an argument, make sure to search the rest of the table for mentions of that argument by its old name
namespace {
vector<OptionEntry> OptionTable = // NOLINT(cert-err58-cpp)
{
{"--alias", "-a", Option::Alias, Operation::None, IOReq::Source, OVQ::One, OVT::File,
HC::IO, static_cast<uint64_t>(Option::Unset),
"For supplying a text file with shorthand names for file paths that can be used in a command or commands. Path aliases allow you to "
"reference files by shorthand names prefixed by a '@' when specifying a source or destination for an operation, e.g. "
"\"--source @airport\". Here are some sample lines in a path alias file:\n"
#if MAC
" winr /path/to/Installations/Windows retail/GameDataFolder\n"
" airport /path/to/Installations/Mac retail/GameDataFolder/level4_Final.dat\n"
#elif WIN
" winr C:\\path\\to\\Installations\\Windows retail\\GameDataFolder\n"
" airport C:\\path\\to\\Installations\\Mac retail\\GameDataFolder\\level4_Final.dat\n"
#endif
" If an alias points to a GameDataFolder directory, as with \"winr\" above, you can append a level number to the alias when you call "
"Kanabo, e.g. \"--source @winr8\", to have it resolve to the level data file for that level. Folder expansion works for the PS2's GDF "
"as well, i.e. \"/1\" in the disc directory. Make sure not to end an alias name in the file with a number, e.g. \"airport2\", or "
"Kanabo will assume it's a reference to level 2 within a GameDataFolder pointed to by an alias called \"airport\".",
"For supplying a text file with shorthand names for file paths."},
{"--batch", "-b", Option::Batch, Operation::None, IOReq::None, OVQ::One, OVT::File,
HC::Task, static_cast<uint64_t>(Option::Unset),
"Runs a series of commands listed in a text file. Each command must be separated by a newline. If specifying an alias file to use "
"with your commands, you must do it on the command line alongside the --batch option. You must specify a destination as part of each "
"command if the command produces file output.",
"Runs a series of commands listed in a text file."},
{"--compliance", "-c", Option::Compliance, Operation::Compliance, IOReq::None, OVQ::None, OVT::Nothing,
HC::About, static_cast<uint64_t>(Option::Unset),
"Prints information on which templates Kanabo has been certified as understanding on a field by field level. Templates in this list "
"can have their field values displayed, filtered by, etc. and can be exported smartly.",
"Prints information on which templates Kanabo has been certified for."},
{"--dest", "-d", Option::Destination, Operation::None, IOReq::None, OVQ::One, OVT::FileOrFolder,
HC::IO, static_cast<uint64_t>(Option::Unset),
"Specifies the location to which files should be outputted, or the game data file or folder of game data files to create.",
"Specifies the location to which files should be outputted."},
{"--display-scale", "-ds", Option::DisplayScale, Operation::None, IOReq::None, OVQ::One, OVT::DataPacket,
HC::Modifier, static_cast<uint64_t>(Option::Display),
"Sets the scale factor for image display with --display (default: 2x on Retina displays, 1x otherwise). Only takes integer values.",
"Sets the scale factor to use with --display."},
{"--display", "-di", Option::Display, Operation::Display, IOReq::Source, OVQ::None, OVT::Nothing,
HC::Task, static_cast<uint64_t>(Option::Unset),
"Displays an instance on the command line using a graphics protocol, if the user is in a graphics-capable terminal (currently "
"supported: kitty on macOS, WezTerm on Windows). Falls back to writing a temporary file and opening it with the default image viewer "
"if the terminal does not support a graphics protocol. Supported types: TXMP and TXAN (textures/animations), TSFT and TSFF (font/font "
"family). Must be used with the --filter-name option to limit the instances to be displayed. Note that displaying a TXAN will omit the "
"first frame of the animation because this is found in the initial TXMP; you should display the texture using the name of the animated "
"TXMP to see the full animation.",
"Displays an instance visually if possible."},
{"--export", "-e", Option::Export, Operation::None, IOReq::All, OVQ::One, OVT::DataPacket,
HC::Task, static_cast<uint64_t>(Option::Unset),
"Exports the desired instances from a game data file. Requires an option value with the intended format: \"onix\" exports OniSplit-"
"compatible .onix files; \"png\" exports PNG/APNG images (assuming template support). Must be used with a filter option to specify "
"which instances to export.",
"Exports instances from a game data file as .onix files or PNG."},
{"--filter-case-insens", "-fci", Option::FilterCaseInsens, Operation::None, IOReq::None, OVQ::None, OVT::Nothing,
HC::Modifier, static_cast<uint64_t>(Option::FilterName),
"Tells the name filter to be case-insensitive.",
"Tells the name filter to be case-insensitive."},
{"--filter-field", "-ff", Option::FilterField, Operation::None, IOReq::None, OVQ::One, OVT::DataPacket,
HC::Modifier, static_cast<uint64_t>(Option::ListFields),
"Adds onto the --list-fields modifier to the --list command, telling it to only show the field whose name matches the value you supply "
"to this option. Note that --list-fields requires a filter option to limit the templates shown, regardless of whether you use this "
"filter modifier.",
"Limits --list-fields output to only the named field."},
{"--filter-index", "-fi", Option::FilterIndex, Operation::None, IOReq::Source, OVQ::One, OVT::DataPacket,
HC::Filter, static_cast<uint64_t>(Option::Filterable),
"Limits the results of a task to instances which match this index number or range of numbers. Specify a range with the '-' character.",
"Limits the results of a task to instances matching an index number or range."},
{"--filter-name", "-fn", Option::FilterName, Operation::None, IOReq::Source, OVQ::One, OVT::DataPacket,
HC::Filter, static_cast<uint64_t>(Option::Filterable),
"Limits the results of a task to instances which fully match this name. Searching for \"TRAMKON\" will not return any results because "
"there are no resources by that name. You must add the wildcard operator, '-': \"TRAMKON-\"; this will show all resources beginning "
"with TRAMKON. You could also search for \"-KON-\" to see all resources with that string somewhere in their name. See the filter "
"arguments under Modifiers for additional options.",
"Limits the results of a task to instances fully matching this name."},
{"--filter-name-partial", "-fnp", Option::FilterNamePartial, Operation::None, IOReq::None, OVQ::None, OVT::Nothing,
HC::Modifier, static_cast<uint64_t>(Option::FilterName),
"Tells the name filter to validate on a partial match. This means you do not need to use the wildcard operator with the name filter in "
"order to find resources that contain a partial string.",
"Tells the name filter to validate on a partial match without the wildcard."},
{"--filter-size", "-fs", Option::FilterSize, Operation::None, IOReq::None, OVQ::One, OVT::DataPacket,
HC::Filter, static_cast<uint64_t>(Option::ListInstances),
"Limits the instance list to instances whose total size (including aux data) matches the given expression. Use a comparison operator "
"(e.g. \">100\", \"<=512\"), a range (e.g. \"100-200\", inclusive), or an exact value (e.g. \"256\" or \"=256\"). All values are in "
"bytes. Remember to use quotes around option values that use '<' or '>' or your shell may intercept them.",
"Limits the instance list to instances of a certain size."},
{"--filter-tag", "-ft", Option::FilterTag, Operation::None, IOReq::Source, OVQ::One, OVT::DataPacket,
HC::Filter, static_cast<uint64_t>(Option::TagFilterable),
"Limits the results of a task to instances with the supplied tag. For BINA and OBJC subtypes, you can search with either the qualified "
"or unqualified name, e.g. \"TRIG\" would show both TRIG and BINA/OBJC/TRIG, but \"BINA/OBJC/TRIG\" will only show the latter.",
"Limits the results of a task to instances with the supplied tag."},
{"--filter-value", "-fv", Option::FilterValue, Operation::None, IOReq::None, OVQ::One, OVT::DataPacket,
HC::Filter, static_cast<uint64_t>(Option::FilterField),
"Filters the instance list to only show instances where the field specified by --filter-field contains the given value. Your input "
"must align with the kind of data the field holds: for numeric fields, use a comparison operator (e.g. \">0\", \"<=10\"), a range "
"(e.g. \"10-20\" or \"-5.0-5.0\", inclusive), or an exact value (floats are compared with tolerance); you can also use a \"!=\" prefix "
"to negate the match, e.g. \"!=60\" to find fields whose value is not 60, or \"!=60-90\" to find fields outside that range. For "
"boolean fields, use \"true\"/\"false\" or \"yes\"/\"no\". For flag set fields, use \"has:FlagName\" to match when the named flag is "
"set, \"has no:FlagName\" to match when it is not set, or \"has none\" to match when no flags are set; multiple flag conditions can be "
"combined in one filter (e.g. \"has:FlagA has no:FlagB\") and all must be satisfied. For type code fields, use \"type:TypeName\" to "
"match by type name, \"type not:TypeName\" to match any type other than the given name, or a numeric value to match by raw code. For "
"textual fields, the match is exact and case-sensitive. Filtering using a syntax incompatible with the field's type will produce no "
"results for that field.",
"Filters the instance list to instances with a certain field value."},
{"--filter-value-whole", "-fvw", Option::FilterValueWhole, Operation::None, IOReq::None, OVQ::None, OVT::Nothing,
HC::Modifier, static_cast<uint64_t>(Option::FilterValue),
"Modifies --filter-value to show all fields of each matching instance rather than only the field that matched.",
"Modifies --filter-value to show all fields of each matching instance."},
{"--flags", "-f", Option::Flags, Operation::Flags, IOReq::None, OVQ::None, OVT::Nothing,
HC::About, static_cast<uint64_t>(Option::Unset),
"Lists the names of all flags defined across all certified templates. Useful as a reference when using the --filter-value option on "
"a flag set field.",
"Lists the names of all flags defined across all certified templates."},
{"--help", "-h", Option::Help, Operation::None, IOReq::None, OVQ::ZeroOrOne, OVT::DataPacket,
HC::About, static_cast<uint64_t>(Option::Unset),
"Prints this help message. Use '--help <option name without leading hyphens>' for detailed help on a specific option.",
"Prints this help message. Use '--help <option name>' for detailed help."},
{"--info", "-i", Option::Info, Operation::Info, IOReq::Source, OVQ::None, OVT::Nothing,
HC::Task, static_cast<uint64_t>(Option::Unset),
"Gives basic information about a game data file.",
"Gives basic information about a game data file."},
{"--knowledge", "-k", Option::Knowledge, Operation::Knowledge, IOReq::None, OVQ::None, OVT::Nothing,
HC::About, static_cast<uint64_t>(Option::Unset),
"Prints Kanabo's knowledge of each certified template, showing the address of each field for all known game data formats, plus the "
"template's total size. The --filter-tag option will work with this operation to only show the desired template.",
"Prints Kanabo's knowledge of each certified template's fields and sizes."},
{"--list", "-l", Option::ListInstances, Operation::ListInstances, IOReq::Source, OVQ::None, OVT::Nothing,
HC::Task, static_cast<uint64_t>(Option::Unset),
"Lists the instances in a game data file.",
"Lists the instances in a game data file."},
{"--list-fields", "-lf", Option::ListFields, Operation::None, IOReq::None, OVQ::None, OVT::Nothing,
HC::Modifier, static_cast<uint64_t>(Option::ListInstances),
"Tells the instance list command to show the names and values of each field in each instance. Outputs a large amount of text, so a "
"filter must also be used with this option.",
"Tells the instance list command to show an instance's field names and values."},
{"--list-tables", "-tab", Option::ListTables, Operation::ListTables, IOReq::Source, OVQ::None, OVT::Nothing,
HC::Task, static_cast<uint64_t>(Option::Unset),
"Prints the raw descriptor tables in a game data file: template descriptors, name descriptors and instance descriptors. Use the tag "
"filter if you only want to see one template.",
"Prints the raw descriptor tables in a game data file."},
{"--list-templates", "-tem", Option::ListTemplates, Operation::ListTemplates, IOReq::Source, OVQ::None, OVT::Nothing,
HC::Task, static_cast<uint64_t>(Option::Unset),
"Scans the game data file(s) or folder(s) of data files and prints a sorted list of the template tags and their checksums across all "
"files. If the checksum for a given template differs across the files specified, all checksums will be printed. Use the tag filter if "
"you only want to see one template.",
"Compiles a list of template tags and checksums in some data file(s)."},
{"--output", "-o", Option::DestName, Operation::None, IOReq::None, OVQ::One, OVT::DataPacket,
HC::IO, static_cast<uint64_t>(Option::WritesOutput),
"Overrides the default output file name for an exported file (which would be the name of the resource being exported). Do not include "
"the file suffix when using this option, as the suffix will be determined by Kanabo.",
"Overrides the default output file name for an exported file."},
{"--overwrite-policy", "-op", Option::OverwritePolicy, Operation::None, IOReq::None, OVQ::One, OVT::DataPacket,
HC::Modifier, static_cast<uint64_t>(Option::WritesOutput),
"Sets the name collision policy when performing an export operation. Use \"overwrite\" (or \"o\") to overwrite an existing file "
"without asking, \"skip\" (or \"s\") to automatically pass over the file, or \"unique\" (or \"u\") to automatically generate a unique "
"file name for the new export.",
"Sets the name collision policy when performing an export operation."},
{"--sanitize-policy", "-sp", Option::SanitizePolicy, Operation::None, IOReq::None, OVQ::One, OVT::DataPacket,
HC::Modifier, static_cast<uint64_t>(Option::WritesOutput),
"Sets the policy for how illegal characters in file names are encoded when exporting an instance. Use \"unicode\" (or \"u\") to "
"replace illegal characters with their Unicode full-width equivalents (the default), or \"html\" (or \"h\") to replace them with HTML "
"percent-encodings.",
"Sets the policy for encoding illegal characters in file names when exporting."},
{"--show-field-offsets", "-sfo", Option::ShowFieldOffsets, Operation::None, IOReq::None, OVQ::None, OVT::Nothing,
HC::Modifier, static_cast<uint64_t>(Option::ListFields),
"Tells the instance list command to show the byte offset of each field within its instance. Use this alongside the option to list "
"fields, otherwise it will have no effect.",
"Shows the byte offset of each field with the instance list command."},
{"--show-linkees", "-sle", Option::ShowLinkees, Operation::None, IOReq::None, OVQ::One, OVT::DataPacket,
HC::Modifier, static_cast<uint64_t>(Option::ListInstances),
"Controls whether the instance list command shows the instances which are linked to by filter-matched instances. Use \"yes\" to show "
"matching instances followed by their linkees or \"only\" to show only the linkees rather than the matching instances themselves. "
"Kanabo's default behavior is to not show linked-to instances, so using \"no\" with this option produces the same behavior as not "
"using it at all. The search for linked-to instances is recursive: Kanabo shows instances linked to by the matching instance's "
"linkees, and so on. When this option is set to anything other than \"no\", --list will require at least one filter option to be "
"supplied. Cannot be used together with --show-linkers.",
"Shows instances linked to by filter-matched instances."},
{"--show-linkers", "-slr", Option::ShowLinkers, Operation::None, IOReq::None, OVQ::One, OVT::DataPacket,
HC::Modifier, static_cast<uint64_t>(Option::ListInstances),
"Controls whether the instance list command shows the instances which link to filter-matched instances. Use \"yes\" to show matching "
"instances followed by their linkers or \"only\" to show only the linkers rather than the matching instances themselves. Kanabo's "
"default behavior is to not show linking instances, so using \"no\" with this option produces the same behavior as not using it at "
"all. The search for linking instances is recursive: Kanabo shows instances that link to the matching instance's linkers, and so on. "
"When this option is set to anything other than \"no\", --list will require at least one filter option to be supplied. Cannot be used "
"together with --show-linkees.",
"Shows instances that link to filter-matched instances."},
{"--show-placeholders", "-spl", Option::ShowPlaceholders, Operation::None, IOReq::None, OVQ::One, OVT::DataPacket,
HC::Modifier, static_cast<uint64_t>(Option::ListInstances),
"Controls whether placeholder instances are shown in the instance list. Use \"no\" to hide them or \"only\" to show only the "
"placeholders. Kanabo's default behavior is to show placeholders, so using \"yes\" with this option produces the same behavior as not "
"using it at all.",
"Controls whether placeholder instances are shown in the instance list."},
{"--show-unnamed", "-sun", Option::ShowUnnamed, Operation::None, IOReq::None, OVQ::One, OVT::DataPacket,
HC::Modifier, static_cast<uint64_t>(Option::ListInstances),
"Controls whether unnamed instances are shown in the instance list. Use \"no\" to hide them or \"only\" to show only the unnamed "
"instances. Kanabo's default behavior is to show unnamed instances, so using \"yes\" with this option produces the same behavior as "
"not using it at all.",
"Controls whether unnamed instances are shown in the instance list."},
{"--show-orphans", "-sor", Option::ShowOrphans, Operation::None, IOReq::None, OVQ::One, OVT::DataPacket,
HC::Modifier, static_cast<uint64_t>(Option::ListInstances),
"Controls whether orphan instances are shown in the instance list. An orphan is an unnamed instance that has no linkers (no other "
"instance links to it). Use \"only\" to show only orphans or \"no\" to hide them. Kanabo's default behavior is to show orphans, so "
"using \"yes\" with this option produces the same behavior as not using it at all. Cannot be used together with --show-linkers or "
"--show-linkees.",
"Controls whether orphan instances are shown in the instance list."},
{"--source", "-s", Option::Source, Operation::None, IOReq::None, OVQ::Some, OVT::FileOrFolder,
HC::IO, static_cast<uint64_t>(Option::Unset),
"Specifies the game data file(s) or folder(s) of game data files to draw from.",
"Specifies the game data file(s) or folder(s) to draw from."},
{"--typecodes", "-t", Option::TypeCodes, Operation::TypeCodes, IOReq::None, OVQ::None, OVT::Nothing,
HC::About, static_cast<uint64_t>(Option::Unset),
"Lists the names of all type codes defined across all certified templates. Useful as a reference when using the --filter-value "
"option on a type code field.",
"Lists the names of all type codes defined across all certified templates."},
{"--version", "-v", Option::Version, Operation::None, IOReq::None, OVQ::None, OVT::Nothing,
HC::About, static_cast<uint64_t>(Option::Unset),
"Prints the program version.",
"Prints the program version."},
};
} // anonymous namespace
/****************************** Globals ******************************/
// Constants
constexpr string kAppVer = "1.3.27.0";
// Variables
namespace { // enforce internal linkage
namespace Main
{
RunMode mode = RunMode::Unset;
string batchPath; // path to file with list of commands to run in Batch mode
unordered_map<string, string> aliases; // path aliases loaded from the --alias argument's file; persists across batch iterations
vector<string> arguments; // all arguments the program received
vector<string> srcPaths; // source file(s)/folder(s) for a task
string dstPath; // destination file/folder for a task
string destName; // custom output name for an exported file
Option taskRequested = Option::Unset; // which task the user requested
vector<string> taskData; // data packet for a task
// These are the primary filters available for limiting the instances acted on by an operation
InstanceFilter instanceFilter = {.ifActive = false, .ifName = "", .ifPartialMatch = false, // NOLINT(cert-err58-cpp)
.ifCaseInsensitive = false, .ifIndex1 = 0, .ifIndex2 = 0, .ifTag = "", .ifFilterValue = "",
.ifFilterSize = ""};
// Settings for the "list instance" task
ListSettings listSettings = {.lsShowValues = false, .lsShowOffsets = false, .lsFilterField = "", // NOLINT(cert-err58-cpp)
.lsShowWholeValueMatch = false, .lsShowLinkers = YesNoOnly::No,
.lsShowLinkees = YesNoOnly::No, .lsShowPlaceholders = YesNoOnly::Yes,
.lsShowUnnamed = YesNoOnly::Yes, .lsShowOrphans = YesNoOnly::Yes};
int displayScale = -1; // scale factor for displaying textures (-1 = auto-detect display scale)
optional<OverwritePolicy> overwritePolicy; // policy on name collision; nullopt means user has not specified one, so use default behavior
optional<SanitizePolicy> sanitizePolicy; // policy on illegal char encoding; nullopt means use Unicode full-width default
}
} // anonymous namespace
/************************** Helper Functions **************************/
// Subclass the current locale to add commas to number output
class comma_numpunct : public numpunct<char>
{
protected:
virtual char do_thousands_sep() const
{
return ',';
}
virtual string do_grouping() const
{
return "\3";
}
};
// A streambuf subclass that intercepts characters written to cout/cerr and word-wraps long lines to fit the terminal window
class WordWrapBuf : public streambuf
{
public:
WordWrapBuf(streambuf * sink) : sink_(sink), terminalWidth_(getTerminalWidth()) {}
protected:
int_type overflow(int_type c) override
{
if (c == traits_type::eof())
return traits_type::eof();
if (c == '\n')
{
flushLine();
return sink_->sputc('\n');
}
lineBuffer_ += static_cast<char>(c);
return c;
}
int sync() override
{
// Do not flush the partial-line buffer here; content will be word-wrapped and emitted when a newline arrives via overflow(). Flushing
// here would cause each operator<< (cerr has unitbuf set) to be treated as an independent line, breaking the wrap budget for messages
// assembled across multiple operator<< calls.
return sink_->pubsync();
}
private:
streambuf * sink_;
string lineBuffer_;
int terminalWidth_;
static int getTerminalWidth()
{
#if MAC
struct winsize ws = {};
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0)
return ws.ws_col;
#elif WIN
CONSOLE_SCREEN_BUFFER_INFO csbi = {};
if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi))
return csbi.srWindow.Right - csbi.srWindow.Left + 1;
#endif
return 80; // fallback terminal width
}
void flushLine()
{
if (lineBuffer_.empty())
return;
// Fast path: line already fits within the terminal width
if (static_cast<int>(lineBuffer_.size()) <= terminalWidth_)
{
sink_->sputn(lineBuffer_.data(), static_cast<streamsize>(lineBuffer_.size()));
lineBuffer_.clear();
return;
}
// Measure leading whitespace to use as an indent on continuation lines
size_t indentLen = 0;
while (indentLen < lineBuffer_.size() && lineBuffer_[indentLen] == ' ')
indentLen++;
string indent(indentLen, ' ');
// Tokenize the line by spaces, hyphens, and forward slashes. Spaces are consumed as separators.
// Hyphens and forward slashes are sticky break characters: they stay at the end of their token
// and the token that follows them carries no space prefix. This allows wrapping at path
// separators and hyphenated words without altering the visible characters on each line.
vector<pair<string, bool>> tokens; // (token text, spaceBefore)
size_t i = indentLen; // start after the leading indent which is prepended separately
size_t tokenStart = i;
bool spaceBefore = false; // first token is prefixed only by the indent, not a space
while (i <= lineBuffer_.size())
{
char c = (i < lineBuffer_.size()) ? lineBuffer_[i] : '\0';
if (c == ' ' || c == '\0')
{
if (i > tokenStart)
{
tokens.push_back({lineBuffer_.substr(tokenStart, i - tokenStart), spaceBefore});
spaceBefore = true; // tokens after a space are space-separated
}
else
spaceBefore = true; // consecutive spaces; carry through the space flag
tokenStart = i + 1;
}
else if (c == '-' || c == '/')
{
// Include the break character at the end of this token
tokens.push_back({lineBuffer_.substr(tokenStart, i - tokenStart + 1), spaceBefore});
spaceBefore = false; // token following a break char attaches without a space
tokenStart = i + 1;
}
i++;
}
// Greedily pack tokens onto output lines
string currentLine = indent;
bool firstToken = true;
for (const auto & [text, needsSpace] : tokens)
{
if (firstToken)
{
currentLine += text;
firstToken = false;
}
else
{
int candidateSize = static_cast<int>(currentLine.size()) + (needsSpace ? 1 : 0)
+ static_cast<int>(text.size());
if (candidateSize <= terminalWidth_)
{
if (needsSpace)
currentLine += ' ';
currentLine += text;
}
else
{
// Current line is full; emit it and start a new one with the continuation indent
sink_->sputn(currentLine.data(), static_cast<streamsize>(currentLine.size()));
sink_->sputc('\n');
currentLine = indent + text;
}
}
}
if (not currentLine.empty())
sink_->sputn(currentLine.data(), static_cast<streamsize>(currentLine.size()));
lineBuffer_.clear();
}
};
namespace {
// Get an OptionTable entry's name by its option code
string GetNameByOption(Option opt)
{
auto result = find_if(OptionTable.begin(), OptionTable.end(), [opt](const OptionEntry & e) {return e.optCode == opt;});
return (result != OptionTable.end() ? format("{}/{}", result->fullName, result->shortName) : format("(lookup of option code {} failed!)",
static_cast<int>(opt)));
};
// Checks both the long and short name of an option against the string passed in
bool ArgNameMatchesOption(const string & arg, Option opt)
{
auto result = find_if(OptionTable.begin(), OptionTable.end(), [arg, opt](const OptionEntry & e)
{return (e.optCode == opt && (e.fullName == arg || e.shortName == arg));});
return (result != OptionTable.end());
}
// Find the first element in a range of arguments that matches an option by either its long or short name
vector<string>::iterator FindOptionArg(vector<string>::iterator begin, vector<string>::iterator end, Option opt)
{
auto optEntry = find_if(OptionTable.begin(), OptionTable.end(), [opt](const OptionEntry & e) {return e.optCode == opt;});
if (optEntry == OptionTable.end())
return end;
return find_if(begin, end, [&optEntry](const string & s) {return s == optEntry->fullName || s == optEntry->shortName;});
}
// Find the first element in a range of arguments that matches any of the given options; sets matchedOpt to indicate which one was found
vector<string>::iterator FindOptionArg(vector<string>::iterator begin, vector<string>::iterator end,
initializer_list<Option> opts, Option & matchedOpt)
{
auto earliest = end;
for (Option opt : opts)
{
auto it = FindOptionArg(begin, end, opt);
if (it != end && (earliest == end || it < earliest))
{
earliest = it;
matchedOpt = opt;
}
}
return earliest;
}
// Get an OptionValueTable entry's help message by its requirements
string GetOptionValueUsageByOption(OptionEntry opt)
{
auto result = find_if(OptionValueTable.begin(), OptionValueTable.end(), [opt](const OptionValueEntry & e)
{return (e.oveQuan == opt.valQReq) && (e.oveType == opt.valTReq);});
return (result != OptionValueTable.end() ? result->errorHelpText : format("(lookup of usage for quantity req {} and type req {} failed!)",
static_cast<int>(opt.valQReq), static_cast<int>(opt.valTReq)));
}
// For when we need to re-init globals between executing lines of a batch file
void InitMain(void)
{
Main::arguments.clear();
Main::srcPaths.clear();
Main::dstPath.clear();
Main::destName.clear();
Main::taskRequested = Option::Unset;
Main::taskData.clear();
Main::instanceFilter.ifActive = false;
Main::instanceFilter.ifName = "";
Main::instanceFilter.ifPartialMatch = false;
Main::instanceFilter.ifCaseInsensitive = false;
Main::instanceFilter.ifIndex1 = 0;
Main::instanceFilter.ifIndex2 = 0;
Main::instanceFilter.ifFilterValue = "";
Main::instanceFilter.ifFilterSize = "";
Main::listSettings.lsShowValues = false;
Main::listSettings.lsShowOffsets = false;
Main::listSettings.lsFilterField = "";
Main::listSettings.lsShowWholeValueMatch = false;
Main::listSettings.lsShowLinkers = YesNoOnly::No;
Main::listSettings.lsShowLinkees = YesNoOnly::No;
Main::listSettings.lsShowPlaceholders = YesNoOnly::Yes;
Main::listSettings.lsShowUnnamed = YesNoOnly::Yes;
Main::displayScale = -1;
Main::overwritePolicy = nullopt;
Main::sanitizePolicy = nullopt;
}
// Print brief listing of all accepted options
void PrintHelp(void)
{
cout << "Kanabo is a tool for analyzing and manipulating data for the Oni game engine.\n";
cout << "It examines, alters and repacks game resources in all known binary file formats.\n";
cout << "Use '--help <option>' (without leading hyphens) for detailed help on any option.\n";
cout << "You can also find the detailed documentation at https://wiki.oni2.net/Kanabo.\n";
// Print options by help category
for (auto thisHC = HelpCategoryEntryTable.begin(); thisHC != HelpCategoryEntryTable.end(); thisHC++)
{
bool printedCat = false;
bool firstAfterCatName = true;
// Find all options in this category
for (auto thisOpt = OptionTable.begin(); thisOpt != OptionTable.end(); thisOpt++)
{
if (thisOpt->helpCat == thisHC->hceCat)
{
// Only print category name if we found a member in that category
if (not printedCat)
{
cout << "\n" << thisHC->hceName << "\n";
cout << thisHC->hceDesc << "\n";
printedCat = true;
}
// Print option name
cout << (firstAfterCatName ? "" : "\n") << " " << GetNameByOption(thisOpt->optCode) << " ";
if (firstAfterCatName)
firstAfterCatName = false;
// Find usage message that goes along with this option
auto thisOV = OptionValueTable.begin();
for (; thisOV != OptionValueTable.end(); thisOV++)
{
if (thisOV->oveQuan == thisOpt->valQReq && thisOV->oveType == thisOpt->valTReq)
{
if (thisOV->oveType != OVT::Nothing)
{
#if WIN
// Substitute backward slashes for forward slashes in the sample paths
replace(thisOV->usageHelpText.begin(), thisOV->usageHelpText.end(), '/', '\\');
#endif
cout << thisOV->usageHelpText << "\n";
}
else
cout << '\n';
break;
}
}
if (thisOV == OptionValueTable.end())
cout << format("(lookup of help for quantity req {} and type req {} failed!)\n",
static_cast<int>(thisOpt->valQReq), static_cast<int>(thisOpt->valTReq));
// Print brief option description
cout << " " << thisOpt->shortHelp << "\n";
}
}
}
}
// Print detailed help for a single option identified by name (without leading hyphens)
void PrintArgLongHelp(string_view argName)
{
// Strip any accidental leading hyphens the user may have included
while (not argName.empty() && argName.front() == '-')
argName.remove_prefix(1);
// Search for a matching option by full or short name
auto thisOpt = OptionTable.end();
for (auto it = OptionTable.begin(); it != OptionTable.end(); it++)
{
if (it->fullName == "--" + string(argName) ||
it->shortName == "-" + string(argName))
{
thisOpt = it;
break;
}
}
if (thisOpt == OptionTable.end())
{
cerr << "Advisory: Unknown option " << quoted(string(argName)) << "; use '--help' with no option value to list all options.\n";
return;
}
// Print option name line
cout << "Printing detailed help for " << thisOpt->fullName << ".\n";
cout << GetNameByOption(thisOpt->optCode) << " ";
// Find and print the usage text for this option
auto thisOV = OptionValueTable.begin();
for (; thisOV != OptionValueTable.end(); thisOV++)
{
if (thisOV->oveQuan == thisOpt->valQReq && thisOV->oveType == thisOpt->valTReq)
{
if (thisOV->oveType != OVT::Nothing)
{
#if WIN
replace(thisOV->usageHelpText.begin(), thisOV->usageHelpText.end(), '/', '\\');
#endif
cout << thisOV->usageHelpText << "\n";
}
else
cout << '\n';
break;
}
}
if (thisOV == OptionValueTable.end())
cout << format("(lookup of long help for quantity req {} and type req {} failed!)\n",
static_cast<int>(thisOpt->valQReq), static_cast<int>(thisOpt->valTReq));
// Print full detail description
cout << " " << thisOpt->longHelp << "\n";
}
/************************ Argument Processing ************************/
// If a path or data packet supplied as an argument contains spaces, it may be recorded into our argument array in pieces (this is known to
// happen when reading arguments from a text file). Here we search for arguments that are pieces of a fragmented value and re-unify them. Values
// using either single/double quote marks or backslash-escaped spaces are accepted.
bool UnifySplitValues(void)
{
bool usesQuotes = false;
// Set up a few lambdas
auto looksLikeOption = [](auto arg)
{
return (arg->front() == '-');
};
auto quoteAtStart = [](auto arg)
{
return arg->front() == '"' || arg->front() == '\'';
};
auto quoteAtEnd = [](auto arg)
{
return arg->back() == '"' || arg->back() == '\'';
};
auto backslashAtEnd = [](auto arg)
{
return arg->back() == '\\';
};
auto looksLikeStartOfSplitValue = [quoteAtStart, quoteAtEnd, backslashAtEnd, &usesQuotes](auto arg)
{
if (quoteAtStart(arg) && not quoteAtEnd(arg))
{
usesQuotes = true;
return true;
}
else if (backslashAtEnd(arg))
{
usesQuotes = false;
return true;
}
return false;
};
auto looksLikeEndOfSplitValue = [quoteAtEnd, backslashAtEnd, &usesQuotes](auto arg)
{
if (usesQuotes && quoteAtEnd(arg))
return true;
if (not usesQuotes && not backslashAtEnd(arg))
return true;
return false;
};
auto clearQuotesOrBackslashes = [&usesQuotes](auto arg)
{
if (usesQuotes)
{
arg->erase(0, 1);
if (arg->size() >= 2)
arg->erase(arg->length()-1);
}
else
{
size_t pos = 0;
while ((pos = arg->find('\\', pos)) != string::npos)
arg->erase(pos, 1);
}
};
for (auto thisArg = Main::arguments.begin(); thisArg != Main::arguments.end(); thisArg++)
{
if (looksLikeStartOfSplitValue(thisArg))
{
// Scan ahead and merge the split value's pieces until we hit the last piece
auto thisArg2 = next(thisArg);
while(thisArg2 != Main::arguments.end())
{
if (looksLikeOption(thisArg2))
{
cerr << "Error: Could not process arguments because this value did not end before another option was reached: "
<< quoted(*thisArg2) << "\n";
return false;
}
// Tack on this fragment of the value, restoring the space that got eaten by argv[], then delete the fragment
*thisArg = *thisArg + " " + *thisArg2;
if (looksLikeEndOfSplitValue(thisArg2))
{
Main::arguments.erase(thisArg2);
break;
}
else
{
thisArg2 = Main::arguments.erase(thisArg2); // advances "thisArg2"
continue;
}
}
clearQuotesOrBackslashes(thisArg);
}
// While we're here, we need to account for the possibility that the user passed in a path or data packet in quotes that did not need
// them, and therefore it was never split into separate elements of argv[]. Although this isn't a fragmented path, we still have to
// remove the quote marks.
else if (quoteAtStart(thisArg) && quoteAtEnd(thisArg))
{
usesQuotes = true;
clearQuotesOrBackslashes(thisArg);
}
}
return true;
}
// Make the arguments ready to be iterated through, then use the incoming arguments to determine the mode we're supposed to run in
bool ProcessArgumentsAndSetMode(const char * argv[])
{
// Move the args into a global vector
for (int i = 0; argv[i] != nullptr; i++)
Main::arguments.push_back(argv[i]);
// If we have no arguments, program must have been opened with a double-click
if (Main::arguments.empty())
{
Main::mode = RunMode::GUI;
return true;
}
// Remove the first argument (program name) as it's not helpful from here on
Main::arguments.erase(Main::arguments.begin());
// Was anything passed in besides the program name?
if (Main::arguments.empty())
{
cerr << "Advisory: No options received. Run Kanabo with the " << quoted(GetNameByOption(Option::Help)) << " option.\n";
return false;
}
// Merge arguments that represent pieces of file paths into single elements
if (not UnifySplitValues())
return false;
// This is a bit of a hack; we're cheating by running ahead of ProcessOptionsAndValues(). But we need to check for the version arg now so
// that "taskRequested" can be set before PrintHeader() runs
if (FindOptionArg(Main::arguments.begin(), Main::arguments.end(), Option::Version) != Main::arguments.end())
{
if (Main::arguments.size() != 1)
{
cerr << "Advisory: When asking for the version, you should not pass Kanabo any other arguments.\n";
return false;
}
else // Main::arguments.size() == 1
{
Main::taskRequested = Option::Version;
Main::mode = RunMode::CLI;
return true;
}
}
// Check for the batch option and save location of batch file
auto batchArgIter = FindOptionArg(Main::arguments.begin(), Main::arguments.end(), Option::Batch);
if (batchArgIter != Main::arguments.end())
{
auto batchValueIter = next(batchArgIter);
if (batchValueIter == Main::arguments.end() || batchValueIter->front() == '-')
{
cerr << "Error: Did not receive a path to the batch file you want to run.\n";
return false;
}
// Validate that the only other arguments present are at most one --alias <file> pair