Привет!
В bash частенько можно столкнуться с ситуацией, когда вроде как уже разобрался, и тут внезапно какая-то магия. Ковырнешь ее, а там еще целый пласт вещей, о которых раньше и не подозревал…
Под катом — несколько забавных задачек на bash, которые (надеюсь) могут оказаться интересными даже для середнячков. Удивить гуру я не надеюсь.., но все же перед тем как залезть под кат, сперва пообещайте ответить на задачки хотя бы для себя вслух — без man/info/google.
-
Задачка простая.
Какую одну команду нужно выполнить, чтобы следующая команда из примера вывела Hello на ваш терминал?
$ echo "Hello" > 1
Ответ$ cd /proc/$$/fd $ echo "Hello" > 1 Hello
Как это работает под капотом?Стандартные потоки ввода/вывода (STDIN, STDOUT, STDERR) — это файловые дескрипторы. Они автоматически создаются для каждого процесса с tty — то есть каждого терминала.
Мы выполняем команду cd в подкаталог на procfs (/proc), подкаталог нашего процесса определяем через /proc/$$ (специальная переменная, в которой хранится PID текущего процесса), и наконец в подкаталог с дескрипторами "/proc/$$/fd". Cтандартные дескрипторы тут так и лежат 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) используются следующие символы: *, ? и ~.
И если есть файловые сущности, которые подходят под ваш паттерн, то последний будет развернут шеллом в список и только после команда будет выполнена с уже измененным списком аргументов. Если нет подходящих файловых сущностей — паттерн останется без изменений.простой пример$ mkdir test $ cd test $ echo file* file* $ touch file1 $ echo file* file1 $ touch file2 $ echo file* file1 file2
И мы получаем команду, которая то работает, то неработает, то работает непонятно как.
Написанное выше — общеизвестно, но вот не все знают, что *nix также поддерживает в wildcard перечисление символов [abc] — как в регулярных выражениях.
В нашем случае шелл «раскрыл» маску и передал в grep длинную строку, попытавшись выполнить команду «ls -1 | grep file1 file2 file3 file4 file5». Понятно что grep не смог найти строку, в которой есть сразу все значения и вернул пустой результат.
Если выполнить команду, содержащую wildcard в каталоге, где нет подходящих файлов, она не изменится и мы получим как в предыдущем примере с '*':
$ cd ..;echo file[1-5] file[1-5]
Кстати частенько даже со старыми знакомыми масками многие новички совершают ошибку, например при выполнении команды find, и получают что-то вроде:
$ find . -name file* find: paths must precede expression: file2
Вывод: Используйте кавычки!
Кстати, перечисление символов работает и с диапазонами и отрицаниями. Примеры:
$ echo file[1-5] file1 file2 file3 file4 file5 # выведем файлы, у которых после file идет символ не из диапазона 1-5: $ echo file[^1-5] file6 file7 file8 file9
4. Какой простой способ отрезать расширение у файла?
ОтветСтандартный и популярный способ — использовать утилиту basename, который отрезает весь путь слева, а если указать дополнительный параметр, то дополнительно отрежет справа и суффикс. Например пишем file.txt и суффикс .txt$ basename file.txt .txt file
Но можно не запускать целый отдельный процесс для такого простого действия, и обойтись внутренними преобразованиями в bash:
$ 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, что и предыдущие. И счетчик ссылок увеличился.
Итог: жесткие ссылки на папки — обязательная часть файловой системы, которая используется для построения дерева директорий. Однако, если дать возможность пользователю создавать произвольные хардлинки на директории, он может ошибиться и создать зацикленную ссылку.
При этом все команды, пробегающие по дереву каталогов (find, du, ls) уйдут в бесконечный цикл, завершаемый только прерыванием или stack overflow, поэтому пользовательской команды нет.
На этом у меня все. Пользуясь случаем, заранее передаю спасибо тем, кто отметится в опросе!
!function(e){function t(t,n){if(!(n in e)){for(var r,a=e.document,i=a.scripts,o=i.length;o--;)if(-1!==i[o].src.indexOf(t)){r=i[o];break}if(!r){r=a.createElement("script"),r.type="text/javascript",r.async=!0,r.defer=!0,r.src=t,r.charset="UTF-8";;var d=function(){var e=a.getElementsByTagName("script")[0];e.parentNode.insertBefore(r,e)};"[object Opera]"==e.opera?a.addEventListener?a.addEventListener("DOMContentLoaded",d,!1):e.attachEvent("onload",d):d()} } }t("//top-fwz1.mail.ru/js/code.js","_tmr"),t("//mediator.imgsmail.ru/2/mpf-mediator.min.js","_mediator")}(window);
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
-
+3
-
103 -
Комментировать
-
16 марта 2015 в 16:55
Bash Booster — SCM инструмент на чистом баше -
29 мая 2011 в 19:14
Учим bash-скрипты, пишем Sokoban -
5 октября 2009 в 15:15
Использование bash completion в командной строке, собственных скриптах и приложениях. Часть 1
window._yaparams = {'hubs':[], 'flow':'admin (Администрирование)'};
// hubs to GA
_yaparams['hubs'].push('sys_admin (Системное администрирование)');
_yaparams['hubs'].push('shells (Оболочки)');
_yaparams['hubs'].push('nix (*nix)');
Автор: Сергей Кулик
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.