Правильный способ печати широких строк на терминалах с разными кодировками
Ниже я постараюсь напечатать строку XЯ
(латинский "ex", кириллический "ya" и финикийский "teth") к терминалам с различными кодировками, а именно utf8, cp1251 и C (POSIX). Я ожидаю увидеть XЯ
в терминале utf8, XЯ?
в терминале cp1251 и X??
в терминале C (POSIX). Вопросительные знаки объясняются тем, что выходная библиотека C++ заменяет символы, которые она не может представить ?
, Это правильное и ожидаемое поведение.
(1) Моя первая наивная попытка была просто напечатать строку широких символов в wcout:
wchar_t str[] = L"\U00000058\U0000042f\U00010908";
std::wcout << str << std::endl;
// utf8 terminal output: X??
// cp1251: X??
// C: X??
Во всех терминалах он правильно печатал только первый символ ascii7. Другие символы были заменены на "?" Метки. Оказалось, что это произошло потому, что во время запуска программы LC_ALL установлен на C.
(2) Вторая попытка была вызвать вручную std::setlocale()
с кодировкой utf8:
wchar_t str[] = L"\U00000058\U0000042f\U00010908";
std::setlocale(LC_ALL, "en_US.UTF-8");
std::wcout << str << std::endl;
// utf8: XЯ
// cp1251: XЯ𐤈
// C: XЯð¤
Очевидно, это работало правильно в терминале utf8, но приводило к мусору в двух других терминалах.
(3) Третья попытка была разобрать $LANG
Переменная окружения для фактического кодирования, используемого терминалом (и надеюсь, что все части терминала используют одинаковое кодирование):
const char* lang = std::getenv("LANG");
if (!lang) {
std::cerr << "Couldn't get LANG" << std::endl;
exit(1);
}
wchar_t str[] = L"\U00000058\U0000042f\U00010908";
std::setlocale(LC_ALL, lang);
std::wcout << str << std::endl;
// utf8: XЯ
// cp1251: XЯ?
// C: X??
Теперь выход во всех трех терминалах был, как я ожидал. Тем не менее, смешивание std::cout
а также std::wcout
плохая идея, и std::cout
определенно используется некоторыми сторонними библиотеками, используемыми в моей программе. Это делает std::wcout
непригодным для использования.
(4) Итак, четвертая попытка (или, собственно, идея) состояла в том, чтобы обнаружить кодирование терминала из $LANG
использовать codevct()
преобразовать wchar_t[]
введите в кодировку терминала и распечатайте его обычным способом std::cout.write()
, К сожалению, я не смог найти способ явно установить целевую кодировку для codevct()
,
(5) Пятая и пока самая лучшая попытка заключалась в использовании iconv()
вручную:
// get $LANG env var
const char* lang = std::getenv("LANG");
if (!lang) {
std::cerr << "Couldn't get $LANG" << std::endl;
exit(1);
}
// find out encoding from $LANG, e.g. "utf8", "cp1251", etc
std::string enc(lang);
size_t pos = enc.rfind('.');
if (pos != std::string::npos) {
enc = enc.substr(pos + 1);
}
if (enc == "C" || enc == "POSIX") {
enc = "iso8859-1";
}
// convert wchar_t[] string into terminal encoding
wchar_t str[] = L"\U00000058\U0000042f\U00010908";
iconv_t handler = iconv_open(enc.c_str(), "UTF32LE");
if (handler == (iconv_t)-1) {
std::cerr << "Couldn't create iconv handler: " << strerror(errno) << std::endl;
exit(1);
}
char buf[1024];
char* inbuf = (char*)str;
size_t inbytes = sizeof(str);
char* outbuf = buf;
size_t outbytes = sizeof(buf);
while (true) {
size_t res = iconv(handler, &inbuf, &inbytes, &outbuf, &outbytes);
if (res != (size_t)-1) {
break;
}
if (errno == EILSEQ) {
// replace non-convertable code point with question mark and retry iconv()
inbuf[0] = '\x3f';
inbuf[1] = '\x00';
inbuf[2] = '\x00';
inbuf[3] = '\x00';
} else {
std::cerr << "iconv() failed: %s" << strerror(errno) << std::endl;
exit(1);
}
}
iconv_close(handler);
// write converted string to std::cout
std::cout.write(buf, sizeof(buf) - outbytes);
std::cout << std::endl;
// utf8: XЯ
// cp1251: XЯ?
// C: X??
Это работало правильно во всех трех терминалах. И теперь я тоже не боюсь, что std::cout
используется в других частях программы. Однако я нахожу это решение не C++- способом.
Итак, вопрос в том, как правильно печатать широкие строки в C++? Я был бы в порядке с платформно-ориентированным решением (Linux + glibc + GCC).