Successivo: , Precedente: , Su: Cloni   [Contenuti][Indice]


11.2.6 Stampare righe di testo non duplicate

Il programma di utilità uniq legge righe di dati ordinati sul suo standard input, e per default rimuove righe duplicate. In altre parole, stampa solo righe uniche; da cui il nome. uniq ha diverse opzioni. La sintassi è la seguente:

uniq [-udc [-n]] [+n] [file_input [file_output]]

Le opzioni per uniq sono:

-d

Stampa solo righe ripetute (duplicate).

-u

Stampa solo righe non ripetute (uniche).

-c

Contatore righe. Quest’opzione annulla le opzioni -d e -u. Sia le righe ripetute che quelle non ripetute vengono contate.

-n

Salta n campi prima di confrontare le righe. La definizione di campo è simile al default di awk: caratteri non bianchi, separati da sequenze di spazi e/o TAB.

+n

Salta n caratteri prima di confrontare le righe. Eventuali campi specificati con ‘-n’ sono saltati prima.

file_input

I dati sono letti dal file in input specificato sulla riga di comando, invece che dallo standard input.

file_output

L’output generato è scritto sul file di output specificato, invece che sullo standard output.

Normalmente uniq si comporta come se siano state specificate entrambe le opzioni -d e -u.

uniq usa la funzione di libreria getopt() (vedi la sezione Elaborare opzioni specificate sulla riga di comando) e la funzione di libreria join() (vedi la sezione Trasformare un vettore in una sola stringa).

Il programma inizia con una funzione sintassi() e poi con una breve spiegazione delle opzioni e del loro significato, sotto forma di commenti. La regola BEGIN elabora gli argomenti della riga di comando e le opzioni. Viene usato un artificio per poter impiegare getopt() con opzioni della forma ‘-25’, trattando quest’opzione come la lettera di opzione ‘2’ con l’argomento ‘5’. Se si specificano due o più cifre (Optarg sembra essere numerico), Optarg è concatenato con la cifra che costituisce l’opzione e poi al risultato è addizionato zero, per trasformarlo in un numero. Se c’è solo una cifra nell’opzione, Optarg non è necessario. In tal caso, Optind dev’essere decrementata, in modo che getopt() la elabori quando viene nuovamente richiamato. Questo codice è sicuramente un po’ intricato.

Se non sono specificate opzioni, per default si stampano sia le righe ripetute che quelle non ripetute. Il file di output, se specificato, è assegnato a file_output. In precedenza, file_output è inizializzato allo standard output, /dev/stdout:

# uniq.awk --- implementa uniq in awk
#
# Richiede le funzioni di libreria getopt() e join()

function sintassi()
{
    print("sintassi: uniq [-udc [-n]] [+n] [ in [ out ]]") > "/dev/stderr"
    exit 1
}

# -c    contatore di righe. prevale su -d e -u
# -d    solo righe ripetute
# -u    solo righe non ripetute
# -n    salta n campi
# +n    salta n caratteri, salta prima eventuali campi

BEGIN {
    contatore = 1
    file_output = "/dev/stdout"
    opts = "udc0:1:2:3:4:5:6:7:8:9:"
    while ((c = getopt(ARGC, ARGV, opts)) != -1) {
        if (c == "u")
            solo_non_ripetute++
        else if (c == "d")
            solo_ripetute++
        else if (c == "c")
            conta_record++
        else if (index("0123456789", c) != 0) {
            # getopt() richiede argomenti per le opzioni
            # questo consente di gestire cose come -5
            if (Optarg ~ /^[[:digit:]]+$/)
                contatore_file = (c Optarg) + 0
            else {
                contatore_file = c + 0
                Optind--
            }
        } else
            sintassi()
    }

    if (ARGV[Optind] ~ /^\+[[:digit:]]+$/) {
        conta_caratteri = substr(ARGV[Optind], 2) + 0
        Optind++
    }

    for (i = 1; i < Optind; i++)
        ARGV[i] = ""

    if (solo_ripetute == 0 && solo_non_ripetute == 0)
        solo_ripetute = solo_non_ripetute = 1

    if (ARGC - Optind == 2) {
        file_output = ARGV[ARGC - 1]
        ARGV[ARGC - 1] = ""
    }
}

La funzione seguente, se_sono_uguali(), confronta la riga corrente, $0, con la riga precedente, ultima. Gestisce il salto di campi e caratteri. Se non sono stati richiesti né contatori di campo né contatori di carattere, se_sono_uguali() restituisce uno o zero a seconda del risultato di un semplice confronto tra le stringhe ultima e $0.

In caso contrario, le cose si complicano. Se devono essere saltati dei campi, ogni riga viene suddivisa in un vettore, usando split() (vedi la sezione Funzioni di manipolazione di stringhe); i campi desiderati sono poi nuovamente uniti in un’unica riga usando join(). Le righe ricongiunte vengono immagazzinate in campi_ultima e campi_corrente. Se non ci sono campi da saltare, campi_ultima e campi_corrente sono impostati a ultima e $0, rispettivamente. Infine, se occorre saltare dei caratteri, si usa substr() per eliminare i primi conta_caratteri caratteri in campi_ultima e campi_corrente. Le due stringhe sono poi confrontare e se_sono_uguali() restituisce il risultato del confronto:

function se_sono_uguali(    n, m, campi_ultima, campi_corrente,\
vettore_ultima, vettore_corrente)
{
    if (contatore_file == 0 && conta_caratteri == 0)
        return (ultima == $0)

    if (contatore_file > 0) {
        n = split(ultima, vettore_ultima)
        m = split($0, vettore_corrente)
        campi_ultima = join(vettore_ultima, contatore_file+1, n)
        campi_corrente = join(vettore_corrente, contatore_file+1, m)
    } else {
        campi_ultima = ultima
        campi_corrente = $0
    }
    if (conta_caratteri) {
        campi_ultima = substr(campi_ultima, conta_caratteri + 1)
        campi_corrente = substr(campi_corrente, conta_caratteri + 1)
    }

    return (campi_ultima == campi_corrente)
}

Le due regole seguenti sono il corpo del programma. La prima è eseguita solo per la prima riga dei dati. Imposta ultima al record corrente $0, in modo che le righe di testo successive abbiano qualcosa con cui essere confrontate.

La seconda regola fa il lavoro. La variabile uguale vale uno o zero, a seconda del risultato del confronto effettuato in se_sono_uguali(). Se uniq sta contando le righe ripetute, e le righe sono uguali, viene incrementata la variabile contatore. Altrimenti, viene stampata la riga e azzerato contatore, perché le due righe non sono uguali.

Se uniq non sta contando, e se le righe sono uguali, contatore è incrementato. Non viene stampato niente, perché l’obiettivo è quello di rimuovere i duplicati. Altrimenti, se uniq sta contando le righe ripetute e viene trovata più di una riga, o se uniq sta contando le righe non ripetute e viene trovata solo una riga, questa riga viene stampata, e contatore è azzerato.

Infine, una logica simile è usata nella regola END per stampare l’ultima riga di dati in input:

NR == 1 {
    ultima = $0
    next
}

{
    uguale = se_sono_uguali()

    if (conta_record) {    # prevale su -d e -u
        if (uguale)
            contatore++
        else {
            printf("%4d %s\n", contatore, ultima) > file_output
            ultima = $0
            contatore = 1    # reset
        }
        next
    }

    if (uguale)
        contatore++
    else {
        if ((solo_ripetute && contatore > 1) ||
            (solo_non_ripetute && contatore == 1))
                print ultima > file_output
        ultima = $0
        contatore = 1
    }
}

END {
    if (conta_record)
        printf("%4d %s\n", contatore, ultima) > file_output
    else if ((solo_ripetute && contatore > 1) ||
            (solo_non_ripetute && contatore == 1))
        print ultima > file_output
    close(file_output)
}

Incidentalmente, questo programma non segue la convenzione, raccomandabile, di assegnare alle variabili globali un nome con la lettera iniziale maiuscola. Se lo si fosse fatto, il programma sarebbe stato un po’ più semplice da comprendere.


Successivo: , Precedente: , Su: Cloni   [Contenuti][Indice]