Основы Bash-скриптинга для непрограммистов. Часть 2

    В первой части статьи мы рассмотрели командные оболочки, профили, синонимы и первые команды. Под спойлером я также рассказал, как развернуть тестовую виртуальную машину.

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

    Скрипты

    Для выполнения нескольких команд одним вызовом удобно использовать скрипты. Скрипт – это текстовый файл, содержащий команды для shell. Это могут быть как внутренние команды shell, так и вызовы внешних исполняемых файлов.

    Как правило, имя файла скрипта имеет окончание .sh, но это не является обязательным требованием и используется лишь для того, чтобы пользователю было удобнее ориентироваться по имени файла. Для интерпретатора более важным является содержимое файла, а также права доступа к нему.

    Перейдем в домашнюю директорию командой cd ~ и создадим в ней с помощью редактора nano (nano script.sh)файл, содержащий 2 строки:

    #!/bin/bash
    echo Hello!

    Чтобы выйти из редактора nano после набора текста скрипта, нужно нажать Ctrl+X, далее на вопрос "Save modified buffer?" нажать Y, далее на запрос "File Name to Write:" нажать Enter. При желании можно использовать любой другой текстовый редактор.

    Скрипт запускается командой ./<имя_файла>, т.е. ./ перед именем файла указывает на то, что нужно выполнить скрипт или исполняемый файл, находящийся в текущей директории. Если выполнить команду script.sh, то будет выдана ошибка, т.к. оболочка будет искать файл в директориях, указанных в переменной среды PATH, а также среди встроенных команд (таких, как, например, pwd):

    test@osboxes:~$ script.sh
    script.sh: command not found

    Ошибки не будет, если выполнять скрипт с указанием абсолютного пути, но данный подход является менее универсальным: /home/user/script.sh. Однако на данном этапе при попытке выполнить созданный файл будет выдана ошибка:

    test@osboxes:~$ ./script.sh
    -bash: ./script.sh: Permission denied

    Проверим права доступа к файлу:

    test@osboxes:~$ ls -l script.sh
    -rw-rw-r-- 1 test test 22 Nov  9 05:27 script.sh

    Из вывода команды ls видно, что отсутствуют права на выполнение. Рассмотрим подробнее на картинке:

    Права доступа задаются тремя наборами: для пользователя, которому принадлежит файл; для группы, в которую входит пользователь; и для всех остальных. Здесь r, w и x означают соответственно доступ на чтение, запись и выполнение.

    В нашем примере пользователь (test) имеет доступ на чтение и запись, группа также имеет доступ на чтение и запись, все остальные – только на чтение. Эти права выданы в соответствии с правами, заданными по умолчанию, которые можно проверить командой umask -S. Изменить права по умолчанию можно, добавив вызов команды umask с нужными параметрами в файл профиля пользователя (файл ~/.profile), либо для всех пользователей в общесистемный профиль (файл /etc/profile).

    Для того, чтобы установить права, используется команда chmod <параметры> <имя_файла>. Например, чтобы выдать права на выполнение файла всем пользователям, нужно выполнить команду:

    test@osboxes:~$ chmod a+x script.sh

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

    test@osboxes:~$ chmod ug+rx script.sh

    Чтобы запретить доступ на запись (изменение содержимого) файла всем:

    test@osboxes:~$ chmod a-w script.sh

    Также для указания прав можно использовать маску. Например, чтобы разрешить права на чтение, запись, выполнение пользователю, чтение и выполнение группе, и чтение – для остальных, нужно выполнить:

    test@osboxes:~$ chmod 754 script.sh

    Будут выданы права -rwxr-xr--:

    test@osboxes:~$ ls -la script.sh
    -rwxr-xr-- 1 test test 22 Nov  9 05:27 script.sh

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

    Символ перед наборами прав доступа указывает на тип файла ( означает обычный файл, d – директория, l – ссылка, c – символьное устройство, b – блочное устройство, и т. д.). Соответствие числа, его двоичного представления и прав доступ можно представить в виде таблицы:

    Число

    Двоичный вид

    Права доступа

    0

    000

    Нет прав

    1

    001

    Только выполнение (x)

    2

    010

    Только запись (w)

    3

    011

    Запись и выполнение (wx)

    4

    100

    Только чтение (r)

    5

    101

    Чтение и выполнение (rx)

    6

    110

    Чтение и запись (rw)

    7

    111

    Чтение, запись и выполнение (rwx)

    Выдав права на выполнение, можно выполнить скрипт:

    test@osboxes:~$ ./script.sh
    Hello!

    Первая строка в скрипте содержит текст #!/bin/bash. Пара символов #! называется Шеба́нг (англ. shebang) и используется для указания интерпретатору, с помощью какой оболочки выполнять указанный скрипт. Это гарантирует корректность исполнения скрипта в нужной оболочке в случае, если у пользователя будет указана другая.

    Также в скриптах можно встретить строку #!/bin/sh. Но, как правило, /bin/sh является ссылкой на конкретный shell, и в нашем случае /bin/sh ссылается на /bin/dash, поэтому лучше явно указывать необходимый интерпретатор. Вторая строка содержит команду echo Hello!, результат работы которой мы видим в приведенном выводе.

    Параметры скриптов

    Для того, чтобы обеспечить некоторую универсальность, существует возможность при вызове передавать скрипту параметры. В этом случае вызов скрипта будет выглядеть так: <имя_скрипта> <параметр1> <параметр2> …, например ./script1.sh Moscow Russia.

    Для того, чтобы получить значение первого параметра, необходимо в скрипте указать $1, второго - $2, и т.д. Существует также ряд других переменных, значения которых можно использовать в скрипте:
    $0 – имя скрипта
    $# – количество переданных параметров
    $$ – PID(идентификатор) процесса, выполняющего скрипт
    $? – код завершения предыдущей команды

    Создадим файл script1.sh следующего содержания:

    #!/bin/bash
    echo Hello, $USER!
    printf "Specified City is: %s, Country is: %s\n" $1 $2

    Выдадим права на выполнение и выполним скрипт с параметрами:

    test@osboxes:~$ chmod u+x script1.sh
    test@osboxes:~$ ./script1.sh Moscow Russia
    Hello, test!
    Specified City is: Moscow, Country is: Russia

    Мы передали 2 параметра, указывающие город и страну, и использовали их в скрипте, чтобы сформировать строку, выводимую командой printf. Также для вывода в строке Hello использовали имя пользователя из переменной USER.

    Для того, чтобы передать значения параметров, состоящие из нескольких слов (содержащие пробелы), нужно заключить их в кавычки:

    test@osboxes:~$ ./script1.sh "San Francisco" "United States"
    Hello, test!
    Specified City is: San Francisco, Country is: United States

    При этом нужно доработать скрипт, чтобы в команду printf параметры также передавались в кавычках:

    printf "Specified City is: %s, Country is: %s\n" "$1" "$2"

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

    COUNTRY=RUSSIA
    echo $COUNTRY

    Операторы условного выполнения, выбора и циклы

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

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

    if [ <условие> ]
    then
      <команда1>
    else
      <команда2>
    fi

    Создадим скрипт, проверяющий длину введенной строки (например, для проверки длины пароля), которая должна быть не меньше (т.е. больше) 8 символов:

    #!/bin/bash
    echo Hello, $USER!
    echo -n "Enter string: "
    read str
    if [ ${#str} -lt 8 ]
    then
      echo String is too short
    else
      echo String is ok
    fi

    Выполним 2 теста, с длиной строки 5 и 8 символов:

    test@osboxes:~$ ./script2.sh
    Hello, test!
    Enter string: abcde
    String is too short
    test@osboxes:~$ ./script2.sh
    Hello, test!
    Enter string: abcdefgh
    String is ok

    Командой read str мы получаем значение, введенное пользователем и сохраняем его в переменную str. С помощью выражения ${#str} мы получаем длину строки в переменной str и сравниваем её с 8. Если длина строки меньше, чем 8 (-lt 8), то выдаем сообщение «String is too short», иначе – «String is ok».

    Условия можно комбинировать, например, чтобы указать, чтоб длина должна быть не меньше восьми 8 и не больше 16 символов, для условия некорректных строк нужно использовать выражение [ ${#str} -lt 8 ] || [ ${#str} -gt 16 ]. Здесь || означает логическое "ИЛИ", а для логического "И" в bash используется &&.

    Условия также могут быть вложенными:

    #!/bin/bash
    echo Hello, $USER!
    echo -n "Enter string: "
    read str
    if [ ${#str} -lt 8 ]
    then
      echo String is too short
    else
      if [ ${#str} -gt 16 ]
      then
        echo String is too long
      else
        echo String is ok
      fi
    fi

    Здесь мы сначала проверяем, что строка меньше 8 символов, отсекая минимальные значения, и выводим "String is too short", если условие выполняется. Если условие не выполняется(строка не меньше 8 символов) - идем дальше(первый else) и проверяем, что строка больше 16 символов. Если условие выполняется - выводим "String is too long", если не выполняется(второй else) - выводим "String is ok".

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

    test@osboxes:~$ ./script2.sh
    Hello, test!
    Enter string: abcdef
    String is too short
    test@osboxes:~$ ./script2.sh
    Hello, test!
    Enter string: abcdefghijklmnopqrstuv
    String is too long
    test@osboxes:~$ ./script2.sh
    Hello, test!
    Enter string: abcdefghijkl
    String is ok

    Оператор выбора выглядит следующим образом:

    case "$переменная" in
     "$значение1" )
     <команда1>;;
     "$значение2" )
     <команда2>;;
    esac

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

    #!/bin/bash
    echo -n "Enter the name of planet: "
    read PLANET
    echo -n "The $PLANET has "
    case $PLANET in
      Mercury | Venus ) echo -n "no";;
      Earth ) echo -n "one";;
      Mars ) echo -n "two";;
      Jupiter ) echo -n "79";;
      *) echo -n "an unknown number of";;
    esac
    echo " satellite(s)."

    Тест:

    test@osboxes:~$ ./script3.sh
    Enter the name of planet: Mercury
    The Mercury has no satellite(s).
    test@osboxes:~$ ./script3.sh
    Enter the name of planet: Venus
    The Venus has no satellite(s).
    test@osboxes:~$ ./script3.sh
    Enter the name of planet: Earth
    The Earth has one satellite(s).
    test@osboxes:~$ ./script3.sh
    Enter the name of planet: Mars
    The Mars has two satellite(s).
    test@osboxes:~$ ./script3.sh
    Enter the name of planet: Jupiter
    The Jupiter has 79 satellite(s).
    test@osboxes:~$ ./script3.sh
    Enter the name of planet: Alpha555
    The Alpha555 has an unknown number of satellite(s).

    Здесь в зависимости от введенного названия планеты скрипт выводит количество её спутников.
    В case мы использовали выражение Mercury | Venus, где | означает логическое "ИЛИ" (в отличие от if, где используется ||), чтобы выводить "no" для Меркурия и Венеры, не имеющих спутников. В case также можно указывать диапазоны с помощью []. Например, скрипт для проверки принадлежности диапазону введенного символа будет выглядеть так:

    #!/bin/bash
    
    echo -n "Enter key: "
    read -n 1 key
    echo
    case "$key" in
      [a-z]   ) echo "Lowercase";;
      [A-Z]   ) echo "Uppercase";;
      [0-9]   ) echo "Digit";;
      *       ) echo "Something else";;
    esac

    Мы проверяем символ на принадлежность одному из четырех диапазонов(английские символы в нижнем регистре, английские символы в верхнем регистре, цифры, все остальные символы). Результат теста:

    test@osboxes:~$ ./a.sh
    Enter key: t
    Lowercase
    test@osboxes:~$ ./a.sh
    Enter key: P
    Uppercase
    test@osboxes:~$ ./a.sh
    Enter key: 5
    Digit
    test@osboxes:~$ ./a.sh
    Enter key: @
    Something else

    Цикл может задаваться тремя разными способами:

    Выполняется в интервале указанных значений (либо указанного множества):

    for [ <условие> ] do <команды> done

    Выполняется, пока соблюдается условие:

    while [ <условие> ] do <команды> done

    Выполняется, пока не начнёт соблюдаться условие:

    until [ <условие> ] do <команды> done

    Добавим в скрипт с планетами цикл с условием while и будем выходить из скрипта, если вместо имени планеты будет введено EXIT

    #!/bin/bash
    PLANET="-"
    while [ $PLANET != "EXIT" ]
    do
      echo -n "Enter the name of planet: "
      read PLANET
      if [ $PLANET != "EXIT" ]
      then
        echo -n "The $PLANET has "
        case $PLANET in
          Mercury | Venus ) echo -n "no";;
          Earth ) echo -n "one";;
          Mars ) echo -n "two";;
          Jupiter ) echo -n "79";;
          *) echo -n "an unknown number of";;
        esac
      echo " satellite(s)."
      fi
    done

    Здесь мы также добавили условие, при котором оператор выбора будет выполняться только в случае, если введено не EXIT. Таким образом, мы будем запрашивать имя планеты и выводить количество её спутников до тех пор, пока не будет введено EXIT:

    test@osboxes:~$ ./script4.sh
    Enter the name of planet: Earth
    The Earth has one satellite(s).
    Enter the name of planet: Jupiter
    The Jupiter has 79 satellite(s).
    Enter the name of planet: Planet123
    The Planet123 has an unknown number of satellite(s).
    Enter the name of planet: EXIT

    Нужно отметить, что условие while [ $PLANET != "EXIT" ] можно заменить на until [ $PLANET == "EXIT" ]. == означает "равно", != означает "не равно".

    Приведем пример циклов с указанием интервалов и множеств:

    #!/bin/bash
    
    rm *.dat
    
    echo -n "File count: "
    read count
    
    for (( i=1; i<=$count; i++ ))
    do
      head -c ${i}M </dev/urandom >myfile${i}mb.dat
    done
    ls -l *.dat
    
    echo -n "Delete file greater than (mb): "
    read maxsize
    
    for f in *.dat
    do
      size=$(( $(stat -c %s $f) /1024/1024))
      if [ $size -gt $maxsize ]
      then
        rm $f
        echo Deleted file $f
      fi
    done
    ls -l *.dat
    
    read

    Сначала мы запрашиваем у пользователя количество файлов, которые необходимо сгенерировать (read count).

    В первом цикле (for (( i=1; i<=$count; i++ ))) мы генерируем несколько файлов, количество которых задано в переменной count, которую введет пользователь. В команду head передаем количество мегабайт, считываемых из устройства /dev/random, чтение из которого позволяет получать случайные байты.

    Символ < указывает перенаправление входного потока (/dev/urandom) для команды head.

    Символ > указывает перенаправление выходного потока (вывод команды head -c ${i}M ) в файл, имя которого мы генерируем на основе постоянной строки с добавлением в неё значения переменной цикла (myfile${i}mb.dat).

    Далее мы запрашиваем размер, файлы больше которого необходимо удалить.

    Во втором цикле (for f in *.dat) мы перебираем все файлы .dat в текущей директории и сравниваем размер каждого файла со значением, введенным пользователем. В случае, если размер файла больше, мы удаляем этот файл.

    В конце скрипта выводим список файлов .dat, чтобы отобразить список оставшихся файлов (ls -l *.dat). Результаты теста:

    test@osboxes:~$ ./script5.sh
    File count: 10
    -rw-rw-r-- 1 test test 10485760 Nov  9 08:48 myfile10mb.dat
    -rw-rw-r-- 1 test test  1048576 Nov  9 08:48 myfile1mb.dat
    -rw-rw-r-- 1 test test  2097152 Nov  9 08:48 myfile2mb.dat
    -rw-rw-r-- 1 test test  3145728 Nov  9 08:48 myfile3mb.dat
    -rw-rw-r-- 1 test test  4194304 Nov  9 08:48 myfile4mb.dat
    -rw-rw-r-- 1 test test  5242880 Nov  9 08:48 myfile5mb.dat
    -rw-rw-r-- 1 test test  6291456 Nov  9 08:48 myfile6mb.dat
    -rw-rw-r-- 1 test test  7340032 Nov  9 08:48 myfile7mb.dat
    -rw-rw-r-- 1 test test  8388608 Nov  9 08:48 myfile8mb.dat
    -rw-rw-r-- 1 test test  9437184 Nov  9 08:48 myfile9mb.dat
    Delete file greater than (mb): 5
    Deleted file myfile10mb.dat
    Deleted file myfile6mb.dat
    Deleted file myfile7mb.dat
    Deleted file myfile8mb.dat
    Deleted file myfile9mb.dat
    -rw-rw-r-- 1 test test 1048576 Nov  9 08:48 myfile1mb.dat
    -rw-rw-r-- 1 test test 2097152 Nov  9 08:48 myfile2mb.dat
    -rw-rw-r-- 1 test test 3145728 Nov  9 08:48 myfile3mb.dat
    -rw-rw-r-- 1 test test 4194304 Nov  9 08:48 myfile4mb.dat
    -rw-rw-r-- 1 test test 5242880 Nov  9 08:48 myfile5mb.dat

    Мы создали 10 файлов (myfile1mb.dat .. myfile10mb.dat) размером от 1 до 10 мегабайт и далее удалили все файлы .dat размером больше 5 мегабайт. При этом для каждого удаляемого файла вывели сообщение о его удалении (Deleted file myfile10mb.dat). В конце вывели список оставшихся файлов (myfile1mb.dat .. myfile5mb.dat).

    В следующей части мы рассмотрим функции, планировщик заданий cron, а также различные полезные команды.

    Комментарии 10

      +2
      Я извиняюсь, но есть нюанс
      в правах на файл по POSIX — owner, group, others
      И никак не user
      буква используется «u», но называется «owner» и эти слова стандарт различает.
      вот пример, описание sys/stat.h из стандарта
      https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/sys_stat.h.html
        0
        Здравствуйте! Спасибо за комментарий, Вы правы. Но, надеюсь, в том виде, в котором я рассказал, также понятно. Ведь в в большинстве случает user=owner.
        –1

        ИМХО, такие статьи надо начинать так: "Язык bash, с уродливым синтаксисом, подталкивающий вас делать ошибки, и лишь по несчастливой случайности ставший самым распространенным языком для shell скриптов..."

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

            Исправить на «Выполняется, пока не начнёт соблюдаться условие:»

              0
              Спасибо! Исправил.
              0
                if [ $PLANET != "EXIT" ]
                then.

              А в чём назначение точки? Или это опечатка?
                0
                Опечатка. Исправил, спасибо!
                  0
                  Тогда ещё поправьте:
                  do
                    size=$(( $(stat -c %s $f) /1024/1024))
                    if [ $size -gt $maxsize ]
                    then.

                  И спасибо за всю серию статей!
                    0
                    Поправил, благодарю!

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое