1/*
2 * Copyright 2015-present 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
21#include <folly/FileUtil.h>
22#include <folly/Format.h>
23#include <folly/experimental/io/FsUtil.h>
24
25namespace po = ::boost::program_options;
26
27namespace folly {
28
29namespace {
30
31// Guess the program name as basename(executable)
32std::string guessProgramName() {
33 try {
34 return fs::executable_path().filename().string();
35 } catch (const std::exception&) {
36 return "UNKNOWN";
37 }
38}
39
40} // namespace
41
42ProgramExit::ProgramExit(int status, const std::string& msg)
43 : std::runtime_error(msg), status_(status) {
44 // Message is only allowed for non-zero exit status
45 CHECK(status_ != 0 || msg.empty());
46}
47
48constexpr StringPiece const NestedCommandLineApp::kHelpCommand;
49constexpr StringPiece const NestedCommandLineApp::kVersionCommand;
50
51NestedCommandLineApp::NestedCommandLineApp(
52 std::string programName,
53 std::string version,
54 std::string programHeading,
55 std::string programHelpFooter,
56 InitFunction initFunction)
57 : programName_(std::move(programName)),
58 programHeading_(std::move(programHeading)),
59 programHelpFooter_(std::move(programHelpFooter)),
60 version_(std::move(version)),
61 initFunction_(std::move(initFunction)),
62 globalOptions_("Global options") {
63 addCommand(
64 kHelpCommand.str(),
65 "[command]",
66 "Display help (globally or for a given command)",
67 "Displays help (globally or for a given command).",
68 [this](
69 const po::variables_map& vm, const std::vector<std::string>& args) {
70 displayHelp(vm, args);
71 });
72 builtinCommands_.insert(kHelpCommand);
73
74 addCommand(
75 kVersionCommand.str(),
76 "[command]",
77 "Display version information",
78 "Displays version information.",
79 [this](const po::variables_map&, const std::vector<std::string>&) {
80 displayVersion();
81 });
82 builtinCommands_.insert(kVersionCommand);
83
84 globalOptions_.add_options()(
85 kHelpCommand.str().c_str(),
86 "Display help (globally or for a given command)")(
87 kVersionCommand.str().c_str(), "Display version information");
88}
89
90po::options_description& NestedCommandLineApp::addCommand(
91 std::string name,
92 std::string argStr,
93 std::string shortHelp,
94 std::string fullHelp,
95 Command command) {
96 CommandInfo info{
97 std::move(argStr),
98 std::move(shortHelp),
99 std::move(fullHelp),
100 std::move(command),
101 po::options_description(folly::sformat("Options for `{}'", name))};
102
103 auto p = commands_.emplace(std::move(name), std::move(info));
104 CHECK(p.second) << "Command already exists";
105
106 return p.first->second.options;
107}
108
109void NestedCommandLineApp::addAlias(std::string newName, std::string oldName) {
110 CHECK(aliases_.count(oldName) || commands_.count(oldName))
111 << "Alias old name does not exist";
112 CHECK(!aliases_.count(newName) && !commands_.count(newName))
113 << "Alias new name already exists";
114 aliases_.emplace(std::move(newName), std::move(oldName));
115}
116
117void NestedCommandLineApp::displayHelp(
118 const po::variables_map& /* globalOptions */,
119 const std::vector<std::string>& args) const {
120 if (args.empty()) {
121 // General help
122 printf(
123 "%s\nUsage: %s [global_options...] <command> [command_options...] "
124 "[command_args...]\n\n",
125 programHeading_.c_str(),
126 programName_.c_str());
127 std::cout << globalOptions_;
128 printf("\nAvailable commands:\n");
129
130 size_t maxLen = 0;
131 for (auto& p : commands_) {
132 maxLen = std::max(maxLen, p.first.size());
133 }
134 for (auto& p : aliases_) {
135 maxLen = std::max(maxLen, p.first.size());
136 }
137
138 for (auto& p : commands_) {
139 printf(
140 " %-*s %s\n",
141 int(maxLen),
142 p.first.c_str(),
143 p.second.shortHelp.c_str());
144 }
145
146 if (!aliases_.empty()) {
147 printf("\nAvailable aliases:\n");
148 for (auto& p : aliases_) {
149 printf(
150 " %-*s => %s\n",
151 int(maxLen),
152 p.first.c_str(),
153 resolveAlias(p.second).c_str());
154 }
155 }
156 std::cout << "\n" << programHelpFooter_ << "\n";
157 } else {
158 // Help for a given command
159 auto& p = findCommand(args.front());
160 if (p.first != args.front()) {
161 printf(
162 "`%s' is an alias for `%s'; showing help for `%s'\n",
163 args.front().c_str(),
164 p.first.c_str(),
165 p.first.c_str());
166 }
167 auto& info = p.second;
168
169 printf(
170 "Usage: %s [global_options...] %s%s%s%s\n\n",
171 programName_.c_str(),
172 p.first.c_str(),
173 info.options.options().empty() ? "" : " [command_options...]",
174 info.argStr.empty() ? "" : " ",
175 info.argStr.c_str());
176
177 printf("%s\n", info.fullHelp.c_str());
178
179 std::cout << globalOptions_;
180
181 if (!info.options.options().empty()) {
182 printf("\n");
183 std::cout << info.options;
184 }
185 }
186}
187
188void NestedCommandLineApp::displayVersion() const {
189 printf("%s %s\n", programName_.c_str(), version_.c_str());
190}
191
192const std::string& NestedCommandLineApp::resolveAlias(
193 const std::string& name) const {
194 auto dest = &name;
195 for (;;) {
196 auto pos = aliases_.find(*dest);
197 if (pos == aliases_.end()) {
198 break;
199 }
200 dest = &pos->second;
201 }
202 return *dest;
203}
204
205auto NestedCommandLineApp::findCommand(const std::string& name) const
206 -> const std::pair<const std::string, CommandInfo>& {
207 auto pos = commands_.find(resolveAlias(name));
208 if (pos == commands_.end()) {
209 throw ProgramExit(
210 1,
211 folly::sformat(
212 "Command '{}' not found. Run '{} {}' for help.",
213 name,
214 programName_,
215 kHelpCommand));
216 }
217 return *pos;
218}
219
220int NestedCommandLineApp::run(int argc, const char* const argv[]) {
221 if (programName_.empty()) {
222 programName_ = fs::path(argv[0]).filename().string();
223 }
224 return run(std::vector<std::string>(argv + 1, argv + argc));
225}
226
227int NestedCommandLineApp::run(const std::vector<std::string>& args) {
228 int status;
229 try {
230 doRun(args);
231 status = 0;
232 } catch (const ProgramExit& ex) {
233 if (ex.what()[0]) { // if not empty
234 fprintf(stderr, "%s\n", ex.what());
235 }
236 status = ex.status();
237 } catch (const po::error& ex) {
238 fprintf(
239 stderr,
240 "%s",
241 folly::sformat(
242 "{}. Run '{} help' for {}.\n",
243 ex.what(),
244 programName_,
245 kHelpCommand)
246 .c_str());
247 status = 1;
248 }
249
250 if (status == 0) {
251 if (ferror(stdout)) {
252 fprintf(stderr, "error on standard output\n");
253 status = 1;
254 } else if (fflush(stdout)) {
255 fprintf(
256 stderr,
257 "standard output flush failed: %s\n",
258 errnoStr(errno).c_str());
259 status = 1;
260 }
261 }
262
263 return status;
264}
265
266void NestedCommandLineApp::doRun(const std::vector<std::string>& args) {
267 if (programName_.empty()) {
268 programName_ = guessProgramName();
269 }
270
271 bool not_clean = false;
272 std::vector<std::string> cleanArgs;
273 std::vector<std::string> endArgs;
274
275 for (auto& na : args) {
276 if (na == "--") {
277 not_clean = true;
278 } else if (not_clean) {
279 endArgs.push_back(na);
280 } else {
281 cleanArgs.push_back(na);
282 }
283 }
284
285 auto parsed = parseNestedCommandLine(cleanArgs, globalOptions_);
286 po::variables_map vm;
287 po::store(parsed.options, vm);
288 if (vm.count(kHelpCommand.str())) {
289 std::vector<std::string> helpArgs;
290 if (parsed.command) {
291 helpArgs.push_back(*parsed.command);
292 }
293 displayHelp(vm, helpArgs);
294 return;
295 }
296
297 if (vm.count(kVersionCommand.str())) {
298 displayVersion();
299 return;
300 }
301
302 if (!parsed.command) {
303 throw ProgramExit(
304 1,
305 folly::sformat(
306 "Command not specified. Run '{} {}' for help.",
307 programName_,
308 kHelpCommand));
309 }
310
311 auto& p = findCommand(*parsed.command);
312 auto& cmd = p.first;
313 auto& info = p.second;
314
315 auto cmdOptions =
316 po::command_line_parser(parsed.rest).options(info.options).run();
317
318 po::store(cmdOptions, vm);
319 po::notify(vm);
320
321 auto cmdArgs =
322 po::collect_unrecognized(cmdOptions.options, po::include_positional);
323
324 cmdArgs.insert(cmdArgs.end(), endArgs.begin(), endArgs.end());
325
326 if (initFunction_) {
327 initFunction_(cmd, vm, cmdArgs);
328 }
329
330 info.command(vm, cmdArgs);
331}
332
333bool NestedCommandLineApp::isBuiltinCommand(const std::string& name) const {
334 return builtinCommands_.count(name);
335}
336
337} // namespace folly
338