sh
linux shells ash shДефолтный shell интерпретатор в ubuntu dash, он же ash, в других дистрибутивах может по-другому, но синтаксис и опции по идее должны быть плюс, минус одинаковыми.
Про ash есть серия статей по тэгу ash.
В этом разделе собран своего рода cheatsheet по shell.
Источником для серии статей и этого раздела является документацию по dash.
Режимы запуска #
Interactive shell #
Обычно так и запускается, принудительно можно включить опцией -i
.
Ожидает команд из stdin, в stdout пишет результаты, в stderr отладку и ошибки - подробнее будет в linux-streams
Non-interactive shell #
Включается при указании пути к скрипту или с аргументом -c
$ sh ./test.sh
$ sh -c 'echo 100500'
Login shell #
Еще одним режимом запуска является login, включается опцией -l
или передачей первым аргументом символа -
.
$ ash -l
$ ash -
Работает в interactive и non-interactive режимах.
Смысл режима инициализировать окружение для пользователя. В login-режиме ash ищет и выполняет shell-скрипты /etc/profile
и $HOME/.profile
. Если в этих файлах указать и экспортировать переменную ENV, в которой указать путь к файлу, его содержимое также будет обработано и выполнено как shell-скрипт на этапе запуска.
ENV=$HOME/.shinit; export ENV
Arguments #
noexec, -n
#
Проверка скрипта без выполнения команд. Работает только при выполнении файла-скрипта, но не работает при указании команд через -c
.
$ cat test.sh
echo 123 && exit 0
$ ash test.sh
123
$ ash -n test.sh
У последней команды не будет никакого вывода, потому что указан аргумент -n
и команды echo и exit не будет выполнены, но в случае именно ошибки синтаксиса выведется текст ошибки и ненулевой exit code.
$ cat test.sh
& echo 123 && exit 0
# '&' в начале это ошибка в shell-скрипте
$ ash -n test.sh
./test.sh: line 1: syntax error: unexpected "&"
Конечно этот режим не проверяет наличие самих команд, но для проверки синтаксиса удобно.
verbose, -v
#
В stderr будет записано все что ash читает в процессе работы.
Например, в login-режиме (про него далее) читается файл $HOME/.profile
и выполняются команды из него – с опцией -v
все содержимое .profile
будет записано в stderr для отладки.
xtrace, -x
#
С этим аргументом каждая выполняемая команда будет записано в stderr
$ cat test.sh
echo 123 && exit 0
$ ash -x test.sh
+ echo 123
123
+ exit 0
Вообще все опции включатся через -
, а выключаются через +
// кажется что нелогично и должно быть наоборот
В интерактивном режиме тоже можно включать опции с помощью builtin-команды set
$ set -x
# xtrace ВКЛючен
$ set +x
# xtrace ВЫКЛючен
errexit, -e
#
С -e
все непротестированные команды (без проверки exit-кода через if
, elif
) будут приводить к завершению скрипта.
Для примера рассмотрим скрипт с листингом несуществующей директории.
$ cat test.sh
ls /none; echo 'good'
$ ash test.sh
ls: /none: No such file or directory
good
$ echo $?
0
При обычном запуске команды ls
и echo
выполняются независимо друг от друга и echo
будет выполнено после ls
// конечно в данном случае нужно использовать &&
, но об этом в следующих статьях
С аргументом -e
выход из скрипта произойдет после ls
и скрипт завершится с ненулевым exit-кодом.
$ ash -e test.sh
ls: /none: No such file or directory
$ echo $?
1
Еще один полезный момент — передача параметров при запуске скрипта. Все аргументы после указания пути к скрипту считаются позиционными аргументами и могут быть почитаны в скрипте из переменных $1, $2
$ cat test.sh
echo $1
echo $11
$ ash test.sh 1 2 3 4 5 6 7 8 9 10 11
1
11
Отдельно стоит отметить $0 - это переменная содержит путь к скрипту
$ cat test.sh
echo $0
$ ash ./test.sh
./test.sh
allexport, -a
#
Экспорт env-переменных // не увидел разницы с обычным режимом, все назначенные переменные окружения и так видны внутри shell при запуске
$ env -i TEST=100 ash -c 'echo $TEST'
100
$ env -i TEST=100 ash -a -c 'echo $TEST'
100
command, -c
#
Режим выполнения команд, выполняет команды указанные после аргумента
$ ash -c 'echo 100500'
100500
noclobber, -C
#
тут речь про оператор >
при перенаправление потока в файл, по умолчанию этот оператор создает или перезаписывает содержимое файла, аргумент -C
меняет это поведение — если файл уже существует, произойдет ошибка.
# скрипт записывает значение первого аргумента в файл tmp.txt
$ cat ./test.sh
echo $1 > tmp.txt
# выполняем
$ ash ./test.sh 100
# в файл записано 100
$ cat tmp.txt
100
# выполняем еще раз
$ ash ./test.txt 101
# файл перезаписан со значением 101
$ cat tmp.txt
101
# выполняем c аргументом -C - получаем ошибку
$ ash -C ./test.txt 102
test.sh: line 1: can't create test.txt: File exists
noglob, -f
#
В шеллах существует прекрасная штука - file globbing - это возможность указывать путь к файлам не полностью, а, например, через wildcard — cat *.txt
— вывести на экран все файлы с расширением txt.
Так вот опция -f
отключает file globbing для выполняемого скрипта.
$ cat test.sh
# this is script
cat *.sh
# выводит все файлы с расширением sh
$ ash test.sh
# this is script
cat *.sh
file globbing отключен - *.sh интерпретируется как имя файла
$ ash -f test.sh
cat: can't open '*.sh': No such file or directory
nounset, -u
#
Завершать скрипт с ошибкой если происходит обращение к переменной, которая не определена.
$ cat test.sh
echo $SOME
# скрипт успешно выполнен, хотя переменная SOME не определена
$ ash test.sh
# с аргументом -u присходит ошибка
$ ash -u test.sh
test.sh: line 1: SOME: parameter not set
ignoreeof, -I
#
Игнорирует EOF (символы конца строки) из stdin
// Пока не очень понятно зачем это нужно, но работает так
# echo передает EOF (/n) после строки и скрипт выводит содержимое файла скриптов
$ echo 'cat *.sh' | ash
echo $SOME
# с -I конец строки игнорируется и будет ошибка
$ echo 'cat *.sh' | ash -I
Use "exit" to leave shell.
Use "exit" to leave shell.
...
interactive, -i
#
принудительно включается интерактивный режим
login, -l
#
Включает login-режим
monitor, -m
#
включает job control, автоматически включается в интерактивном режиме, видимо позволяет включать работу с jobs в скриптах // рассмотрим позже, но уже интересно — можно запустить задачу фоном в скрипте, а потом проконтролировать ее и завершить скрипт
stdin,-s
#
читать скрипт из stdin. Если не указан путь к скрипту, то включено.
$ echo 'date' | ash
Fri Dec 24 00:38:31 UTC 2020
# идентично c -s
$ echo 'date' | ash -s
Fri Dec 24 00:38:31 UTC 2020
# но не работает если указан скрипт
$ echo 'date' | ash -s test.sh
# test script commands
# при этом выполняются обе команды с -c и -s
$ echo 'date' | ash -s -c 'date'
Fri Dec 24 00:38:31 UTC 2020
Fri Dec 24 00:38:31 UTC 2020
Operators #
ash читает скрипт построчно, разбивает его на слова по пробелам и табам.
Некоторые последовательности символов определяются как операторы.
Операторы бывают двух видов:
- Control operators — для объединения команд в группы, цепочки-пайпланы и условия
& && ( ) ; ;; | || <newline>
- Redirection operators — для перенаправления потоков между командами и не только
< > >| << >> <& >& <<- <>
Escaping #
Стандартной проблемой в шеллах и в языках программирования является использование специальных символов (и последовательностей) как обычных символов – это называют экранированием (escape special chars). В ash экранирование спец символов можно сделать 3 способами:
- обратный слэш — backslash
- парные одинарные кавычки — matched single quotes
- парные двойные кавычки — matched double qoutes
обратный слэш — backslash #
Backslash перед спец символом делает его обычным символом
# команда ничего не выведет,
# потому что точка с запятой интеретируется как разделитель команд
$ echo ;
# выведется точка с запятой, как обычный символ
$ echo \;
;
Кроме этого backslash перед символом конца строки (переводом строки) интерпретируется как продолжение строки
$ echo 1\
2\
3
123
парные одинарные кавычки — matched single quotes #
Все что внутри одинарных кавычек, кроме одинарных кавычек, считается обычным символом
$ echo '&;|>>'
&;|>>
Одинарные кавычки при необходимости можно вывести отдельно с помощью backslash
# последняя кавычка экранирована с помощью backslash
# и считается обычным символом
$ echo '&|$'\'
&|$'
$ echo '$PATH'
$PATH
парные двойные кавычки — matched double qoutes #
С двойными кавычками все немного сложнеe.
Все символы внутри двойных кавычек считаются обычными, за исключением символов:
- знака доллара (dollarsign) -
$
, - обратной кавычки (backquote) -
`
- и обратного слэша (backslash) -
\
backslash внутри двойных кавычек позволяет экранировать только символы: $ ` " \ \
<newline>
Кратко рассмотрим зачем вообще нужны эти символы – дальше они будут разобраны подробнее.
$
- dollarsign #
с помощью $
можно использовать shell-переменные вообще без кавычек
$ echo $PATH
/sbin:/usr/sbin:/bin:/usr/bin
# аналогично $ работает в двойных кавычках
$ echo "$PATH"
/sbin:/usr/sbin:/bin:/usr/bin
`
- backquote #
команда, заключенная в обратные кавычки, выполняется и output команды подставляется вместо обратных кавычек. Работает и без двойных кавычек.
$ echo `pwd`
/home/ubuntu
$ echo "current directory is `pwd`"
current directory is /home/ubuntu
\
- backslash #
работает как и раньше, экранирует символы: $ ` " \
и backslash перед переводом строки считается продолжением строки без переноса
$ echo "\$PWD = \
\"$PWD\""
$PWD = "/home/ubuntu"
Комбинирование разных кавычек для команд и аргументов #
Все способы формирования можно комбинировать: использовать поочерердно разные кавычки, переменные:
$ echo my' path is '" = "$PWD""
my path is = /home/ubuntu
Главное чтобы в рамках одного аргумента строки соединялись без пробелов, иначе после пробела shell будет считать строку уже другим аргументом.
Commands #
Вот мы и подобрались к сути, ведь shell нужен как раз для выполнения команд.
Если при обработке строки скрипта первое слово не является зарезервирванным, то ash начинает обработку этой строки как команды.
Cуществуют 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
Functions #
name () {
command
...
return [exitstatus]
}
Функции могут содержать всего одну команду и записываться без фигурных скобок
$ hello() echo 'Hello'
$ hello
Hello
Или несколько команд
$ hello() {
echo 'Hello'
echo 'World'
}
$ hello
Hello
World
Каждая функция работает как обычная команда-программа и возвращает exit-код. По умолчанию возвращается 0, но можно вернуть нужный exit-код c помощью return. При этом выполнение функции прекращается на return.
$ hello() {
echo 'Hello'
return 100
echo 'World'
}
# World не выводится
$ hello
Hello
# $? содержит exit-код предыдущей команды
$ echo $?
100
Параметры и переменные #
Параметры (Parameters) могут позиционными и специальными, а параметры с именем называются переменными (Variables).
Позиционные параметры - Positional Parameters #
При запуске скрипта все параметры перечисленные после команды являются позиционными параметрами и ash позволяет обращаться к ним по номерам, начиная с 1.
В параметре $0
хранится путь к текущему скрипту или путь к shell, если запущен интерактивный режим
# параметр $0 самого shell хранит путь к shell-программе
$ echo $0
/usr/bin/ash
$ cat ./test.sh
echo '$0='$0
echo '$1='$1
echo '$2='$2
# передаем пару параметров скрипту test.sh
$ ash ./test.sh first second
$0=./test.sh
$1=first
$2=second
Точно такая же логика с позиционными параметрами работает и для функций. В $0
будет содержаться путь к shell, а параметры в функции можно передавать аналогично скриптам
$ hello() { echo '$0='$0; echo "Hello, $1"; }
$ hello World
$0=/usr/bin/ash
Hello, World
Позиционные параметры в скриптах и функциях можно переопределять с помощью builtin-команды set
(подробно синтаксис set разберем в отдельной статье про builtin-команды)
$ hello() {
echo "Hello, $1 and $2";
set Beavis Butthead;
echo "Hello, $1 and $2";
}
$ hello Rick Morty
Hello, Rick and Morty
Hello, Beavis and Butthead
Переменные - Variables #
Shell-переменные можно использовать внутри скриптов для хранения значений
$ a=1
$ b=2
$ TEST=100500
$ echo "$a-$b-$TEST"
1-2-100500
Но эти параметры не передаются при вызовах команд в environment-переменные, можно делать вручную задавать environment-переменные для каждой команды
$ cat ./test.sh
echo '$TEST='$TEST
# переменная TEST будет пуста
$ ./test.sh
$TEST=
# передаем значение переменной TEST на момент вызова
$ TEST=1 ./test.sh
$TEST=1
Для того чтобы shell-переменные передавались в окружение (environment) всем выполняемым командам, нужно экспортировать переменную
$ export TEST=100500
$ ./test.sh
$TEST=100500
Переменные в функциях #
В функциях видны все shell-переменные (и environment-переменные тоже) — их можно использовать внутри.
Также все команды, выполненные внутри функций влияют на shell, в общем все точно также как и при выполнении обычных команд без функции. Чтобы команды не влияли на текущий shell можно группировать их в круглых скобках .
Локальные переменные в функциях #
Если определенные переменные нужны только внутри функции можно воспользоваться builtin-функцией local.
Команда local
должна быть вызвана в начале функции и должна содержать имя локальной переменной. Локальные переменные никак не инициализируются и задавать значения им нужно отдельно.
# в shell своя переменная name
$ name=Pachino
$ hello () {
local name # в функции своя переменная name
echo 'local name='$name
name=DeNiro
echo 'local name='$name
}
$ hello
local name=Pachino
local name=DeNiro
# в shell переменная не поменялась
$ echo 'name='$name
name=Pachino
Кроме этого scope локальных переменных сохраняется при создании вложенных функций - то есть для вложенной функции будет своя переменная, а не глобальная.
$ name=Pachino
$ hello () {
local name
name=DeNiro
echo 'name='$name
bang() { echo 'bang name='$name }
bang
}
$ hello
name=DeNiro
# в функции bang переиспользуется локальная переменная name
# из функции hello
bang name=DeNiro
# в shell осталась своя переменная
$ echo $name
Pachino
Вместо имени переменной в local
можно указать -
$ hello() {
local -
# ...
}
В этом случае все установленные опции с помощью команды set
будут локальными и вернуться к своим значениям при выходе из функции.
Потоки в функциях #
Раз функция ведет себя как команда, значит внутри нее можно работать с потоками и использовать функции в пайплайнах
# передаем в cat данные из stdin-потока
$ hello { echo 'Hello'; cat <&0; }
$ echo 'World' | hello
Hello
World
stdout и stderr- потоки у функции также свои
$ hello() { echo 'Hello' >&1; echo 'World' >&2; }
$ hello
Hello
World
# перенаправляем stderr в /dev/null
$ hello 2>/dev/null
Hello
Redirections #
про потоки процесса и их нумерацию:
- 0 - stdin - входной поток процесса, через него могут передаваться данные из другой команды
- 1 - stdout - выходной поток процесса, в этот поток процесс может писать результаты
- 2 - stderr - поток для ошибок и отладки
Все перенаправления потоков, рассмотренные дальше, действуют только в рамках текущей команды или набора команд.
оператор [n]> file
#
Потоки можно перенаправлять в файлы
# 123 будет записано в файл
# тут мы перенаправили поток stdout в файл tmp.txt
$ echo 123 > tmp.txt
Оператор >
позволяет перенаправить поток в файл по номеру потока. Приведенная выше команда это краткая запись команды. Полная запись выглядит так
# перенаправляем stdout в файл tmp.txt
$ echo 123 1> tmp.txt
Аналогичным образом можно перенаправить stderr
$ echo 'error' 2> errors.log
Оператор >
полностью перезаписывает содержимое файла новыми данными, если файл уже существует. Если файла нет - создает и записывает данные.
Для защиты от перезаписи файлов, например, при отладке каких-то уже существующих скриптов, существует аргумент -С
(noclobber), который предотвращает перезапись файла — с этим аргументом ash будет выдавать ошибку, если файл уже существует.
оператор [n]>| file
#
Делает все то же самое что и предыдущий оператор, но при запуске с аргументом -C
(noclobber) файл все равно будет перезаписан — такой force для перенаправления потока в файл.
# скрипт записывает текст в файл с использованием оператора >
$ cat test.sh
echo 123 > tmp.txt
# выполняем - в файл tmp.txt записывается 123
$ ash test.sh
# еще раз выполняем тот же скрипт,
# файл tmp.txt будет перезаписан с тем же текстом 123
$ ash test.sh
# с аргументом -C при перенаправлении с оператором > будет ошибка
$ ash -C test.sh
test.sh: line 1: can't create tmp.txt: File exists
# если переделать оператор > на >|
$ cat test.sh
echo 1234 >| tmp.txt
# то даже с аргументом -C файл будет перезаписан
$ ash -C test.sh
оператор [n]>> file
#
Дописывает данные из потока в конец файла, если файла нет - то создает
$ echo 567 1>> tmp.txt
$ echo 890 >> tmp.txt
$ cat tmp.txt
1234
567
890
оператор [n]< file
#
чтение потока из файла
В основном используется для чтения stdin
# команда cat выводит stdin
# или содержимое файла (если указан путь к файлу)
# будет выведено содержимое stdin,
# которое будет прочитано из файла tmp.txt
$ cat 0< tmp.txt
1234
567
890
# сокращенная запись без указания номера потока
$ cat < tmp.txt
# вывод такой же
Оператор перенаправления может быть записан и до команды
$ < tmp.txt cat
# командой cat из stdin будет выведено содержимое файла tmp.txt
оператор [n1]>&n2
#
Перенаправляет поток n1 (или stdout - 1 по умолчанию) в поток с номером n2
Интерактивный шелл тоже процесс и у него есть свои stdin, stdout, stderr
# пишем в stdout - поток с номером 1
$ echo 'out' >&1
out
# пишем в stderr - поток с номером 2
$ echo 'error' >&2
error
Вывод обеих команд будет отображен в консоли, потому что для интерактивного шелла весь вывод идет на экран.
Для скриптов также можно писать отдельно в stdout и stderr
# скрипт пишет раздельно в stdout и в stderr
$ cat test.sh
echo 'out' >&1
echo 'error' >&2
# если просто выполнить скрипт, то всё увидим в консоли
$ ash test.sh
out
error
# С помощью оператора >
# мы можем перенаправить stdout и stderr в разные файлы
$ ash test.sh 1> stdout.txt 2>stderr.txt
$ cat stdout.txt
out
$ cat stderr.txt
error
Еще с помощью этого оператора можно наоборот сводить потоки, например, stdout и stderr в один файл
$ ash test.sh 1> stdout.txt
error
# в консоли вывелось error,
# потому что sterr не перенаправлен и отображается в консоль
# заворачиваем поток stderr в stdout
$ ash test.sh 1> stdout.txt 2>&1
# и stdout и stderr записались в файл stdout.txt
$ cat stdout.txt
out
error
Многострочные данные для потока - here-document, heredoc #
Во всех примерах до этого мы рассмотрели перенаправления в файл и из файла. С помощью backslash(/) можно визуально переносить строки для лучшей читаемости и перенаправить их на вход другой команде (об этом в следующих статьях). Но при этом переносы строк не будут выводится
$ echo "very very very \
long long long \
text text text"
very very very long long long text text text
Для решения этой проблемы в ash существует синтаксис here-document, который в общем виде выглядит так
[n]<< delimiter
here-doc-text ...
delimiter
delimiter может быть любой последовательностью символов, первый раз после указания delimiter на следующей строке должны начаться данные, после второго указания delimiter с начала строки данные завершились. Обычно delimiter указывают EOL - как аббревиатура End Of Line, но он может быть и другой последовательностью символов.
$ cat 0<< EOL
> very very very
> long long long
> text text text
> EOL
very very very
long long long
text text text
В интерактивном режиме после EOL и переноса строки shell символом >
в начале строки подсказывает что ввод команды продолжается.
Для скриптов heredoc используется аналогично
$ cat test.sh
cat << EOL
1
2
3
EOL
$ ash test.sh
1
2
3
Если требуется в скрипте визуально отделить содержимое heredoc, можно использовать символ табуляции и добавить оператор <<-
– в этом случае shell удалит все табы с начала строк
$ cat test.sh
cat <<- EOL
<tab>1
<tab>2
<tab>3
EOL
$ ash test.sh
1
2
3
Кроме этого внутри heredoc можно использовать shell-переменные
$ cat test.sh
cat << EOL
pwd = $PWD
EOL
$ ash test.sh
pwd = /home/ubuntu
Чтобы текст внутри heredoc обрабатывался как обычные символы без подстановки переменных, нужно первый delimiter заключить в кавычки (двойные или одинарные)
$ cat test.sh
cat << 'EOL'
pwd = $PWD
EOL
$ ash test.sh
pwd = $PWD
Работа с дескрипторами #
Еще одна фича для работы с дескрипторами файлов — это возможность открыть файл в какой-то номер дескриптора, номера могут быть от 0 до 9.
Для того чтобы использовать номер дескриптора в скрипте или интерактивном shell для нескольких команд нужно использовать builtin-команду exec (подробнее мы рассмотрим ее в следующих статьях).
Сейчас нам нужно знать что все перенаправления выполненные в exec становятся постоянными (permanant) в рамках шелла или скрипта, то есть открытые дескрипторы можно использовать для последующих команд.
оператор [n]<> file
#
Позволяет открыть дескриптор файла на чтение и запись
# открываем поток (дескриптор файла) с номером 3 для файла tmp.txt
$ exec 3<> ./tmp.txt
# так как команда была выполнена с помощью exec
# дескриптор доступен для следующих команд
$ echo '1' >&3
$ echo '2' >&3
$ echo '3' >&3
$ cat ./tmp.txt
1
2
3
оператор [n]>-
#
Закрыть дескриптор файла
# продолжая предыдущий пример
$ exec 3>-
# дескриптор файла tmp.txt закрыт
# если снова открыть дескриптор,
# то запись или чтение из файла начнется с начала
$ exec 3<> ./tmp.txt
# читаем из дескриптора 2 байта (1\n) - единицу и перенос строки
$ read -n 2 <&3
# сейчас указатель дексриптора указывает на вторую строку (2\n)
# запишем 9
$ echo 9 >&3
$ cat ./tmp.txt
1
9
3
Аналогично можно работать с input с помощью операторов [n1]<&n2
, [n]<-
Variables #
Команда запускает процесс, у процесса могут быть переменные окружения (environment variables, env vars). Переменные окружения для конкретной команды можно задать непосредственно при вызове:
$ a=1 env
SHLVL=1
PATH=/sbin:/usr/sbin:/bin:/usr/bin
PWD=/home/ubuntu
a=1 # <<< переменная 'a' есть в переменных окружения
Вообще и в интерактивном шелле и в скриптах все что соответствует паттертну name=value
с начала строки (или после служебного слова) считается переменной и если после переменных следует команда, то все эти переменные попадут в переменные окружения процесса запущенного этой командой:
$ a=1 b=2 env
# ...
a=1
b=2
Если переменные указаны без команды, то они определяются как переменные шелла - shell variables.
Shell variables доступны для подстановки в самом шелле, но во все выполняемые команды передаются только environment variables.
Shell-переменные (в том числе environment-переменные) можно посмотреть builtin-командой set
, а env-переменные командой env
или export
.
Несколько примеров чтобы стало понятнее
# определяем shell-переменную my_test_var
$ my_test_var=1
# смотрим что переменная определилась
# в результатах set выводятся вообще все переменные,
# в том числе environment variables
$ set
# ...
PS4='+ '
PWD='/home/ubuntu'
SHLVL='1'
my_test_var='1' # <<< тут есть переменная 'my_test_var'
# так как my_test_var еще не environment variable,
# ее нет в выводе команды env
$ env
SHLVL=1
PATH=/sbin:/usr/sbin:/bin:/usr/bin
PWD=/home/ubuntu
# но ее можно использовать в самом шелле
$ echo 'my_test_var = '$my_test_var
my_test_var = 1
# для того чтобы переменная стала environment-переменной
# ее нужно экспортировать
$ export my_test_var
# теперь переменная my_test_var является environment-переменной
# и передаётся в environment всех команд запущенных в шелле
$ env
SHLVL=1
PATH=/sbin:/usr/sbin:/bin:/usr/bin
PWD=/home/ubuntu
my_test_var=1 # <<<
# для конкретного вызова команды
# environment-переменную всегда можно переопределить
$ my_test_var=100500 env
# ...
my_test_var=100500
Пример показывает что все переменные доступны для подстановки внутри shell, но только environment-переменные передаются в окружение всем командам, выполняемым в shell.
В названии переменных могут использоваться символы латинского алфавита, числа и символ подчеркивания, название переменной не может начинаться с числа.
Для более явного разделения переменных существует негласное правило:
environment-переменные пишутся заглавными буквами, а обычные shell-переменные строчными
# PATH - environment-переменная
$ echo $PATH
# my_test_var - обычная shell-переменная
$ my_test_var=1
Environment variables #
HOME #
Домашняя директория пользователя и passwd файла, переменная устанавливается командой login и используется множеством разных команд, например, командой cd без параметров для перехода в домашнюю директорию пользователя
Для проверки можно запустить ash без env-переменных от root пользователя и выполнить команду login
## запускаем ash без env-параметров текущего shell и под root
## root - нужен для запуска login,
## иначе будет ошибка login: must be suid to work properly
$ sudo env -i /usr/bin/ash
## далее одинарная решетка это prompt root пользователя
# env
SHLVL=1
PATH=/sbin:/usr/sbin:/bin:/usr/bin
PWD=/home/ubuntu
# login -f ubuntu
## ...
PATH=/home/ubuntu/bin:/home/ubuntu/.local/bin:/data/bin:/bin:/usr/bin
PWD=/home/ubuntu
SHLVL=1
USER=ubuntu
HOME=/home/ubuntu
## ...
PATH #
Список директорий разделенный двоеточиями. В этих директориях shell ищет программы.
CDPATH #
Путь относительно которого работает команда cd
# задаем CDPATH=/home/ubuntu
$ export CDPATH=/home/ubuntu
$ cd
/home/ubuntu
# теперь все относительные пути без указания первого слэша
# будут считаться относительно /home/ubuntu
$ cd tmp
# мы в /home/ubuntu/tmp - если такая директория существует
MAIL #
Следующие 3 env-переменных нужны для проверки почты, точнее в самом shell реализовано слежение за изменением в файле
в MAIL может содержаться путь к файлу, за изменениями которого нужно следить.
MAILCHECK #
в MAILCHECK устанавливается время в секундах, через сколько нужно проверять файл или файлы указанные в MAIL или MAILPATH
По умолчанию = 60
Если поставить 0, то проверка будет происходит после каждого выполнения команды, В доке формулируется так “If set to 0, the check will occur at each prompt”, то есть при каждом показе приглашения к вводу команды.
MAILPATH #
в MAILPATH через двоеточие :
может быть указано несколько файлов, которые нужно проверять
если указан MAILPATH, то MAIL игнорируется.
работает это так
$ touch ~/mail.txt
$ export MAILPATH=/home/ubuntu/mail.txt
$ export MAILCHECK=5
# меняем файл, в случае почтовых программ они меняют файл
# и bash видимо умеет их парсить и выводить даже количество писем
$ echo 1 >> ~/mail.txt
# далее после выполнения команды или просто пустой строки ash выведет
$
you have mail
$
# то есть проверка происходит перед показом prompt
PS1 #
Prompt String - это основная подсказка к вводу команды и по умолчанию для обычного пользователя это переменная равна $
(доллар и пробел), для суперпользователя root #
(решетка и пробел)
значение может содержать не специальные последовательности
# например, по умолчанию PS1 в ash выводит еще и текущую директорию
# сейчас мы в домашней директории
~ $
# а переменная равна
$ echo $PS1
\w \$
Подробнее про эти опции поговорим в статьях про bash
PS2 #
Подсказка ввода для многострочных команд. По умолчанию >
$ (
> echo 123
> )
123
$
PS3 #
Почему-то не описано в документации, возможно в ash не используется
PS4 #
В случае если включена трассировка опцией -x
, перед каждой выполненной командой выводится значение PS4
По умолчанию равно +
$ set -x
$ echo 1 ; echo 2
+ echo 1
1
+ echo 2
2
$
IFS #
Input Field Separators
Разделители при чтении и выводе параметров - подробнее разберем в следующей статье про Special Parameters (здесь будет ссылка на статью)
TERM #
В переменной содержится название текущего терминала, и используется программами для определения возможностей терминала, например, программа может определить поддерживает ли терминал цвета (xterm или xterm-256color)
HISTSIZE #
Сколько команд хранить в истории shell’a (это переключение истории команд стрелками)
По умолчанию равно 15
PWD #
Текущая директория, эту переменную меняет команда cd
OLDPWD #
Предыдущая директория
PPID #
ID процесса текущего shell
$ echo $PPID
1527