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), status_(status) { |
44 | // Message is only allowed for non-zero exit status |
45 | CHECK(status_ != 0 || msg.empty()); |
46 | } |
47 | |
48 | constexpr StringPiece const NestedCommandLineApp::kHelpCommand; |
49 | constexpr StringPiece const NestedCommandLineApp::kVersionCommand; |
50 | |
51 | NestedCommandLineApp::NestedCommandLineApp( |
52 | std::string programName, |
53 | std::string version, |
54 | std::string programHeading, |
55 | std::string , |
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 | |
90 | po::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 | |
109 | void 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 | |
117 | void 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 | |
188 | void NestedCommandLineApp::displayVersion() const { |
189 | printf("%s %s\n" , programName_.c_str(), version_.c_str()); |
190 | } |
191 | |
192 | const 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 | |
205 | auto 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 | |
220 | int 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 | |
227 | int 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 | |
266 | void 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 | |
333 | bool NestedCommandLineApp::isBuiltinCommand(const std::string& name) const { |
334 | return builtinCommands_.count(name); |
335 | } |
336 | |
337 | } // namespace folly |
338 | |