a50374a9489366002435f7f5b82456897dd3d0be
[folly.git] / folly / experimental / NestedCommandLineApp.cpp
1 /*
2  * Copyright 2017 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().string();
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   // Message is only allowed for non-zero exit status
45   CHECK(status_ != 0 || msg.empty());
46 }
47
48 NestedCommandLineApp::NestedCommandLineApp(
49     std::string programName,
50     std::string version,
51     InitFunction initFunction)
52   : programName_(std::move(programName)),
53     version_(std::move(version)),
54     initFunction_(std::move(initFunction)),
55     globalOptions_("Global options") {
56   addCommand("help", "[command]",
57              "Display help (globally or for a given command)",
58              "Displays help (globally or for a given command).",
59              [this] (const po::variables_map& vm,
60                      const std::vector<std::string>& args) {
61                displayHelp(vm, args);
62              });
63
64   globalOptions_.add_options()
65     ("help,h", "Display help (globally or for a given command)")
66     ("version", "Display version information");
67 }
68
69 po::options_description& NestedCommandLineApp::addCommand(
70     std::string name,
71     std::string argStr,
72     std::string shortHelp,
73     std::string fullHelp,
74     Command command) {
75   CommandInfo info {
76     std::move(argStr),
77     std::move(shortHelp),
78     std::move(fullHelp),
79     std::move(command),
80     po::options_description(folly::sformat("Options for `{}'", name))
81   };
82
83   auto p = commands_.emplace(std::move(name), std::move(info));
84   CHECK(p.second) << "Command already exists";
85
86   return p.first->second.options;
87 }
88
89 void NestedCommandLineApp::addAlias(std::string newName,
90                                      std::string oldName) {
91   CHECK(aliases_.count(oldName) || commands_.count(oldName))
92     << "Alias old name does not exist";
93   CHECK(!aliases_.count(newName) && !commands_.count(newName))
94     << "Alias new name already exists";
95   aliases_.emplace(std::move(newName), std::move(oldName));
96 }
97
98 void NestedCommandLineApp::displayHelp(
99     const po::variables_map& /* globalOptions */,
100     const std::vector<std::string>& args) {
101   if (args.empty()) {
102     // General help
103     printf(
104         "Usage: %s [global_options...] <command> [command_options...] "
105         "[command_args...]\n\n", programName_.c_str());
106     std::cout << globalOptions_;
107     printf("\nAvailable commands:\n");
108
109     size_t maxLen = 0;
110     for (auto& p : commands_) {
111       maxLen = std::max(maxLen, p.first.size());
112     }
113     for (auto& p : aliases_) {
114       maxLen = std::max(maxLen, p.first.size());
115     }
116
117     for (auto& p : commands_) {
118       printf("  %-*s    %s\n",
119              int(maxLen), p.first.c_str(), p.second.shortHelp.c_str());
120     }
121
122     if (!aliases_.empty()) {
123       printf("\nAvailable aliases:\n");
124       for (auto& p : aliases_) {
125         printf("  %-*s => %s\n",
126                int(maxLen), p.first.c_str(), resolveAlias(p.second).c_str());
127       }
128     }
129   } else {
130     // Help for a given command
131     auto& p = findCommand(args.front());
132     if (p.first != args.front()) {
133       printf("`%s' is an alias for `%s'; showing help for `%s'\n",
134              args.front().c_str(), p.first.c_str(), p.first.c_str());
135     }
136     auto& info = p.second;
137
138     printf(
139         "Usage: %s [global_options...] %s%s%s%s\n\n",
140         programName_.c_str(),
141         p.first.c_str(),
142         info.options.options().empty() ? "" : " [command_options...]",
143         info.argStr.empty() ? "" : " ",
144         info.argStr.c_str());
145
146     std::cout << globalOptions_;
147
148     if (!info.options.options().empty()) {
149       printf("\n");
150       std::cout << info.options;
151     }
152
153     printf("\n%s\n", info.fullHelp.c_str());
154   }
155 }
156
157 const std::string& NestedCommandLineApp::resolveAlias(
158     const std::string& name) const {
159   auto dest = &name;
160   for (;;) {
161     auto pos = aliases_.find(*dest);
162     if (pos == aliases_.end()) {
163       break;
164     }
165     dest = &pos->second;
166   }
167   return *dest;
168 }
169
170 auto NestedCommandLineApp::findCommand(const std::string& name) const
171   -> const std::pair<const std::string, CommandInfo>& {
172   auto pos = commands_.find(resolveAlias(name));
173   if (pos == commands_.end()) {
174     throw ProgramExit(
175         1,
176         folly::sformat("Command `{}' not found. Run `{} help' for help.",
177                        name, programName_));
178   }
179   return *pos;
180 }
181
182 int NestedCommandLineApp::run(int argc, const char* const argv[]) {
183   if (programName_.empty()) {
184     programName_ = fs::path(argv[0]).filename().string();
185   }
186   return run(std::vector<std::string>(argv + 1, argv + argc));
187 }
188
189 int NestedCommandLineApp::run(const std::vector<std::string>& args) {
190   int status;
191   try {
192     doRun(args);
193     status = 0;
194   } catch (const ProgramExit& ex) {
195     if (ex.what()[0]) {  // if not empty
196       fprintf(stderr, "%s\n", ex.what());
197     }
198     status = ex.status();
199   } catch (const po::error& ex) {
200     fprintf(stderr, "%s. Run `%s help' for help.\n",
201             ex.what(), programName_.c_str());
202     status = 1;
203   }
204
205   if (status == 0) {
206     if (ferror(stdout)) {
207       fprintf(stderr, "error on standard output\n");
208       status = 1;
209     } else if (fflush(stdout)) {
210       fprintf(stderr, "standard output flush failed: %s\n",
211               errnoStr(errno).c_str());
212       status = 1;
213     }
214   }
215
216   return status;
217 }
218
219 void NestedCommandLineApp::doRun(const std::vector<std::string>& args) {
220   if (programName_.empty()) {
221     programName_ = guessProgramName();
222   }
223   auto parsed = parseNestedCommandLine(args, globalOptions_);
224   po::variables_map vm;
225   po::store(parsed.options, vm);
226   if (vm.count("help")) {
227     std::vector<std::string> helpArgs;
228     if (parsed.command) {
229       helpArgs.push_back(*parsed.command);
230     }
231     displayHelp(vm, helpArgs);
232     return;
233   }
234
235   if (vm.count("version")) {
236     printf("%s %s\n", programName_.c_str(), version_.c_str());
237     return;
238   }
239
240   if (!parsed.command) {
241     throw ProgramExit(
242         1,
243         folly::sformat("Command not specified. Run `{} help' for help.",
244                        programName_));
245   }
246
247   auto& p = findCommand(*parsed.command);
248   auto& cmd = p.first;
249   auto& info = p.second;
250
251   auto cmdOptions =
252     po::command_line_parser(parsed.rest).options(info.options).run();
253   po::store(cmdOptions, vm);
254   po::notify(vm);
255
256   auto cmdArgs = po::collect_unrecognized(cmdOptions.options,
257                                           po::include_positional);
258
259   if (initFunction_) {
260     initFunction_(cmd, vm, cmdArgs);
261   }
262
263   info.command(vm, cmdArgs);
264 }
265
266 }  // namespaces