การใช้คำสั่ง cat ของ Windows อย่างง่าย
บน 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);
}
```
คำตอบ
หลีกเลี่ยงการแปลงอาร์กิวเมนต์บรรทัดคำสั่งเป็น 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
การใช้งานของคุณถูกใช้ในชุดสคริปต์รหัสการออกที่ไม่ใช่ศูนย์จะอนุญาตให้สคริปต์นั้นตรวจพบข้อผิดพลาดแทนที่จะดำเนินการต่อโดยสุ่มสี่สุ่มห้าด้วยข้อมูลที่ไม่ถูกต้อง