Командная строка в UNIX-подобных системах выполняет текстовые команды, и этот текст может быть записан в виде построчной программы, пригодной к выполнению из файла.
Скрипт командной строки обычно начинается со строчки вида:
#!/usr/bin/env bash
Данная строка синтаксически является комментарием во многих языках программирования, но имеет специальное назначение в UNIX-подобных системах. Исполняемые файлы, которые начинаются с символов #!
подразумевают запуск программы-интерпретатора, указанной после #!
, которой передается в качестве аргумента передается имя файла скрипта.
В программы-интерпретатора должен быть указан ее полный путь. Расположение некоторых интерпретаторов, например /bin/sh
, является стандартизированным для всех UNIX-подобных систем, для других, например bash, этот путь может быть как /bin/bash
, так и /usr/bin/bash
или /usr/local/bin/bash
, - гарантировать путь однозначно нельзя. Для поиска интерпретатора в переменной окружения PATH
используется утилита /usr/bin/env
, которая устанавливает переменные окружения, включая PATH
, и запускает программу, переданную ей в качестве аргумента.
В разных Linux-системах и других UNIX-подобных системах используются различные интерпретаторы командной строки, общим предком которых является классический интерпретатор /bin/sh
. При этом, сам интерпретатор /bin/sh
является символической ссылкой на используемый в дистрибутиве интерпретатор по умолчанию (за исключением MacOS, где /bin/sh
- это bash
старой версии, в то время как в системе используется zsh
).
Можно считать, что программа, ориентированная на /bin/sh
может быть запущена на любой UNIX-подобной системе, но при написании таких скриптов необходимо ориентироваться на общее подмножество функциональности различных интерпретаторов, которое регламентировано стандартом POSIX. В дальнейшем мы будем использовать интерпретатор bash
, который присутствует во всех популярных дистрибутивах Linux, и обладает широкой фунциональностью.
Каждая команда shell-скрипта может располагаться на отдельной строке, либо заканчиваться символом точки с запятой, если необходимо записать в одну строку несколько команд.
Команды скриптов, в большинстве случаев, - это внешние программы, которые располагаются в одном из каталогов, перечисленных в переменной окружения PATH
, но некоторые команды не могут быть реализованы как отдельные программы, поскольку изменяют текущее окружение, что не может быть сделано внешней программмой. Примерами таких команд являются:
cd
- изменение текущего каталога;export
- делают переменную доступной дочерним процессам;read
- читает текст из файла или стандартного потока ввода, и записывает результат в переменную;ulimit
- устанавливает ограничения ресурсов на текущий сеанс;exit
- завершает работу командного интерпретатора.
Кроме того, поскольку запуск внешних программ является ресурсозатратной операцией, в некоторых оболочках отдельные часто используемые команды реализованы как встроенные, хотя их функциональность дублируется внешними одноименными программами, например команда echo
для интерпретаторов bash
и zsh
, или команда [
для оболочки bash
. Полный список встроенных команд для текущей оболочки можно получить командой man builtins
.
Команды могут иметь аргументы, которые разделяютя пробелами. В случае, если аргумент должен содержать пробел, или какой-либо другой символ, имеющий специальное назначение, такой аргумент нужно заключать в кавычки. Также зарезервированные символы можно экранировать с помощью символа \
. Список зарезервированных символов, помимо пробельных, которые нужно экранировать, или заключать в кавычки:
| & ; < > ( ) $ ` \ " ' * ? [ # ~ = %
Экранирование символа переноса строки выполняется специальным образом, что обусловлено необходимостью читабельности кода скрипта: $'\n'
.
В скриптах используются три вида кавычек, которые имеют различное семантическое назначение:
- 'одинарные кавычки' - сохраняет текст без изменений;
- `обратные одинарные кавычки` - выполняют команду и результатом является вывод этой команды;
- "двойные кавычки" - внутри них возможно экранирование символом , подстановка значений переменных (начинаются с символа $), и возможно выполнение команд с помощью вложенных
обратных кавычек
.
Переменные объявляются символом =
, причем пробелы вокруг этого символа не допускаются. Использовать переменные можно в виде $переменная
или ${переменная}
.
# значение 123
var1=123
# пустое значение
var2=
# строка
var3="hello world"
# так нельзя - будет ошибка
var4 = 123
# обращение к переменным
# вывод будет: $var1 hello world
echo '$var1' "$var2" "$var3" "$var_not_exist"
Использование несуществующей переменной не приводит к ошибке, - будет просто пустое значение.
Если необходимо сохранить вывод команды в переменную, то используется один из двух способов:
- `обратные одинарные кавычки`, между которыми заключена команда и ее аргументы;
- заключение к конструкцию
$()
.
Если вывод команды содержит в конце символы перевода строк, то они удаляются.
Пример:
os_name=`uname -s`
arch_name=$(uname -m)
echo "OS is $os_name running on $arch_name"
# OS is Linux running on x86_64
Если необходимо получить код возврата команды, а не результат ее вывода, то можно использовать переменную $?
сразу после ее выполнения.
$?
- целочисленный код возврата последней команды;$0
...$9
- аргументы команды от 0 до 9, при этом$0
- имя самого скрипта;$#
- целочисленное значение количества аргументов;$@
- список всех аргументов, начиная с первого;$*
- строка, которая содержит список всех аргументов, начиная с первого.
Функции - это команды, которые доступны только из текущего скрипта, которым можно передавать аргументы, и они могут возвращать текст с помощью записи на "стандартный поток вывода".
Интерпретаторы bash
и zsh
поддерживают три вида синтаксиса объявлений:
# 1. Только имя и скобки
very_important_function() {
# реализация функции
}
# 2. Полный синтаксис
function very_important_function() {
# реализация функции
}
# 3. Без скобок
function very_important_function {
# реализация функции
}
# вызов функции с двумя аргументами, и сохранением результата
value=$(very_important_function hello world)
# вывзов функции без сохранения возвращаемого результата
very_important_function hello world
Если необходимо обеспечить совместимость с произвольным интерпретатором POSIX sh
, то можно использовать только первый вариант объявления.
Аргументы в функцию передаются точно так же, как и в команду, и доступны через специальные переменные.
Все переменные, объявляенные внутри функции, становятся доступными глобально после ее завершения. Если переменные нужно только локально, то в bash
и zsh
перед объявлением переменной можно использовать ключевое слово local
.
Функции можно импортировать из другого файла, используя синтаксис . имя_файла
.
Для передачи данных от одной функции/команды к другой, не обязательно сохранять результаты в переменную, можно передавать из через механизм перенаправления, используя оператор |
.
function f() {
# вывод kek и списка аргументов
echo "kek $*"
}
function g() {
# замена e на E
sed 's/e/E/g'
}
function h() {
# замена d на первый аргумент функции
echo $0
sed "s/d/$1/g"
}
f first second third | g | h Meaow
# kEk first sEconMeaow thirMeaow
# команда wc -c подсчитвает количество байт
f | wc -c
# 5
Результатом работы команды, помимо вывода, является целочисленный код возврата, причем целое число должно быть в диапазоне от 0 до 127. Значение 0 означает успешное завершение команды, остальные значения, - признак "ошибки" или ложного значения. Код завершения предыдущей выполненной команды хранится в переменной $?
. При объявлении функций код возврата определяется кодом возврата последней выполненной внутри функции команды, либо задается с помощью оператора return
.
Команды можно объединять в последовательности, которые выполняются в зависимости от результата выполнения предыдущей команды:
cmd1 && cmd2
-cmd2
будет выполнена, еслиcmd1
выполнена успешно, а итоговый результат - это код возвратаcmd2
;cmd1 || cmd2
-cmd2
будет выполнена, если не удалось успешно выполнитьcmd1
, результат - либо значение 0, либо код возвратаcmd2
.
Выполнение цепочки команд можно заключать в круглые скобки для указания приоритетов логических операций.
function f() {
echo "I'm function f"
return 0
}
function g() {
echo "I'm function g"
return 5
}
function h() {
echo "I'm function h"
return 0
}
f && g && h # вывод только от f и g, но не h
echo "---"
f && (g || h) # вывод от f, g и h
Предусмотрены две программы, которые не делают абсолютно ничего, а только возвращают код 0 или 1: это команда true
, и команда false
. Они предназначены для использования внутри таких "логических выражений", например, если нужно подавить код ошибки для необязательной операции:
rm -f файл_который_не_существует || true # всегда будет успешный код возврата
Логические условия могут быть использованы условных конструкций, как в обычных языках программирования.
if true
then
# эта часть всегда будет выполняться
fi
if false
then
# это не будет выполняться никогда
fi
Аргументом команды if
может быть любая команда. Истинным условием считается нулевой код возврата, а ложным - ненулевой. Для выполнения различных логических операций служит конструкция [ .... ]
, которая реализована, в общем случае, с помощью отдельной команды [
.
$x -eq $y
- истина, если значения$x
и$y
равны;$x -nq $y
- истина, если значения$x
и$y
не равны;$x -gt $y
- истина, если значение$x
>$y
;$x -lt $y
- истина, если значение$x
<$y
;$x -ge $y
- истина, если значение$x
>=$y
;$x -le $y
- истина, если значение$x
<=$y
;-n $str
- истина, если строка$str
не пустая;-z $str
- истина, если строка$str
пустая;$str1 = $str2
- истина, если строки$str1
и$str2
равны;-e $pathname
- истина, если существует путь$pathname
;-f $filename
- истина, если существует обычный файл$filename
;-d $dirname
- истина, если существует каталог$dirname
;-x $filename
- истина, если существует обычный файл$filename
, и он является выполняемым.
Внутри конструкции [ ... ]
можно использовать круглые скобки для указания приоритетов, и оператор отрицания !
.
Важно особенностью этой конструкции является то, что между символами [
, ]
и разными операторами внутри конструкции обязавтельно должны быть пробельные символы, поскольку это вызов команды с аргументами.
Простейшим циклом является цикл while
, аргумент которого точно такой же, как у конструкции if
.
while true
do
# не только простейшая, но и самая
# опасная конструкция, поскольку цикл
# может никогда не завершиться
done
Конструкция for
предназначена для итерации по элементам списка.
# 1. Итерация по элементам простого списка
for item in i love akos
do
echo "$item"
done
# 2. Итерация по элемента генерируемого по маске списка файлов
for filename in *.txt
do
echo "$filename might be plain text"
done
Интерпретаторы bash
и zsh
имеют еще одну, нестандартную для POSIX sh
, конструкцию циклов, которая синтаксически близка к Си-подобным языкам.
# только bash/zsh
for (( i=0; i<10; i++ ))
do
echo "$i"
done
Элементы списка разделяются символом пробел в самом скрипте, если они перечислены после ключевого слова in
, но могут также быть прочитаны из файла, либо получены из произвольной строки.
for item in $(echo "i love akos")
do
echo "$item"
done
# i
# love
# akos
В этом случае разделителями считаются подряд идущие последовательности пробельных символов: пробел, табуляция и символ перевода строки. Часто бывает необходимо переопределить символ разделителя, например для обработки текстовых файлов определенного формата. Для этого предназначена специальная переменная интерпретатора IFS
(аббривеатура от Internal Field Separator).
IFS=💞
for item in $(echo "i💞love💞💞💞akos")
do
echo "$item"
done
# i
# love
#
#
# akos
После переопределения символа в переменной IFS
, разделители не группируются. В качестве разделителя можно использовать только односимвольную строку, причем интерпретаторы bash
и zsh
корректно обрабатывают многобайтные символы Юникода, но это не гарантируется для других интерпретаторов.
Чтение из файла можно организовать либо через команду cat
, либо используя встроенную функцию read
(обычно используется внутри цикла while
), которая читает очередную лексему, ограниченную разделителем из IFS
, и возвращает код 0, в случае успешного чтения.
Командный интерпретатор sh
позволяет вычислять произвольные арифметические выражения, но с принципиальным ограничением: допускается только знаковая целочисленная арифметика. Синтаксически операции эквивалентны таковым в других языках программирования, а сами выражения заключаются в конструкцию $(( ... ))
. В отличии от команды [
, круглые скобки не являются внешней командой, поэтому пробельные символы не обязательны.
a=5
b=3
c=$(($a+$b))
d=$(($a/$b))
echo "a = $a, b = $b, c = $c, d = $d"
# a = 5, b = 3, c = 8, d = 1
Для вычислений с вещественнозначными значениями можно использовать простой консольный калькулятор bc
(Basic Calculator). Эта программа выполняет вычисление арифметических выражений, позволяет использовать функции логарифма, экспоненты и тригонометрические функции.
echo '(1+3)*2' | bc
# 8
# по умолчанию используется целочисленная арифметика,
# флаг -l подключает дополнительную функциональность
echo '(1+3)/2.5' | bc -l
# 1.60000000000000000000
Массивы являются нестандартным расширением sh
, реализованным (по-разному) в командных интерпретаторах bash
и zsh
.
Переменные массива объявляются как список, разделенный пробельными символами, заключенный в круглые скобки. Пустой массив обявляется как ()
.
Элементы массива можно индексировать целыми числами с 0 (для bash
) или с 1 (для zsh
). Индексы указываются в квадратных скобках, поэтому для исключения неоднозначности операторов, при использовании значения из массива, обязательно заключать переменную с индексом в фигурные скобки.
Адресация массива целиком (например, для вывода), а не его отдельного элемента осуществляется с указанием индекса [@]
. Размер массива определяется как ${#массив[@]}
.
# пример для bash - индексация с 0
array=(1 2 3 4 5 6)
array_size=${#array[@]}
for (( i=0; i<$array_size; i++ ))
do
# удвоенное значение
array[$i]=$(( ${array[$i]} * 2 ))
done
echo "${array[@]}"
Задачи обработки текстов возникают очень часто, и во многих случаях для их решения совершенно не обязательно писать программы на высокоуровневых языках программирования, - можно воспользоваться стандартными утилитами среды POSIX.
Регулярное выражение - это текстовый шаблон, включающий в себя специальные символы-подставновки, который предназначен для поиска и замен в тексте.
Синтаксис описания регулярных выражений бывает различный, наиболее распространенный из них - это в формате языка программирования Perl, который также используется во многих других языках программирования.
Стандарт POSIX для регулярных выражений при этом является менее функциональным, и определяет два уровня языка описания: базовый (BRE) и расширенный (ERE). Расширенный синтаксис POSIX отличается от базового тем, что не требует обязательного экранирования символов скобок, а также вводит операции ?
, +
и |
. В дальнейшем будем использовать именно расширенный синтаксис (утилиты sed
и grep
требуют явного указания ключа -E
для работы в расширенном синтаксисе).
Для тестирования регулярных выражений можно использовать веб-приложение regex101.com, которое не поддерживает синтаксис POSIX, поэтому при написании выражений нужно не забывать о том, что не поддерживаются PCRE-специфичные конструкции, например определения классов символов через символ \
.
Символы-подстановки, используемые в регулярных выражениях:
^
- начало строки;.
- любой символ;[ ]
- любой символ или диапазон символов, из перечисленных в квадратных скобках;[^ ]
- то же самое, но с отрицанием;$
- признак конца строки;( )
- группа символов или подстановок;*
- повторение предыдущего символа 0 или более раз;?
- повторение предыдущего символа 0 или 1 раз (только ERE);+
- повторение предыдущего символа 1 или более раз (только ERE);{n}
- повторение предыдущего символа ровноn
раз (только ERE);{m, n}
- повторение предыдущего символа отm
доn
раз (только ERE);|
- выбор одного из вариантов, между которыми встретился этот символ (только ERE).
Утилита grep
построчно просматривает текст из файла или стандартного потока ввода, и выполняет фильтрацию содержимого, оставляя только те строки текста, которые соответствуют шаблону.
Пример. Содержимое исходного файла test.txt
:
мама мыла раму
папа кушал сидр
акос любят все
мы все умрем
физтех чемпион физтех лучше всех
# только одна строка, которая содержит слово "мама"
> grep мама test.txt
мама мыла раму
# строки, которые содержат слова "мама" и "папа"
> grep .а.а test.txt
мама мыла раму
папа кушал сидр
# все строки, которые начинаются со слова из четырех букв
> grep -E '^.{4} ' test.txt
мама мыла раму
папа кушал сидр
акос любят все
В последнем примере обратите внимание на следующие особенности:
- необходима опция
-E
, поскольку используется конструкция{n}
, определенная в расширенном стандарте; - регулярное выражение содержит символ пробела, поэтому заключено в кавычки.
Части регулярного выражения, которые заключены в круглые скобки, запоминают вхождение текста, и могут быть использованы в самом шаблоне. Эти вхождения нумеруются от \1
до \9
.
# найти все строки, в которых слово из алфавита [а-яА-Я]
# повторяется через одно слово
> grep -E '([а-яА-Я]+) .+ \1'
физтех чемпион физтех лучше всех
Вместе с утилитой grep
часто используется утилита cut
, которая в найденной строке выбирает определенный "столбец", считая разделителем либо символ табуляции, либо какой-то произвольно заданный символ.
# найдем все вторые слова в строках, которые начинаются
# со слова из четырех букв
> grep -E '^.{4} ' test.txt | cut -d ' ' -f 2
мыла
кушал
любят
Помимо поиска, второй важный класс задач со строками, - это редактирование текста. Для автоматизации используются командные текстовые редакторы, такие как sed
или awk
. В отличии от обычных текстовых редакторов с пользовательским интерфейсом, потоковые редакторы оперируют набором команд редактирования.
Команды sed
разделяются символом ;
и выполняют одно из действий: вставка в начало, вставка в конец, удаление и замена текста. Общий вид команд: [ПОЗИЦИЯ]ДЕЙСТВИЕ
, где ПОЗИЦИЯ
- это необязательная часть команды, определяющая позицию курсора редактирвоания, ДЕЙСТВИЕ
- однобуквенная команда с возможными аргументами.
Основные команды редактирования:
d
удаление;a
добавление текста после курсора;i
добавление текста перед курсором;s
замена текста по шаблону.
ПОЗИЦИЯ
описывается одним в одном из форматов:
ЧИСЛО
- номер строки, которые нумеруются с 1;ЧИСЛО~ШАГ
- номер строки с повторением действия через определенное количество шагов;$
- последняя строка;/РЕГУЛЯРКА/
- все строки, сопоставленные с шаблоном.
Набор команд является обязательным позиционным аргументом для команды sed
. Как и для утилиты grep
, если предполагается использование расширенного синтаксиса регулярных выражений, необходим флаг -E
.
Если утилите sed
не указывать имя входного файла, то подразумевается взаимодействие со стандартными потоками ввода и вывода. Если указываются файлы (их может быть несколько), то прозводится чтение из указанных файлов, причем по умолчанию используется сквозная нумерация строк по всем файлам. Для того, чтобы каждый файл обрабатывался по-отдельности, необходима опция -s
.
Опция -i
, также как и для clang-format
, подразумевает сохранение изменений в исходный файл, а не вывод результата на стандартный поток вывода. Используйте эту опцию с осторожностью.
# удалить первую и последнюю строки из файла
> sed '1d; $d' test.txt
папа кушал сидр
акос любят все
мы все умрем
# удалить все нечетные строки из файла
> sed '1~2d' test.txt
папа кушал сидр
мы все умрем
# вставить строку #!/bin/cat в начало файла
> sed '1i#!/bin/cat' test.txt
#!/bin/cat
мама мыла раму
папа кушал сидр
акос любят все
мы все умрем
физтех чемпион физтех лучше всех
# вставить пустую строку после первой строки
> sed '1a\ ' test.txt
мама мыла раму
папа кушал сидр
акос любят все
мы все умрем
физтех чемпион физтех лучше всех
# заменить слова "мама" и "папа" на "родитель"
> sed -E 's/(мама|папа) /родитель /' test.txt
родитель мыла раму
родитель кушал сидр
акос любят все
мы все умрем
физтех чемпион физтех лучше всех
# удалить все комментарии из Python-файлов текущего каталога
# (без контроля синтаксиса, в том числе из строковых констант)
> sed -i '/ *#/d' *.py
# поменять местами два первых слова в каждой строке
> sed -E 's/([а-я]+) ([а-я]+) (.*)/\2 \1 \3/' test.txt
мыла мама раму
кушал папа сидр
любят акос все
все мы умрем
чемпион физтех физтех лучше всех
# то же самое, то только для строк, начинающихся с буквы "м"
> sed -E '/^[м].*/s/([а-я]+) ([а-я]+) (.*)/\2 \1 \3/' test.txt
мыла мама раму
папа кушал сидр
акос любят все
все мы умрем
физтех чемпион физтех лучше всех