Implementasi Windows sederhana dari perintah cat
Di Linux ada cat
perintah 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
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 stderr
untuk melaporkan kesalahan
Pertimbangkan bahwa bukan tidak mungkin pengguna cat
implementasi 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 stdin
sampai Anda mencapai EOF
Jika Anda menentukan -
sebagai argumen, Anda hanya membaca hingga 2048 byte dari stdin
sebelum 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 stdin
mungkin juga telah dialihkan, dan akan benar-benar membaca dari file, atau membaca keluaran dari program lain.
Gunakan buffer yang sama stdin
seperti untuk file
Tidak perlu memiliki dua buffer terpisah, karena Anda hanya menangani file atau stdin
dalam 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 stderr
dan kemudian segera keluar dengan kode keluar bukan nol. Ini akan memberi tahu pengguna tentang kesalahan. Selain itu, jika cat
implementasi 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.