Последовательное выполнение команд

Основная цель написания сценариев — автоматизация действий пользователя по выполнению часто повторяемых задач. Эти задачи могут иметь отношение к администрированию системы, обработке текстовых файлов, преобразованию видеоформатов и пр. В любом случае перед автором сценария стоит задача упрощения работы, выполняемой пользователем. 

В простейшем случае можно считать, что сценарий — это последовательность команд системы. Такую последовательность можно выполнить и без написания сценария, достаточно написать их в командной строке, разделяя точкой с запятой:

$date ; ls
Thu Nov 13 22:06:12 MSK 2008
calc data2.dat program.c
data.dat exams.tex text.txt

Более интересна возможность сохранения последовательности команд в текстовом файле (обычно имеющем расширение .sh), который потом можно было бы многократно запускать. Такой файл мог бы выглядеть следующим образом:

#!/bin/bash
#Call two commands
date
ls

В первой строке сценария указывается интерпретатор сценария. В нашем случае это Bourne Again Shell (bash). Строки, начинающиеся с #, являются комментариями. Третья и четвертая строки — собственно последовательность исполняемых команд. Предположим, что сценарий сохранен в файле script.sh. Чтобы этот сценарий можно было бы запустить, ему нужно дать права на выполнение:

$chmod a+x script1.sh
$./script1.sh
Thu Nov 13 22:11:02 MSK 2008
calc data2.dat program.c text.txt
data.dat exams.tex script1.sh
$

Обратите внимание на команду запуска сценария. В этом примере он запускается из каталога, в котором находится, поэтому явно прописывается ссылка на текущий каталог. Если в командной строке написать только имя сценария, то командный интерпретатор будет искать его в каталогах, указанных в переменной окружения PATH.

Иногда в сценариях требуется вывести какой-нибудь текст. Для этого можно использовать команду echo:

$echo This is a test
This is a test
$

Если текст содержит специальные символы, то текст нужно заключать в кавычки, причем неважно, одинарные или двойные:

$echo "This is a test to see if you.re paying attention"
This is a test to see if you.re paying attention
$echo 'Rich says "scripting is easy".'
Rich says "scripting is easy".
$

Эту же команду можно добавить в сценарий для вывода дополнительной информации:

#!/bin/bash
#Call two commands
echo The time and date are:
date
echo And these are files in current directory:
ls

Результат работы этого сценария будет следующим:

$./script1.sh
The time and date are:
Thu Nov 13 22:23:05 MSK 2008
And these are files in current directory:
calc data2.dat program.c text.txt
data.dat exams.tex script1.sh
$

Команда echo по умолчанию после вывода строки переходит на новую строку. Ключ -n позволяет изменить это поведение. Первый ее вызов можно исправить так:

echo -n "The time and date are: "

Тогда в результате работы сценария будет выведено следующее:

$./script1.sh
The time and date are: Thu Nov 13 23:06:30 MSK 2008
And these are files in current directory:
calc data2.dat program.c text.txt
data.dat exams.tex script1.sh
$

Использование переменных

В сценариях можно использовать два вида переменных: переменные окружения и переменные, определенные пользователем. Узнать значения всех переменные окружения можно, выполнив команду env:

$ env
BACKSPACE=Delete
BASH=/bin/bash
EUID=1000
HOME=/home/bravit
HOSTNAME=edu
HOSTTYPE=i586
LANG=en
LANGUAGE=en US:en
LINES=24
LOGNAME=bravit
...

Внутри сценария получить значения этих переменных можно, поставив перед их именем знак $:

#!/bin/bash
#Using variables
echo "Home directory: $HOME"
$./script2.sh
Home directory: /home/bravit
$

Обратите, что здесь обращение к переменной происходит внутри строки, тем не менее, ее значение подставляется правильно. Если знак $ нужно вывести на консоль, его нужно предварить обратным слешем:

$ echo "The cost of the item is \$15"
The cost of the item is $15

Значения пользовательских переменных присваиваются непосредственно в тексте сценария:

#!/bin/bash
# testing variables
days=10
guest="Katie"
echo "$guest checked in $days days ago"
days=5
guest="Jessica"
echo "$guest checked in $days days ago"

Результат выполнения:

$./script3.sh
Katie checked in 10 days ago
Jessica checked in 5 days ago
$

Обратите внимание на то, что знак = должен быть написан без пробелов слева и справа. В противном случае первое слово будет восприниматься как команда.

Присваивание значения одной переменной другой будет выглядеть так:

var2=$var1

Знак $ здесь появляется только в правой части, поскольку именно там происходит чтения значения переменной var1.

Переменной можно присвоить результат вывода любой команды. Делается это с помощью оператора «обратные кавычки»:

$cat script4.sh
#!/bin/bash
# testing variables
current_time=`date`
echo "Current time is $current_time"
$./script4.sh
Current time is Thu Nov 13 23:43:00 MSK 2008
$

Приведем более осмысленный пример использования этого оператора. Предположим, что необходимо регулярно сохранять список файлов каталога в файл, в имя которого входит текущее время и дата. В этом случае имя файла должно формироваться на основе вывода команды date:

#!/bin/bash
# Using backtick
today=`date +%y%m%d`
ls > log.$today

После выполнения такого сценария в текущем каталоге появится файл, имя которого будет похоже на "log.081113". Команда date с ключом +%y%m%d выводит две цифры номера года, две цифры номера месяца и две цифры дня:

$date +%y%m%d
081113 

Чтение параметров командной строки

Параметры, необходимые для работы сценария, удобно задавать при его запуске в командной строке. Доступ к этим параметрам внутри сценария выполняется с помощью специальных переменных с именами $1 (первый параметр), $2 (второй параметр) и т.д. Переменная с именем $# содержит общее количество заданных параметров, а переменная $0 — имя сценария. Предположим, к примеру, что сценарий (params.sh) имеет следующий вид:

#!/bin/sh
echo "Сценарий: $0"
echo "Всего параметров: $#"
echo "Первый параметр: $1"
echo "Второй параметр: $2"

Тогда при его запуске с указанием двух параметров получим следующий результат:

$ ./params.sh first second
Сценарий: ./params.sh
Всего параметров: 2
Первый параметр: first
Второй параметр: second

Чтобы включить в параметр символ пробела, весь его текст следует заключить в кавычки, например:

$ ./params.sh "first second"
Сценарий: ./params.sh
Всего параметров: 1
Первый параметр: first second
Второй параметр:

Арифметические вычисления

Для вычислений с целыми значениями используется синтаксис с двойными круглыми скобками. Посмотрите пример с его использованием:

#!/bin/bash
var1=100
var2=50
var3=45
var4=$(( $var1 * ($var2 - $var3) ))
echo The final result is $var4

В результате выполнения получаем:

$./script6.sh
The final result is 500
$

К сожалению, для вещественной арифметики этот способ не подходит. При необходимости проведения в сценариях вычислений с вещественными числами, нужно пользоваться командой bc.

Ранее для целочисленной арифметики в bash использовался синтаксис с квадратными скобками (они применялись вместо двойных круглых). Однако сейчас он признан устаревшим и планируется к исключению.

Условные операторы и циклы

Условный оператор

В простейшей форме условный оператор выглядит так:

if command
then
 commands
fi

В отличие от традиционных языков программирования после слова if здесь пишется не условие, а команда, причем анализируется код завершения этой команды. Если код завершения равен нулю, то содержимое условного оператора выполняется, а если не равен, то не выполняется.

Сравните два примера:

$cat test1
#!/bin/bash
if date
then
 echo It works!
fi
$./test1
Fri Nov 14 09:56:48 MSK 2008
It works!
$

Здесь команда date выполняется успешно и возвращает нулевое значение, поэтому выполняется и действие внутри условного оператора.

$cat test1a
#!/bin/bash
if unexisted_command
then
 echo It works!
fi
echo if is over!
$./test1a
./test1a: line 2: unexisted_command: command not found
if is over!
$

А в этом примере тело условного оператора не выполнилось.

Некоторые команды возвращают ненулевое значение при неудачном выполнении. Например, команда grep, которая ищет строки в файлах по заданному критерию, возвращает ненулевое значение, если ни одной строки не найдено. Таким образом можно в зависимости от наличия строки в файле выполнять или не выполнять некоторые действия.

$cat test2
#!/bin/bash
# testing multiple commands in the then section
testuser=bravit
if grep $testuser /etc/passwd
then
 echo The bash files for user $testuser are:
 ls -a /home/$testuser/.b*
fi
$./test2
bravit:*:1001:0:Vitaly Bragilevsky:/home/bravit:/bin/csh
The bash files for user bravit are:
/home/bravit/.bash_history /home/bravit/.bashrc
$

Если переменной testuser присвоить имя несуществующего пользователя, то выведено ничего не будет:

$cat test2
#!/bin/bash
# testing multiple commands in the then section
testuser=unexisted
if grep $testuser /etc/passwd
then
 echo The bash files for user $testuser are:
 ls -a /home/$testuser/.b*
fi
$./test2
$

Модифицируем сценарий так, чтобы при отсутствии пользователя выводилось соответствующее сообщение. Для этого нам понадобится вторая форма условного оператора:

if command
then
 commands
else
 commands
fi

Исправленный сценарий и результат его выполнения будут выглядеть так:

$cat test2
#!/bin/bash
# testing multiple commands in the then section
testuser=unexisted
if grep $testuser /etc/passwd
then
 echo The bash files for user $testuser are:
 ls -a /home/$testuser/.b*
else
 echo "User $testuser is not registered"
fi
$./test2
User unexisted is not registered
$

Условные операторы можно вкладывать один в другой, проверяя различные условия:

if command1
then
 command set 1
elif command2
then
 command set 2
elif command3
then
 command set 3
elif command4
then
 command set 4
else
 commands
fi

Ветка else может отсутствовать.

Проверка условий

Есть еще один вариант синтаксиса условного оператора с возможностью проверки условий.

if [ condition ]
then
 commands
fi

Обратите внимание на пробелы до и после условия condition, при их отсутствии сценарий не будет правильно интерпретироваться.

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

  • числовые (только для целых чисел);
  • строковые (сравнение строк);
  • файловые (существование, права доступа и пр.).

Рассмотрим пример со сравнением чисел:

$ cat test5
#!/bin/bash
# using numeric test comparisons
val1=10
val2=11
if [ $val1 -gt 5 ]
then
 echo "The test value $val1 is greater than 5"
fi
if [ $val1 -eq $val2 ]
then
 echo "The values are equal"
else
 echo "The values are different"
fi

Операторы сравнения обозначаются здесь -gt и -eq. Первый означает «больше», а второй «равно». В следующей таблице приведены все операторы сравнения целых чисел.

Операция Описание
n1 -eq n2 n1 равно n2 (equal)
n1 -ge n2 n1 больше или равно n2 (greater or equal)
n1 -gt n2 n1 больше n2 (greater than)
n1 -le n2 n1 меньше или равно n2 (less or equal)
n1 -lt n2 n1 меньше n2 (less than)
n1 -ne n2 n1 не равно n2 (not equal)

При сравнении строк операторы выглядят более традиционно:

$ cat test5
#!/bin/bash
# using string test comparisons
# testing string equality
testuser=rich
if [ $USER = $testuser ]
then
 echo "Welcome $testuser"
fi
$ ./test7
Welcome rich
$

Все операторы сравнения строк перечислены в таблице:

ОперацияОписание
str1 = str2Строки равны
str1 != str2Строки не равны
str1 < str2Строкаstr1 меньше строки str2
str1 > str2Строка str1 больше строки str2
-n str1Строка str1 имеет ненулевую длину
-z str1Строка str1 имеет нулевую длину

Оператор > необходимо экранировать обратным слешем, иначе он будет считаться символом перенаправления ввода-вывода:

$ cat test9
#!/bin/bash
# using string comparisons
val1=baseball
val2=hockey
if [ $val1 \> $val2 ]
then
 echo "$val1 is greater than $val2"
else
 echo "$val1 is less than $val2"
fi
$./test9
baseball is less than hockey
$

В сравнении строк используется лексикографический порядок с учетом регистра символов.

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

ОперацияОписание
-d fileФайл с именем file является каталогом
-e fileФайл существует
-f fileФайл существует и является регулярным файлом
-r fileФайл существует, и у сценария есть права на его чтение
-s fileФайл существует и не пуст.
-w fileФайл существует, и у сценария есть права на его запись
-x fileФайл существует и является исполняемым
-O fileФайл существует и принадлежит текущему пользователю
-G file Файл существует и его группа совпадает с группой пользователя
file1 -nt file2 file1 новее, чем file2
file1 -ot file2 file1 старее, чем file2

В следующем сценарии проверяется существование и возможность чтения файла паролей:

$cat test6
#!/bin/bash
# testing if you can read a file
pwfile=/etc/shadow
# first, test if the file exists, and is a file
if [ -f $pwfile ]
then
 # now test if you can read it
 if [ -r $pwfile ]
 then
 tail $pwfile
 else
 echo "Sorry, I’m unable to read the $pwfile file"
 fi
else
 echo "Sorry, the file $file doesn’t exist"
fi
$./test6
Sorry, I’m unable to read the /etc/shadow file
$

В еще одном примере проверяется, можно ли запустить файл, и он запускается:

$cat test7
#!/bin/bash
# testing file execution
if [ -x test6 ]
then
 echo "You can run the script:"
 ./test6
else
 echo "Sorry, you are unable to execute the script"
fi

Условия можно объединять с помощью логических операций:

    
    [ condition1 ] && [ condition2 ]
    [ condition1 ] || [ condition2 ]

Например:

$cat test8
#!/bin/bash
# testing compound comparisons
if [ -d $HOME ] && [ -w $HOME/testing ]
then
 echo "The file exists and you can write to it"
else
 echo "I can’t write to the file"
fi
$ ./test8
I can’t write to the file
$ touch $HOME/testing
$./test8
The file exists and you can write to it
$

Команда case

Оператор case используется для выбора одного из нескольких вариантов:

case variable in
pattern1 | pattern2) commands1;;
pattern3) commands2;;
*) default commands;;
esac

Каждый вариант заканчивается закрывающей скобкой, вертикальная черта означает "или", конец набора команд для одного варианта заканчивается двумя точками с запятой.

$ cat test9
#!/bin/bash
# using the case command
case $USER in
rich | barbara)
 echo "Welcome, $USER"
 echo "Please enjoy your visit";;
testing)
 echo "Special testing account";;
jessica)
 echo "Don’t forget to log off when you’re done";;
*)
 echo "Sorry, you’re not allowed here";;
esac
$ ./test9
Welcome, rich
Please enjoy your visit
$

Действия, аналогичные оператору case, можно выполнить и средствами условного оператора, однако считается, что синтаксис case является более наглядным.

Цикл for

В программах часто требуется перебирать наборы данных, выполняя для каждого из них некоторые действия. Такими наборами данных могут быть список всех файлов каталога, список всех пользователей системы, все строки файла. Соответствующая конструкция в языках программирования называется циклом.

Общий вид цикла for для языка сценариев bash выглядит так:

for var in list
do
 commands
done

Параметром list задается набор перебираемых данных. На каждом шаге цикла переменная var принимает значение одного из элементов списка, и для каждого такого значения выполняются команды, написанные между do и done. Рассмотрим разные способы задания набора перебираемых данных.

  1. Самый простой способ задания списка — непосредственное перечисление его элементов.
            $cat test1
            #!/bin/bash
            for student in Ivanov Petrov Sidorov
            do
             echo "Student is $student"
            done
            $./test1
            Student is Ivanov
            Student is Petrov
            Student is Sidorov
            $
    

    Если отдельные элементы списка содержат пробелы, их нужно записывать с помощью кавычек.

            $cat test1b
            #!/bin/bash
            for student in "Ivanov I.I." "Petrov P.P." "Sidorov S.S."
            do
             echo "Student is $student"
            done
            $./test1b
            Student is Ivanov I.I.
            Student is Petrov P.P.
            Student is Sidorov S.S.
            $
    

    Обратите внимание, что после завершения цикла переменная student остается доступной, ее значение совпадает с последним элементом списка.

  2. Список переменных может задаваться переменной.
            $cat test2
            #!/bin/bash
            students="Ivanov Petrov Sidorov"
            students=$students" Kozlov"
            
            for student in $students
            do
             echo "Student is $student"
            done
            $./test2
            Student is Ivanov
            Student is Petrov
            Student is Sidorov
            Student is Kozlov
            $
    

    Заметьте, каким образом здесь проведено сцепление (конкатенация) строк.

  3. Список переменных может являться результатом выполнения команды. Предположим, что в текущем каталоге есть файл names:
            $cat names
            Ivanov
            Petrov
            Sidorov
            $
    

    В следующем сценарии все строки этого файла перебираются в цикле:

            $cat test3
            #!/bin/bash
            file=names
            for student in `cat $file`
            do
             echo "Student is $student"
            done
            $./test3
            Student is Ivanov
            Student is Petrov
            Student is Sidorov
            $
    

    Если бы строки файла содержали пробелы, то каждая их часть перебиралась бы отдельно. Это связано с текущим символом-разделителем. По умолчанию, в качестве разделителя выступают пробелы, символы табуляции и переводы на новую строку, однако значение символа-разделителя можно изменить. Предположим, что файл с именами студентов выглядит так:

            $cat names2
            Ivanov I.I.
            Petrov P.P.
            Sidorov S.S.
            $
    

    Следующий сценарий устанавливает в качестве разделителя только перевод строки:

            $cat test4
            #!/bin/bash
            IFS=$'\n'
            file=names2
            for student in `cat $file`
            do
             echo "Student is $student"
            done
            $./test4
            Student is Ivanov I.I.
            Student is Petrov P.P.
            Student is Sidorov S.S.
            $
    

    Можно также установить сразу несколько разделителей:

            IFS=$'\n':;"
    
  4. Наконец, список значений может быть результатом использования шаблонных символов в именах файлов.
            $cat test5
            #!/bin/bash
            for file in /etc/*
            do
             echo "File is $file"
            done
            $./test5
            File is /etc/X11
            File is /etc/aliases
            File is /etc/amd.map
            File is /etc/apmd.conf
            File is /etc/auth.conf
            File is /etc/bluetooth
            File is /etc/crontab
            ...
    

    В списке может указываться сразу несколько имен:

            for file in /dir1/* /dir2/* /dir3/*
            do
             echo "File is $file"
            done 
    

Циклы while и until

В простейшем варианте цикл while выглядит так:

while [ condition ]
do
 other commands
done

Например,

$ cat test6
#!/bin/bash
# while command test
var1=10
while [ $var1 -gt 0 ]
do
 echo $var1
 var1=$(( $var1 - 1 ))
done
$ ./test6
10
9
8
7
6
5
4
3
2
1
$

Команда until имеет следующий вид:

until [ condition ]
do
 commands
done

Например,

$ cat test12
#!/bin/bash
# using the until command
var1=100
until [ $var1 -eq 0 ]
do
 echo $var1
 var1=$(( $var1 - 25 ))
done
$ ./test12
100
75
50
25

Смысл проверяемого командой until условия следующий: цикл выполняется, пока условие не станет истинным.

Последнее изменение: пятница, 22 октября 2021, 22:21