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