sysfatal.github.io

Logo

sysfatal(blog)

13 January 2023

ChatGPT aprueba el examen

by e__soriano




Muchos llevamos unas semanas revolucionados con ChatGPT, la IA especializada en generación de diálogo de OpenAI. Supongo que a estas alturas no hace falta explicar qué es. A mí me gusta decir que es una charlatana excelente: habla mucho pero no entiende nada, sólo dice lo que ha escuchado por ahí :)

Este post describe un experimento: he dado a ChatGPT el enunciado del examen de prácticas que pusimos esta semana en la asignatura Sistemas Operativos (tercer curso de los grados Ing. Telemática / Ing. Tecnologías de Teleco.).

Lo ha hecho bien. ChatGPT aprueba el examen, como el 30% de los alumnos que se han presentado al examen (el 20% de los matriculados en la asignatura).

Enunciado

El enunciado es muy sencillo:

Escribe un programa llamado concattxt en C para Linux que admita un
único argumento, que será una ruta.

El programa debe escribir el contenido de todos los ficheros del
directorio de trabajo actual cuyo nombre termine en .txt en el fichero
indicado por el argumento. No importa el orden en el que se recorra los
ficheros correspondientes (como estén en el directorio). Si el fichero
ya existe, se debe truncar. En otro caso, se debe crear.

El programa debe avisar y fallar si se ejecuta sin argumentos o con más
de un argumento.

Los ficheros pueden tener cualquier tamaño.

No se puede ejecutar ningún programa externo.

Por ejemplo:

$ echo xxx > uno.txt
$ echo yyy > dos.txt
$ echo zzz > tres.txt
$ ls *.txt
dos.txt  tres.txt  uno.txt
$ ./concattxt /tmp/salida
$ cat /tmp/salida
yyy
zzz
xxx
$

La prueba es presencial. Los alumnos tienen una hora para hacer este programa en un sistema GNU Linux Ubuntu 22.04 con las páginas de manual instaladas, sin material (apuntes, etc.) ni conexión a internet. Como se puede ver, no es una prueba muy difícil.

Nuestra solución

Esta es la solución de los profesores de la asignatura:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <err.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>

enum {
	BufSize = 32 * 1024,
};

int
dump(char *path, int fdout)
{
	int fdin;
	int nr;
	char buf[BufSize];

	fdin = open(path, O_RDONLY);
	if (fdin < 0) {
		warn("can't open %s", path);
		return -1;
	}
	while ((nr = read(fdin, buf, BufSize)) > 0) {
		if (write(fdout, buf, nr) != nr) {
			close(fdin);
			warn("can't write");
			return -1;
		}
	}
	if (nr < 0) {
		close(fdin);
		warn("can't read file %s", path);
		return -1;
	}
	close(fdin);
	return 0;
}

int
istxt(struct dirent *ent)
{
	char *p;

	p = strrchr(ent->d_name, '.');
	if (p == NULL) {
		return 0;
	}
	return strcmp(p, ".txt") == 0 && ent->d_type == DT_REG;
}

int
concattxt(int fdout)
{
	DIR *dir;
	struct dirent *ent;
	int sts;

	sts = 0;
	dir = opendir(".");
	if (dir == NULL) {
		warn("can't open .");
		return 1;
	}
	while ((ent = readdir(dir)) != NULL) {
		if (ent->d_name[0] != '.' && istxt(ent)) {
			if (dump(ent->d_name, fdout) < 0) {
				warnx("can't process file: %s", ent->d_name);
				sts++;
			}
		}
	}
	closedir(dir);
	return sts;
}

int
main(int argc, char *argv[])
{
	int fdout;
	int sts;

	if (argc != 2) {
		fprintf(stderr, "usage: concattxt file\n");
		exit(EXIT_FAILURE);
	}
	fdout = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0660);
	if (fdout < 0) {
		err(EXIT_FAILURE, "can't create file %s", argv[1]);
	}
	sts = concattxt(fdout);
	close(fdout);
	exit(sts);
}

Primer intento

yo: Hola

ChatGPT: ¡Hola! ¿En qué puedo ayudarte hoy?

yo: Quiero hacer un programa en C para linux

ChatGPT: ¡Genial! Para comenzar a programar en C en Linux, necesitarás un compilador de C, como GCC (GNU Compiler Collection). La mayoría de las distribuciones de Linux vienen con GCC preinstalado, pero si no está instalado en tu sistema, puedes instalarlo usando el administrador de paquetes de tu distribución. Una vez que tengas GCC instalado, puedes crear un archivo de código fuente con un editor de texto (como nano o vi) y guardarlo con la extensión “.c”. Luego puedes compilar el archivo fuente utilizando el comando “gcc” seguido del nombre del archivo.

yo: Escribe un programa llamado concattxt en C que admita un único argumento, que será una ruta. El programa debe escribir el contenido de todos los ficheros del directorio de trabajo actual cuyo nombre termine en .txt en el fichero indicado por el argumento. No importa el orden en el que se recorra los ficheros correspondientes (como estén en el directorio). Si el fichero ya existe, se debe truncar. En otro caso, se debe crear. El programa debe avisar y fallar si se ejecuta sin argumentos o con más de un argumento. Los ficheros pueden tener cualquier tamaño. No se puede ejecutar ningún programa externo.

ChatGPT: Aquí te dejo un ejemplo de código de un programa en C que cumple con los requisitos especificados:

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <string.h>

int main(int argc, char* argv[]) {
    if (argc != 2) {
        printf("Uso: concattxt [archivo de salida]\n");
        return 1;
    }

    char* outputFile = argv[1];
    FILE* fp = fopen(outputFile, "w");
    if (!fp) {
        printf("No se pudo abrir el archivo de salida: %s\n", outputFile);
        return 1;
    }

    DIR* dir = opendir(".");
    if (!dir) {
        printf("No se pudo abrir el directorio actual\n");
        return 1;
    }

    struct dirent* ent;
    while ((ent = readdir(dir)) != NULL) {
        if (ent->d_type != DT_REG) {
            continue;
        }

        int len = strlen(ent->d_name);
        if (len < 4 || strcmp(ent->d_name + len - 4, ".txt") != 0) {
            continue;
        }

        FILE* file = fopen(ent->d_name, "r");
        if (!file) {
            printf("No se pudo abrir el archivo: %s\n", ent->d_name);
            continue;
        }

        char buffer[4096];
        size_t bytesRead;
        while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0) {
            fwrite(buffer, 1, bytesRead, fp);
        }

        fclose(file);
    }

    closedir(dir);
    fclose(fp);

    return 0;
}

Ok. Este programa está bien. Hace lo que pide el enunciado y funciona. Sin embargo, no es del todo correcto (ignoraremos todo lo que tiene que ver con el estilo de código que utilizamos en la asignatura, porque ChatGPT no lo puede saber):

¿Estaría este programa aprobado aplicando los mismos criterios de corrección? Sin duda. Teniendo en cuenta las entregas del examen, la nota de ChatGPT estaría en el primer cuartil.


Mejorando: partir el programa

yo: Está todo implementado en la función principal, ¿puedes partir en funciones?

ChatGPT: Claro, aquí te muestro cómo podría dividir el código en varias funciones para una mejor legibilidad y reutilización:

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <string.h>

int checkArguments(int argc, char* argv[]) {
    if (argc != 2) {
        printf("Uso: concattxt [archivo de salida]\n");
        return 1;
    }
    return 0;
}

FILE* openOutputFile(char* outputFile) {
    FILE* fp = fopen(outputFile, "w");
    if (!fp) {
        printf("No se pudo abrir el archivo de salida: %s\n", outputFile);
        exit(1);
    }
    return fp;
}

DIR* openCurrentDir() {
    DIR* dir = opendir(".");
    if (!dir) {
        printf("No se pudo abrir el directorio actual\n");
        exit(1);
    }
    return dir;
}

void concatenateTXT(FILE* fp, DIR* dir) {
    struct dirent* ent;
    while ((ent = readdir(dir)) != NULL) {
        if (ent->d_type != DT_REG) {
            continue;
        }

        int len = strlen(ent->d_name);
        if (len < 4 || strcmp(ent->d_name + len - 4, ".txt") != 0) {
            continue;
        }

        FILE* file = fopen(ent->d_name, "r");
        if (!file) {
            printf("No se pudo abrir el archivo: %s\n", ent->d_name);
            continue;
        }

        char buffer[4096];
        size_t bytesRead;
        while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0) {
            fwrite(buffer, 1, bytesRead, fp);
        }

        fclose(file);
    }
}

int main(int argc, char* argv[]) {
    if (checkArguments(argc, argv))
        return 1;
    char* outputFile = argv[1];
    FILE* fp = openOutputFile(outputFile);
    DIR* dir = openCurrentDir();
    concatenateTXT(fp, dir);
    closedir(dir);
    fclose(fp);
    return 0;
}

ChatGPT: De esta manera el código queda más organizado, se puede entender mejor y es más fácil de mantener y extender. Cada función tiene un propósito específico, es fácil de entender y testear por separado, y se pueden reutilizar en otros programas.


Como ella misma dice, partir el programa mejora la legibilidad y la reutilización del código. Vemos que ha partido el código, como se ha pedido. No está muy bien partido, porque básicamente está recubriendo las funciones de biblioteca y sacando parte del programa principal a otra función. Al menos, lo ha intentado.


Los errores, por stderr

yo: Los errores deberian salir por la salida de errores, ¿no?

ChatGPT: Sí, tienes razón. Es recomendable que los errores y mensajes de advertencia se escriban en la salida de error estándar (stderr) en lugar de la salida estándar (stdout), ya que la salida de error estándar suele ser redirigida a un archivo o a una consola separada, lo que facilita su monitoreo y registro. Para escribir en la salida de error estándar, se puede usar la función fprintf() con el puntero de archivo “stderr” en lugar de “stdout”. Por ejemplo:

fprintf(stderr, “Error: No se pudo abrir el archivo de salida: %s\n”, outputFile);

yo: ¿Puedes reescribir el código de esa manera?

ChatGPT: Claro, aquí te dejo una versión actualizada del código donde se escriben los mensajes de error en la salida de error estándar:

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <string.h>

int checkArguments(int argc, char* argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Uso: concattxt [archivo de salida]\n");
        return 1;
    }
    return 0;
}

FILE* openOutputFile(char* outputFile) {
    FILE* fp = fopen(outputFile, "w");
    if (!fp) {
        fprintf(stderr, "Error: No se pudo abrir el archivo de salida: %s\n", outputFile);
        exit(1);
    }
    return fp;
}

DIR* openCurrentDir() {
    DIR* dir = opendir(".");
    if (!dir) {
        fprintf(stderr, "Error: No se pudo abrir el directorio actual\n");
        exit(1);
    }
    return dir;
}

void concatenateTXT(FILE* fp, DIR* dir) {
    struct dirent* ent;
    while ((ent = readdir(dir)) != NULL) {
        if (ent->d_type != DT_REG) {
            continue;
        }

        int len = strlen(ent->d_name);
        if (len < 4 || strcmp(ent->d_name + len - 4, ".txt") != 0) {
            continue;
        }

        FILE* file = fopen(ent->d_name, "r");
        if (!file) {
            fprintf(stderr, "Error: No se pudo abrir el archivo: %s\n", ent->d_name);
            continue;
        }

        char buffer[4096];
        size_t bytesRead;
        while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0) {
            fwrite(buffer, 1, bytesRead, fp);
        }

        fclose(file);
    }
}

int main(int argc, char* argv[]) {
    if (checkArguments(argc, argv))
        return 1;
    char* outputFile = argv[1];
    FILE* fp = openOutputFile(outputFile);
    DIR* dir = openCurrentDir();
    concatenateTXT(fp, dir);
    closedir(dir);
    fclose(fp);
    return 0;
}

Correcto, ahora está mejor.


Constantes

yo: ¿Puedes definir constantes cuando sea apropiado?

ChatGPT: Sí, se pueden definir constantes en C para mejorar la legibilidad y mantenibilidad del código. Una constante es un valor que no puede ser modificado una vez que se asigna. En C, se pueden definir constantes utilizando la palabra clave “const” delante del tipo de datos. Por ejemplo, se puede definir una constante entera llamada “BUFFER_SIZE” de la siguiente manera: const int BUFFER_SIZE = 4096; En el código anterior se puede observar como se usa una constante BUFFER_SIZE para definir el tamaño del buffer, esto tiene varias ventajas:

  1. Facilita el mantenimiento del código, ya que si se necesita cambiar el tamaño del buffer solo se tiene que modificar una sola línea de código.

  2. Mejora la legibilidad del código, ya que se puede entender que el tamaño del buffer es un valor constante.

  3. Aumenta la seguridad del código, ya que al ser una constante no se puede modificar accidentalmente.

También se pueden utilizar constantes en otros lugares donde sea apropiado, como en los argumentos de las funciones, o para definir el tamaño de un arreglo.

yo: ¿Puedes reescribir el código con constantes cuando hagan falta?

ChatGPT: Claro, aquí te muestro cómo podría reescribir el código utilizando constantes cuando sea apropiado:

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <string.h>

const int BUFFER_SIZE = 4096;

int checkArguments(int argc, char* argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Uso: concattxt [archivo de salida]\n");
        return 1;
    }
    return 0;
}

FILE* openOutputFile(char* outputFile) {
    FILE* fp = fopen(outputFile, "w");
    if (!fp) {
        fprintf(stderr, "Error: No se pudo abrir el archivo de salida: %s\n", outputFile);
        exit(1);
    }
    return fp;
}

DIR* openCurrentDir() {
    DIR* dir = opendir(".");
    if (!dir) {
        fprintf(stderr, "Error: No se pudo abrir el directorio actual\n");
        exit(1);
    }
    return dir;
}

void concatenateTXT(FILE* fp, DIR* dir) {
    struct dirent* ent;
    while ((ent = readdir(dir)) != NULL) {
        if (ent->d_type != DT_REG) {
            continue;
        }

        int len = strlen(ent->d_name);
        if (len < 4 || strcmp(ent->d_name + len - 4, ".txt") != 0) {
            continue;
        }

        FILE* file = fopen(ent->d_name, "r");
        if (!file) {
            fprintf(stderr, "Error: No se pudo abrir el archivo: %s\n", ent->d_name);
            continue;
        }

        char buffer[BUFFER_SIZE];
        size_t bytesRead;
        while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0) {
            fwrite(buffer, 1, bytesRead, fp);
        }

        fclose(file);
    }
}

int main(int argc, char* argv[]) {
    if (checkArguments(argc, argv))
        return 1;
    char* outputFile = argv[1];
    FILE* fp = openOutputFile(outputFile);
    DIR* dir = openCurrentDir();
    concatenateTXT(fp, dir);
    closedir(dir);
    fclose(fp);
    return 0;
}

ChatGPT: Espero que esta nueva versión del código cumpla con tus necesidades.


Aquí está usando const para definir la constante. Entramos en un viejo debate: ¿cuál es la mejor forma de definir constantes en C?

Tenemos tres formas para definir constantes enteras: define, const y enum.

define es una orden para el preprocesador de C que realiza una sustitución textual en el fuente antes de pasar al compilador. Eso trae muchos problemas. const es un cualificador que indica que dicha “variable” es inmutable, que no es lo mismo que definir un valor que sea constante durante toda la ejecución del programa: la memoria de dicha variable puede estar en una región de memoria que sea modificable y un bug puede provocar un cambio en el valor de la constante (cosa que nunca debería pasar), se puede conseguir su dirección de memoria con &, etc. Puedes ver un ejemplo aquí. Para definir constantes enteras en C, lo más apropiado es usar enum, que sirve para crear tipos enumerados y permite asignar nombres a constantes enteras.


Usar las llamadas al sistema para I/O

yo: ¿Puedes cambiar el programa de C para que no use stdio y use las llamadas al sistema read y write?

ChatGPT: Sí, se puede reescribir el programa para que utilice las llamadas al sistema read() y write() en lugar de las funciones de la biblioteca estándar stdio. Sin embargo, es importante tener en cuenta que estas llamadas al sistema son más bajas nivel y requieren un mayor cuidado y precisión al manejar los errores y los tamaños de los datos. Claro, aquí te muestro el programa completo reescrito para utilizar las llamadas al sistema read() y write():

#include <unistd.h>
#include <stdlib.h>
#include <dirent.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define BUFFER_SIZE 4096

int checkArguments(int argc, char* argv[]) {
    if (argc != 2) {
        write(STDERR_FILENO, "Uso: concattxt [archivo de salida]\n", 34);
        return 1;
    }
    return 0;
}

int openOutputFile(char* outputFile) {
    int fd = open(outputFile, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        char error[50];
        snprintf(error, 50, "Error: No se pudo abrir el archivo de salida: %s\n", outputFile);
        write(STDERR_FILENO, error, strlen(error));
        exit(1);
    }
    return fd;
}

DIR* openCurrentDir() {
    DIR* dir = opendir(".");
    if (!dir) {
        write(STDERR_FILENO, "Error: No se pudo abrir el directorio actual\n", 42);
        exit(1);
    }
    return dir;
}

void concatenateTXT(int fd, DIR* dir) {
    struct dirent* ent;
    while ((ent = readdir(dir)) != NULL) {
        if (ent->d_type != DT_REG) {
            continue;
        }

        int len = strlen(ent->d_name);
        if (len < 4 || strcmp(ent->d_name + len - 4, ".txt") != 0) {
            continue;
        }

        int file = open(ent->d_name, O_RDONLY);
        if (file == -1) {
            char error[50];
            snprintf(error, 50, "Error: No se pudo abrir el archivo: %s\n", ent->d_name);
            write(STDERR_FILENO, error, strlen(error));
            continue;
        }

        char buffer[BUFFER_SIZE];
        ssize_t bytesRead;
        while ((bytesRead = read(file, buffer, sizeof(buffer))) > 0) {
            write(fd, buffer, bytesRead);
        }

        close(file);
    }
}

int main(int argc, char* argv[]) {
    if (checkArguments(argc, argv))
        return 1;
    char* outputFile = argv[1];
    int fd = openOutputFile(outputFile);
    DIR* dir = openCurrentDir();
    concatenateTXT(fd, dir);
    closedir(dir);
    close(fd);
    return 0;
}

Aquí se empieza a torcer el asunto.

En realidad, yo quería que usara las llamadas al sistema open, read, write y close en lugar de fopen, fread, fwrite y fclose para la lectura/escritura de los ficheros, no para escribir los mensajes de error/diagnóstico en stderr (eso se podía quedar con fprintf). De todas formas, es natural que ChatGPT no me haya entendido bien y haya cambiado todo.

El problema es que ahora el código no compila: ha quitado includes pero sigue usando funciones que los necesitan.

Sigue sin comprobar los errores en las lecturas y las escrituras.


Un script de shell

yo: ¿Puedes reescribir el programa en shell scripting?

ChatGPT: Claro, aquí te muestro una posible implementación del programa en shell scripting:

#!/bin/bash

if [[ $# -ne 1 ]]
then
    echo "Uso: concattxt [archivo de salida]" >&2
    exit 1
fi

outputFile=$1

> $outputFile
for file in *.txt
do
    cat $file >> $outputFile
done

ChatGPT: El script comienza verificando el número de argumentos pasados al script mediante el uso de la expresión if [[ $# -ne 1 ]]. El uso de >&2 en el echo, redirige la salida a la salida de error, de esta forma se diferencian los mensajes de error de los normales. Luego, se asigna el primer argumento (nombre del archivo de salida) a una variable outputFile. Por último, se utiliza el comando for para recorrer todos los archivos con extensión .txt en el directorio actual. Dentro del ciclo, se utiliza el comando cat para concatenar el contenido de cada archivo al archivo de salida. El operador » se utiliza para agregar el contenido al final del archivo de salida en lugar de sobreescribirlo. Espero que esta implementación sea de ayuda.


El script es correcto, incluso escribe los errores por la salida de errores.


Opinión

Estas herramientas van a cambiar la forma de programar, eso es lo único que tengo claro.

Llevo escuchando que los programadores dejarán de existir desde que empecé a estudiar en los 90s. Entonces, se decía por las herramientas CASE. En la última década, por la IA. Matt Welsh declaró hace pocos días el fin de la programación en Communications of the ACM. El exprofesor de Harvard (y exdirector de ingeniería en Apple y Google) es el fundador y CEO de Fixie.ai, una startup de IA para desarrollo software. Puede que eso tenga algo que ver con su opinión.

Los avances de los últimos años parecen indicar que los programadores menos cualificados, los dedicados a copiar y pegar código de stack overflow, ya están en peligro. Yo creo que se necesitarán programadores expertos para, entre otras cosas, auditar, modificar e integrar la salida de estas herramientas. Si fuese un estudiante TIC, ahora mismo estaría bastante preocupado e intentaría esforzarme para ser uno de ellos.

Como nos recuerda Rodney Brooks, exdirector del MIT Computer Science and Artificial Intelligence Lab, hay que calmarse. Hasta el propio CEO de OpenAI, Sam Altman, dice que ChatGPT tiene muchas limitaciones pero es lo suficientemente buena como para dar una falsa impresión de grandeza y es un error depender de ella a día de hoy.

Esta vez ha quedado entre los mejores de la clase, pero podría haber fallado. De hecho, he repetido el experimento y ha generado una solución peor que usa fgets y fputs (no son apropiadas para este programa). En otras ocasiones me ha dado soluciones y explicaciones totalmente incorrectas.

Ya hay estudios que indican que el código creado con asistentes de este tipo es más inseguro. Apuesto a que este hype nos traerá muchos problemas durante los próximos años.

En el futuro, puede que los humanos olvidemos las matemáticas y la programación, y no sepamos cómo funcionan las máquinas. Ya nos avisó Asimov (o eso dice ChatGPT).

ACTUALIZACIÓN (21 de enero) ¿Y un test?

Para ver qué tal le salen los test, he probado con el de otra asignatura: Seguridad en Sistemas Robóticos de cuarto curso de Ingeniería de Robótica Software.

La prueba consiste en un test de Moodle. La prueba es presencial y no se tiene salida a Internet. Los alumnos disponen de 50 minutos para hacer el test y pueden usar los materiales de clase. El test tiene 20 preguntas en las que se combinan conceptos de criptografía, autenticación, control de acceso, protocolos seguros, programación segura, seguridad en sistemas ROS2, seguridad perimetral, ataques, VPN, exploits, etc. Cada pregunta tiene cinco respuestas y sólo una es correcta. Todas las preguntas puntúan igual. Las respuestas correctas cuentan un 100% y las incorrectas restan un 20%. Las respuestas en blanco no cuentan.

La nota media de la clase ha sido 4.25. ChatGPT saca un 4.6 y se sitúa en el segundo cuartil.

Comentarios

Puedes comentar este post en Mastodon: https://social.linux.pizza/@esoriano/109684708021342526

(cc) Enrique Soriano-Salvador Creative Commons (by-nc-nd). Creative Commons, 559 Nathan Abbott Way, Stanford, California 94305, USA.

tags: ChatGPT - linux - programación - IA