![image](https://webcf.waybackmachine.org/web/20220827165908/https://habrastorage.org/webt/59/d6/1e/59d61eb42f06c615299111.png)
Привет Хабр!
В bash частенько можно столкнуться с ситуацией, когда вроде как уже разобрался, и тут внезапно какая-то магия. Ковырнешь ее, а там еще целый пласт вещей, о которых раньше и не подозревал…
Под катом — несколько забавных задачек на bash, которые (надеюсь) могут оказаться интересными даже для середнячков. Удивить гуру я не надеюсь.., но все же перед тем как залезть под кат, сперва пообещайте ответить на задачки хотя бы для себя вслух — без man/info/google.
Задачка простая.
Какую одну команду нужно выполнить, чтобы следующая команда из примера вывела Hello на ваш терминал?
$ echo "Hello" > 1
Ответ$ cd /proc/$$/fd $ echo "Hello" > 1 Hello
Как это работает под капотом?Для стандартных потоков (STDIN, STDOUT, STDERR) каждого процесса, автоматически создаются файловые дескрипторы (0, 1, 2).
Мы заходим в подкаталог на procfs (/proc), подкаталог нашего процесса определяем через /proc/$$ (специальная переменная, в которой хранится PID текущего процесса), и наконец в подкаталог с дескрипторами "/proc/$$/fd". Дескрипторы тут так и лежат 0(stdin), 1(stdout), 2(stderr). С ними можно работать как с обычными символьными устройствами. Тут же будут создаваться дескрипторы других файлов, которые открыты в указанном процессе.
Суперпользователь может писать и в дескрипторы процессов других пользователей, выводя текст на их терминалы.
Именно через этот механизм работает популярная утилита write — когда пользователь может написать другому пользователю сообщение без запуска какого-то мессенджера — просто в его терминал. А для того, чтобы write могла писать в дескриптор другого пользователя, на бинарнике write стоит флаг SGID (пользователи должны быть добавлены в группу tty).
Через этот же механизм система оповещает подключенных пользователей о ребутах и других системных алертах.
2. Не столько задачка, сколько вопрос-напоминание.
Что выведет следующая команда?
$ cat /home/*/.ssh/authorized_keys
Выдаст ошибку? Выведет первый попавшийся файл? Выведет все файлы?
А куда мы зайдем следующей командой:
$ cd /home/*/.ssh
Какой результат последней команды:
$ cp /home/*/.ssh/authorized_keys .
ОтветыУверен, что все ответили верно:
Команда cat выведет все файлы, обойдя все подходящие по шаблону директории.
cd зайдет в первую, подошедшую под шаблон директорию. Обходить она не будет, просто подберет первое по алфавиту.
cp скопирует первый подошедший по шаблону файл в текущую директорию, а на остальные будет ругаться с ошибкой, потому что cp не может перезаписать в тот же самый destination в пределах выполнения одного экземпляра.
На всякий случай — а что будет, если сделать:
cp /home/*/.ssh/authorized_keys /home/*/ssh/authorized_new
ОтветНикакой магии, будет просто синтаксическая ошибка ;)
3. А вот это действительно забавная задачка!
Даже хотел ее кинуть первой, но решил оставить на закуску. Итак ситуация такая:
# Создадим несколько файлов: $ touch file{1..9} $ ls -1 file1 file2 file3 file4 file5 file6 file7 file8 file9
Теперь выведем их через "ls -1" и простой регуляркой отфильтруем первые пять:
$ ls -1 | grep file[1-5]
В результате пусто? Что за? где мои файлы?
Правильная командаВсе очень просто. Правильно будет:
$ ls -1 | grep "file[1-5]" file1 file2 file3 file4 file5
Но почему?Все знают что в масках файлов (wildcards) используются следующие символы: *, ? и ~.
И если есть файловые сущности, которые подходят под ваш wildcards, то последний будет развернут шеллом в список значений через пробел, и только после этого команда будет выполнена с уже измененным списком аргументов. Если подходящих файловых сущностей нет — паттерн останется без изменений:
простой наглядный примерТо есть при использовании wildcard, мы можем получить команду, которая то работает, то неработает, то работает непонятно как. Исправляется это простым заключением wildcard в кавычки.$ mkdir test $ cd test $ echo file* file* $ touch file1 $ echo file* file1 $ touch file2 $ echo file* file1 file2
Совершенно не обязательно заключать в кавычки все подряд, поэтому часто простые слова или регулярки, которые не являются wildcards используют без кавычек, и это отлично работает.
Написанное выше — общеизвестно, но вот не все знают, что *nix также поддерживает в wildcards перечисление символов в виде [abc].
В нашем случае шелл «раскрыл» маску и передал в grep длинную строку, попытавшись выполнить команду «ls -1 | grep file1 file2 file3 file4 file5». В этом случае grep будет искать строку file1 в файлах file2, file3, file4, file5, но так как файлы пустые, то он ничего не вернет (спасибо mickvav за уточнение).
Если выполнить команду, содержащую wildcard в каталоге, где нет подходящих файлов, она не изменится и мы получим как в предыдущем примере с '*':
$ cd ..;echo file[1-5] file[1-5]
Кстати частенько даже со старыми знакомыми масками многие новички совершают ошибку, например при выполнении команды find, и получают что-то вроде:
$ find . -name file* find: paths must precede expression: file2
Вывод: Используйте кавычки!
Перечисление символов в wildcard поддерживает и диапазоны и инвертирование. Примеры:
# выведем файлы, заканчивающиеся на 1-5 $ echo file[1-5] file1 file2 file3 file4 file5 # выведем файлы, заканчивающиеся не на 1-5: $ echo file[^1-5] file6 file7 file8 file9
4. Какой простой способ отрезать расширение у файла?
ОтветСтандартный и популярный способ — использовать утилиту basename, который отрезает весь путь слева, а если указать дополнительный параметр, то дополнительно отрежет справа и суффикс. Например пишем file.txt и суффикс .txt
$ basename file.txt .txt file
Но можно не запускать целый отдельный процесс для такого простого действия, и обойтись внутренними преобразованиями в bash (bash variable expansions):
$ filename=file.txt; echo ${filename%.*} file
Или наоборот, отрезать имя файла и оставить только расширение:
filename=file.txt; echo ${filename##*.} txt
Как это работает?% — отрезает все символы с конца до первого подходящего паттерна (поиск идет справа налево)
%% — отрезает все символы с конца до последнего подходящего паттерна (справа налево)
# — отрезает с начала до первого подходящего паттерна (поиск идет слева направо)
## — отрезает с начала до последнего подходящего паттерна (слева направо)
Таким образом, "${filename%.*}" означает — начиная справа налево проходим все символы (*) и доходим до первой точки. Отрезаем найденное.
Если бы мы использовали "${filename%%.*)", то в файлах, где точка встречается больше одного раза, у нас бы оно дошло до последней точки, отрезав лишнее.
$ filename="file.hello.txt"; echo "${filename%%.*}" file
5. Совсем немного про перенаправления <, << и <<<
Первое перенаправление "<" из именованного потока или из файла. Давно известное и годами перетёртое мозолями суровых админов. Поэтому сразу перейдем к двум другим, которые встречаются реже.
<<, так называемая конструкция here document. Позволяет разместить многострочный текст прямо в скрипте и перенаправить его, словно из внешнего потока.
Пример$ cat <<EOF \ hello, \ Habr \ EOF hello, Habr
Cat читает данные из файла. Мы перенаправляем ему в STDIN файл — конструкция here document генерит его прямо на месте, поэтому не нужно создавать отдельный файл.
Это действительно удобный способ, чтобы вызвать какую-то внешнюю утилиту и скормить ей много данных. Но в последнее время я предпочитаю пользоваться <<<
И вот почемуВо-первых, <<< лучше читается, а во-вторых через <<< тоже можно передавать многострочные данные. В третьих — … в третьих больше нет, но и первых двух для меня хватило. Сравните два примера на читабельность:
#!/bin/bash . load_credentials sqlplus -s $connstring << EOF set line 1000 select name, lastlogin from users; exit; EOF
#!/bin/bash . load_credentials SLQ_REQUEST=" set line 1000 select name, lastlogin from users; exit;" sqlplus -s ${connstring} <<<"${SQL_REQUEST}"
На мой взгляд второй вариант выглядит потенциально удобнее. Мы можем задать многострочную переменную в удобном для нас месте, и использовать ее в <<<.
А при коротком запросе все выглядит вообще прекрасно:
#!/bin/bash . load_credentials sqlplus -s ${connstring} <<<"select name, lastlogin from users;exit;"
Если оперировать скриптами побольше, и запросами подлиннее, то использование <<< с перенаправлениеим из переменных (а сами переменные мы можем объявить заранее, в специально отведенном и оборудованом комментариями месте), то код получается гораздо читабельнее.
Только представьте себе, что вам нужно вызвать несколько внешних команд с перенаправлением им кучи многострочных данных, и расположить эти команды например внутри нескольких if/loop конструкций разной вложенности.
here document сильно портит форматирование и читабельность подобного кода будет ужасной.
6. Можно ли создать hardlink на папку?
Детальный ответКонечно можно! Но не всем. POSIX файловые системы активно пользуются хардлинками и мы их все время видим! Пример:
# создаем директорию test $ mkdir test # выводим информацию о количестве ссылок и номер iNode для test $ stat -c "LinkCount:%h iNode:%i" test LinkCount:2 iNode:522366
Как? Только создали и уже два линка?
# заходим в созданную директорию test $ cd test # внутри выводим статистику для текущей директории "." $ stat -c "LinkCount:%h iNode:%i" . LinkCount:2 iNode:522366
В обоих случаях мы видим тот же номер iNode. То есть test и "." внутри него — это та же самая директория. И "." это не какой-то специальный алиас баша, и даже не операционной системы. Это просто жесткая ссылка на уровне файловой системы. Проверим еще один момент:
# создаем поддиректорию test2 внутри нашего test $ mkdir test2 # заходим в поддиректорию test2 $ cd test2 # смотрим статистику о родительской директории ".." $ stat -c "LinkCount:%h iNode:%i" .. LinkCount:3 iNode:522366
".." имеет тот же iNode 522366, соответствующий директории test. И счетчик ссылок увеличился.
Итог: жесткие ссылки на папки — обязательная часть файловой системы, которая используется для построения дерева директорий. (На самом деле в большинстве современных файловых систем это уже абстракция, и непосредственно на носителе хардлинки к директориям и не хранятся)
Однако, если дать возможность пользователю создавать произвольные хардлинки на директории, он может ошибиться и создать закицленную директорию. При этом все команды, пробегающие по дереву каталогов (find, du, ls) уйдут в бесконечный цикл, завершаемый только прерыванием или stack overflow, поэтому пользовательской команды и нет.
На этом у меня все.
Пользуясь случаем, заранее передаю спасибо тем, кто отметится в опросе!
Updated: немного исправлено форматирование и спасибо mickvav за исправление неточности.
Only registered users can participate in poll. Log in, please.
Попалось ли вам что-то новое?
8.24%
Попался на третьем, с масками файлов [abc]!
14
79.41%
Больше половины полезно
135
12.35%
Ничто не ново под луной :/
21
170 users voted.
47 users abstained.