Skip to content

Commit 1930f4b

Browse files
committed
Fix GitHub Issue #1: Implement conflicting options
- The user can now set two or more options as conflicting
1 parent 0d22a41 commit 1930f4b

File tree

9 files changed

+293
-3
lines changed

9 files changed

+293
-3
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,38 @@ Options:
247247
248248
```
249249

250+
## General: Setting two or more options as conflicting
251+
252+
Sometimes we want to prevent the user from giving a certain set of arguments.
253+
254+
Consider this example code:
255+
256+
```
257+
...
258+
259+
juzzlin::Argengine ae(argc, argv);
260+
ae.addOption(
261+
{ "foo" }, [] {
262+
std::cout << "Foo enabled!" << std::endl;
263+
});
264+
265+
ae.addOption(
266+
{ "bar" }, [] {
267+
std::cout << "Bar enabled!" << std::endl;
268+
});
269+
270+
// Set "foo" and "bar" as conflicting options.
271+
ae.addConflictingOptions({ "foo", "bar" });
272+
...
273+
```
274+
275+
Now, if we give both `foo` and `bar` to the application, we'll get an error like this:
276+
277+
278+
```
279+
Argengine: Conflicting options: 'bar', 'foo'. These options cannot coexist.
280+
```
281+
250282
## General: Error handling
251283

252284
For error handling there are two options: exceptions or error value.

src/argengine.cpp

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ class Argengine::Impl
6464
return od;
6565
}
6666

67+
void addConflictingOptions(ConflictingOptionSet conflictingOptionSet)
68+
{
69+
m_conflictingOptionSets.push_back(conflictingOptionSet);
70+
}
71+
6772
ArgumentVector arguments() const
6873
{
6974
return m_args;
@@ -327,10 +332,34 @@ class Argengine::Impl
327332
return tokens;
328333
}
329334

335+
void checkConflictingOptions(const ArgumentVector & tokens)
336+
{
337+
OptionDefinitionVector optionDefinitions;
338+
for (size_t i = 1; i < tokens.size(); i++) {
339+
if (const auto arg = tokens.at(i); const auto definition = getOptionDefinition(arg)) {
340+
optionDefinitions.push_back(definition);
341+
}
342+
}
343+
for (auto && conflictingOptionSet : m_conflictingOptionSets) {
344+
ConflictingOptionSet conflictingOptionSetForError;
345+
for (auto && conflictingOption : conflictingOptionSet) {
346+
for (auto && optionDefinition : optionDefinitions) {
347+
if (optionDefinition->matches({ conflictingOption })) {
348+
conflictingOptionSetForError.insert(conflictingOption);
349+
}
350+
}
351+
}
352+
if (conflictingOptionSetForError.size() > 1) {
353+
throwConflictingOptionsError(conflictingOptionSetForError);
354+
}
355+
}
356+
}
357+
330358
void processArgs(bool dryRun)
331359
{
332360
const auto tokens = tokenize(m_args);
333-
ArgumentVector positionalArguments;
361+
362+
checkConflictingOptions(tokens);
334363

335364
// Process help first as it's a special case
336365
for (size_t i = 1; i < tokens.size(); i++) {
@@ -343,6 +372,7 @@ class Argengine::Impl
343372
}
344373

345374
// Other arguments
375+
ArgumentVector positionalArguments;
346376
for (size_t i = 1; i < tokens.size(); i++) {
347377
if (const auto arg = tokens.at(i); const auto definition = getOptionDefinition(arg)) {
348378
if (!definition->isHelp) {
@@ -385,6 +415,18 @@ class Argengine::Impl
385415
return currentIndex;
386416
}
387417

418+
[[noreturn]] void throwConflictingOptionsError(const ConflictingOptionSet & conflictingOptionSet) const
419+
{
420+
std::string optionsString;
421+
for (auto && option : conflictingOptionSet) {
422+
if (!optionsString.empty() && optionsString.back() == '\'') {
423+
optionsString += ", ";
424+
}
425+
optionsString += "'" + option + "'";
426+
}
427+
throw std::runtime_error(name() + ": Conflicting options: " + optionsString + ". These options cannot coexist.");
428+
}
429+
388430
[[noreturn]] void throwOptionExistingError(const OptionDefinition & existing) const
389431
{
390432
throw std::runtime_error(name() + ": Option '" + existing.getVariantsString() + "' already defined!");
@@ -411,7 +453,10 @@ class Argengine::Impl
411453

412454
HelpSorting m_helpSorting = HelpSorting::None;
413455

414-
std::vector<OptionDefinitionPtr> m_optionDefinitions;
456+
using OptionDefinitionVector = std::vector<OptionDefinitionPtr>;
457+
OptionDefinitionVector m_optionDefinitions;
458+
459+
std::vector<ConflictingOptionSet> m_conflictingOptionSets;
415460

416461
MultiStringCallback m_positionalArgumentCallback = nullptr;
417462

@@ -445,6 +490,11 @@ void Argengine::addHelp(OptionVariants optionVariants, ValuelessCallback callbac
445490
m_impl->addOption(optionVariants, callback, false, SHOW_THIS_HELP_TEXT)->isHelp = true;
446491
}
447492

493+
void Argengine::addConflictingOptions(ConflictingOptionSet conflictingOptionSet)
494+
{
495+
m_impl->addConflictingOptions(conflictingOptionSet);
496+
}
497+
448498
Argengine::ArgumentVector Argengine::arguments() const
449499
{
450500
return m_impl->arguments();

src/argengine.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ class Argengine
7474
//! \param callback Callback to be called when the help option has been given. Signature: `void()`.
7575
void addHelp(OptionVariants optionVariants, ValuelessCallback callback);
7676

77+
//! Adds a options that cannot coexist.
78+
//! \param conflictingOptions A set of possible options that cannot coexist, e.g.: {"--bar", "--foo"}
79+
using ConflictingOptionSet = std::set<std::string>;
80+
void addConflictingOptions(ConflictingOptionSet conflictingOptionSet);
81+
7782
//! \return All given arguments.
7883
ArgumentVector arguments() const;
7984

src/examples/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
add_subdirectory(ex1)
2-
2+
add_subdirectory(ex2)

src/examples/ex2/CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
set(ARGENGINE_DIR ${CMAKE_SOURCE_DIR}/src)
2+
include_directories(${ARGENGINE} ${CMAKE_CURRENT_SOURCE_DIR})
3+
4+
set(NAME ex2)
5+
set(SRC ${NAME}.cpp)
6+
7+
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
8+
add_executable(${NAME} ${SRC})
9+
target_link_libraries(${NAME} ${LIBRARY_NAME})
10+

src/examples/ex2/ex2.cpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// MIT License
2+
//
3+
// Copyright (c) 2023 Jussi Lind <jussi.lind@iki.fi>
4+
//
5+
// https://github.com/juzzlin/Argengine
6+
//
7+
// Permission is hereby granted, free of charge, to any person obtaining a copy
8+
// of this software and associated documentation files (the "Software"), to deal
9+
// in the Software without restriction, including without limitation the rights
10+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
// copies of the Software, and to permit persons to whom the Software is
12+
// furnished to do so, subject to the following conditions:
13+
//
14+
// The above copyright notice and this permission notice shall be included in all
15+
// copies or substantial portions of the Software.
16+
//
17+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
// SOFTWARE.
24+
25+
#include "argengine.hpp"
26+
27+
#include <cstdlib>
28+
#include <iostream>
29+
30+
using juzzlin::Argengine;
31+
32+
//
33+
// Ex2: Demonstrate conflicting options.
34+
//
35+
36+
int main(int argc, char ** argv)
37+
{
38+
// Instantiate Argengine and give it the raw argument data.
39+
Argengine ae(argc, argv);
40+
41+
// Add a minimal dummy option "foo".
42+
ae.addOption(
43+
{ "foo" }, [] {
44+
std::cout << "Foo enabled!" << std::endl;
45+
});
46+
47+
// Add a minimal dummy option "bar".
48+
ae.addOption(
49+
{ "bar" }, [] {
50+
std::cout << "Bar enabled!" << std::endl;
51+
});
52+
53+
// Set "foo" and "bar" as conflicting options.
54+
ae.addConflictingOptions({ "foo", "bar" });
55+
56+
// Parse the arguments and store possible error to variable error.
57+
Argengine::Error error;
58+
ae.parse(error);
59+
60+
// Check error and print the possible error message.
61+
if (error.code != Argengine::Error::Code::Ok) {
62+
std::cerr << error.message << std::endl
63+
<< std::endl;
64+
ae.printHelp(); // Print the auto-generated help.
65+
return EXIT_FAILURE;
66+
}
67+
68+
return EXIT_SUCCESS;
69+
}

src/tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
add_subdirectory(conflicting_arguments_test)
12
add_subdirectory(help_test)
23
add_subdirectory(positional_argument_test)
34
add_subdirectory(single_value_test)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
set(ARGENGINE_DIR ${CMAKE_SOURCE_DIR}/src)
2+
include_directories(${ARGENGINE} ${CMAKE_CURRENT_SOURCE_DIR})
3+
4+
set(NAME conflicting_arguments_test)
5+
set(SRC ${NAME}.cpp)
6+
7+
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/tests)
8+
add_executable(${NAME} ${SRC})
9+
add_test(${NAME} ${CMAKE_BINARY_DIR}/tests/${NAME})
10+
target_link_libraries(${NAME} ${LIBRARY_NAME})
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// MIT License
2+
//
3+
// Copyright (c) 2023 Jussi Lind <jussi.lind@iki.fi>
4+
//
5+
// https://github.com/juzzlin/Argengine
6+
//
7+
// Permission is hereby granted, free of charge, to any person obtaining a copy
8+
// of this software and associated documentation files (the "Software"), to deal
9+
// in the Software without restriction, including without limitation the rights
10+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
// copies of the Software, and to permit persons to whom the Software is
12+
// furnished to do so, subject to the following conditions:
13+
//
14+
// The above copyright notice and this permission notice shall be included in all
15+
// copies or substantial portions of the Software.
16+
//
17+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
// SOFTWARE.
24+
25+
#include "../../argengine.hpp"
26+
27+
// Don't compile asserts away
28+
#ifdef NDEBUG
29+
#undef NDEBUG
30+
#endif
31+
32+
#include <cassert>
33+
#include <cstdlib>
34+
#include <iostream>
35+
36+
using juzzlin::Argengine;
37+
38+
const auto name = "Argengine";
39+
40+
void testConflictingArguments_ConflictingArgumentsGiven_ShouldFail()
41+
{
42+
Argengine ae({ "test", "foo", "bar" });
43+
bool fooCalled {};
44+
ae.addOption({ "foo" }, [&] {
45+
fooCalled = true;
46+
});
47+
bool barCalled {};
48+
ae.addOption({ "bar" }, [&] {
49+
barCalled = true;
50+
});
51+
ae.addConflictingOptions({ "foo", "bar" });
52+
std::string error;
53+
try {
54+
ae.parse();
55+
} catch (std::runtime_error & e) {
56+
error = e.what();
57+
}
58+
assert(!fooCalled);
59+
assert(!barCalled);
60+
assert(error == std::string(name) + ": Conflicting options: 'bar', 'foo'. These options cannot coexist.");
61+
}
62+
63+
void testConflictingArguments_ConflictingArgumentsSetButNotEnoughGiven_ShouldSucceed()
64+
{
65+
Argengine ae({ "test", "foo" });
66+
bool fooCalled {};
67+
ae.addOption({ "foo" }, [&] {
68+
fooCalled = true;
69+
});
70+
ae.addConflictingOptions({ "foo", "bar" });
71+
std::string error;
72+
try {
73+
ae.parse();
74+
} catch (std::runtime_error & e) {
75+
error = e.what();
76+
}
77+
assert(fooCalled);
78+
assert(error.empty());
79+
}
80+
81+
void testConflictingArguments_ConflictingArgumentsGiven_ShouldNotReferToVariants()
82+
{
83+
Argengine ae({ "test", "foo", "bar" });
84+
bool fooCalled {};
85+
ae.addOption({ "foo", "-f" }, [&] {
86+
fooCalled = true;
87+
});
88+
bool barCalled {};
89+
ae.addOption({ "-b", "bar" }, [&] {
90+
barCalled = true;
91+
});
92+
ae.addConflictingOptions({ "foo", "bar" });
93+
std::string error;
94+
try {
95+
ae.parse();
96+
} catch (std::runtime_error & e) {
97+
error = e.what();
98+
}
99+
assert(!fooCalled);
100+
assert(!barCalled);
101+
assert(error == std::string(name) + ": Conflicting options: 'bar', 'foo'. These options cannot coexist.");
102+
}
103+
104+
int main(int, char **)
105+
{
106+
testConflictingArguments_ConflictingArgumentsGiven_ShouldFail();
107+
108+
testConflictingArguments_ConflictingArgumentsSetButNotEnoughGiven_ShouldSucceed();
109+
110+
testConflictingArguments_ConflictingArgumentsGiven_ShouldNotReferToVariants();
111+
112+
return EXIT_SUCCESS;
113+
}

0 commit comments

Comments
 (0)