การใช้คำสั่ง cat ของ Windows อย่างง่าย

Aug 19 2020

บน Linux มีcatคำสั่งที่ส่งออกไฟล์ที่เชื่อมต่อกัน แต่ใน Windows ไม่มีคำสั่งดังกล่าว ด้วยเหตุนี้ฉันจึงตัดสินใจที่จะพยายามสร้างเวอร์ชันที่เรียบง่ายขึ้นมาใหม่ แต่ด้วยความท้าทายซึ่งฉันไม่สามารถใช้ส่วนใดส่วนหนึ่งของไลบรารีรันไทม์ 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);
}
```

คำตอบ

4 G.Sliepen Aug 22 2020 at 21:22

หลีกเลี่ยงการแปลงอาร์กิวเมนต์บรรทัดคำสั่งเป็น ASCII

ไม่มีเหตุผลที่ดีในการแปลงอาร์กิวเมนต์บรรทัดคำสั่งเป็น ASCII ฟังก์ชันทั้งหมดที่คุณใช้ซึ่งนำตัวชี้ไปยังสตริง ASCII ยังมีตัวแปรที่จัดการกับสตริงแบบกว้างเช่นlstrcmpW()และCreateFileW(). CommandLineToArgvA()วิธีนี้คุณสามารถกำจัด

ใช้stderrรายงานข้อผิดพลาด

พิจารณาว่าไม่น่าเป็นไปได้ที่ผู้ใช้catการติดตั้งของคุณจะเปลี่ยนเส้นทางเอาต์พุตมาตรฐานไปยังไฟล์อื่น หากมีข้อผิดพลาดแทนที่จะพิมพ์ลงในคอนโซลคุณกำลังเขียนข้อความแสดงข้อผิดพลาดไปยังไฟล์นั้นแทน เพียงเพิ่มstderr = GetStdHandle(STD_ERROR_HANDLE)และใช้สิ่งนั้นสำหรับข้อความแสดงข้อผิดพลาด

หลีกเลี่ยงการจัดสรรบัฟเฟอร์ให้ใหญ่เท่ากับไฟล์อินพุตแต่ละไฟล์

โดยทั่วไปเนื้อที่ดิสก์จะมีขนาดใหญ่กว่า RAM เป็นอย่างน้อย หากคุณต้องการ cat ไฟล์ที่มีขนาดใหญ่กว่าจำนวน RAM ว่างที่มีอยู่โปรแกรมของคุณจะล้มเหลว จะเป็นการดีกว่าที่จะจัดสรรบัฟเฟอร์ที่มีขนาดคงที่คือ 64 KiB และใช้การโทรหลายครั้งReadFile()หากจำเป็นเพื่ออ่านอินพุตเป็นชิ้นส่วนมากถึง 64 KiB ในแง่หนึ่งหมายถึงค่าใช้จ่ายที่มากขึ้นจากการโทรหลายครั้งReadFile()ในทางกลับกันคุณมีแนวโน้มที่จะอยู่ในแคช L2 ของ CPU ของคุณ ไม่ว่าในกรณีใดฉันคาดว่าประสิทธิภาพจะไม่เปลี่ยนแปลงอย่างมากจากสิ่งนี้ แต่ตอนนี้โปรแกรมของคุณจัดการไฟล์ที่มีขนาดตามอำเภอใจ

นอกจากนี้ยังช่วยลดความซับซ้อนของโค้ดของคุณ: คุณไม่จำเป็นต้องรับขนาดไฟล์และปรับขนาดบัฟเฟอร์อีกต่อไปหากจำเป็น แต่เพียงแค่อ่านจนกว่าจะถึงจุดสิ้นสุดของแฟ้ม

ใช้การวนซ้ำเพื่ออ่านตั้งแต่stdinจนถึง EOF ของคุณ

หากคุณระบุ-เป็นอาร์กิวเมนต์คุณจะอ่านได้สูงสุด 2048 ไบต์เท่านั้นstdinก่อนที่จะดำเนินการต่อไปยังอาร์กิวเมนต์บรรทัดคำสั่งถัดไป และถ้าคุณไม่ระบุอาร์กิวเมนต์ใด ๆ เลยคุณจะมีการวนซ้ำที่ไม่สิ้นสุดซึ่งอ่านจากstdinแม้ว่าจะไม่มีอะไรให้อ่านอีกต่อไป

โปรดทราบว่าstdinอาจมีการเปลี่ยนเส้นทางเช่นกันและจะอ่านจากไฟล์หรืออ่านผลลัพธ์จากโปรแกรมอื่น

ใช้บัฟเฟอร์เดียวกันstdinสำหรับไฟล์

ไม่จำเป็นต้องมีบัฟเฟอร์แยกกันสองตัวเนื่องจากคุณจัดการไฟล์หรือทีละไฟล์stdinเท่านั้น เพียงตรวจสอบให้แน่ใจว่ามีขนาดใหญ่เพียงพอ

จัดการข้อผิดพลาดในการอ่านและเขียน

สิ่งต่างๆอาจผิดพลาดได้ หากมีข้อผิดพลาดในการอ่านไฟล์หรือการเขียนถึงstdoutคุณควรพิมพ์ข้อความแสดงข้อผิดพลาดไปที่stderrแล้วออกทันทีโดยใช้รหัสออกที่ไม่ใช่ศูนย์ สิ่งนี้จะแจ้งให้ผู้ใช้ทราบถึงข้อผิดพลาด นอกจากนี้หากcatการใช้งานของคุณถูกใช้ในชุดสคริปต์รหัสการออกที่ไม่ใช่ศูนย์จะอนุญาตให้สคริปต์นั้นตรวจพบข้อผิดพลาดแทนที่จะดำเนินการต่อโดยสุ่มสี่สุ่มห้าด้วยข้อมูลที่ไม่ถูกต้อง