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보다 10 배 이상 더 큽니다. 사용 가능한 RAM 용량보다 큰 파일을 찾으려면 프로그램이 실패합니다. 64KiB의 고정 크기로 버퍼를 할당하고 ReadFile()필요한 경우 최대 64KiB의 청크로 입력을 읽기 위해 여러 호출을 사용 하는 것이 좋습니다. 한편으로는 ReadFile()에 대한 여러 호출에서 더 많은 오버 헤드 가 발생하고 다른 한편으로는 CPU의 L2 캐시 내에있을 가능성이 높습니다. 어쨌든 성능이 크게 변경되지는 않을 것으로 예상하지만 이제 프로그램은 임의의 크기의 파일을 처리합니다.

이것은 또한 코드를 단순화 할 것입니다. 더 이상 파일 크기를 얻고 필요한 경우 버퍼 크기를 조정할 필요가 없습니다. 대신 파일 의 끝에 도달 할 때까지 읽으십시오 .

루프를 사용하여 stdinEOF에 도달 할 때까지 읽기

-인수로 지정 stdin하는 경우 다음 명령 줄 인수를 계속하기 전에 최대 2048 바이트 만 읽습니다 . 그리고 인수를 전혀 지정하지 않으면 stdin더 이상 읽을 것이 없더라도 에서 읽는 무한 루프가 생깁니다.

명심 stdin또한 리디렉션 될 수도 있고, 실제로 파일에서 읽을 것, 또는 다른 프로그램에서 출력을 읽습니다.

stdin파일 과 동일한 버퍼 사용

파일 또는 stdin한 번에 하나만 처리하므로 두 개의 별도 버퍼를 가질 필요가 없습니다 . 충분히 큰지 확인하십시오.

읽기 및 쓰기 오류 처리

일이 잘못 될 수 있습니다. 파일을 읽거나에 쓰는 중 stdout오류가 발생하면 오류 메시지를에 인쇄 한 stderr다음 0이 아닌 종료 코드로 즉시 종료해야합니다. 사용자에게 오류를 알립니다. 또한 cat구현이 배치 스크립트에서 사용되는 경우 0이 아닌 종료 코드를 사용하면 해당 스크립트가 잘못된 데이터를 맹목적으로 계속하는 대신 오류를 감지 할 수 있습니다.