Prosta implementacja polecenia cat w systemie Windows
W systemie Linux istnieje catpolecenie, które wyprowadza pliki połączone, ale w systemie Windows nie ma takiego polecenia. W rezultacie postanowiłem spróbować odtworzyć jego prostą wersję, ale z wyzwaniem polegającym na tym, że nie mogłem użyć żadnej części biblioteki wykonawczej 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);
}
```
Odpowiedzi
Unikaj konwertowania argumentów wiersza poleceń na ASCII
Nie ma powodu, aby konwertować argumenty wiersza poleceń na ASCII. Wszystkie używane funkcje pobierające wskaźniki do łańcuchów ASCII mają również warianty obsługujące szerokie ciągi, na przykład lstrcmpW()i CreateFileW(). W ten sposób możesz się ich pozbyć CommandLineToArgvA().
Służy stderrdo zgłaszania błędów
Należy wziąć pod uwagę, że nie jest nieprawdopodobne, że użytkownik catimplementacji przekierowuje standardowe wyjście do innego pliku. Jeśli wystąpi błąd, zamiast wydrukować go na konsoli, piszesz komunikat o błędzie do tego pliku. Po prostu dodaj stderr = GetStdHandle(STD_ERROR_HANDLE)i używaj tego do komunikatów o błędach.
Unikaj przydzielania buforu tak dużego jak każdy plik wejściowy
Miejsce na dysku jest zwykle co najmniej o rząd wielkości większe niż pamięć RAM. Jeśli chcesz pobrać plik większy niż ilość dostępnej wolnej pamięci RAM, program zakończy się niepowodzeniem. Lepiej jest przydzielić bufor o stałym rozmiarze, powiedzmy 64 KB, i w ReadFile()razie potrzeby użyć wielu wywołań , aby odczytać dane wejściowe jako fragmenty o wielkości do 64 KB. Z jednej strony oznacza to więcej narzutów z wielu wywołań ReadFile(), z drugiej strony prawdopodobnie pozostaniesz w pamięci podręcznej L2 procesora. W każdym razie spodziewam się, że wydajność nie zmieni się dramatycznie przez to, ale teraz twój program obsługuje pliki o dowolnych rozmiarach.
Uprości to również twój kod: nie musisz już pobierać rozmiaru pliku i zmieniać rozmiaru bufora, jeśli to konieczne. Zamiast tego czytaj, aż dojdziesz do końca pliku .
Użyj pętli do czytania od stdindo osiągnięcia EOF
Jeśli określisz -jako argument, odczytasz tylko do 2048 bajtów od stdinprzed przejściem do następnego argumentu wiersza poleceń. A jeśli w ogóle nie podasz żadnych argumentów, masz nieskończoną pętlę, z której czyta stdin, nawet jeśli nie ma już nic do czytania.
Należy pamiętać, że stdinmogło również zostać przekierowane i faktycznie odczyta z pliku lub odczyta dane wyjściowe z innego programu.
Użyj tego samego bufora stdindo plików
Nie ma potrzeby posiadania dwóch oddzielnych buforów, ponieważ obsługujesz tylko plik lub stdinnaraz. Po prostu upewnij się, że jest wystarczająco duży.
Obsługa błędów odczytu i zapisu
Może się nie udać. Jeśli wystąpi błąd podczas odczytu pliku lub zapisu do pliku stdout, należy wydrukować komunikat o błędzie, stderra następnie natychmiast zakończyć z niezerowym kodem zakończenia. To powiadomi użytkownika o błędach. Ponadto, jeśli catimplementacja jest używana w skrypcie wsadowym, niezerowy kod zakończenia umożliwi temu skryptowi wykrycie błędu, zamiast ślepego kontynuowania z nieprawidłowymi danymi.