Правильный способ печати широких строк на терминалах с разными кодировками - PullRequest
0 голосов
/ 05 января 2019

Ниже я попытаюсь напечатать строку 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).

...