Implementación simple de Windows del comando cat
En Linux existe el cat
comando que genera archivos concatenados, pero en Windows no existe tal comando. Como resultado, decidí intentar recrear una versión simple pero con el desafío de que no podía usar ninguna parte de la biblioteca de tiempo de ejecución de 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);
}
```
Respuestas
Evite convertir argumentos de línea de comando a ASCII
No hay una buena razón para convertir los argumentos de la línea de comandos a ASCII. Todas las funciones que usa que llevan punteros a cadenas ASCII también tienen variantes que manejan cadenas anchas, por ejemplo lstrcmpW()
y CreateFileW()
. De esta manera, puede deshacerse de CommandLineToArgvA()
.
Úselo stderr
para informar errores
Tenga en cuenta que no es improbable que el usuario de su cat
implementación redirija la salida estándar a otro archivo. Si hay un error, en lugar de imprimirlo en la consola, está escribiendo el mensaje de error en ese archivo. Simplemente agregue stderr = GetStdHandle(STD_ERROR_HANDLE)
y utilícelo para los mensajes de error.
Evite asignar un búfer tan grande como cada archivo de entrada
El espacio en disco suele ser al menos un orden de magnitud mayor que la RAM. Si desea capturar un archivo más grande que la cantidad de RAM libre disponible, su programa fallará. Es mejor asignar un búfer con un tamaño fijo de, por ejemplo, 64 KiB, y usar múltiples llamadas ReadFile()
si es necesario para leer la entrada como fragmentos de hasta 64 KiB. Por un lado, significa más gastos generales de múltiples llamadas a ReadFile()
, por otro lado, es probable que permanezca dentro de la caché L2 de su CPU. En cualquier caso, espero que el rendimiento no cambie drásticamente por esto, pero ahora su programa maneja archivos de tamaño arbitrario.
Esto también simplificará su código: ya no tendrá que obtener el tamaño del archivo y cambiar el tamaño del búfer si es necesario. En su lugar, simplemente lea hasta llegar al final del archivo .
Use un bucle para leer stdin
hasta llegar a EOF
Si lo especifica -
como un argumento, solo leerá hasta 2048 bytes stdin
antes de continuar con el siguiente argumento de línea de comando. Y si no especifica ningún argumento, tiene un bucle infinito desde el que se lee stdin
, incluso si ya no hay nada para leer.
Tenga en cuenta que stdin
también podría haber sido redirigido y, de hecho, leerá de un archivo o leerá la salida de otro programa.
Utilice el mismo búfer stdin
para archivos
No es necesario tener dos búferes separados, ya que solo maneja un archivo o stdin
al mismo tiempo. Solo asegúrate de que sea lo suficientemente grande.
Manejar errores de lectura y escritura
Las cosas pueden salir mal. Si hay un error al leer un archivo o al escribir en stdout
, debe imprimir un mensaje de error stderr
y luego salir inmediatamente con un código de salida distinto de cero. Esto notificará al usuario de los errores. Además, si su cat
implementación se utiliza en un script por lotes, el código de salida distinto de cero permitirá que el script detecte el error, en lugar de continuar ciegamente con datos no válidos.