Salta ai contenuti

Lezione 11: Interazione con l'OS: CLI, Flussi di Testo e Tokenizzazione

Copertina Lezione 11

Fino ad oggi, i nostri programmi hanno interagito con l’utente in modo esclusivamente interattivo, fermandosi ad attendere dati digitati da tastiera a run-time. Tuttavia, nello sviluppo software professionale e nell’automazione di sistema, i programmi devono poter ricevere parametri direttamente all’avvio dal terminale (Command Line Interface) ed elaborare file in modo robusto senza bloccarsi davanti a dati corrotti.

In questa lezione vedremo come il Sistema Operativo passa parametri al processo (l’anatomia di argc e argv in memoria), come differenziare i dati dall’output diagnostico usando lo Standard Error (stderr), come implementare letture stabili prevenendo le trappole intrinseche di scanf e, infine, come effettuare il parsing robusto e la tokenizzazione in memoria RAM tramite sscanf e strtok.


1. La Riga di Comando e lo Standard Error (CLI & STDERR)

Sezione intitolata “1. La Riga di Comando e lo Standard Error (CLI & STDERR)”

I comandi di sistema professionali (come cp, ls o git) non avviano dialoghi interattivi con domande del tipo “Inserisci la sorgente:” o “Inserisci la destinazione:”. Essi ricevono le istruzioni operative direttamente al momento del lancio, ad esempio scrivendo cp foto.png backup/. Questo modello consente di concatenare ed automatizzare i comandi all’interno di script. In C, questa interazione viene gestita tramite due parametri speciali passati alla funzione main.

Per accogliere i parametri di lancio, definiamo il main con la seguente firma:

int main(int argc, char *argv[])
  • argc (Argument Count): Un intero che conta il numero totale di argomenti digitati sul terminale, incluso il nome stesso del programma. È sempre ≥1\ge 1.
  • argv (Argument Vector): Un array di puntatori a caratteri (stringhe).
    • argv[0] contiene sempre la stringa con il nome dell’eseguibile (ad es. "./elabora").
    • argv[1], argv[2], …, argv[argc-1] contengono i parametri di lancio veri e propri.
    • argv[argc] è un puntatore nullo (NULL), che serve come sentinella di fine array.

Un errore comune è ritenere che le stringhe lette da argv si trovino nell’area di memoria a sola lettura (RoData). In realtà, all’avvio del processo, il loader del Sistema Operativo deposita i testi degli argomenti all’inizio assoluto della memoria Stack (agli indirizzi fisici più alti), seguiti dall’array di puntatori argv. Solo dopo viene allocato lo Stack Frame della funzione main.

Essendo allocate nello Stack, le stringhe di input sono mutabili: il programma può scriverci dentro e alterarle (es. argv[1][0] = 'X') senza provocare crash di segmentazione.

Anatomia di argc e argv nello Stack

Codice C: Stampare e modificare gli argomenti a riga di comando

Sezione intitolata “Codice C: Stampare e modificare gli argomenti a riga di comando”
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
printf("Hai lanciato il programma con %d argomenti.\n", argc);
for (int i = 0; i < argc; i++) {
printf("argv[%d]: \"%s\"\n", i, argv[i]);
}
// Intercettare un'opzione di aiuto
if (argc > 1 && strcmp(argv[1], "--help") == 0) {
printf("\nUso: ./programma [valore_1] [operatore] [valore_2]\n");
}
// Dimostrazione di mutabilità nello Stack
if (argc > 1) {
argv[1][0] = 'X';
printf("Argomento 1 modificato fisicamente: %s\n", argv[1]);
}
return 0;
}

Se un’applicazione stampa un messaggio di errore (come “Impossibile aprire il file”) usando una normale printf, questo messaggio finisce sullo Standard Output (stdout) insieme ai dati regolari prodotti dal programma. Ciò rende difficile separare i dati puliti dai diagnostici d’errore.

Per ovviare a questo problema, ogni programma C dispone fin dall’avvio di tre flussi standard (identificati a basso livello da numeri detti File Descriptor):

  1. stdin (File Descriptor 0): Canale di ingresso dei dati (tastiera).
  2. stdout (File Descriptor 1): Canale di uscita standard dei dati (schermo del terminale).
  3. stderr (File Descriptor 2): Canale di uscita indipendente dedicato a errori e messaggi diagnostici.

Utilizzando la funzione fprintf (dove la f sta per file/stream) possiamo deviare i messaggi sul canale desiderato:

fprintf(stdout, "Questo e' un dato normale.\n"); // Equivalente a printf(...)
fprintf(stderr, "ERRORE CRITICO: Parametro non valido!\n"); // Va su stderr

L’esistenza di canali separati permette al terminale di effettuare operazioni di redirezione:

  • Deviare solo stdout su file: make > output.txt (lo schermo rimarrà pulito, i dati andranno nel file, ma gli eventuali errori verranno comunque stampati a schermo).
  • Deviare solo stderr su file: ./programma 2> errori.log (i dati corretti vanno a schermo, gli errori vengono isolati nel log).
  • Deviare entrambi su file separati: ./programma > output.txt 2> errori.log
  • Unire stderr a stdout su unico file: ./programma > tutto.txt 2>&1 (devia il canale 2 nel canale 1).

Architettura dei Flussi Standard Unix


2. I Limiti dell’Input e l’Acquisizione Robusta

Sezione intitolata “2. I Limiti dell’Input e l’Acquisizione Robusta”

La funzione scanf è pensata per l’acquisizione formattata rapida, ma presenta gravi limiti di stabilità in scenari reali:

  1. Il problema del tipo errato: Se il codice richiede un numero (scanf("%d", &eta)) e l’utente inserisce la lettera 'A', la funzione fallisce la conversione e restituisce immediatamente il controllo al programma senza valorizzare la variabile. Se quest’ultima non è inizializzata, conterrà dati casuali dello stack (“spazzatura”). Inoltre, la 'A' rimarrà incastrata nel buffer di sistema, mandando in blocco o in loop infinito le successive chiamate di lettura.
  2. Il limite degli spazi: La lettura di stringhe tramite scanf("%s", buffer) si interrompe sistematicamente davanti al primo carattere di spaziatura (spazio, tabulazione, invio). Non è quindi possibile acquisire stringhe contenenti frasi complete.

Per garantire la stabilità del software, si applicano due regole fondamentali:

Regola 1: Verificare sempre il valore di ritorno di scanf

Sezione intitolata “Regola 1: Verificare sempre il valore di ritorno di scanf”

La funzione scanf restituisce il numero di elementi convertiti con successo. Dobbiamo sempre verificarne il valore:

int eta = 0;
printf("Inserisci la tua eta': ");
if (scanf("%d", &eta) != 1) {
fprintf(stderr, "Errore: Inserire un valore numerico valido!\n");
// Gestione dell'errore e svuotamento del buffer...
}

Per acquisire testi completi, si deve usare fgets(buffer, dimensione, stream), che legge un’intera riga (inclusi gli spazi intermedi) e previene attacchi di Buffer Overflow rispettando rigorosamente la dimensione massima specificata:

char nome[50];
printf("Inserisci nome e cognome: ");
fgets(nome, sizeof(nome), stdin); // Legge in sicurezza fino a 49 caratteri + '\0'

2.3 EOF, getchar e la lettura carattere per carattere

Sezione intitolata “2.3 EOF, getchar e la lettura carattere per carattere”

Per avere il controllo completo sull’input o svuotare il buffer, si ricorre alla lettura a basso livello tramite getchar().

getchar() estrae un singolo byte dallo stream di ingresso. Essa deve poter restituire tutti i 256 possibili valori di un carattere (da 0 a 255), più una condizione speciale che indichi la fine dell’input o del file: la costante EOF (End Of File), definita solitamente come -1.

Se getchar() restituisse un tipo char, il comportamento varierebbe a seconda del compilatore, poiché lo standard C non specifica se il char base sia signed o unsigned:

  • Se char è signed: il byte 0xFF (valore ASCII 255) verrebbe letto come -1, venendo scambiato erroneamente per il segnale di EOF.
  • Se char è unsigned: il segnale di EOF (-1) verrebbe convertito automaticamente in 255, venendo scambiato per un carattere valido e causando un ciclo infinito.

L’uso di un tipo int permette di mappare in sicurezza EOF al di fuori dell’intervallo dei caratteri validi, mantenendo distinti tutti i 256 valori positivi dal segnale di fine file.

Codice C: Costruire stringhe carattere per carattere invertendole

Sezione intitolata “Codice C: Costruire stringhe carattere per carattere invertendole”
#include <stdio.h>
int main(void) {
char stringa[50];
printf("Inserisci la stringa (max 49 car. - premi invio per confermare): ");
int len = 0;
int c;
// Lettura manuale a basso livello con getchar()
do {
c = getchar();
// Escludiamo il newline e l'EOF, rispettando il limite fisico del buffer
if (c != '\n' && c != EOF && len < 49) {
stringa[len] = (char)c;
len++;
}
} while (c != EOF && c != '\n' && len < 49);
// Inserimento manuale del terminatore di stringa
stringa[len] = '\0';
printf("\nStringa invertita: ");
for (int i = len - 1; i >= 0; i--) {
printf("%c", stringa[i]);
}
printf("\n");
return 0;
}

3. Persistenza, File Stream e Tokenizzazione in Memoria

Sezione intitolata “3. Persistenza, File Stream e Tokenizzazione in Memoria”

La persistenza delle informazioni viene gestita tramite puntatori a descrittori di file (FILE *). La struttura FILE racchiude i dettagli di stato del file gestiti dal Sistema Operativo (posizione corrente del cursore, buffer, flag di errore).

  • fopen("nome", "modo"): Apre un file. I modi principali sono "r" (lettura, restituisce NULL se il file è assente), "w" (scrittura, crea un file vuoto cancellando l’eventuale contenuto preesistente) e "a" (append, scrive in coda senza cancellare).
  • fclose(fp): Chiude lo stream. Questa chiamata è fondamentale per forzare lo svuotamento dei buffer in memoria e garantire che i dati vengano fisicamente scritti sul disco prima che il programma termini.

Cercare di interpretare un file di dati strutturati (es. un elenco CSV) tramite istruzioni dirette quali fscanf(fp, "%s %d", ...) è rischioso. Se una singola riga del file risulta malformata o contiene una stringa al posto di un numero (ad es. Analisi_1 9 ASSENTE invece di Analisi_1 9 27), la conversione fallisce, i dati incongruenti rimangono memorizzati nel buffer del file stream e il programma entrerà in loop infinito o salterà erroneamente tutte le righe rimanenti del file.

La soluzione ingegneristica standard consiste nel dividere il processo di lettura dal processo di decodifica:

  1. Si acquisisce l’intera riga “grezza” come stringa indipendente in memoria RAM usando la sicura fgets().
  2. Si effettua il parsing e la decodifica (tokenizzazione) della sola stringa in memoria RAM. In questo modo, se una riga è corrotta, la decodifica fallirà localmente senza bloccare il file stream. Il programma potrà segnalare l’anomalia su stderr e passare tranquillamente alla riga successiva.

sscanf funziona in modo speculare a scanf/fscanf, ma estrae i dati analizzando una stringa in memoria RAM invece che un canale fisico:

char riga[] = "Programmazione_1 9 27";
char materia[50];
int cfu, voto;
if (sscanf(riga, "%49s %d %d", materia, &cfu, &voto) == 3) {
printf("Letto: %s (%d CFU) - Voto: %d\n", materia, cfu, voto);
}

Strumento B: La funzione strtok (In-Place Tokenizer)

Sezione intitolata “Strumento B: La funzione strtok (In-Place Tokenizer)”

Definita in <string.h>, strtok divide una stringa in sotto-stringhe (token) separate da uno o più caratteri delimitatori (es. la virgola per file CSV).

strtok è una funzione distruttiva: opera in-place alterando la stringa originaria. Ogni volta che individua un delimitatore, lo sovrascrive inserendovi il terminatore di stringa '\0'. Per questa ragione, passargli come argomento una stringa costante non modificabile (ad es. char *s = "Etna,Corso,Catania") provocherà un crash immediato per violazione di accesso in scrittura (Segmentation Fault).

Per mantenere traccia della posizione durante l’analisi, strtok adotta una logica specifica:

  • Alla prima chiamata, si passa la stringa da dividere come primo argomento: strtok(riga, ",").
  • Alle chiamate successive, si passa NULL come primo parametro per indicare alla funzione di riprendere l’analisi dallo stato memorizzato internamente: strtok(NULL, ",").

Funzionamento interno di strtok in RAM

Cosa stampa l’istruzione printf alla fine di questo codice?

#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Ciao,sono,io";
char *a = strtok(str, ",");
while (a != NULL) {
printf("%s\n", a);
a = strtok(NULL, ",");
}
printf("Stringa finale: %s\n", str);
return 0;
}

L’istruzione finale stamperà a schermo solo: Ciao. Poiché strtok ha modificato la prima virgola in \0, qualunque funzione standard (come la printf con %s) arresterà la lettura all’indirizzo iniziale non appena incontra la prima sentinella \0, ignorando le parole successive che pure risiedono ancora intatte in memoria RAM.


1. Dove vengono collocate fisicamente in memoria le stringhe passate come argomenti tramite argv?

  • A) Nel segmento RoData (sola lettura), trattandosi di costanti letterali.
  • B) All’inizio assoluto dello Stack, subito prima dello Stack Frame del main().
  • C) Nello Heap, allocate dinamicamente dal sistema operativo.
  • D) Nel segmento BSS, in quanto variabili globali non inizializzate.
▶ Mostra Risposta Corretta

Risposta corretta: B

Spiegazione: Le stringhe dei parametri di riga di comando vengono caricate nello Stack dal loader dell’OS prima dell’inizio dell’esecuzione del programma. Questo le rende mutabili (possono essere modificate scrivendo in argv[i]).

2. Cosa accade se si reindirizza l’output di un programma tramite comando myapp > out.txt ed il programma riscontra un errore che stampa su stderr?

  • A) L’errore viene scritto nel file out.txt insieme ai dati ordinari.
  • B) L’errore viene stampato comunque sullo schermo del terminale.
  • C) Il programma viene interrotto istantaneamente dall’OS.
  • D) L’errore viene scartato silenziosamente.
▶ Mostra Risposta Corretta

Risposta corretta: B

Spiegazione: Il reindirizzamento con > (o 1>) devia solo lo standard output (stdout), lasciando invariata la destinazione di stderr che continua a essere diretta allo schermo del terminale (salvo redirezione esplicita con 2>).

3. Perché il tipo di ritorno della funzione standard getchar() è int e non char?

  • A) Perché deve consentire l’acquisizione di numeri interi completi in formato testuale.
  • B) Perché deve poter restituire un valore speciale (EOF, pari a -1) distinto da tutti i caratteri validi, evitando che su macchine con char con segno il byte 0xFF venga scambiato per fine file, o che con char senza segno EOF si trasformi in 255.
  • C) Per uniformità con lo standard POSIX che richiede solo tipi a 32 o 64 bit.
  • D) Per evitare overflow aritmetici nello stack frame.
▶ Mostra Risposta Corretta

Risposta corretta: B

Spiegazione: Se getchar() restituisse char, lo standard C non specifica se sia con o senza segno. Se fosse signed, il carattere valido 255 (0xFF) verrebbe promosso a -1, confondendosi con EOF. Se fosse unsigned, EOF (-1) verrebbe convertito in 255, impedendo il rilevamento di fine file e causando cicli infiniti.

4. Cosa succede se si passa una costante di stringa definita con le virgolette (ad es. char *s = "dati,csv";) come primo parametro della funzione strtok?

  • A) La stringa viene duplicata nello Heap e tokenizzata in sicurezza.
  • B) La funzione sostituisce la prima virgola con un terminatore senza effetti collaterali.
  • C) Il programma subisce un crash istantaneo (Segmentation Fault) per tentativo di scrittura su memoria protetta.
  • D) Viene restituito un puntatore nullo (NULL).
▶ Mostra Risposta Corretta

Risposta corretta: C

Spiegazione: Le costanti stringa definite tramite virgolette risiedono nel segmento RoData (Read-Only Data) della memoria del processo. Poiché strtok opera in-place modificando la stringa (sostituendo i delimitatori con \0), l’accesso in scrittura a tale area provoca una violazione di memoria.

5. Quale valore numerico identifica il File Descriptor del flusso standard d’errore stderr?

  • A) 0
  • B) 1
  • C) 2
  • D) -1
▶ Mostra Risposta Corretta

Risposta corretta: C

Spiegazione: Per convenzione Unix, i tre file descriptor standard sono: stdin = 0, stdout = 1, stderr = 2.

6. Qual è il valore memorizzato nella cella d’indice argc all’interno dell’array argv?

  • A) Il nome stesso del programma.
  • B) Il valore intero 0.
  • C) Il puntatore nullo NULL.
  • D) L’indirizzo iniziale dello stack frame.
▶ Mostra Risposta Corretta

Risposta corretta: C

Spiegazione: Lo standard C stabilisce che argv[argc] deve essere sempre uguale a NULL. Questo consente di scorrere i puntatori di riga di comando usando un puntatore incrementale senza basarsi necessariamente su argc.

7. Da dove legge i dati la funzione standard sscanf?

  • A) Dalla tastiera, bloccando il terminale fino alla pressione di Invio.
  • B) Da uno stream di file precedentemente aperto con fopen.
  • C) Dallo standard error.
  • D) Da una stringa di caratteri (buffer) già presente in memoria RAM.
▶ Mostra Risposta Corretta

Risposta corretta: D

Spiegazione: La prima “s” in sscanf sta per “string”. Essa analizza il contenuto di una stringa in memoria RAM formattandolo secondo gli specificatori di formato forniti, esattamente come scanf fa con lo standard input.

8. Se si apre un file in scrittura con fopen("dati.txt", "w") e il file esiste già sul disco, qual è il comportamento del sistema?

  • A) Il file viene troncato, ovvero svuotato completamente diventando di dimensione zero.
  • B) La funzione fallisce restituendo NULL.
  • C) I nuovi dati vengono aggiunti in coda a quelli già esistenti.
  • D) Viene sollevato un errore di runtime non intercettabile.
▶ Mostra Risposta Corretta

Risposta corretta: A

Spiegazione: La modalità "w" (write) cancella l’eventuale contenuto preesistente del file. Per scrivere in coda preservando i dati presenti occorre utilizzare la modalità "a" (append).

9. Perché è strettamente necessario invocare fclose(fp) al termine delle operazioni di scrittura su file?

  • A) Per rilasciare i puntatori ed evitare integer overflow nello stack.
  • B) Perchè la scrittura avviene inizialmente in un buffer in memoria RAM gestito dall’OS; fclose forza il riversamento (flush) fisico dei dati sul disco rigido.
  • C) Per consentire a fscanf di riposizionare il cursore all’inizio del file.
  • D) Per forzare la compilazione del file stream.
▶ Mostra Risposta Corretta

Risposta corretta: B

Spiegazione: Per ragioni di prestazioni, le scritture su file sono “bufferizzate”. I dati non vengono inviati al disco per ogni singolo carattere. La chiusura del file chiude il descrittore ed esegue il flush del buffer, garantendo l’integrità fisica dei dati memorizzati.

10. Nel dividere una stringa con strtok("a,b,c", ","), in cosa vengono convertite le virgole all’interno dell’array originario?

  • A) In caratteri di spazio.
  • B) Nel carattere terminatore di stringa '\0'.
  • C) Rimangono inalterate, la funzione lavora su una copia nascosta.
  • D) Vengono eliminate compattando la stringa in memoria.
▶ Mostra Risposta Corretta

Risposta corretta: B

Spiegazione: strtok opera in-place e sostituisce fisicamente ciascun delimitatore individuato con il carattere sentinella \0. Questo spezza l’array in più sotto-stringhe disgiunte ma allocate nella stessa area di memoria RAM.


Esercizio 1: Calcolatrice da Riga di Comando (cli_calc.c)

Sezione intitolata “Esercizio 1: Calcolatrice da Riga di Comando (cli_calc.c)”

Si scriva un programma in C che accetti esattamente tre argomenti da riga di comando: due numeri interi e un operatore matematico (+, -, x). Il programma deve stampare a schermo il risultato dell’operazione. Se il numero di argomenti è errato o l’operatore non è supportato, stampare un opportuno messaggio d’errore su stderr e terminare con exit code 1.

▶ Mostra Soluzione
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]) {
if (argc != 4) {
fprintf(stderr, "Uso: %s [n1] [op] [n2]\n", argv[0]);
return 1;
}
int n1 = atoi(argv[1]), n2 = atoi(argv[3]);
if (strcmp(argv[2], "+") == 0) printf("%d\n", n1 + n2);
else if (strcmp(argv[2], "-") == 0) printf("%d\n", n1 - n2);
else if (strcmp(argv[2], "x") == 0) printf("%d\n", n1 * n2);
else { fprintf(stderr, "Op errato\n"); return 1; }
return 0;
}

Esercizio 2: Funzione di Lettura Robusta Interi (robust_input.c)

Sezione intitolata “Esercizio 2: Funzione di Lettura Robusta Interi (robust_input.c)”

Si scriva una funzione C int read_int(const char *prompt, int *val) che implementi l’acquisizione robusta di un numero intero tramite fgets e sscanf.

▶ Mostra Soluzione
#include <stdio.h>
#include <stdbool.h>
int read_int(const char *prompt, int *val) {
char buf[128];
while(true) {
printf("%s", prompt);
if(!fgets(buf, sizeof(buf), stdin)) return 0;
if(sscanf(buf, "%d", val) == 1) return 1;
fprintf(stderr, "Input non valido\n");
}
}

Esercizio 3: Parser In-Memory di Configurazione (config_parser.c)

Sezione intitolata “Esercizio 3: Parser In-Memory di Configurazione (config_parser.c)”

Si realizzi un programma che legga coppie chiave=valore saltando commenti (#).

▶ Mostra Soluzione
#include <stdio.h>
#include <string.h>
int main() {
char line[256];
while(fgets(line, sizeof(line), stdin)) {
if(line[0] == '#' || line[0] == '\n') continue;
char *k = strtok(line, "=");
char *v = strtok(NULL, "\n");
if(k && v) printf("Key: %s, Val: %s\n", k, v);
}
return 0;
}

Si scriva un filtro CSV che stampa solo i cognomi con voto >= soglia CLI.

▶ Mostra Soluzione
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]) {
long soglia = atol(argv[1]);
char line[256];
FILE *fp = fopen("studenti.csv", "r");
while(fgets(line, sizeof(line), fp)) {
char *nome = strtok(line, ",");
char *voto_s = strtok(NULL, "\n");
if(atoi(voto_s) >= soglia) printf("%s\n", nome);
}
fclose(fp);
return 0;
}

Esercizio 5: Inversione degli argomenti CLI (cli_reverse.c)

Sezione intitolata “Esercizio 5: Inversione degli argomenti CLI (cli_reverse.c)”

Stampa gli argomenti CLI al contrario, in maiuscolo.

▶ Mostra Soluzione
#include <stdio.h>
#include <ctype.h>
#include <string.h>
int main(int argc, char *argv[]) {
for(int i = argc - 1; i > 0; i--) {
for(int j = 0; argv[i][j]; j++) putchar(toupper(argv[i][j]));
putchar(' ');
}
return 0;
}

Conta caratteri, parole e righe da stdin.

▶ Mostra Soluzione
#include <stdio.h>
#include <ctype.h>
int main() {
int c, chars = 0, words = 0, lines = 0, in_w = 0;
while((c = getchar()) != EOF) {
chars++;
if(c == '\n') lines++;
if(isspace(c)) in_w = 0;
else if(!in_w) { in_w = 1; words++; }
}
printf("Righe: %d, Parole: %d, Caratteri: %d\n", lines, words, chars);
return 0;
}

Esercizio 7: Copia File con Error Logging (file_copy.c)

Sezione intitolata “Esercizio 7: Copia File con Error Logging (file_copy.c)”

Si scriva un programma C che accetti due nomi di file come parametri (sorgente e destinazione). Copia il contenuto del primo nel secondo. Gestire tutti i casi di errore (argomenti mancanti, file non apribili) stampando messaggi significativi su stderr e terminando con exit code 1.

▶ Mostra Soluzione
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Uso: %s [sorgente] [destinazione]\n", argv[0]);
return 1;
}
FILE *in = fopen(argv[1], "r");
if (in == NULL) {
fprintf(stderr, "Errore: impossibile aprire il file sorgente '%s'\n", argv[1]);
return 1;
}
FILE *out = fopen(argv[2], "w");
if (out == NULL) {
fprintf(stderr, "Errore: impossibile creare il file di destinazione '%s'\n", argv[2]);
fclose(in);
return 1;
}
int c;
while ((c = fgetc(in)) != EOF) {
fputc(c, out);
}
fclose(in);
fclose(out);
return 0;
}

Esercizio 8: Validatore di Indirizzi IP (ip_parser.c)

Sezione intitolata “Esercizio 8: Validatore di Indirizzi IP (ip_parser.c)”

Si scriva un programma C che prenda un indirizzo IP in formato A.B.C.D come parametro e lo stampi in formato esadecimale, validando che gli ottetti siano tra 0 e 255.

▶ Mostra Soluzione
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Uso: %s [indirizzo_IP]\n", argv[0]);
return 1;
}
int o[4];
char residuo;
if (sscanf(argv[1], "%d.%d.%d.%d %c", &o[0], &o[1], &o[2], &o[3], &residuo) != 4) {
fprintf(stderr, "Errore: formato IP non valido. Richiesto A.B.C.D\n");
return 1;
}
for (int i = 0; i < 4; i++) {
if (o[i] < 0 || o[i] > 255) {
fprintf(stderr, "Errore: ottetto %d (%d) fuori dal range [0..255]\n", i, o[i]);
return 1;
}
}
printf("Hex: %02X.%02X.%02X.%02X\n", o[0], o[1], o[2], o[3]);
return 0;
}

Esercizio 9: Tokenizzazione di Frase (word_tokenizer.c)

Sezione intitolata “Esercizio 9: Tokenizzazione di Frase (word_tokenizer.c)”

Estrae parole da stringa inserita tramite standard input usando come delimitatori spazi e punteggiatura.

▶ Mostra Soluzione
#include <stdio.h>
#include <string.h>
int main(void) {
char s[256];
printf("Inserisci una frase: ");
if (!fgets(s, sizeof(s), stdin)) {
return 0;
}
char *t = strtok(s, " ,.?! \n\t");
int i = 1;
while (t) {
printf("Parola %d: %s\n", i++, t);
t = strtok(NULL, " ,.?! \n\t");
}
return 0;
}

Esercizio 10: Unione Alternata di File (file_merge.c)

Sezione intitolata “Esercizio 10: Unione Alternata di File (file_merge.c)”

Prende come parametri due file sorgente ed un file destinazione. Copia alternativamente una riga del primo e del secondo file nel file destinazione.

▶ Mostra Soluzione
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc != 4) {
fprintf(stderr, "Uso: %s [file1] [file2] [destinazione]\n", argv[0]);
return 1;
}
FILE *f1 = fopen(argv[1], "r");
if (f1 == NULL) {
fprintf(stderr, "Errore: impossibile aprire '%s'\n", argv[1]);
return 1;
}
FILE *f2 = fopen(argv[2], "r");
if (f2 == NULL) {
fprintf(stderr, "Errore: impossibile aprire '%s'\n", argv[2]);
fclose(f1);
return 1;
}
FILE *o = fopen(argv[3], "w");
if (o == NULL) {
fprintf(stderr, "Errore: impossibile creare '%s'\n", argv[3]);
fclose(f1);
fclose(f2);
return 1;
}
char l1[256], l2[256];
char *r1, *r2;
do {
r1 = fgets(l1, sizeof(l1), f1);
if (r1) {
fputs(l1, o);
}
r2 = fgets(l2, sizeof(l2), f2);
if (r2) {
fputs(l2, o);
}
} while (r1 || r2);
fclose(f1);
fclose(f2);
fclose(o);
return 0;
}