Pull to refresh

Немного предпятничных задачек на Bash

Reading time7 min
Views23K
image

Привет Хабр!



В bash частенько можно столкнуться с ситуацией, когда вроде как уже разобрался, и тут внезапно какая-то магия. Ковырнешь ее, а там еще целый пласт вещей, о которых раньше и не подозревал…
Под катом — несколько забавных задачек на bash, которые (надеюсь) могут оказаться интересными даже для середнячков. Удивить гуру я не надеюсь.., но все же перед тем как залезть под кат, сперва пообещайте ответить на задачки хотя бы для себя вслух — без man/info/google.

  1. Задачка простая.


    Какую одну команду нужно выполнить, чтобы следующая команда из примера вывела 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, то последний будет развернут шеллом в список значений через пробел, и только после этого команда будет выполнена с уже измененным списком аргументов. Если подходящих файловых сущностей нет — паттерн останется без изменений:
    простой наглядный пример
    $ mkdir test
    $ cd test
    $ echo file*
    file*
    $ touch file1
    $ echo file*
    file1
    $ touch file2
    $ echo file*
    file1 file2
    То есть при использовании wildcard, мы можем получить команду, которая то работает, то неработает, то работает непонятно как. Исправляется это простым заключением wildcard в кавычки.

    Совершенно не обязательно заключать в кавычки все подряд, поэтому часто простые слова или регулярки, которые не являются 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.14% Попался на третьем, с масками файлов [abc]!14
79.65% Больше половины полезно137
12.21% Ничто не ново под луной :/21
172 users voted. 47 users abstained.
Tags:
Hubs:
+13
Comments25

Articles