Quando si tratta di dividere un file di testo in più file in Linux, la maggior parte delle persone usa il comando split. Non c'è niente di sbagliato nel comando split, tranne che si basa sulla dimensione del byte o sulla dimensione della riga per dividere i file.
Questo non è conveniente nelle situazioni in cui è necessario dividere i file in base al contenuto, anziché alle dimensioni. Ti faccio un esempio.
Gestisco i miei tweet programmati utilizzando i file YAML. Un tipico file tweet contiene diversi tweet, separati da quattro trattini:
----
event:
repeat: { days: 180 }
status: |
I think I use the `sed` command daily. And you?
https://www.yesik.it/EP07
#Shell #Linux #Sed #YesIKnowIT
----
status: |
Print the first column of a space-separated data file:
awk '{print $1}' data.txt # Print out just the first column
For some unknown reason, I find that easier to remember than:
cut -f1 data.txt
#Linux #AWK #Cut
----
status: |
For the #shell #beginners :
[...]
Quando li importo nel mio sistema, devo scrivere ogni tweet nel proprio file. Lo faccio per evitare di registrare tweet duplicati.
Ma come dividere un file in più parti in base al suo contenuto? Bene, probabilmente puoi ottenere qualcosa di convincente usando i comandi awk:
sh$ awk < tweets.yaml '
> /----/ { OUTPUT="tweet." (N++) ".yaml" }
> { print > OUTPUT }
> '
Tuttavia, nonostante una relativa semplicità, una soluzione del genere non è molto robusta:ad esempio, non ho chiuso correttamente i vari file di output, quindi questo potrebbe benissimo raggiungere il limite di file aperti. O se ho dimenticato il separatore prima del primo tweet del file? Naturalmente, tutto ciò può essere gestito e corretto nello script AWK, a scapito di renderlo più complesso. Ma perché preoccuparsene quando abbiamo il csplit
strumento per svolgere tale compito?
Utilizzo di csplit per dividere i file in Linux
Il csplit
strumento è un cugino di split
strumento che può essere utilizzato per dividere un file in dimensioni fisse pezzi. Ma csplit
identificherà i limiti del blocco in base al contenuto del file, anziché utilizzare il conteggio dei byte.
In questo tutorial, dimostrerò l'utilizzo del comando csplit e spiegherò anche l'output di questo comando.
Quindi, ad esempio, se voglio dividere il mio file tweet in base al ----
delimitatore, potrei scrivere:
sh$ csplit tweets.yaml /----/
0
10846
Potresti aver indovinato il csplit
strumento ha utilizzato l'espressione regolare fornita sulla riga di comando per identificare il separatore. E quali potrebbero essere quei 0
e 10983
risultato visualizzato sullo standard output? Bene, sono la dimensione in byte di ogni blocco di dati creato.
sh$ ls -l xx0*
-rw-r--r-- 1 sylvain sylvain 0 Jun 6 11:30 xx00
-rw-r--r-- 1 sylvain sylvain 10846 Jun 6 11:30 xx01
Apetta un minuto! Dove quei xx00
e xx01
da cui provengono i nomi dei file? E perché csplit
dividere il file in solo due blocchi ? E perché il primo blocco di dati ha una lunghezza di zero byte ?
La risposta alla prima domanda è semplice:xxNN
(o più formalmente xx%02d
) è il formato del nome file predefinito utilizzato da csplit
. Ma puoi cambiarlo usando il --suffix-format
e --prefix
opzioni. Ad esempio, potrei cambiare il formato in qualcosa di più significativo per le mie esigenze:
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> /----/
0
10846
sh$ ls -l tweet.*
-rw-r--r-- 1 sylvain sylvain 0 Jun 6 11:30 tweet.000.yaml
-rw-r--r-- 1 sylvain sylvain 10846 Jun 6 11:30 tweet.001.yaml
Il prefisso è una semplice stringa, ma il suffisso è una stringa di formato come quella usata dalla libreria C standard printf
funzione. La maggior parte dei caratteri del formato verrà utilizzata alla lettera, ad eccezione delle specifiche di conversione introdotte dal segno di percentuale (%
) e che termina con un identificatore di conversione (qui, d
). Nel mezzo, il formato può anche contenere vari flag e opzioni. Nel mio esempio, il %03d
specifica di conversione significa:
- visualizza il numero del blocco come numero intero decimale (
d
), - in un campo di tre caratteri (
3
), - alla fine riempito a sinistra con zeri (
0
).
Ma questo non riguarda gli altri interrogatori che ho avuto sopra:perché ne abbiamo solo due blocchi, uno dei quali contenente zero byte? Forse hai già trovato la risposta a quest'ultima domanda da solo:il mio file di dati inizia con ----
sulla sua prima riga. Quindi, csplit
lo considerava un delimitatore e poiché non c'erano dati prima di quella riga, ha creato un primo blocco vuoto. Possiamo disabilitare la creazione di file di lunghezza zero byte usando il --elide-empty-files
opzione:
sh$ rm tweet.*
rm: cannot remove 'tweet.*': No such file or directory
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> /----/
10846
sh$ ls -l tweet.*
-rw-r--r-- 1 sylvain sylvain 10846 Jun 6 11:30 tweet.000.yaml
Ok:niente più file vuoti. Ma in un certo senso, il risultato è il peggiore ora, da csplit
dividere il file in solo uno pezzo. Riusciamo a malapena a chiamare questo "suddivisione" di un file, vero?
La spiegazione di quel risultato sorprendente è csplit
non supponiamo che ogni mandrino debba essere diviso in base allo stesso separatore. In realtà, csplit
richiede di fornire ogni separatore utilizzato. Anche se è più volte uguale:
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> /----/ /----/ /----/
170
250
10426
Ho inserito tre separatori (identici) sulla riga di comando. Quindi, csplit
identificato la fine del primo blocco in base al primo separatore. Porta a un blocco di lunghezza zero byte che è stato eliminato. Il secondo blocco è stato delimitato dalla riga successiva che corrisponde a /----/
. Porta a un blocco di 170 byte. Infine, è stato identificato un terzo blocco di 250 byte basato sul terzo separatore. I dati rimanenti, 10426 byte, sono stati inseriti nell'ultimo blocco.
sh$ ls -l tweet.???.yaml
-rw-r--r-- 1 sylvain sylvain 170 Jun 6 11:30 tweet.000.yaml
-rw-r--r-- 1 sylvain sylvain 250 Jun 6 11:30 tweet.001.yaml
-rw-r--r-- 1 sylvain sylvain 10426 Jun 6 11:30 tweet.002.yaml
Ovviamente, non sarebbe pratico se dovessimo fornire tanti separatori sulla riga di comando quanti sono i blocchi nel file di dati. Soprattutto perché quel numero esatto di solito non è noto in anticipo. Fortunatamente, csplit
ha un pattern speciale che significa "ripetere il pattern precedente il più possibile." Nonostante la sua sintassi ricorda il quantificatore di stelle in un'espressione regolare, questo è più vicino al concetto di Kleene plus poiché viene utilizzato per ripetere un separatore che ha già stato abbinato una volta:
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> /----/ '{*}'
170
250
190
208
140
[...]
247
285
194
214
185
131
316
221
E questa volta, finalmente, ho diviso la mia raccolta di tweet in singole parti. Tuttavia, csplip
hai altri bei modelli "speciali" come quello? Bene, non so se possiamo chiamarli "speciali", ma sicuramente, csplit
capire di più sui modelli.
Altri pattern csplit
Abbiamo appena visto nella sezione precedente come utilizzare il quantificatore '{*}' per le ripetizioni non legate. Tuttavia, sostituendo la stella con un numero, puoi richiedere un numero esatto di ripetizioni:
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> /----/ '{6}'
170
250
190
208
140
216
9672
Questo porta a un interessante caso d'angolo. Cosa verrebbe aggiunto se il numero di ripetizioni superasse il numero di delimitatori effettivi nel file di dati? Bene, vediamolo su un esempio:
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> /----/ '{999}'
csplit: ‘/----/’: match not found on repetition 62
170
250
190
208
[...]
91
247
285
194
214
185
131
316
221
sh$ ls tweet.*
ls: cannot access 'tweet.*': No such file or directory
È interessante notare che non solo csplit
ha segnalato un errore, ma ha anche rimosso tutto i file chunk creati durante il processo. Presta particolare attenzione alla mia formulazione:è rimossa loro. Ciò significa che i file sono stati creati, quindi, quando csplit
riscontrato l'errore, li ha cancellati. In altre parole, se hai già un file il cui nome sembra un file chunk, verrà rimosso:
sh$ touch tweet.002.yaml
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> /----/ '{999}'
csplit: ‘/----/’: match not found on repetition 62
170
250
190
[...]
87
91
247
285
194
214
185
131
316
221
sh$ ls tweet.*
ls: cannot access 'tweet.*': No such file or directory
Nell'esempio sopra, il tweet.002.yaml
il file che abbiamo creato manualmente è stato sovrascritto, quindi rimosso da csplit
.
Puoi cambiare quel comportamento usando il --keep-files
opzione. Come suggerisce il nome, non rimuoverà i blocchi creati da csplit dopo aver riscontrato un errore:
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> --keep-files \
> /----/ '{999}'
csplit: ‘/----/’: match not found on repetition 62
170
250
190
[...]
316
221
sh$ ls tweet.*
tweet.000.yaml
tweet.001.yaml
tweet.002.yaml
tweet.003.yaml
[...]
tweet.058.yaml
tweet.059.yaml
tweet.060.yaml
tweet.061.yaml
Nota in tal caso, e nonostante l'errore, csplit
non ha scartato alcun dato:
sh$ diff -s tweets.yaml <(cat tweet.*)
Files tweets.yaml and /dev/fd/63 are identical
Ma cosa succede se ci sono dei dati nel file che voglio scartare? Bene, csplit
ha un supporto limitato per l'utilizzo di un %regex%
modello.
Salto dei dati in csplit
Quando si utilizza un segno di percentuale (%
) come delimitatore regex invece di una barra (/
), csplit
salta dati fino a (ma esclusa) la prima riga che corrisponde all'espressione regolare. Questo può essere utile per ignorare alcuni record, specialmente all'inizio o alla fine del file di input:
sh$ # Keep only the first two tweets
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> --keep-files \
> /----/ '{2}' %----% '{*}'
170
250
sh$ head tweet.00[012].yaml
==> tweet.000.yaml <==
----
event:
repeat: { days: 180 }
status: |
I think I use the `sed` command daily. And you?
https://www.yesik.it/EP07
#Shell #Linux #Sed #YesIKnowIT
==> tweet.001.yaml <==
----
status: |
Print the first column of a space-separated data file:
awk '{print $1}' data.txt # Print out just the first column
For some unknown reason, I find that easier to remember than:
cut -f1 data.txt
#Linux #AWK #Cut
sh$ # Skip the first two tweets
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> --keep-files \
> %----% '{2}' /----/ '{2}'
190
208
140
9888
sh$ head tweet.00[012].yaml
==> tweet.000.yaml <==
----
status: |
For the #shell #beginners :
« #GlobPatterns : how to move hundreds of files in not time [1/3] »
https://youtu.be/TvW8DiEmTcQ
#Unix #Linux
#YesIKnowIT
==> tweet.001.yaml <==
----
status: |
Want to know the oldest file in your disk?
find / -type f -printf '%TFT%.8TT %p\n' | sort | less
(should work on any Single UNIX Specification compliant system)
#UNIX #Linux
==> tweet.002.yaml <==
----
status: |
When using the find command, use `-iname` instead of `-name` for case-insensitive search
#Unix #Linux #Shell #Find
sh$ # Keep only the third and fourth tweets
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> --keep-files \
> %----% '{2}' /----/ '{2}' %----% '{*}'
190
208
140
sh$ head tweet.00[012].yaml
==> tweet.000.yaml <==
----
status: |
For the #shell #beginners :
« #GlobPatterns : how to move hundreds of files in not time [1/3] »
https://youtu.be/TvW8DiEmTcQ
#Unix #Linux
#YesIKnowIT
==> tweet.001.yaml <==
----
status: |
Want to know the oldest file in your disk?
find / -type f -printf '%TFT%.8TT %p\n' | sort | less
(should work on any Single UNIX Specification compliant system)
#UNIX #Linux
==> tweet.002.yaml <==
----
status: |
When using the find command, use `-iname` instead of `-name` for case-insensitive search
#Unix #Linux #Shell #Find
Utilizzo degli offset durante la divisione dei file con csplit
Quando si utilizzano espressioni regolari (o /…/
o %…%
) puoi specificare un positivo (+N
) o negativo (-N
) offset alla fine del pattern, quindi csplit
dividerà il file N righe dopo o prima della riga corrispondente. Ricorda, in tutti i casi, il modello specifica la fine del pezzo:
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> --keep-files \
> %----%+1 '{2}' /----/+1 '{2}' %----% '{*}'
190
208
140
sh$ head tweet.00[012].yaml
==> tweet.000.yaml <==
status: |
For the #shell #beginners :
« #GlobPatterns : how to move hundreds of files in not time [1/3] »
https://youtu.be/TvW8DiEmTcQ
#Unix #Linux
#YesIKnowIT
----
==> tweet.001.yaml <==
status: |
Want to know the oldest file in your disk?
find / -type f -printf '%TFT%.8TT %p\n' | sort | less
(should work on any Single UNIX Specification compliant system)
#UNIX #Linux
----
==> tweet.002.yaml <==
status: |
When using the find command, use `-iname` instead of `-name` for case-insensitive search
#Unix #Linux #Shell #Find
----
Dividi per numero di riga
Abbiamo già visto come possiamo usare un'espressione regolare per dividere i file. In tal caso, csplit
dividerà il file alla prima riga corrispondente quella regex. Ma puoi anche identificare la linea divisa in base al suo numero di linea come la vedremo ora.
Prima di passare a YAML, memorizzavo i miei tweet programmati in un file flat.
In quel file, un tweet era composto da due righe. Uno contenente una ripetizione facoltativa e il secondo contenente il testo del tweet, con nuove righe sostituite da \n. Ancora una volta quel file di esempio è disponibile online.
Con quel formato a "dimensione fissa" è stato anche possibile utilizzare csplit
per inserire ogni singolo tweet nel proprio file:
sh$ csplit tweets.txt \
> --prefix='tweet.' --suffix-format='%03d.txt' \
> --elide-empty-files \
> --keep-files \
> 2 '{*}'
csplit: ‘2’: line number out of range on repetition 62
1
123
222
161
182
119
184
81
148
128
142
101
107
[...]
sh$ diff -s tweets.txt <(cat tweet.*.txt)
Files tweets.txt and /dev/fd/63 are identical
sh$ head tweet.00[012].txt
==> tweet.000.txt <==
==> tweet.001.txt <==
{ days:180 }
I think I use the `sed` command daily. And you?\n\nhttps://www.yesik.it/EP07\n#Shell #Linux #Sed\n#YesIKnowIT
==> tweet.002.txt <==
{}
Print the first column of a space-separated data file:\nawk '{print $1}' data.txt # Print out just the first column\n\nFor some unknown reason, I find that easier to remember than:\ncut -f1 data.txt\n\n#Linux #AWK #Cut
L'esempio sopra sembra facile da capire, ma ci sono due insidie qui. Innanzitutto, il 2
dato come argomento a csplit
è una riga numero , non una riga conta . Tuttavia, quando utilizzo una ripetizione come ho fatto, dopo la prima corrispondenza, csplit
utilizzerà quel numero come riga count . Se non è chiaro, ti lascio confrontare l'output dei tre seguenti comandi:
sh$ csplit tweets.txt --keep-files 2 2 2 2 2
csplit: warning: line number ‘2’ is the same as preceding line number
csplit: warning: line number ‘2’ is the same as preceding line number
csplit: warning: line number ‘2’ is the same as preceding line number
csplit: warning: line number ‘2’ is the same as preceding line number
1
0
0
0
0
9030
sh$ csplit tweets.txt --keep-files 2 4 6 8 10
1
123
222
161
182
8342
sh$ csplit tweets.txt --keep-files 2 '{4}'
1
123
222
161
182
8342
Ho accennato a una seconda trappola, in qualche modo collegata alla prima. Forse hai notato la riga vuota nella parte superiore del tweets.txt
file? Porta a quel tweet.000.txt
pezzo che contiene solo il carattere di nuova riga. Sfortunatamente, in quell'esempio era richiesto a causa della ripetizione:ricorda che voglio due righe pezzi. Quindi il 2
è obbligatorio prima della ripetizione. Ma questo significa anche il primo chunk si interromperà, ma non include , la riga due. In altre parole, il primo pezzo contiene una riga. Tutti gli altri conterranno 2 righe. Forse potresti condividere la tua opinione nella sezione commenti, ma per quanto mi riguarda penso che questa sia stata una scelta di design sfortunata.
Puoi mitigare questo problema saltando direttamente alla prima riga non vuota:
sh$ csplit tweets.txt \
> --prefix='tweet.' --suffix-format='%03d.txt' \
> --elide-empty-files \
> --keep-files \
> %.% 2 '{*}'
csplit: ‘2’: line number out of range on repetition 62
123
222
161
[...]
sh$ head tweet.00[012].txt
==> tweet.000.txt <==
{ days:180 }
I think I use the `sed` command daily. And you?\n\nhttps://www.yesik.it/EP07\n#Shell #Linux #Sed\n#YesIKnowIT
==> tweet.001.txt <==
{}
Print the first column of a space-separated data file:\nawk '{print $1}' data.txt # Print out just the first column\n\nFor some unknown reason, I find that easier to remember than:\ncut -f1 data.txt\n\n#Linux #AWK #Cut
==> tweet.002.txt <==
{}
For the #shell #beginners :\n« #GlobPatterns : how to move hundreds of files in not time [1/3] »\nhttps://youtu.be/TvW8DiEmTcQ\n\n#Unix #Linux\n#YesIKnowIT
Lettura da stdin
Ovviamente, come la maggior parte degli strumenti a riga di comando, csplit
può leggere i dati di input dal suo input standard. In tal caso, devi specificare -
come nome del file di input:
sh$ tr [:lower:] [:upper:] < tweets.txt | csplit - \
> --prefix='tweet.' --suffix-format='%03d.txt' \
> --elide-empty-files \
> --keep-files \
> %.% 2 '{3}'
123
222
161
8524
sh$ head tweet.???.txt
==> tweet.000.txt <==
{ DAYS:180 }
I THINK I USE THE `SED` COMMAND DAILY. AND YOU?\N\NHTTPS://WWW.YESIK.IT/EP07\N#SHELL #LINUX #SED\N#YESIKNOWIT
==> tweet.001.txt <==
{}
PRINT THE FIRST COLUMN OF A SPACE-SEPARATED DATA FILE:\NAWK '{PRINT $1}' DATA.TXT # PRINT OUT JUST THE FIRST COLUMN\N\NFOR SOME UNKNOWN REASON, I FIND THAT EASIER TO REMEMBER THAN:\NCUT -F1 DATA.TXT\N\N#LINUX #AWK #CUT
==> tweet.002.txt <==
{}
FOR THE #SHELL #BEGINNERS :\N« #GLOBPATTERNS : HOW TO MOVE HUNDREDS OF FILES IN NOT TIME [1/3] »\NHTTPS://YOUTU.BE/TVW8DIEMTCQ\N\N#UNIX #LINUX\N#YESIKNOWIT
==> tweet.003.txt <==
{}
WANT TO KNOW THE OLDEST FILE IN YOUR DISK?\N\NFIND / -TYPE F -PRINTF '%TFT%.8TT %P\N' | SORT | LESS\N(SHOULD WORK ON ANY SINGLE UNIX SPECIFICATION COMPLIANT SYSTEM)\N#UNIX #LINUX
{}
WHEN USING THE FIND COMMAND, USE `-INAME` INSTEAD OF `-NAME` FOR CASE-INSENSITIVE SEARCH\N#UNIX #LINUX #SHELL #FIND
{}
FROM A POSIX SHELL `$OLDPWD` HOLDS THE NAME OF THE PREVIOUS WORKING DIRECTORY:\NCD /TMP\NECHO YOU ARE HERE: $PWD\NECHO YOU WERE HERE: $OLDPWD\NCD $OLDPWD\N\N#UNIX #LINUX #SHELL #CD
{}
FROM A POSIX SHELL, "CD" IS A SHORTHAND FOR CD $HOME\N#UNIX #LINUX #SHELL #CD
{}
HOW TO MOVE HUNDREDS OF FILES IN NO TIME?\NUSING THE FIND COMMAND!\N\NHTTPS://YOUTU.BE/ZMEFXJYZAQK\N#UNIX #LINUX #MOVE #FILES #FIND\N#YESIKNOWIT
Ed è praticamente tutto ciò che volevo mostrarti oggi. Spero che in futuro utilizzerai csplit per dividere i file in Linux. Se ti è piaciuto questo articolo e non dimenticare di condividerlo e metterlo mi piace sul tuo social network preferito!