Salta al contenuto principale

tee e xargs: dividere e costruire flussi di comandi

Inviato da tuxsa il

tee e xargs sono due piccoli strumenti che completano la pipe. tee divide un flusso: lo mostra a schermo e lo salva in un file nello stesso momento. xargs lo trasforma: prende l'elenco che gli arriva e lo usa per costruire ed eseguire altri comandi. Insieme permettono di fare in una riga cose che altrimenti richiederebbero uno script.

Prima di cominciare: input standard e argomenti

Un comando può ricevere dati in due modi diversi, ed è il punto da capire per usare bene questi due strumenti:

  • come argomenti, scritti dopo il nome: in rm file1 file2, file1 e file2 sono argomenti;
  • come input standard (stdin), il flusso di dati che arriva da una pipe o da un reindirizzamento.

Alcuni comandi, come sort, wc o grep, leggono volentieri lo stdin. Altri, come rm, cp, mkdir, kill, lo ignorano e vogliono solo argomenti. Per questo ls | rm non funziona: rm non guarda ciò che arriva dalla pipe. xargs serve a colmare questa distanza.

Cosa sono

tee prende il nome dal raccordo a T degli impianti idraulici: il flusso entra da una parte e si divide in due uscite. Fa parte del pacchetto GNU coreutils (/usr/bin/tee).

xargs è l'abbreviazione di extended arguments, "argomenti estesi". Fa parte del pacchetto GNU findutils, lo stesso di find (/usr/bin/xargs), ed è nato per lavorare con lui.

Cosa fanno

  • tee legge lo standard input e lo scrive sia sullo standard output sia in uno o più file. Il testo continua il suo cammino nella pipe, e intanto ne resta una copia.
  • xargs legge dallo standard input un elenco di elementi (separati da spazi o a capo) e li passa come argomenti a un comando, lanciandolo una o più volte. Se il comando non è indicato, usa echo.

Come si usano

$ comando | tee [opzioni] file...
$ comando | xargs [opzioni] [comando [argomenti]]

tee

Esempio 1: mostrare e salvare insieme

$ uname -srm | tee sistema.txt
Linux 6.8.0-45-generic x86_64
$ cat sistema.txt
Linux 6.8.0-45-generic x86_64

Il risultato compare a schermo e viene scritto anche in sistema.txt. Con il solo reindirizzamento > sistema.txt non vedresti nulla.

Esempio 2: aggiungere in coda con -a

$ echo "controllo eseguito" | tee -a registro.txt
controllo eseguito

Per impostazione predefinita tee sovrascrive il file, come farebbe >. Con -a (append) aggiunge invece in fondo, come >>.

Esempio 3: scrivere in più file

$ date | tee data1.txt data2.txt

Tutti i nomi indicati dopo le opzioni ricevono una copia.

Esempio 4: tee nel mezzo di una pipeline

$ du -sh ~/* 2>/dev/null | sort -h | tee dimensioni.txt | tail -n 3
2,1G	/home/enzo/Immagini
3,4G	/home/enzo/Video
5,2G	/home/enzo/Archivio

A dimensioni.txt arriva l'elenco completo e ordinato, mentre a schermo ne compaiono solo le ultime tre righe. È utile per salvare un risultato intermedio senza interrompere la catena.

Esempio 5: registrare l'output di un comando con tutti i messaggi

$ ./installa.sh 2>&1 | tee installazione.log

Con 2>&1 anche i messaggi di errore (stderr) vengono inviati nella pipe insieme all'output normale, e tee li mostra e li salva. È il modo più semplice per tenere un registro di un'installazione o di una compilazione e continuare a seguirla a schermo. In Bash esiste la scorciatoia |&, equivalente a 2>&1 |.

Esempio 6: scrivere un file di sistema con sudo

$ echo "127.0.0.1 miosito.local" | sudo tee -a /etc/hosts
127.0.0.1 miosito.local
$ echo "127.0.0.1 miosito.local" | sudo tee -a /etc/hosts > /dev/null

È la soluzione al problema del reindirizzamento visto negli articoli su sudo ed echo: sudo echo ... >> file non funziona, perché il reindirizzamento lo fa la tua shell senza privilegi. Qui invece è tee a scrivere il file, e lo fa con sudo. Il secondo comando aggiunge > /dev/null per non vedere ripetuto il testo a schermo.

Esempio 7: creare un file di configurazione di più righe

$ sudo tee /etc/nginx/conf.d/prova.conf > /dev/null << 'EOF'
server {
    listen 8080;
    root /var/www/prova;
}
EOF

Con il here document, visto nell'articolo su cat, si scrive un testo di più righe direttamente in un file protetto.

Esempio 8: dividere il flusso verso più comandi

$ cat frutta.txt | tee >(sort -u > uniche.txt) >(wc -l > righe.txt) > /dev/null

La forma >(comando), detta process substitution, funziona in Bash e Zsh (non in sh) e permette a tee di inviare il flusso a più comandi, e non solo a file. Qui lo stesso elenco viene ordinato senza doppioni in un file e contato in un altro. I comandi girano in parallelo, e subito dopo il ritorno del prompt i file possono non essere ancora completi.

Esempio 9: uso negli script

#!/bin/bash
set -o pipefail
LOG="$HOME/backup.log"

registra() {
    echo "$(date '+%F %T') $*" | tee -a "$LOG"
}

registra "Avvio backup"
tar -czf "$HOME/backup.tar.gz" -C "$HOME" Documenti 2>&1 | tee -a "$LOG"
registra "Backup terminato"

Ogni messaggio viene mostrato e anche aggiunto al registro. L'istruzione set -o pipefail serve perché, in una pipeline, per impostazione predefinita conta solo l'esito dell'ultimo comando (tee, che riesce sempre): con pipefail un errore di tar si riflette nel codice di uscita della pipeline ($?), che puoi controllare subito dopo.

xargs

Esempio 10: il comportamento di base

$ echo "uno due tre" | xargs
uno due tre
$ echo "uno due tre" | xargs -n 1
uno
due
tre

Senza un comando, xargs usa echo. Con -n 1 passa un solo elemento per volta, e quindi echo viene lanciato tre volte. È un buon modo per capire come xargs spezza l'input.

Esempio 11: eliminare i file trovati da find

$ find . -name "*.tmp" -print0 | xargs -0 rm

Il comando find produce l'elenco dei file e xargs lo passa a rm come argomenti. L'accoppiata -print0 e -0 è fondamentale: separa i nomi con un carattere nullo invece che con lo spazio. Senza di essa, un file chiamato mio file.tmp verrebbe spezzato in due elementi, mio e file.tmp, e rm proverebbe a eliminare due file che forse non esistono, o peggio, che esistono ma non volevi toccare.

Esempio 12: guardare prima di eseguire

$ find . -name "*.bak" -print0 | xargs -0 echo rm
rm ./vecchio.bak ./nota.bak
$ find . -name "*.bak" -print0 | xargs -0 -t rm
rm ./vecchio.bak ./nota.bak
$ find . -name "*.bak" -print0 | xargs -0 -p rm
rm ./vecchio.bak ./nota.bak ?...y

Tre modi per evitare brutte sorprese: anteporre echo al comando (si vede cosa verrebbe eseguito, come per rm *.tmp), usare -t (trace), che stampa ogni comando prima di lanciarlo, oppure -p (prompt), che chiede conferma.

Esempio 13: quanti elementi per comando con -n

$ printf '%s\n' a b c d e | xargs -n 2 echo
a b
c d
e

Con -n 2 ogni esecuzione riceve al massimo due argomenti.

Esempio 14: scegliere dove va l'elemento con -I

$ cat nomi.txt
Rossi
Bianchi
Verdi
$ cat nomi.txt | xargs -I{} mkdir -p "clienti/{}"
$ ls clienti
Bianchi  Rossi  Verdi

Di default xargs mette gli elementi in fondo al comando. Con -I{} (il segno {} è un segnaposto a scelta) decidi tu dove inserirli, e il comando viene eseguito una volta per ogni riga. Con -I l'input viene separato solo dagli a capo, quindi i nomi con spazi restano interi.

Esempio 15: più processi in parallelo con -P

$ find /var/log/miaapp -name "*.log" -print0 | xargs -0 -n 1 -P 4 gzip
$ cat indirizzi.txt | xargs -n 1 -P 4 curl -sO

Con -P 4 xargs esegue fino a quattro comandi in contemporanea. Il primo comprime quattro log alla volta, il secondo scarica quattro file alla volta. Per usare tutti i processori del computer: -P "$(nproc)". Con l'esecuzione parallela l'ordine di completamento non è prevedibile, e l'output di processi diversi può mescolarsi.

Esempio 16: cercare un testo nei file trovati

$ find . -name "*.php" -print0 | xargs -0 grep -l "mysql_connect"
./vecchio/db.php
./vecchio/utenti.php

Equivale a find ... -exec grep -l "mysql_connect" {} +, visto nell'articolo su find. Con xargs si possono però aggiungere -P per il parallelismo e usare qualsiasi elenco, e non solo quello di find.

Esempio 17: separatore personalizzato con -d

$ echo -n "a,b,c" | xargs -d ',' -n 1
a
b
c

Con -d si sceglie il carattere che separa gli elementi. Il -n di echo evita l'a capo finale, che altrimenti verrebbe considerato parte dell'ultimo elemento. Una forma molto utile è -d '\n': separa solo agli a capo, e i nomi con spazi restano interi, quando l'elenco non proviene da find -print0.

Esempio 18: tee e xargs insieme

$ grep -rl "ERROR" /var/log/miaapp | tee file_con_errori.txt | xargs -d '\n' ls -lh

La ricerca produce l'elenco dei file con errori, tee lo salva in file_con_errori.txt e xargs lo passa a ls -lh per vederne le dimensioni.

Esempio 19: fermare i processi trovati

$ pgrep -f "miascript.py" | xargs -r kill

Il comando pgrep stampa i PID e xargs li passa a kill come argomenti. Con -r (no-run-if-empty) il comando non viene eseguito se l'elenco è vuoto.

Esempio 20: uso negli script

#!/bin/bash
LOG_DIR="/var/log/miaapp"

[ -d "$LOG_DIR" ] || { echo "Cartella non trovata" >&2; exit 1; }

# comprime in parallelo i log più vecchi di 7 giorni
find "$LOG_DIR" -type f -name "*.log" -mtime +7 -print0 \
    | xargs -0 -r -n 1 -P "$(nproc)" gzip

echo "Compressione completata"

Opzioni utili

Per tee:

  • -a: aggiunge in coda ai file invece di sovrascriverli.
  • -i: ignora il segnale di interruzione (Ctrl+C).
  • -p: continua a scrivere sui file anche se l'uscita verso la pipe si chiude (vedi le avvertenze).

Per xargs:

  • -0: separa gli elementi con il carattere nullo (si abbina a find -print0).
  • -d carattere: sceglie il separatore (per esempio -d '\n').
  • -n N: al massimo N argomenti per esecuzione.
  • -I{}: segnaposto per il punto in cui inserire l'elemento, un'esecuzione per elemento.
  • -P N: fino a N esecuzioni in parallelo.
  • -r: non esegue il comando se l'input è vuoto.
  • -t: stampa ogni comando prima di eseguirlo; -p: chiede conferma.
  • -L N: usa N righe di input per ogni esecuzione.

Attenzione a

  • tee sovrascrive: senza -a, il file indicato viene svuotato. E non usare lo stesso file come ingresso e come uscita (cat file | tee file): il file viene svuotato prima di essere letto, come per cat file > file.
  • L'esito di una pipeline è quello dell'ultimo comando: comando | tee log riesce sempre, anche se comando fallisce. In Bash si usa set -o pipefail oppure si legge ${PIPESTATUS[0]} subito dopo.
  • L'errore va in un altro canale: tee registra solo lo standard output. Per includere gli errori scrivi 2>&1 prima della pipe.
  • Con sudo davanti al comando sbagliato: sudo comando | tee file dà i privilegi a comando ma non a tee, e la scrittura può fallire. Il privilegio serve a tee: comando | sudo tee file.
  • Quando si chiude il lato destro: in comando | tee file | head -n 3, quando head ha letto le sue righe e si chiude, tee riceve un errore di scrittura e termina, e il file può restare incompleto. Con tee -p continua a scrivere sul file.
  • Colori e programmi interattivi: molti programmi, quando l'output passa da una pipe, smettono di usare i colori e di visualizzare le barre di avanzamento, perché non sono più collegati a un terminale. Con alcuni programmi si può forzare (ls --color=always), ma nei file di registro i codici di colore diventano caratteri indesiderati. Inoltre, un programma che mette l'output in un buffer può mostrarlo a blocchi: stdbuf -oL comando lo forza riga per riga.
  • I nomi con spazi in xargs: per impostazione predefinita xargs separa su spazi e a capo, e interpreta anche apici e barre rovesciate. Un nome con spazio viene spezzato e uno con un apostrofo può dare l'errore unmatched single quote. Usa sempre find -print0 | xargs -0 oppure -d '\n'.
  • Mai ls | xargs: l'output di ls non è fatto per essere elaborato da altri programmi. Usa find con -print0 o i caratteri jolly.
  • Input vuoto: con GNU xargs, se non arriva nulla, il comando viene eseguito lo stesso, una volta, senza argomenti. Con rm otterresti un errore di operando mancante, ma con altri comandi l'effetto può essere ben diverso (ls mostrerebbe la cartella corrente). Aggiungi -r. Su macOS e sui sistemi BSD il comportamento predefinito è il contrario e l'opzione -r non serve.
  • Più esecuzioni, non una: se i parametri sono molti, xargs lancia il comando più volte, a gruppi. I comandi che si aspettano di vedere tutti i file insieme possono dare risultati sbagliati: find ... | xargs tar -cf archivio.tar può creare un archivio con solo l'ultimo gruppo, perché ogni esecuzione ricrea il file. In quel caso tar ha un'opzione dedicata: find ... -print0 | tar --null -czf archivio.tar.gz -T -. Anche i totali di wc compaiono più volte.
  • Un solo carattere in più: xargs rm con un elenco sbagliato elimina davvero. Prima di una cancellazione di massa, anteponi echo o usa -p.
  • L'esito di xargs: restituisce 0 se tutte le esecuzioni riescono, 123 se almeno una è terminata con un errore, e altri codici in casi particolari (per esempio 127 se il comando non esiste).
  • I comandi interattivi (come vim o less) con xargs non ricevono la tastiera, perché il loro input è occupato dall'elenco. Con GNU xargs l'opzione -o li riapre sul terminale.

Riepilogo

ComandoCosa fa
comando | tee fileMostra l'output e lo salva in un file
comando | tee -a fileCome sopra, aggiungendo in coda
comando 2>&1 | tee logRegistra anche i messaggi di errore
echo "x" | sudo tee -a /etc/fileScrive in un file protetto
… | tee file | altroSalva un risultato intermedio e continua la catena
… | xargs comandoPassa l'elenco come argomenti al comando
find … -print0 | xargs -0 comandoCome sopra, sicuro con spazi e caratteri strani
… | xargs -n 1 comandoUn elemento per esecuzione
… | xargs -I{} comando {} altroInserisce l'elemento dove vuoi
… | xargs -P 4 comandoEsegue fino a 4 comandi in parallelo
… | xargs -r comandoNon esegue nulla se l'elenco è vuoto
… | xargs echo comando / -t / -pAnteprima, traccia o conferma prima di eseguire