Implémentation Windows simple de la commande cat

Aug 19 2020

Sous Linux, il existe la catcommande qui produit des fichiers concaténés, mais sous Windows, une telle commande n'existe pas. En conséquence, j'ai décidé d'essayer de recréer une version simple de celui-ci, mais avec un défi qui était que je ne pouvais utiliser aucune partie de la bibliothèque d'exécution 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);
}
```

Réponses

4 G.Sliepen Aug 22 2020 at 21:22

Évitez de convertir les arguments de ligne de commande en ASCII

Il n'y a aucune bonne raison de convertir les arguments de ligne de commande en ASCII. Toutes les fonctions que vous utilisez qui prennent des pointeurs vers des chaînes ASCII ont également des variantes qui gèrent des chaînes larges, par exemple lstrcmpW()et CreateFileW(). De cette façon, vous pouvez vous en débarrasser CommandLineToArgvA().

Utilisez stderrpour signaler des erreurs

Considérez qu'il n'est pas improbable que l'utilisateur de votre catimplémentation redirige la sortie standard vers un autre fichier. S'il y a une erreur, au lieu de l'imprimer sur la console, vous écrivez le message d'erreur dans ce fichier à la place. Ajoutez simplement stderr = GetStdHandle(STD_ERROR_HANDLE)et utilisez-le pour les messages d'erreur.

Évitez d'allouer une mémoire tampon aussi grande que chaque fichier d'entrée

L'espace disque est généralement au moins un ordre de grandeur plus grand que la RAM. Si vous voulez cat un fichier plus grand que la quantité de RAM disponible, votre programme échouera. Il est préférable d'allouer un tampon avec une taille fixe de 64 Kio, par exemple, et d'utiliser plusieurs appels ReadFile()si nécessaire pour lire l'entrée sous forme de blocs allant jusqu'à 64 Kio. D'une part, cela signifie plus de temps système dû à plusieurs appels à ReadFile(), d'autre part, vous resterez probablement dans le cache L2 de votre CPU. Dans tous les cas, je m'attends à ce que les performances ne soient pas radicalement modifiées par cela, mais maintenant votre programme gère des fichiers de taille arbitraire.

Cela simplifiera également votre code: vous n'avez plus besoin d'obtenir la taille du fichier et de redimensionner le tampon si nécessaire. Au lieu de cela, lisez simplement jusqu'à ce que vous atteigniez la fin du fichier .

Utilisez une boucle pour lire stdinjusqu'à ce que vous atteigniez EOF

Si vous spécifiez -comme argument, vous ne lisez que jusqu'à 2 048 octets stdinavant de passer à l' argument de ligne de commande suivant. Et si vous ne spécifiez aucun argument, vous avez une boucle infinie qui lit stdin, même s'il n'y a plus rien à lire.

Gardez à l'esprit que cela stdinpeut également avoir été redirigé et lira en fait à partir d'un fichier, ou lit la sortie d'un autre programme.

Utilisez le même tampon pour stdinque pour les fichiers

Il n'est pas nécessaire d'avoir deux tampons séparés, car vous ne gérez qu'un fichier ou stdinà la fois. Assurez-vous simplement qu'il est suffisamment grand.

Gérer les erreurs de lecture et d'écriture

Les choses peuvent mal tourner. S'il y a une erreur de lecture d'un fichier ou d'écriture dans stdout, vous devez imprimer un message d'erreur dans stderr, puis quitter immédiatement avec un code de sortie différent de zéro. Cela informera l'utilisateur des erreurs. De plus, si votre catimplémentation est utilisée dans un script batch, le code de sortie différent de zéro permettra à ce script de détecter l'erreur, au lieu de continuer aveuglément avec des données non valides.