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 читает скрипт построчно, разбивает его на слова по пробелам и табам.

Некоторые последовательности символов определяются как операторы.

Операторы бывают двух видов:

  1. Control operators — для объединения команд в группы, цепочки-пайпланы и условия
& && ( ) ; ;; | || <newline>
  1. Redirection operators — для перенаправления потоков между командами и не только
< > >| << >> <& >& <<- <>

 


Escaping

Стандартной проблемой в шеллах и в языках программирования является использование специальных символов (и последовательностей) как обычных символов – это называют экранированием (escape special chars). В ash экранирование спец символов можно сделать 3 способами:

обратный слэш — backslash

Backslash перед спец символом делает его обычным символом

# команда ничего не выведет,
# потому что точка с запятой интеретируется как разделитель команд
$ echo ;

# выведется точка с запятой, как обычный символ
$ echo \;
;

Кроме этого backslash перед символом конца строки (переводом строки) интерпретируется как продолжение строки

$ echo 1\
2\
3
123

парные одинарные кавычки — matched single quotes

Все что внутри одинарных кавычек, кроме одинарных кавычек, считается обычным символом

$ echo '&;|>>'
&;|>>

Одинарные кавычки при необходимости можно вывести отдельно с помощью backslash

# последняя кавычка экранирована с помощью backslash
# и считается обычным символом
$ echo '&|$'\'
&|$'

$ echo '$PATH'
$PATH

парные двойные кавычки — matched double qoutes

С двойными кавычками все немного сложнеe.

Все символы внутри двойных кавычек считаются обычными, за исключением символов:

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

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

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

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

 


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

про потоки процесса и их нумерацию:

Все перенаправления потоков, рассмотренные дальше, действуют только в рамках текущей команды или набора команд.

 

оператор [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