Files
bash-programming-from-scratch/manuscript/BashScripting/arithmetic-expression.md
2020-10-17 10:22:39 +02:00

88 KiB
Raw Blame History

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

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

I> В Bash для арифметики с плавающей точкой используйте калькулятор bc или dc.

Представление целых чисел

Рассмотрим способы представления целых чисел в памяти компьютера. Это поможет лучше понять, как работают математические операции в Bash.

Целые числа могут быть положительными и отрицательными. Соответствующий им тип данных называется целое (integer).

Если переменная целого типа принимает только положительные значения, она называется беззнаковой (unsigned). Если допустимы как положительные, так и отрицательные значения — это переменная со знаком (signed).

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

Прямой код

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

Начнём с самого простого представления чисел — прямого кода. Его можно использовать двумя способами:

  • Для записи только положительных целых (беззнаковых).

  • Для записи как положительных, так и отрицательных целых (со знаком).

Под любое число отводится фиксированный блок памяти. В первом варианте прямого кода все биты этой памяти используются одинаково. В них хранится значение числа. Таблица 3-13 приводит примеры такого способа хранения.

{caption: "Таблица 3-13. Представление беззнаковых целых в прямом коде", width: "70%"}

Десятичное число Шестнадцатеричный формат Прямой код
0 0 0000 0000
5 5 0000 0101
60 3C 0011 1100
110 6E 0110 1110
255 FF 1111 1111

Предположим, что на число выделен один байт памяти. Тогда в прямом коде можно сохранить целые беззнаковые числа от 0 до 255.

Прямой код можно использовать вторым способом. Он позволяет хранить целые числа со знаком. Для этого старший бит числа резервируется для знака. Поэтому на значение числа остаётся меньше битов. Например, отведём для хранения числа один байт памяти. Один бит уйдёт на знак. Останется только семь битов на значение числа.

Таблица 3-14 демонстрирует представление целых со знаком в прямом коде.

{caption: "Таблица 3-14. Представление целых со знаком в прямом коде", width: "70%"}

Десятичное число Шестнадцатеричный формат Прямой код
-127 FF 1111 1111
-110 EE 1110 1110
-60 BC 1011 1100
-5 85 1000 0101
-0 80 1000 0000
0 0 0000 0000
5 5 0000 0101
60 3C 0011 1100
110 6E 0110 1110
127 7F 0111 1111

Обратите внимание, что старший (первый) бит всех отрицательных чисел равен единице, а положительных — нулю. Из-за знака теперь нельзя сохранить числа больше 127 в одном байте. По этой же причине минимальное отрицательное число равно -127.

Прямой код не получил широкого распространения в компьютерной технике по двум причинам:

  1. Арифметические операции над отрицательными числами требуют усложнения архитектуры процессора. Модуль процессора для суммирования положительных чисел не подходит для отрицательных.

  2. Существует два представления нуля: положительное (0000 0000) и отрицательное (1000 0000). Это осложняет операцию сравнения, так как в памяти эти значения не равны.

Постарайтесь разобраться в принципе работы прямого кода. Без этого вы не поймёте другие два способа представления целых.

Обратный код

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

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

Для примера сложим числа 10 и -5. Представим их в прямом коде. Предположим, что на каждое число отводится один байт в памяти компьютера. Тогда получим следующий результат: {line-numbers: false}

10 = 0000 1010
-5 = 1000 0101

Теперь возникает вопрос — как процессору сложить эти числа? У любого современного процессора есть стандартный модуль под названием сумматор. Он побитово складывает два числа. Если применить его для нашей задачи, получим следующее: {line-numbers: false}

10 + (-5) = 0000 1010 + 1000 0101 = 1000 1111 = -15

Результат неверный. Это означает, что сумматор не подходит для сложения целых в прямом коде. Проблема в том, что при сложении не учитывается старший бит числа.

Проблема решается двумя способами:

  1. Добавить в процессор специальный модуль для операций над отрицательными числами.

  2. Изменить способ представления отрицательных целых так, чтобы сумматор смог их складывать.

Развитие компьютерной техники пошло по второму пути. Он дешевле, чем усложнение процессора.

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

Таблица 3-15 демонстрирует представление чисел в обратном коде.

{caption: "Таблица 3-15. Представление целых со знаком в обратном коде", width: "70%"}

Десятичное число Шестнадцатеричный формат Обратный код
-127 80 1000 0000
-110 91 1001 0001
-60 C3 1100 0011
-5 FA 1111 1010
-0 FF 1111 1111
0 0 0000 0000
5 5 0000 0101
60 3C 0011 1100
110 6E 0110 1110
127 7F 0111 1111

Вместимость памяти при использовании прямого и обратного кодов одинакова. В одном байте по-прежнему можно сохранить числа от -127 до 127.

Что дало инвертирование битов значения для отрицательных чисел? Проверим, как теперь будет работать сложение чисел. Представим 10 и -5 в обратном коде. Затем сложим их с помощью сумматора.

Числа в обратном коде выглядят так: {line-numbers: false}

10 = 0000 1010
-5 = 1111 1010

Сложение даст следующее: {line-numbers: false}

10 + (-5) = 0000 1010 + 1111 1010 = 1 0000 0100

Обратите внимание, что в результате сложения произошло переполнение. Старшая единица не поместилась в один байт, отведённый под число. В этом случае она отбрасывается. Тогда результат сложения станет таким: {line-numbers: false}

0000 0100

Отброшенная единица влияет на конечный результат. Нужен второй этап вычисления, чтобы её учесть. На этом этапе просто добавим единицу к результату: {line-numbers: false}

0000 0100 + 0000 0001 = 0000 0101 = 5

Мы получили правильный результат сложения чисел 10 и -5.

Если в результате сложения получилось отрицательное число, второй этап вычисления не нужен. Для примера сложим числа -7 и 2. Сначала представим их в обратном коде: {line-numbers: false}

-7 = 1111 1000
2 = 0000 0010

Выполним первый этап сложения: {line-numbers: false}

-7 + 2 = 1111 1000 + 0000 0010 = 1111 1010

Старший бит равен единице. Это значит, что мы получили отрицательное число. В этом случае второй этап сложения не нужен.

Проверим корректность результата. Для удобства переведём число из обратного кода в прямой. Чтобы это сделать, инвертируем все биты значения числа. Знаковый бит оставляем без изменений. В результате получим следующее: {line-numbers: false}

1111 1010 -> 1000 0101 = -5

Мы снова получили верный результат.

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

У прямого кода есть вторая проблема: представление нуля двумя способами. Её обратный код решить не смог.

Дополнительный код

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

Положительные числа в дополнительном коде выглядят так же, как и в прямом. Старший знаковый бит равен нулю. Остальные биты хранят значение числа. У отрицательных чисел старший бит равен единице. Биты значения инвертируются, как в обратном коде. Затем к результату прибавляется единица.

Представление чисел в дополнительном коде приведено в таблице 3-16.

{caption: "Таблица 3-16. Представление целых со знаком в дополнительном коде", width: "70%"}

Десятичное число Шестнадцатеричный формат Дополнительный код
-127 81 1000 0001
-110 92 1001 0010
-60 C4 1100 0100
-5 FB 1111 1011
0 0 0000 0000
5 5 0000 0101
60 3C 0011 1100
110 6E 0110 1110
127 7F 0111 1111

Вместимость памяти при использовании дополнительного кода не меняется. По-прежнему в одном байте можно сохранить числа от -127 до 127.

Рассмотрим сложение отрицательных чисел в обратном коде. Для примера сложим 14 и -8. Сначала представим каждое число в дополнительном коде. Получим: {line-numbers: false}

14 = 0000 1110
-8 = 1111 1000

Теперь выполним сложение: {line-numbers: false}

14 + (-8) = 0000 1110 + 1111 1000 = 1 0000 0110

В результате сложения произошло переполнение. Старшая единица не поместилась в один байт. Её надо отбросить. Тогда конечный результат будет таким: {line-numbers: false}

0000 0110 = 6

Если результат сложения отрицательный, то отбрасывать старший бит не нужно. Для примера сложим числа -25 и 10. В дополнительном коде они выглядят так: {line-numbers: false}

-25 = 1110 0111
10 = 0000 1010

Сложение чисел даст: {line-numbers: false}

-25 + 10 = 1110 0111 0000 1010 = 1111 0001

Переведём результат из дополнительного кода в обратный, а потом в прямой. Для этого выполним следующие преобразования: {line-numbers: false}

1111 0001 - 1 = 1111 0000 -> 1000 1111 = -15

При переводе из обратного кода в прямой мы инвертируем все разряды кроме старшего со знаком. В итоге мы получим правильный результат сложения чисел -25 и 10.

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

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

Во всех современных компьютерах целые представляются в дополнительном коде.

{caption: "Упражнение 3-7. Арифметические действия в дополнительном коде", format: text, line-numbers: false}

Выполните сложение однобайтовых целых в дополнительном коде:

* 79 и -46
* -97 и 96

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

* 12868 и -1219

Конвертирование чисел

Мы узнали, как числа представляются в памяти компьютера. Когда это может пригодиться вам на практике?

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

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

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

Рассмотрим пример. Предположим, что вы пишете драйвер для периферийного устройства. Устройство периодически отправляет на CPU данные (например, результаты измерений). Возникает задача интерпретировать их правильно. Представление чисел на устройстве и компьютере может отличаться (например, порядком байтов). В этом случае вам понадобятся знания о представлении чисел в памяти.

Ещё одна задача, с которой сталкивается каждый программист, — это отладка. Отладкой программы называется поиск и устранение в ней ошибок. Для примера в арифметическом выражении происходит переполнение. Зная как числа представляются в памяти, вам будет легче обнаружить проблему.

Оператор ((

Bash выполняет целочисленную арифметику в математическом контексте (math context). Его синтаксис напоминает язык C.

Предположим, что результат сложения двух чисел надо сохранить в переменной. Объявим её с целочисленным атрибутом -i. Затем сразу присвоим ей значение. Например, так: {line-numbers: false, format: Bash}

declare -i var=12+7

В результате переменная будет равна числу 19, а не строке "12+7". Если объявить переменную с атрибутом -i, присваиваемое ей значение всегда будет вычисляться в математическом контексте. Это и произошло в нашем примере.

Математический контекст можно объявить явно. Это делает встроенная Bash-команда let.

Предположим, что переменная объявлена без атрибута -i. Тогда команда let позволит присвоить ей значение арифметического выражения. Например, так: {line-numbers: false, format: Bash}

let text=5*7

В результате переменная text будет равна 35.

Если переменная объявлялась с атрибутом -i, команда let не нужна. Например: {line-numbers: false, format: Bash}

declare -i var
var=5*7

Значением переменной var будет 35.

Объявление переменной с атрибутом -i приводит к неявному математическому контексту. Это может стать источником ошибок. Поэтому старайтесь не использовать атрибут -i. Независимо от него, значение переменной хранится в памяти в виде строки. Конвертирование строки в число и обратно происходит каждый раз при присвоении.

Команда let позволяет работать со строковой переменной как с целочисленной. Например, так: {line-numbers: true, format: Bash}

let var=12+7
let var="12 + 7"
let "var = 12 + 7"
let 'var = 12 + 7'

Результат всех четырёх команд одинаков. Переменной var будет присвоено значение 19.

Команда let принимает на вход параметры. Каждый из них должен быть корректным арифметическим выражением. Если в выражении встречаются пробелы, оно будет разделено на части из-за word splitting. В этом случае let вычислит каждую часть выражения по отдельности. Это может привести к ошибке.

Для примера рассмотрим такую команду: {line-numbers: false, format: Bash}

let var=12 + 7

Здесь в результате word splitting команда let получит на вход три выражения: "var=12", "+" и "7". Вычисление второго из них "+" приведёт к ошибке. Плюс означает арифметическую операцию сложения. Она требует двух операндов. Но в нашем случае операндов нет.

Предположим, что все переданные в команду let выражения корректны. Тогда они вычисляются друг за другом. Например: {line-numbers: true, format: Bash}

let a=1+1 b=5+1
let "a = 1 + 1" "b = 5 + 1"
let 'a = 1 + 1' 'b = 5 + 1'

В результате всех трёх команд переменной a будет присвоено значение 2, а переменной b — 6.

Чтобы предотвратить word splitting в параметрах команды let, заключайте их в одинарные или двойные кавычки.

У команды let есть синоним — оператор ((. В нём word splitting не выполняется. Поэтому выражения в операторе не требуют кавычек. Всегда используйте оператор (( вместо let. Это поможет избежать ошибки.

I> Отношения оператора (( и команды let напоминают отношения test и [[. В обоих случаях используйте операторы, а не команды.

Оператор (( имеет две формы. Первая форма называется арифметической оценкой (arithmetic evaluation). Это синоним команды let. Арифметическая оценка выглядит так: {line-numbers: false, format: Bash}

((var = 12 + 7))

Здесь вместо команды let ставятся открывающие скобки ((. В конце добавляются закрывающие скобки )). Эта форма оператора (( возвращает код ноль при успешном выполнении и единицу в случае ошибки. Вычислив выражение, Bash подставит вместо него код возврата.

Вторая форма оператора (( называется арифметической подстановкой (arithmetic expansion). Она выглядит так: {line-numbers: false, format: Bash}

var=$((12 + 7))

Здесь перед оператором (( ставится знак доллара $. В этом случае Bash вычислит значение выражения. Затем он подставит это значение вместо выражения. Это отличается от поведения первой формы оператора ((, при которой подставляется код возврата.

I> Вторая форма оператора (( является частью POSIX-стандарта. Используйте её для переносимого кода. Первая форма оператора (( доступна только в интерпретаторах Bash, ksh и zsh.

В операторе (( имена переменных можно указывать без знака доллар $. Bash всё равно правильно подставит их значения. Например, следующие два выражения для вычисления result эквивалентны: {line-numbers: true, format: Bash}

a=5 b=10
result=$(($a + $b))
result=$((a + b))

В обоих случаях переменная result станет равна 15.

Не используйте знак доллара в операторе ((. Это сделает ваш код чище и понятнее.

I> В Bash есть устаревшая форма арифметической подстановки — оператор "$[ ]". Никогда не используйте её. Для расчёта арифметических выражений есть GNU-утилита expr. Она нужна для совместимости со старыми скриптами, написанными на Bourne Shell. Никогда не используйте expr при разработке новых скриптов.

Таблица 3-17 демонстрирует операции, допустимые в арифметических выражениях.

{caption: "Таблица 3-17. Операции в арифметических выражениях", width: "100%"}

Операция Описание Пример
Вычисления
* Умножение echo "$((2 * 9)) = 18"
/ Деление echo "$((25 / 5)) = 5"
% Остаток от деления echo "$((8 % 3)) = 2"
+ Сложение echo "$((7 + 3)) = 10"
- Вычитание echo "$((8 - 5)) = 3"
** Возведение в степень echo "$((4**3)) = 64"
Битовые операции
~ Побитовое НЕ (NOT) echo "$((~5)) = -6"
<< Битовый сдвиг влево echo "$((5 << 1)) = 10"
>> Битовый сдвиг вправо echo "$((5 >> 1)) = 2"
& Побитовое И (AND) echo "$((5 & 4)) = 4"
` ` Побитовое ИЛИ (OR)
^ Побитовое исключающее ИЛИ (XOR) echo "$((5 ^ 4)) = 1"
Присваивания
= Обычное присваивание echo "$((num = 5)) = 5"
*= Умножение и присваивание результата echo "$((num *= 2)) = 10"
/= Деление и присваивание результата echo "$((num /= 2)) = 5"
%= Остаток от деления и присваивание результата echo "$((num %= 2)) = 1"
+= Сложение и присваивание результата echo "$((num += 7)) = 8"
-= Вычитание и присваивание результата echo "$((num -= 3)) = 5"
<<= Битовый сдвиг влево и присваивание результата echo "$((num <<= 1)) = 10
>>= Битовый сдвиг вправо и присваивание результата echo "$((num >>= 2)) = 2"
&= Побитовое И (AND), затем присваивание результата echo "$((num &= 3)) = 2"
^= Побитовое исключающее ИЛИ (XOR), затем присваивание результата echo "$((num^=7)) = 5"
` =` Побитовое ИЛИ (OR), затем присваивание результата
Сравнения
< Меньше ((num < 5)) && echo "переменная num меньше 5"
> Больше ((num > 5)) && echo "переменная num больше 3"
<= Меньше или равно ((num <= 20)) && echo "переменная num меньше или равна 20"
>= Больше или равно ((num >= 15)) && echo "переменная num больше или равна 15"
== Равно ((num == 3)) && echo "переменная num равна 3"
!= Не равно ((num != 3)) && echo "переменная num не равна 3"
Логические операции
! Логическое НЕ (NOT) (( ! num )) && echo "переменная num имеет значение ЛОЖЬ"
&& Логическое И (AND) (( 3 < num && num < 5 )) && echo "переменная num больше 3, но меньше 5"
` `
Другие операции
num++ Постфикс-инкремент echo "$((num++))"
num-- Постфикс-декремент echo "$((num--))"
++num Префикс-инкремент echo "$((++num))"
--num Префикс-декремент echo "$((--num))"
+num Унарный плюс или умножение числа на 1 a=$((+num))"
-num Унарный минус или умножение числа на -1 a=$((-num))"
УСЛОВИЕ ? ДЕЙСТВИЕ1 : ДЕЙСТВИЕ2 Тернарная условная операция a=$(( b < c ? b : c ))
ДЕЙСТВИЕ1, ДЕЙСТВИЕ2 Список выражений ((a = 4 + 5, b = 16 - 7))
( ДЕЙСТВИЕ1 ) Группирование выражений (подвыражение) a=$(( (4 + 5) * 2 ))

Все операции выполняются в порядке их приоритета. Операции с большим приоритетом исполняются первыми.

Таблица 3-18 демонстрирует порядок операций.

{caption: "Таблица 3-18. Порядок выполнения математических операций", width: "100%"}

Порядок выполнения Операция Описание
1 ( ДЕЙСТВИЕ1 ) Группирование выражений
2 num++, num-- Постфиксный инкремент и декремент
3 ++num, --num Префиксный инкремент и декремент
4 +num, -num Унарный плюс и минус
5 ~, ! Побитовое и логическое отрицание
6 ** Возведение в степень
7 *, /, % Умножение, деление, нахождение остатка
8 +, - Сложение и вычитание
9 <<, >> Битовые сдвиги
10 <, <=, >, >= Сравнения
11 ==, != Равенство и неравенство
12 & Побитовое И
13 ^ Побитовое исключающее ИЛИ
14 ` `
15 && Логическое И
16 `
17 УСЛОВИЕ ? ДЕЙСТВИЕ1 : ДЕЙСТВИЕ2 Тернарная условная операция
18 `=, *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, =`
19 ДЕЙСТВИЕ1, ДЕЙСТВИЕ2 Список выражений

Порядок выполнения можно изменить с помощью круглых скобок "( )". Их содержимое называется подвыражением (subexpression). Bash вычисляет значения подвыражений в первую очередь. Если подвыражений несколько, они вычисляются по порядку.

Предположим, в вашем коде используется числовая константа. Её значение можно указать в произвольной системе счисления. Для выбора системы счисления используйте префикс. Список допустимых префиксов приведён в таблице 3-19.

{caption: "Таблица 3-19. Префиксы для указания системы счисления константы", width: "100%"}

Префикс Система счисления Пример
0 Восьмеричная echo "$((071)) = 57"
0x Шестнадцатеричная echo "$((0xFF)) = 255"
0X Шестнадцатеричная echo "$((0XFF)) = 255"
<основание># Система с указанным основанием от 2 до 64 echo "$((16#FF)) = 255"
echo "$((2#101)) = 5"

При выводе на экран или в файл Bash всегда переводит значения чисел в десятичную систему. Встроенная команда printf меняет формат вывода чисел. Её можно вызвать, например, так: {line-numbers: false, format: Bash}

printf "%x\n" 250

Эта команда выведет на экран число 250 в шестнадцатеричной системе.

Аналогично можно вывести и значение переменной: {line-numbers: false, format: Bash}

printf "%x\n" $var

Арифметические действия

Начнём с самых простых математических операций — арифметических. В языках программирования они обозначаются привычными символами:

    • сложение
    • вычитание
  • / деление
    • умножение

Кроме них в программировании часто встречаются ещё два действия: возведение в степень и вычисление остатка от деления.

Возведение в степень принято записывать в виде a^b^. Здесь a является основанием, а b — показателем степени. Например, два в степени семь записывается как 2^7^. В Bash это арифметическое действие обозначается двумя звёздочками: {line-numbers: false}

2**7

Вычисление остатка от деления — это сложная, но важная в программировании операция. Рассмотрим её подробнее. Предположим, что мы разделили одно целое число на другое. В результате получилось дробное число. Тогда говорят, что при делении появился остаток.

Например, разделим 10 (делимое) на 3 (делитель). Если округлить результат, получим 3,33333 (частное). В этом случае остаток от деления равен 1. Чтобы его найти, умножим делитель 3 на целую часть частного 3 (неполное частное). Результат вычтем из делимого 10. Получим остаток 1.

Запишем наши вычисления в виде формул. Для этого введём следующие обозначения:

  • a — делимое
  • b — делитель
  • q — неполное частное
  • r — остаток

Тогда делимое вычисляется по формуле: {line-numbers: false}

a = b * q + r

Отсюда выведем формулу для нахождения остатка: {line-numbers: false}

r = a - b * q

Выбор неполного частного q вызывает вопросы. Иногда на его роль подходят несколько чисел. Выбрать из них правильное помогает ограничение. Частное q должно быть таким, чтобы остаток от деления r по абсолютной величине оказался меньше делителя b. Другими словами должно выполняться неравенство: {line-numbers: false}

|r| < |b|

Операция нахождения остатка в Bash обозначается знаком процент %. В некоторых языках этим же символом обозначается операция modulo. Это два разных действия. Когда знаки делимого и делителя совпадают, они дают одинаковый результат.

Для примера вычислим остаток и modulo при делении 19 на 12 и -19 на -12. Получим: {line-numbers: false}

19 % 12 = 19 - 12 * 1 = 7
19 modulo 12 = 19 - 12 * 1 = 7

-19 % -12 = -19 - (-12) * 1 = -7
-19 modulo -12 = -19 - (-12) * 1 = -7

Теперь рассмотрим случаи, когда знаки делимого и делителя различаются: {line-numbers: false}

19 % -12 = 19 - (-12) * (-1) = 7
19 modulo -12 = 19 - (-12) * (-2) = -5

-19 % 12 = -19 - 12 * (-1) = -7
-19 modulo 12 = -19 - 12 * (-2) = 5

Остаток и modulo различаются.

Для расчёта modulo применяется та же формула, что и для остатка. Отличается только выбор неполного частного q. Для нахождения остатка, частное вычисляется по формуле: {line-numbers: false}

q = a / b

Результат округляется к меньшему по модулю числу. То есть все знаки после запятой отбрасываются.

Неполное частное для modulo считается по-разному в зависимости от знаков a и b. Если знаки совпадают, формула для частного та же: {line-numbers: false}

q = a / b

Если знаки разные, формула другая: {line-numbers: false}

q = (a / b) + 1

В обоих случаях результат округляется к меньшему по модулю числу.

Когда говорят об остатке от деления r, обычно предполагают, что и делимое a и делитель b положительны. Поэтому в справочниках часто встречается такое условие для остатка: {line-numbers: false}

0 ≤ r < |b|

Однако при делении чисел с разными знаками остаток может быть отрицательным. Запомните простое правило: у остатка r всегда такой же знак, что и у делимого a. Если знаки различаются, значит вы нашли modulo.

Всегда помните о различии остатка от деления и modulo. Одни языки программирования вычисляют остаток в операторе %, другие — modulo. Это приводит к путанице.

Если сомневаетесь в своих вычислениях, проверьте их. Bash в операторе % всегда считает остаток от деления. Предположим, что нужно найти остаток деления 32 на -7. Следующая команда выведет результат: {line-numbers: false, format: Bash}

echo $((32 % -7))

Остаток от деления равен четырём.

Теперь найдём modulo для этой же пары чисел. Воспользуйтесь онлайн-калькулятором. В поле "Expression" введите 32, в поле "Modulus" — 7. Нажмите кнопку "CALCULATE". Вы получите два результата:

  • "Result" равен 4.
  • "Symmetric representation" равно -3.

Второй ответ -3 и есть modulo.

Для чего в программировании используют остаток от деления? Самая распространённая задача — это проверка числа на чётность. С её помощью контролируется целостность переданных данных в компьютерных сетях. Такая проверка называется бит контроля чётности.

I> Чтобы проверить число на чётность, достаточно найти остаток его деления на 2. Если остаток равен нулю, значит число чётное. В противном случае — нечётное.

Другая задача, в которой не обойтись без вычисления остатка, — это преобразование единиц времени. Рассмотрим пример. Предположим, что 128 секунд надо перевести в минуты. Для этого подсчитаем целое число минут в 128 секундах. Затем добавим к результату остаток.

Чтобы найти целое число минут, разделим 128 на 60. Получим неполное частное 2. То есть в 128 секундах — 2 минуты. Чтобы найти оставшиеся секунды, вычислим остаток от деления 128 на 60: {line-numbers: false}

r = 128 - 60 * 2 = 8

Остаток равен 8. Получается, что 128 секунд равны двум минутам и восьми секундам.

Вычисление остатка будет полезно и при работе с циклами. Например, если нужно выполнять действие на каждой N-ой итерации цикла. Предположим, что нас интересует каждая 10 итерация. Тогда надо проверять остаток от деления счётчика цикла на 10. Если остаток равен нулю, значит текущая итерация кратна 10.

Операция modulo широко применяется в криптографии.

{caption: "Упражнение 3-8. Вычисление modulo и остатка от деления", format: text, line-numbers: false}

Вычислите modulo и остаток от деления:

* 1697 % 13
* 1697 modulo 13

* 772 % -45
* 772 modulo -45

* -568 % 12
* -568 modulo 12

* -5437 % -17
* -5437 modulo -17

Битовые операции

Битовые операции — это ещё один тип математических действий. Эти операции активно используется в программировании. Своё название они получили потому, что выполняются над каждым битом числа по отдельности. Чтобы выполнить битовую операцию, число представляется в двоичном виде. Затем действие выполняется над каждым его битом.

Побитовое отрицание

Начнём с самой простой битовой операции — отрицания. В компьютерной литературе она иногда обозначается как НЕ или NOT. В Bash отрицание обозначается знаком тильда ~.

Чтобы выполнить побитовое отрицание, замените значение каждого бита числа на противоположное. То есть каждая единица заменяется на ноль и наоборот.

Например, выполним побитовое отрицание числа 5. Получим: {line-numbers: false}

5 = 101
~5 = 010

Если ограничиться чистой математикой, побитовое отрицание — это очень простая операция. В программировании же с ней возникают сложности. Прежде всего встаёт вопрос: сколько байтов отводится под число? Предположим, что в нашем примере число 5 хранится в двухбайтовой переменной. Тогда в памяти в двоичном виде оно выглядит так: {line-numbers: false}

00000000 00000101

После побитового отрицания значение переменной станет таким: {line-numbers: false}

11111111 11111010

Как интерпретировать полученный результат? Если переменная объявлена как беззнаковое целое, результатом будет число 65530 в прямом коде. Если же переменная знаковая, её значение хранится в дополнительном коде. В этом случае результатом будет -6.

Команды и операторы Bash представляют целые по-разному. Например, echo всегда выводит числа как знаковые. Команда printf позволяет указать формат вывода: знаковое или беззнаковое целое. 

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

В Bash под целые числа отводится 64 бита независимо от наличия знака. Таблица 3-20 демонстрирует их максимальные и минимальные допустимые значения.

{caption: "Таблица 3-20. Максимальные и минимальные целые в Bash", width: "100%"}

Целое число Шестнадцатеричная система Десятичная система
Максимальное положительное знаковое 7FFFFFFFFFFFFFFF 9223372036854775807
Минимальное отрицательное знаковое 8000000000000000 -9223372036854775808
Максимальное беззнаковое FFFFFFFFFFFFFFFF 18446744073709551615

Следующие примеры демонстрируют интерпретацию целых в командах echo, printf и операторе ((: {line-numbers: true, format: Bash}

$ echo $((16#FFFFFFFFFFFFFFFF))
-1

$ printf "%llu\n" $((16#FFFFFFFFFFFFFFFF))
18446744073709551615

$ if ((18446744073709551615 == 16#FFFFFFFFFFFFFFFF)); then echo "ok"; fi
ok

$ if ((-1 == 16#FFFFFFFFFFFFFFFF)); then echo "ok"; fi
ok

$ if ((18446744073709551615 == -1)); then echo "ok"; fi
ok

Последний пример со сравнением чисел 18446744073709551615 и -1 показывает, что знаковые и беззнаковые целые хранятся в памяти одинаково. Но в зависимости от контекста они интерпретируются по-разному.

Вернёмся к примеру с побитовым отрицанием числа 5. В Bash его результатом будет 64 битное число 0xFFFFFFFFFFFFFFFA в шестнадцатеричной системе. Число можно вывести как положительное или как отрицательное целое: {line-numbers: true, format: Bash}

$ echo $((~5))
-6

$ printf "%llu\n" $((~5))
18446744073709551610

Числа 18446744073709551610 и -6 равны с точки зрения Bash. Потому что все их биты в памяти совпадают.

{caption: "Упражнение 3-9. Вычисление побитового отрицания", format: text, line-numbers: false}

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

* 56
* 1018
* 58362

Повторите вычисления для случая, когда эти целые являются знаковыми.

Побитовое И, ИЛИ, исключающее ИЛИ

Операция побитового И также известна как AND. Она напоминает логическое И.

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

Побитовое И выполняется над двумя числами. Они представляются в двоичном виде. Затем над каждой соответствующей парой битов двух чисел выполняется логическое И.

Запишем подробнее алгоритм для выполнения побитового И:

  1. Представить оба операнда в двоичном виде.

  2. Если число битов в одном операнде меньше чем в другом, дополнить его слева нулями.

  3. Применить логическое И к каждой паре битов, которые стоят на одинаковых позициях. То есть выполнить логическое И с первым битом первого числа и с первым битом второго числа. Затем перейти ко второму биту и т.д.

Рассмотрим пример. Вычислим побитовое И для чисел 5 и 3. В двоичном виде они выглядят так: {line-numbers: false}

5 = 101
3 = 11

У числа 3 оказалось меньше битов, чем у 5. Поэтому дополним его двоичное представление одним нулём слева. Получим: {line-numbers: false}

3 = 011

Теперь выполним операцию логического И для каждой пары битов чисел 5 и 3. Для удобства запишем двоичное представление чисел в столбик. Получим следующее: {line-numbers: false}

101
011
---
001

Переведём результат в десятичную систему: {line-numbers: false}

001 = 1

Это значит, что результат побитового И для чисел 5 и 3 равен 1.

В Bash операция побитового И обозначается знаком амперсанд &. Выполним наше вычисление и выведем результат на экран: {line-numbers: false, format: Bash}

echo $((5 & 3))

Операция побитового ИЛИ (OR) выполняется аналогично побитовому И. Только вместо логического И над каждой парой битов чисел выполняется логическое ИЛИ.

Вычислим побитовое ИЛИ для чисел 10 и 6. В двоичном виде они выглядят так: {line-numbers: false}

10 = 1010
6 = 110

Число 6 надо дополнить нулём до четырёх битов: {line-numbers: false}

6 = 0110

Теперь выполним логическое ИЛИ над каждой парой битов чисел 10 и 6: {line-numbers: false}

1010
0110
----
1110

Переведём результат в десятичную систему: {line-numbers: false}

1110 = 14

В Bash побитовое ИЛИ обозначается знаком |. Выведем результат для нашего примера: {line-numbers: false, format: Bash}

echo $((10 | 6))

Операция побитового исключающего ИЛИ (XOR) похожа на побитовое ИЛИ. В ней над каждой парой битов операндов выполняется логическое исключающее ИЛИ. В исключающем ИЛИ если оба операнда равны единице, результат будет ноль. В остальных случаях результат будет такой же, как у обычного ИЛИ.

Вычислим исключающее ИЛИ для чисел 12 и 5. Переведём числа в двоичный вид: {line-numbers: false}

12 = 1100
5 = 101

Дополним число 5 до четырёх битов: {line-numbers: false}

5 = 0101

Выполним побитовое исключающее ИЛИ для каждой пары битов: {line-numbers: false}

1100
0101
----
1001

Переведём результат в десятичную систему: {line-numbers: false}

1001 = 9

В Bash исключающее ИЛИ обозначается символом ^. Расчёт нашего примера будет выглядеть так: {line-numbers: false, format: Bash}

echo $((12 ^ 5))

{caption: "Упражнение 3-10. Вычисление побитовых И, ИЛИ, исключающего ИЛИ", format: text, line-numbers: false}

Выполните побитовое И, ИЛИ, исключающее ИЛИ для следующих беззнаковых двухбайтовых целых:

* 1122 и 908
* 49608 и 33036

Битовые сдвиги

Битовым сдвигом называется смена позиций битов числа.

Есть три типа сдвигов:

  1. Логический
  2. Арифметический
  3. Циклический

Самый простой из них — это логический. Рассмотрим сначала его.

Операция битового сдвига принимает два операнда. Первый — это число, над которым выполняется операция. Второй — количество битов, на которое происходит сдвиг.

Чтобы выполнить логический сдвиг, представьте исходное число в двоичном виде. Предположим, что выполняется сдвиг вправо на два бита. Тогда два крайние бита числа справа отбрасываются. Вместо них слева добавляются нули. Аналогично выполняется сдвиг влево. Два бита слева отбрасываются. Два нуля справа добавляются.

Рассмотрим пример. Выполним логический сдвиг беззнакового однобайтового целого 58 вправо на три бита. Сначала представим число в двоичном виде: {line-numbers: false}

58 = 0011 1010

Теперь отбросим три бита справа: {line-numbers: false}

0011 1010 >> 3 = 0011 1

Затем дополним результат нулями слева: {line-numbers: false}

0011 1 = 0000 0111 = 7

Результат сдвига — число 7.

Попробуем сдвинуть число 58 на три бита влево. Получим следующее: {line-numbers: false}

0011 1010 << 3 = 1 1010 = 1101 0000 = 208

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

Рассмотрим второй тип сдвига — арифметический. Влево он выполняется точно так же, как и логический сдвиг.

Арифметический сдвиг вправо отличается от логического. Чтобы его выполнить, отбросьте нужное количество битов справа. Затем дополните результат битами справа. Их значение должно совпадать со старшим битом числа. Если он равен единице, добавляем справа единицы. В противном случае добавляем нули. Благодаря этому, после сдвига знак числа не меняется.

Для примера выполним арифметический сдвиг знакового однобайтового целого -105 вправо на два бита.

Сначала представим число в дополнительном коде: {line-numbers: false}

-105 = 1001 0111

Теперь выполним сдвиг вправо на два бита. Получим: {line-numbers: false}

1001 0111 >> 2 -> 1001 01 -> 1110 0101

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

Мы получили отрицательное число в дополнительном коде. Переведём его в прямой: {line-numbers: false}

1110 0101 = 1001 1011 = -27

Результат сдвига — число -27.

Операции << и >> интерпретатора Bash выполняют арифметические сдвиги. Рассмотренные нами примеры можно вычислить с помощью следующих команд: {line-numbers: true, format: Bash}

$ echo $((58 >> 3))
7

$ echo $((58 << 3))
464

$ echo $((-105 >> 2))
-27

Результат сдвига 58 влево на три бита отличается от нашего, потому что Bash оперирует восьмибайтовыми целыми.

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

В циклическом сдвиге отброшенные биты появляются на освободившемся месте с другого конца числа.

Например, выполним циклический сдвиг числа 58 вправо на три бита. Результат будет следующим: {line-numbers: false}

0011 1010 >> 3 = 010 0011 1 = 0100 0111 = 71

Отброшенные справа биты 010 оказались в левой части результата.

{caption: "Упражнение 3-11. Вычисление битовых сдвигов", format: text, line-numbers: false}

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

* 25649 >> 3
* 25649 << 2
* -9154 >> 4
* -9154 << 3

Применение битовых операций

Битовые операции широко применяются в системном программировании. Часто при работе с компьютерной сетью и периферийными устройствами приходится переводить данные из одного формата в другой.

Рассмотрим пример. Предположим, вы работаете с периферийным устройством. На устройстве порядок байтов от старшего к младшему (big-endian). Ваш компьютер использует другой порядок — от младшего к старшему (little-endian).

Устройство посылает на компьютер целое беззнаковое число. В шестнадцатеричной системе оно равно 0xAABB. Чтобы компьютер правильно прочитал это число, надо изменить порядок байтов в нём. После преобразования число 0xAABB станет равно 0xBBAA.

Для изменения порядка байтов сделаем следующее:

  1. Прочитаем младший байт числа (крайний справа) и сдвинем его влево на восемь битов, т.е. на один байт. Это делает следующая Bash-команда: {line-numbers: false, format: Bash}
little=$(((0xAABB & 0x00FF) << 8))
  1. Прочитаем старший байт числа (крайний слева) и сдвинем его вправо на восемь битов. Команда: {line-numbers: false, format: Bash}
big=$(((0xAABB & 0xFF00) >> 8))
  1. Соединим старший и младший байты с помощью побитового ИЛИ: {line-numbers: false, format: Bash}
result=$((little | big))

В результате в переменную result запишется число 0xBBAA.

Все шаги нашего вычисления можно выполнить одной командой: {line-numbers: false, format: Bash}

value=0xAABB
result=$(( ((value & 0x00FF) << 8) | ((value & 0xFF00) >> 8) ))

Другой пример применения битовых операций. Они незаменимы для вычисления масок. Нам уже знакомы маски с правами доступа к файлам в Unix-окружении. Предположим, что файл имеет права -rw-r--r--. В двоичном виде эта маска выглядит так: {line-numbers: false}

0000 0110 0100 0100

Проверим, имеет ли владелец файла право на его исполнение. Для этого вычислим побитовое И с маской 0000 0001 0000 0000. Получим: {line-numbers: false}

0000 0110 0100 0100 & 0000 0001 0000 0000 = 0000 0000 0000 0000 = 0

Результат равен нулю. Это значит, что владелец не может исполнять файл.

Для добавления битов в маску применяется побитовое ИЛИ. Добавим владельцу файла право на исполнение. Вычисление выглядит так: {line-numbers: false}

0000 0110 0100 0100 | 0000 0001 0000 0000 = 0000 0111 0100 0100 = -rwxr--r--

I> Нумерация битов в числе начинается с нуля и обычно идёт справа налево.

Мы выполнили побитовое ИЛИ маски с числом 0000 0001 0000 0000. В нём восьмой бит равен единице. С его помощью мы меняем восьмой бит маски. При этом значение бита маски неважно. Он будет выставлен в единицу не зависимо от текущего значения. Все биты числа кроме восьмого равны нулям. Благодаря этому, биты маски в тех же позиция не изменятся.

Для удаления битов из маски применяется побитовое И. Например, удалим право владельца файла на запись. Получим следующее: {line-numbers: false}

0000 0111 0100 0100 & 1111 1101 1111 1111 = 0000 0101 0100 0100 = -r-xr--r--

Чтобы выставить девятый бит маски в ноль, мы выполнили побитовое И с числом 1111 1101 1111 1111. В нём девятый бит равен нулю, а все остальные — единицам. Поэтому в результате побитового И изменится только девятый бит маски. Все остальные сохранят свои значения.

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

Рассмотрим последний пример использования битовых операций. До недавнего времени битовые сдвиги широко применялись как альтернатива умножения и деления на степень двойки. Например, сдвиг влево на два бита соответствует умножению на 2^2^ (т.е. четыре). Проверим это утверждение такой Bash-командой: {line-numbers: true, format: Bash}

$ echo $((3 << 2))
12

Результат правильный. Умножение 3 на 4 даст 12.

Для примера сдвиг вправо на три бита соответствует делению на 2^3^ (т.е. восемь). Проверим: {line-numbers: true, format: Bash}

$ echo $((16 >> 3))
2

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

Битовые операции также активно применяются в криптографии и компьютерной графике.

Логические операции

Для сравнения целых чисел в конструкции if оператор [[ неудобен. В нём отношения между числами обозначают двухбуквенные сокращения. Например, -gt для отношения больше. Удобнее использовать оператор (( в форме арифметической оценки. Тогда сокращения заменяются на привычные символы сравнения чисел (>, <, =).

Рассмотрим пример. Предположим, что значение переменной надо сравнить с константой 5. Это сделает следующая конструкция if: {line-numbers: true, format: Bash}

if ((var < 5))
then
  echo "Значение var меньше 5"
fi

Оператор (( можно заменить на команду let. В результате получим то же поведение: {line-numbers: true, format: Bash}

if let "var < 5"
then
  echo "Значение var меньше 5"
fi

Однако оператор (( использовать всегда предпочтительнее.

Обратите внимание на важное отличие арифметической оценки и подстановки. Согласно POSIX-стандарту, любая программа или команда при успешном выполнении возвращает ноль. При ошибке возвращается код возврата от 1 до 255. Этот код интерпретируется так: ноль означает истину, а не ноль — ложь. В этом смысле результат арифметической подстановки инвертирован, а оценки нет.

Арифметическая оценка — это синоним команды let. Значит она подчиняется требованиям POSIX-стандарта, как и любая другая команда. Арифметическая подстановка выполняется в контексте другой команды. Поэтому результат её работы зависит от реализации интерпретатора. В Bash если условие в операторе (( в форме подстановки истинно, будет возвращена единица. В противном случае оператор возвращает ноль. Такое поведение соответствует правилам вывода логических выражений языка C.

Рассмотрим пример. Предположим, есть команда для вывода результата сравнения переменной с числом. Она выглядит так: {line-numbers: false, format: Bash}

((var < 5)) && echo "Значение var меньше 5"

Здесь используется арифметическая оценка. Поэтому если значение переменной меньше 5, оператор (( выполнится успешно. Тогда, согласно стандарту POSIX, он вернёт код ноль.

Если использовать оператором (( в форме арифметической подстановки, результат будет отличаться. Например: {line-numbers: false, format: Bash}

echo "$((var < 5))"

Если условие истинно, команда echo выведет число 1. Такой результат согласуется с правилами вывода языка C.

Логические операции обычно применяют в форме арифметической оценки оператора ((. Они работают так же, как логические операторы Bash.

Для примера сравним значение переменной с двумя константами: {line-numbers: true, format: Bash}

if ((1 < var && var < 5))
then
  echo "Значение var меньше 5, но больше 1"
fi

В этом случае условие истинно, когда выполняются оба неравенства.

Аналогично работает логическое ИЛИ: {line-numbers: true, format: Bash}

if ((var < 1 || 5 < var))
then
  echo "Значение var меньше 1 или больше 5"
fi

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

Логическое НЕ редко применяется к самим числам. Чаще его используют для отрицания выражения. Если применить НЕ к числу, вывод результата соответствует POSIX-стандарту. Другими словами ноль означает истинна, а не ноль — ложь. Например: {line-numbers: true, format: Bash}

if ((! var))
then
  echo "Значение var равно истина или ноль"
fi

Это условие выполнится только, если переменная равна нулю.

Инкремент и декремент

Операции инкремента и декремента впервые появились в языке программирования B. Кен Томпсон и Денис Ритчи разработали его в 1969 году, работая в Bell Labs. Позднее Денис Ритчи перенёс эти операции в свой новый язык C. Оттуда их скопировали в Bash.

Начнём с операций присваивания. Тогда смысл инкремента и декремента станет понятнее.

Обычное присваивание в арифметической оценке выглядит так: {line-numbers: false, format: Bash}

((var = 5))

В результате значение переменной var станет равно 5.

Bash позволяет объединить присваивание с арифметическим действием или битовой операцией. Например, одновременное сложение и присваивание выглядит так: {line-numbers: false, format: Bash}

((var += 5))

Здесь выполняются два действия:

  1. К текущему значению переменной var прибавляется число 5.

  2. Результат сложения записывается в переменную var.

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

Теперь рассмотрим операции инкремента и декремент. У них есть две формы: постфиксная и префиксная. Они записываются по-разному. В постфиксной форме знаки ++ и -- идут после имени переменной, а в префиксной — до.

Рассмотрим префиксный инкремент: {line-numbers: false, format: Bash}

((++var))

Результат этой команды такой же, как у следующей операции присваивания: {line-numbers: false, format: Bash}

((var+=1))

Инкремент увеличивает значение переменной на единицу. Декремент — уменьшает на единицу.

Зачем вводить отдельные операции для прибавления и вычитания единицы? Ведь есть достаточно компактные операции сложения и вычитания, совмещённые с присваиванием (+= и -=).

Скорее всего дело в счётчике цикла. Он часто используется в программировании. Счётчик отсчитывать номер итерации цикла. Это нужно, чтобы вовремя прервать его выполнение. Инкремент и декремент упрощают работу со счётчиком. Кроме того современные процессоры выполняют эти операции на аппаратном уровне. Поэтому они работают быстрее, чем сложение и вычитание с присваиванием.

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

Например, следующие команды увеличат значение переменной на единицу: {line-numbers: true, format: Bash}

((++var))
((var++))

Разница между формами инкремента появляется, при присваивании результата переменной.

Рассмотрим следующий пример: {line-numbers: true, format: Bash}

var=1
((result = ++var))

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

Если расписать префиксный инкремент по отдельным командам, получится следующее: {line-numbers: true, format: Bash}

var=1
((var = var + 1))
((result = var))

Поведение постфиксного инкремента отличается. Заменим форму инкремента в нашем примере: {line-numbers: true, format: Bash}

var=1
((result = var++))

После выполнения команд в переменную result запишется единица, а в var — двойка. Постфиксный инкремент сначала возвращает значение, а потом прибавляет единицу.

Распишем постфиксный инкремент по отдельным командам: {line-numbers: true, format: Bash}

var=1
((tmp = var))
((var = var + 1))
((result = tmp))

Обратите внимание на порядок выполнения постфиксного инкремента. Сначала var увеличивается на единицу. Только после этого её прошлое значение возвращается в качестве результата. Поэтому прошлое значение var пришлось сохранить во временную переменную tmp.

Постфиксная и префиксная формы декремента работают аналогично инкременту.

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

Тернарная условная операция

Тернарная условная операция также известна как тернарный оператор. Она впервые появилась в языке Алгол. Операция оказалась удобной и востребованной программистами. Поэтому её добавили в языки следующего поколения: BCPL и C. Дальше её переняли почти все современные языки: C++, C#, Java, Python, PHP и т.д.

Тернарный оператор представляет собой компактную форму конструкции if.

Для примера рассмотрим такой оператор if: {line-numbers: true, format: Bash}

if ((var < 10))
then
    ((result = 0))
else
    ((result = var))
fi

Здесь переменной result присваивается ноль, если var меньше 10. В противном случае result присваивается значение var.

Такое же поведение даст тернарный оператор. Он выглядит так: {line-numbers: false, format: Bash}

((result = var < 10 ? 0 : var))

Одна строка заменила шесть строк конструкции if.

Тернарный оператор состоит из условного выражения и двух действий. В общем случае он выглядит так: {line-numbers: false}

(( УСЛОВИЕ ? ДЕЙСТВИЕ 1 : ДЕЙСТВИЕ 2 ))

Если УСЛОВИЕ истинно, выполняется ДЕЙСТВИЕ 1. Иначе — ДЕЙСТВИЕ 2. Такое поведение полностью совпадает с условным оператором if. Запишем его тоже в общем виде: {line-numbers: true}

if УСЛОВИЕ
then
    ДЕЙСТВИЕ 1
else
    ДЕЙСТВИЕ 2
fi

Сравните тернарный оператор и конструкцию if.

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

Используйте тернарный оператор как можно чаще. Это считается хорошей практикой. С ним код станет компактнее и удобнее для чтения. Также считается, что в меньшем объёме кода меньше места для возможной ошибки.