Semplice implementazione in Windows del comando cat
Su Linux c'è il cat
comando che emette file concatenati ma su Windows non esiste tale comando. Di conseguenza ho deciso di provare a ricrearne una versione semplice, ma con una sfida che era che non potevo usare nessuna parte della libreria di 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);
}
```
Risposte
Evita di convertire gli argomenti della riga di comando in ASCII
Non ci sono buone ragioni per convertire gli argomenti della riga di comando in ASCII. Tutte le funzioni che usi che accettano puntatori a stringhe ASCII hanno anche varianti che gestiscono stringhe larghe, ad esempio lstrcmpW()
e CreateFileW()
. In questo modo, puoi sbarazzartene CommandLineToArgvA()
.
Utilizzare stderr
per segnalare errori
Considera che non è improbabile che l'utente della tua cat
implementazione reindirizzi l'output standard a un altro file. Se si verifica un errore, invece di stamparlo sulla console, stai invece scrivendo il messaggio di errore su quel file. Basta aggiungere stderr = GetStdHandle(STD_ERROR_HANDLE)
e usarlo per i messaggi di errore.
Evita di allocare un buffer grande quanto ogni file di input
Lo spazio su disco è in genere almeno un ordine di grandezza maggiore della RAM. Se vuoi cat un file più grande della quantità di RAM libera disponibile, il tuo programma fallirà. È meglio allocare un buffer con una dimensione fissa di diciamo 64 KiB e utilizzare più chiamate a ReadFile()
se necessario per leggere l'input come blocchi fino a 64 KiB. Da un lato, significa più overhead da più chiamate a ReadFile()
, dall'altro probabilmente rimarrai nella cache L2 della tua CPU. In ogni caso, mi aspetto che le prestazioni non vengano modificate drasticamente da questo, ma ora il tuo programma gestisce file di dimensioni arbitrarie.
Questo semplificherà anche il tuo codice: non dovrai più ottenere la dimensione del file e ridimensionare il buffer se necessario. Invece, continua a leggere fino a raggiungere la fine del file .
Usa un ciclo da cui leggere stdin
fino a raggiungere EOF
Se si specifica -
come argomento, si leggono solo fino a 2048 byte stdin
prima di continuare con l'argomento successivo della riga di comando. E se non specifichi alcun argomento, hai un ciclo infinito che legge da stdin
, anche se non c'è più niente da leggere.
Tieni presente che stdin
potrebbe anche essere stato reindirizzato e in realtà leggerà da un file o leggerà l'output da un altro programma.
Usa lo stesso buffer per stdin
i file
Non è necessario disporre di due buffer separati, poiché gestisci solo un file o stdin
alla volta. Assicurati solo che sia abbastanza grande.
Gestisci gli errori di lettura e scrittura
Le cose possono andare storte. Se si verifica un errore durante la lettura o la scrittura di un file stdout
, è necessario stampare un messaggio di errore stderr
e quindi uscire immediatamente con un codice di uscita diverso da zero. Questo avviserà l'utente degli errori. Inoltre, se l' cat
implementazione viene utilizzata in uno script batch, il codice di uscita diverso da zero consentirà a tale script di rilevare l'errore, invece di continuare ciecamente con dati non validi.