Захват порожденного процесса stdout как unicode

В моем коде C++/WinAPI я хочу запустить некоторые команды и зафиксировать их вывод. Чтобы проверить вывод, отличный от ASCII, я переименовал свое сетевое соединение в Ethérnét אבג БбГгДд и запустил ipconfig. При запуске в командной строке выходные данные отображаются правильно (видно при использовании поддерживающего шрифта, такого как Courier New):

C:\>ipconfig
Windows IP Configuration

Ethernet adapter Ethérnét אבג БбГгДд:
(...)

Я попытался перенаправить вывод в канал, следуя примеру в этом ответе. Но массив байтов, возвращенный из ReadFile(), не является юникодом — он закодирован в CP_OEMCP (в моем случае CP437), поэтому символы иврита и русского языка отображаются как «?». Поскольку символы уже потеряны, никакая дальнейшая обработка не может их восстановить.

Очевидно, это возможно, так как это делает cmd в окне консоли. Как мне это сделать?


person Jonathan    schedule 03.01.2017    source источник
comment
ReadFile возвращает байты, он понятия не имеет, что такое Unicode. Покажите, как вы обрабатываете его буфер.   -  person Alex K.    schedule 03.01.2017
comment
Я просмотрел возвращенные байты из отладчика, и они представляют собой текст, закодированный в CP437, с заменой еврейских/русских символов на настоящие '?'. Поскольку символы потеряны, никакая обработка не восстановит это. Я хотел знать, как cmd.exe (или окно консоли?) Умеет правильно фиксировать эти символы.   -  person Jonathan    schedule 03.01.2017
comment
поэтому конвертируйте его в юникод на MultiByteToWideChar(CP_OEMCP, - символы не теряются   -  person RbMm    schedule 03.01.2017
comment
Это то, что я делаю сейчас. Однако, поскольку CP_OEMCP не может кодировать все символы — например, иврит + русский в моем примере — они отображаются как настоящие знаки «?», и преобразование не может их восстановить, поскольку они потеряны.   -  person Jonathan    schedule 03.01.2017
comment
CP_OEMCP can't encode all characters - ты в этом уверен? я думаю, ты ошибаешься здесь   -  person RbMm    schedule 03.01.2017
comment
никаких потерь в конверсии - думаю, что вы просто неправильно отображаете данные   -  person RbMm    schedule 03.01.2017
comment
@RbMm См. список символов в CP437. Здесь нет ни иврита, ни русских символов. MultiByteToWideChar не волшебным образом восстановит персонажей, которых нет в этом списке.   -  person roeland    schedule 04.01.2017
comment
@roeland - вы пробуете код, который я вставляю? попробуйте использовать MultiByteToWideChar(CP_OEMCP - ? какой код вы используете - опять же, ничего не потеряно в многобайтовых преобразованиях   -  person RbMm    schedule 04.01.2017
comment
ipconfig.exe использовал WriteConsoleW для вывода на консоль - в результате всегда правильный вывод на любых языках и не зависит от текущих кодовых страниц. если приложение использует функции A или записывает в файл как многобайтовые, возникнет проблема, если попытаться напечатать символы, которые не существуют при использовании кодовой страницы   -  person RbMm    schedule 04.01.2017
comment
@RbMm Если вы получаете данные, закодированные в наборе символов OEM, символы уже потеряны, и ваша программа ничего не может сделать для их восстановления. Например. дочерний процесс выводит "αβ", который затем сокращается до набора символов OEM (вероятно, что-то вроде "ab") и только потом передается вашей программе.   -  person roeland    schedule 04.01.2017
comment
@roeland - насколько я понимаю, когда перечитал вопрос OP, он использовал CP437 - с этим WideCharToMultiByte(CP_OEMCP) действительно потеряли данные для иврита и русских символов. ipconfig.exe однако используйте функцию UNICODE для записи в консоль - в результате текст отображается правильно.   -  person RbMm    schedule 04.01.2017
comment
@RbMm: Совершенно очевидно, что набор символов, содержащий всего 256 символов, не может использоваться для кодирования всех 100 000+ символов Unicode.   -  person MSalters    schedule 04.01.2017
comment
@MSalters - нет, потому что WideCharToMultiByte сопоставляет 1 символ Юникода с несколькими (обычными 2) многобайтными символами для не en - поэтому у нас не 256, а 256*256   -  person RbMm    schedule 04.01.2017
comment
точнее, когда мы используем CP_ACP или CP_OEMCP, у нас есть один к одному по len unicode в многобайтовый, но в случае CP_UTF8 - обычный один не 'en' wchar преобразуется в 2 char   -  person RbMm    schedule 04.01.2017
comment
@RbMm: это предполагает, что CP_ACP и CP_OEM на самом деле многобайтовые. Возможно, но редко, а если CP_OEM — это обычный CP437, то он однобайтный.   -  person MSalters    schedule 04.01.2017
comment
@MSalters — CP_ACP и CP_OEM переводят символы Unicode на выбранную страницу. он использует преобразование один к одному символу. если скажем, мы используем Hebrew страницу - мы можем перевести (без потери данных) символы иврита и английского языка, но не русский или другой язык   -  person RbMm    schedule 04.01.2017


Ответы (2)


Казалось бы, ipconfig производит вывод в формате Unicode, когда обнаруживает, что устройством вывода является консоль, и в противном случае выводит ANSI. Вероятно, это мера обратной совместимости.

Большинство других встроенных инструментов командной строки, вероятно, будут либо только ANSI, либо будут вести себя так же, как ipconfig, по той же причине. В Windows инструменты командной строки предназначены для использования в командной строке; программистам не рекомендуется раскошеливаться на них и анализировать вывод. Вместо этого следует использовать соответствующие API.

Если вы знаете, какой язык вы ожидаете, вы можете выбрать кодовую страницу, которая сохранит содержимое.

Добавлено @Jonathan: Недокументировано: Оказывается, вы можете управлять кодировкой встроенных команд, используя переменную среды OutputEncoding. Я тестировал с помощью ipconfig, но предположительно он работает и с другими встроенными инструментами:

> for %e in ("" Unicode Ansi UTF8) do (set OutputEncoding=%~e& ipconfig >ipconfig-%~e.txt)
> (set OutputEncoding=  & ipconfig  1>ipconfig-.txt )
> (set OutputEncoding=Unicode  & ipconfig  1>ipconfig-Unicode.txt )
> (set OutputEncoding=Ansi  & ipconfig  1>ipconfig-Ansi.txt )
> (set OutputEncoding=UTF8  & ipconfig  1>ipconfig-UTF8.txt )

И действительно, ipconfig-*.txt зашифрованы как положено! Обратите внимание, что это недокументировано, но это работает для меня.

Дополнение: начиная с Windows 10 версии 1809 другой альтернативой является создание псевдоконсоль.

person Harry Johnston    schedule 04.01.2017
comment
Что объясняет его. Я просмотрел ipconfig и добавил свои выводы к ответу. Я бы хотел, чтобы мы могли установить CP_OEMCP в CP_UTF8 (и CP_ACP тоже)... - person Jonathan; 04.01.2017
comment
@Jonathan, опубликованный вами фрагмент кода доступен только в том случае, если вывод осуществляется на консоль, он не имеет отношения к случаю, когда вывод был перенаправлен в канал. Однако интересно, что именно библиотека времени выполнения C отвечает за преобразование из UTF-16 в текущую локаль. Из того, что я вижу в исходном коде CRT, для этого используется wcstomb_s, хотя я смотрю на Visual Studio CRT, не совсем такой, как встроенный в Windows. К сожалению, похоже, нет никакого способа заставить CRT генерировать UTF-8. - person Harry Johnston; 05.01.2017
comment
Действительно, мой код был неактуален. Однако я обнаружил, что преобразование происходит внутри ipconfig.exe — и вы можете управлять кодовой страницей, используя недокументированную переменную OutputEncoding env. Я добавлю образец к вашему ответу. - person Jonathan; 05.01.2017
comment
Аккуратная находка! (Возможно, это стоит опубликовать как отдельный ответ, я бы, например, проголосовал за него.) Любопытно, что строка OutputEncoding не появляется в исходном коде CRT Visual Studio 2010 или в msvcrt.dll, если уж на то пошло, но появляется в shell32.dll что заставляет меня думать, что это может быть что-то, что делает операционная система, а не ЭЛТ. Впрочем, детали не имеют особого значения. - person Harry Johnston; 06.01.2017
comment
Правильно - OutputEncoding происходит в ipconfig.exe, а не в msvcrt - вы можете увидеть это, используя строки SysInternal. По-видимому, это относится только к некоторым инструментам - netstat.exe, но не к robocopy.exe. - person Jonathan; 07.01.2017

консольное приложение может использовать разные способы вывода.

  • для дескриптора консоли мы можем использовать WriteConsoleW для вывода уже в UNICODE.
  • если мы хотим использовать WriteConsoleA или WriteFile для дескриптора консоли необходимо сначала преобразовать UNICODE текст в несколько байтов с помощью WideCharToMultiByte с CodePage := GetConsoleOutputCP()
  • если у нас изначально не UNICODE текст для вывода (скажем, UTF-8 или Ansi), необходимо сначала преобразовать его в UNICODE с помощью MultiByteToWideCharCP_UTF8 или CP_ACP) и потом уже снова конвертировать в многобайтный WideCharToMultiByte(GetConsoleOutputCP(), ..)

обычный (по умолчанию) GetConsoleOutputCP()< /a> возвращает то же значение, что и GetOEMCP(), так что тот же эффект будет в MultiByteToWideChar и WideCharToMultiByte как CP_OEMCP (это постоянное значение преобразуется в GetOEMCP() )

когда выходной дескриптор перенаправляется в файл, нужно использовать только WriteFile. однако приложение может записывать данные в файл в любом формате: UNICODE, Ansi (CP_ACP), UTF-8 (CP_UTF8) и т. д. какой формат будет использоваться - очень зависит от конкретного приложения. вы не можете полностью контролировать это. обычно вы получите многобайтовый вывод в кодировке CP_OEMCP. затем вам нужно решить, как его обработать - быстрее всего вам нужно будет сначала преобразовать его в UNICODE и использовать unicode форму. если вам нужно Ansi - вам нужно будет сделать еще одно преобразование.

скажем, если вы попытаетесь использовать вывод канала в кодировке CP_OEMCP с OutputDebugStringA - вы получили вывод ошибки (нечитаемый) для неанглоязычного текста. но после 2 преобразований CP_OEMCP -> UNICODE -> CP_ACP вы можете исправить отображаемый текст с помощью OutputDebugStringA, но потому что OutputDebugStringW существуют - здесь достаточно только UNICODE преобразовать

также в некоторых приложениях есть специальные опции для контроля вывода в формат файла. скажем, ipconfig.exe ищет "OutputEncoding" переменную среды и зависит от ее строкового значения ("Unicode", "Ansi", "UTF-8") для получения другого вывода. по умолчанию (если эта переменная среды не существует или ее значение неизвестно) CP_OEMCP используется

пример процедуры чтения канала. предположим, что входные данные в кодировке CP_OEMCP:

void OnRead(PVOID buf, ULONG cbTransferred)
{
    if (cbTransferred)
    {
        if (int len = MultiByteToWideChar(CP_OEMCP, 0, (PSTR)buf, cbTransferred, 0, 0))
        {
            PWSTR pwz = (PWSTR)alloca((1 + len) * sizeof(WCHAR));

            if (len = MultiByteToWideChar(CP_OEMCP, 0, (PSTR)buf, cbTransferred, pwz, len))
            {
                if (g_bUseAnsi)
                {
                    if (cbTransferred = WideCharToMultiByte(CP_ACP, 0, pwz, len, 0, 0, 0, 0))
                    {
                        PSTR psz = (PSTR)alloca(cbTransferred + 1);

                        if (cbTransferred = WideCharToMultiByte(CP_ACP, 0, pwz, len, psz, cbTransferred, 0, 0))
                        {
                            DoPrint(psz, cbTransferred, OutputDebugStringA);
                        }
                    }
                }
                else
                {
                    DoPrint(pwz, len, OutputDebugStringW);
                }
            }
        }
    }
}

// debugger can incomplete print too big buffer, so split it on small chunks
template<typename T> void DoPrint(T* p, ULONG len, void (WINAPI* fnOutput)(const T*))
{
    ULONG cb;
    T* q = p;
    do 
    {
        cb = min(len, 256);

        q = p + cb;

        T c = *q;

        *q = 0;

        fnOutput(p);

        *q = c;

        p = q;

    } while (len -= cb);
}

о вашем конкретном случае - ipconfig.exe используется WriteConsoleW для вывода на консоль. в результате он не зависит от текущей локали системы и может корректно отображать многоязычный текст. но другие инструменты, такие как route.exe, использовали WriteFile для вывода (как на консоль, так и в файл) и преобразовать перед этим UNICODE текст в многобайтный по WideCharToMultiByte(CP_OEMCP,..) - в результате здесь будут проблемы, если попытаться отобразить символы, которых нет в кодовой странице CP_OEMCP (текущая локаль системы) . если у вас CP437 - еврейские и русские символы будут потеряны, если использовать UNICODE -> CP_OEMCP, нужен только прямой вывод с юникодом на консоль и в файл. возможно ли это - зависит от конкретного приложения. скажем, route.exe это невозможно. для ipconfig.exe это возможно, потому что он всегда записывает в консоль в формате unicode и может записывать в файл также в unicode или utf-8, если вы установите "OutputEncoding" в "Unicode" или "UTF-8"

person RbMm    schedule 03.01.2017
comment
Это не учитывает многобайтовые символы, которые охватывают пакеты. Если IsDBCSLeadByte равен TRUE для конечной единицы кода, преобразование разбивает как этот блок, так и следующий за ним блок байтов. - person IInspectable; 20.02.2017
comment
@IInspectable - что такое сбой? ты о ? - person RbMm; 20.02.2017
comment
Извините, я не понимаю этого языка. - person IInspectable; 20.02.2017