Einfache Windows-Implementierung des Befehls cat

Aug 19 2020

Unter Linux gibt es den catBefehl, der verkettete Dateien ausgibt, unter Windows gibt es keinen solchen Befehl. Infolgedessen habe ich mich entschlossen, eine einfache Version davon neu zu erstellen, aber mit der Herausforderung, dass ich keinen Teil der C-Laufzeitbibliothek verwenden konnte.

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

Antworten

4 G.Sliepen Aug 22 2020 at 21:22

Vermeiden Sie die Konvertierung von Befehlszeilenargumenten in ASCII

Es gibt keinen guten Grund, die Befehlszeilenargumente in ASCII zu konvertieren. Alle Funktionen , die Sie das nehmen Zeiger auf ASCII - Strings verwenden auch Varianten, die breite Strings verarbeiten, zum Beispiel lstrcmpW()und CreateFileW(). Auf diese Weise können Sie loswerden CommandLineToArgvA().

Verwenden Sie stderrdiese Option , um Fehler zu melden

Beachten Sie, dass es nicht unwahrscheinlich ist, dass der Benutzer Ihrer catImplementierung die Standardausgabe in eine andere Datei umleitet. Wenn ein Fehler auftritt, schreiben Sie stattdessen die Fehlermeldung in diese Datei, anstatt sie auf die Konsole zu drucken. Fügen Sie einfach hinzu stderr = GetStdHandle(STD_ERROR_HANDLE)und verwenden Sie dies für die Fehlermeldungen.

Vermeiden Sie es, einen Puffer zuzuweisen, der so groß ist wie jede Eingabedatei

Der Speicherplatz ist normalerweise mindestens eine Größenordnung größer als der Arbeitsspeicher. Wenn Sie eine Datei katzen möchten, die größer ist als der verfügbare freie Arbeitsspeicher, schlägt Ihr Programm fehl. Es ist besser, einen Puffer mit einer festen Größe von beispielsweise 64 KiB zuzuweisen und bei Bedarf mehrere Aufrufe zu verwenden ReadFile(), um die Eingabe als Blöcke von bis zu 64 KiB zu lesen. Einerseits bedeutet dies mehr Overhead von mehreren Aufrufen an ReadFile(), andererseits bleiben Sie wahrscheinlich im L2-Cache Ihrer CPU. Auf jeden Fall erwarte ich, dass sich die Leistung dadurch nicht dramatisch ändert, aber jetzt verarbeitet Ihr Programm Dateien mit beliebiger Größe.

Dies vereinfacht auch Ihren Code: Sie müssen nicht mehr die Dateigröße abrufen und die Größe des Puffers bei Bedarf ändern. Lesen Sie stattdessen einfach, bis Sie das Ende der Datei erreicht haben .

Verwenden Sie eine Schleife, um von stdinbis zu Ihrer EOF zu lesen

Wenn Sie -als Argument angeben , lesen Sie nur bis zu 2048 Byte, stdinbevor Sie mit dem nächsten Befehlszeilenargument fortfahren. Und wenn Sie überhaupt keine Argumente angeben, haben Sie eine Endlosschleife, aus der gelesen wird stdin, auch wenn nichts mehr zu lesen ist.

Beachten Sie, dass dies stdinmöglicherweise auch umgeleitet wurde und tatsächlich aus einer Datei oder der Ausgabe eines anderen Programms liest.

Verwenden Sie den gleichen Puffer stdinwie für Dateien

Es sind keine zwei separaten Puffer erforderlich, da Sie nur eine Datei oder stdingleichzeitig bearbeiten . Stellen Sie einfach sicher, dass es groß genug ist.

Behandeln Sie Lese- und Schreibfehler

Dinge können schief gehen. Wenn beim Lesen oder Schreiben einer Datei ein Fehler auftritt stdout, sollten Sie eine Fehlermeldung an drucken stderrund dann sofort mit einem Exit-Code ungleich Null beenden. Dies benachrichtigt den Benutzer über Fehler. Wenn Ihre catImplementierung in einem Batch-Skript verwendet wird, ermöglicht das Exit-Code ungleich Null diesem Skript, den Fehler zu erkennen, anstatt blind mit ungültigen Daten fortzufahren.