Ответы

Общая информация

Упражнение 1-1. Перевод чисел из BIN в HEX
* 10100110100110 = 0010 1001 1010 0110 = 2 9 A 6 = 29A6

* 1011000111010100010011 = 0010 1100 0111 0101 0001 0011 = 2 C 7 5 1 3 = 2C7513

* 1111101110001001010100110000000110101101 = 1111 1011 1000 1001 0101 0011 0000 0001\
 1010 1101 = F B 8 9 5 3 0 1 A D = FB895301AD
Упражнение 1-2. Перевод чисел из HEX в BIN
* FF00AB02 = F F 0 0 A B 0 2 = 1111 1111 0000 0000 1010 1011 0000 0010 = 11111111000\
000001010101100000010

* 7854AC1 = 7 8 5 4 A C 1 = 0111 1000 0101 0100 1010 1100 0001 = 1111000010101001010\
11000001

* 1E5340ACB38 = 1 E 5 3 4 0 A C B 3 8 = 0001 1110 0101 0011 0100 0000 1010 1100 1011\
 0011 1000 = 11110010100110100000010101100101100111000

Командный интерпретатор Bash

Упражнение 2-1. Шаблоны поиска

Правильный ответ: README.md.

Строка 00_README.txt не подходит. Согласно шаблону *ME.??, после точки идут два символа. В строке 00_README.txt их три.

В строке README нет точки. Поэтому она тоже не подходит.

Упражнение 2-2. Шаблоны поиска

Шаблону поиска */doc?openssl* соответствуют три строки:

  • /usr/share/doc/openssl/IPAddressChoice_new.html
  • /usr/share/doc_openssl/IPAddressChoice_new.html
  • /doc/openssl

Строка doc/openssl не подходит. В ней нет символа / перед doc.

Упражнение 2-3. Поиск файлов утилитой find

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

find /usr -name "*.txt"

Текстовые файлы хранятся только в /usr. Поэтому нет смысла проверять остальные системные каталоги.

Подсчитаем число строк в найденных файлах. Для этого добавим действие с вызовом утилиты wc:

find /usr -name "*.txt" -exec wc -l {} +

Чтобы найти все текстовые файлы на диске, начните поиск с корневого каталога. Например, так:

find / -name "*.txt"

Если к этому вызову добавить действие с утилитой wc, произойдёт ошибка. То есть следующая команда не сработает в окружении MSYS2:

find / -name "*.txt" -exec wc -l {} +

Проблема связана с ошибкой на иллюстрации 2-17. Текст ошибки передаётся утилите wc. Утилита рассматривает каждое полученное на вход слово как путь до файла. Текст — это не путь. Поэтому wc завершит работу с ошибкой.

Упражнение 2-4. Поиск файлов утилитой grep

Ищите информацию о лицензиях приложений в системном каталоге с документацией /usr/share/doc.

В документации на приложение с лицензией GNU General Public License встречается строка “General Public License”. Найдём такие документы следующей командой:

grep -Rl "General Public License" /usr/share/doc

Также проверим файлы каталога /usr/share/licenses:

grep -Rl "General Public License" /usr/share/licenses

В окружении MSYS2 есть два дополнительных каталога установки приложений: /mingw32 и /mingw64. Они не соответствуют POSIX-стандарту. Проверим установленные в них программы следующими командами:

1 grep -Rl "General Public License" /mingw32/share/doc
2 grep -Rl "General Public License" /mingw64/share

Чтобы найти приложения с лицензией MIT, подойдёт строка поиска “MIT license”. Для Apache лицензии — строка “Apache license”, а для BSD — “BSD license”.

Упражнение 2-6. Работа с файлами и каталогами

Для начала создайте каталоги для каждого года и месяца. Например, так:

1 mkdir -p ~/photo/2019/11
2 mkdir -p ~/photo/2019/12
3 mkdir -p ~/photo/2020/01

Предположим, что фотографии хранятся в каталоге D:\Photo. С помощью утилиты find найдём там файлы, созданные в ноябре 2019 года. Чтобы проверить дату создания файла, используйте параметр -newermt. Например:

find /d/Photo -type f -newermt 2019-11-01 ! -newermt 2019-12-01

Эта команда ищет файлы в каталоге /d/Photo. Он соответствует пути D:\Photo в Windows-окружении.

Первое выражение -newermt 2019-11-01 означает искать только файлы, изменённые начиная с 1 ноября 2019 года. За ним следует выражение ! -newermt 2019-12-01. Оно исключает из результата файлы, модифицированные начиная с 1 декабря 2019 года. Восклицательный знак перед выражением — это отрицание. Между выражениями нет условия. Но утилита find подставит логическое И по умолчанию. В результате получится выражение: “файлы, созданные после 1 ноября 2019 года, но не позднее 30 ноября 2019 года”. Другими словами — “файлы за ноябрь месяц”.

Команда поиска файлов готова. Добавим к ней действие копирования. Получим следующее:

find /d/Photo -type f -newermt 2019-11-01 ! -newermt 2019-12-01 -exec cp {} ~/photo/\
2019/11 \;

Эта команда скопирует файлы за ноябрь 2019 года в каталог ~/photo/2019/11.

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

1 find /d/Photo -type f -newermt 2019-12-01 ! -newermt 2020-01-01 -exec cp {} ~/photo/\
2 2019/12 \;
3 find /d/Photo -type f -newermt 2020-01-01 ! -newermt 2020-02-01 -exec cp {} ~/photo/\
4 2020/01 \;

Предположим, что файлы в каталоге D:\Photo не нужны. Тогда заменим копирование на переименование. Получим такие команды:

1 find /d/Photo -type f -newermt 2019-11-01 ! -newermt 2019-12-01 -exec mv {} ~/photo/\
2 2019/11 \;
3 find /d/Photo -type f -newermt 2019-12-01 ! -newermt 2020-01-01 -exec mv {} ~/photo/\
4 2019/12 \;
5 find /d/Photo -type f -newermt 2020-01-01 ! -newermt 2020-02-01 -exec mv {} ~/photo/\
6 2020/01 \;

Обратите внимание на масштабируемость нашего решения. Количество файлов в каталоге D:\Photo неважно. Чтобы разбить их на три месяца, нужно три команды.

Упражнение 2-7. Конвейеры и перенаправление потоков ввода-вывода

Выясним, как работает утилита bsdtar. Вызовите её с опцией --help. На экран выведется справка по опциям и параметрам. Из справки следует, что утилита создаст архив каталога, если передать ей опции -c и -f. После опций идёт имя архива. Вот пример вызова утилиты:

bsdtar -c -f test.tar test

Эта команда создаст архив с именем test.tar и содержимым каталога test. Обратите внимание, что команда не сожмёт файлы. То есть архив займёт столько же места на диске, сколько и собранные в него файлы.

Цели операций архивирования и сжатия разные. Архивирование нужно для хранения и копирования большого числа файлов. Сжатие уменьшает объём, занимаемый данными на диске. Часто эти операции совмещают в одну.

Чтобы создать архив и сжать его, добавьте в вызов bsdtar опцию -j. Например, так:

bsdtar -c -j -f test.tar.bz2 test

Опции -c, -j и -f можно объединить в одну группу. Получится следующее:

bsdtar -cjf test.tar.bz2 test

Напишем команду для прохода по каталогу фотографий. Для каждого месяца она создаст отдельный архив.

Следующий вызов утилиты find найдёт каталоги с месяцами:

find ~/photo -type d -path */2019/* -o -path */2020/*

Вывод этой команды перенаправим на вход утилиты xargs. Она сформирует вызов bsdtar. Получится такая команда:

find ~/photo -type d -path */2019/* -o -path */2020/* | xargs -I% bsdtar -cf %.tar %

Чтобы bsdtar сжимала файлы, добавьте опцию -j. Получим:

find ~/photo -type d -path */2019/* -o -path */2020/* | xargs -I% bsdtar -cjf %.tar.\
bz2 %

Мы передаём параметр -I утилите xargs. Он указывает место подстановки аргументов в сформированную команду. В вызове утилиты bsdtar таких мест два: имя создаваемого архива и путь до обрабатываемого каталога.

Не забывайте про имена файлов с символами перевода строки. Чтобы обработать их корректно, добавим опцию -print0 в вызов утилиты find. Получим:

find ~/photo -type d -path */2019/* -o -path */2020/* -print0 | xargs -0 -I% bsdtar \
-cjf %.tar.bz2 %

Предположим, что файлы в архивах должны храниться без относительных путей (например 2019/11). Для удаления путей используйте опцию bsdtar --strip-components. Например, так:

find ~/photo -type d -path */2019/* -o -path */2020/* -print0 | xargs -0 -I% bsdtar \
--strip-components=3 -cjf %.tar.bz2 %
Упражнение 2-8. Логические операторы

Реализуем алгоритм по шагам. Первое действие — копирование файла README в домашний каталог пользователя. Это делает следующая команда:

cp /usr/share/doc/bash/README ~

С помощью оператора && и echo выведем результат команды в лог-файл. Получим:

cp /usr/share/doc/bash/README ~ && echo "cp - OK" > result.log

Для архивации файла вызовем утилиту bsdtar или tar. Например, так:

bsdtar -cjf ~/README.tar.bz2 ~/README

Результат утилиты выведем в лог-файл с помощью оператора && и echo:

bsdtar -cjf ~/README.tar.bz2 ~/README && echo "bsdtar - OK" >> result.log

Теперь команда echo дописывает строку в конец существующего лог-файла.

Объединим вызовы утилит cp и bsdtar в одну команду. Утилита bsdtar вызывается только после успешного копирования файла README. Чтобы добиться такой зависимости, поставим между командами оператор &&. Получим:

cp /usr/share/doc/bash/README ~ && echo "cp - OK" > result.log && bsdtar -cjf ~/READ\
ME.tar.bz2 ~/README && echo "bsdtar - OK" >> result.log

Добавим последнее действие — удаление файла README:

cp /usr/share/doc/bash/README ~ && echo "cp - OK" > ~/result.log && bsdtar -cjf ~/RE\
ADME.tar.bz2 ~/README && echo "bsdtar - OK" >> ~/result.log && rm ~/README && echo "\
rm - OK" >> ~/result.log

Запустите эту команду. Если она выполнится без ошибок, в лог-файл запишется следующее:

1 cp - OK
2 bsdtar - OK
3 rm - OK

Команда с вызовами трёх утилит подряд выглядит громоздко. Её неудобно читать и редактировать. Разобьём команду на строки. Для этого есть несколько способов.

Способ первый — перенос строк после логических операторов. Применим его и получим следующее:

1 cp /usr/share/doc/bash/README ~ && echo "cp - OK" > ~/result.log &&
2 bsdtar -cjf ~/README.tar.bz2 ~/README && echo "bsdtar - OK" >> ~/result.log &&
3 rm ~/README && echo "rm - OK" >> ~/result.log

Попробуйте скопировать эту команду в окно терминала и исполнить. Она выполнится без ошибок.

Второй способ разбить команду на строки — использовать символ обратный слэш . Сразу после него поставьте перенос строки. Применяйте этот способ, когда в команде нет логических операторов.

Для примера поставим обратные слэши перед операторами && в нашей команде. Получим:

1 cp /usr/share/doc/bash/README ~ && echo "cp - OK" > ~/result.log \
2 && bsdtar -cjf ~/README.tar.bz2 ~/README && echo "bsdtar - OK" >> ~/result.log \
3 && rm ~/README && echo "rm - OK" >> ~/result.log

Разработка Bash-скриптов

Упражнение 3-2. Полная форма подстановки параметров

Утилита find рекурсивно ищет файлы, начиная с указанного пути. Используйте параметр -maxdepth, чтобы исключить из поиска подкаталоги.

Команда поиска TXT файлов в текущем каталоге выглядит так:

find . -maxdepth 1 -type f -name "*.txt"

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

find . -maxdepth 1 -type f -name "*.txt" -exec cp -t ~ {} \;

Создайте скрипт с именем txt-copy.sh. Скопируйте в него команду поиска.

В скрипт будем передавать параметр. В зависимости от него, выбирается действие: копирование или переименование. В качестве параметра удобнее передать имя утилиты: cp или mv. Скрипт вызовет утилиту по имени для каждого файла, найденного find.

Рассмотрим интерфейс скрипта txt-copy.sh. Копирование выполняется следующей командой:

./txt-copy.sh cp

Команда для переименования файлов такая:

./txt-copy.sh mv

Первый параметр скрипта сохраняется в переменной $1. Подставим её в вызов утилиты find. Получится следующее:

find . -maxdepth 1 -type f -name "*.txt" -exec "$1" -t ~ {} \;

Если действие не указано, скрипт должен копировать файлы. То есть допустим такой вызов:

./txt-copy.sh

Чтобы это сработало, добавим в подстановку параметра $1 значение по умолчанию. Получим скрипт find-txt.sh из листинга 5-1.

Листинг 5-1. Скрипт для поиска TXT-файлов
1 #!/bin/bash
2 
3 find . -maxdepth 1 -type f -name "*.txt" -exec "${1:-cp}" -t ~ {} \;
Упражнение 3-4. Оператор if

Исходная команда выглядит так:

( grep -RlZ "123" target | xargs -0 cp -t . && echo "cp - OK" || ! echo "cp - FAILS"\
 ) && ( grep -RLZ "123" target | xargs -0 rm && echo "rm - OK" || echo "rm - FAILS" \
)

Обратите внимание на отрицание вызова echo “cp - FAILS”. Из-за него утилита grep вызовется второй раз только, если первый вызов выполнился успешно.

Заменим логический оператор && между вызовами grep на конструкцию if-else. Получится следующее:

1 if grep -RlZ "123" target | xargs -0 cp -t .
2 then
3   echo "cp - OK"
4   grep -RLZ "123" target | xargs -0 rm && echo "rm - OK" || echo "rm - FAILS"
5 else
6   echo "cp - FAILS"
7 fi

Теперь заменим операторы || во втором вызове grep на if-else. Получим:

 1 if grep -RlZ "123" target | xargs -0 cp -t .
 2 then
 3   echo "cp - OK"
 4   if grep -RLZ "123" target | xargs -0 rm
 5   then
 6     echo "rm - OK"
 7   else
 8     echo "rm - FAILS"
 9   fi
10 else
11   echo "cp - FAILS"
12 fi

Чтобы избежать вложенных конструкций if-else, применим технику раннего возврата. Также добавим в начале скрипта шебанг. Листинг 5-2 демонстрирует результат.

Листинг 5-2. Скрипт для поиска стоки в файлах
 1 #!/bin/bash
 2 
 3 if ! grep -RlZ "123" target | xargs -0 cp -t .
 4 then
 5   echo "cp - FAILS"
 6   exit 1
 7 fi
 8 
 9 echo "cp - OK"
10 
11 if grep -RLZ "123" target | xargs -0 rm
12 then
13   echo "rm - OK"
14 else
15   echo "rm - FAILS"
16 fi
Упражнение 3-5. Оператор [[

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

Для начала надо пройти по всем файлам каждого каталога. Применим утилиту find. Поиск файлов в каталоге dir1 выглядит так:

find dir1 -type f

Вот пример вывода этой команды:

dir1/test3.txt
dir1/test1.txt
dir1/test2.txt

Мы получили список файлов в каталоге dir1. Проверим, что каждый из них есть в каталоге dir2. Для этого добавим действие -exec в вызов find.

Есть одна проблема. Утилита find добавляет имя каталога dir1 к каждому найденному файлу. Чтобы исключить dir1 в списке файлов, перейдём в этот каталог перед запуском find. Получим такие команды:

1 cd dir1
2 find . -type f

Теперь вывод find выглядит так:

./test3.txt
./test1.txt
./test2.txt

С помощью действия -exec и команды test проверим, что каждый найденный файл есть в каталоге dir2. Получим:

1 cd dir1
2 find . -type f -exec test -e ../dir2/{} \;

Здесь мы используем команду test вместо оператора [[. Дело в том, что встроенный интерпретатор find не способен обработать этот оператор корректно. Это одно из исключений, когда [[ надо заменить на test. В общем случае предпочитайте оператор [[.

Если файла не оказалось в каталоге dir2, выведем его имя на экран. Для этого инвертируем проверку test и добавим второе действие -exec с выводом echo. Между действиями поставим логический оператор И. В результате получим следующие команды:

1 cd dir1
2 find . -type f -exec test ! -e ../dir2/{} \; -a -exec echo {} \;

Добавим аналогичный вызов find для проверки файлов каталога dir2 в каталоге dir1.

Листинг 5-3 демонстрирует полный скрипт dir-diff.sh для сравнения каталогов.

Листинг 5-3. Скрипт для сравнения каталогов
1 #!/bin/bash
2 
3 cd dir1
4 find . -type f -exec test ! -e ../dir2/{} \; -a -exec echo {} \;
5 
6 cd ../dir2
7 find . -type f -exec test ! -e ../dir1/{} \; -a -exec echo {} \;
Упражнение 3-6. Оператор case

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

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

Напишем алгоритм скрипта переключения между файлами конфигурации. Он выглядит так:

  1. Удалить существующую символьную ссылку или файл по пути ~/.bashrc.
  2. Проверить опцию командной строки, переданную в скрипт.
  3. В зависимости от опции создать символьную ссылку на файл .bashrc-home или .bashrc-work.

Реализуем этот алгоритм с помощью оператора case. Листинг 5-4 демонстрирует результат.

Листинг 5-4. Скрипт для переключения конфигурационных файлов
 1 #!/bin/bash
 2 
 3 file="$1"
 4 
 5 rm ~/.bashrc
 6 
 7 case "$file" in
 8   "h")
 9     ln -s ~/.bashrc-home ~/.bashrc
10     ;;
11 
12   "w")
13     ln -s ~/.bashrc-work ~/.bashrc
14     ;;
15 
16   *)
17     echo "Указана недопустимая опция"
18     ;;
19 esac

Конфигурационный файл выбираем в зависимости от переданного в скрипт параметра $1.

Команды вызова утилиты ln отличаются только именем файла. Такое подобие подсказывает, что оператор case можно заменить на ассоциативный массив. Тогда получится скрипт, похожий на листинг 5-5.

Листинг 5-5. Скрипт для переключения конфигурационных файлов
 1 #!/bin/bash
 2 
 3 option="$1"
 4 
 5 declare -A files=(
 6   ["h"]="~/.bashrc-home"
 7   ["w"]="~/.bashrc-work")
 8 
 9 if [[ -z "$option" || ! -v files["$option"] ]]
10 then
11   echo "Указана недопустимая опция"
12   exit 1
13 fi
14 
15 rm ~/.bashrc
16 
17 ln -s "${files["$option"]}" ~/.bashrc

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

Упражнение 3-7. Арифметические действия в дополнительном коде

Результаты сложения однобайтовых целых:

* 79 + (-46) = 0100 1111 + 1101 0010 = 1 0010 0001 -> 0010 0000 = 33

* -97 + 96 = 1001 1111 + 0110 0000 = 1111 1111 -> 1111 1110 -> 1000 0001 = -1

Результат сложения двухбайтовых целых:

* 12868 + (-1219) = 0011 0010 0100 0100 + 1111 1011 0011 1101 = 1 0010 1101 1000 000\
1 -> 0010 1101 1000 0001 = 11649

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

Упражнение 3-8. Modulo и остаток от деления
* 1697 % 13
q = 1697 / 13 ~ 130.5385 ~ 130
r = 1697 - 13 * 130 = 7

* 1697 modulo 13
q = 1697 / 13 ~ 130.5385 ~ 130
r = 1697 - 13 * 130 = 7

* 772 % -45
q = 772 / -45 ~ -17.15556 ~ -17
r = 772 - (-45) * (-17) = 7

* 772 modulo -45
q = (772 / -45) - 1 ~ -18.15556 ~ -18
r = 772 - (-45) * (-18) = -38

* -568 % 12
q = -568 / 12 ~ -47.33333 ~ -47
r = -568 - 12 * (-47) = -4

* -568 modulo 12
q = (-568 / 12) - 1 ~ -48.33333 ~ -48
r = -568 - 12 * (-48) = 8

* -5437 % -17
q = -5437 / -17 ~ 319.8235 ~ 319
r = -5437 - (-17) * 319 = -14

* -5437 modulo -17
q = -5437 / -17 ~ 319.8235 ~ 319
r = -5437 - (-17) * 319 = -14

Проверьте ваши расчёты этим Python скриптом.

Упражнение 3-9. Побитовое отрицание

Сначала вычислим побитовое отрицание для беззнаковых двухбайтовых целых.

 56 = 0000 0000 0011 1000
~56 = 1111 1111 1100 0111 = 65479

 1018 = 0000 0011 1111 1010
~1018 = 1111 1100 0000 0101 = 64517

 58362 = 1110 0011 1111 1010
~58362 = 0001 1100 0000 0101 = 7173

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

 56 = 0000 0000 0011 1000
~56 = 1111 1111 1100 0111 -> 1000 0000 0011 1001 = -57

 1018 = 0000 0011 1111 1010
~1018 = 1111 1100 0000 0101 -> 1000 0011 1111 1011 = -1019

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

58362 = 1110 0011 1111 1010 -> 1001 1100 0000 0110 = -7174

Теперь выполним побитовое отрицание:

  -7174  = 1110 0011 1111 1010
~(-7174) = 0001 1100 0000 0101 = 7173

Проверим результаты для знаковых целых с помощью Bash-команд:

1 $ echo $((~56))
2 -57
3 $ echo $((~1018))
4 -1019
5 $ echo $((~(-7174)))
6 7173

Проверить отрицание двухбайтового беззнакого целого 58362 с помощью Bash нельзя. Интерпретатор сохранит число в знаковом восьмибайтовом целом. Результат отрицания будет такой:

1 $ echo $((~58362))
2 -58363
Упражнение 3-10. Побитовые И, ИЛИ, исключающее ИЛИ

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

1122 & 908 = 0000 0100 0110 0010 & 0000 0011 1000 1100 = 0000 0000 000 0000 = 0

1122 | 908 = 0000 0100 0110 0010 | 0000 0011 1000 1100 = 0000 0111 1110 1110 = 2030

1122 ^ 908 = 0000 0100 0110 0010 ^ 0000 0011 1000 1100 = 0000 0111 1110 1110 = 2030


49608 & 33036 = 1100 0001 1100 1000 & 1000 0001 0000 1100 = 1000 0001 0000 1000 = 33\
032

49608 | 33036 = 1100 0001 1100 1000 | 1000 0001 0000 1100 = 1100 0001 1100 1100 = 49\
612

49608 ^ 33036 = 1100 0001 1100 1000 ^ 1000 0001 0000 1100 = 0100 0000 1100 0100 = 16\
580

Если целые знаковые, результаты битовых операций для первой пары чисел 1122 и 908 такие же. Для второй пары, вычисление отличается. Рассмотрим его.

Сначала получим значение чисел 49608 и 33036 в дополнительном коде:

49608 = 1100 0001 1100 1000 -> 1011 1110 0011 1000 = -15928

33036 = 1000 0001 0000 1100 -> 1111 1110 1111 0100 = -32500

Теперь выполним битовые операции:

-15928 & -32500 = 1100 0001 1100 1000 & 1000 0001 0000 1100 = 1000 0001 0000 1000 ->\
 1111 1110 1111 1000 = -32504

-15928 | -32500 = 1100 0001 1100 1000 | 1000 0001 0000 1100 = 1100 0001 1100 1100 ->\
 1011 1110 0011 0100 = -15924

-15928 ^ -32500 = 1100 0001 1100 1000 ^ 1000 0001 0000 1100 = 0100 0000 1100 0100 = \
16580

Вот Bash-команды для проверки результатов:

 1 $ echo $((1122 & 908))
 2 0
 3 $ echo $((1122 | 908))
 4 2030
 5 $ echo $((1122 ^ 908))
 6 2030
 7 
 8 $ echo $((49608 & 33036))
 9 33032
10 $ echo $((49608 | 33036))
11 49612
12 $ echo $((49608 ^ 33036))
13 16580
14 
15 $ echo $((-15928 & -32500))
16 -32504
17 $ echo $((-15928 | -32500))
18 -15924
19 $ echo $((-15928 ^ -32500))
20 16580
Упражнение 3-11. Битовые сдвиги

Вычисление битовых сдвигов:

* 25649 >> 3 = 0110 0100 0011 0001 >> 3 = 0110 0100 0011 0 = 0000 1100 1000 0110 = 3\
206

* 25649 << 2 = 0110 0100 0011 0001 << 2 = 10 0100 0011 0001 -> 1001 0000 1100 0100 =\
 -28476

* -9154 >> 4 = 1101 1100 0011 1110 >> 4 = 1101 1100 0011 -> 1111 1101 1100 0011 = -5\
73

* -9154 << 3 = 1101 1100 0011 1110 << 3 = 1 1100 0011 1110 -> 1110 0001 1111 0000 = \
-7696

Bash-команды для проверки результатов:

1 $ echo $((25649 >> 3))
2 3206
3 $ echo $((25649 << 2))
4 102596
5 $ echo $((-9154 >> 4))
6 -573
7 $ echo $((-9154 << 3))
8 -73232

Результаты Bash-команд отличаются для второго и четвертого сдвига. Причина в том, что Bash хранит все числа в восьми батах.

Проверьте свои расчёты с помощью онлайн-калькулятора.

Упражнение 3-12. Операторы цикла

Чтобы отгадать число, игроку даётся семь попыток. Каждая попытка обрабатывается по одному и тому же алгоритму. Поместим его в цикл for.

Алгоритм обработки действия игрока следующий:

  1. Прочитать ввод с помощью команды read.
  2. Сравнить введённое число с загаданным.
  3. Если игрок ошибся, вывести подсказку и перейти к шагу 1.
  4. Если игрок угадал число, завершить работу скрипта.

Чтобы загадать случайное число, обратимся к зарезервированной переменной RANDOM. При чтении она возвращает случайное значение от 0 до 32767. Нам нужно число от 1 до 100. Получим его из RANDOM по такому алгоритму:

1. Получим случайное число от 0 до 99. Для этого вычислим остаток от деления RANDOM на 100 по формуле:

number=$((RANDOM % 100))

2. Получим число в диапазоне от 1 до 100. Для этого прибавим единицу к предыдущему результату.

Полная формула вычисления случайного числа от 1 до 100 выглядит так:

number=$((RANDOM % 100 + 1))

Листинг 5-6 демонстрирует скрипт, выполняющий алгоритм игры.

Листинг 5-6. Скрипт игры Больше-Меньше
 1 #!/bin/bash
 2 
 3 number=$((RANDOM % 100 + 1))
 4 
 5 for i in {1..7}
 6 do
 7   echo "Введите число:"
 8 
 9   read input
10 
11   if (( input < number))
12   then
13     echo "Число $input меньше искомого"
14   elif (( number < input))
15   then
16     echo "Число $input больше искомого"
17   else
18     echo "Вы отгадали число"
19     exit 0
20   fi
21 done
22 
23 echo "Вы не отгадали число"

Чтобы отгадать число за семь попыток, примените двоичный поиск. Его идея в разделении массива чисел на половины. Рассмотрим пример игры “Больше-Меньше” и двоичного поиска.

Как только игра началась мы отгадываем число в диапазоне от 1 до 100. Середина этого диапазона — число 50. Введите это значение первым. Программа даст подсказку, в какой половине диапазона находится загаданное число. Предположим, программа ответила, что 50 меньше искомого числа. Это означает, что искать надо в диапазоне от 50 до 100. Введём середину этого диапазона, то есть число 75. Получаем ответ, что 75 тоже меньше искомого. Вывод — искомое число находится между 75 и 100. Середина этого диапазона X рассчитывается так:

X = 75 + (100 - 75) / 2 = 87.5

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

Упражнение 3-13. Функции

Мы рассмотрели вариант функции code_to_error в примерах раздела “Функции в скриптах”.

Объединим код функций print_error и code_to_error в один файл. Получим скрипт из листинга 5-7.

Листинг 5-7. Скрипт для вывода сообщений об ошибках
 1 #!/bin/bash
 2 
 3 code_to_error()
 4 {
 5   case $1 in
 6     1)
 7       echo "Не найден файл"
 8       ;;
 9     2)
10       echo "Нет прав для чтения файла"
11       ;;
12   esac
13 }
14 
15 print_error()
16 {
17   echo "$(code_to_error $1) $2" >> debug.log
18 }
19 
20 print_error 1 "readme.txt"

Сейчас функция code_to_error выводит сообщения на русском языке. Переименуем её на code_to_error_ru. Тогда язык сообщений станет понятен из имени функции.

Добавим в скрипт функцию code_to_error_en. Она печатает текст на английском языке для переданного в неё кода ошибки. Код функции выглядит так:

 1 code_to_error_en()
 2 {
 3   case $1 in
 4     1)
 5       echo "File not found:"
 6       ;;
 7     2)
 8       echo "Permission to read the file denied:"
 9       ;;
10   esac
11 }

Теперь надо выбрать, какую функцию code_to_error вызывать из print_error. Проверим региональные настройки в переменной окружения LANG. Если значение переменной соответствует шаблону “ru_RU*”, вызовем функцию code_to_error_ru. В противном случае вызовем code_to_error_en.

Полный код скрипта приведён в листинге 5-8.

Листинг 5-8. Скрипт для вывода сообщений об ошибках
 1 #!/bin/bash
 2 
 3 code_to_error_ru()
 4 {
 5   case $1 in
 6     1)
 7       echo "Не найден файл"
 8       ;;
 9     2)
10       echo "Нет прав для чтения файла"
11       ;;
12   esac
13 }
14 
15 code_to_error_en()
16 {
17   case $1 in
18     1)
19       echo "File not found:"
20       ;;
21     2)
22       echo "Permission to read the file denied:"
23       ;;
24   esac
25 }
26 
27 print_error()
28 {
29   if [[ "$LANG" == ru_RU* ]]
30   then
31     echo "$(code_to_error_ru $1) $2" >> debug.log
32   else
33     echo "$(code_to_error_en $1) $2" >> debug.log
34   fi
35 }
36 
37 print_error 1 "readme.txt"

Оператор if в функции print_error можно заменить на case. Например, так:

 1 print_error()
 2 {
 3   case $LANG in
 4     ru_RU*)
 5       echo "$(code_to_error_ru $1) $2" >> debug.log
 6       ;;
 7     en_US*)
 8       echo "$(code_to_error_en $1) $2" >> debug.log
 9       ;;
10     *)
11       echo "$(code_to_error_en $1) $2" >> debug.log
12       ;;
13   esac
14 }

Вариант с case удобнее, если надо поддерживать более двух языков.

Сейчас в функции print_error код дублируется. В каждом блоке оператора case вызывается одинаковая команда echo. Единственное различие между блоками — имя функции для конвертирования кода ошибки в текст. Чтобы избежать дублирования, сохраним имя функции в переменной func. Затем подставим эту переменную в вызов echo. Получится следующее:

 1 print_error()
 2 {
 3   case $LANG in
 4     ru_RU)
 5       local func="code_to_error_ru"
 6       ;;
 7     en_US)
 8       local func="code_to_error_en"
 9       ;;
10     *)
11       local func="code_to_error_en"
12       ;;
13   esac
14 
15   echo "$($func $1) $2" >> debug.log
16 }

Альтернативное решение проблемы дублирования кода — использовать индексируемый массив. Заменим операторы case в функциях code_to_error_ru и code_to_error_en на массивы. Например, так:

 1 code_to_error_ru()
 2 {
 3   declare -a messages
 4 
 5   messages[1]="Не найден файл"
 6   messages[2]="Нет прав для чтения файла"
 7 
 8   echo "${messages[$1]}"
 9 }
10 
11 code_to_error_en()
12 {
13   declare -a messages
14 
15   messages[1]="The following file was not found:"
16   messages[2]="You do not have permissions to read the following file:"
17 
18   echo "${messages[$1]}"
19 }

Можно упростить код и обойтись без функций code_to_error. Объединим сообщения на всех языках в один ассоциативный массив. Поместим его в функцию print_error. Ключами массива будут комбинации значения переменной LANGUAGE и кода ошибки. Получим такую функцию print_error как в листинге 5-9.

Листинг 5-9. Скрипт для вывода сообщений об ошибках
 1 #!/bin/bash
 2 
 3 print_error()
 4 {
 5   declare -A messages
 6 
 7   messages["ru_RU",1]="Не найден файл"
 8   messages["ru_RU",2]="Нет прав для чтения файла"
 9 
10   messages["en_US",1]="File not found:"
11   messages["en_US",2]="Permission to read the file denied:"
12 
13   echo "${messages[$LANGUAGE,$1]} $2" >> debug.log
14 }
15 
16 print_error 1 "readme.txt"
Упражнение 3-14. Область видимости переменных

Скрипт из листинга 3-37 выведет на консоль следующий текст:

1 main1: var =
2 foo1: var = foo_value
3 bar1: var = foo_value
4 bar2: var = bar_value
5 foo2: var = bar_value
6 main2: var =

Начнём с вывода “main1” и “main2”. Переменная var объявлена в функции foo с атрибутом local. Поэтому она доступна только в функциях foo и bar. Следовательно, до и после вызова foo переменная varсчитается необъявленной. Необъявленные переменные имеют пустое значение в Bash.

Далее скрипт выводит значение переменной var в начале функции foo. Так мы получаем строку “foo1: var = foo_value”.

Следующие два вывода происходят из функции bar. Первый печатает строку foo_value. Мы получили это значение, потому что тело функции bar является областью видимости переменной var, объявленной в foo.

Скрипт присваивает значение bar_value переменной var в функции bar. Обратите внимание, что это не объявление новой глобальной переменной с именем var. Это перезапись существующей локальной переменной. Её значение bar_value мы получим в выводах “bar2” и “foo2”.