Linux Tools: shells, ash #4 - ash syntax, complex commands, pipelines, conditions and loops
linux linux-tools shell ashВ предыдущей статье изучили как работает перенаправление потоков, сегодня начнем разбираться как выполнять комбинации команд, но сперва обсудим какие могут быть команды.
Предыдущая статья: shells, ash #3 - ash syntax, redirections
Следующая статья: shells, ash #5 - ash syntax, functions
Типы команд #
В ash существуют 3 типа команд: shell-функции, buildin-команды и программы. При выполнении осуществляется поиск команды именно в таком порядке.
Выполнение shell-функций #
Все аргументы переданные с вызовом функции, передаются в функцию. Все environment-переменные также доступны внутри функции. Сама функция выполняется в текущем шелле. Сейчас важно то что функции также как можно использовать также как и команды-программы и builtin-функции, то есть создавать комбинации из функций и команд.
Функции рассмотрим в следующих статьях. Сейчас небольшой пример использования
$ hello () echo "Hello, $1"
# stdout можно перенаправить
$ hello Kenneth > ./tmp.txt
# или использовать в пайплайнах
# - пайплайны разберем в этой статье
$ hello Kenneth | cat
hello, Kenneth
Выполнение builtin-команд #
builtin-команды выполняются внутри текущего shell, без запуска отдельного процесса.
builtin-команды в ash также разберем отдельной статьей, их достаточно много, но в общем некоторые общие команды такие как echo
, ls
и другие могут быть встроены в shell.
Выполнение команд-программ #
Если имя команды не является shell-функцией или builtin-командой, значит это программа в файловой системе и ее нужно найти и выполнить.
Если имя команды содержит слэш /
, то команда сразу выполняется без поиска в файловой системе – имя команды используется как путь к программе
$ /usr/bin/htop
# команда запуститься сразу
Если слэшей в имени команды нет, то происходит поиск файла программы по имени в директориях из environment-переменной PATH.
В переменной PATH содержится список директорий разделенных двоеточием :
, конечно же переменную PATH можно изменить, установить свой список директорий или дополнить существующий.
$ echo $PATH
/sbin:/usr/sbin:/bin:/usr/bin
$ PATH=$PATH:/home/ubuntu/bin
$ echo $PATH
/sbin:/usr/sbin:/bin:/usr/bin:/home/ubuntu/bin
Текущая директория (PWD) тоже может содержаться в PATH, для этого нужно указать пустую строку между двоеточиями или в конце
# в конце строки
$ PATH=/sbin:/bin:
# пустая директория
$ PATH=/sbin:/bin::
# в начале строки
$ PATH=:/sbin:/bin
Для выполнения будет использована первая найденная команда, то есть порядок директорий важен.
Если команда не существует ни в одной директории, получим ошибку not found
$ noop
/usr/bin/ash: noop: not found
Выполнение команд-программ #
Когда команда найдена начинается ее выполнение и тут снова возможно несколько сценариев:
magic_number #
исполняемый файл-программа начинается с magic number. Тут имеется ввиду ELF-заголовок по которому определяется что программа может выполняться и дальше при выполнении происходит чтение из этого файла используя ELF-формат.
В этом случае создается новый процесс, которому переданы все аргументы команды и переменные окружения.
shebang #
Если программа не имеет ELF-заголовка, но начинается с #!
программа выполняется как отдельный shell. Причем после #!
указывается интрепретатор и один аргумент
Ситаксис
#!interprer [optional-arg]
Пример
#!/usr/bin/ash
echo 123
Скрипт выше будет выполнен интерпертатором ash. Кроме shell таком образом могут выполняться программы на интерпретируемых языках программирования, например, python или php
#!/usr/bin/python
print('123')
Для того чтобы выполнить скрипт как программу нужно чтобы для файла был установлен атрибут eXecutable
$ chmod +x ./test.sh
$ ./test.sh
123
Exit status #
В Linux каждый процесс завершается с определенным числовым статусом, exit-статусом или exit-кодом.
Если exit-код равен 0 — программа выполнена успешно, другие значения означают ошибку. У каждой команды свой список exit-кодов и их значение можно посмотреть в документации (man) команды.
Builtin-команды и shell-функции также возвращают exit-коды.
Забегая немного вперед - exit-код последней выполненной команды можно посмотреть с помощью специальной shell-переменной $?
$ env
# ...
$ echo $?
0
В случае списков команд итоговым будет exit-код последней команды.
Complex Commands #
Все предыдущие разделы были необходимой подготовкой к пониманию более сложных конструкций из команд.
Итак команды могут быть одним из следующих:
- простой командой (simple command)
- пайплайном (pipeline)
- списком команд (list)
- составной командой (compound command)
- определением функции (function definition)
Pipelines #
Pipeline - последовательность команд разделенных управляющим оператором |
.
Stdout каждой команды перенаправляется в stdin следующей команды, а для последней команды stdout работает как обычно. Как итоговый exit-код используется exit-код последней команды.
$ echo abc | head -c 1
a
В примере выше stdout команды echo abc
передается в stdin команды head
, которая берет из своего stdin только один символ.
Для примера с exit-кодом не будем передавать значение аргумента -c
, в этом случае команда head
завершается с ошибкой
$ echo abc | head -c
head: option requires an argument -- 'c'
# ... сообщение об ошибке
$ echo $?
1 # <<< команда завершилась с ошибкой
Exit-код пайплайна можно инвертировать, если добавить перед пайплайном восклицательный знак !
$ ! echo abc | head -c
head: option requires an argument -- 'c'
# ... сообщение об ошибке
$ echo $?
0
Для каждой команды можно использовать операторы перенаправления. Для примера возьмем вывод команды head -c
$ head -c | cat
head: option requires an argument -- 'c'
# ...
head -c
передает stdout команде cat
, а уже stdout команды cat
выводится в терминал
Если перенаправить результат cat в /dev/null …
$ head -c | cat > /dev/null
head: option requires an argument -- 'c'
# ...
… то ничего не поменяется, потому что текст ошибки выводится в stderr-поток, тут мы можем использовать перенаправление — перенаправляем stderr в stdout — в итоге и stderr и stdout команды head
попадают в stdin команды cat
, а потом в /dev/null
$ head -c 2>&1 | cat > /dev/null
Списки - Lists #
Для последовательного выполнения команд без перенаправления потоков существуют списки команд.
Simple List - ;
#
Самы простой вариант составить список без связи команд это использовать между командами ;
$ echo 1; echo 2; echo 3;
1
2
3
Команды будут выполнены последовательно одна за другой в указанном порядке, при этом даже если первая команда завершится с ненулевым exit-кодом, остальные все равно будут выполнены.
AND-List - &&
#
Для того чтобы команды выполнялись только в случае успешного завершения предыдущей команды используют оператор &&
.
В этом случае следующая команда будет выполнена только если предыдущая завершилась с exit-кодом равным 0.
# есть текстовый файл abc.txt, в котором содержатся 3 символа 'abc'
$ cat abc.txt
abc
# составляем список команд команд:
# - head читает один символ из файла
# - echo выводит '- first char in file'
# если предыдущая команда завершилась успешно
$ head -c 1 <abc.txt && echo ' - first char in file'
a - first char in file
# head выполнилась успешно и затем выполнилась команда echo
# если убрать число символов в команде head - возникнет ошибка -
# head завершится с exit-кодом 1 и echo не будет выполнено
$ head -c <abc.txt && echo ' - first char in file'
head: option requires an argument -- 'c'
# ...
OR-List - ||
#
В OR-List команда следующая за оператором ||
выполняется только если exit-код предыдущей команды больше 0.
Возьмем последнюю команду из предыдущего примера и немного изменим
# убираем значение аргумента -с
# меняем && на ||
# меняем текст сообщения
$ head -c <abc.txt || echo 'it is failed'
head: option requires an argument -- 'c'
# ...
it is failed
# echo выполнилось потому что exit-код первой команды больше 0
# если исправить первую команду - echo перестанет выполняться
$ head -c 1 <abc.txt || echo 'it is failed'
a
Группировка списков команд #
Команды могут быть сгруппированы двумя способами:
Первый - используя круглые скобки ()
#
В этом случае команды будут выполнены в отдельном shell и команды не будут влиять на текущий shell
$ (echo 'abc' | head -c 1)
a
$ (export TEST=100)
# переменная TEST не появится в текущем shell
Второй способ - фигурные скобки - {}
#
Этот способ также как и первый позволяет сгруппировать stdout списка команд, как будто это одна команда, но команды выполняются в текущем shell.
Первое обязательное условие этого способа - это наличие пробелов после первой и перед второй фигурными скобок.
Вторым обязательным условием является наличие точки с запятой ;
перед второй, завершающей скобкой }
.
$ { echo 1; echo 2; echo 3; } | head -n 2
1
2
# также можно использовать другие списки, пайплайны
$ { echo 'abc' | head -c 1 ; }
a
Условные конструкции и циклы #
С этого момента простое выполнение команд начинает превращаться в shell-программирование 😃
if … then #
Синтаксис
if list
then list
[ elif list
then list ] ...
[ else list ]
fi
Если list в if
завершается с exit-кодом 0 - выполняется первый then list
, а если exit-код больше 0 и есть else list
- то будет выполнен он, аналогично для elif .. then
.
Все что после if, then, else, elif является просто списками команд.
Единственным условием является разделение списка команд от служебных слов с помощью переноса строки, либо точкой с запятой ;
$ if echo 1
> then echo 2
> fi
1
2
# то же самое в одну строку
$ if echo 1; then echo 2; fi
1
2
# чуть более сложно выглядящий пример
$ if ({ echo 1; echo 2; } | head -n 1) then echo 3; fi
1
3
# первый список команд завершается с exit-кодом больше 0
$ if ls /nowhere >/dev/null 2>&1; then echo 'good'; else echo 'bad'; fi
bad
# выполнился else
В примерах мы видим stdout всех выполняющихся команд. Если значения stdout или stderr не нужны - их нужно перенаправлять в файлы или в /dev/null.
После описания синтаксиса и примеров может возникнуть вопрос про условия в квадратных скобках
$ TEST=100
$ if [ $TEST == '100' ] ; then
echo 'good'
else
echo 'bad'
fi
Разве квадратные скобки не являются частью синтаксиса?
Нет, не являются. И это одна из “магических” штук, которые с ходу кажутся простыми, но в итоге могут работать не так как ожидаешь.
Оказывается открывающяя скобка [
это команда test
.
Документация по man [
и man test
будет содержать одинаковую информацию.
Если поискать такую команду с помощью which
(which определяет где хранится команда на файловой системе используя PATH)
$ which [
/usr/bin/[
Закрывающая скобка ]
является обязательным завершающим аргументом для команды [
$ [
ash: missing ]
Конкретно в случае ash команды [
, test
и некоторые другие являются builtin-командами, мы разберем их через пару статей этой серии.
Теперь if
конструкция с квадратными скобками …
if [ $TEST == '100' ] ; then
echo 'good'
fi
… просто преобразуется в список из одной команды
if test $TEST == '100' ; then
echo 'good'
fi
и точка с запятой в конце нужна чтобы разделить аргументы команды test
от служебного слова then
while #
C циклом while
все еще проще чем if
Синтаксис
while list
do list
done
Пока список команд после while
завершается с exit-code равным 0 выполняется список команд после do
.
$ echo 1 > 1.txt
$ while cat 1.txt >/dev/null 2>&1
do echo 'has file' ; rm 1.txt
done
has file
# has file - выводится один раз и после этого следует выход из цикла
until #
until
работает аналогично while
, только цикл выполняется пока exit-код первого списка команд не равен 0.
# пока нет файла 2.txt цикл будет работать
$ until cat 2.txt >/dev/null 2>&1
do echo 'has no file' ; echo 2 > 2.txt ;
done
has no file
for #
Ситаксис
for variable [ in [ word ... ] ]
do list
done
Цикл for
позволяет перебирать список слов и на каждом шаге что-то выполнять с элементом
$ for i in 1 2 3; do echo "i = $i"; done
i = 1
i = 2
i = 3
С возможность подстановки результата команды (command substitution) - $(command)
- цикл for
позволяет перебирать все что угодно. Command substitution и другие полезные возможности рассмотрим в следующей статье.
# команда ls выдает список файлов в текущей директории
# выдает в строку разделенные пробелами
$ ls
1.txt 2.txt 3.txt
# используя for и command substitution
# можем перебрать в цикле список файлов
$ for file in $(ls); do echo "file = $file"; done
file = 1.txt
file = 2.txt
file = 3.txt
break, continue #
синтаксис
break [n]
continue [n]
break
- прерывает циклfor
илиwhile
(until
)continue
- переход на следующую итерацию цикла
Если есть вложенные циклы, то можно указывая n применять break
или continue
для нескольких циклов
$ for i in 1 2 3
> do for j in a b c; do echo "$i - $j"; break 1 ; done
> done
1 - a
2 - a
3 - a
# каждый раз второй цикл будет прерываться на первой итерации
# если поменять на break 2 то будет выведено только 1 - a
case #
Синтаксис
case word in
[(]pattern) list ;;
...
esac
Проверяет соответствие word одному или нескольким паттернам, выполняется список команд у паттерна, который совпадает первым.
завершающий esac
это case
наоборот, также как if
и fi
, кто-то придумал - так и осталось (или там какая-то другая история) 😃
В самом простом виде в case
можно выполнять команды или устанавливать переменные или и то и другое вместе
i=2
case $i in
(1)
echo '1'
;;
(2) echo '2' ;;
esac
Первую круглую скобку можно опустить. Кроме указания полных значений можно использовать Shell Patterns.
Shell patterns позволяют указывать паттерны нестрогого соответствия для строк.
В Shell Patterns могут использоваться мета-символы ! * ? [
*
- соответвует любой последовательности символов, можно использовать как case
по умолчанию.
i = 100
case $i in
1) echo 1 ;;
2) echo 2 ;;
*) echo 'default' ;;
esac
?
- соответствует одному любому символу
i = 100
case $i in
1) echo 1 ;;
2) echo 2 ;;
1??) echo '100 or 1??' ;;
esac
[
- позволяет указать класс символов, внутри можно указать конкретные символы или диапазон символов, также можно комбинировать с другими мета-символами
i = a100
case $i in
1) echo 1 ;;
2) echo 2 ;;
[ab][1-9]*) echo 'ab1-9' ;;
esac
Cимволы ]
и -
тоже можно использовать как символы внутри последовательности. Для этого символ ]
нужно разместить сразу после открывающей фигурной скобки [
(но после !
, если используется). Символ -
нужно разместить первым или последним внутри класса символов.
!
- используется внутри класса символов и означает несоответствие классу.
i = a100
case $i in
1) echo 1 ;;
2) echo 2 ;;
[!ab][1-9]*) echo 'c1-9' ;;
esac
# последний паттерн сработает и выведет c1-9 если i будет равно c100
Итого #
В статье разобрались как выполняются команды, рассмотрели пайплайны, списки и группировку команд, возможности связать команды через AND-, OR- списки, условия и циклы. Вроде бы самый простой shell, но внутри еще достаточно много функционала для изучения.
Дальше разберем функции и возможности работы с переменными.
Следующая статья: shells, ash #5 - ash syntax, functions
Ссылки #
- https://man7.org/linux/man-pages/man1/dash.1.html
- https://ru.wikipedia.org/wiki/Executable_and_Linkable_Format
- https://ru.wikipedia.org/wiki/Шебанг_(Unix)
- https://en.wikipedia.org/wiki/Exit_status