web-dev-qa-db-fra.com

Utilisation inutile du chat?

C'est probablement dans de nombreuses FAQ - au lieu d'utiliser:

cat file | command

(qui est appelé utilisation inutile de chat), manière correcte supposée être:

command < file

Dans le 2ème cas, "correct" - OS n'a pas besoin de générer un processus supplémentaire.
Malgré le fait de savoir cela, j'ai continué à utiliser un chat inutile pour 2 raisons.

  1. plus esthétique - j'aime quand les données ne se déplacent uniformément que de gauche à droite. Et il est plus facile de remplacer cat par autre chose (gzcat, echo, ...), d'ajouter un 2ème fichier ou d'insérer un nouveau filtre (pv, mbuffer, grep ...).

  2. J'ai "senti" que cela pourrait être plus rapide dans certains cas. Plus rapide car il y a 2 processus, le 1er (cat) fait la lecture et le second fait quoi que ce soit. Et ils peuvent fonctionner en parallèle, ce qui signifie une exécution parfois plus rapide.

Ma logique est-elle correcte (pour la 2ème raison)?

88
Leonid Volnitsky

Je n'étais pas au courant de la récompense jusqu'à aujourd'hui quand une recrue a essayé de m'épingler UUOC pour l'une de mes réponses. C'était un cat file.txt | grep foo | cut ... | cut .... Je lui ai donné une idée de mon esprit, et ce n'est qu'après avoir visité le lien qu'il m'a donné faisant référence aux origines du prix et à la pratique de le faire. Des recherches plus poussées m'ont amené à cette question. Malheureusement, malgré une réflexion consciente, aucune des réponses ne comprenait ma justification.

Je n'avais pas voulu être sur la défensive pour lui répondre. Après tout, dans ma jeunesse, j'aurais écrit la commande comme grep foo file.txt | cut ... | cut ... parce que chaque fois que vous effectuez les simples greps fréquents, vous apprenez l'emplacement de l'argument de fichier et il est facile de savoir que le premier est le modèle et les derniers sont les noms de fichier.

C'était un choix conscient d'utiliser cat lorsque j'ai répondu à la question, en partie pour une raison de "bon goût" (selon les termes de Linus Torvalds) mais principalement pour une raison de fonction convaincante.

Cette dernière raison est plus importante, je vais donc la présenter en premier. Lorsque j'offre un pipeline comme solution, je m'attends à ce qu'il soit réutilisable. Il est fort probable qu'un pipeline soit ajouté à la fin ou épissé dans un autre pipeline. Dans ce cas, avoir un argument de fichier pour grep gâche la réutilisabilité, et très probablement le faire en silence sans message d'erreur si l'argument de fichier existe. C'est à dire. grep foo xyz | grep bar xyz | wc vous indiquera combien de lignes dans xyz contiennent bar pendant que vous attendez le nombre de lignes qui contiennent à la fois foo et bar. Devoir changer les arguments d'une commande dans un pipeline avant de l'utiliser est sujet à des erreurs. Ajoutez-y la possibilité d'échecs silencieux et cela devient une pratique particulièrement insidieuse.

La première raison n'est pas non plus sans importance car beaucoup de " bon goût " est simplement une justification subconsciente intuitive pour des choses comme les échecs silencieux ci-dessus auxquels vous ne pouvez pas penser juste au moment où une personne dans le besoin l'éducation dit "mais ce chat n'est-il pas inutile".

Cependant, j'essaierai également de faire prendre conscience de l'ancienne raison du "bon goût" que j'ai mentionnée. Cette raison est liée à l'esprit de conception orthogonale d'Unix. grep ne fait pas cut et ls ne fait pas grep. Par conséquent, au moins grep foo file1 file2 file3 va à l'encontre de l'esprit du design. La façon orthogonale de le faire est cat file1 file2 file3 | grep foo. À présent, grep foo file1 n'est qu'un cas particulier de grep foo file1 file2 file3, et si vous ne le traitez pas de la même façon, vous utilisez au moins des cycles d'horloge cérébrale en essayant d'éviter le prix inutile du chat.

Cela nous amène à l'argument selon lequel grep foo file1 file2 file3 concatène et cat concatène donc il convient de cat file1 file2 file3 mais parce que cat ne concatène pas dans cat file1 | grep foo nous violons donc l'esprit à la fois du cat et du tout-puissant Unix. Eh bien, si c'était le cas, Unix aurait besoin d'une commande différente pour lire la sortie d'un fichier et le cracher sur stdout (pas le paginer ou quoi que ce soit juste un pur crachat sur stdout). Vous auriez donc la situation où vous dites cat file1 file2 ou vous dites dog file1 et n'oubliez pas d'éviter cat file1 pour éviter d'obtenir le prix, tout en évitant dog file1 file2 puisque, espérons-le, la conception de dog générerait une erreur si plusieurs fichiers sont spécifiés.

Avec un peu de chance, à ce stade, vous sympathisez avec les concepteurs Unix pour ne pas avoir inclus de commande séparée pour cracher un fichier sur stdout, tout en nommant cat pour concaténer plutôt que de lui donner un autre nom. <edit> a supprimé les commentaires incorrects sur <, En réalité, < est une fonction efficace sans copie pour cracher un fichier sur stdout que vous pouvez positionner au début d'un pipeline afin que les concepteurs Unix aient inclus quelque chose spécifiquement pour cela </edit>

La question suivante est pourquoi est-il important d'avoir des commandes qui crachent simplement un fichier ou la concaténation de plusieurs fichiers vers stdout, sans aucun traitement supplémentaire? L'une des raisons est d'éviter d'avoir chaque commande Unix unique qui fonctionne sur une entrée standard pour savoir comment analyser au moins un argument de fichier de ligne de commande et l'utiliser comme entrée s'il existe. La deuxième raison est d'éviter aux utilisateurs de se rappeler: (a) où vont les arguments de nom de fichier; et (b) éviter le bogue du pipeline silencieux comme mentionné ci-dessus.

Cela nous amène à comprendre pourquoi grep a une logique supplémentaire. La raison d'être est de permettre à l'utilisateur de maîtriser les commandes qui sont utilisées fréquemment et sur une base autonome (plutôt que comme pipeline). Il s'agit d'un léger compromis d'orthogonalité pour un gain de convivialité significatif. Toutes les commandes ne doivent pas être conçues de cette façon et les commandes qui ne sont pas fréquemment utilisées devraient éviter complètement la logique supplémentaire des arguments de fichier (rappelez-vous que la logique supplémentaire conduit à une fragilité inutile (la possibilité d'un bogue)). L'exception est d'autoriser les arguments de fichier comme dans le cas de grep. (Soit dit en passant, notez que ls a une raison complètement différente non seulement d'accepter mais d'exiger des arguments de fichier)

Enfin, ce qui aurait pu être mieux fait, c'est que des commandes exceptionnelles telles que grep (mais pas nécessairement ls) génèrent une erreur si l'entrée standard est également disponible lorsque des arguments de fichier sont spécifiés.

70
necromancer

Nan!

Tout d'abord, peu importe où dans une commande la redirection se produit. Donc, si vous aimez votre redirection vers la gauche de votre commande, ça va:

< somefile command

est le même que

command < somefile

Deuxièmement, il y a n + 1 processus et un sous-shell se produit lorsque vous utilisez un tuyau. Il est décidément plus lent. Dans certains cas, n aurait été égal à zéro (par exemple, lorsque vous redirigez vers un shell intégré), donc en utilisant cat vous ajoutez un nouveau processus de manière totalement inutile.

En général, chaque fois que vous vous retrouvez à utiliser un tuyau, cela vaut la peine de prendre 30 secondes pour voir si vous pouvez l'éliminer. (Mais cela ne vaut probablement pas la peine de prendre plus de 30 secondes.) Voici quelques exemples où les tuyaux et les processus sont fréquemment utilisés inutilement:

for Word in $(cat somefile); … # for Word in $(<somefile); … (or better yet, while read < somefile)

grep something | awk stuff; # awk '/something/ stuff' (similar for sed)

echo something | command; # command <<< something (although echo would be necessary for pure POSIX)

N'hésitez pas à modifier pour ajouter plus d'exemples.

55
kojiro

Avec la version UUoC, cat doit lire le fichier en mémoire, puis l'écrire dans le canal, et la commande doit lire les données du canal, donc le noyau doit copier le fichier entier - trois fois alors que dans le cas redirigé, le noyau n'a à copier le fichier qu'une seule fois. Il est plus rapide de faire quelque chose une fois que de le faire trois fois.

En utilisant:

cat "$@" | command

est une utilisation totalement différente et pas nécessairement inutile de cat. Elle est toujours inutile si la commande est un filtre standard qui accepte zéro ou plusieurs arguments de nom de fichier et les traite à son tour. Considérez la commande tr: c'est un filtre pur qui ignore ou rejette les arguments de nom de fichier. Pour y alimenter plusieurs fichiers, vous devez utiliser cat comme indiqué. (Bien sûr, il y a une discussion distincte selon laquelle la conception de tr n'est pas très bonne; il n'y a aucune raison réelle pour laquelle elle n'aurait pas pu être conçue comme un filtre standard.) Cela peut également être valide si vous souhaitez que la commande traiter toutes les entrées comme un seul fichier plutôt que comme plusieurs fichiers séparés, même si la commande accepte plusieurs fichiers distincts: par exemple, wc est une telle commande.

C'est le cat single-file cas qui est inconditionnellement inutile.

27
Jonathan Leffler

Je ne suis pas d'accord avec la plupart des cas de prix UUOC excessivement suffisant parce que, lorsque vous enseignez à quelqu'un d'autre, cat est un espace de rangement pratique pour toute commande ou pipeline de commandes compliqué et croustillant qui produit une sortie adaptée au problème ou à la tâche en cours de discussion .

Cela est particulièrement vrai sur des sites comme Stack Overflow, ServerFault, Unix et Linux ou l'un des sites SE.

Si quelqu'un vous pose une question spécifique sur l'optimisation, ou si vous avez envie d'ajouter des informations supplémentaires à ce sujet, alors, parlez de la façon dont l'utilisation de cat est inefficace. Mais ne réprimandez pas les gens parce qu'ils ont choisi de viser la simplicité et la facilité de compréhension dans leurs exemples plutôt que de me regarder comme je suis cool! complexité.

En bref, parce que le chat n'est pas toujours un chat.

Aussi parce que la plupart des gens qui aiment décerner des UUOC le font parce qu'ils sont plus soucieux de montrer à quel point ils sont `` intelligents '' que d'aider ou d'enseigner aux gens. En réalité, ils démontrent qu'ils ne sont probablement qu'un autre débutant qui a trouvé un minuscule bâton pour battre leurs pairs.


Mise à jour

Voici un autre UUOC que j'ai publié dans une réponse à https://unix.stackexchange.com/a/301194/7696 :

sqlq() {
  local filter
  filter='cat'

  # very primitive, use getopts for real option handling.
  if [ "$1" == "--delete-blank-lines" ] ; then
    filter='grep -v "^$"'
    shift
  fi

  # each arg is piped into sqlplus as a separate command
  printf "%s\n" "$@" | sqlplus -S sss/eee@sid | $filter
}

Les pédants UUOC diraient que c'est une UUOC car il est facilement possible de faire $filter par défaut à la chaîne vide et avoir l'instruction if do filter='| grep -v "^$"' mais IMO, en n'incorporant pas le caractère pipe dans $filter, cette "inutile" cat sert le but extrêmement utile de l'auto-documentation du fait que $filter sur la ligne printf n'est pas seulement un autre argument de sqlplus, c'est un filtre de sortie facultatif sélectionnable par l'utilisateur.

S'il est nécessaire d'avoir plusieurs filtres de sortie facultatifs, le traitement des options pourrait simplement ajouter | whatever à $filter aussi souvent que nécessaire - un cat supplémentaire dans le pipeline ne nuira à rien ni n'entraînera de perte notable de performances.

26
cas

Dans défense du chat:

Oui,

   < input process > output 

ou

   process < input > output 

est plus efficace, mais de nombreuses invocations n'ont pas de problèmes de performances, donc vous ne vous en souciez pas.

raisons ergonomiques:

Nous avons l'habitude de lire de gauche à droite, donc une commande comme

    cat infile | process1 | process2 > outfile

est trivial à comprendre.

    process1 < infile | process2 > outfile

doit sauter par-dessus process1, puis lire de gauche à droite. Cela peut être guéri par:

    < infile process1 | process2 > outfile

ressemble en quelque sorte, comme s'il y avait une flèche pointant vers la gauche, où il n'y a rien. Plus déroutant et ressemblant à une citation de fantaisie est:

    process1 > outfile < infile

et la génération de scripts est souvent un processus itératif,

    cat file 
    cat file | process1
    cat file | process1 | process2 
    cat file | process1 | process2 > outfile

où vous voyez votre progression par étapes, tout en

    < file 

ne fonctionne même pas. Les méthodes simples sont moins sujettes aux erreurs et la caténation des commandes ergonomiques est simple avec cat.

Un autre sujet est que la plupart des gens ont été exposés à> et <en tant qu'opérateurs de comparaison, bien avant d'utiliser un ordinateur et lorsqu'ils utilisent un ordinateur comme programmeurs, sont beaucoup plus souvent exposés à ces derniers en tant que tels.

Et comparer deux opérandes avec <et> est contre-commutatif, ce qui signifie

(a > b) == (b < a)

Je me souviens de la première fois que j'ai utilisé <pour la redirection d'entrée, je craignais

a.sh < file 

pourrait signifier la même chose que

file > a.sh

et en quelque sorte écraser mon script a.sh. C'est peut-être un problème pour de nombreux débutants.

différences rares

wc -c journal.txt
15666 journal.txt
cat journal.txt | wc -c 
15666

Ce dernier peut être utilisé directement dans les calculs.

factor $(cat journal.txt | wc -c)

Bien sûr, le <peut également être utilisé ici, au lieu d'un paramètre de fichier:

< journal.txt wc -c 
15666
wc -c < journal.txt
15666

mais peu importe - 15k?

Si je rencontrais occasionnellement des problèmes, je changerais sûrement mon habitude d'invoquer le chat.

Lorsque vous utilisez des fichiers très volumineux ou très nombreux, éviter chat n'est pas un problème. Pour la plupart des questions, l'utilisation du chat est orthogonale, hors sujet, pas un problème.

Commencer ces utilisations inutiles et inutiles des discussions sur tous les sujets sur Shell est ennuyeux et ennuyeux. Obtenez une vie et attendez votre minute de gloire, lorsque vous traitez des questions de performance.

19
user unknown

Un problème supplémentaire est que le tuyau peut masquer silencieusement une sous-coque. Pour cet exemple, je remplacerai cat par echo, mais le même problème existe.

echo "foo" | while read line; do
    x=$line
done

echo "$x"

Vous pouvez vous attendre à ce que x contienne foo, mais ce n'est pas le cas. Le x que vous avez défini était dans un sous-shell généré pour exécuter la boucle while. x dans le shell qui a démarré le pipeline a une valeur indépendante ou n'est pas du tout définie.

Dans bash4, vous pouvez configurer certaines options du shell de sorte que la dernière commande d'un pipeline s'exécute dans le même shell que celui qui démarre le pipeline, mais vous pouvez alors essayer ceci

echo "foo" | while read line; do
    x=$line
done | awk '...'

et x est de nouveau local au sous-shell de while.

17
chepner

En tant que personne qui souligne régulièrement cela et un certain nombre d'autres antipatterns de programmation Shell, je me sens obligé, tardivement, de peser.

Le script shell est un langage de copier/coller. Pour la plupart des gens qui écrivent des scripts Shell, ils ne sont pas là pour apprendre la langue; c'est juste un obstacle qu'ils doivent surmonter pour continuer à faire les choses dans la ou les langues qu'ils connaissent un peu.

Dans ce contexte, je considère qu'il est perturbateur et potentiellement destructeur de propager divers anti-modèles de script Shell. Le code que quelqu'un trouve sur Stack Overflow devrait idéalement être possible de copier/coller dans son environnement avec un minimum de changements et une compréhension incomplète.

Parmi les nombreuses ressources de script Shell sur le net, Stack Overflow est inhabituel dans la mesure où les utilisateurs peuvent aider à façonner la qualité du site en modifiant les questions et réponses sur le site. Cependant, les modifications de code peuvent être problématiques car il est facile d'apporter des modifications qui n'étaient pas prévues par l'auteur du code. Par conséquent, nous avons tendance à laisser des commentaires pour suggérer des modifications au code.

L'UUCA et les commentaires anti-modèles associés ne sont pas uniquement destinés aux auteurs du code sur lequel nous commentons; ils sont autant un caveat emptor pour aider les lecteurs du site à devenir conscients des problèmes dans le code qu'ils trouvent ici.

Nous ne pouvons pas espérer parvenir à une situation où aucune réponse sur Stack Overflow ne recommande des cats inutiles (ou des variables non cotées, ou chmod 777, ou une grande variété d'autres fléaux anti-modèles), mais nous pouvons au moins aider à éduquer l'utilisateur qui est sur le point de copier/coller ce code dans la boucle la plus étroite de leur script qui s'exécute des millions de fois.

En ce qui concerne les raisons techniques, la sagesse traditionnelle veut que nous essayions de minimiser le nombre de processus externes; cela continue à être un bon guide général lors de l'écriture de scripts Shell.

13
tripleee

J'utilise souvent cat file | myprogram dans les exemples. Parfois, je suis accusé d'utilisation inutile de chat ( http://porkmail.org/era/unix/award.html ). Je ne suis pas d'accord pour les raisons suivantes:

  • Il est facile de comprendre ce qui se passe.

    Lors de la lecture d'une commande UNIX, vous vous attendez à une commande suivie d'arguments suivis d'une redirection. Il est possible de mettre la redirection n'importe où mais cela est rarement vu - ainsi les gens auront plus de mal à lire l'exemple. Je crois

    cat foo | program1 -o option -b option | program2
    

    est plus facile à lire que

    program1 -o option -b option < foo | program2
    

    Si vous déplacez la redirection au début, vous confondez les gens qui ne sont pas habitués à cette syntaxe:

    < foo program1 -o option -b option | program2
    

    et les exemples doivent être faciles à comprendre.

  • C'est facile à changer.

    Si vous savez que le programme peut lire à partir de cat, vous pouvez normalement supposer qu'il peut lire la sortie de n'importe quel programme qui sort vers STDOUT, et ainsi vous pouvez l'adapter à vos propres besoins et obtenir des résultats prévisibles.

  • Il souligne que le programme n'échoue pas, si STDIN n'est pas un fichier.

    Il n'est pas sûr de supposer que si program1 < foo fonctionne alors cat foo | program1 fonctionnera également. Cependant, il est prudent de supposer le contraire. Ce programme fonctionne si STDIN est un fichier, mais échoue si l'entrée est un canal, car il utilise la recherche:

    # works
    < foo Perl -e 'seek(STDIN,1,1) || die;print <STDIN>'
    
    # fails
    cat foo | Perl -e 'seek(STDIN,1,1) || die;print <STDIN>'
    

Coût de performance

Il y a un coût à faire le cat supplémentaire. Pour donner une idée de combien j'ai exécuté quelques tests pour simuler la ligne de base (cat), un faible débit (bzip2), débit moyen (gzip) et débit élevé (grep).

cat $ISO | cat
< $ISO cat
cat $ISO | bzip2
< $ISO | bzip2
cat $ISO | gzip
< $ISO gzip
cat $ISO | grep no_such_string
< $ISO grep no_such_string

Les tests ont été effectués sur un système bas de gamme (0,6 GHz) et un ordinateur portable ordinaire (2,2 GHz). Ils ont été exécutés 10 fois sur chaque système et le meilleur timing a été choisi pour imiter la situation optimale pour chaque test. L'ISO $ était ubuntu-11.04-desktop-i386.iso. (Des tableaux plus jolis ici: http://oletange.blogspot.com/2013/10/useless-use-of-cat.html )

CPU                       0.6 GHz ARM
Command                   cat $ISO|                        <$ISO                            Diff                             Diff (pct)
Throughput \ Time (ms)    User       Sys        Real       User       Sys        Real       User       Sys        Real       User       Sys        Real
Baseline (cat)                     55      14453      33090         23       6937      33126         32       7516        -36        239        208         99
Low (bzip2)                   1945148      16094    1973754    1941727       5664    1959982       3420      10430      13772        100        284        100
Medium (gzip)                  413914      13383     431812     407016       5477     416760       6898       7906      15052        101        244        103
High (grep no_such_string)      80656      15133      99049      79180       4336      86885       1476      10797      12164        101        349        114

CPU                       Core i7 2.2 GHz
Command                   cat $ISO|           <$ISO             Diff          Diff (pct)
Throughput \ Time (ms)    User     Sys Real   User   Sys Real   User Sys Real User       Sys Real
Baseline (cat)                    0 356    215      1  84     88    0 272  127          0 423  244
Low (bzip2)                  136184 896 136765 136728 160 137131 -545 736 -366         99 560   99
Medium (gzip)                 26564 788  26791  26332 108  26492  232 680  298        100 729  101
High (grep no_such_string)      264 392    483    216  84    304   48 308  179        122 466  158

Les résultats montrent que pour un débit faible et moyen, le coût est de l'ordre de 1%. C'est bien dans l'incertitude des mesures, donc en pratique il n'y a pas de différence.

Pour un débit élevé, la différence est plus grande et il existe une nette différence entre les deux.

Cela conduit à la conclusion: vous devez utiliser < au lieu de cat | si:

  • la complexité du traitement est similaire à un simple grep
  • les performances comptent plus que la lisibilité.

Sinon, peu importe que vous utilisiez < ou cat |.

Et donc vous ne devriez attribuer un prix UUoC que si et seulement si:

  • vous pouvez mesurer une différence significative dans la performance (publiez vos mesures lorsque vous attribuez le prix)
  • les performances comptent plus que la lisibilité.
7
Ole Tange