问候语
你好!我想分享一下我用C ++编写跨平台项目以将自动完成功能集成到CLI应用程序中的经验,请坐下来。
作业说明
- 该应用程序必须在Linux,macOS,Windows上运行
- 您需要能够设置自动完成规则
- 提供错别字
- 使用键盘箭头提供提示更改
制备
我会立即警告您,我们将使用 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:
- Windows: https://stackoverflow.com/questions/4053837/colorizing-text-in-the-console-with-c#answer-4053879
- Posix: https://stackoverflow.com/questions/2616906/how-do-i-output-coloured-text-to-a-linux-terminal#answer-45300654
- , "" .
/**
* 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."
);
}
.
- , .
-
\t
? . - 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. .
:
如您所见,编写跨平台代码并不容易(在我们的案例中,我们必须手动为Windows开箱即用地编写Windows上的内容,反之亦然),但这非常有趣,而且事实证明它们都可以在所有三个OS上正常工作...
我希望我对某人有所帮助。如果您有什么要补充的,我会在评论中仔细听。
源代码可以在这里找到。
使用它对您的健康。