Implementação simples do comando cat no Windows

Aug 19 2020

No Linux, existe o catcomando que produz arquivos concatenados, mas no Windows não existe tal comando. Como resultado, decidi tentar recriar uma versão simples dele, mas com o desafio de não poder usar nenhuma parte da biblioteca C runtime.

#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);
}
```

Respostas

4 G.Sliepen Aug 22 2020 at 21:22

Evite converter argumentos de linha de comando em ASCII

Não há um bom motivo para converter os argumentos da linha de comando em ASCII. Todas as funções que você usa que levam ponteiros para strings ASCII também têm variantes que tratam de strings largas, por exemplo lstrcmpW()e CreateFileW(). Dessa forma, você pode se livrar CommandLineToArgvA().

Use stderrpara relatar erros

Considere que não é improvável que o usuário de sua catimplementação redirecione a saída padrão para outro arquivo. Se houver um erro, em vez de imprimi-lo no console, você gravará a mensagem de erro nesse arquivo. Basta adicionar stderr = GetStdHandle(STD_ERROR_HANDLE)e usar para as mensagens de erro.

Evite alocar um buffer tão grande quanto cada arquivo de entrada

O espaço em disco é normalmente pelo menos uma ordem de magnitude maior do que a RAM. Se você quiser catar um arquivo maior do que a quantidade de RAM disponível, seu programa falhará. É melhor alocar um buffer com um tamanho fixo de, digamos, 64 KiB e usar várias chamadas para, ReadFile()se necessário, ler a entrada como blocos de até 64 KiB. Por um lado, isso significa mais sobrecarga de várias chamadas para ReadFile(), por outro lado, você provavelmente permanecerá no cache L2 de sua CPU. Em qualquer caso, espero que o desempenho não seja alterado drasticamente por isso, mas agora seu programa lida com arquivos de tamanhos arbitrários.

Isso também simplificará seu código: você não precisa mais obter o tamanho do arquivo e redimensionar o buffer, se necessário. Em vez disso, apenas leia até chegar ao final do arquivo .

Use um loop para ler stdinaté chegar ao EOF

Se você especificar -como um argumento, você lerá apenas até 2048 bytes stdinantes de continuar para o próximo argumento da linha de comando. E se você não especificar nenhum argumento, terá um loop infinito que lê stdin, mesmo que não haja mais nada para ler.

Lembre-se de que stdintambém pode ter sido redirecionado e, na verdade, ler de um arquivo ou ler a saída de outro programa.

Use o mesmo buffer stdinpara os arquivos

Não há necessidade de ter dois buffers separados, pois você só lida com um arquivo ou stdinpor vez. Apenas certifique-se de que é grande o suficiente.

Lidar com erros de leitura e gravação

As coisas podem dar errado. Se houver um erro ao ler ou gravar um arquivo stdout, você deve imprimir uma mensagem de erro stderre sair imediatamente com um código de saída diferente de zero. Isso notificará o usuário sobre erros. Além disso, se sua catimplementação for usada em um script em lote, o código de saída diferente de zero permitirá que o script detecte o erro, em vez de continuar cegamente com dados inválidos.