Commit 5532f19f authored by Tudor Bosman's avatar Tudor Bosman Committed by facebook-github-bot-1

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
parent 838cf146
...@@ -111,6 +111,7 @@ nobase_follyinclude_HEADERS = \ ...@@ -111,6 +111,7 @@ nobase_follyinclude_HEADERS = \
experimental/io/FsUtil.h \ experimental/io/FsUtil.h \
experimental/JSONSchema.h \ experimental/JSONSchema.h \
experimental/LockFreeRingBuffer.h \ experimental/LockFreeRingBuffer.h \
experimental/NestedCommandLineApp.h \
experimental/ProgramOptions.h \ experimental/ProgramOptions.h \
experimental/Select64.h \ experimental/Select64.h \
experimental/StringKeyedCommon.h \ experimental/StringKeyedCommon.h \
...@@ -372,6 +373,7 @@ libfolly_la_SOURCES = \ ...@@ -372,6 +373,7 @@ libfolly_la_SOURCES = \
experimental/FunctionScheduler.cpp \ experimental/FunctionScheduler.cpp \
experimental/io/FsUtil.cpp \ experimental/io/FsUtil.cpp \
experimental/JSONSchema.cpp \ experimental/JSONSchema.cpp \
experimental/NestedCommandLineApp.cpp \
experimental/ProgramOptions.cpp \ experimental/ProgramOptions.cpp \
experimental/Select64.cpp \ experimental/Select64.cpp \
experimental/TestUtil.cpp experimental/TestUtil.cpp
......
/*
* 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
/*
* 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_ */
/*
* 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);
}
/*
* 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
/*
* 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);
}
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