Простейшим способом отображения содержимого файла является использование строковой команды cat
:
cat file.txt
Тот же результат, который я получаю, используя перенаправление ввода:
cat < file.txt
Тогда, что чем они отличаются?
Нет никакого различия. Эти команды делают то же самое.
можно использовать любой, который удобен в случае.
существует почти всегда много способов получить тот же результат.
cat
принимает файл от аргументов или stdin
, при отсутствии аргументов.
См. 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
cat
программа откроется, считать и закрыть файл.
cat < file
Ваша оболочка откроет файл и подключит содержание с cat
stdin. cat
распознает, что это не имеет никаких аргументов файла и будет читать из stdin.
На большой разнице cat
без перенаправления, обработает подстановочные знаки. С перенаправлением происходит сообщение об ошибке:
$ cat * > /dev/null
$ cat < * > /dev/null
bash: *: ambiguous redirect
я думал с перенаправлением, это будет медленнее, но нет никакой заметной разницы во времени:
$ 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
Примечания:
Существенное различие - то, кто открывает файл, оболочку или кошку. Они могут работать с различными режимами разрешения, таким образом
sudo cat /proc/some-protected-file
может работать, в то время как
sudo cat < /proc/some-protected-file
перестанет работать. Этот вид режима разрешения может быть немного хитрым для работы вокруг, просто желая использовать echo
для легких сценариев, таким образом, существует целесообразность неправильного использования tee
как в
echo level 7|sudo tee /proc/acpi/ibm/fan
, который действительно не работает с помощью перенаправления вместо этого из-за проблемы разрешения.
С cat file.txt
приложение (в данном случае cat
) получило один позиционный параметр , выполняет на нем системный вызов open(2), а проверки разрешений происходят внутри приложений.
С cat < file.txt
оболочка выполнит системный вызов dup2()
, чтобы преобразовать стандартный ввод в копию дескриптора файла (обычно следующего доступного, например 3), соответствующего file.txt
и закройте этот файловый дескриптор (например, 3). Приложение не выполняет open(2) для файла и не знает о существовании файла; он работает строго со своим файловым дескриптором stdin. Проверка разрешений возлагается на оболочку. Описание открытого файла останется таким же, как при открытии файла оболочкой.
На поверхности cat file.txt
и cat < файл.txt
ведут себя одинаково, но за кулисами происходит гораздо больше с этой единственной разницей в символах. Этот символ <
меняет то, как оболочка понимает file.txt
, кто открывает файл и как файл передается между оболочкой и командой. Конечно, для того, чтобы объяснить все эти детали, нам также необходимо понять, как открытие файлов и выполнение команд работает в оболочке, и именно на это направлен мой ответ - рассказать читателю в самых простых словах о том, что на самом деле происходит в эти, казалось бы, простые команды. В этом ответе вы найдете несколько примеров, в том числе те, которые используют команду strace для резервного копирования объяснений того, что на самом деле происходит за кулисами.
Поскольку внутренняя работа оболочек и команд основана на стандартных системных вызовах, важно рассматривать cat
как одну из многих других команд. Если вы новичок, читающий этот ответ, будьте непредвзяты и помните, что prog file.txt
не всегда будет таким же, как prog
Оболочки запускают команды, создавая дочерний процесс с системным вызовом fork(2) и вызывая системный вызов execve(2), который выполняет команду с указанными аргументами и переменными среды. Команда, вызываемая внутри execve()
, возьмет на себя управление и заменит процесс; например, когда оболочка вызывает cat
, она сначала создает дочерний процесс с PID 12345, а после execve()
PID 12345 становится cat
.
Это подводит нас к различию между cat file.txt
и cat < file.txt
. В первом случае cat file.txt
— это команда, вызываемая с одним позиционным параметром, и оболочка соберет вместе execve()
соответствующим образом:
$ strace -e execve cat testfile.txt
execve("/bin/cat", ["cat", "testfile.txt"], 0x7ffcc6ee95f8 /* 50 vars */) = 0
hello, I am testfile.txt
+++ exited with 0 +++
Во втором случае Часть <
является оператором оболочки, а < testfile.txt
указывает оболочке открыть testfile.txt
и преобразовать дескриптор файла stdin 0 в копию дескриптора файла, который соответствует в testfile.txt
. Это означает, что < testfile.txt
не будет передаваться самой команде в качестве позиционного аргумента:
$ strace -e execve cat < testfile.txt
execve("/bin/cat", ["cat"], 0x7ffc6adb5490 /* 50 vars */) = 0
hello, I am testfile.txt
+++ exited with 0 +++
$
Это может иметь значение, если программе требуется позиционный параметр для правильной работы. В этом случае cat
по умолчанию принимает ввод со стандартного ввода, если не были предоставлены позиционные параметры, соответствующие файлам. Что также подводит нас к следующей теме: стандартный ввод и файловые дескрипторы.
Кто открывает файл — cat
или оболочка? Как они его открывают? У них вообще есть разрешение открыть его? Это вопросы, которые можно задать, но сначала нам нужно понять, как работает открытие файла.
Когда процесс выполняет open()
или openat()
над файлом, эти функции предоставляют процессу целое число, соответствующее открытому файлу,затем программы могут вызывать вызовы read()
, seek()
и write()
и множество других системных вызовов, ссылаясь на это целое число. Конечно, система (также известная как ядро) будет хранить в памяти, как был открыт конкретный файл, с какими разрешениями, в каком режиме — только чтение, только запись, чтение/запись — и где в файле мы сейчас находимся. - в байте 0 или байте 1024 - что называется смещением. Это называется открыть описание файла.
На самом базовом уровне cat testfile.txt
— это место, где cat
открывает файл, и на него будет ссылаться следующий доступный файловый дескриптор, равный 3 (обратите внимание на 3 в чтение(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 +++
Напротив, cat < testfile.txt
будет использовать файловый дескриптор 0 (также известный как 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 +++
Помните, как ранее мы узнали, что оболочки сначала запускают команды через fork()
затем exec()
тип процесса? Оказывается, то, как открыт файл, влияет на дочерние процессы, созданные с помощью шаблона fork()/exec()
. Процитируем open(2) manual:
Когда файловый дескриптор дублируется (используя dup(2) или подобное), дубликат относится к тому же описанию открытого файла, что и оригинал файловый дескриптор, и, следовательно, два файловых дескриптора совместно используют флаги смещения файла и состояния файла. Такое совместное использование также может происходить между процессами: дочерний процесс, созданный с помощью fork(2), наследует дубликаты файловых дескрипторов своего родителя, и эти дубликаты обратитесь к тем же описаниям открытых файлов
Что это означает для cat file.txt
и cat
cat file.txt
cat
открывает файл, что означает, что он контролирует, как файл открывается. Во втором случае оболочка откроет файл file.txt
, и способ его открытия останется неизменным для дочерних процессов, составных команд и конвейеров. Где мы сейчас находимся в файле, также останется прежним.
В качестве примера возьмем этот файл:
$ cat testfile.txt
hello, I am testfile.txt and this is first line
line two
line three
last line
Посмотрите на пример ниже. Почему слово строка
не изменилось в первой строке?
$ { 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
Ответ кроется в цитате из руководства open(2) выше: файл, открытый оболочкой, дублируется на стандартный ввод составной команды, и каждая выполняемая команда/процесс разделяет смещение описания открытого файла. head
просто перематывал файл на одну строку вперед, а sed
обрабатывал все остальное. В частности, мы увидим 2 последовательности системных вызовов dup2()
/fork()
/execve()
, и в каждом случае мы получим копию дескриптор файла, который ссылается на то же описание файла в открытом testfile.txt
.Смущенный ? Давайте возьмем немного более сумасшедший пример:
$ { 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
Здесь мы напечатали первую строку, затем перемотали описание открытого файла на 5 байт вперед (что исключило слово строка
), а затем просто напечатали остальное. И как нам это удалось? Описание открытого файла в testfile.txt
остается прежним, с общим смещением в файле.
Теперь, почему это полезно понимать, кроме написания сумасшедших составных команд, подобных приведенным выше? Как разработчик, вы можете воспользоваться преимуществом или остерегаться такого поведения. Скажем, вместо cat
вы написали программу на C, которой нужна конфигурация либо в виде файла, либо из стандартного ввода, и вы запускаете ее как myprog myconfig.json
. Что произойдет, если вместо этого вы запустите { head -n1; myprog;} < myconfig.json
? В лучшем случае ваша программа получит неполные данные конфигурации, а в худшем - сломает программу. Мы также можем использовать это как преимущество, чтобы создать дочерний процесс и позволить родителю перемотать данные, о которых должен позаботиться дочерний процесс.
Давайте на этот раз начнем с примера файла, у которого нет прав на чтение или запись для других пользователей:
$ 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
Что здесь произошло? Почему мы можем прочитать файл в первом примере как пользователь potato
, но не во втором? Это восходит к той же цитате из справочной страницы open(2), упомянутой ранее. С < file.txt
оболочка открывает файл, поэтому проверки разрешений происходят во время open
/openat()
, выполняемых оболочкой.В это время оболочка запускается с привилегиями владельца файла, у которого есть права на чтение файла. В силу того, что описание открытого файла наследуется через вызовы dup2
, оболочка передает копию дескриптора открытого файла в sudo
, который передает копию дескриптора файла в cat
, и cat
, не подозревая ни о чем другом, с удовольствием читает содержимое файла. В последней команде cat
под пользователем картофеля выполняет open()
над файлом,и, конечно же, у этого пользователя нет прав на чтение файла.
Более практично и чаще всего, вот почему пользователи недоумевают, почему что-то подобное не работает (запуск привилегированной команды для записи в файл, который они не могут открыть):
$ sudo echo 100 > /sys/class/drm/*/intel_backlight/brightness
bash: /sys/class/drm/card0-eDP-1/intel_backlight/brightness: Permission denied
Но что-то вроде этого работает (используя привилегированная команда для записи в файл, который не требует привилегий):
$ echo 100 |sudo tee /sys/class/drm/*/intel_backlight/brightness
[sudo] password for administrator:
100
Теоретический пример ситуации, противоположной той, что я показал ранее (где привилегированная_программа < файл.txt
не выполняется, но привилегированная_прог файл.txt
действительно работает) будет с программами SUID. SUID-программы, такие как passwd
, позволяют выполнять действия с разрешениями владельца исполняемого файла. Вот почему команда passwd
позволяет вам изменить свой пароль, а затем записать это изменение в /etc/shadow, даже если файл принадлежит пользователю root.
И ради примера и развлечения я на самом деле пишу быстрое демо-приложение, похожее на cat
, на C (исходный код здесь) с установленным битом SUID, но если вы получите пункт - не стесняйтесь переходить к следующему разделу этого ответа и игнорировать эту часть. Примечание: ОС игнорирует бит SUID в интерпретируемых исполняемых файлах с #!
, так что версия той же самой вещи на Python потерпит неудачу.
Давайте проверим права доступа к программе и 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
Выглядит хорошо, только владелец файла и те, кто входит в группу administrator
, могут прочитать этот файл.Теперь давайте войдем в систему как пользователь картофеля и попробуем прочитать файл:
$ 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
Выглядит нормально, ни оболочка, ни cat
, имеющие права пользователя картофеля, не могут прочитать файл, чтение которого им не разрешено. Обратите также внимание, кто сообщает об ошибке — cat
против bash
. Давайте проверим нашу программу 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
Работает, как задумано! Опять же, суть этой небольшой демонстрации заключается в том, что prog file.txt
и prog < file.txt
различаются тем, кто открывает файл, и различаются разрешениями на открытие файла.
Мы уже знаем, что < testfile.txt
переписывает стандартный ввод таким образом, что данные будут поступать из указанного файла, а не с клавиатуры. Теоретически и исходя из философии Unix «делать одно и делать это хорошо», программы, читающие со стандартного ввода (он же файловый дескриптор 0), должны вести себя согласованно, и поэтому prog1 | prog2
должен быть похож на prog2 file.txt
. Но что, если prog2
хочет перемотать с помощью системного вызова lseek, например, чтобы перейти к определенному байту или перемотать в конец, чтобы узнать, сколько у нас данных ?
Некоторые программы запрещают чтение данных из канала, поскольку конвейеры нельзя перемотать с помощью системного вызова lseek(2) или данные нельзя загрузить в память с помощью mmap(2) для более быстрой обработки. . Это было раскрыто в превосходном ответе Стефана Шазеласа на этот вопрос: В чем разница между «cat file | ./binary» и «./binary < file”? Я настоятельно рекомендую это прочитать.
К счастью, cat < file.txt
и cat file.txt
ведут себя последовательно, и cat
никоим образом не против каналов, хотя мы знаем, что это читается совершенно разные файловые дескрипторы. Как это применимо в prog file.txt
и prog
file.txt
будет достаточным для выхода с ошибкой, но приложение все равно может использовать lseek()
на stdin, чтобы проверить, является ли он каналом или нет (хотя isatty(3) или обнаружение режима S_ISFIFO в fstat(2) чаще используются для обнаружения ввода канала), в в этом случае выполнение чего-то вроде ./binary <(файл шаблона grep.txt)
или ./binary < <(файл шаблона grep.txt)
может не сработать.
Тип файла может влиять на поведение prog file
и prog < file
. Что в некоторой степени подразумевает, что как пользователь программы вы выбираете системные вызовы, даже если вы не знаете об этом. Например, предположим, что у нас есть сокет домена Unix, и мы запускаем сервер nc
для его прослушивания, возможно, мы даже подготовили некоторые данные для обслуживания.
$ nc -U -l /tmp/mysocket.sock < testfile.txt
В данном случае /tmp/mysocket.sock
будет открыт с помощью разных системных вызовов:
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
Теперь попробуем прочитать данные из этого сокета в другом терминале:
$ 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
И shell, и cat выполняют системный вызов open(2)
на что требует совершенно другого системного вызова - пары сокета (2) и соединения (2). Даже это не работает:
$ nc -U < /tmp/mysocket.sock
bash: /tmp/mysocket.sock: No such device or address
Но если мы осознаем тип файла и то, как мы можем вызвать правильный системный вызов, мы можем получить желаемое поведение:
$ nc -U /tmp/mysocket.sock
hello, I am testfile.txt and this is first line
line two
line three
last line
Цитата из руководства open(2) гласит, что разрешения на файловый дескриптор наследуются. Теоретически есть способ изменить разрешения на чтение/запись файлового дескриптора, но это должно быть сделано на уровне исходного кода.
Что такое описание открытого файла?. См. также Определение POSIX
Почему поведение команды 1>file.txt 2>file.txt
отличается от поведения команды 1>файл.txt 2>&1
?