Helper for writing nested command line apps
[folly.git] / folly / experimental / NestedCommandLineApp.cpp
1 /*
2  * Copyright 2015 Facebook, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 #include <folly/experimental/NestedCommandLineApp.h>
18
19 #include <iostream>
20 #include <folly/FileUtil.h>
21 #include <folly/Format.h>
22 #include <folly/experimental/io/FsUtil.h>
23
24 namespace po = ::boost::program_options;
25
26 namespace folly {
27
28 namespace {
29
30 // Guess the program name as basename(executable)
31 std::string guessProgramName() {
32   try {
33     return fs::executable_path().filename().native();
34   } catch (const std::exception&) {
35     return "UNKNOWN";
36   }
37 }
38
39 }  // namespace
40
41 ProgramExit::ProgramExit(int status, const std::string& msg)
42   : std::runtime_error(msg),
43     status_(status) {
44   CHECK_NE(status, 0);
45 }
46
47 NestedCommandLineApp::NestedCommandLineApp(
48     std::string programName,
49     std::string version,
50     InitFunction initFunction)
51   : programName_(std::move(programName)),
52     version_(std::move(version)),
53     initFunction_(std::move(initFunction)),
54     globalOptions_("Global options") {
55   addCommand("help", "[command]",
56              "Display help (globally or for a given command)",
57              "Displays help (globally or for a given command).",
58              [this] (const po::variables_map& vm,
59                      const std::vector<std::string>& args) {
60                displayHelp(vm, args);
61              });
62
63   globalOptions_.add_options()
64     ("help,h", "Display help (globally or for a given command)")
65     ("version", "Display version information");
66 }
67
68 po::options_description& NestedCommandLineApp::addCommand(
69     std::string name,
70     std::string argStr,
71     std::string shortHelp,
72     std::string fullHelp,
73     Command command) {
74   CommandInfo info {
75     std::move(argStr),
76     std::move(shortHelp),
77     std::move(fullHelp),
78     std::move(command),
79     po::options_description(folly::sformat("Options for `{}'", name))
80   };
81
82   auto p = commands_.emplace(std::move(name), std::move(info));
83   CHECK(p.second) << "Command already exists";
84
85   return p.first->second.options;
86 }
87
88 void NestedCommandLineApp::addAlias(std::string newName,
89                                      std::string oldName) {
90   CHECK(aliases_.count(oldName) || commands_.count(oldName))
91     << "Alias old name does not exist";
92   CHECK(!aliases_.count(newName) && !commands_.count(newName))
93     << "Alias new name already exists";
94   aliases_.emplace(std::move(newName), std::move(oldName));
95 }
96
97 void NestedCommandLineApp::displayHelp(
98     const po::variables_map& globalOptions,
99     const std::vector<std::string>& args) {
100   if (args.empty()) {
101     // General help
102     printf(
103         "Usage: %s [global_options...] <command> [command_options...] "
104         "[command_args...]\n\n", programName_.c_str());
105     std::cout << globalOptions_;
106     printf("\nAvailable commands:\n");
107
108     size_t maxLen = 0;
109     for (auto& p : commands_) {
110       maxLen = std::max(maxLen, p.first.size());
111     }
112     for (auto& p : aliases_) {
113       maxLen = std::max(maxLen, p.first.size());
114     }
115
116     for (auto& p : commands_) {
117       printf("  %-*s    %s\n",
118              int(maxLen), p.first.c_str(), p.second.shortHelp.c_str());
119     }
120
121     if (!aliases_.empty()) {
122       printf("\nAvailable aliases:\n");
123       for (auto& p : aliases_) {
124         printf("  %-*s => %s\n",
125                int(maxLen), p.first.c_str(), resolveAlias(p.second).c_str());
126       }
127     }
128   } else {
129     // Help for a given command
130     auto& p = findCommand(args.front());
131     if (p.first != args.front()) {
132       printf("`%1$s' is an alias for `%2$s'; showing help for `%2$s'\n",
133              args.front().c_str(), p.first.c_str());
134     }
135     auto& info = p.second;
136
137     printf(
138         "Usage: %s [global_options...] %s%s%s%s\n\n",
139         programName_.c_str(),
140         p.first.c_str(),
141         info.options.options().empty() ? "" : " [command_options...]",
142         info.argStr.empty() ? "" : " ",
143         info.argStr.c_str());
144
145     std::cout << globalOptions_;
146
147     if (!info.options.options().empty()) {
148       printf("\n");
149       std::cout << info.options;
150     }
151
152     printf("\n%s\n", info.fullHelp.c_str());
153   }
154 }
155
156 const std::string& NestedCommandLineApp::resolveAlias(
157     const std::string& name) const {
158   auto dest = &name;
159   for (;;) {
160     auto pos = aliases_.find(*dest);
161     if (pos == aliases_.end()) {
162       break;
163     }
164     dest = &pos->second;
165   }
166   return *dest;
167 }
168
169 auto NestedCommandLineApp::findCommand(const std::string& name) const
170   -> const std::pair<const std::string, CommandInfo>& {
171   auto pos = commands_.find(resolveAlias(name));
172   if (pos == commands_.end()) {
173     throw ProgramExit(
174         1,
175         folly::sformat("Command `{}' not found. Run `{} help' for help.",
176                        name, programName_));
177   }
178   return *pos;
179 }
180
181 int NestedCommandLineApp::run(int argc, const char* const argv[]) {
182   if (programName_.empty()) {
183     programName_ = fs::path(argv[0]).filename().native();
184   }
185   return run(std::vector<std::string>(argv + 1, argv + argc));
186 }
187
188 int NestedCommandLineApp::run(const std::vector<std::string>& args) {
189   int status;
190   try {
191     doRun(args);
192     status = 0;
193   } catch (const ProgramExit& ex) {
194     if (ex.what()[0]) {  // if not empty
195       fprintf(stderr, "%s\n", ex.what());
196     }
197     status = ex.status();
198   } catch (const po::error& ex) {
199     fprintf(stderr, "%s. Run `%s help' for help.\n",
200             ex.what(), programName_.c_str());
201     status = 1;
202   }
203
204   if (status == 0) {
205     if (ferror(stdout)) {
206       fprintf(stderr, "error on standard output\n");
207       status = 1;
208     } else if (fflush(stdout)) {
209       fprintf(stderr, "standard output flush failed: %s\n",
210               errnoStr(errno).c_str());
211       status = 1;
212     }
213   }
214
215   return status;
216 }
217
218 void NestedCommandLineApp::doRun(const std::vector<std::string>& args) {
219   if (programName_.empty()) {
220     programName_ = guessProgramName();
221   }
222   auto parsed = parseNestedCommandLine(args, globalOptions_);
223   po::variables_map vm;
224   po::store(parsed.options, vm);
225   if (vm.count("help")) {
226     std::vector<std::string> helpArgs;
227     if (parsed.command) {
228       helpArgs.push_back(*parsed.command);
229     }
230     displayHelp(vm, helpArgs);
231     return;
232   }
233
234   if (vm.count("version")) {
235     printf("%s %s\n", programName_.c_str(), version_.c_str());
236     return;
237   }
238
239   if (!parsed.command) {
240     throw ProgramExit(
241         1,
242         folly::sformat("Command not specified. Run `{} help' for help.",
243                        programName_));
244   }
245
246   auto& p = findCommand(*parsed.command);
247   auto& cmd = p.first;
248   auto& info = p.second;
249
250   auto cmdOptions =
251     po::command_line_parser(parsed.rest).options(info.options).run();
252   po::store(cmdOptions, vm);
253   po::notify(vm);
254
255   auto cmdArgs = po::collect_unrecognized(cmdOptions.options,
256                                           po::include_positional);
257
258   if (initFunction_) {
259     initFunction_(cmd, vm, cmdArgs);
260   }
261
262   info.command(vm, cmdArgs);
263 }
264
265 }  // namespaces