Lorsqu'il s'agit de diviser un fichier texte en plusieurs fichiers sous Linux, la plupart des gens utilisent la commande split. Rien de mal avec la commande split sauf qu'elle s'appuie sur la taille en octets ou la taille de ligne pour diviser les fichiers.
Ce n'est pas pratique dans les situations où vous devez diviser des fichiers en fonction de leur contenu plutôt que de leur taille. Laissez-moi vous donner un exemple.
Je gère mes tweets programmés à l'aide de fichiers YAML. Un fichier de tweet typique contient plusieurs tweets, séparés par quatre tirets :
----
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 :
[...]
Lors de leur importation dans mon système, je dois écrire chaque tweet dans son propre fichier. Je fais cela pour éviter d'enregistrer des tweets en double.
Mais comment scinder un fichier en plusieurs parties en fonction de son contenu ? Eh bien, vous pouvez probablement obtenir quelque chose de convaincant en utilisant les commandes awk :
sh$ awk < tweets.yaml '
> /----/ { OUTPUT="tweet." (N++) ".yaml" }
> { print > OUTPUT }
> '
Cependant, malgré une relative simplicité, une telle solution n'est pas très robuste :par exemple, je n'ai pas fermé correctement les différents fichiers de sortie, donc cela pourrait très bien atteindre la limite des fichiers ouverts. Ou que se passe-t-il si j'ai oublié le séparateur avant le tout premier tweet du fichier ? Bien sûr, tout cela peut être géré et corrigé dans le script AWK, au prix de le rendre plus complexe. Mais pourquoi s'embêter avec ça quand on a le csplit
outil pour accomplir cette tâche ?
Utiliser csplit pour diviser des fichiers sous Linux
Le csplit
l'outil est un cousin du split
outil qui peut être utilisé pour diviser un fichier en taille fixe morceaux. Mais csplit
identifiera les limites des blocs en fonction du contenu du fichier, plutôt qu'en utilisant le nombre d'octets.
Dans ce didacticiel, je vais démontrer l'utilisation de la commande csplit et expliquer également la sortie de cette commande.
Ainsi, par exemple, si je souhaite diviser mon fichier tweet en fonction du ----
délimiteur, je pourrais écrire :
sh$ csplit tweets.yaml /----/
0
10846
Vous avez peut-être deviné le csplit
L'outil a utilisé l'expression régulière fournie sur la ligne de commande pour identifier le séparateur. Et quels pourraient être ces 0
et 10983
résultat affiché sur la sortie standard ? Eh bien, ils sont la taille en octets de chaque bloc de données créé.
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
Attendez une minute! Où ces xx00
et xx01
d'où proviennent les noms de fichiers ? Et pourquoi csplit
diviser le fichier en deux morceaux seulement ? Et pourquoi le premier bloc de données a une longueur de zéro octet ?
La réponse à la première question est simple :xxNN
(ou plus formellement xx%02d
) est le format de nom de fichier par défaut utilisé par csplit
. Mais vous pouvez changer cela en utilisant le --suffix-format
et --prefix
options. Par exemple, je pourrais changer le format en quelque chose de plus significatif pour mes besoins :
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
Le préfixe est une chaîne simple, mais le suffixe est une chaîne de format comme celle utilisée par la bibliothèque C standard printf
une fonction. La plupart des caractères du format seront utilisés textuellement, à l'exception des spécifications de conversion qui sont introduites par le signe pourcentage (%
) et qui se termine par un spécificateur de conversion (ici, d
). Entre les deux, le format peut également contenir divers drapeaux et options. Dans mon exemple, le %03d
spécification de conversion signifie :
- afficher le numéro de bloc sous forme d'entier décimal (
d
), - dans un champ de trois caractères (
3
), - éventuellement complété à gauche par des zéros (
0
).
Mais cela ne répond pas aux autres interrogations que j'avais ci-dessus :pourquoi n'en avons-nous que deux morceaux, l'un d'eux contenant zéro octets ? Peut-être avez-vous déjà trouvé la réponse à cette dernière question par vous-même :mon fichier de données commence par ----
sur sa toute première ligne. Donc, csplit
l'a considéré comme un délimiteur, et comme il n'y avait pas de données avant cette ligne, il a créé un premier bloc vide. Nous pouvons désactiver la création de fichiers de longueur zéro octet en utilisant le --elide-empty-files
choix :
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 :plus de fichiers vides. Mais dans un sens, le résultat est pire maintenant, puisque csplit
diviser le fichier en un seul tronçon. Nous pouvons à peine appeler cela "diviser" un fichier, n'est-ce pas ?
L'explication de ce résultat surprenant est csplit
ne pas supposez que chaque mandrin doit être divisé en fonction du même séparateur. En fait, csplit
vous oblige à fournir chaque séparateur utilisé. Même si c'est plusieurs fois la même :
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> /----/ /----/ /----/
170
250
10426
J'ai mis trois séparateurs (identiques) sur la ligne de commande. Donc, csplit
identifié la fin du premier morceau en fonction du premier séparateur. Cela conduit à un bloc de zéro octet qui a été élidé. Le deuxième morceau était délimité par la ligne suivante correspondant à /----/
. Conduisant à un morceau de 170 octets. Enfin, un troisième bloc de 250 octets a été identifié sur la base du troisième séparateur. Les données restantes, 10 426 octets, ont été placées dans le dernier bloc.
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
Évidemment, ce ne serait pas pratique si nous devions fournir autant de séparateurs sur la ligne de commande qu'il y a de morceaux dans le fichier de données. D'autant plus que ce nombre exact n'est généralement pas connu à l'avance. Heureusement, csplit
a un motif spécial signifiant "répéter le motif précédent autant que possible." Malgré sa syntaxe rappelant le quantificateur étoile dans une expression régulière, celui-ci est plus proche du concept de Kleene plus puisqu'il est utilisé pour répéter un séparateur qui a déjà été mis en correspondance une fois :
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
Et cette fois, enfin, j'ai divisé ma collection de tweets en parties individuelles. Cependant, est-ce que csplip
avez-vous d'autres jolis motifs "spéciaux" comme ça ? Eh bien, je ne sais pas si nous pouvons les appeler "spéciaux", mais définitivement, csplit
mieux comprendre les modèles.
Plus de modèles csplit
Nous venons de voir dans la section précédente comment utiliser le quantificateur ‘{*}’ pour les répétitions non liées. Cependant, en remplaçant l'étoile par un chiffre, vous pouvez demander un nombre exact de répétitions :
sh$ csplit tweets.yaml \
> --prefix='tweet.' --suffix-format='%03d.yaml' \
> --elide-empty-files \
> /----/ '{6}'
170
250
190
208
140
216
9672
Cela conduit à un cas d'angle intéressant. Qu'est-ce qui s'ajouterait si le nombre de répétitions dépassait le nombre de délimiteurs réels dans le fichier de données ? Eh bien, voyons cela sur un exemple :
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
Fait intéressant, non seulement csplit
signalé une erreur, mais il a également supprimé tous les fichiers de blocs créés au cours du processus. Portez une attention particulière à ma formulation :elle est supprimée eux. Cela signifie que les fichiers ont été créés, alors, lorsque csplit
rencontré l'erreur, il les a supprimés. En d'autres termes, si vous avez déjà un fichier dont le nom ressemble à un fichier fragmenté, il sera supprimé :
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
Dans l'exemple ci-dessus, le tweet.002.yaml
le fichier que nous avons créé manuellement a été écrasé, puis supprimé par csplit
.
Vous pouvez modifier ce comportement en utilisant le --keep-files
option. Comme son nom l'indique, il ne supprimera pas les morceaux créés par csplit après avoir rencontré une erreur :
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
Remarquez dans ce cas, et malgré l'erreur, csplit
n'a supprimé aucune donnée :
sh$ diff -s tweets.yaml <(cat tweet.*)
Files tweets.yaml and /dev/fd/63 are identical
Mais que se passe-t-il s'il y a des données dans le fichier que je veux supprimer ? Eh bien, csplit
a un support limité pour cela en utilisant un %regex%
motif.
Saut de données dans csplit
Lors de l'utilisation d'un signe de pourcentage (%
) comme délimiteur de regex au lieu d'une barre oblique (/
), csplit
va ignorer données jusqu'à (sans inclure) la première ligne correspondant à l'expression régulière. Cela peut être utile pour ignorer certains enregistrements, notamment au début ou à la fin du fichier d'entrée :
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
Utilisation des décalages lors du fractionnement de fichiers avec csplit
Lors de l'utilisation d'expressions régulières (soit /…/
ou %…%
) vous pouvez spécifier un positif (+N
) ou négatif (-N
) décalage à la fin du motif donc csplit
divisera le fichier N lignes après ou avant la ligne correspondante. N'oubliez pas que dans tous les cas, le modèle spécifie la fin du morceau :
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
----
Fractionner par numéro de ligne
Nous avons déjà vu comment nous pouvons utiliser une expression régulière pour diviser des fichiers. Dans ce cas, csplit
divisera le fichier à la première ligne correspondant cette expression régulière. Mais vous pouvez aussi identifier la ligne fractionnée par son numéro de ligne comme nous allons le voir maintenant.
Avant de passer à YAML, je stockais mes tweets programmés dans un fichier plat.
Dans ce fichier, un tweet était composé de deux lignes. L'un contenant une répétition facultative, et le second contenant le texte du tweet, avec des retours à la ligne remplacés par \n. Encore une fois, cet exemple de fichier est disponible en ligne.
Avec ce format de "taille fixe", il était également possible d'utiliser csplit
pour mettre chaque tweet individuel dans son propre fichier :
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'exemple ci-dessus semble facile à comprendre, mais il y a ici deux pièges. Tout d'abord, le 2
donné en argument de csplit
est un numéro de ligne , pas une ligne compte . Cependant, lors de l'utilisation d'une répétition comme je l'ai fait, après la première correspondance, csplit
utilisera ce nombre comme ligne compter . Si ce n'est pas clair, je vous laisse comparer le résultat des trois commandes suivantes :
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
J'ai mentionné un deuxième écueil, un peu lié au premier. Peut-être avez-vous remarqué la ligne vide tout en haut du tweets.txt
dossier? Cela mène à ce tweet.000.txt
morceau qui contient uniquement le caractère de nouvelle ligne. Malheureusement, c'était nécessaire dans cet exemple à cause de la répétition :rappelez-vous que je veux deux lignes morceaux. Donc le 2
est obligatoire avant la répétition. Mais cela signifie aussi le premier le morceau se cassera à, mais n'incluant pas , la ligne deux. En d'autres termes, le premier bloc contient une ligne. Tous les autres contiendront 2 lignes. Vous pourriez peut-être partager votre opinion dans la section des commentaires, mais en ce qui me concerne, je pense que c'était un choix de conception malheureux.
Vous pouvez atténuer ce problème en passant directement à la première ligne non vide :
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
Lecture depuis stdin
Bien sûr, comme la plupart des outils en ligne de commande, csplit
peut lire les données d'entrée à partir de son entrée standard. Dans ce cas, vous devez spécifier -
comme nom de fichier d'entrée :
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
Et c'est à peu près tout ce que je voulais vous montrer aujourd'hui. J'espère qu'à l'avenir, vous utiliserez csplit pour diviser des fichiers sous Linux. Si vous avez apprécié cet article, n'oubliez pas de le partager et de l'aimer sur votre réseau social préféré !