web-dev-qa-db-fra.com

Pourquoi Shell ne corrige-t-il pas automatiquement «l'utilisation inutile de chat»?

Beaucoup de gens utilisent des oneliners et des scripts contenant du code le long des lignes

cat "$MYFILE" | command1 | command2 > "$OUTPUT"

Le premier cat est souvent appelé "utilisation inutile de chat" car il nécessite techniquement le démarrage d'un nouveau processus (souvent /usr/bin/cat) où cela pourrait être évité si la commande avait été

< "$MYFILE" command1 | command2 > "$OUTPUT"

car alors Shell n'a plus qu'à démarrer command1 et pointez simplement son stdin vers le fichier donné.

Pourquoi le Shell ne fait-il pas cette conversion automatiquement? Je pense que la syntaxe "utilisation inutile de chat" est plus facile à lire et Shell devrait avoir suffisamment d'informations pour se débarrasser automatiquement de chat inutile. Le cat est défini dans la norme POSIX donc Shell doit être autorisé à l'implémenter en interne au lieu d'utiliser un chemin binaire dans. Le shell peut même contenir une implémentation uniquement pour une seule version d'argument et se replier sur le chemin binaire.

28
Mikko Rantalainen

Les 2 commandes ne sont pas équivalentes: considérez la gestion des erreurs:

cat <file that doesn't exist> | less produira un flux vide qui sera transmis au programme canalisé ... en tant que tel, vous vous retrouvez avec un affichage ne montrant rien.

< <file that doesn't exist> less n'ouvrira pas la barre, puis ne l'ouvrira pas moins du tout.

Tenter de remplacer le premier par le second peut casser un nombre quelconque de scripts qui s'attendent à exécuter le programme avec une entrée potentiellement vierge.

25
UKMonkey

"L'utilisation inutile de cat" concerne davantage la façon dont vous écrivez votre code que ce qui s'exécute réellement lorsque vous exécutez le script. C'est une sorte de design anti-pattern , une façon de faire quelque chose qui pourrait probablement être fait de manière plus efficace. C'est un échec dans la compréhension de la meilleure façon de combiner les outils donnés pour créer un nouvel outil. Je dirais que la chaîne de plusieurs commandes sed et/ou awk ensemble dans un pipeline pourrait parfois être considérée comme un symptôme de ce même anti-modèle.

La correction d'instances d '"utilisation inutile de cat" dans un script consiste principalement à corriger manuellement le code source du script. Un outil tel que ShellCheck peut vous aider en soulignant les cas évidents:

$ cat script.sh
#!/bin/sh
cat file | cat
$ shellcheck script.sh

In script.sh line 2:
cat file | cat
    ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead.

Il serait difficile d'obtenir le shell pour le faire automatiquement en raison de la nature des scripts Shell. La façon dont un script s'exécute dépend de l'environnement hérité de son processus parent et de l'implémentation spécifique des commandes externes disponibles.

Le shell ne sait pas nécessairement ce qu'est cat. Il peut s'agir de n'importe quelle commande de n'importe où dans votre $PATH, ou une fonction.

S'il s'agissait d'une commande intégrée (qu'elle peut être dans certains shells), elle aurait la possibilité de réorganiser le pipeline comme elle le sait la sémantique de sa commande intégrée cat. Avant de faire cela, il devrait en outre faire des hypothèses sur la prochaine commande dans le pipeline, après le cat d'origine.

Notez que la lecture à partir d'une entrée standard se comporte légèrement différemment lorsqu'elle est connectée à un tuyau et lorsqu'elle est connectée à un fichier. Un canal n'est pas recherchable, donc selon ce que fait la prochaine commande dans le pipeline, il peut ou non se comporter différemment si le pipeline a été réorganisé (il peut détecter si l'entrée est recherchée et décider de faire les choses différemment si c'est le cas ou si il ne l'est pas, en tout cas il se comporterait alors différemment).

Cette question est similaire (dans un sens général très ) à " Y a-t-il des compilateurs qui tentent de corriger eux-mêmes les erreurs de syntaxe? "(sur le site Software Engineering StackExchange), bien que cette question concerne évidemment les erreurs de syntaxe, pas les modèles de conception inutiles. L'idée de changer automatiquement le code en fonction de l'intention est cependant largement la même.

51
Kusalananda

Parce que ce n'est pas inutile.

Dans le cas de cat file | cmd, Le fd 0 (Stdin) de cmd sera un tuyau, et dans le cas de cmd <file Ce peut être un régulier fichier, périphérique, etc.

Un tube a une sémantique différente d'un fichier normal, et sa sémantique n'est pas un sous-ensemble de celles d'un fichier normal:

  • un fichier normal ne peut pas être select(2) éd ou poll(2) éd de manière significative; une select(2) dessus renverra toujours "prêt". Les interfaces avancées comme epoll(2) sous Linux ne fonctionneront tout simplement pas avec les fichiers normaux.

  • sous Linux, il existe des appels système (splice(2), vmsplice(2), tee(2)) qui ne fonctionnent que sur les canaux [1]

Puisque cat est tellement utilisé, il pourrait être implémenté comme un shell intégré qui évitera un processus supplémentaire, mais une fois que vous avez commencé sur ce chemin, la même chose pourrait être faite avec la plupart des commandes - transformer le Shell dans un Perl ou python plus lent et plus maladroit. il est probablement préférable d'écrire un autre langage de script avec une syntaxe de type pipe facile à utiliser pour continuations à la place ;-)

[1] Si vous voulez un exemple simple non fait pour l'occasion, vous pouvez regarder mon "exécutable binaire de stdin" git Gist avec quelques explications dans le commentaire ici . Implémenter cat à l'intérieur pour le faire fonctionner sans UUoC l'aurait rendu 2 ou 3 fois plus gros.

36
mosvy

Parce que détecter un chat inutile est vraiment très difficile.

J'avais un script Shell où j'écrivais

cat | (somecommand <<!
...
/proc/self/fd/3
...
!) 0<&3

Le script Shell a échoué en production si le cat a été supprimé car il a été appelé via su -c 'script.sh' someuser. Le cat apparemment superflu a fait changer le propriétaire de l'entrée standard par l'utilisateur que le script exécutait, de sorte que la réouverture via /proc travaillé.

17
Joshua

tl; dr: Les obus ne le font pas automatiquement car les coûts dépassent le avantages probables.

D'autres réponses ont souligné la différence technique entre stdin étant un tuyau et un fichier. Gardant cela à l'esprit, le Shell pourrait effectuer l'une des actions suivantes:

  1. Implémentez cat en tant que fonction intégrée, tout en préservant la distinction fichier/canal. Cela permettrait d'économiser le coût d'un exec et peut-être, éventuellement, d'une fourchette.
  2. Effectuez une analyse complète du pipeline en connaissant les différentes commandes utilisées pour voir si le fichier/canal est important, puis agissez en fonction de cela.

Ensuite, vous devez considérer les coûts et les avantages de chaque approche. Les avantages sont assez simples:

  1. Dans les deux cas, évitez un exec (de cat)
  2. Dans le second cas, lorsque la substitution de redirection est possible, évite un fork.
  3. Dans les cas où vous devez utiliser un tuyau, il peut parfois éviter un fork/vfork, mais souvent non. En effet, l'équivalent cat doit fonctionner en même temps que le reste du pipeline.

Vous économisez donc un peu de temps CPU et de mémoire, surtout si vous pouvez éviter le fork. Bien sûr, vous économisez ce temps et cette mémoire uniquement lorsque la fonction est réellement utilisée. Et vous ne faites vraiment que gagner du temps fork/exec; avec des fichiers plus volumineux, le temps est principalement le temps d'E/S (c'est-à-dire que le chat lit un fichier à partir du disque). Vous devez donc vous demander: à quelle fréquence cat est-il utilisé (inutilement) dans les scripts Shell où les performances sont réellement importantes? Comparez-le à d'autres commandes Shell courantes comme test - il est difficile d'imaginer que cat est utilisé (inutilement) même un dixième aussi souvent que test est utilisé dans des endroits importants. C'est une supposition, je n'ai pas mesuré, ce que vous voudriez faire avant toute tentative de mise en œuvre. (Ou de la même manière, demander à quelqu'un d'autre de l'implémenter dans, par exemple, une demande de fonctionnalité.)

Ensuite, vous demandez: quels sont les coûts. Les deux coûts qui viennent à l'esprit sont (a) le code supplémentaire dans le Shell, qui augmente sa taille (et donc éventuellement l'utilisation de la mémoire), nécessite plus de travail de maintenance, est un autre endroit pour les bogues, etc .; et (b) les surprises de compatibilité ascendante, POSIX cat omet de nombreuses fonctionnalités, par exemple, GNU coreutils cat, vous devez donc faire attention exactement ce que l'implémentation cat implémenterait.

  1. L'option intégrée supplémentaire n'est probablement pas si mauvaise - en ajoutant une autre intégrée là où un groupe existe déjà. Si vous aviez des données de profilage montrant que cela aiderait, vous pourriez probablement convaincre les auteurs de votre Shell préféré de les ajouter.

  2. En ce qui concerne l'analyse du pipeline, je ne pense pas que les shells font quelque chose comme ça actuellement (quelques-uns reconnaissent la fin d'un pipeline et peuvent éviter un fork). Essentiellement, vous ajouteriez un optimiseur (primitif) au shell; les optimiseurs s'avèrent souvent être du code compliqué et la source de nombreux bugs. Et ces bogues peuvent être surprenants - de légers changements dans le script Shell pourraient éviter ou déclencher le bogue.

Postscript: Vous pouvez appliquer une analyse similaire à vos utilisations inutiles de chat. Avantages: plus facile à lire (bien que si command1 prendra un fichier comme argument, probablement pas). Coûts: fork et exec supplémentaires (et si command1 peut prendre un fichier en argument, probablement des messages d'erreur plus confus). Si votre analyse vous dit d'utiliser inutilement cat, alors allez-y.

13
derobert

La commande cat peut accepter - comme marqueur pour stdin . ( POSIX , " Si un fichier est '-', l'utilitaire cat doit lire à partir de l'entrée standard à ce point dans la séquence. ") Cela permet une gestion simple d'un fichier ou stdin où sinon cela serait interdit.

Considérez ces deux alternatives triviales, où l'argument Shell $1 est -:

cat "$1" | nl    # Works completely transparently
nl < "$1"        # Fails with 'bash: -: No such file or directory'

Une autre fois que cat est utile est où il est intentionnellement utilisé comme no-op simplement pour maintenir la syntaxe Shell:

file="$1"
reader=cat
[[ $file =~ \.gz$ ]] && reader=zcat
[[ $file =~ \.bz2$ ]] && reader=bzcat
"$reader" "$file"

Enfin, je crois que la seule fois où UUOC peut vraiment être correctement appelé est lorsque cat est utilisé avec un nom de fichier connu pour être un fichier normal (c'est-à-dire pas un périphérique ou un canal nommé), et qu'aucun indicateur sont donnés à la commande:

cat file.txt

Dans toute autre situation, les propriétés de cat lui-même peuvent être requises.

10
roaima

La commande cat peut faire des choses que le Shell ne peut pas nécessairement faire (ou du moins, ne peut pas faire facilement). Par exemple, supposons que vous souhaitiez imprimer des caractères qui pourraient autrement être invisibles, tels que des tabulations, des retours chariot ou des retours à la ligne. Il y a * peut-être * un moyen de le faire avec seulement les commandes intégrées de Shell, mais je ne peux penser à aucune de mes capacités. La version GNU de cat peut le faire avec l'argument -A Ou les arguments -v -E -T (Je ne connais pas les autres versions de cat, cependant). Vous pouvez également préfixer chaque ligne avec un numéro de ligne en utilisant -n (Encore une fois, IDK si les versions non GNU peuvent le faire).

Un autre avantage de cat est qu'il peut facilement lire plusieurs fichiers. Pour ce faire, il suffit de taper cat file1 file2 file3. Pour faire de même avec un Shell, les choses deviendraient délicates, bien qu'une boucle soigneusement conçue pourrait très probablement obtenir le même résultat. Cela dit, voulez-vous vraiment prendre le temps d'écrire une telle boucle, alors qu'il existe une alternative aussi simple? Je ne!

La lecture de fichiers avec cat utiliserait probablement moins de CPU que le shell, car cat est un programme précompilé (l'exception évidente est tout shell qui a un chat intégré). Lors de la lecture d'un grand groupe de fichiers, cela peut devenir apparent, mais je ne l'ai jamais fait sur mes machines, donc je ne peux pas en être sûr.

La commande cat peut également être utile pour forcer une commande à accepter une entrée standard dans des cas où elle ne le pourrait pas. Considérer ce qui suit:

echo 8 | sleep

Le nombre "8" ne sera pas accepté par la commande "sleep", car il n'a jamais vraiment été conçu pour accepter une entrée standard. Ainsi, le sommeil ignorera cette entrée, se plaindra d'un manque d'arguments et quittera. Cependant, si l'on tape:

echo 8 | sleep $(cat)

De nombreux shells étendront cela à sleep 8, Et le sommeil attendra 8 secondes avant de quitter. Vous pouvez également faire quelque chose de similaire avec ssh:

command | ssh 1.2.3.4 'cat >> example-file'

Cette commande avec append exemple-fichier sur la machine avec l'adresse de 1.2.3.4 avec tout ce qui est sorti de "commande".

Et c'est (probablement) juste gratter la surface. Je suis sûr que je pourrais trouver plus d'exemples de chats utiles si je le voulais, mais ce post est assez long. Je conclurai donc en disant ceci: demander au Shell d'anticiper tous ces scénarios (et plusieurs autres) n'est pas vraiment faisable.

6
TSJNachos117

N'oubliez pas qu'un utilisateur peut avoir un cat dans son $PATH qui n'est pas exactement le POSIX cat (mais peut-être une variante qui pourrait enregistrer quelque chose quelque part). Dans ce cas, vous ne voulez pas que le shell le supprime.

PATH pourrait changer dynamiquement, puis cat n'est pas ce que vous croyez. Il serait assez difficile d'écrire un Shell faisant l'optimisation dont vous rêvez.

De plus, dans la pratique, cat est un programme assez rapide. Il y a peu de raisons pratiques (sauf esthétiques) pour l'éviter.

Voir aussi l'excellent Parsing POSIX [s] hell conférence de Yann Regis-Gianas au FOSDEM2018. Cela donne d'autres bonnes raisons d'éviter d'essayer de faire ce dont vous rêvez dans un Shell.

Si les performances étaient vraiment un problème pour les shells, quelqu'un aurait proposé un Shell qui utilise une optimisation sophistiquée du compilateur de programme complet, une analyse statique du code source et des techniques de compilation juste à temps (ces trois domaines ont des décennies de progrès et des publications scientifiques et dédiés conférences, par exemple sous SIGPLAN ). Malheureusement, même en tant que sujet de recherche intéressant, qui n'est actuellement pas financé par des agences de recherche ou des investisseurs en capital-risque, et j'en déduis que cela ne vaut tout simplement pas la peine. En d'autres termes, il n'y a probablement pas de marché significatif pour l'optimisation des coques . Si vous avez un demi-million d'euros à dépenser pour de telles recherches, vous trouverez facilement quelqu'un pour le faire, et je pense que cela donnerait des résultats intéressants.

D'un point de vue pratique, la réécriture, pour améliorer ses performances, un petit script Shell (une centaine de lignes) dans n'importe quel meilleur langage de script (Python, AWK, Guile, ...) est couramment réalisé. Et il n'est pas raisonnable (pour de nombreuses raisons d'ingénierie logicielle) d'écrire de gros scripts Shell: lorsque vous écrivez un script Shell dépassant une centaine de lignes, vous devez envisager de le réécrire (même pour des raisons de lisibilité et de maintenance) dans un langage plus approprié. : comme un langage de programmation le Shell est très pauvre. Cependant, il existe de nombreux gros scripts générés Shell, et pour de bonnes raisons (par exemple GNU scripts autoconf générés configure).

En ce qui concerne les énormes fichiers textuels, les passer à cat comme argument single n'est pas une bonne pratique, et la plupart des administrateurs système le savent (lorsqu'un script Shell prend plus d'une minute à s'exécuter, vous commencez à envisager de l’optimiser). Pour les fichiers volumineux de gigaoctets, cat est jamais le bon outil pour les traiter.

3

En ajoutant à la réponse @Kusalananda (et au commentaire @alephzero), le chat pourrait être n'importe quoi:

alias cat='gcc -c'
cat "$MYFILE" | command1 | command2 > "$OUTPUT"

ou

echo 'echo 1' > /usr/bin/cat
cat "$MYFILE" | command1 | command2 > "$OUTPUT"

Il n'y a aucune raison que cat (seul) ou/usr/bin/cat sur le système soit en fait cat l'outil de concaténation.

2
Rob

Deux utilisations "inutiles" du chat:

sort file.txt | cat header.txt - footer.txt | less

... ici cat est utilisé pour mélanger le fichier et l'entrée canalisée.

find . -name '*.info' -type f | sh -c 'xargs cat' | sort

... ici xargs peut accepter un nombre pratiquement infini de noms de fichiers et exécuter cat autant de fois que nécessaire, tout en se comportant comme un seul flux. Cela fonctionne donc pour les grandes listes de fichiers où l'utilisation directe de xargs sort ne fait pas.

1
tasket

Mis à part d'autres choses, cat- check ajouterait une surcharge de performances supplémentaire et une confusion quant à l'utilisation de cat qui est en fait inutile, à mon humble avis, car ces vérifications peuvent être inefficaces et créer des problèmes avec _ cat utilisation.

Lorsque les commandes traitent des flux standard, elles n'ont qu'à se soucier de la lecture/écriture dans les descripteurs de fichiers standard. Les commandes peuvent savoir si stdin est recherche/recherche ou non, ce qui indique un canal ou un fichier.

Si nous ajoutons au mélange en vérifiant quel processus fournit réellement ce contenu stdin, nous devrons trouver le processus de l'autre côté du tuyau et appliquer l'optimisation appropriée. Cela peut être fait en termes de Shell lui-même, comme le montre le post SuperUser de Kyle Jones, et en termes de Shell c'est

(find /proc -type l | xargs ls -l | fgrep 'pipe:[20043922]') 2>/dev/null

comme indiqué dans le post lié. Il s'agit de 3 commandes supplémentaires (donc extra fork() s et exec() s) et traversées récursives (donc beaucoup d'appels readdir()).

En termes de code source C et Shell, le Shell connaît déjà le processus enfant, donc il n'y a pas besoin de récursivité, mais comment savoir quand optimiser et quand cat est réellement inutile? Il y a en fait des utilisations utiles de chat , comme

# adding header and footer to file
( cmd; cat file; cmd ) | cmd
# tr command does not accept files as arguments
cat log1 log2 log3 | tr '[:upper:]' '[:lower:]'

Il serait probablement inutile et inutile d'ajouter une telle optimisation au shell. Comme la réponse de Kusalanda l'a déjà mentionné, UUOC concerne davantage le manque de compréhension de l'utilisateur quant à la meilleure façon de combiner les commandes pour de meilleurs résultats.

0