Implementación simple de Windows del comando cat

Aug 19 2020

En Linux existe el catcomando que genera archivos concatenados, pero en Windows no existe tal comando. Como resultado, decidí intentar recrear una versión simple pero con el desafío de que no podía usar ninguna parte de la biblioteca de tiempo de ejecución de 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);
}
```

Respuestas

4 G.Sliepen Aug 22 2020 at 21:22

Evite convertir argumentos de línea de comando a ASCII

No hay una buena razón para convertir los argumentos de la línea de comandos a ASCII. Todas las funciones que usa que llevan punteros a cadenas ASCII también tienen variantes que manejan cadenas anchas, por ejemplo lstrcmpW()y CreateFileW(). De esta manera, puede deshacerse de CommandLineToArgvA().

Úselo stderrpara informar errores

Tenga en cuenta que no es improbable que el usuario de su catimplementación redirija la salida estándar a otro archivo. Si hay un error, en lugar de imprimirlo en la consola, está escribiendo el mensaje de error en ese archivo. Simplemente agregue stderr = GetStdHandle(STD_ERROR_HANDLE)y utilícelo para los mensajes de error.

Evite asignar un búfer tan grande como cada archivo de entrada

El espacio en disco suele ser al menos un orden de magnitud mayor que la RAM. Si desea capturar un archivo más grande que la cantidad de RAM libre disponible, su programa fallará. Es mejor asignar un búfer con un tamaño fijo de, por ejemplo, 64 KiB, y usar múltiples llamadas ReadFile()si es necesario para leer la entrada como fragmentos de hasta 64 KiB. Por un lado, significa más gastos generales de múltiples llamadas a ReadFile(), por otro lado, es probable que permanezca dentro de la caché L2 de su CPU. En cualquier caso, espero que el rendimiento no cambie drásticamente por esto, pero ahora su programa maneja archivos de tamaño arbitrario.

Esto también simplificará su código: ya no tendrá que obtener el tamaño del archivo y cambiar el tamaño del búfer si es necesario. En su lugar, simplemente lea hasta llegar al final del archivo .

Use un bucle para leer stdinhasta llegar a EOF

Si lo especifica -como un argumento, solo leerá hasta 2048 bytes stdinantes de continuar con el siguiente argumento de línea de comando. Y si no especifica ningún argumento, tiene un bucle infinito desde el que se lee stdin, incluso si ya no hay nada para leer.

Tenga en cuenta que stdintambién podría haber sido redirigido y, de hecho, leerá de un archivo o leerá la salida de otro programa.

Utilice el mismo búfer stdinpara archivos

No es necesario tener dos búferes separados, ya que solo maneja un archivo o stdinal mismo tiempo. Solo asegúrate de que sea lo suficientemente grande.

Manejar errores de lectura y escritura

Las cosas pueden salir mal. Si hay un error al leer un archivo o al escribir en stdout, debe imprimir un mensaje de error stderry luego salir inmediatamente con un código de salida distinto de cero. Esto notificará al usuario de los errores. Además, si su catimplementación se utiliza en un script por lotes, el código de salida distinto de cero permitirá que el script detecte el error, en lugar de continuar ciegamente con datos no válidos.