La façon la plus simple d'afficher le contenu d'un fichier est d'utiliser la commande cat
:
cat file.txt
Je peux obtenir le même résultat en utilisant la redirection d'entrée:
cat < file.txt
Alors, quelle est la différence entre eux?
Il n'y a aucune différence d'un point de vue utilisateur. Ces commandes font la même chose.
Techniquement, la différence réside dans le programme qui ouvre le fichier: le programme cat
ou le shell qui l'exécute. Les redirections sont définies par le shell, avant d'exécuter une commande.
(Ainsi, dans certaines commandes other - c'est-à-dire not la commande indiquée dans la question - il peut y avoir une différence. En particulier, si vous ne pouvez pas accéder à file.txt
mais l'utilisateur root peut, puis Sudo cat file.txt
fonctionne mais Sudo cat < file.txt
ne fait pas.)
Vous pouvez utiliser l'un ou l'autre qui est pratique dans votre cas.
Il existe presque toujours plusieurs façons d'obtenir le même résultat.
cat
accepte un fichier d'arguments ou stdin
s'il n'y a pas d'arguments.
Voir man cat
:
SYNOPSIS
cat [OPTION]... [FILE]...
DESCRIPTION
Concatenate FILE(s) to standard output.
With no FILE, or when FILE is -, read standard input.
cat file
Le programme cat
ouvrira, lira et fermera le fichier.
cat < file
Votre Shell ouvrira le fichier et connectera le contenu au stdin de cat
. cat
reconnaît qu'il n'a aucun argument de fichier et lira à partir de stdin.
Une grande différence est avec le *
, ?
, ou [
globbing caractères (caractères génériques) ou tout autre élément que le shell peut développer en plusieurs noms de fichiers. Tout ce que le Shell développe en deux éléments ou plus, plutôt que de le traiter comme un seul nom de fichier, ne peut pas être ouvert pour la redirection.
Sans redirection (c'est-à-dire sans <
), le shell passe plusieurs noms de fichiers à cat
, qui sort le contenu des fichiers les uns après les autres. Par exemple, cela fonctionne:
$ ls hello?.py
hello1.py hello2.py
$ cat hello?.py
# Output for two files 'hello1.py' and 'hello2.py' appear on your screen
Mais avec redirection (<
) un message d'erreur se produit:
$ ls < hello?.py
bash: hello?.py: ambiguous redirect
$ cat < hello?.py
bash: hello?.py: ambiguous redirect
Je pensais qu'avec la redirection ce serait plus lent mais il n'y a pas de différence de temps perceptible:
$ time for f in * ; do cat "$f" > /dev/null ; done
real 0m3.399s
user 0m0.130s
sys 0m1.940s
$ time for f in * ; do cat < "$f" > /dev/null ; done
real 0m3.430s
user 0m0.100s
sys 0m2.043s
Notes:
La principale différence est de savoir qui ouvre le fichier, Shell ou cat. Ils peuvent fonctionner avec différents régimes d'autorisation, donc
Sudo cat /proc/some-protected-file
peut fonctionner pendant
Sudo cat < /proc/some-protected-file
échouera. Ce type de régime d'autorisation peut être un peu difficile à contourner lorsque vous souhaitez simplement utiliser echo
pour faciliter l'écriture de scripts, il y a donc la possibilité de mal utiliser tee
comme dans
echo level 7|Sudo tee /proc/acpi/ibm/fan
ce qui ne fonctionne pas vraiment en utilisant la redirection à la place à cause du problème d'autorisation.
Avec cat file.txt
, L'application (dans ce cas cat
) a reçu un paramètre positionnel, exécute syscall open (2) dessus et des vérifications de permission ont lieu dans les applications.
Avec cat < file.txt
, Le shell exécutera dup2()
syscall pour faire de stdin une copie du descripteur de fichier (généralement le prochain disponible, par exemple 3) correspondant à file.txt
Et fermera ce descripteur de fichier (par exemple 3). L'application n'effectue pas open (2) sur le fichier et ignore l'existence du fichier; il fonctionne strictement sur son descripteur de fichier stdin. La vérification des autorisations incombe au shell. La description du fichier ouvert restera la même que lorsque le shell a ouvert le fichier.
En surface, cat file.txt
Et cat < file.txt
Se comportent de la même façon, mais il y a beaucoup plus de choses en coulisse avec cette différence de caractère unique. Ce caractère <
Change la façon dont Shell comprend file.txt
, Qui ouvre le fichier et comment le fichier est transmis entre Shell et la commande. Bien sûr, afin d'expliquer tous ces détails, nous devons également comprendre comment fonctionne l'ouverture des fichiers et l'exécution des commandes dans Shell, et c'est ce que ma réponse vise à atteindre - éduquer le lecteur, en termes les plus simples possibles, sur ce qui se passe réellement dans ces commandes apparemment simples. Dans cette réponse, vous trouverez plusieurs exemples, y compris ceux qui utilisent la commande strace pour sauvegarder les explications de ce qui se passe réellement dans les coulisses.
Parce que le fonctionnement interne des shells et des commandes est basé sur des appels système standard, il est important de visualiser cat
comme une seule commande parmi tant d'autres. Si vous êtes un débutant lisant cette réponse, veuillez vous fixer avec un esprit ouvert et être conscient que prog file.txt
Ne sera pas toujours le même que prog < file.txt
. Une commande différente peut se comporter entièrement différemment lorsque les deux formulaires lui sont appliqués, et cela dépend des autorisations ou de la façon dont le programme est écrit. Je vous demande également de suspendre votre jugement et de considérer cela du point de vue de différents utilisateurs - pour un utilisateur Shell occasionnel, les besoins peuvent être entièrement différents de ceux d'un administrateur système et d'un développeur.
Les shells exécutent des commandes en créant un processus enfant avec fork (2) syscall et en appelant execve (2) syscall, qui exécute la commande avec les arguments et les variables d'environnement spécifiés. La commande appelée inside execve()
prendra le relais et remplacera le processus; par exemple, lorsque Shell appelle cat
, il crée d'abord un processus enfant avec le PID 12345 et après que execve()
se produit, le PID 12345 devient cat
.
Cela nous amène à la différence entre cat file.txt
Et cat < file.txt
. Dans le premier cas, cat file.txt
Est une commande appelée avec un paramètre positionnel, et le Shell assemblera execve()
de manière appropriée:
$ strace -e execve cat testfile.txt
execve("/bin/cat", ["cat", "testfile.txt"], 0x7ffcc6ee95f8 /* 50 vars */) = 0
hello, I am testfile.txt
+++ exited with 0 +++
Dans le second cas, la partie <
Est l'opérateur Shell et < testfile.txt
Dit au Shell d'ouvrir testfile.txt
Et de faire du descripteur de fichier stdin 0 une copie du descripteur de fichier qui correspond à testfile.txt
. Cela signifie que < testfile.txt
Ne sera pas transmis à la commande elle-même comme argument positionnel:
$ strace -e execve cat < testfile.txt
execve("/bin/cat", ["cat"], 0x7ffc6adb5490 /* 50 vars */) = 0
hello, I am testfile.txt
+++ exited with 0 +++
$
Cela peut être important si le programme nécessite un paramètre positionnel pour fonctionner correctement. Dans ce cas, cat
accepte par défaut l'entrée de stdin si aucun paramètre de position correspondant aux fichiers n'a été fourni. Ce qui nous amène également au sujet suivant: stdin et descripteurs de fichiers.
Qui ouvre le fichier - cat
ou Shell? Comment l'ouvrent-ils? Ont-ils même la permission de l'ouvrir? Ce sont les questions qui peuvent être posées, mais nous devons d'abord comprendre comment fonctionne l'ouverture d'un fichier.
Lorsqu'un processus exécute open()
ou openat()
sur un fichier, ces fonctions fournissent au processus un entier correspondant au fichier ouvert, et les programmes peuvent alors appeler read()
, seek()
et write()
appels et une myriade d'autres appels système en faisant référence à ce nombre entier. Bien sûr, le système (alias noyau) gardera en mémoire comment un fichier particulier a été ouvert, avec quel type d'autorisations, avec quel type de mode - lecture seule, écriture seule, lecture/écriture - et où dans le fichier nous sommes actuellement - à l'octet 0 ou à l'octet 1024 - qui est appelé décalage. Cela s'appelle description du fichier ouvert.
Au niveau très basique, cat testfile.txt
Est l'endroit où cat
ouvre le fichier et il sera référencé par le prochain descripteur de fichier disponible qui est 3 (notez le 3 dans lire (2) ).
$ strace -e read -f cat testfile.txt > /dev/null
...
read(3, "hello, I am testfile.txt and thi"..., 131072) = 79
read(3, "", 131072) = 0
+++ exited with 0 +++
En revanche, cat < testfile.txt
Utilisera le descripteur de fichier 0 (alias stdin):
$ strace -e read -f cat < testfile.txt > /dev/null
...
read(0, "hello, I am testfile.txt and thi"..., 131072) = 79
read(0, "", 131072) = 0
+++ exited with 0 +++
Vous vous souvenez quand plus tôt nous avons appris que les shells exécutent des commandes via fork()
d'abord puis exec()
type de processus? Eh bien, il s'avère que comment le fichier est ouvert carie sur les processus enfants créés avec le modèle fork()/exec()
. Pour citer ouvrir (2) manuel :
Lorsqu'un descripteur de fichier est dupliqué (en utilisant dup (2) ou similaire), le doublon fait référence à la même description de fichier ouvert que le descripteur de fichier d'origine, et les deux descripteurs de fichiers partagent par conséquent les indicateurs de décalage et d'état de fichier. Un tel partage peut également se produire entre les processus: un processus enfant créé via fork (2) hérite les doublons des descripteurs de fichiers de son parent, et ces doublons font référence aux mêmes descriptions de fichiers ouverts
Qu'est-ce que cela signifie pour cat file.txt
Vs cat < file.txt
? Beaucoup en fait. Dans cat file.txt
, Le cat
ouvre le fichier, ce qui signifie qu'il contrôle la façon dont le fichier est ouvert. Dans le second cas, Shell ouvrira le file.txt
Et comment il a été ouvert restera inchangé pour les processus enfants, les commandes composées et les pipelines. Où nous en sommes actuellement dans le fichier restera également le même.
Utilisons ce fichier comme exemple:
$ cat testfile.txt
hello, I am testfile.txt and this is first line
line two
line three
last line
Regardez l'exemple ci-dessous. Pourquoi le mot line
n'a-t-il pas changé dans la première ligne?
$ { head -n1; sed 's/line/potato/'; } < testfile.txt 2>/dev/null
hello, I am testfile.txt and this is first line
potato two
potato three
last potato
La réponse réside dans la citation de open (2) manuel ci-dessus: le fichier ouvert par le shell est dupliqué sur stdin de la commande composée et chaque commande/processus qui s'exécute partage l'offset de la description du fichier ouvert . head
a simplement rembobiné le fichier en avant d'une ligne, et sed
s'est occupé du reste. Plus précisément, nous verrions 2 séquences de syscalls dup2()
/fork()
/execve()
, et dans chaque cas, nous obtenons la copie du descripteur de fichier qui fait référence au même description du fichier sur le dossier ouvert testfile.txt
. Confus ? Prenons un exemple un peu plus fou:
$ { head -n1; dd of=/dev/null bs=1 count=5; cat; } < testfile.txt 2>/dev/null
hello, I am testfile.txt and this is first line
two
line three
last line
Ici, nous avons imprimé la première ligne, puis rembobiné la description du fichier ouvert 5 octets à l'avance (ce qui a éliminé le mot line
), puis nous avons simplement imprimé le reste. Et comment avons-nous réussi à le faire? La description du fichier ouvert sur testfile.txt
Reste la même, avec un décalage partagé sur le fichier.
Maintenant, pourquoi cela est utile à comprendre, à part écrire des commandes composées folles comme ci-dessus? En tant que développeur, vous souhaiterez peut-être profiter de ce comportement ou vous en méfier. Disons qu'au lieu de cat
vous avez écrit un programme C qui a besoin d'une configuration passée en fichier ou passée de stdin, et vous l'exécutez comme myprog myconfig.json
. Que se passera-t-il si vous exécutez à la place { head -n1; myprog;} < myconfig.json
? Au mieux, votre programme obtiendra des données de configuration incomplètes, et au pire - interrompez le programme. Nous pouvons également utiliser cela comme un avantage pour générer un processus enfant et permettre aux parents de revenir en arrière aux données dont le processus enfant devrait s'occuper.
Commençons par un exemple cette fois sur un fichier sans autorisation de lecture ou d'écriture pour les autres utilisateurs:
$ Sudo -u potato cat < testfile.txt
hello, I am testfile.txt and this is first line
line two
line three
last line
$ Sudo -u potato cat testfile.txt
cat: testfile.txt: Permission denied
Que s'est-il passé ici ? Pourquoi pouvons-nous lire le fichier dans le premier exemple en tant qu'utilisateur potato
mais pas dans le second? Cela revient à la même citation de la page de manuel open (2) mentionnée précédemment. Avec < file.txt
Shell ouvre le fichier, d'où les vérifications de permission se produisent au moment de open
/openat()
effectuée par Shell. Le shell à ce moment-là s'exécute avec les privilèges du propriétaire du fichier qui dispose des autorisations de lecture sur le fichier. En raison de l'héritage de la description du fichier ouvert entre les appels dup2
, Le shell transmet la copie du descripteur de fichier ouvert à Sudo
, qui transmet la copie du descripteur de fichier à cat
et cat
ne sachant rien d'autre lit joyeusement le contenu du fichier. Dans la dernière commande, l'utilisateur cat
sous potato exécute open()
sur le fichier, et bien sûr, cet utilisateur n'a pas l'autorisation de lire le fichier.
Plus concrètement et plus communément, c'est pourquoi les utilisateurs sont perplexes quant à la raison pour laquelle quelque chose comme ça ne fonctionne pas (exécution d'une commande privilégiée pour écrire dans un fichier qu'ils ne peuvent pas ouvrir):
$ Sudo echo 100 > /sys/class/drm/*/intel_backlight/brightness
bash: /sys/class/drm/card0-eDP-1/intel_backlight/brightness: Permission denied
Mais quelque chose comme ça fonctionne (en utilisant une commande privilégiée pour écrire dans un fichier qui nécessite des privilèges):
$ echo 100 |Sudo tee /sys/class/drm/*/intel_backlight/brightness
[Sudo] password for administrator:
100
Un exemple théorique de la situation opposée à celle que j'ai montrée précédemment (où privileged_prog < file.txt
Échoue mais privileged_prog file.txt
Fonctionne) serait avec les programmes SUID. Les programmes SUID , tels que passwd
, permettent d'effectuer des actions avec les autorisations du propriétaire de l'exécutable. C'est pourquoi la commande passwd
vous permet de changer votre mot de passe puis d'écrire cette modification dans / etc/shadow même si le fichier appartient à l'utilisateur root.
Et par souci d'exemple et de plaisir, j'écris en fait une démo rapide cat
- comme une application en C ( code source ici) avec le bit SUID défini, mais si vous obtenez le point - ressentez libre de passer à la section suivante de cette réponse et d'ignorer cette partie. Note latérale: le système d'exploitation ignore le bit SUID sur les exécutables interprétés avec #!
, Donc une version Python de cette même chose échouerait).
Vérifions les autorisations sur le programme et le testfile.txt
:
$ ls -l ./privileged
-rwsr-xr-x 1 administrator administrator 8672 Nov 29 16:39 ./privileged
$ ls -l testfile.txt
-rw-r----- 1 administrator administrator 79 Nov 29 12:34 testfile.txt
Semble bon, seuls le propriétaire du fichier et ceux qui appartiennent au groupe administrator
peuvent lire ce fichier. Maintenant, connectons-nous en tant qu'utilisateur de pomme de terre et essayons de lire le fichier:
$ su potato
Password:
potato@my-PC:/home/administrator$ cat ./testfile.txt
cat: ./testfile.txt: Permission denied
potato@my-PC:/home/administrator$ cat < ./testfile.txt
bash: ./testfile.txt: Permission denied
Ça a l'air bien, ni Shell ni cat
qui ont des droits d'utilisateur de pomme de terre ne peuvent lire le fichier qu'ils ne sont pas autorisés à lire. Notez également qui a signalé l'erreur - cat
vs bash
. Testons notre programme SUID:
potato@my-PC:/home/administrator$ ./privileged testfile.txt
hello, I am testfile.txt and this is first line
line two
line three
last line
potato@my-PC:/home/administrator$ ./privileged < testfile.txt
bash: testfile.txt: Permission denied
Fonctionne comme prévu! Encore une fois, le point soulevé par cette petite démo est que prog file.txt
Et prog < file.txt
Diffèrent dans qui ouvre le fichier et diffèrent dans les autorisations de fichier ouvert.
Nous savons déjà que < testfile.txt
Réécrit stdin de telle manière que les données proviendront du fichier spécifié au lieu du clavier. En théorie, et basés sur la philosophie Unix de "faire une chose et bien le faire", les programmes lisant depuis stdin (aka descripteur de fichier 0) devraient se comporter de manière cohérente, et en tant que tel, prog1 | prog2
Devrait être similaire à prog2 file.txt
. Mais que se passe-t-il si prog2
Veut rembobiner avec lseek syscall, par exemple pour passer à certains octets ou rembobiner jusqu'à la fin afin de trouver la quantité de données dont nous disposons) ?
Certains programmes n'autorisent pas la lecture des données du tuyau, car les pipelines ne peuvent pas être rembobinés avec lseek (2) syscall ou les données ne peuvent pas être chargées en mémoire avec mmap (2) pour un traitement plus rapide. Cela a été couvert par une excellente réponse de Stephane Chazelas dans cette question: Quelle est la différence entre "cat file | ./binary" et "./binary <file"? = Je recommande fortement de lire cela.
Heureusement, cat < file.txt
Et cat file.txt
Se comportent de manière cohérente et cat
n'est en aucun cas contre les canaux, bien que nous sachions qu'il lit des descripteurs de fichiers entièrement différents. Comment cela s'applique-t-il dans prog file.txt
Contre prog < file.txt
En général? Si un programme ne veut vraiment rien faire avec les tuyaux, le paramètre de position manquant file.txt
Suffira pour quitter avec erreur, mais l'application peut toujours utiliser lseek()
sur stdin pour le vérifier est un tuyau ou non (bien que isatty (3) ou la détection du mode S_ISFIFO dans fstat (2) soit plus susceptible d'être utilisé pour détecter l'entrée de tuyau), auquel cas faire quelque chose comme ./binary <(grep pattern file.txt)
ou ./binary < <(grep pattern file.txt)
peut ne pas fonctionner.
Un type de fichier peut influencer le comportement de prog file
Par rapport à prog < file
. Ce qui implique dans une certaine mesure qu'en tant qu'utilisateur d'un programme, vous choisissez les appels système même si vous n'en avez pas conscience. Par exemple, supposons que nous ayons une socket de domaine Unix et que nous exécutions le serveur nc
pour l'écouter, peut-être avons-nous même préparé des données à servir
$ nc -U -l /tmp/mysocket.sock < testfile.txt
Dans ce cas, /tmp/mysocket.sock
Sera ouvert via différents appels système:
socket(AF_UNIX, SOCK_STREAM, 0) = 3
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {sa_family=AF_UNIX, Sun_path="/tmp/mysocket.sock"}, 20) = 0
Maintenant, essayons de lire les données de ce socket dans un terminal différent:
$ cat /tmp/mysocket.sock
cat: /tmp/mysocket.sock: No such device or address
$ cat < /tmp/mysocket.sock
bash: /tmp/mysocket.sock: No such device or address
Le shell et le chat exécutent tous les deux open(2)
syscall sur ce qui nécessite un syscall entièrement différent - la paire socket (2) et connect (2). Même cela ne fonctionne pas:
$ nc -U < /tmp/mysocket.sock
bash: /tmp/mysocket.sock: No such device or address
Mais si nous sommes conscients du type de fichier et de la façon dont nous pouvons appeler le bon syscall, nous pouvons obtenir le comportement souhaité:
$ nc -U /tmp/mysocket.sock
hello, I am testfile.txt and this is first line
line two
line three
last line
La citation de open (2) manual indique que les autorisations sur le descripteur de fichier sont héritées. En théorie, il existe un moyen de modifier les autorisations de lecture/écriture sur un descripteur de fichier mais cela doit être fait au niveau du code source.
Qu'est-ce qu'une description de fichier ouvert? . Voir aussi définition POSIX
Comment Linux vérifie-t-il l'autorisation du descripteur de fichier?