Простая реализация команды cat в Windows

Aug 19 2020

В Linux есть catкоманда, которая выводит объединенные файлы, но в Windows такой команды нет. В результате я решил попытаться воссоздать простую его версию, но с проблемой, которая заключалась в том, что я не мог использовать какую-либо часть библиотеки времени выполнения C.

#include <windows.h>

/* global variables */
HANDLE stdout = NULL;
HANDLE stdin = NULL;
char *input_buffer = NULL;
CONSOLE_READCONSOLE_CONTROL crc = { .nLength = sizeof(crc), .dwCtrlWakeupMask = 1 << '\n' };
char *output_buffer = NULL;
DWORD output_capacity = 0;

/* There is only CommandLineToArgvW so a version for ascii is needed */
LPSTR *CommandLineToArgvA(LPWSTR lpWideCmdLine, INT *pNumArgs)
{
    int retval;
    int numArgs;
    LPWSTR *args;
    args = CommandLineToArgvW(lpWideCmdLine, &numArgs);
    if (args == NULL)
        return NULL;

    int storage = numArgs * sizeof(LPSTR);
    for (int i = 0; i < numArgs; ++i) {
        BOOL lpUsedDefaultChar = FALSE;
        retval = WideCharToMultiByte(CP_ACP, 0, args[i], -1, NULL, 0, NULL, &lpUsedDefaultChar);
        if (!SUCCEEDED(retval)) {
            LocalFree(args);
            return NULL;
        }

        storage += retval;
    }

    LPSTR *result = (LPSTR *)LocalAlloc(LMEM_FIXED, storage);
    if (result == NULL) {
        LocalFree(args);
        return NULL;
    }

    int bufLen = storage - numArgs * sizeof(LPSTR);
    LPSTR buffer = ((LPSTR)result) + numArgs * sizeof(LPSTR);
    for (int i = 0; i < numArgs; ++i) {
        BOOL lpUsedDefaultChar = FALSE;
        retval = WideCharToMultiByte(CP_ACP, 0, args[i], -1, buffer, bufLen, NULL, &lpUsedDefaultChar);
        if (!SUCCEEDED(retval)) {
            LocalFree(result);
            LocalFree(args);
            return NULL;
        }

        result[i] = buffer;
        buffer += retval;
        bufLen -= retval;
    }

    LocalFree(args);

    *pNumArgs = numArgs;
    return result;
}


static void lmemcpy(char *dest, const char *src, DWORD len)
{
    /* copy 4 bytes at once */
    for (; len > 3; len -= 4, dest += 4, src += 4)
        *(long *)dest = *(long *)src;
    while (len--)
        *dest++ = *src++;
}

static void catstdin(void)
{
    DWORD chars_read = 0;
    ReadConsoleA(stdin, input_buffer, 2048, &chars_read, &crc);
    WriteConsoleA(stdout, input_buffer, chars_read, NULL, NULL);
}

static void catfile(char *filepath)
{
    HANDLE filehandle = CreateFileA(filepath, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (filehandle == INVALID_HANDLE_VALUE) {
        WriteConsoleA(stdout, "Error could not open file: ", 27, NULL, NULL);
        WriteConsoleA(stdout, filepath, lstrlenA(filepath), NULL, NULL);
        ExitProcess(GetLastError());
    }
    DWORD filelength = GetFileSize(filehandle, NULL);
    if (filelength > output_capacity) { /* see if we need to allocate more memory */
        char *new_buffer = HeapAlloc(GetProcessHeap(), 0, filelength * 2); /* copy the data from the old memory to the new memory */
        lmemcpy(new_buffer, output_buffer, output_capacity);
        HeapFree(GetProcessHeap(), 0, output_buffer); /* free old memory */
        output_capacity = filelength * 2;
        output_buffer = new_buffer;
    }

    ReadFile(filehandle, output_buffer, filelength, NULL, NULL);
    WriteConsoleA(stdout, output_buffer, filelength, NULL, NULL);
    CloseHandle(filehandle); /* close file */
}

void __cdecl mainCRTStartup(void)
{
    /* setup global variables */
    stdout = GetStdHandle(STD_OUTPUT_HANDLE);
    stdin = GetStdHandle(STD_INPUT_HANDLE);
    input_buffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 2048);
    output_buffer = HeapAlloc(GetProcessHeap(), 0, 2048);
    output_capacity = 2048;

    /* get argc and argv */
    int argc;
    char **argv = CommandLineToArgvA(GetCommandLineW(), &argc) + 1;
    argc--; /* the first arg is always the program name */

    switch (argc) {
        case 0:
            for (;;) catstdin();
            break;
        default:
            for (int i = 0; i < argc; ++i) {
                if (!lstrcmpA(argv[i], "-"))
                    catstdin();
                else
                    catfile(argv[i]);
            }
    }

    /* free memory */
    HeapFree(GetProcessHeap(), 0, input_buffer);
    HeapFree(GetProcessHeap(), 0, output_buffer);
    LocalFree(argv);

    /* exit */
    ExitProcess(0);
}
```

Ответы

4 G.Sliepen Aug 22 2020 at 21:22

Избегайте преобразования аргументов командной строки в ASCII

Нет веских причин для преобразования аргументов командной строки в ASCII. Все функции, которые вы используете, которые принимают указатели на строки ASCII, также имеют варианты, которые обрабатывают широкие строки, например lstrcmpW()и CreateFileW(). Таким образом, вы можете избавиться от CommandLineToArgvA().

Используйте stderrдля сообщения об ошибках

Учтите, что вполне вероятно, что пользователь вашей catреализации перенаправит стандартный вывод в другой файл. Если есть ошибка, вместо вывода ее на консоль вы записываете сообщение об ошибке в этот файл. Просто добавьте stderr = GetStdHandle(STD_ERROR_HANDLE)и используйте это для сообщений об ошибках.

Избегайте выделения буфера размером с каждый входной файл

Дисковое пространство обычно как минимум на порядок больше, чем ОЗУ. Если вы хотите скопировать файл, размер которого превышает объем доступной свободной оперативной памяти, ваша программа завершится ошибкой. Лучше выделить буфер фиксированного размера, скажем, 64 КиБ, и использовать несколько вызовов, ReadFile()если необходимо, для чтения ввода в виде фрагментов размером до 64 КиБ. С одной стороны, это означает больше накладных расходов из-за нескольких вызовов ReadFile(), с другой стороны, вы, скорее всего, останетесь в кэше L2 вашего процессора. В любом случае, я ожидаю, что производительность при этом не изменится кардинально, но теперь ваша программа обрабатывает файлы произвольного размера.

Это также упростит ваш код: вам больше не нужно получать размер файла и при необходимости изменять размер буфера. Вместо этого просто читайте, пока не дойдете до конца файла .

Используйте цикл для чтения, stdinпока не достигнете EOF

Если вы укажете -в качестве аргумента, вы прочитаете только до 2048 байтов, stdinпрежде чем перейти к следующему аргументу командной строки. А если вы вообще не укажете никаких аргументов, у вас будет бесконечный цикл, из которого выполняется чтение stdin, даже если читать больше нечего.

Имейте в виду, что stdinэто также могло быть перенаправлено и на самом деле будет читать из файла или считывать вывод из другой программы.

Используйте тот же буфер, stdinчто и для файлов

Нет необходимости иметь два отдельных буфера, поскольку вы обрабатываете только файл или stdinодновременно. Просто убедитесь, что он достаточно большой.

Обработка ошибок чтения и записи

Что-то может пойти не так. Если при чтении файла или записи в stdoutнего произошла ошибка, вы должны распечатать сообщение об ошибке, stderrа затем немедленно выйти с ненулевым кодом выхода. Это уведомит пользователя об ошибках. Кроме того, если ваша catреализация используется в пакетном сценарии, ненулевой код выхода позволит этому сценарию обнаружить ошибку вместо того, чтобы вслепую продолжать работу с недопустимыми данными.