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

Все предыдущие разделы были необходимой подготовкой к пониманию более сложных конструкций из команд.

Итак команды могут быть одним из следующих:

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]

Если есть вложенные циклы, то можно указывая 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

 

Ссылки


Все статьи серии “Linux Tools”