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

    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 за исправление неточности.
    Попалось ли вам что-то новое?

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

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 25
    • +1
      Просто прорекламирую здесь свой веб сервер написанный ради развлечения на bash.
      • 0
        Но если честно, то там 1/3 bash и 2/3 perl?
        • 0
          Нет. Там два в одном: один на баше — рабочий и на перле — черновой вариант. Между собой независимы.
      • 0

        Ну вообще то stdin,stdout и stderror это файлы, которые создаются при создании любого процесса в unix системе, а дескриптор это номер структуры в таблице, в полях хранится вся информация о файле.

        • 0
          В какой такой таблице и структуре?
          Информация о файле хранится в iNode, и при открытии файла, вы работаете с ним именно через iNode.
          Но номер iNode и номер дескриптора — совершенно разные вещи.
          • 0
            Inode это структура, которая в своих полях содержит информацию о размере файла, его адресе на диске, права и т.д. Так же есть «файл» корневого каталога /, в котором находится двумерный массив: список из пар «номер inode-struct inode».Если мы создаем файл file в папке в директории /, создается файл каталога, внутри которого находится тоже список строка-номер inode, примерно вот так "/myCatalog/file" — 0001, где 0001 это номер индексного дескриптора,inode, в котором собственно находится адрес file на диске.А файловый дескриптор-это целое число, которое возвращает вызов read при открытии любого файла.По стандарту posix файловые дескрипторы с номерами 0,1,2 принадлежат файлам стандартного ввода/вывода и потока ошибок, которые открываются по умолчанию для любого дочернего процесса.
            • 0
              вообще-то stdin/stdout/stderr это стандартные потоки для каждого процесса, и они уже открыты под зарезервированными для них дескрипторами 0, 1, 2 — о чем и говорит их наличие в /proc/xxx/fd. Если вы открываете еще один файл — появляется еще один поток с очередным дескриптором в /fd

              Как таковых файлов stdin/stdout/stderr на файловой системе нет — это зарезервированные слова для обращения к этим дескрипторам.

              А так как вся информация о файловой сущности хранится именно в iNode, а не в «номер структуры в таблице, в полях хранится вся информация о файле.», я и переспросил какую структуру вы имеете ввиду. Поскольку в каталоге есть только имя и ссылка на iNode на устройстве.
              • 0
                Понял, вы спутали файловый дескриптор (номер открытого потока) и индексный дескриптор (iNode).

                Сами iNode не хранятся в структуре каталога, там хранится только тип, имя и номер соответствующей iNode для файлов/каталогов/ссылок, и другая информация для остальных файловых сущностей (блочных устройств, пайпов..)

                А сами iNode хранятся в отдельных блоках. В старых файловых системах блоки с iNode сразу создавались при форматировании, сейчас (в ext4, например), создается часть iNode, но если их не хватит, дополнительные iNode могут быть созданы позже.

                Следовательно первичны именно 0, 1, 2. А STDIN, STDOUT, STDERR это уже алиасы, которые появились в POSIX позже
                • 0
                  Я вроде все прозрачно написал, и ничего не путал.Как я написал выше,inode или индексный дескриптор-это и чисто физический обьект(структура данных), и абстракция уровня файловой системы, которая для ФС представляет собой номер этой самой структуры в файле корневого каталога.В файлах других каталогов номер индексного дескриптора связан с путем к файлу, которому принадлежит этот inode(связан через двумерный массив, где строка это адрес вида "/home/vasia/etot_file" а одно из значений этой строки-номер inode).
                  • 0
                    Простите, но это все — про индексный дескриптор.

                    А файловый дескриптор — это поток, открытый в конкретном процессе. Он имеет порядковый номер, начиная с нуля. Так как в linux мы имеем виртуальную файловую систему procfs, то файловый дескриптор мы можем увидеть в виде файла с именем 0, 1, 2 в /procfs — в данном случае имя файла и номер файлового дескриптора совпадают (и практически являются одним и тем же) — procfs это прямой интерфейс к процессам.

                    А вот файлов stdin,stdout и stderror — не существует ни на какой файловой системе.
                    • 0
                      Поток это поток(хотя в linux модель потока слишком смазана относительно процесса), а файловый дескриптор это файловый дескриптор.И я имею ввиду поток выполнения.Да, я дал в комментарии выше определение индексному дескриптору.А файловый дескриптор это всего лишь абстрактная сущность.Fd это всего лишь целое число, которое является указателем на открытый файл. При открытии файла read возвращает файловый дескриптор, и все дальнейшие манипуляции с этим файлом происходят через этот указатель.Стандартный ввод, вывод и поток ошибок вполне себе физические обьекты, которые существуют в адресном пространстве процесса, которому они принадлежат.При желании можно их скопировать в любой файл.
                      • 0
                        Снова не соглашусь )
                        Поток и поток — разные вещи. Не путайте stream и thread — там ничего не смазано.

                        Fd это всего лишь целое число

                        FD = File Descriptor. Быть указателем на открытый файл и есть суть дескриптора. При открытии файла, у процесса появляется новый file descriptor, который ссылается на конкретный iNode. Да, у дескриптора есть номер, чтобы программист мог обратиться к конкретному дескриптору.
                        Но чтобы записать в файл данные, обычная программа их пишет не в iNode, а в дескриптор. Как именно данные положить в файл/поток или что там по этому дескриптору на самом деле открыто — думает не разработчик приложения а операционная система (драйвер этого устройства, драйвер конкретной файловой системы), разработчик, пользуясь готовыми функциями должен лишь соблюдать правильный формат данных.
          • +1
            За пример с sqlplus и с <<< отдельный респект!!! Возьму на вооружение.
            • 0
              могу еще посоветовать всегда выносить креденшелы из скриптов в отдельный файл. Его можно прописать в .gitignore и хранить скрипты в общем репо
              • +1
                Логины-пароли я, при необходимости, храню в base64-encoded-файле. От целенаправленного взлома не спасёт, но для сохранения от любопытных глаз достаточно. Однако за совет — спасибо! При случае воспользуюсь.
            • +1
              1. Я сделал так:

              ln -s /dev/tty 1

              3. grep будет искать слово «file1» в файлах «file2» «file3» и т.д.
              • 0
                1. Можно также примаунтить конкретный /dev/pts/xx, или тот же /proc/$$/fd/1, вариантов много, но оставляет за собой мусор =), а я хотел как раз сделать акцент на /fd в этом вопросе

                3. Дополнил статью, спасибо!
                • 0
                  Ну, кстати ещё, что надо понимать — [1-5] не генерирует диапазон, а именно что ищет файлы, попадающие под маску и передает маску как есть, если ни одного не нашло. e.g.:
                  mick@mick-office:~$ mkdir fffg
                  mick@mick-office:~$ cd fffg
                  mick@mick-office:~/fffg$ echo [1-3]
                  [1-3]
                  mick@mick-office:~/fffg$ touch 2
                  mick@mick-office:~/fffg$ echo [1-3]
                  2
                  
                  • 0
                    Ну именно про этом у меня как минимум два примера в статье и так есть, что маска раскрывается только в случае наличия подходящих файлов/файловых сущностей.
              • 0
                Задача 3 — кавычки. Синтаксис грепания как и в любом языке, поиск подстроки в строке. То есть надо сказать грепу именно строку "", а не последовательность названий файлов. Хз его поведение, я кавычки не пропускаю и не пользуюсь им соло.
                • 0
                  Ну смотрите

                  $ cd /dev/shm
                  $ echo -e "hello\nworld" > file.txt
                  $ grep hello ./*
                  hello
                  $ grep hel* ./*
                  hello


                  То есть кавычки совершенно не обязательны, и это нормалная практика.
                  Но о том, что маски файлов поддерживают перечень символов — я не знал, и в какой-то момент впал в ступор (примерный случай в статье привел), не понимая что происходит.
                  • 0
                    Да, я глянул под спойлер потом. Занятно… Но пофигу, кавычки забывать нельзя =) Оно может и не возбраняется… Но как и у хейтеров перла это главный аргумент против. Одно дело разок забыть и это не повлияет на результат, другое — систематически забивать. Я грепаю только с кавычками, благо не так часто оно мне надо.
                    Тут, видимо, важно учитывать поведение именно самого баша, потому что регулярку он развернул согласно команде (файл1 файл2… — вот уже и логическая ошибка при валидном синтаксисе). Если ищем строку, то надо оформлять как строку.
                • 0
                  Update: поправлено форматирование и исправлена замеченая неточность.
                  • +1
                    Вау, шикарная статья! Спасибо! Жаль, она прошла мимо моего внимания в период голосования…
                    Особенно за первую задачку спасибо! У нас в офисе нет таких гуру линукса, поэтому, когда я начал строчить коллегам сообщения в терминалы на общем сервере, то получилось эффектно)
                    • 0
                      Спасибо =)

                      Вообще write активно использовался еще в 90-е, когда из «мессенджеров» были только irc и icq, так что многое новое — хорошо забытое старое

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