Форматный вывод в консоли линукс

Есть вот такая программа:

#include <iostream>
#include <string>
#include <cstdio>
#include <clocale>
#include <iomanip>

using namespace std;

const size_t BUF_SIZE = 23;

int main() {
    string s;
    char buf[BUF_SIZE];

    setlocale(LC_ALL, "ru-RU.UTF-8");
    cout << "Введите строку:" << endl;
    getline(cin, s);
    int printed = snprintf(buf, BUF_SIZE, "%10s|%10i\n", s.c_str(), 5);
    cout << "0123456789012345678901234567890\n";
    cout << buf;
    cout << "printed = " << printed << endl;

    cout << setw(10) << s << "|" << setw(10) << 5 << endl;

    return 0;
}

Если я ввожу строку в латинице — работает как и надо. Но если в кириллице, snprintf в спецификации %10s считает не code-point Юникода, а сырые байты. Т.е. на каждый кириллический символ функция считает 2 позиции, а реально отображается один символ. Соответственно форматирование рушится.

Если использовать потоковый вывод — эффект аналогичный. Вызов setlocale ничего не меняет.

Как можно побороть проблему? Хотелось бы с форматной строкой. Но на крайний случай можно и с потоковым выводом.

Да, это всё под Убунтой. Под виндой с кодировкой в cp-1251 проблем нет.

Как можно побороть проблему?

Большей частью под линуксом используется utf-8,
так что нужно научиться с ней работать.

Один из вариантов — конвертация в строку широких символов:

#include <iostream>
#include <string>
#include <cstdio>
#include <locale>
#include <codecvt>


const size_t BUF_SIZE = 32;

int main() {
    std::string s;
    wchar_t buf[BUF_SIZE];

    std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;

    std::cout << "Введите строку:" << std::endl;
    std::getline(std::cin, s);

    std::wstring wstr = converter.from_bytes(s);

    int printed = std::swprintf(buf, BUF_SIZE, L"%10ls|%10i\n", wstr.c_str(), 5);
    std::cout << "0123456789012345678901234567890\n";
    std::cout << converter.to_bytes(buf);
    std::cout << "printed = " << printed << std::endl;
}

Спасибо, с swprintf работает ))

А как будет с потоками? Если я в строку

cout << setw(10) << s << "|" << setw(10) << 5 << endl;

добавленную в самый конец вашего варианта, вместо s тупо вставляю wstr — компилятор ругается матерно.

Если вставляю converter.to_bytes(wstr), то опять сбивается форматирование.

И еще вопрос. Вы написали «один из вариантов». А какие есть еще варианты?

А как будет с потоками?

Например, работать везде с широкими символами,
а при выводе уже конвертировать:

#include <iostream>
#include <string>
#include <cstdio>
#include <locale>
#include <codecvt>
#include <iomanip>
#include <sstream>

const size_t BUF_SIZE = 32;

int main() {
    std::string s;
    wchar_t buf[BUF_SIZE];

    std::locale::global(std::locale(""));

    std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;

    std::cout << "Введите строку:" << std::endl;
    std::getline(std::cin, s);

    std::wstring wstr = converter.from_bytes(s);

    int printed = std::swprintf(buf, BUF_SIZE, L"%10ls|%10i\n", wstr.c_str(), 5);
    std::cout << "0123456789012345678901234567890\n";
    std::cout << converter.to_bytes(buf);
    std::cout << "printed = " << printed << std::endl;

    std::wstringstream wss;    
    wss << std::setw(10) << wstr << "|" << std::setw(10) << 5 << std::endl;    
    std::cout << converter.to_bytes(wss.str());
}

http://rextester.com/HQJH24524

Вы написали «один из вариантов». А какие есть еще варианты?

Например, установить какую-нибудь
однобайтную кодировку в терминале.

Спасибо.

Например, установить какую-нибудь однобайтную кодировку в терминале.

Не вариант.

Кстати интересно, а как из программы определить какая кодировка установлена в терминале?

А не могли бы вы на пальцах объяснить что такое wchar_t? А также TCHAR. И как оно соотносится с UTF-8 и лр. разновидностями юникода.

А то в книжках это описано как-то невнятно. И вообще выглядит как wchat_t == UNICODE :(

Кстати интересно, а как из программы определить какая кодировка установлена в терминале?

Возможно, поможет nl_langinfo, но не уверен.
Нужно будет в мануалах смотреть.

А не могли бы вы на пальцах объяснить что такое wchar_t?

Это тип для хранения т.н. широких символов.
Для Linux это 4 байта.
Для Windows это 2 байта.

А также TCHAR.

Это макрос из windows.
В зависимости от определения макросов UNICODE (_UNICODE),
раскрывается либо в char, либо в wchar_t.

И как оно соотносится с UTF-8 и лр. разновидностями юникода.

Если определены вышеуказанные макросы,
то работа будет производится с широкими символами.
Но utf-8 придется «перекодировать» в нужную кодировку.
Но можете работать и с utf-8, если необходимо.
Например, в glib есть для этого средства,
а в glibmm имеется готовая обертка — ustring.

А то в книжках это описано как-то невнятно

Про wchar_t упоминают мало и редко в книгах.
Наверное, потому что это добавляет сложности,
которая при обучении и для примеров явно не нужна.

А в wchar_t символы в какой кодировке хранятся?

Я хотел еще попробовать сделать типа платформонезависимый (windows/linux) файл данных. Думал для этого использовать wchar_t. Но с разной длиной wchar_t вы меня как-то подкосили :( Получается, что фактически ни на что нельзя положиться: у числовых типов размерность в байтах не гарантирована, wchar_t тоже разный, хваленый юникод сделан через жопу и там, и там. Ппц!

Попробовал писать в файл wchar_t через wofstream. Под виндой пишет тупо в cp1251. А под линуксом вроде как в UTF-8. Но на каждый символ отводит по 2 байта. Т.е. получается, что внутри программы строка сидит в wchar_t, а при вводе/выводе в файл перекодируется из/в «родную» кодировку? Не, можно конечно тупо привести указатель на буфер к char* и писать через fwrite. Но это как-то уж совсем грубо.

И не могу понять как определить какие локали присутствуют в системе. Если нераспознаваемую локаль подсовываешь, setlocale игнорирует, а locale() дает исключение. А что оно распознает — хрен его знает. Гарантируется только наличие двух локалей: "C" и "". Где нарыть остальное?

А в wchar_t символы в какой кодировке хранятся?

В Linux — UTF-32.

хваленый юникод сделан через жопу

В C++ с Unicode беда.
Но обещают сделать нормальную поддержку.

Получается, что фактически ни на что нельзя положиться: у числовых типов размерность в байтах не гарантирована

Есть типы char16_t, char32_t.

перекодируется из/в «родную» кодировку?

Врядли. В utf-8 латиница будет занимать один байт,
а кириллица два (конечно, если адекватно закодировать).
Посмотрите как у Вас получается в файле.

Не, можно конечно тупо привести указатель на буфер к char* и писать через fwrite.

Нельзя. UTF-8 и UTF16/32 кодируются по разному.
Предложенный способ даст полную белиберду в файле.

И не могу понять как определить какие локали присутствуют в системе.

Стандартными средствами — никак.

Если нераспознаваемую локаль подсовываешь, setlocale игнорирует

setlocale возвращает ошибку,
которую в нубокнигах не считают нужной обрабатывать.

А что оно распознает — хрен его знает. Гарантируется только наличие двух локалей: «C» и «».

Оно и понятно. В разных ОС локали имеют разные названия.
Как-то стандартизировать эту шнягу будет не просто.

Где нарыть остальное?

В документации к ОС. Ставить нужные и т.д.
Получение списка локалей тоже будет зависеть от системы.

перекодируется из/в «родную» кодировку?

Врядли. В utf-8 латиница будет занимать один байт,
а кириллица два (конечно, если адекватно закодировать).
Посмотрите как у Вас получается в файле.

В файле:

  • под линуксом латиница 1 байт на символ, кириллица — 2 байта на символ.
  • под виндой и латиница и кириллица — по 1 байту на символ.

А вот если под виндой сделать

    std::wstring wsrc_str;
    // ...
    std::ofstream ofs("datafile.dat", std::ios::binary);
    if (!ofs.bad()) {
        const char *ptr = reinterpret_cast<const char *>(wsrc_str.c_str());
        ofs.write(ptr, wsrc_str.length() * 2 + 1);
        ofs.close();
    }

    wsrc_str.clear();

то в файле видно, что все символы в двухбайтовой кодировке.

И не могу понять как определить какие локали присутствуют в системе.

Стандартными средствами — никак.

В винде я это раскопал:

#include <locale> 
#include <iostream> 
#include <windows.h>

using namespace std;

const size_t BUF_SIZE = 256;

BOOL CALLBACK myLocalesProc(LPTSTR lpLocaleString, DWORD flags, LPARAM lParam) {
    wcout << lpLocaleString << endl;
    return true;
}

BOOL CALLBACK myCodepagesProc(LPTSTR lpCodePageString) {
    wcout << lpCodePageString << endl;
    return true;
}


int main() {
    WCHAR buf[BUF_SIZE];

    if (GetUserDefaultLocaleName(buf, BUF_SIZE) == 0) {
        cerr << "GetUserDefaultLocaleName() failed" << endl;
        return 1;
    }
    wcout << L"User default locale: " << buf << endl;

    if (GetSystemDefaultLocaleName(buf, BUF_SIZE) == 0) {
        cerr << "GetSystemDefaultLocaleName() failed" << endl;
        return 1;
    }
    wcout << L"System default locale: " << buf << endl;


    if (!EnumSystemLocalesEx(myLocalesProc, LOCALE_ALL, 0, NULL)) {
        cerr << "EnumSystemLocalesEx() failed" << endl;
        return 1;
    }

    if (!EnumSystemCodePages(myCodepagesProc, CP_INSTALLED)) {
        cerr << "EnumSystemCodePages() failed" << endl;
        return 1;
    }
}

А вот с линуксом засада какая-то. В консоли команда locale дает нужную информацию. Но как это сделать программно я не понял. Попробовал покопать исходники locale — жуть и мрак. Эта программа входит в пакет glibc, а там одно на другое завязано и выдрать ее отдельно не получается. Окончательно завяз на том, что отсутствует один из заголовков: видимо он генерируется при построении всего пакета.

Если нераспознаваемую локаль подсовываешь, setlocale игнорирует

setlocale возвращает ошибку,
которую в нубокнигах не считают нужной обрабатывать.

Ну да, вместо указателя на строку, возвращает NULL, и даже errno не выставляет. Сиди и гадай, что ей не понравилось в названии локали.

Оно и понятно. В разных ОС локали имеют разные названия.
Как-то стандартизировать эту шнягу будет не просто.

На самом деле, ничего сложного. Просто этим вопросом никто не занимается.
Вот, например, в линуксе русская локаль называется «ru_RU», а в винде «ru-RU». Ну почему по-разному?! Разве сложно закрепить стандартом (или хотя бы RFC) формат наименования локалей? Или такое уже есть, но Майкрософт, как всегда, забил на всех большой болт?

Как я вижу, чем больше я ковыряюсь в этом вопросе, тем дальше от основной цели, ради чего всё и затевалось. Видимо надо забить на это. А то получится, что модуль чтения/записи файла данных будет весить больше, чем вся остальная программа :( Ради теоретической возможности прочитать информацию из файла под другой осью.

то в файле видно, что все символы в двухбайтовой кодировке.

Я Вам уже писал, что в windows wchar_t — 2 байта.

под линуксом латиница 1 байт на символ, кириллица — 2 байта на символ.

Под линуском для символов в подавляющем
большинстве используется мультибайтная кодировка utf-8,
отсюда и разница в количестве байт.

под виндой и латиница и кириллица — по 1 байту на символ.

А в локализованной windows cp1251 используется,
но никто не мешает сохранять в utf-8.

а в винде «ru-RU»

Или не так.

Разве сложно закрепить стандартом (или хотя бы RFC) формат наименования локалей? Или такое уже есть, но Майкрософт, как всегда, забил на всех большой болт?

В POSIX он есть и под Linux он соблюдается.
Майкрософт, как всегда, идет своей дорогой,
у них свои стандарты именования локалей,
но, под линуксом достаточно поставить дефолтную локаль — «»,
а дальше конвертировать всё в wchar_t и работать. :)
Как раз недавно писал упрощенный парсер markdown
и форматированный вывод в терминал.
Если бы не некоторые требования к приложению,
которые обязывают юзать линукс,
то приложение кроссплатформенное.

Ради теоретической возможности прочитать информацию из файла под другой осью.

Пусть Ваше приложение работает с одной кодировкой — utf-8.
Внутри конвертируется в wchar_t.
Если кодировка файла не utf-8,
то либо белиберда будет,
либо можно распознать, что кодировка не такая,
и выдать ошибку пользователю,
либо попытаться программно определить кодировку
и заюзать соответствующую конвертацию.

Ответить

Вы можете использовать разметку markdown для оформления комментариев и постов. Используйте функцию предпросмотра для проверки корректности разметки.

Пожалуйста, оформляйте исходный код в соответствии с правилами разметки. Для того, чтобы вставить код в комментарий, скопируйте его в текстовое поле ниже, после чего выделите то, что скопировали и нажмите кнопку «код» в панели инструментов. Иначе ваш код может принять нечитаемый вид.

Либо производите оформление кода вручную, следующим образом:

``` #include <iostream> using namespace std; int main() { // ... } ```

Предпросмотр сообщения

Ваше сообщение пусто.