400 lines
22 KiB
Markdown
400 lines
22 KiB
Markdown
[https://habr.com/ru/company/ruvds/blog/325928/](https://habr.com/ru/company/ruvds/blog/325928/)
|
||
|
||
---
|
||
|
||
|
||
|
||
В [прошлый раз](https://habrahabr.ru/company/ruvds/blog/325522/) мы рассказали об основах программирования для bash. Даже то немногое, что уже разобрано, позволяет всем желающим приступить к автоматизации работы в Linux. В этом материале продолжим рассказ о bash-скриптах, поговорим об управляющих конструкциях, которые позволяют выполнять повторяющиеся действия. Речь идёт о циклах `for` и `while`, о методах работы с ними и о практических примерах их применения.
|
||
|
||
## Циклы for
|
||
|
||
Оболочка bash поддерживает циклы `for`, которые позволяют организовывать перебор последовательностей значений. Вот какова базовая структура таких циклов:
|
||
|
||
```
|
||
for var in list
|
||
do
|
||
команды
|
||
done
|
||
```
|
||
|
||
В каждой итерации цикла в переменную `var` будет записываться следующее значение из списка `list`. В первом проходе цикла, таким образом, будет задействовано первое значение из списка. Во втором — второе, и так далее — до тех пор, пока цикл не дойдёт до последнего элемента.
|
||
|
||
## Перебор простых значений
|
||
|
||
Пожалуй, самый простой пример цикла `for` в bash-скриптах — это перебор списка простых значений:
|
||
|
||
```
|
||
#!/bin/bash
|
||
for var in first second third fourth fifth
|
||
do
|
||
echo The $var item
|
||
done
|
||
```
|
||
|
||
Ниже показаны результаты работы этого скрипта. Хорошо видно, что в переменную `$var` последовательно попадают элементы из списка. Происходит так до тех пор, пока цикл не дойдёт до последнего из них.
|
||
|
||
![[029f5b70be710c0aab128d612e7ca4bc.png]]
|
||
|
||
Простой цикл for
|
||
|
||
Обратите внимание на то, что переменная `$var` сохраняет значение при выходе из цикла, её содержимое можно менять, в целом, работать с ней можно как с любой другой переменной.
|
||
|
||
## Перебор сложных значений
|
||
|
||
В списке, использованном при инициализации цикла `for`, могут содержаться не только простые строки, состоящие из одного слова, но и целые фразы, в которые входят несколько слов и знаков препинания. Например, всё это может выглядеть так:
|
||
|
||
```
|
||
#!/bin/bash
|
||
for var in first "the second" "the third" "I’ll do it"
|
||
do
|
||
echo "This is: $var"
|
||
done
|
||
```
|
||
|
||
Вот что получится после того, как этот цикл пройдётся по списку. Как видите, результат вполне ожидаем.
|
||
|
||
![[2e5e3f0e856ee457a7c821f1fa3126b8.png]]
|
||
|
||
Перебор сложных значений
|
||
|
||
TNW-CUS-FMP — промо-код на 10% скидку на наши услуги, доступен для активации в течение 7 дней"
|
||
|
||
## Инициализация цикла списком, полученным из результатов работы команды
|
||
|
||
Ещё один способ инициализации цикла `for` заключается в передаче ему списка, который является результатом работы некоей команды. Тут используется подстановка команд для их исполнения и получения результатов их работы.
|
||
|
||
```
|
||
#!/bin/bash
|
||
file="myfile"
|
||
for var in $(cat $file)
|
||
do
|
||
echo " $var"
|
||
done
|
||
```
|
||
|
||
В этом примере задействована команда `cat`, которая читает содержимое файла. Полученный список значений передаётся в цикл и выводится на экран. Обратите внимание на то, что в файле, к которому мы обращаемся, содержится список слов, разделённых знаками перевода строки, пробелы при этом не используются.
|
||
|
||
![[84276530b0d56f7d038aaa0a1f80906f.png]]
|
||
|
||
Цикл, который перебирает содержимое файла
|
||
|
||
Тут надо учесть, что подобный подход, если ожидается построчная обработка данных, не сработает для файла более сложной структуры, в строках которого может содержаться по несколько слов, разделённых пробелами. Цикл будет обрабатывать отдельные слова, а не строки.
|
||
|
||
Что, если это совсем не то, что нужно?
|
||
|
||
## Разделители полей
|
||
|
||
Причина вышеописанной особенности заключается в специальной переменной окружения, которая называется `IFS` (Internal Field Separator) и позволяет указывать разделители полей. По умолчанию оболочка bash считает разделителями полей следующие символы:
|
||
|
||
- Пробел
|
||
- Знак табуляции
|
||
- Знак перевода строки
|
||
|
||
Если bash встречает в данных любой из этих символов, он считает, что перед ним — следующее самостоятельное значение списка.
|
||
|
||
Для того, чтобы решить проблему, можно временно изменить переменную среды `IFS`. Вот как это сделать в bash-скрипте, если исходить из предположения, что в качестве разделителя полей нужен только перевод строки:
|
||
|
||
```
|
||
IFS=$'\n'
|
||
```
|
||
|
||
После добавления этой команды в bash-скрипт, он будет работать как надо, игнорируя пробелы и знаки табуляции, считая разделителями полей лишь символы перевода строки.
|
||
|
||
```
|
||
#!/bin/bash
|
||
file="/etc/passwd"
|
||
IFS=$'\n'
|
||
for var in $(cat $file)
|
||
do
|
||
echo " $var"
|
||
done
|
||
```
|
||
|
||
Если этот скрипт запустить, он выведет именно то, что от него требуется, давая, в каждой итерации цикла, доступ к очередной строке, записанной в файл.
|
||
|
||
![[6f7574e2270854b6200d686ab2682676.png]]
|
||
|
||
Построчный обход содержимого файла в цикле for
|
||
|
||
Разделителями могут быть и другие символы. Например, выше мы выводили на экран содержимое файла `/etc/passwd`. Данные о пользователях в строках разделены с помощью двоеточий. Если в цикле нужно обрабатывать подобные строки, `IFS` можно настроить так:
|
||
|
||
```
|
||
IFS=:
|
||
```
|
||
|
||
## Обход файлов, содержащихся в директории
|
||
|
||
Один из самых распространённых вариантов использования циклов `for` в bash-скриптах заключается в обходе файлов, находящихся в некоей директории, и в обработке этих файлов.
|
||
|
||
Например, вот как можно вывести список файлов и папок:
|
||
|
||
```
|
||
#!/bin/bash
|
||
for file in /home/likegeeks/*
|
||
do
|
||
if [ -d "$file" ]
|
||
then
|
||
echo "$file is a directory"
|
||
elif [ -f "$file" ]
|
||
then
|
||
echo "$file is a file"
|
||
fi
|
||
done
|
||
```
|
||
|
||
Если вы разобрались с [предыдущим материалом](https://habrahabr.ru/company/ruvds/blog/325522/) из этой серии статей, вам должно быть понятно устройство конструкции `if-then`, а так же то, как отличить файл от папки. Если вам сложно понять вышеприведённый код, перечитайте этот материал.
|
||
|
||
Вот что выведет скрипт.
|
||
|
||
![[ec6291ec1b9fbd8e262035bd0bea80c0.png]]
|
||
|
||
Вывод содержимого папки
|
||
|
||
Обратите внимание на то, как мы инициализируем цикл, а именно — на подстановочный знак «*» в конце адреса папки. Этот символ можно воспринимать как шаблон, означающий: «все файлы с любыми именами». он позволяет организовать автоматическую подстановку имён файлов, которые соответствуют шаблону.
|
||
|
||
При проверке условия в операторе `if`, мы заключаем имя переменной в кавычки. Сделано это потому что имя файла или папки может содержать пробелы.
|
||
|
||
## Циклы for в стиле C
|
||
|
||
Если вы знакомы с языком программирования C, синтаксис описания bash-циклов `for` может показаться вам странным, так как привыкли вы, очевидно, к такому описанию циклов:
|
||
|
||
```
|
||
for (i = 0; i < 10; i++)
|
||
{
|
||
printf("number is %d\n", i);
|
||
}
|
||
```
|
||
|
||
В bash-скриптах можно использовать циклы `for`, описание которых выглядит очень похожим на циклы в стиле C, правда, без некоторых отличий тут не обошлось. Схема цикла при подобном подходе выглядит так:
|
||
|
||
```
|
||
for (( начальное значение переменной ; условие окончания цикла; изменение переменной ))
|
||
```
|
||
|
||
На bash это можно написать так:
|
||
|
||
```
|
||
for (( a = 1; a < 10; a++ ))
|
||
```
|
||
|
||
А вот рабочий пример:
|
||
|
||
```
|
||
#!/bin/bash
|
||
for (( i=1; i <= 10; i++ ))
|
||
do
|
||
echo "number is $i"
|
||
done
|
||
```
|
||
|
||
Этот код выведет список чисел от 1 до 10.
|
||
|
||
![[a140bb7bd86b367a68852107cfb88aae.png]]
|
||
|
||
Работа цикла в стиле C
|
||
|
||
## Цикл while
|
||
|
||
Конструкция `for —` не единственный способ организации циклов в bash-скриптах. Здесь можно пользоваться и циклами `while`. В таком цикле можно задать команду проверки некоего условия и выполнять тело цикла до тех пор, пока проверяемое условие возвращает ноль, или сигнал успешного завершения некоей операции. Когда условие цикла вернёт ненулевое значение, что означает ошибку, цикл остановится.
|
||
|
||
Вот схема организации циклов `while`
|
||
|
||
```Shell
|
||
while команда проверки условия
|
||
do
|
||
другие команды
|
||
done
|
||
```
|
||
|
||
Взглянем на пример скрипта с таким циклом:
|
||
|
||
```
|
||
#!/bin/bash
|
||
var1=5
|
||
while [ $var1 -gt 0 ]
|
||
do
|
||
echo $var1
|
||
var1=$[ $var1 - 1 ]
|
||
done
|
||
```
|
||
|
||
На входе в цикл проверяется, больше ли нуля переменная `$var1`. Если это так, выполняется тело цикла, в котором из значения переменной вычитается единица. Так происходит в каждой итерации, при этом мы выводим в консоль значение переменной до его модификации. Как только `$var1` примет значение 0, цикл прекращается.
|
||
|
||
![[95efea2a30cb0785ac7f78b0e0066831.png]]
|
||
|
||
Результат работы цикла while
|
||
|
||
Если не модифицировать переменную `$var1`, это приведёт к попаданию скрипта в бесконечный цикл.
|
||
|
||
## Вложенные циклы
|
||
|
||
В теле цикла можно использовать любые команды, в том числе — запускать другие циклы. Такие конструкции называют вложенными циклами:
|
||
|
||
```
|
||
#!/bin/bash
|
||
for (( a = 1; a <= 3; a++ ))
|
||
do
|
||
echo "Start $a:"
|
||
for (( b = 1; b <= 3; b++ ))
|
||
do
|
||
echo " Inner loop: $b"
|
||
done
|
||
done
|
||
```
|
||
|
||
Ниже показано то, что выведет этот скрипт. Как видно, сначала выполняется первая итерация внешнего цикла, потом — три итерации внутреннего, после его завершения снова в дело вступает внешний цикл, потом опять — внутренний.
|
||
|
||
![[94f7bf0bb554801508dd1a57301fc83a.png]]
|
||
|
||
Вложенные циклы
|
||
|
||
## Обработка содержимого файла
|
||
|
||
Чаще всего вложенные циклы используют для обработки файлов. Так, внешний цикл занимается перебором строк файла, а внутренний уже работает с каждой строкой. Вот, например, как выглядит обработка файла `/etc/passwd`:
|
||
|
||
```
|
||
#!/bin/bash
|
||
IFS=$'\n'
|
||
for entry in $(cat /etc/passwd)
|
||
do
|
||
echo "Values in $entry –"
|
||
IFS=:
|
||
for value in $entry
|
||
do
|
||
echo " $value"
|
||
done
|
||
done
|
||
```
|
||
|
||
В этом скрипте два цикла. Первый проходится по строкам, используя в качестве разделителя знак перевода строки. Внутренний занят разбором строк, поля которых разделены двоеточиями.
|
||
|
||
![[c4dc8f6d9a2c6774baec8ecac895761b.png]]
|
||
|
||
Обработка данных файла
|
||
|
||
Такой подход можно использовать при обработке файлов формата CSV, или любых подобных файлов, записывая, по мере надобности, в переменную окружения `IFS` символ-разделитель.
|
||
|
||
## Управление циклами
|
||
|
||
Возможно, после входа в цикл, нужно будет остановить его при достижении переменной цикла определённого значения, которое не соответствует изначально заданному условию окончания цикла. Надо ли будет в такой ситуации дожидаться нормального завершения цикла? Нет конечно, и в подобных случаях пригодятся следующие две команды:
|
||
|
||
- `break`
|
||
- `continue`
|
||
|
||
## Команда break
|
||
|
||
Эта команда позволяет прервать выполнение цикла. Её можно использовать и для циклов `for`, и для циклов `while`:
|
||
|
||
```
|
||
#!/bin/bash
|
||
for var1 in 1 2 3 4 5 6 7 8 9 10
|
||
do
|
||
if [ $var1 -eq 5 ]
|
||
then
|
||
break
|
||
fi
|
||
echo "Number: $var1"
|
||
done
|
||
```
|
||
|
||
Такой цикл, в обычных условиях, пройдётся по всему списку значений из списка. Однако, в нашем случае, его выполнение будет прервано, когда переменная `$var1` будет равна 5.
|
||
|
||
![[3dcd9a3f50a731102f3fac8bc8f9c9db.png]]
|
||
|
||
Досрочный выход из цикла for
|
||
|
||
Вот — то же самое, но уже для цикла `while`:
|
||
|
||
```
|
||
#!/bin/bash
|
||
var1=1
|
||
while [ $var1 -lt 10 ]
|
||
do
|
||
if [ $var1 -eq 5 ]
|
||
then
|
||
break
|
||
fi
|
||
echo "Iteration: $var1"
|
||
var1=$(( $var1 + 1 ))
|
||
done
|
||
```
|
||
|
||
Команда `break`, исполненная, когда значение `$var1` станет равно 5, прерывает цикл. В консоль выведется то же самое, что и в предыдущем примере.
|
||
|
||
## Команда continue
|
||
|
||
Когда в теле цикла встречается эта команда, текущая итерация завершается досрочно и начинается следующая, при этом выхода из цикла не происходит. Посмотрим на команду `continue` в цикле `for`:
|
||
|
||
```
|
||
#!/bin/bash
|
||
for (( var1 = 1; var1 < 15; var1++ ))
|
||
do
|
||
if [ $var1 -gt 5 ] && [ $var1 -lt 10 ]
|
||
then
|
||
continue
|
||
fi
|
||
echo "Iteration number: $var1"
|
||
done
|
||
```
|
||
|
||
Когда условие внутри цикла выполняется, то есть, когда `$var1` больше 5 и меньше 10, оболочка исполняет команду `continue`. Это приводит к пропуску оставшихся в теле цикла команд и переходу к следующей итерации.
|
||
|
||
![[6e951c537c742d829c1e8b5d7b9abf44.png]]
|
||
|
||
Команда continue в цикле for
|
||
|
||
## Обработка вывода, выполняемого в цикле
|
||
|
||
Данные, выводимые в цикле, можно обработать, либо перенаправив вывод, либо передав их в конвейер. Делается это с помощью добавления команд обработки вывода после инструкции `done`.
|
||
|
||
Например, вместо того, чтобы показывать на экране то, что выводится в цикле, можно записать всё это в файл или передать ещё куда-нибудь:
|
||
|
||
```
|
||
#!/bin/bash
|
||
for (( a = 1; a < 10; a++ ))
|
||
do
|
||
echo "Number is $a"
|
||
done > myfile.txt
|
||
echo "finished."
|
||
```
|
||
|
||
Оболочка создаст файл `myfile.txt` и перенаправит в этот файл вывод конструкции `for`. Откроем файл и удостоверимся в том, что он содержит именно то, что ожидается.
|
||
|
||
![[f8dd885acd2d4bbbbe34b4304ad2e9aa.png]]
|
||
|
||
Перенаправление вывода цикла в файл
|
||
|
||
## Пример: поиск исполняемых файлов
|
||
|
||
Давайте воспользуемся тем, что мы уже разобрали, и напишем что-нибудь полезное. Например, если надо выяснить, какие именно исполняемые файлы доступны в системе, можно просканировать все папки, записанные в переменную окружения `PATH`. Весь арсенал средств, который для этого нужен, у нас уже есть, надо лишь собрать всё это воедино:
|
||
|
||
```
|
||
#!/bin/bash
|
||
IFS=:
|
||
for folder in $PATH
|
||
do
|
||
echo "$folder:"
|
||
for file in $folder/*
|
||
do
|
||
if [ -x $file ]
|
||
then
|
||
echo " $file"
|
||
fi
|
||
done
|
||
done
|
||
```
|
||
|
||
Такой вот скрипт, небольшой и несложный, позволил получить список исполняемых файлов, хранящихся в папках из `PATH`.
|
||
|
||
![[6221751817e7e6b620374e85cf861e0c.png]]
|
||
|
||
Поиск исполняемых файлов в папках из переменной PATH
|
||
|
||
## Итоги
|
||
|
||
Сегодня мы поговорили о циклах `for` и `while` в bash-скриптах, о том, как их запускать, как ими управлять. Теперь вы умеете обрабатывать в циклах строки с разными разделителями, знаете, как перенаправлять данные, выведенные в циклах, в файлы, как просматривать и анализировать содержимое директорий.
|
||
|
||
Если предположить, что вы — разработчик bash-скриптов, который знает о них только то, что изложено в [первой части](https://habrahabr.ru/company/ruvds/blog/325522/) этого цикла статей, и в этой, второй, то вы уже вполне можете написать кое-что полезное. Впереди — третья часть, разобравшись с которой, вы узнаете, как передавать bash-скриптам параметры и ключи командной строки, и что с этим всем делать.
|
||
|
||
Уважаемые читатели! В комментариях к [предыдущему материалу](https://habrahabr.ru/company/ruvds/blog/325522/) вы рассказали нам много интересного. Уверены, всё это окажет неоценимую помощь тем, кто хочет научиться программировать для bash. Но тема эта огромна, поэтому снова просим знатоков поделиться опытом, а новичков — впечатлениями.
|