Helper for writing nested command line apps
authorTudor Bosman <tudorb@fb.com>
Thu, 23 Jul 2015 14:30:52 +0000 (07:30 -0700)
committerfacebook-github-bot-1 <folly-bot@fb.com>
Thu, 23 Jul 2015 15:22:17 +0000 (08:22 -0700)
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
folly/experimental/NestedCommandLineApp.cpp [new file with mode: 0644]
folly/experimental/NestedCommandLineApp.h [new file with mode: 0644]
folly/experimental/test/NestedCommandLineAppExample.cpp [new file with mode: 0644]
folly/experimental/test/NestedCommandLineAppTest.cpp [new file with mode: 0644]
folly/experimental/test/NestedCommandLineAppTestHelper.cpp [new file with mode: 0644]

index 1250f4b715967edd7e9ea12f4ccb4308f7c65bca..6509a3a91ca8eecc2d5dfe17ec5eb1e185eea5d2 100644 (file)
@@ -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 (file)
index 0000000..b468f51
--- /dev/null
@@ -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 (file)
index 0000000..aa9d061
--- /dev/null
@@ -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 (file)
index 0000000..7d14ae3
--- /dev/null
@@ -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 (file)
index 0000000..cdfcd31
--- /dev/null
@@ -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 (file)
index 0000000..08caecf
--- /dev/null
@@ -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);
+}