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

в 18:04, , рубрики: bash, Оболочки, системное администрирование

image

Привет!

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

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

    Какую одну команду нужно выполнить, чтобы следующая команда из примера вывела 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);

Попалось ли вам что-то новое?

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Поделиться публикацией

googletag.cmd.push(function() { googletag.display('div-gpt-hh-inpage-wide'); });

Реклама помогает поддерживать и развивать наши сервисы

Подробнее

Реклама

Комментарии 0

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

window._yaparams = {'hubs':[], 'flow':'admin (Администрирование)'};
// hubs to GA
_yaparams['hubs'].push('sys_admin (Системное администрирование)');
_yaparams['hubs'].push('shells (Оболочки)');
_yaparams['hubs'].push('nix (*nix)');

Автор: Сергей Кулик

Источник

Поделиться

* - обязательные к заполнению поля