cat un très grand nombre de fichiers ensemble dans le bon ordre

23

J'ai environ 15 000 fichiers nommés file_1.pdb, file_2.pdbetc. Je peux en répertorier quelques milliers dans l'ordre en faisant:

cat file_{1..2000}.pdb >> file_all.pdb

Cependant, si je fais cela pour 15 000 fichiers, j'obtiens l'erreur

-bash: /bin/cat: Argument list too long

J'ai vu ce problème être résolu en faisant, find . -name xx -exec xxmais cela ne préserverait pas l'ordre avec lequel les fichiers sont joints. Comment puis-je atteindre cet objectif?

nitrate de sodium
la source
3
Quel est le dixième fichier nommé? (Ou tout fichier avec plus d'une commande numérotée à un seul chiffre.)
roaima
J'ai (maintenant) 15 000 de ces fichiers dans un répertoire et votre cat file_{1..15000}.pdbconstruction me convient parfaitement.
roaima
11
dépend du système quelle est la limite. getconf ARG_MAXdevrait dire.
ilkkachu
3
Pensez à remplacer votre question par "des milliers de" ou "un très grand nombre de" fichiers. Pourrait rendre la question plus facile à trouver pour d'autres personnes ayant un problème similaire.
msouth

Réponses:

49

L' utilisation find, sortet xargs:

find . -maxdepth 1 -type f -name 'file_*.pdb' -print0 |
sort -zV |
xargs -0 cat >all.pdb

La findcommande trouve tous les fichiers pertinents, puis imprime leurs noms de chemin pour faire sortun "tri de version" pour les obtenir dans le bon ordre (si les nombres dans les noms de fichiers avaient été remplis de zéro à une largeur fixe, nous n'aurions pas eu besoin -V). xargsprend cette liste de chemins triés et les exécute caten lots aussi importants que possible.

Cela devrait fonctionner même si les noms de fichiers contiennent des caractères étranges tels que des sauts de ligne et des espaces. Nous utilisons -print0with findpour donner sortdes noms terminés par nul à trier, et les sorttraitons en utilisant -z. xargslit également les noms sans terminaison avec son -0drapeau.

Notez que j'écris le résultat dans un fichier dont le nom ne correspond pas au modèle file_*.pdb.


La solution ci-dessus utilise des indicateurs non standard pour certains utilitaires. Ceux-ci sont pris en charge par la mise en œuvre GNU de ces utilitaires et au moins par la mise en œuvre d'OpenBSD et de macOS.

Les drapeaux non standard utilisés sont

  • -maxdepth 1, pour findne faire entrer que le répertoire le plus haut mais pas de sous-répertoires. POSIX, utilisezfind . ! -name . -prune ...
  • -print0, pour créer finddes noms de chemin de terminaison nulles (cela a été considéré par POSIX mais rejeté). On pourrait utiliser à la -exec printf '%s\0' {} +place.
  • -z, pour faire sortprendre des enregistrements terminés par nul. Il n'y a pas d'équivalence POSIX.
  • -V, pour sorttrier par exemple 200après 3. Il n'y a pas d'équivalence POSIX, mais pourrait être remplacé par un tri numérique sur des parties spécifiques du nom de fichier si les noms de fichiers ont un préfixe fixe.
  • -0, pour créer des xargsenregistrements terminés en lecture nulle. Il n'y a pas d'équivalence POSIX. POSIX, il faudrait citer les noms de fichiers dans un format reconnu par xargs.

Si les chemins d'accès se comportent bien et si la structure du répertoire est plate (pas de sous-répertoires), alors on pourrait se passer de ces drapeaux, sauf -Vavec sort.

Kusalananda
la source
1
Vous n'avez pas besoin d'une terminaison nulle non standard pour cela. Ces noms de fichiers sont extrêmement ennuyeux et les outils POSIX sont alors entièrement capables de les gérer.
Kevin
6
Vous pouvez également écrire ce plus succinctement les spécifications du demandeur comme printf ‘file_%d.pdb\0’ {1..15000} | xargs -0 cat, ou même avec le point de Kevin, echo file_{1..15000}.pdb | xargs cat. La findsolution a considérablement plus de frais généraux car elle doit rechercher le système de fichiers pour ces fichiers, mais elle est plus utile lorsque certains fichiers peuvent ne pas exister.
kojiro
4
@Kevin, bien que ce que vous dites soit vrai, il est sans doute préférable d'avoir une réponse qui s'applique dans des circonstances plus générales. Parmi les milliers de personnes suivantes qui ont cette question, il est probable que certaines d'entre elles auront des espaces ou quoi que ce soit dans leurs noms de fichiers.
msouth
1
@chrylis Une redirection ne fait jamais partie des arguments d'une commande, et elle est xargsplutôt que catcelle qui est redirigée (chaque catinvocation utilisera xargsune sortie standard). Si nous l'avions dit, xargs -0 sh -c 'cat >all.pdb'il aurait été judicieux d'utiliser à la >>place de >, si c'est ce à quoi vous faites allusion.
Kusalananda
1
Il semblerait que sort -n -k1.6cela fonctionnerait (pour les file_nnnnoms de fichiers originaux ou sort -n -k1.5pour ceux sans le soulignement).
Scott
14

Avec zsh(d'où {1..15000}vient cet opérateur):

autoload zargs # best in ~/.zshrc
zargs file_{1..15000}.pdb -- cat > file_all.pdb

Ou pour tous les file_<digits>.pdbfichiers dans l'ordre numérique:

zargs file_<->.pdb(n) -- cat > file_all.pdb

(où <x-y>est un opérateur glob qui correspond aux nombres décimaux x à y. Sans xni y, c'est n'importe quel nombre décimal. Équivalent à extendedglob's [0-9]##ou kshglob' +([0-9])(un ou plusieurs chiffres)).

Avec ksh93, à l'aide de sa catcommande intégrée (donc pas affecté par cette limite de l' execve()appel système car il n'y a pas d' exécution ):

command /opt/ast/bin/cat file_{1..15000}.pdb > file_all.pdb

Avec bash/ zsh/ ksh93(qui supporte zshles {x..y}et ont printfintégré):

printf '%s\n' file_{1..15000}.pdb | xargs cat > file_all.pdb

Sur un système GNU ou compatible, vous pouvez également utiliser seq:

seq -f 'file_%.17g.pdb' 15000 | xargs cat > file_all.pdb

Pour les xargssolutions basées sur, une attention particulière devrait être apportée aux noms de fichiers contenant des blancs, des guillemets simples ou doubles ou des barres obliques inverses.

Comme pour -It's a trickier filename - 12.pdb, utilisez:

seq -f "\"./-It's a trickier filename - %.17g.pdb\"" 15000 |
  xargs cat > file_all.pdb
Stéphane Chazelas
la source
C'est seq -f | xarg cat > la solution la plus élégante et la plus efficace. (A MON HUMBLE AVIS).
Hastur
Vérifiez le nom de fichier le plus délicat ... peut '"./-It'\''s a trickier filename - %.17g.pdb"'- être ?
Hastur
@Hastur, oups! Oui, merci, je l'ai changé pour une autre syntaxe de citation. Le vôtre fonctionnerait également.
Stéphane Chazelas
11

Une boucle for est possible et très simple.

for i in file_{1..15000}.pdb; do cat $i >> file_all.pdb; done

L'inconvénient est que vous invoquez catbeaucoup de fois. Mais si vous ne vous souvenez pas exactement comment faire les choses findet que les frais généraux d'invocation ne sont pas trop mauvais dans votre situation, alors cela vaut la peine de garder à l'esprit.

OmnipotentEntity
la source
J'ajoute souvent un echo $i;corps dans la boucle comme "indicateur de progression"
Rolf
3
seq 1 15000 | awk '{print "file_"$0".dat"}' | xargs cat > file_all.pdb
LarryC
la source
1
awk peut faire le travail suivants ici et suivants peuvent faire le travail de awk: seq -f file_%.10g.pdb 15000. Notez que ce seqn'est pas une commande standard.
Stéphane Chazelas
Merci Stéphane - je pense que seq -f c'est une excellente façon de le faire; s'en souviendra.
LarryC
2

Prémisse

Vous ne devriez pas encourir cette erreur pour seulement 15 000 fichiers avec ce format de nom spécifique [ 1 , 2 ] .

Si vous exécutez cette extension à partir d'un autre répertoire et que vous devez ajouter le chemin d'accès à chaque fichier, la taille de votre commande sera plus grande et, bien sûr, cela peut se produire.

Solution exécutez la commande à partir de ce répertoire.

(cd That/Directory ; cat file_{1..2000}.pdb >> file_all.pdb )

Meilleure solution Si à la place j'ai deviné mauvais et que vous l'exécutez à partir du répertoire dans lequel se trouvent les fichiers ... À
mon humble avis, la meilleure solution est celle de Stéphane Chazelas :

seq -f 'file_%.17g.pdb' 15000 | xargs cat > file_all.pdb

avec printf ou seq; testé sur des fichiers 15k avec seulement leur nombre à l'intérieur pré-mis en cache, il est même le plus rapide (à l'heure actuelle et à l'exception de l'OP du même répertoire dans lequel se trouvent les fichiers).

Quelques mots de plus

Vous devriez pouvoir passer à vos lignes de commande shell plus longtemps.
Votre ligne de commande contient 213914 caractères et contient 15003 mots
cat file_{1..15000}.pdb " > file_all.pdb" | wc

... même l'ajout de 8 octets pour chaque mot est 333 938 octets (0,3 M) bien en deçà du 2097142 (2,1 M) rapporté par ARG_MAXsur un noyau 3.13.0 ou du 2088232 légèrement plus petit rapporté comme "Longueur maximale de commande que nous pourrions réellement utiliser " parxargs --show-limits

Donnez un aperçu de votre système à la sortie de

getconf ARG_MAX
xargs --show-limits

Solution guidée paresse

Dans des cas comme celui-ci, je préfère travailler avec des blocs, même parce que généralement une solution efficace en temps.
La logique (le cas échéant) est que je suis beaucoup trop paresseux pour écrire 1 ... 1000 1001..2000 etc etc ...
Je demande donc à un script de le faire pour moi.
Ce n'est qu'après avoir vérifié que la sortie est correcte que je la redirige vers un script.

... mais la paresse est un état d'esprit .
Comme je suis allergique à xargs(j'aurais vraiment dû l'utiliser xargsici) et que je ne veux pas vérifier comment l'utiliser, je termine ponctuellement pour réinventer la roue comme dans les exemples ci-dessous (tl; dr).

Notez que puisque les noms de fichiers sont contrôlés (pas d'espaces, de nouvelles lignes ...), vous pouvez aller facilement avec quelque chose comme le script ci-dessous.

tl; dr

Version 1: passez en paramètre optionnel le 1er numéro de fichier, le dernier, la taille du bloc, le fichier de sortie

#!/bin/bash
StartN=${1:-1}          # First file number
EndN=${2:-15000}        # Last file number
BlockN=${3:-100}        # files in a Block 
OutFile=${4:-"all.pdb"} # Output file name

CurrentStart=$StartN 
for i in $(seq $StartN $BlockN $EndN)
do 
  CurrentEnd=$i ;  
    cat $(seq -f file_%.17g.pdb $CurrentStart $CurrentEnd)  >> $OutFile;
  CurrentStart=$(( CurrentEnd + 1 )) 
done
# Here you may need to do a last iteration for the part cut from seq
[[ $EndN -ge $CurrentStart ]] && 
    cat $(seq -f file_%.17g.pdb $CurrentStart $EndN)  >> $OutFile;

Version 2

Appel bash pour l'expansion (un peu plus lent dans mes tests ~ 20%).

#!/bin/bash
StartN=${1:-1}          # First file number
EndN=${2:-15000}        # Last file number
BlockN=${3:-100}        # files in a Block 
OutFile=${4:-"all.pdb"} # Output file name

CurrentStart=$StartN 
for i in $(seq $StartN $BlockN $EndN)
do 
  CurrentEnd=$i ;
    echo  cat file_{$CurrentStart..$CurrentEnd}.pdb | /bin/bash  >> $OutFile;
  CurrentStart=$(( CurrentEnd + 1 )) 
done
# Here you may need to do a last iteration for the part cut from seq
[[ $EndN -ge $CurrentStart ]] && 
    echo  cat file_{$CurrentStart..$EndN}.pdb | /bin/bash  >> $OutFile;

Bien sûr, vous pouvez aller de l'avant et vous débarrasser complètement de seq [ 3 ] (de coreutils) et travailler directement avec les variables dans bash, ou utiliser python, ou compiler un programme ac pour le faire [ 4 ] ...

Hastur
la source
Notez que %gc'est court pour %.6g. Cela représenterait 1 000 000 comme 1e + 06 par exemple.
Stéphane Chazelas
Les gens vraiment paresseux utilisent les outils conçus pour contourner cette limitation E2BIG comme les xargszsh zargsou ksh93les command -x.
Stéphane Chazelas
seqn'est pas un bash intégré, c'est une commande de GNU coreutils. seq -f %g 1000000 1000000sorties 1e + 06 même dans la dernière version de coreutils.
Stéphane Chazelas
@ StéphaneChazelas La paresse est un état d'esprit. C'est étrange à dire, mais je me sens plus à l'aise quand je peux voir (et vérifier visuellement la sortie d'une commande sérialisée) et ensuite seulement rediriger vers l'exécution. Cette construction me donne à penser moins que xarg... mais je comprends que c'est personnel et peut-être lié uniquement à moi.
Hastur
@ StéphaneChazelas Gotcha, à droite ... Fixé. Merci. Je n'ai testé qu'avec les 15k fichiers donnés par l'OP, ma mauvaise.
Hastur
0

Une autre façon de le faire pourrait être

(cat file_{1..499}.pdb; cat file_{500..999}.pdb; cat file_{1000..1499}.pdb; cat file_{1500..2000}.pdb) >> file_all.pdb
glglgl
la source