Commit 09cc63cb authored by Andrew Sun's avatar Andrew Sun Committed by Facebook GitHub Bot

Allow customizing boost::po::command_line_style in NestedCommandLineApp

Summary:
NestedCommandLineApp works by performing two passes over the argument vector:

1. Treat the entire argv as a single command, ignoring subcommands. Consume all options that "match" a global flag here.

2. Find the first positional argument in argv and treat it as a subcommand, then re-parse everything after it as options to the subcommand.

This allows constructs like `cli subcommand --subcommand-flag --global-flag`. However, as a side effect, if `subcommand-flag` happens to be even a prefix of a global flag name, boost "helpfully" matches it with the global flag in the first pass so that the subcommand never even sees it.

Fixing the issue of a subcommand flag conflicting with a global flag would break backwards compatibility; however, we can at least add an option to disable prefix matching so that there would need to be an exact flag name collision for this scenario to occur. It may be possible to extend NestedCommandLineApp in the future to detect flag collisions as well, instead of silently associating arguments with the wrong flag.

Reviewed By: yfeldblum

Differential Revision: D27419341

fbshipit-source-id: 941c0769e16d40c87f96620155f0ab09927cef3d
parent 12466d5f
...@@ -61,7 +61,8 @@ NestedCommandLineApp::NestedCommandLineApp( ...@@ -61,7 +61,8 @@ NestedCommandLineApp::NestedCommandLineApp(
programHelpFooter_(std::move(programHelpFooter)), programHelpFooter_(std::move(programHelpFooter)),
version_(std::move(version)), version_(std::move(version)),
initFunction_(std::move(initFunction)), initFunction_(std::move(initFunction)),
globalOptions_("Global options") { globalOptions_("Global options"),
optionStyle_(po::command_line_style::default_style) {
addCommand( addCommand(
kHelpCommand.str(), kHelpCommand.str(),
"[command]", "[command]",
...@@ -117,6 +118,11 @@ void NestedCommandLineApp::addAlias(std::string newName, std::string oldName) { ...@@ -117,6 +118,11 @@ void NestedCommandLineApp::addAlias(std::string newName, std::string oldName) {
aliases_.emplace(std::move(newName), std::move(oldName)); aliases_.emplace(std::move(newName), std::move(oldName));
} }
void NestedCommandLineApp::setOptionStyle(
boost::program_options::command_line_style::style_t style) {
optionStyle_ = style;
}
void NestedCommandLineApp::displayHelp( void NestedCommandLineApp::displayHelp(
const po::variables_map& /* globalOptions */, const po::variables_map& /* globalOptions */,
const std::vector<std::string>& args) const { const std::vector<std::string>& args) const {
...@@ -285,7 +291,7 @@ void NestedCommandLineApp::doRun(const std::vector<std::string>& args) { ...@@ -285,7 +291,7 @@ void NestedCommandLineApp::doRun(const std::vector<std::string>& args) {
} }
} }
auto parsed = parseNestedCommandLine(cleanArgs, globalOptions_); auto parsed = parseNestedCommandLine(cleanArgs, globalOptions_, optionStyle_);
po::variables_map vm; po::variables_map vm;
po::store(parsed.options, vm); po::store(parsed.options, vm);
if (vm.count(kHelpCommand.str())) { if (vm.count(kHelpCommand.str())) {
...@@ -315,8 +321,10 @@ void NestedCommandLineApp::doRun(const std::vector<std::string>& args) { ...@@ -315,8 +321,10 @@ void NestedCommandLineApp::doRun(const std::vector<std::string>& args) {
auto& cmd = p.first; auto& cmd = p.first;
auto& info = p.second; auto& info = p.second;
auto cmdOptions = auto cmdOptions = po::command_line_parser(parsed.rest)
po::command_line_parser(parsed.rest).options(info.options).run(); .options(info.options)
.style(optionStyle_)
.run();
po::store(cmdOptions, vm); po::store(cmdOptions, vm);
po::notify(vm); po::notify(vm);
......
...@@ -46,6 +46,17 @@ class FOLLY_EXPORT ProgramExit : public std::runtime_error { ...@@ -46,6 +46,17 @@ class FOLLY_EXPORT ProgramExit : public std::runtime_error {
* App that uses a nested command line, of the form: * App that uses a nested command line, of the form:
* *
* program [--global_options...] command [--command_options...] command_args... * program [--global_options...] command [--command_options...] command_args...
*
* Note: Global options (including GFlags, if added using addGFlags()) are
* recognized anywhere in the command line, and are prefix matched with higher
* priority than command options. For example, a global option named "--foobar"
* would be matched over a command option named "--foo", even if you specify
* "--foo" on the command line. You can disable prefix matching with:
*
* int style = boost::program_options::command_line_style::default_style;
* style &= ~boost::program_options::command_line_style::allow_guessing;
* app.setOptionStyle(
* static_cast<boost::program_options::command_line_style::style_t>(style));
*/ */
class NestedCommandLineApp { class NestedCommandLineApp {
public: public:
...@@ -120,6 +131,12 @@ class NestedCommandLineApp { ...@@ -120,6 +131,12 @@ class NestedCommandLineApp {
*/ */
void addAlias(std::string newName, std::string oldName); void addAlias(std::string newName, std::string oldName);
/**
* Sets the style in which options will be accepted by the parser.
*/
void setOptionStyle(
boost::program_options::command_line_style::style_t style);
/** /**
* Run the command and return; the return code is 0 on success or * Run the command and return; the return code is 0 on success or
* non-zero on error, so it is idiomatic to call this at the end of main(): * non-zero on error, so it is idiomatic to call this at the end of main():
...@@ -165,6 +182,7 @@ class NestedCommandLineApp { ...@@ -165,6 +182,7 @@ class NestedCommandLineApp {
std::string version_; std::string version_;
InitFunction initFunction_; InitFunction initFunction_;
boost::program_options::options_description globalOptions_; boost::program_options::options_description globalOptions_;
boost::program_options::command_line_style::style_t optionStyle_;
std::map<std::string, CommandInfo> commands_; std::map<std::string, CommandInfo> commands_;
std::map<std::string, std::string> aliases_; std::map<std::string, std::string> aliases_;
std::set<folly::StringPiece> builtinCommands_; std::set<folly::StringPiece> builtinCommands_;
......
...@@ -285,10 +285,12 @@ po::options_description getGFlags(ProgramOptionsStyle style) { ...@@ -285,10 +285,12 @@ po::options_description getGFlags(ProgramOptionsStyle style) {
namespace { namespace {
NestedCommandLineParseResult doParseNestedCommandLine( NestedCommandLineParseResult doParseNestedCommandLine(
po::command_line_parser&& parser, const po::options_description& desc) { po::command_line_parser&& parser,
const po::options_description& desc,
boost::program_options::command_line_style::style_t style) {
NestedCommandLineParseResult result; NestedCommandLineParseResult result;
result.options = parser.options(desc).allow_unregistered().run(); result.options = parser.options(desc).style(style).allow_unregistered().run();
bool setCommand = true; bool setCommand = true;
for (auto& opt : result.options.options) { for (auto& opt : result.options.options) {
...@@ -319,14 +321,20 @@ NestedCommandLineParseResult doParseNestedCommandLine( ...@@ -319,14 +321,20 @@ NestedCommandLineParseResult doParseNestedCommandLine(
} // namespace } // namespace
NestedCommandLineParseResult parseNestedCommandLine( NestedCommandLineParseResult parseNestedCommandLine(
int argc, const char* const argv[], const po::options_description& desc) { int argc,
return doParseNestedCommandLine(po::command_line_parser(argc, argv), desc); const char* const argv[],
const po::options_description& desc,
boost::program_options::command_line_style::style_t style) {
return doParseNestedCommandLine(
po::command_line_parser(argc, argv), desc, style);
} }
NestedCommandLineParseResult parseNestedCommandLine( NestedCommandLineParseResult parseNestedCommandLine(
const std::vector<std::string>& cmdline, const std::vector<std::string>& cmdline,
const po::options_description& desc) { const po::options_description& desc,
return doParseNestedCommandLine(po::command_line_parser(cmdline), desc); boost::program_options::command_line_style::style_t style) {
return doParseNestedCommandLine(
po::command_line_parser(cmdline), desc, style);
} }
} // namespace folly } // namespace folly
...@@ -77,10 +77,14 @@ struct NestedCommandLineParseResult { ...@@ -77,10 +77,14 @@ struct NestedCommandLineParseResult {
NestedCommandLineParseResult parseNestedCommandLine( NestedCommandLineParseResult parseNestedCommandLine(
int argc, int argc,
const char* const argv[], const char* const argv[],
const boost::program_options::options_description& desc); const boost::program_options::options_description& desc,
boost::program_options::command_line_style::style_t style =
boost::program_options::command_line_style::default_style);
NestedCommandLineParseResult parseNestedCommandLine( NestedCommandLineParseResult parseNestedCommandLine(
const std::vector<std::string>& cmdline, const std::vector<std::string>& cmdline,
const boost::program_options::options_description& desc); const boost::program_options::options_description& desc,
boost::program_options::command_line_style::style_t style =
boost::program_options::command_line_style::default_style);
} // namespace folly } // namespace folly
...@@ -75,10 +75,10 @@ TEST(ProgramOptionsTest, Errors) { ...@@ -75,10 +75,10 @@ TEST(ProgramOptionsTest, Errors) {
TEST(ProgramOptionsTest, Help) { TEST(ProgramOptionsTest, Help) {
// Not actually checking help output, just verifying that help doesn't fail // Not actually checking help output, just verifying that help doesn't fail
callHelper({"--version"}); callHelper({"--version"});
callHelper({"--h"}); callHelper({"-h"});
callHelper({"--h", "foo"}); callHelper({"-h", "foo"});
callHelper({"--h", "bar"}); callHelper({"-h", "bar"});
callHelper({"--h", "--", "bar"}); callHelper({"-h", "--", "bar"});
callHelper({"--help"}); callHelper({"--help"});
callHelper({"--help", "foo"}); callHelper({"--help", "foo"});
callHelper({"--help", "bar"}); callHelper({"--help", "bar"});
...@@ -108,6 +108,8 @@ TEST(ProgramOptionsTest, CutArguments) { ...@@ -108,6 +108,8 @@ TEST(ProgramOptionsTest, CutArguments) {
"running foo\n" "running foo\n"
"foo global-foo 43\n" "foo global-foo 43\n"
"foo local-foo 42\n" "foo local-foo 42\n"
"foo conflict-global 42\n"
"foo conflict 42\n"
"foo arg b\n" "foo arg b\n"
"foo arg --local-foo\n" "foo arg --local-foo\n"
"foo arg 44\n" "foo arg 44\n"
...@@ -120,13 +122,17 @@ TEST(ProgramOptionsTest, Success) { ...@@ -120,13 +122,17 @@ TEST(ProgramOptionsTest, Success) {
EXPECT_EQ( EXPECT_EQ(
"running foo\n" "running foo\n"
"foo global-foo 42\n" "foo global-foo 42\n"
"foo local-foo 42\n", "foo local-foo 42\n"
"foo conflict-global 42\n"
"foo conflict 42\n",
callHelper({"foo"})); callHelper({"foo"}));
EXPECT_EQ( EXPECT_EQ(
"running foo\n" "running foo\n"
"foo global-foo 43\n" "foo global-foo 43\n"
"foo local-foo 44\n" "foo local-foo 44\n"
"foo conflict-global 42\n"
"foo conflict 42\n"
"foo arg a\n" "foo arg a\n"
"foo arg b\n", "foo arg b\n",
callHelper({"--global-foo", "43", "foo", "--local-foo", "44", "a", "b"})); callHelper({"--global-foo", "43", "foo", "--local-foo", "44", "a", "b"}));
...@@ -136,6 +142,8 @@ TEST(ProgramOptionsTest, Success) { ...@@ -136,6 +142,8 @@ TEST(ProgramOptionsTest, Success) {
"running foo\n" "running foo\n"
"foo global-foo 43\n" "foo global-foo 43\n"
"foo local-foo 44\n" "foo local-foo 44\n"
"foo conflict-global 42\n"
"foo conflict 42\n"
"foo arg a\n" "foo arg a\n"
"foo arg b\n", "foo arg b\n",
callHelper({"foo", "--global-foo", "43", "--local-foo", "44", "a", "b"})); callHelper({"foo", "--global-foo", "43", "--local-foo", "44", "a", "b"}));
...@@ -146,6 +154,8 @@ TEST(ProgramOptionsTest, Aliases) { ...@@ -146,6 +154,8 @@ TEST(ProgramOptionsTest, Aliases) {
"running foo\n" "running foo\n"
"foo global-foo 43\n" "foo global-foo 43\n"
"foo local-foo 44\n" "foo local-foo 44\n"
"foo conflict-global 42\n"
"foo conflict 42\n"
"foo arg a\n" "foo arg a\n"
"foo arg b\n", "foo arg b\n",
callHelper({"--global-foo", "43", "bar", "--local-foo", "44", "a", "b"})); callHelper({"--global-foo", "43", "bar", "--local-foo", "44", "a", "b"}));
...@@ -160,5 +170,33 @@ TEST(ProgramOptionsTest, BuiltinCommand) { ...@@ -160,5 +170,33 @@ TEST(ProgramOptionsTest, BuiltinCommand) {
NestedCommandLineApp::kHelpCommand.str() + "nonsense")); NestedCommandLineApp::kHelpCommand.str() + "nonsense"));
} }
TEST(ProgramOptionsTest, ConflictingFlags) {
EXPECT_EQ(
"running foo\n"
"foo global-foo 42\n"
"foo local-foo 42\n"
"foo conflict-global 43\n"
"foo conflict 42\n",
callHelper({"--conflict-global", "43", "foo"}));
EXPECT_EQ(
"running foo\n"
"foo global-foo 42\n"
"foo local-foo 42\n"
"foo conflict-global 43\n"
"foo conflict 42\n",
callHelper({"foo", "--conflict-global", "43"}));
EXPECT_EQ(
"running foo\n"
"foo global-foo 42\n"
"foo local-foo 42\n"
"foo conflict-global 42\n"
"foo conflict 43\n",
callHelper({"foo", "--conflict", "43"}));
callHelper({"--conflict", "43", "foo"}, 1);
}
} // namespace test } // namespace test
} // namespace folly } // namespace folly
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
#include <folly/portability/GFlags.h> #include <folly/portability/GFlags.h>
DEFINE_int32(global_foo, 42, "Global foo"); DEFINE_int32(global_foo, 42, "Global foo");
DEFINE_int32(conflict_global, 42, "Global conflict");
namespace po = ::boost::program_options; namespace po = ::boost::program_options;
...@@ -34,6 +35,8 @@ void foo( ...@@ -34,6 +35,8 @@ void foo(
const po::variables_map& options, const std::vector<std::string>& args) { const po::variables_map& options, const std::vector<std::string>& args) {
printf("foo global-foo %d\n", options["global-foo"].as<int32_t>()); printf("foo global-foo %d\n", options["global-foo"].as<int32_t>());
printf("foo local-foo %d\n", options["local-foo"].as<int32_t>()); printf("foo local-foo %d\n", options["local-foo"].as<int32_t>());
printf("foo conflict-global %d\n", options["conflict-global"].as<int32_t>());
printf("foo conflict %d\n", options["conflict"].as<int32_t>());
for (auto& arg : args) { for (auto& arg : args) {
printf("foo arg %s\n", arg.c_str()); printf("foo arg %s\n", arg.c_str());
} }
...@@ -43,11 +46,17 @@ void foo( ...@@ -43,11 +46,17 @@ void foo(
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
folly::NestedCommandLineApp app("", "0.1", "", "", init); folly::NestedCommandLineApp app("", "0.1", "", "", init);
int style = po::command_line_style::default_style;
style &= ~po::command_line_style::allow_guessing;
app.setOptionStyle(static_cast<po::command_line_style::style_t>(style));
app.addGFlags(); app.addGFlags();
// clang-format off // clang-format off
app.addCommand("foo", "[args...]", "Do some foo", "Does foo", foo) app.addCommand("foo", "[args...]", "Do some foo", "Does foo", foo)
.add_options() .add_options()
("local-foo", po::value<int32_t>()->default_value(42), "Local foo"); ("local-foo", po::value<int32_t>()->default_value(42), "Local foo")
("conflict", po::value<int32_t>()->default_value(42), "Local conflict");
// clang-format on // clang-format on
app.addAlias("bar", "foo"); app.addAlias("bar", "foo");
app.addAlias("baz", "bar"); app.addAlias("baz", "bar");
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment