From 5532f19f7b604a9b12c79a1d0a4fe3b0ff74f3d1 Mon Sep 17 00:00:00 2001 From: Tudor Bosman <tudorb@fb.com> Date: Thu, 23 Jul 2015 07:30:52 -0700 Subject: [PATCH] Helper for writing nested command line apps Summary: Many command line apps are of the form "program [--global_options] command [--command_options] args..." Make writing such things less painful. +jdelong because smcc Reviewed By: @meyering Differential Revision: D2217248 --- folly/Makefile.am | 2 + folly/experimental/NestedCommandLineApp.cpp | 265 ++++++++++++++++++ folly/experimental/NestedCommandLineApp.h | 150 ++++++++++ .../test/NestedCommandLineAppExample.cpp | 213 ++++++++++++++ .../test/NestedCommandLineAppTest.cpp | 126 +++++++++ .../test/NestedCommandLineAppTestHelper.cpp | 52 ++++ 6 files changed, 808 insertions(+) create mode 100644 folly/experimental/NestedCommandLineApp.cpp create mode 100644 folly/experimental/NestedCommandLineApp.h create mode 100644 folly/experimental/test/NestedCommandLineAppExample.cpp create mode 100644 folly/experimental/test/NestedCommandLineAppTest.cpp create mode 100644 folly/experimental/test/NestedCommandLineAppTestHelper.cpp diff --git a/folly/Makefile.am b/folly/Makefile.am index 1250f4b7..6509a3a9 100644 --- a/folly/Makefile.am +++ b/folly/Makefile.am @@ -111,6 +111,7 @@ nobase_follyinclude_HEADERS = \ experimental/io/FsUtil.h \ experimental/JSONSchema.h \ experimental/LockFreeRingBuffer.h \ + experimental/NestedCommandLineApp.h \ experimental/ProgramOptions.h \ experimental/Select64.h \ experimental/StringKeyedCommon.h \ @@ -372,6 +373,7 @@ libfolly_la_SOURCES = \ experimental/FunctionScheduler.cpp \ experimental/io/FsUtil.cpp \ experimental/JSONSchema.cpp \ + experimental/NestedCommandLineApp.cpp \ experimental/ProgramOptions.cpp \ experimental/Select64.cpp \ experimental/TestUtil.cpp diff --git a/folly/experimental/NestedCommandLineApp.cpp b/folly/experimental/NestedCommandLineApp.cpp new file mode 100644 index 00000000..b468f515 --- /dev/null +++ b/folly/experimental/NestedCommandLineApp.cpp @@ -0,0 +1,265 @@ +/* + * Copyright 2015 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <folly/experimental/NestedCommandLineApp.h> + +#include <iostream> +#include <folly/FileUtil.h> +#include <folly/Format.h> +#include <folly/experimental/io/FsUtil.h> + +namespace po = ::boost::program_options; + +namespace folly { + +namespace { + +// Guess the program name as basename(executable) +std::string guessProgramName() { + try { + return fs::executable_path().filename().native(); + } catch (const std::exception&) { + return "UNKNOWN"; + } +} + +} // namespace + +ProgramExit::ProgramExit(int status, const std::string& msg) + : std::runtime_error(msg), + status_(status) { + CHECK_NE(status, 0); +} + +NestedCommandLineApp::NestedCommandLineApp( + std::string programName, + std::string version, + InitFunction initFunction) + : programName_(std::move(programName)), + version_(std::move(version)), + initFunction_(std::move(initFunction)), + globalOptions_("Global options") { + addCommand("help", "[command]", + "Display help (globally or for a given command)", + "Displays help (globally or for a given command).", + [this] (const po::variables_map& vm, + const std::vector<std::string>& args) { + displayHelp(vm, args); + }); + + globalOptions_.add_options() + ("help,h", "Display help (globally or for a given command)") + ("version", "Display version information"); +} + +po::options_description& NestedCommandLineApp::addCommand( + std::string name, + std::string argStr, + std::string shortHelp, + std::string fullHelp, + Command command) { + CommandInfo info { + std::move(argStr), + std::move(shortHelp), + std::move(fullHelp), + std::move(command), + po::options_description(folly::sformat("Options for `{}'", name)) + }; + + auto p = commands_.emplace(std::move(name), std::move(info)); + CHECK(p.second) << "Command already exists"; + + return p.first->second.options; +} + +void NestedCommandLineApp::addAlias(std::string newName, + std::string oldName) { + CHECK(aliases_.count(oldName) || commands_.count(oldName)) + << "Alias old name does not exist"; + CHECK(!aliases_.count(newName) && !commands_.count(newName)) + << "Alias new name already exists"; + aliases_.emplace(std::move(newName), std::move(oldName)); +} + +void NestedCommandLineApp::displayHelp( + const po::variables_map& globalOptions, + const std::vector<std::string>& args) { + if (args.empty()) { + // General help + printf( + "Usage: %s [global_options...] <command> [command_options...] " + "[command_args...]\n\n", programName_.c_str()); + std::cout << globalOptions_; + printf("\nAvailable commands:\n"); + + size_t maxLen = 0; + for (auto& p : commands_) { + maxLen = std::max(maxLen, p.first.size()); + } + for (auto& p : aliases_) { + maxLen = std::max(maxLen, p.first.size()); + } + + for (auto& p : commands_) { + printf(" %-*s %s\n", + int(maxLen), p.first.c_str(), p.second.shortHelp.c_str()); + } + + if (!aliases_.empty()) { + printf("\nAvailable aliases:\n"); + for (auto& p : aliases_) { + printf(" %-*s => %s\n", + int(maxLen), p.first.c_str(), resolveAlias(p.second).c_str()); + } + } + } else { + // Help for a given command + auto& p = findCommand(args.front()); + if (p.first != args.front()) { + printf("`%1$s' is an alias for `%2$s'; showing help for `%2$s'\n", + args.front().c_str(), p.first.c_str()); + } + auto& info = p.second; + + printf( + "Usage: %s [global_options...] %s%s%s%s\n\n", + programName_.c_str(), + p.first.c_str(), + info.options.options().empty() ? "" : " [command_options...]", + info.argStr.empty() ? "" : " ", + info.argStr.c_str()); + + std::cout << globalOptions_; + + if (!info.options.options().empty()) { + printf("\n"); + std::cout << info.options; + } + + printf("\n%s\n", info.fullHelp.c_str()); + } +} + +const std::string& NestedCommandLineApp::resolveAlias( + const std::string& name) const { + auto dest = &name; + for (;;) { + auto pos = aliases_.find(*dest); + if (pos == aliases_.end()) { + break; + } + dest = &pos->second; + } + return *dest; +} + +auto NestedCommandLineApp::findCommand(const std::string& name) const + -> const std::pair<const std::string, CommandInfo>& { + auto pos = commands_.find(resolveAlias(name)); + if (pos == commands_.end()) { + throw ProgramExit( + 1, + folly::sformat("Command `{}' not found. Run `{} help' for help.", + name, programName_)); + } + return *pos; +} + +int NestedCommandLineApp::run(int argc, const char* const argv[]) { + if (programName_.empty()) { + programName_ = fs::path(argv[0]).filename().native(); + } + return run(std::vector<std::string>(argv + 1, argv + argc)); +} + +int NestedCommandLineApp::run(const std::vector<std::string>& args) { + int status; + try { + doRun(args); + status = 0; + } catch (const ProgramExit& ex) { + if (ex.what()[0]) { // if not empty + fprintf(stderr, "%s\n", ex.what()); + } + status = ex.status(); + } catch (const po::error& ex) { + fprintf(stderr, "%s. Run `%s help' for help.\n", + ex.what(), programName_.c_str()); + status = 1; + } + + if (status == 0) { + if (ferror(stdout)) { + fprintf(stderr, "error on standard output\n"); + status = 1; + } else if (fflush(stdout)) { + fprintf(stderr, "standard output flush failed: %s\n", + errnoStr(errno).c_str()); + status = 1; + } + } + + return status; +} + +void NestedCommandLineApp::doRun(const std::vector<std::string>& args) { + if (programName_.empty()) { + programName_ = guessProgramName(); + } + auto parsed = parseNestedCommandLine(args, globalOptions_); + po::variables_map vm; + po::store(parsed.options, vm); + if (vm.count("help")) { + std::vector<std::string> helpArgs; + if (parsed.command) { + helpArgs.push_back(*parsed.command); + } + displayHelp(vm, helpArgs); + return; + } + + if (vm.count("version")) { + printf("%s %s\n", programName_.c_str(), version_.c_str()); + return; + } + + if (!parsed.command) { + throw ProgramExit( + 1, + folly::sformat("Command not specified. Run `{} help' for help.", + programName_)); + } + + auto& p = findCommand(*parsed.command); + auto& cmd = p.first; + auto& info = p.second; + + auto cmdOptions = + po::command_line_parser(parsed.rest).options(info.options).run(); + po::store(cmdOptions, vm); + po::notify(vm); + + auto cmdArgs = po::collect_unrecognized(cmdOptions.options, + po::include_positional); + + if (initFunction_) { + initFunction_(cmd, vm, cmdArgs); + } + + info.command(vm, cmdArgs); +} + +} // namespaces diff --git a/folly/experimental/NestedCommandLineApp.h b/folly/experimental/NestedCommandLineApp.h new file mode 100644 index 00000000..aa9d061f --- /dev/null +++ b/folly/experimental/NestedCommandLineApp.h @@ -0,0 +1,150 @@ +/* + * Copyright 2015 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FOLLY_NESTEDCOMMANDLINEAPP_H_ +#define FOLLY_NESTEDCOMMANDLINEAPP_H_ + +#include <functional> +#include <stdexcept> + +#include <folly/experimental/ProgramOptions.h> + +namespace folly { + +/** + * Exception that commands may throw to force the program to exit cleanly + * with a non-zero exit code. NestedCommandLineApp::run() catches this and + * makes run() print the given message on stderr (followed by a newline, unless + * empty), and return the exit code. (Other exceptions will propagate out of + * run()) + */ +class ProgramExit : public std::runtime_error { + public: + explicit ProgramExit(int status, const std::string& msg = std::string()); + int status() const { return status_; } + private: + int status_; +}; + +/** + * App that uses a nested command line, of the form: + * + * program [--global_options...] command [--command_options...] command_args... + */ +class NestedCommandLineApp { + public: + typedef std::function<void( + const std::string& command, + const boost::program_options::variables_map& options, + const std::vector<std::string>& args)> InitFunction; + + typedef std::function<void( + const boost::program_options::variables_map& options, + const std::vector<std::string>&)> Command; + + /** + * Initialize the app. + * + * If programName is not set, we try to guess (readlink("/proc/self/exe")). + * + * version is the version string printed when given the --version flag. + * + * initFunction, if specified, is called after parsing the command line, + * right before executing the command. + */ + explicit NestedCommandLineApp( + std::string programName = std::string(), + std::string version = std::string(), + InitFunction initFunction = InitFunction()); + + /** + * Add GFlags to the list of supported options with the given style. + */ + void addGFlags(ProgramOptionsStyle style = ProgramOptionsStyle::GNU) { + globalOptions_.add(getGFlags(style)); + } + + /** + * Return the global options object, so you can add options. + */ + boost::program_options::options_description& globalOptions() { + return globalOptions_; + } + + /** + * Add a command. + * + * name: command name + * argStr: description of arguments in help strings + * (<filename> <N>) + * shortHelp: one-line summary help string + * fullHelp: full help string + * command: function to run + * + * Returns a reference to the options_description object that you can + * use to add options for this command. + */ + boost::program_options::options_description& addCommand( + std::string name, + std::string argStr, + std::string shortHelp, + std::string fullHelp, + Command command); + + /** + * Add an alias; running the command newName will have the same effect + * as running oldName. + */ + void addAlias(std::string newName, std::string oldName); + + /** + * 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(): + * return app.run(argc, argv); + */ + int run(int argc, const char* const argv[]); + int run(const std::vector<std::string>& args); + + private: + void doRun(const std::vector<std::string>& args); + const std::string& resolveAlias(const std::string& name) const; + + struct CommandInfo { + std::string argStr; + std::string shortHelp; + std::string fullHelp; + Command command; + boost::program_options::options_description options; + }; + + const std::pair<const std::string, CommandInfo>& + findCommand(const std::string& name) const; + + void displayHelp( + const boost::program_options::variables_map& options, + const std::vector<std::string>& args); + + std::string programName_; + std::string version_; + InitFunction initFunction_; + boost::program_options::options_description globalOptions_; + std::map<std::string, CommandInfo> commands_; + std::map<std::string, std::string> aliases_; +}; + +} // namespaces + +#endif /* FOLLY_NESTEDCOMMANDLINEAPP_H_ */ diff --git a/folly/experimental/test/NestedCommandLineAppExample.cpp b/folly/experimental/test/NestedCommandLineAppExample.cpp new file mode 100644 index 00000000..7d14ae3a --- /dev/null +++ b/folly/experimental/test/NestedCommandLineAppExample.cpp @@ -0,0 +1,213 @@ +/* + * Copyright 2015 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Example application using the nested command line parser. +// +// Implements two commands: "cat" and "echo", which behave similarly to their +// Unix homonyms. + +#include <folly/String.h> +#include <folly/ScopeGuard.h> +#include <folly/experimental/NestedCommandLineApp.h> +#include <folly/experimental/ProgramOptions.h> + +namespace po = ::boost::program_options; + +namespace { + +class InputError : public std::runtime_error { + public: + explicit InputError(const std::string& msg) + : std::runtime_error(msg) { } +}; + +class OutputError : public std::runtime_error { + public: + explicit OutputError(const std::string& msg) + : std::runtime_error(msg) { } +}; + +class Concatenator { + public: + explicit Concatenator(const po::variables_map& options) + : printLineNumbers_(options["number"].as<bool>()) { } + + void cat(const std::string& name); + void cat(FILE* file); + + bool printLineNumbers() const { return printLineNumbers_; } + + private: + bool printLineNumbers_; + size_t lineNumber_ = 0; +}; + +FOLLY_NORETURN void throwOutputError() { + throw OutputError(folly::errnoStr(errno).toStdString()); +} + +FOLLY_NORETURN void throwInputError() { + throw InputError(folly::errnoStr(errno).toStdString()); +} + +void Concatenator::cat(FILE* file) { + char* lineBuf = nullptr; + size_t lineBufSize = 0; + SCOPE_EXIT { + free(lineBuf); + }; + + ssize_t n; + while ((n = getline(&lineBuf, &lineBufSize, file)) >= 0) { + ++lineNumber_; + if ((printLineNumbers_ && printf("%6zu ", lineNumber_) < 0) || + fwrite(lineBuf, 1, n, stdout) < size_t(n)) { + throwOutputError(); + } + } + + if (ferror(file)) { + throwInputError(); + } +} + +void Concatenator::cat(const std::string& name) { + auto file = fopen(name.c_str(), "r"); + if (!file) { + throwInputError(); + } + + // Ignore error, as we might be processing an exception; + // during normal operation, we call fclose() directly further below + auto guard = folly::makeGuard([file] { fclose(file); }); + + cat(file); + + guard.dismiss(); + if (fclose(file)) { + throwInputError(); + } +} + +void runCat(const po::variables_map& options, + const std::vector<std::string>& args) { + Concatenator concatenator(options); + bool ok = true; + auto catFile = [&concatenator, &ok] (const std::string& name) { + try { + if (name == "-") { + concatenator.cat(stdin); + } else { + concatenator.cat(name); + } + } catch (const InputError& e) { + ok = false; + fprintf(stderr, "cat: %s: %s\n", name.c_str(), e.what()); + } + }; + + try { + if (args.empty()) { + catFile("-"); + } else { + for (auto& name : args) { + catFile(name); + } + } + } catch (const OutputError& e) { + throw folly::ProgramExit( + 1, folly::to<std::string>("cat: write error: ", e.what())); + } + if (!ok) { + throw folly::ProgramExit(1); + } +} + +void runEcho(const po::variables_map& options, + const std::vector<std::string>& args) { + try { + const char* sep = ""; + for (auto& arg : args) { + if (printf("%s%s", sep, arg.c_str()) < 0) { + throw OutputError(folly::errnoStr(errno).toStdString()); + } + sep = " "; + } + if (!options["-n"].as<bool>()) { + if (putchar('\n') == EOF) { + throw OutputError(folly::errnoStr(errno).toStdString()); + } + } + } catch (const OutputError& e) { + throw folly::ProgramExit( + 1, folly::to<std::string>("echo: write error: ", e.what())); + } +} + +} // namespace + +int main(int argc, char *argv[]) { + // Initialize a NestedCommandLineApp object. + // + // The first argument is the program name -- an empty string will cause the + // program name to be deduced from the executable name, which is usually + // fine. The second argument is a version string. + // + // You may also add an "initialization function" that is always called + // for every valid command before the command is executed. + folly::NestedCommandLineApp app("", "0.1"); + + // Add any GFlags-defined flags. These are global flags, and so they should + // be valid for any command. + app.addGFlags(); + + // Add any commands. For our example, we'll implement simplified versions + // of "cat" and "echo". Note that addCommand() returns a reference to a + // boost::program_options object that you may use to add command-specific + // options. + app.addCommand( + // command name + "cat", + + // argument description + "[file...]", + + // short help string + "Concatenate files and print them on standard output", + + // Long help string + "Concatenate files and print them on standard output.", + + // Function to execute + runCat) + .add_options() + ("number,n", po::bool_switch(), "number all output lines"); + + app.addCommand( + "echo", + "[string...]", + "Display a line of text", + "Display a line of text.", + runEcho) + .add_options() + (",n", po::bool_switch(), "do not output the trailing newline"); + + // You may also add command aliases -- that is, multiple command names + // that do the same thing; see addAlias(). + + // app.run returns an appropriate error code + return app.run(argc, argv); +} diff --git a/folly/experimental/test/NestedCommandLineAppTest.cpp b/folly/experimental/test/NestedCommandLineAppTest.cpp new file mode 100644 index 00000000..cdfcd318 --- /dev/null +++ b/folly/experimental/test/NestedCommandLineAppTest.cpp @@ -0,0 +1,126 @@ +/* + * Copyright 2015 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <folly/experimental/NestedCommandLineApp.h> + +#include <folly/Subprocess.h> +#include <folly/experimental/io/FsUtil.h> +#include <glog/logging.h> +#include <gtest/gtest.h> + +namespace folly { namespace test { + +namespace { + +std::string getHelperPath() { + auto path = fs::executable_path(); + path.remove_filename() /= "nested_command_line_app_test_helper"; + return path.native(); +} + +std::string callHelper(std::initializer_list<std::string> args, + int expectedExitCode = 0, + int stdoutFd = -1) { + static std::string helperPath = getHelperPath(); + + std::vector<std::string> allArgs; + allArgs.reserve(args.size() + 1); + allArgs.push_back(helperPath); + allArgs.insert(allArgs.end(), args.begin(), args.end()); + + Subprocess::Options options; + if (stdoutFd != -1) { + options.stdout(stdoutFd); + } else { + options.pipeStdout(); + } + options.pipeStderr(); + + Subprocess proc(allArgs, options); + auto p = proc.communicate(); + EXPECT_EQ(expectedExitCode, proc.wait().exitStatus()); + + return p.first; +} + +} // namespace + +TEST(ProgramOptionsTest, Errors) { + callHelper({}, 1); + callHelper({"--wtf", "foo"}, 1); + callHelper({"qux"}, 1); + callHelper({"--global-foo", "x", "foo"}, 1); +} + +TEST(ProgramOptionsTest, Help) { + // Not actually checking help output, just verifying that help doesn't fail + callHelper({"--version"}); + callHelper({"--help"}); + callHelper({"--help", "foo"}); + callHelper({"--help", "bar"}); + callHelper({"help"}); + callHelper({"help", "foo"}); + callHelper({"help", "bar"}); + + // wrong command name + callHelper({"--help", "qux"}, 1); + callHelper({"help", "qux"}, 1); +} + +TEST(ProgramOptionsTest, DevFull) { + folly::File full("/dev/full", O_RDWR); + callHelper({"--help"}, 1, full.fd()); +} + +TEST(ProgramOptionsTest, Success) { + EXPECT_EQ( + "running foo\n" + "foo global-foo 42\n" + "foo local-foo 42\n", + callHelper({"foo"})); + + EXPECT_EQ( + "running foo\n" + "foo global-foo 43\n" + "foo local-foo 44\n" + "foo arg a\n" + "foo arg b\n", + callHelper({"--global-foo", "43", "foo", "--local-foo", "44", + "a", "b"})); + + // Check that global flags can still be given after the command + EXPECT_EQ( + "running foo\n" + "foo global-foo 43\n" + "foo local-foo 44\n" + "foo arg a\n" + "foo arg b\n", + callHelper({"foo", "--global-foo", "43", "--local-foo", "44", + "a", "b"})); +} + +TEST(ProgramOptionsTest, Aliases) { + EXPECT_EQ( + "running foo\n" + "foo global-foo 43\n" + "foo local-foo 44\n" + "foo arg a\n" + "foo arg b\n", + callHelper({"--global-foo", "43", "bar", "--local-foo", "44", + "a", "b"})); +} + +}} // namespaces diff --git a/folly/experimental/test/NestedCommandLineAppTestHelper.cpp b/folly/experimental/test/NestedCommandLineAppTestHelper.cpp new file mode 100644 index 00000000..08caecf2 --- /dev/null +++ b/folly/experimental/test/NestedCommandLineAppTestHelper.cpp @@ -0,0 +1,52 @@ +/* + * Copyright 2015 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <folly/experimental/NestedCommandLineApp.h> +#include <gflags/gflags.h> + +DEFINE_int32(global_foo, 42, "Global foo"); + +namespace po = ::boost::program_options; + +namespace { + +void init(const std::string& cmd, + const po::variables_map& options, + const std::vector<std::string>& args) { + printf("running %s\n", cmd.c_str()); +} + +void foo(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 local-foo %d\n", options["local-foo"].as<int32_t>()); + for (auto& arg : args) { + printf("foo arg %s\n", arg.c_str()); + } +} + +} // namespace + +int main(int argc, char *argv[]) { + folly::NestedCommandLineApp app("", "0.1", init); + app.addGFlags(); + app.addCommand("foo", "[args...]", "Do some foo", "Does foo", foo) + .add_options() + ("local-foo", po::value<int32_t>()->default_value(42), "Local foo"); + app.addAlias("bar", "foo"); + app.addAlias("baz", "bar"); + return app.run(argc, argv); +} -- 2.34.1