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よりも少なくとも1桁大きくなります。使用可能な空きRAMの量よりも大きいファイルをキャットしたい場合、プログラムは失敗します。たとえば64KiBの固定サイズのバッファーを割り当て、ReadFile()必要に応じて複数の呼び出しを使用して、最大64KiBのチャンクとして入力を読み取ることをお勧めします。一方では、への複数の呼び出しによるオーバーヘッドが増えることを意味しReadFile()ますが、他方では、CPUのL2キャッシュ内にとどまる可能性があります。いずれにせよ、これによってパフォーマンスが劇的に変わることはないと思いますが、プログラムは任意のサイズのファイルを処理するようになりました。

これにより、コードも簡素化されます。必要に応じて、ファイルサイズを取得し、バッファーのサイズを変更する必要がなくなります。代わりに、ファイルの終わりに達するまで読んでください。

ループを使用して、stdinEOFに到達するまで読み取ります

-引数として指定した場合stdin、次のコマンドライン引数に進む前に、最大2048バイトしか読み取れません。また、引数をまったく指定しないと、読み取るものstdinがなくなった場合でも、から読み取る無限ループが発生します。

これstdinもリダイレクトされている可能性があり、実際にはファイルから読み取るか、別のプログラムからの出力を読み取ることに注意してください。

stdinファイルと同じバッファを使用する

ファイルまたは一度に処理するだけなので、2つの別々のバッファーを持つ必要はありませんstdin。十分な大きさであることを確認してください。

読み取りおよび書き込みエラーを処理する

物事はうまくいかない可能性があります。ファイルの読み取りまたはへの書き込みでエラーが発生した場合は、にエラーstdoutメッセージを出力stderrし、すぐにゼロ以外の終了コードで終了する必要があります。これにより、ユーザーにエラーが通知されます。また、cat実装がバッチスクリプトで使用されている場合、ゼロ以外の終了コードを使用すると、無効なデータを盲目的に続行する代わりに、そのスクリプトでエラーを検出できます。