Esempio di programma in C per terminale

Stampa
( 0 Votes ) 
Valutazione attuale:  / 0
ScarsoOttimo 
Categoria: Informatica
Data pubblicazione
Scritto da MGuruDC Visite: 2273

Esempio di programma in C per terminale

Ecco a voi un esempio di programma in C in grado di interfacciarsi con il terminale in cui viene eseguito.

Il compito di questo eseguibile è mettere in ordine alfabetico le stringhe contenute in un file.
Usando l'opzione "-h" avremo una piccola guida.
Indicheremo al programma il file in ingresso con "-i" e poi il nome del file e il file in uscita con "-o" e il nome del file. Sono disponibili 3 opzioni:
- "d", elimina i doppioni;
- "p", modalità prolisso;
- "s", usa spazio anzichè l'a capo.
Ad esempio se vogliamo leggere il file1, scrivere sul file2 eliminando i doppioni e usando lo spazio anzichè l'a capo daremo il comando:

---$ ordina -i file1 -o file2 -ds

oppure se vogliamo avere solo la modalità "prolisso":

---$ ordina -o file1 -p -i file1

In pratica l'ordine delle opzioni o degli argomenti può essere casuale, l'importante è che siano specificati i file in ingresso e in uscita preceduti rispettivamente da "-i" e "-o".

Ecco un paio di immagini(dalla sacra consolle di linux) che non guastano mai:

Al lavoro su un vocabolario di 60.000 parole in modalità normale



E qui a lavoro sullo stesso vocabolario in modalità prolissa con rimozione dei doppioni

Click this bar to view the full image.
 

Vediamo come funziona


Naturalmente prima di arrivare al cuore del programma ci sono molti parametri da controllare e aggiustare...

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

void ordina(int, int, char*, FILE*, FILE*);

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

    FILE *fIn = NULL, *fOut = NULL;
    char car, parola[30], opzioni[4] = "\0\0\0\0";
    int i, j, rifFile, dimStr, passo;

Gli "#include" delle prime righe indicano al preprocessore di usare librerie "stdio" per la gestione degli input e output, "stdlib" da cui prendiamo le funzioni per scrivere sui file e "string" per gestione delle stringhe.
Subito dopo troviamo la dichiarazione di una procedura. Non è strettamente necessaria ma zittisce parecchi compilatori.
I parametri in ingresso per la funzione main sono delle variabili standard attraverso le quali il terminale comunica col programma.
La variabile "argc" è un numero intero e contiene il numero di argomenti passati al programma, il puntatore a vettore "argv" contiene gli argomenti passati.
Se ad esempio diamo al nostro terminale il comando:

---$ programma argomento1 argomento2 argomento3

ecco cosa succede:
- la variabile "argc" varrà 4;
- il vettore "argv[0]" contiene il nome del programma eseguito, quindi contiene la stringa "programma" d'ra in poi non sarà considerato;
- il vettore "argv[1]" contiene la stringa "argomento1";
- il vettore "argv[2]" contiene la stringa "argomento2";
- il vettore "argv[3]" contiene la stringa "argomento3";
- il vettore "argv[4]" e tutti i successivi sono nulli oppure contengono stringhe casuali, per non leggere queste stringhe useremo la variabile "argc", tra poco vedremo come.
Dopo la testata del "main" troviamo le variabili che verranno usate di qui in poi. I due puntatori alle strutture "FILE"(definita in "stdlib") sono inizializzati(riferiti a "NULL") per prudenza, tra poco "fIn" punterà al file in ingresso e "fOut" a quello in uscita. Il carattere "car" servirà solo per memorizzare eventuali input da tastiera, le due stringhe saranno usate come appoggio in memoria per le operazione da fare.
Le variabili intere saranno usate come indici e come registro per i valori da passare alla funzione "ordina".
Per cominciare l'esecuzione del programma dobbiamo prima di tutto interpretare gli argomenti ricevuti dal terminale. Il perché di questi codici e le scelte effettuate saranno chiare solo tra un po'.

    if(argc == 2 && strcmp(argv[1], "-h") == 0) {
        puts("[...]guida[...]");
        return 0;
        }

Questo blocco di codice ha una funzione molto semplice: verificare che è stato inserito un solo argomento, e che questo argomento è "-h", in caso affermativo stampare a video una piccola guida testuale per l'uso del programma.
La condizione "argc == 2" serve ad assicurarci che sia stato inserito un unico argomento, se questo argomento, confrontato con "strcmp", è "-h" viene eseguito il codice nel blocco, quindi viene stampata la guida(in questo spezzone ho eliminato il testo che fa da argomento alla funzione "puts") e subito dopo il programma viene chiuso tramite il comando "return".
Se l'utente non richiede una guida per l'uso vuole sicuramente provare ad usare il programma, e sappiamo che perchè il programma funzioni ci vogliono almeno 4 argomenti per indicare quali sono i file in  entrata e in uscita, più due indicatori che ci permettono di mettere gli argomenti in modo casuale. Se a questo punto "argc" vale meno di 5, dobbiamo indicare che c'è stato un errore di sintassi, e lo facciamo con questo:

    if(argc < 5) {
        puts("\n>> Errore di sintassi, inserire \"ordina -h\" per una guida\n\n");
        return 0;
        }

Con questo codice andiamo a segnalare all'utente che la sintassi non e corretta(invitando a consultare la guida), dopodichè chiudiamo il programma.
Ora viene un pezzo di codice un po' complicato:

    for(i = 1; i < argc; i ++) {
        if((argv[i])[0] == '-' && strcmp(argv[i], "-i") != 0 && strcmp(argv[i], "-o") != 0) {
            for(j = 1; j < strlen(argv[i]); j++) {
                if((argv[i])[j] != 'd' && (argv[i])[j] != 'p' && (argv[i])[j] != 's') {
                    puts("\n>> Errore di sintassi, inserire \"ordina -h\" per una guida\n\n");
                    return 0;
                    }
                }
            for(j = 1; j < strlen(argv[i]); j++) {
                opzioni[j - 1] = (argv[i])[j];
                }
            break;
            }
        }

Questo ciclo for fa una scansione degli argomenti in ingresso alla ricerca delle opzioni per il programma. Naturalmente il ciclo parte da "argv[1]" perché la prima stringa non ci interessa. Il ciclo viene fermato al raggiungimento di "argv[argc - 1]", in questo modo evitiamo di leggere le stringhe casuali presenti nel vettore.
Per ogni stringa individuata bisogna prima di tutti vedere che non si tratti del nome di un file, quindi il primo carattere della stringa deve essere "-". Qualora vengano trovate delle stringhe che iniziano per "-" bisogna escludere le stringhe che indicano il tipo di file, quindi le stringhe che alla seconda posizione hanno il carattere "i" oppure "o" vanno sorvolate. Qualora vengano soddisfatte le condizioni del primo "if" bisogna fare una scansione della stringa individuata per restituire un errore di sintassi se viene richiesta un'opzione sbagliata, dunque se la stringa contiene caratteri diversi da "d", "p" e "s" il programma segnala l'errore ed esce.
Se è stata individuata una stringa con delle opzioni valide, i caratteri della stringa vengono copiati uno ad uno nella stringa "opzioni", ma viene evitato il primo carattere "-" che non ci interessa.
Adesso dobbiamo vedere se il file in ingresso è stato specificato ed esiste:

    for(i = 1; i < argc; i ++) {                   
        if(strcmp(argv[i], "-i") == 0) {
            fIn = fopen(argv[i + 1], "r");
            if(fIn == NULL) {
                puts("\n>> File in ingresso non trovato\n");
                return 0;
                }
            break;
            }
        }

Facciamo un'altra scansione degli argomenti, stavolta alla ricerca della stringa "-i". Quando questa viene trovata il programma cerca di aprire il file il cui nome è specificato nella stringa successiva a quella che contiene l'indicatore "-i". Se il file cercato non è valido (non esiste o non è accessibile) il puntatore "fIn" sarà riferito a "NULL", in questo caso il programma viene chiuso e l'errore viene segnalato. Nel caso in cui il file è valido il ciclo viene interrotto e il puntatore "fIn" rimane riferito al file, quindi da ora potrà essere usato tranquillamente.
Per il file di uscita complichiamo le cose un po' più del necessario:

    for(i = 0; i < argc; i ++) {
        if(strcmp(argv[i], "-o") == 0) {
            fOut = fopen(argv[i + 1], "r");
            if(fOut == NULL) {
                fOut = fopen(argv[i + 1], "w");
                break;
                }
            else {
                puts("\n>> Il file in uscita è già esistente.\nSecgliendo di continuare il suo contenuto verrà sovrascritto.\nContinuare?[s/n]");
                scanf(" %c", &car);
                if(car != 's' && car != 'S') {
                    puts(">> Uscita...\n");
                    return 0;
                    }
                puts(">> Si è scelto di continuare");
                fclose(fOut);
                fOut = fopen(argv[i + 1], "w");
                break;
                }
            }
        }

Come prima andiamo ad individuare la stringa successiva a "-o" che contiene il nome del file in uscita e per lo apriamo in lettura. Se il puntatore a file vale "NULL" significa che il file non esiste ancora, quindi il file viene riaperto in scrittura e il ciclo viene interrotto. Se il file esiste andiamo a chiedere all'utente se vuole sovrascriverlo, e lo facciamo chiedendo di inserire il carattere "s" o "S". L'inserimento di un qualunque altro carattere porta all'immediata chiusura del programma. Qualora scegliamo di continuare il file viene apero in scrittura e quindi immediatamente sovrascritto.
Se i file su cui il programma deve lavorare non sono specificati bene il programma sarà chiuso dal sistema al primo controllo del puntatore per errore di segmentazione, quindi non è necessario implementare il controllo per l'esistenza dell'argomento.
Finalmente abbiamo finito con gli argomenti!
Adesso passiamo a una scansione del file e alla fine del programma.
Questa procedura è necessaria per memorizzare i dati letti nel file in ingresso nella memoria programma anzichè nell'heap(la parte della memoria RAM che registra i dati non dichiarati all'inizio del programma) in modo da avere una gestione dei dati più semplice e sicura, mettendoci al riparo dagli errori di stack smashing durante l'esecuzione.
Questo metodo non è molto comune e necessita di una messa a punto molto noiosa e laboriosa, ma penso sia un'ottima occasione per imparare qualcosa di nuovo.

    printf("\nScansione del file in ingresso");
    fseek(fIn, 0, SEEK_END);                   
    rifFile = ftell(fIn);
    rifFile /= 20;
    rewind(fIn);

    passo = 1;
    dimStr = 0;
    i = 0;
    while(!feof(fIn)) {                       
        fscanf(fIn, "%s\n", parola);
        i++;
        if(strlen(parola) > dimStr) {
            dimStr = strlen(parola);
            }
        if(ftell(fIn) > passo * rifFile) {
            printf(".");
            passo ++;
            }
        }

    printf("\nNumero stringhe = %d\nDimensione massima stringhe = %d\n\n", i, dimStr);

    ordina(i, dimStr, opzioni, fIn, fOut);               

    fclose(fOut);                           
    fclose(fIn);
    return 0;
    }

Per dare la possibilità di tenere sotto controllo la scansione del file  ho complicato ancora le cose:
Per primo andiamo a determinare la dimensione del file portando il cursore del file in ingresso alla fine con la funzione "fseek" e memorizzando nella variabile "rifFile" la posizione raggiunta con "ftell". Subito dopo il valore di "rifFile" viene diviso per venti e il file viene riavvolto.
Il blocco successivo  fa 3 cose contemporaneamente:
- conta con la variabile "i" le stringhe memorizzate nel file dopo averle lette con "fscanf";
- determina la lunghezza della stringa più lunga e la memorizza in "dimStr";
- stampa a video un puntino "." per ogni ventesimo del file che viene letto.
Una volta ricavati tutti questi dati viene chiamata la funzione "ordina" che si occupa realmente dell'ordinamento delle stringhe.
Alla fine dell'esecuzione della funzione "ordina" i file vengono chiusi(...per sicurezza, nei sistemi moderni non dovrebbe essere più necessario) e anche il ciclo "main" giunge alla fine con la chiusura definitiva del programma.

Adesso analizziamo la funzione "ordina".

void ordina(int n, int dim, char *mod, FILE *fIn, FILE *fOut) {

#define doppi        1
#define prolisso    2
#define spazio        4
#define intervento    8

    short stato;
    int i, j;
    char file[n][dim + 2], temp[dim + 2], conv[5], reg = 0;

Come possiamo vedere dalla testata della funzione, i paramentri necessari per il suo funzionamento sono parecchi:
- "int n" memorizza il numero di stringhe su cui operare;
- "int dim" specifica la grandezza massima che avranno le stringhe;
- "char *mod" contiene le opzioni richieste per l'esecuzione del programma;
- "FILE *fIn" e "FILE fOut" sono puntatori ai file su cui lavorare.
Le direttive successive sono necessarie per l'uso del registro a 8 bit "reg", tra le altre variabili c'è l'array bidimensionale "file" le cui dimensioni vengono  calcolate grazie ai valori in ingresso, la lunghezza delle stringhe è aumentata di 2 per sicurezza. "temp" servirà come appoggio durante l'ordinamento, "stato" servirà per l'eventuale debug e "conv" conterrà la stringa di conversione per la scrittura su file.

    for(i = 0; i < strlen(mod); i ++) {           
        switch(mod[i]) {
            case 'd':
                reg = reg | doppi;
                break;
            case 'p':
                reg = reg | prolisso;
                break;
            case 's':
                reg = reg | spazio;
                break;
            }
        }

Questo blocco serve a memorizzare nel registro "reg" le impostazioni. Viene fatta una scansione dell'array "mod" e nel caso venga trovato il carattere "d", "p" o "s" viene messa a uno la specifica voce del reigstro tramite l'operatore binario e le maschere definite tramite le direttive "#define".

    rewind(fIn);                       

    i = 0;
    while(!feof(fIn)) {                   
        fscanf(fIn, "%s\n", file[i]);
        i ++;
        }

Qui il file viene riavvolto ancora una volta, dopodichè il ciclo "while" memorizza nell'array "file" tutte le stringhe contenute nel file in ingresso.
Adesso viene la parte fondamentale, il codice che tramite l'algoritmo dell'"ordinamento a bolle" va a operare sui dati in memoria.
Questo codice è un po' sporcato dalle opzioni di esecuzione, ma i vari passi dell'algoritmo sono evidenti:

    if((reg & prolisso) != 0 ) {
        printf("Memorizzate %d stringhe\n\n",i);
        reg = reg | intervento;
        while((reg & intervento) != 0) {       
            reg = reg & ~intervento;
            for(i = 0; i < n - 1; i ++) {
                stato = strcmp(file[i], file[i + 1]);
                switch(stato) {
                    case 0:
                        printf("Doppio: #%d  '%s'\n", i, file[i]);
                        break;
                    case 1:
                        printf("#%d   '%s' -> '%s'\n", i, file[i], file[i + 1]);
                        strcpy(temp, file[i]);
                        strcpy(file[i], file [i + 1]);
                        strcpy(file[i + 1], temp);           
                        reg = reg | intervento;
                        break;
                    default:
                        break;
                    }
                }
            }
        }
    else {
        reg = reg | intervento;
        while((reg & intervento) != 0) {
            reg = reg & ~intervento;
            for(i = 0; i < n - 1; i ++) {
                if(strcmp(file[i], file[i + 1]) == 1) {
                    strcpy(temp, file[i]);
                    strcpy(file[i], file [i + 1]);
                    strcpy(file[i + 1], temp);           
                    reg = reg | intervento;
                    }
                }
            }
        }

La modalità "prolisso" per il debug deve essere completamente rifatta in nome dell'efficienza del codice in condizioni normali, quindi il blocco il primo blocco "if-else" serve a distinguere i due casi.
In modalità "prolisso" il programma ci informa sul numero di stringhe effettivamente memorizzate, poi scrive nel registro "reg" la condizione di partenza per l'ordinamento a bolle. Il ciclo "while" fa riperere l'algoritmo finchè sussuste tale condizione, ma per permettere la conclusione dell'algoritmo la voce del registro reg viene invertita tramite gli operatori binari "&", "~" e la maschera "intervento". Il ciclo for annidato fa la scansione dell'array e le stringhe vengono confrontate due a due. Quando le due stringhe sono in ordine corretto la funzione "strcmp" restituisce il valore -1, e in questo caso l'algoritmo non fa niente e prosegue. Se le due stringhe sono uguali il programma in modalità "prolisso" ci avviserà stampando a video la stringa e il suo numero corrispondente. Se le stringhe sono in ordine errato l'algoritmo provvederà segnalarle e quindi a scambiarle di posto usando come appoggio la variabile "temp" e registrando l'intervento nel registro "reg".
Il blocco "else" esegue lo stesso algoritmo ma in maniera molto semplificata, visto che in questo caso non c'è la necesità di segnalare niente all'utente.

    if((reg & doppi) != 0) {
        if((reg & prolisso) != 0) {
            puts("\nEliminazione doppioni");
            for(i = 0; i < n - 1; i ++) {
                if(strcmp(file[i], file[i + 1]) == 0) {
                    printf(">>>Doppio #%d   %s\n", i, file[i]);
                    j = i + 1;
                    for (; j < n - 1; j ++) {
                        strcpy(file[j], file[j + 1]);
                        }
                    n --;
                    }
                }
            }
        else {
            for(i = 0; i < n - 1; i ++) {
                if(strcmp(file[i], file[i + 1]) == 0) {
                    j = i + 1;
                    for (; j < n - 1; j ++) {
                        strcpy(file[j], file[j + 1]);
                        }
                    n --;
                    }
                }
            }
        }

Se è stata richiesta l'eliminazione dei doppioni parte una nuova scansione e le stringhe vengono ancora contfrontate due a due con "strcmp". Quando la funzione restituisce il valore 0 viene segnalato il doppione, dopodichè parte un nuovo ciclo che a partire dalla posizione del doppione copia ogni stringa sulla precedente e alla fine decrementa il numero totale delle stringhe. Anche qui segue nel blocco else il codice per la modalità normale che è un po' più semplice.

    if((reg & spazio) != 0) {
        strcpy(conv, "%s ");
        }
    else {
        strcpy(conv, "%s\n");
        }

Questo piccolo blocco si occupa di scrivere i caratteri giusti nella stringa "conv", in modo che se è stato richiesto nelle opzioni avremo come delimitatore dei file in uscita lo spazio, altrimenti avremo un a capo.

    if((reg & prolisso) != 0) {               
        puts("\nInizio sctrittura su file...");
        if(conv[2] == 32) {
            puts("il carattere ' ' sarà usato come delimitatore");
            }
        for(i = 0; i < n; i ++) {                    //scrittura prolissa file uscita
            stato = fprintf(fOut, conv, file[i]);
            if(stato == 0) {
                puts(">> Errore nella scrittura del file");
                exit(EXIT_FAILURE);
                }
            }
        puts("Fine scrittura");
        }
    else {
        for(i = 0; i < n; i ++) {                    //scrittura normale file uscita
            fprintf(fOut, conv, file[i]);
            }
        }

    puts("\nFinito!\n");
    }

Quest'ultimo blocco si occupa della scrittura dell'array appena ordinato sul file in uscita.
Ancora una volta la chiave "prolisso" del registro "reg" permette di scegliere la versione complicata, con cui il programma ci indica inizio e fine delle operazioni di scrittura, e in più ci indica se c'è stato qualche problema chiudendo con errore il programma. Anche qui il blocco else contiene il codice semplificato.
Dopo tutto ciò, il programma stampa a viedeo il messaggio finale e la funzione si chiude, cedendo di nuovo il controllo al "main" che subito dopo chiuderà il programma.

Allego un archivio contenente il file sorgente completo, un diagramma di flusso disegnato in ASCII(roba da intenditori... ) e un file eseguibile compilato si Ubuntu 9.10.
Il codice è stato scritto in ambiente linux ma è completamente compatibile con windows, dato che non è stata usata alcuna funzione al di fuori delle librerie ANSI.
Nonostante l'algoritmo dell'ordinamento a bolle abbia un costo nel caso peggiore di O(n^2), se non si usa ma modalità prolisso il tutto è abbastanza  veloce, un vocabolario da 135.000 lemmi viene messo in ordine in 1 secondo circa con due CPU a 800MHz abbastanza libere.
Spero  che tutto ciò serva a quacuno

, Guru
Joomla 1.7 Templates designed by College Jacke