Nous écrivons l'auto-complétion pour vos projets CLI

Salutation



salut! Je souhaite partager mon expérience d'écriture d'un projet multiplateforme en C ++ pour intégrer l'auto-complétion dans l'application CLI, asseyez-vous.







Déclaration de la mission



  • L'application doit fonctionner sous Linux, macOS, Windows
  • Vous devez être en mesure de définir des règles pour la saisie semi-automatique
  • Prévoir les fautes de frappe
  • Fournir un changement d'invite avec les flèches du clavier


Préparation



Je vous préviendrai tout de suite, nous utiliserons C++17



. , , .



#if defined(_WIN32) || defined(_WIN64)
    #define OS_WINDOWS
#elif defined(__APPLE__) || defined(__unix__) || defined(__unix)
    #define OS_POSIX
#else
    #error unsupported platform
#endif


:



#if defined(OS_WINDOWS)
    #define ENTER 13
    #define BACKSPACE 8
    #define CTRL_C 3
    #define LEFT 75
    #define RIGHT 77
    #define DEL 83
    #define UP 72
    #define DOWN 80
    #define SPACE 32
#elif defined(OS_POSIX)
    #define ENTER 10
    #define BACKSPACE 127
    #define SPACE 32
    #define LEFT 68
    #define RIGHT 67
    #define UP 65
    #define DOWN 66
    #define DEL 51
#endif
    #define TAB 9


CLI , Linux macOS API, define OS_POSIX. Windows, , , define OS_WINDOWS.



, . Redis CLI, . .



, :



/**
 * Sets the console color.
 *
 * @param color System code of target color.
 * @return Input parameter os.
 */
#if defined(OS_WINDOWS)
std::string set_console_color(uint16_t color) {
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color);
    return "";
#elif defined(OS_POSIX)
std::string set_console_color(std::string color) {
    return "\033[" + color + "m";
#endif
}


- API , , << .



, , API Posix Windows, , stackoverflow:





- , "" .



/**
 * Get count of terminal cols.
 *
 * @return Width of terminal.
 */
#if defined(OS_WINDOWS)
size_t console_width() {
    CONSOLE_SCREEN_BUFFER_INFO info;
    GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);

    short width = --info.dwSize.X;
    return size_t((width < 0) ? 0 : width);
}
#endif

/**
 * Clear terminal line.
 *
 * @param os Output stream.
 * @return input parameter os.
 */
std::ostream& clear_line(std::ostream& os) {
#if defined(OS_WINDOWS)
    size_t width = console_width();
    os << '\r' << std::string(width, ' ');
#elif defined(OS_POSIX)
    std::cout << "\033[2K";
#endif
    return os;
}


Posix , \033[2K, Windows , , .



, . cin , .



_getch(), Windows, — , . Posix , , .



#if defined(OS_POSIX)
/**
 * Read key without press ENTER.
 *
 * @return Code of key on keyboard.
 */
int _getch() {
    int ch;
    struct termios old_termios, new_termios;
    tcgetattr( STDIN_FILENO, &old_termios );
    new_termios = old_termios;
    new_termios.c_lflag &= ~(ICANON | ECHO );
    tcsetattr( STDIN_FILENO, TCSANOW, &new_termios );
    ch = getchar();
    tcsetattr( STDIN_FILENO, TCSANOW, &old_termios );
    return ch;
}
#endif








. , . :



git
    config
        --global
            user.name
                "[name]"
            user.email
                "[email]"
        user.name
            "[name]"
        user.email
            "[email]"
    init
        [repository name]
    clone
        [url]


. 1 . .. git config, init global. config --global, user.name user.email .. , [] ( ).



, , — , -.



typedef std::map<std::string, std::vector<std::string>> Dictionary;


.



/**
 * Parse config file to dictionary.
 *
 * @param file_path The path to the configuration file.
 * @return Tuple of dictionary with autocomplete rules, status of parsing and message.
 */
std::tuple<Dictionary, bool, std::string> 
parse_config_file(const std::string& file_path) {
    Dictionary dict;            //    

    std::map<int, std::string>  //     
    root_words_by_tabsize;      //     

    std::string line;           //   
    std::string token;          //    
    std::string root_word;      //        

    long tab_size = 0;          //    ()
    long tab_count = 0;         //    

    //   
    std::ifstream config_file(file_path);

    //    ,     
    if (!config_file.is_open()) {
        return std::make_tuple(
            dict,
            false,
            "Error! Can't open " + file_path + " file."
        );
    }

    //   
    while (std::getline(config_file, line)) {
        //     
        if (line.empty()) {
            continue;
        }

        //      ,    
        if (std::count(line.begin(), line.end(), '\t') != 0) {
            return std::make_tuple(
                dict,
                false,
                "Error! Use a sequence of spaces instead of a tab character."
            );
        }

        //      
        auto spaces = std::count(
            line.begin(),
            line.begin() + line.find_first_not_of(" "),
            ' '
        );

        //    , 
        //       
        if (spaces != 0 && tab_size == 0) {
            tab_size = spaces;
        }

        //    
        token = trim(line);

        //   
        if (tab_size != 0 && spaces % tab_size != 0) {
            return std::make_tuple(
                dict,
                false,
                "Error! Tab length error was made.\nPossibly in line: " + line
            );
        }

        //   
        tab_count = (tab_size == 0) ? 0 : (spaces / tab_size);

        //       
        root_words_by_tabsize[tab_count] = token;

        //      
        root_word = (tab_count == 0) ? "" : root_words_by_tabsize[tab_count - 1];

        //    ,    
        if (std::count(dict[root_word].begin(), dict[root_word].end(), token) == 0) {
            dict[root_word].push_back(token);
        }
    }

    //  
    config_file.close();

    //      
    return std::make_tuple(
        dict,
        true,
        "Success. The rule dictionary has been created."
    );
}


.



  1. , .
  2. \t ? .
  3. trim, ? .


/**
 * Remove extra spaces to the left and right of the string.
 *
 * @param str Source string.
 * @return Line without spaces on the left and right.
 */
std::string trim(std::string_view str) {
    std::string result(str);
    result.erase(0, result.find_first_not_of(" \n\r\t"));
    result.erase(result.find_last_not_of(" \n\r\t") + 1);
    return result;
}






. , ? .

, - . ? .



.



/**
 * Get the position of the beginning of the last word.
 *
 * @param str String with words.
 * @return Position of the beginning of the last word.
 */
size_t get_last_word_pos(std::string_view str) {
    //  0      
    if (std::count(str.begin(), str.end(), ' ') == str.length()) {
        return 0;
    }

    //    
    auto last_word_pos = str.rfind(' ');

    //  0,    ,    + 1
    return (last_word_pos == std::string::npos) ? 0 : last_word_pos + 1;
}

/**
 * Get the last word in string.
 *
 * @param str String with words.
 * @return Pair Position of the beginning of the
 *         last word and the last word in string.
 */
std::pair<size_t, std::string> get_last_word(std::string_view str) {
    //  
    size_t last_word_pos = get_last_word_pos(str);

    //     
    auto last_word = str.substr(last_word_pos);

    //          ( )
    return std::make_pair(last_word_pos, last_word.data());
}


, , , , , , .



.



//   std::min -  
//  MSVC 
/**
 * Get the minimum of two numbers.
 *
 * @param a First value.
 * @param b Second value.
 * @return Minimum of two numbers.
 */
size_t min_of(size_t a, size_t b) {
    return (a < b) ? a : b;
}

/**
 * Get the penultimate words.
 *
 * @param str String with words.
 * @return Pair Position of the beginning of the penultimate
 *         word and the penultimate word in string.
 */
std::pair<size_t, std::string> get_penult_word(std::string_view str) {
    //    
    size_t end_pos = min_of(str.find_last_not_of(' ') + 2, str.length());

    //     
    size_t last_word = get_last_word_pos(str.substr(0, end_pos));

    size_t penult_word_pos = 0;
    std::string penult_word = "";

    //      
    //    
    if (last_word != 0) {
        //    
        penult_word_pos = str.find_last_of(' ', last_word - 2);

        //       
        if (penult_word_pos != std::string::npos) {
            penult_word = str.substr(penult_word_pos, last_word - penult_word_pos - 1);
        }
        //    - ,     
        else {
            penult_word = str.substr(0, last_word - 1);
        }
    }

    //  
    penult_word = trim(penult_word);

    //       ( )
    return std::make_pair(penult_word_pos, penult_word);
}








? , , .



/**
 * Find strings in vector starts with substring.
 *
 * @param substr String with which the word should begin.
 * @param penult_word Penultimate word in user-entered line.
 * @param dict Vector of words.
 * @param optional_brackets String with symbols for optional values.
 * @return Vector with words starts with substring.
 */
std::vector<std::string>
words_starts_with(std::string_view substr, std::string_view penult_word,
                  Dictionary& dict, std::string_view optional_brackets) {
    std::vector<std::string> result;

    //      penult_word 
    // substr      
    if (!dict.count(penult_word.data()) ||
        substr.find_first_of(optional_brackets) != std::string::npos) 
    {
        return result;
    }

    //   ,    
    //  last_word,  substr 
    if (substr.empty()) {
        return dict[penult_word.data()];
    }

    //  ,   substr
    std::vector<std::string> candidates_list = dict[penult_word.data()];
    for (size_t i = 0 ; i < candidates_list.size(); i++) {
        if (candidates_list[i].find(substr) == 0) {
            result.push_back(dict[penult_word.data()][i]);
        }
    }

    return result;
}


? ? .



/**
 * Find strings in vector similar to a substring (max 1 error).
 *
 * @param substr String with which the word should begin.
 * @param penult_word Penultimate word in user-entered line.
 * @param dict Vector of words.
 * @param optional_brackets String with symbols for optional values.
 * @return Vector with words similar to a substring.
 */
std::vector<std::string>
words_similar_to(std::string_view substr, std::string_view penult_word, 
                 Dictionary& dict, std::string_view optional_brackets) {
    std::vector<std::string> result;

    // ,   
    if (substr.empty()) {
        return result;
    }

    std::vector<std::string> candidates_list = dict[penult_word.data()];
    for (size_t i = 0 ; i < candidates_list.size(); i++) {
        int errors = 0;

        //  
        std::string candidate = candidates_list[i];

        //   
        for (size_t j = 0; j < substr.length(); j++) {

            // ,       
            if (optional_brackets.find_first_of(candidate[j]) != std::string::npos) {
                errors = 2;
                break;
            }

            if (substr[j] != candidate[j]) {
                errors += 1;
            }

            if (errors > 1) {
                break;
            }
        }

        //  ,    
        if (errors <= 1) {
            result.push_back(candidate);
        }
    }

    return result;
}


, .

.



/**
 * Get the word-prediction by the index.
 *
 * @param buffer String with user input.
 * @param dict Dictionary with rules.
 * @param number Index of word-prediction.
 * @param optional_brackets String with symbols for optional values.
 * @return Tuple of word-prediction, phrase for output, substring of buffer
 *         preceding before phrase, start position of last word.
 */
std::tuple<std::string, std::string, std::string, size_t>
get_prediction(std::string_view buffer, Dictionary& dict, size_t number,
               std::string_view optional_brackets) {
    //     
    auto [last_word_pos, last_word] = get_last_word(buffer);

    //     
    auto [_, penult_word] = get_penult_word(buffer);

    std::string prediction; // 
    std::string phrase;     //   
    std::string prefix;     //  ,  

    //  
    std::vector<std::string> starts_with = words_starts_with(
        last_word, penult_word, dict, optional_brackets
    );

    //  ,    
    if (!starts_with.empty()) {
        prediction = starts_with[number % starts_with.size()];
        phrase = prediction;
        prefix = buffer.substr(0, last_word_pos);
    }
    //     
    else {
        //     
        std::vector<std::string> similar = words_similar_to(
            last_word, penult_word, dict, optional_brackets
        );

        //  ,    
        if (!similar.empty()) {
            prediction = similar[number % similar.size()];
            phrase = " maybe you mean " + prediction + "?";
            prefix = buffer;
        }
    }

    //   
    return std::make_tuple(prediction, phrase, prefix, last_word_pos);
}








. .



/**
 * Gets current terminal cursor position.
 *
 * @return Y position of terminal cursor.
 */
short cursor_y_pos() {
#if defined(OS_WINDOWS)
    CONSOLE_SCREEN_BUFFER_INFO info;
    GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);
    return info.dwCursorPosition.Y;
#elif defined(OS_POSIX)
    struct termios term, restore;
    char ch, buf[30] = {0};
    short i = 0, pow = 1, y = 0;

    tcgetattr(0, &term);
    tcgetattr(0, &restore);
    term.c_lflag &= ~(ICANON|ECHO);
    tcsetattr(0, TCSANOW, &term);

    write(1, "\033[6n", 4);

    for (ch = 0; ch != 'R'; i++) {
        read(0, &ch, 1);
        buf[i] = ch;
    }

    i -= 2;
    while (buf[i] != ';') {
        i -= 1;
    }

    i -= 1;
    while (buf[i] != '[') {
        y = y + ( buf[i] - '0' ) * pow;
        pow *= 10;
        i -= 1;
    }

    tcsetattr(0, TCSANOW, &restore);
    return y;
#endif
}

/**
 * Move terminal cursor at position x and y.
 *
 * @param x X position to move.
 * @param x Y position to move.
 * @return Void.
 */
void goto_xy(short x, short y) {
#if defined(OS_WINDOWS)
    COORD xy {--x, y};
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), xy);
#elif defined(OS_POSIX)
    printf("\033[%d;%dH", y, x);
#endif
}

/**
 * Printing user input with prompts.
 *
 * @param buffer String - User input.
 * @param dict Vector of words.
 * @param line_title Line title of CLI when entering command.
 * @param number Hint number.
 * @param optional_brackets String with symbols for optional values.
 * @param title_color System code of title color     (line title color).
 * @param predict_color System code of predict color (prediction color).
 * @param default_color System code of default color (user input color).
 * @return Void.
 */
#if defined(OS_WINDOWS)
void print_with_prompts(std::string_view buffer, Dictionary& dict,
                        std::string_view line_title, size_t number,
                        std::string_view optional_brackets,
                        uint16_t title_color, uint16_t predict_color,
                        uint16_t default_color) {
#else
void print_with_prompts(std::string_view buffer, Dictionary& dict,
                        std::string_view line_title, size_t number,
                        std::string_view optional_brackets,
                        std::string title_color, std::string predict_color,
                        std::string default_color) {
#endif
    //      ,  
    auto [_, phrase, prefix, __] = 
        get_prediction(buffer, dict, number, optional_brackets);

    std::string delimiter = line_title.empty() ? "" : " ";

    std::cout << clear_line;

    std::cout << '\r' << set_console_color(title_color) << line_title
              << set_console_color(default_color) << delimiter << prefix
              << set_console_color(predict_color) << phrase;

    std::cout << '\r' << set_console_color(title_color) << line_title
              << set_console_color(default_color) << delimiter << buffer;
}

/**
 * Reading user input with autocomplete.
 *
 * @param dict Vector of words.
 * @param optional_brackets String with symbols for optional values.
 * @param title_color System code of title color     (line title color).
 * @param predict_color System code of predict color (prediction color).
 * @param default_color System code of default color (user input color).
 * @return User input.
 */
#if defined(OS_WINDOWS)
std::string input(Dictionary& dict, std::string_view line_title,
                  std::string_view optional_brackets, uint16_t title_color,
                  uint16_t predict_color, uint16_t default_color) {
#else
std::string input(Dictionary& dict, std::string_view line_title,
                  std::string_view optional_brackets, std::string title_color,
                  std::string predict_color, std::string default_color) {
#endif
    std::string buffer;       // 
    size_t offset = 0;        //     
    size_t number = 0;        //  () ,  
    short y = cursor_y_pos(); //      Y  

    //  
    #if defined(OS_WINDOWS)
    std::vector<int> ignore_keys({1, 2, 19, 24, 26});
    #elif defined(OS_POSIX)
    std::vector<int> ignore_keys({1, 2, 4, 24});
    #endif

    while (true) {
        //     
        print_with_prompts(buffer, dict, line_title, number, optional_brackets,
                           title_color, predict_color, default_color);

        //     
        short x = short(
            buffer.length() + line_title.length() + !line_title.empty() + 1 - offset
        );
        goto_xy(x, y);

        //   
        int ch = _getch();

        //  ,   Enter
        if (ch == ENTER) {
            return buffer;
        }

        //    CLI  Windows
        #if defined(OS_WINDOWS)
        else if (ch == CTRL_C) {
            exit(0);
        }
        #endif

        //     BACKSPACE
        else if (ch == BACKSPACE) {
            if (!buffer.empty() && buffer.length() - offset >= 1) {
                buffer.erase(buffer.length() - offset - 1, 1);
            }
        }

        //     TAB
        else if (ch == TAB) {
            //   
            auto [prediction, _, __, last_word_pos] = 
                get_prediction(buffer, dict, number, optional_brackets);

            //  ,  
            if (!prediction.empty() && 
                prediction.find_first_of(optional_brackets) == std::string::npos) {
                buffer = buffer.substr(0, last_word_pos) + prediction + " ";
            }

            //     
            offset = 0;
            number = 0;
        }

        //  
        #if defined(OS_WINDOWS)
        else if (ch == 0 || ch == 224)
        #elif defined(OS_POSIX)
        else if (ch == 27 && _getch() == 91)
        #endif
                switch (_getch()) {
                    case LEFT:
                        //  ,    
                        offset = (offset < buffer.length()) 
                                    ? offset + 1
                                    : buffer.length();
                        break;
                    case RIGHT:
                        //  ,    
                        offset = (offset > 0) ? offset - 1 : 0;
                        break;
                    case UP:
                        //   
                        number = number + 1;
                        std::cout << clear_line;
                        break;
                    case DOWN:
                        //   
                        number = number - 1;
                        std::cout << clear_line;
                        break;
                    case DEL:
                    //  ,   DELETE
                    #if defined(OS_POSIX)
                    if (_getch() == 126)
                    #endif
                    {
                        if (!buffer.empty() && offset != 0) {
                            buffer.erase(buffer.length() - offset, 1);
                            offset -= 1;
                        }
                    }
                    default:
                        break;
                }

        //       
        //     
        else if (!std::count(ignore_keys.begin(), ignore_keys.end(), ch)) {
            buffer.insert(buffer.end() - offset, (char)ch);

            if (ch == SPACE) {
                number = 0;
            }
        }
    }
}


, . .





#include <iostream>
#include <string>

#include "../include/autocomplete.h"

int main() {
    //   
    std::string config_file_path = "../config.txt";

    // ,     
    //  ( )
    std::string optional_brackets = "[";

    //   
    #if defined(OS_WINDOWS)
        uint16_t title_color = 160; // by default 10
        uint16_t predict_color = 8; // by default 8
        uint16_t default_color = 7; // by default 7
    #elif defined(OS_POSIX)
        // Set the value that goes between \033 and m ( \033{your_value}m )
        std::string title_color = "0;30;102";  // by default 92
        std::string predict_color = "90";      // by default 90
        std::string default_color = "0";       // by default 90
    #endif

    //    
    size_t command_counter = 0;

    //  
    auto [dict, status, message] = parse_config_file(config_file_path);

    //    
    if (status) {
        std::cerr << "Attention! Please run the executable file only" << std::endl
                  << "through the command line!\n\n";

        std::cerr << "- To switch the prompts press UP or DOWN arrow." << std::endl;
        std::cerr << "- To move cursor press LEFT or RIGHT arrow." << std::endl;
        std::cerr << "- To edit input press DELETE or BACKSPACE key." << std::endl;
        std::cerr << "- To apply current prompt press TAB key.\n\n";

        //  
        while (true) {
            //   
            std::string line_title = "git [" + std::to_string(command_counter) + "]:";

            //      
            std::string command = input(dict, line_title, optional_brackets,
                                        title_color, predict_color, default_color);

            //  -   
            std::cout << std::endl << command << std::endl << std::endl;

            command_counter++;
        }
    }
    //  ,      
    else {
        std::cerr << message << std::endl;
    }

    return 0;
}






macOS, Linux, Windows. .



:



Comme vous pouvez le voir, il n'est pas facile d'écrire du code multiplateforme (dans notre cas, nous avons dû écrire manuellement ce qui est sur Windows prêt à l'emploi pour Linux et vice versa), mais c'est très intéressant et le fait même que tout fonctionne sur les trois OS est extrêmement ...



J'espère avoir aidé quelqu'un. Si vous avez quelque chose à ajouter, j'écouterai attentivement dans les commentaires.



Le code source peut être trouvé ici .

Utilisez-le pour votre santé.




All Articles