Implementasi Windows sederhana dari perintah cat

Aug 19 2020

Di Linux ada catperintah yang mengeluarkan file yang digabungkan tetapi di Windows tidak ada perintah seperti itu. Akibatnya saya memutuskan untuk mencoba membuat ulang versi sederhana tetapi dengan tantangan yang saya tidak dapat menggunakan bagian dari perpustakaan runtime 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);
}
```

Jawaban

4 G.Sliepen Aug 22 2020 at 21:22

Hindari mengonversi argumen baris perintah ke ASCII

Tidak ada alasan bagus untuk mengonversi argumen baris perintah ke ASCII. Semua fungsi yang Anda gunakan yang mengarahkan pointer ke string ASCII juga memiliki varian yang menangani string lebar, misalnya lstrcmpW()dan CreateFileW(). Dengan cara ini, Anda bisa menyingkirkan CommandLineToArgvA().

Gunakan stderruntuk melaporkan kesalahan

Pertimbangkan bahwa bukan tidak mungkin pengguna catimplementasi Anda mengalihkan output standar ke file lain. Jika ada kesalahan, alih-alih mencetaknya ke konsol, Anda menulis pesan kesalahan ke file itu sebagai gantinya. Cukup tambahkan stderr = GetStdHandle(STD_ERROR_HANDLE), dan gunakan itu untuk pesan kesalahan.

Hindari mengalokasikan buffer sebesar setiap file input

Ruang disk biasanya setidaknya urutan besarnya lebih besar dari RAM. Jika Anda ingin membuat file lebih besar dari jumlah RAM kosong yang tersedia, program Anda akan gagal. Lebih baik mengalokasikan buffer dengan ukuran tetap katakanlah 64 KiB, dan gunakan beberapa panggilan ke ReadFile()jika perlu untuk membaca input sebagai potongan hingga 64 KiB. Di satu sisi, ini berarti lebih banyak overhead dari beberapa panggilan ke ReadFile(), di sisi lain Anda kemungkinan besar akan tetap berada dalam cache L2 CPU Anda. Bagaimanapun, saya berharap kinerja tidak akan berubah secara dramatis oleh ini, tetapi sekarang program Anda menangani file berukuran sewenang-wenang.

Ini juga akan menyederhanakan kode Anda: Anda tidak lagi harus mendapatkan ukuran file dan mengubah ukuran buffer jika perlu. Sebaliknya, baca saja sampai Anda mencapai akhir file .

Gunakan putaran untuk membaca dari stdinsampai Anda mencapai EOF

Jika Anda menentukan -sebagai argumen, Anda hanya membaca hingga 2048 byte dari stdinsebelum melanjutkan ke argumen baris perintah berikutnya. Dan jika Anda tidak menentukan argumen sama sekali, Anda memiliki loop tak terbatas yang membaca dari stdin, bahkan jika tidak ada lagi untuk dibaca.

Ingatlah bahwa stdinmungkin juga telah dialihkan, dan akan benar-benar membaca dari file, atau membaca keluaran dari program lain.

Gunakan buffer yang sama stdinseperti untuk file

Tidak perlu memiliki dua buffer terpisah, karena Anda hanya menangani file atau stdindalam satu waktu. Pastikan saja ukurannya cukup besar.

Tangani kesalahan baca dan tulis

Ada yang bisa salah. Jika ada kesalahan membaca file atau menulis ke stdout, Anda harus mencetak pesan kesalahan ke stderrdan kemudian segera keluar dengan kode keluar bukan nol. Ini akan memberi tahu pengguna tentang kesalahan. Selain itu, jika catimplementasi Anda digunakan dalam skrip batch, kode keluar bukan nol akan memungkinkan skrip tersebut mendeteksi kesalahan, alih-alih melanjutkan secara membabi buta dengan data yang tidak valid.