// Copyright (c) 2008, Google Inc. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // // --- // Author: Dave Nicponski // // Bash-style command line flag completion for C++ binaries // // This module implements bash-style completions. It achieves this // goal in the following broad chunks: // // 1) Take a to-be-completed word, and examine it for search hints // 2) Identify all potentially matching flags // 2a) If there are no matching flags, do nothing. // 2b) If all matching flags share a common prefix longer than the // completion word, output just that matching prefix // 3) Categorize those flags to produce a rough ordering of relevence. // 4) Potentially trim the set of flags returned to a smaller number // that bash is happier with // 5) Output the matching flags in groups ordered by relevence. // 5a) Force bash to place most-relevent groups at the top of the list // 5b) Trim most flag's descriptions to fit on a single terminal line #include "config.h" #include <stdio.h> #include <stdlib.h> #include <string.h> // for strlen #include <set> #include <string> #include <utility> #include <vector> #include <gflags/gflags.h> #ifndef PATH_SEPARATOR #define PATH_SEPARATOR '/' #endif DEFINE_string(tab_completion_word, "", "If non-empty, HandleCommandLineCompletions() will hijack the " "process and attempt to do bash-style command line flag " "completion on this value."); DEFINE_int32(tab_completion_columns, 80, "Number of columns to use in output for tab completion"); _START_GOOGLE_NAMESPACE_ namespace { using std::set; using std::string; using std::vector; // Function prototypes and Type forward declarations. Code may be // more easily understood if it is roughly ordered according to // control flow, rather than by C's "declare before use" ordering struct CompletionOptions; struct NotableFlags; // The entry point if flag completion is to be used. static void PrintFlagCompletionInfo(void); // 1) Examine search word static void CanonicalizeCursorWordAndSearchOptions( const string &cursor_word, string *canonical_search_token, CompletionOptions *options); static bool RemoveTrailingChar(string *str, char c); // 2) Find all matches static void FindMatchingFlags( const vector<CommandLineFlagInfo> &all_flags, const CompletionOptions &options, const string &match_token, set<const CommandLineFlagInfo *> *all_matches, string *longest_common_prefix); static bool DoesSingleFlagMatch( const CommandLineFlagInfo &flag, const CompletionOptions &options, const string &match_token); // 3) Categorize matches static void CategorizeAllMatchingFlags( const set<const CommandLineFlagInfo *> &all_matches, const string &search_token, const string &module, const string &package_dir, NotableFlags *notable_flags); static void TryFindModuleAndPackageDir( const vector<CommandLineFlagInfo> all_flags, string *module, string *package_dir); // 4) Decide which flags to use static void FinalizeCompletionOutput( const set<const CommandLineFlagInfo *> &matching_flags, CompletionOptions *options, NotableFlags *notable_flags, vector<string> *completions); static void RetrieveUnusedFlags( const set<const CommandLineFlagInfo *> &matching_flags, const NotableFlags ¬able_flags, set<const CommandLineFlagInfo *> *unused_flags); // 5) Output matches static void OutputSingleGroupWithLimit( const set<const CommandLineFlagInfo *> &group, const string &line_indentation, const string &header, const string &footer, bool long_output_format, int *remaining_line_limit, size_t *completion_elements_added, vector<string> *completions); // (helpers for #5) static string GetShortFlagLine( const string &line_indentation, const CommandLineFlagInfo &info); static string GetLongFlagLine( const string &line_indentation, const CommandLineFlagInfo &info); // // Useful types // Try to deduce the intentions behind this completion attempt. Return the // canonical search term in 'canonical_search_token'. Binary search options // are returned in the various booleans, which should all have intuitive // semantics, possibly except: // - return_all_matching_flags: Generally, we'll trim the number of // returned candidates to some small number, showing those that are // most likely to be useful first. If this is set, however, the user // really does want us to return every single flag as an option. // - force_no_update: Any time we output lines, all of which share a // common prefix, bash will 'helpfully' not even bother to show the // output, instead changing the current word to be that common prefix. // If it's clear this shouldn't happen, we'll set this boolean struct CompletionOptions { bool flag_name_substring_search; bool flag_location_substring_search; bool flag_description_substring_search; bool return_all_matching_flags; bool force_no_update; }; // Notable flags are flags that are special or preferred for some // reason. For example, flags that are defined in the binary's module // are expected to be much more relevent than flags defined in some // other random location. These sets are specified roughly in precedence // order. Once a flag is placed in one of these 'higher' sets, it won't // be placed in any of the 'lower' sets. struct NotableFlags { typedef set<const CommandLineFlagInfo *> FlagSet; FlagSet perfect_match_flag; FlagSet module_flags; // Found in module file FlagSet package_flags; // Found in same directory as module file FlagSet most_common_flags; // One of the XXX most commonly supplied flags FlagSet subpackage_flags; // Found in subdirectories of package }; // // Tab completion implementation - entry point static void PrintFlagCompletionInfo(void) { string cursor_word = FLAGS_tab_completion_word; string canonical_token; CompletionOptions options = { }; CanonicalizeCursorWordAndSearchOptions( cursor_word, &canonical_token, &options); //VLOG(1) << "Identified canonical_token: '" << canonical_token << "'"; vector<CommandLineFlagInfo> all_flags; set<const CommandLineFlagInfo *> matching_flags; GetAllFlags(&all_flags); //VLOG(2) << "Found " << all_flags.size() << " flags overall"; string longest_common_prefix; FindMatchingFlags( all_flags, options, canonical_token, &matching_flags, &longest_common_prefix); //VLOG(1) << "Identified " << matching_flags.size() << " matching flags"; //VLOG(1) << "Identified " << longest_common_prefix // << " as longest common prefix."; if (longest_common_prefix.size() > canonical_token.size()) { // There's actually a shared common prefix to all matching flags, // so may as well output that and quit quickly. //VLOG(1) << "The common prefix '" << longest_common_prefix // << "' was longer than the token '" << canonical_token // << "'. Returning just this prefix for completion."; fprintf(stdout, "--%s", longest_common_prefix.c_str()); return; } if (matching_flags.empty()) { //VLOG(1) << "There were no matching flags, returning nothing."; return; } string module; string package_dir; TryFindModuleAndPackageDir(all_flags, &module, &package_dir); //VLOG(1) << "Identified module: '" << module << "'"; //VLOG(1) << "Identified package_dir: '" << package_dir << "'"; NotableFlags notable_flags; CategorizeAllMatchingFlags( matching_flags, canonical_token, module, package_dir, ¬able_flags); //VLOG(2) << "Categorized matching flags:"; //VLOG(2) << " perfect_match: " << notable_flags.perfect_match_flag.size(); //VLOG(2) << " module: " << notable_flags.module_flags.size(); //VLOG(2) << " package: " << notable_flags.package_flags.size(); //VLOG(2) << " most common: " << notable_flags.most_common_flags.size(); //VLOG(2) << " subpackage: " << notable_flags.subpackage_flags.size(); vector<string> completions; FinalizeCompletionOutput( matching_flags, &options, ¬able_flags, &completions); if (options.force_no_update) completions.push_back("~"); //VLOG(1) << "Finalized with " << completions.size() // << " chosen completions"; for (vector<string>::const_iterator it = completions.begin(); it != completions.end(); ++it) { //VLOG(9) << " Completion entry: '" << *it << "'"; fprintf(stdout, "%s\n", it->c_str()); } } // 1) Examine search word (and helper method) static void CanonicalizeCursorWordAndSearchOptions( const string &cursor_word, string *canonical_search_token, CompletionOptions *options) { *canonical_search_token = cursor_word; if (canonical_search_token->empty()) return; // Get rid of leading quotes and dashes in the search term if ((*canonical_search_token)[0] == '"') *canonical_search_token = canonical_search_token->substr(1); while ((*canonical_search_token)[0] == '-') *canonical_search_token = canonical_search_token->substr(1); options->flag_name_substring_search = false; options->flag_location_substring_search = false; options->flag_description_substring_search = false; options->return_all_matching_flags = false; options->force_no_update = false; // Look for all search options we can deduce now. Do this by walking // backwards through the term, looking for up to three '?' and up to // one '+' as suffixed characters. Consume them if found, and remove // them from the canonical search token. int found_question_marks = 0; int found_plusses = 0; while (true) { if (found_question_marks < 3 && RemoveTrailingChar(canonical_search_token, '?')) { ++found_question_marks; continue; } if (found_plusses < 1 && RemoveTrailingChar(canonical_search_token, '+')) { ++found_plusses; continue; } break; } switch (found_question_marks) { // all fallthroughs case 3: options->flag_description_substring_search = true; case 2: options->flag_location_substring_search = true; case 1: options->flag_name_substring_search = true; }; options->return_all_matching_flags = (found_plusses > 0); } // Returns true if a char was removed static bool RemoveTrailingChar(string *str, char c) { if (str->empty()) return false; if ((*str)[str->size() - 1] == c) { *str = str->substr(0, str->size() - 1); return true; } return false; } // 2) Find all matches (and helper methods) static void FindMatchingFlags( const vector<CommandLineFlagInfo> &all_flags, const CompletionOptions &options, const string &match_token, set<const CommandLineFlagInfo *> *all_matches, string *longest_common_prefix) { all_matches->clear(); bool first_match = true; for (vector<CommandLineFlagInfo>::const_iterator it = all_flags.begin(); it != all_flags.end(); ++it) { if (DoesSingleFlagMatch(*it, options, match_token)) { all_matches->insert(&*it); if (first_match) { first_match = false; *longest_common_prefix = it->name; } else { if (longest_common_prefix->empty() || it->name.empty()) { longest_common_prefix->clear(); continue; } string::size_type pos = 0; while (pos < longest_common_prefix->size() && pos < it->name.size() && (*longest_common_prefix)[pos] == it->name[pos]) ++pos; longest_common_prefix->erase(pos); } } } } // Given the set of all flags, the parsed match options, and the // canonical search token, produce the set of all candidate matching // flags for subsequent analysis or filtering. static bool DoesSingleFlagMatch( const CommandLineFlagInfo &flag, const CompletionOptions &options, const string &match_token) { // Is there a prefix match? string::size_type pos = flag.name.find(match_token); if (pos == 0) return true; // Is there a substring match if we want it? if (options.flag_name_substring_search && pos != string::npos) return true; // Is there a location match if we want it? if (options.flag_location_substring_search && flag.filename.find(match_token) != string::npos) return true; // TODO(daven): All searches should probably be case-insensitive // (especially this one...) if (options.flag_description_substring_search && flag.description.find(match_token) != string::npos) return true; return false; } // 3) Categorize matches (and helper method) // Given a set of matching flags, categorize them by // likely relevence to this specific binary static void CategorizeAllMatchingFlags( const set<const CommandLineFlagInfo *> &all_matches, const string &search_token, const string &module, // empty if we couldn't find any const string &package_dir, // empty if we couldn't find any NotableFlags *notable_flags) { notable_flags->perfect_match_flag.clear(); notable_flags->module_flags.clear(); notable_flags->package_flags.clear(); notable_flags->most_common_flags.clear(); notable_flags->subpackage_flags.clear(); for (set<const CommandLineFlagInfo *>::const_iterator it = all_matches.begin(); it != all_matches.end(); ++it) { //VLOG(2) << "Examining match '" << (*it)->name << "'"; //VLOG(7) << " filename: '" << (*it)->filename << "'"; string::size_type pos = string::npos; if (!package_dir.empty()) pos = (*it)->filename.find(package_dir); string::size_type slash = string::npos; if (pos != string::npos) // candidate for package or subpackage match slash = (*it)->filename.find( PATH_SEPARATOR, pos + package_dir.size() + 1); if ((*it)->name == search_token) { // Exact match on some flag's name notable_flags->perfect_match_flag.insert(*it); //VLOG(3) << "Result: perfect match"; } else if (!module.empty() && (*it)->filename == module) { // Exact match on module filename notable_flags->module_flags.insert(*it); //VLOG(3) << "Result: module match"; } else if (!package_dir.empty() && pos != string::npos && slash == string::npos) { // In the package, since there was no slash after the package portion notable_flags->package_flags.insert(*it); //VLOG(3) << "Result: package match"; } else if (false) { // In the list of the XXX most commonly supplied flags overall // TODO(daven): Compile this list. //VLOG(3) << "Result: most-common match"; } else if (!package_dir.empty() && pos != string::npos && slash != string::npos) { // In a subdirectory of the package notable_flags->subpackage_flags.insert(*it); //VLOG(3) << "Result: subpackage match"; } //VLOG(3) << "Result: not special match"; } } static void PushNameWithSuffix(vector<string>* suffixes, const char* suffix) { string s("/"); s += ProgramInvocationShortName(); s += suffix; suffixes->push_back(s); } static void TryFindModuleAndPackageDir( const vector<CommandLineFlagInfo> all_flags, string *module, string *package_dir) { module->clear(); package_dir->clear(); vector<string> suffixes; // TODO(daven): There's some inherant ambiguity here - multiple directories // could share the same trailing folder and file structure (and even worse, // same file names), causing us to be unsure as to which of the two is the // actual package for this binary. In this case, we'll arbitrarily choose. PushNameWithSuffix(&suffixes, "."); PushNameWithSuffix(&suffixes, "-main."); PushNameWithSuffix(&suffixes, "_main."); // These four are new but probably merited? PushNameWithSuffix(&suffixes, "-test."); PushNameWithSuffix(&suffixes, "_test."); PushNameWithSuffix(&suffixes, "-unittest."); PushNameWithSuffix(&suffixes, "_unittest."); for (vector<CommandLineFlagInfo>::const_iterator it = all_flags.begin(); it != all_flags.end(); ++it) { for (vector<string>::const_iterator suffix = suffixes.begin(); suffix != suffixes.end(); ++suffix) { // TODO(daven): Make sure the match is near the end of the string if (it->filename.find(*suffix) != string::npos) { *module = it->filename; string::size_type sep = it->filename.rfind(PATH_SEPARATOR); *package_dir = it->filename.substr(0, (sep == string::npos) ? 0 : sep); return; } } } } // Can't specialize template type on a locally defined type. Silly C++... struct DisplayInfoGroup { const char* header; const char* footer; set<const CommandLineFlagInfo *> *group; int SizeInLines() const { int size_in_lines = static_cast<int>(group->size()) + 1; if (strlen(header) > 0) { size_in_lines++; } if (strlen(footer) > 0) { size_in_lines++; } return size_in_lines; } }; // 4) Finalize and trim output flag set static void FinalizeCompletionOutput( const set<const CommandLineFlagInfo *> &matching_flags, CompletionOptions *options, NotableFlags *notable_flags, vector<string> *completions) { // We want to output lines in groups. Each group needs to be indented // the same to keep its lines together. Unless otherwise required, // only 99 lines should be output to prevent bash from harassing the // user. // First, figure out which output groups we'll actually use. For each // nonempty group, there will be ~3 lines of header & footer, plus all // output lines themselves. int max_desired_lines = // "999999 flags should be enough for anyone. -dave" (options->return_all_matching_flags ? 999999 : 98); int lines_so_far = 0; vector<DisplayInfoGroup> output_groups; bool perfect_match_found = false; if (lines_so_far < max_desired_lines && !notable_flags->perfect_match_flag.empty()) { perfect_match_found = true; DisplayInfoGroup group = { "", "==========", ¬able_flags->perfect_match_flag }; lines_so_far += group.SizeInLines(); output_groups.push_back(group); } if (lines_so_far < max_desired_lines && !notable_flags->module_flags.empty()) { DisplayInfoGroup group = { "-* Matching module flags *-", "===========================", ¬able_flags->module_flags }; lines_so_far += group.SizeInLines(); output_groups.push_back(group); } if (lines_so_far < max_desired_lines && !notable_flags->package_flags.empty()) { DisplayInfoGroup group = { "-* Matching package flags *-", "============================", ¬able_flags->package_flags }; lines_so_far += group.SizeInLines(); output_groups.push_back(group); } if (lines_so_far < max_desired_lines && !notable_flags->most_common_flags.empty()) { DisplayInfoGroup group = { "-* Commonly used flags *-", "=========================", ¬able_flags->most_common_flags }; lines_so_far += group.SizeInLines(); output_groups.push_back(group); } if (lines_so_far < max_desired_lines && !notable_flags->subpackage_flags.empty()) { DisplayInfoGroup group = { "-* Matching sub-package flags *-", "================================", ¬able_flags->subpackage_flags }; lines_so_far += group.SizeInLines(); output_groups.push_back(group); } set<const CommandLineFlagInfo *> obscure_flags; // flags not notable if (lines_so_far < max_desired_lines) { RetrieveUnusedFlags(matching_flags, *notable_flags, &obscure_flags); if (!obscure_flags.empty()) { DisplayInfoGroup group = { "-* Other flags *-", "", &obscure_flags }; lines_so_far += group.SizeInLines(); output_groups.push_back(group); } } // Second, go through each of the chosen output groups and output // as many of those flags as we can, while remaining below our limit int remaining_lines = max_desired_lines; size_t completions_output = 0; int indent = static_cast<int>(output_groups.size()) - 1; for (vector<DisplayInfoGroup>::const_iterator it = output_groups.begin(); it != output_groups.end(); ++it, --indent) { OutputSingleGroupWithLimit( *it->group, // group string(indent, ' '), // line indentation string(it->header), // header string(it->footer), // footer perfect_match_found, // long format &remaining_lines, // line limit - reduces this by number printed &completions_output, // completions (not lines) added completions); // produced completions perfect_match_found = false; } if (completions_output != matching_flags.size()) { options->force_no_update = false; completions->push_back("~ (Remaining flags hidden) ~"); } else { options->force_no_update = true; } } static void RetrieveUnusedFlags( const set<const CommandLineFlagInfo *> &matching_flags, const NotableFlags ¬able_flags, set<const CommandLineFlagInfo *> *unused_flags) { // Remove from 'matching_flags' set all members of the sets of // flags we've already printed (specifically, those in notable_flags) for (set<const CommandLineFlagInfo *>::const_iterator it = matching_flags.begin(); it != matching_flags.end(); ++it) { if (notable_flags.perfect_match_flag.count(*it) || notable_flags.module_flags.count(*it) || notable_flags.package_flags.count(*it) || notable_flags.most_common_flags.count(*it) || notable_flags.subpackage_flags.count(*it)) continue; unused_flags->insert(*it); } } // 5) Output matches (and helper methods) static void OutputSingleGroupWithLimit( const set<const CommandLineFlagInfo *> &group, const string &line_indentation, const string &header, const string &footer, bool long_output_format, int *remaining_line_limit, size_t *completion_elements_output, vector<string> *completions) { if (group.empty()) return; if (!header.empty()) { if (*remaining_line_limit < 2) return; *remaining_line_limit -= 2; completions->push_back(line_indentation + header); completions->push_back(line_indentation + string(header.size(), '-')); } for (set<const CommandLineFlagInfo *>::const_iterator it = group.begin(); it != group.end() && *remaining_line_limit > 0; ++it) { --*remaining_line_limit; ++*completion_elements_output; completions->push_back( (long_output_format ? GetLongFlagLine(line_indentation, **it) : GetShortFlagLine(line_indentation, **it))); } if (!footer.empty()) { if (*remaining_line_limit < 1) return; --*remaining_line_limit; completions->push_back(line_indentation + footer); } } static string GetShortFlagLine( const string &line_indentation, const CommandLineFlagInfo &info) { string prefix = line_indentation + "--" + info.name + " [" + (info.type == "string" ? ("'" + info.default_value + "'") : info.default_value) + "] "; int remainder = FLAGS_tab_completion_columns - static_cast<int>(prefix.size()); string suffix; if (remainder > 0) suffix = (static_cast<int>(info.description.size()) > remainder ? (info.description.substr(0, remainder - 3) + "...").c_str() : info.description.c_str()); return prefix + suffix; } static string GetLongFlagLine( const string &line_indentation, const CommandLineFlagInfo &info) { string output = DescribeOneFlag(info); // Replace '-' with '--', and remove trailing newline before appending // the module definition location. string old_flagname = "-" + info.name; output.replace( output.find(old_flagname), old_flagname.size(), "-" + old_flagname); // Stick a newline and indentation in front of the type and default // portions of DescribeOneFlag()s description static const char kNewlineWithIndent[] = "\n "; output.replace(output.find(" type:"), 1, string(kNewlineWithIndent)); output.replace(output.find(" default:"), 1, string(kNewlineWithIndent)); output = line_indentation + " Details for '--" + info.name + "':\n" + output + " defined: " + info.filename; // Eliminate any doubled newlines that crept in. Specifically, if // DescribeOneFlag() decided to break the line just before "type" // or "default", we don't want to introduce an extra blank line static const string line_of_spaces(FLAGS_tab_completion_columns, ' '); static const char kDoubledNewlines[] = "\n \n"; for (string::size_type newlines = output.find(kDoubledNewlines); newlines != string::npos; newlines = output.find(kDoubledNewlines)) // Replace each 'doubled newline' with a single newline output.replace(newlines, sizeof(kDoubledNewlines) - 1, string("\n")); for (string::size_type newline = output.find('\n'); newline != string::npos; newline = output.find('\n')) { int newline_pos = static_cast<int>(newline) % FLAGS_tab_completion_columns; int missing_spaces = FLAGS_tab_completion_columns - newline_pos; output.replace(newline, 1, line_of_spaces, 1, missing_spaces); } return output; } } // anonymous void HandleCommandLineCompletions(void) { if (FLAGS_tab_completion_word.empty()) return; PrintFlagCompletionInfo(); exit(0); } _END_GOOGLE_NAMESPACE_