ИТ Блог. Администрирование серверов на основе Linux (Ubuntu, Debian, CentOS, openSUSE)

Математическая арифметика: как выполнить вычисления в Bash?

Математическая арифметика: как выполнить вычисления в Bash?

Математика и арифметика часто используются в написании сценариев на Bash, особенно при написании отчетов crontab, плагинов мониторинга, настройке сценариев с динамическими конфигурациями или других задач автоматизации, таких как отображение температуры процессора Raspberry PI. Всегда нужно произвести какое-то арифметическое вычисление.

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

x=3; 
y=$((x+2)); 
echo $y

 

выведет 5

 

Введение в целые числа и числа с плавающей запятой

Прежде чем мы перейдем к подробностям о том, как выполнять математику в Bash, помните, что integer – это целое число, которое не является дробью или десятичной дробью и находится в любом месте от нуля до положительной или отрицательной бесконечности. Например, 42, 36 и -12 являются целыми числами, в то время как 3,14 и √ 2 – нет. Набор целых чисел включает в себя все отрицательные целые числа (целые числа, значение которых меньше нуля), ноль и все положительные целые числа (целые числа, значение которых больше нуля). Целое число и его противоположность находятся на одинаковом расстоянии от нуля.

Число с плавающей запятой – это термин вычислительного программирования, обозначающий действительное число с дробной частью. Например, 3,14, √ 2, -10,5, 4e-2 являются числами с плавающей запятой. Значение с плавающей запятой определяется базой (двоичной или десятичной), точностью и диапазоном экспонент. Обычно это двоичный файл, состоящий из 32 (простая точность) или 64 бит (двойная точность), хотя в стандарте IEEE 754 указано больше форматов.

Формат Общее количество бит Значимые биты Биты экспоненты Наименьшее число Наибольшее число
Одинарная точность 32 23 + 1 знак 8 ≈ 1.2 ⋅ 10-38 ≈ 3.4 ⋅ 1038
Двойная точность 64 52 + 1 знак 11 ≈ 2.2 ⋅ 10-308 ≈ 1.8 ⋅ 10308

 

Вы можете увидеть, что число с плавающей запятой представлено в виде значения x базового показателя. Например: 3,14 = 314 x 10-2

 

Каковы арифметические операторы Bash?

Оболочка Bash имеет большой список поддерживаемых арифметических операторов для выполнения математических вычислений. Они работают с методами let, declare и арифметического расширения, описанными ниже в этом посте.

Арифметический оператор Описание
id++, id– переменная после увеличения, после уменьшения
++id, –id предварительное увеличение переменной, предварительное уменьшение
-, + унарный минус, плюс
!, ~ логическое и побитовое отрицание
** возведение в степень
*, /, % умножение, деление, остаток (по модулю)
+, – сложение, вычитание
«, » побитовые сдвиги влево и вправо
<=, >=, <, > сравнение
==, != равенство, неравенство
& побитовое и
^ побитовое XOR
| побитовое ИЛИ
&& логические И
|| логический ИЛИ
выражение ? expression : выражение условный оператор
=, *=, /=, %=, +=, -=, «=, »=, &=, ^=, |= задание

 

Выполнение математики в Bash с помощью Integer

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

 

Используя командную строку expr

Устаревшим способом выполнения математических вычислений с помощью integer, и только integer, долгое время было использование командной строки expr. Хотя этот метод может быть медленным, поскольку expr является двоичным, а не встроенной оболочкой. Он вызовет новый процесс, который не идеален в большом цикле for. Кроме того, expr поведение может отличаться в разных системах в зависимости от реализации.

# Вычитание
[andreyex@linux ~]$ expr 1 - 1
0
# Дополнение
[andreyex@linux ~]$ expr 1 + 1
2
# Присвоить результат переменной
[andreyex@linux ~]$ myvar=$(expr 1 + 1)
[andreyex@linux ~]$ echo $myvar
2
# Сложение с переменной
[andreyex@linux ~]$ expr $myvar + 1
3
# Деление
[andreyex@linux ~]$ expr $myvar / 3
0
# Умножение
[andreyex@linux ~]$ expr $myvar \* 3
6

 

⚠️ При умножении на обязательно используйте звездочку (*) или любые другие подстановочные знаки bash, чтобы предотвратить расширение шаблона в Bash. Вы можете экранировать специальный символ, используя обратную косую черту (\), пример: expr $myvar \* 3. Отсутствие экранирования * приведет к expr: syntax error.

 

Используя встроенные команды let или declare оболочки

Альтернативой устаревшей команде expr является использование встроенной команды let или declare для вычисления арифметических выражений. Встроенный метод declare требует опции -i для выполнения целочисленного объявления.

[andreyex@linux ~]$ myvar=6 ; echo $myvar
6
[andreyex@linux ~]$ let myvar+=1 ; echo $myvar
7
[andreyex@linux ~]$ let myvar+1 ; echo $myvar
8
[andreyex@linux ~]$ let myvar2=myvar+1 ; echo $myvar2
9

 

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

[andreyex@linux ~]$ let x=4 y=5 z=x*y u=z/2
[andreyex@linux ~]$ echo $x $y $z $u
4 5 20 10

[andreyex@linux ~]$ declare -i x=4 y=5 z=x*y u=z/2
[andreyex@linux ~]$ echo $x $y $z $u
4 5 20 10

 

⚠️ Целочисленное объявление с использованием declare -i обозначения может привести к запутанным или трудночитаемым сценариям оболочки. Переменная, заданная как целое число, может принимать нецелочисленные значения, но она не будет сохранять и выводить ожидаемую новую строку и может выдать ошибку. Использование declare -i принудительно делает переменную только арифметическим контекстом. Это все равно, что каждый раз префиксить присваивание переменной с помощью команды let. Вместо этого, как правило, понятнее использовать расширение Bash Arithmetic, как подробно описано в следующем разделе.

 

[andreyex@linux ~]$ declare -i myvar3=myvar2*2 ; echo $myvar3
18
[andreyex@linux ~]$ echo $myvar3 ; myvar3="none"; echo $myvar3
18
0

 

Используя арифметическое расширение Bash

Рекомендуемый способ вычисления арифметических выражений с целыми числами в Bash – использовать возможности арифметического расширения командной оболочки. Встроенное расширение оболочки позволяет использовать круглые скобки ((…)) для выполнения математических вычислений.

Формат арифметического расширения Bash – $(( arithmetic expression )). Расширение оболочки вернет результат последнего заданного выражения.

Обозначение $((…)) – это то, что называется арифметическим расширением, в то время как ((…)) обозначение называется составной командой, используемой для вычисления арифметического выражения в Bash.

Предпочтительным способом должно быть обозначение арифметического расширения, если только не выполняется арифметическое вычисление в операторе if в Bash, в цикле for или аналогичных операторах.

👉 Квадратные скобки $[…] также могут выполнять арифметическое разложение в Bash, хотя это обозначение устарело и его следует избегать. Вместо этого предпочитайте использовать вместо $((…)).

 

[andreyex@linux ~]$ myvar=3 && echo $myvar
3
[andreyex@linux ~]$ echo $((myvar+2))
5
[andreyex@linux ~]$ myvar=$((myvar+3))
[andreyex@linux ~]$ echo $myvar
6
[andreyex@linux ~]$ ((myvar+=3))
[andreyex@linux ~]$ echo $myvar
9

 

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

[andreyex@linux ~]$ myvar=3 && echo $myvar
3
[andreyex@linux ~]$ echo $((myvar++))
3
[andreyex@linux ~]$ echo $myvar
4
[andreyex@linux ~]$ echo $((++myvar))
5
[andreyex@linux ~]$ echo $myvar
5

 

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

[andreyex@linux ~]$ echo $((x=4,y=5,z=x*y,u=z/2))
10
[andreyex@linux ~]$ echo $x $y $z $u
4 5 20 10

[andreyex@linux ~]$ ((x=4,y=5,z=x*y,u=z/2)) ; echo $x $y $z $u
4 5 20 10

 

Выполнение арифметики с плавающей запятой в Bash

Используя встроенную команду printf

Примечательный, но нетрадиционный способ выполнения арифметики с плавающей запятой в родном bash – это объединить арифметическое расширение с printf, используя научную нотацию. Поскольку вы не можете использовать функцию с плавающей запятой в bash, вам нужно просто применить заданный множитель в степени 10 к вашей математической операции внутри арифметического расширения, затем использовать printf для отображения значения с плавающей запятой.

Например, операция 2/3 в Artithmetic Expansion as $((2/3)) вернет результат 0. Чтобы получить число с плавающей запятой, используя printf приведенную ниже формулу, где <precision> будет отображаться точность с плавающей запятой и <multiplier> степень десяти множителей. Аналогично, множитель 3 будет означать 10**3, что 1000 равно.

printf %.<precision>f "$((10**<multiplier> * 2/3))e-<multiplier>

 

Обратите внимание, что точность с плавающей запятой в %.<precision>f не должна быть выше самого множителя, поскольку он просто заполнится нулями.

[andreyex@linux ~]$ printf %.3f "$((10**3 * 2/3))e-3"
0.666
[andreyex@linux ~]$ printf %.1f "$((10**3 * 2/3))e-3"
0.7
[andreyex@linux ~]$ printf %.5f "$((10**3 * 2/3))e-3"
0.66600

 

Используя командную строку awk

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

[andreyex@linux ~]$ awk "BEGIN {print 100/3}"
33.3333
[andreyex@linux ~]$ awk "BEGIN {x=100/3; y=6; z=x*y; print z}"
200
[andreyex@linux ~]$ awk "BEGIN {printf \"%.2f\n\", 100/3}"
33.33

 

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

[andreyex@linux ~]$ awk "BEGIN {print -8.4--8}"
awk: cmd. line:1: BEGIN {print -8.4--8}
awk: cmd. line:1:                    ^ syntax error
[andreyex@linux ~]$ awk "BEGIN {print -8.4 - -8}"
-0.4

 

Используя командную строку bc

Поскольку вы не можете выполнять арифметику с плавающей запятой изначально в Bash, вам придется использовать инструмент командной строки. Наиболее распространенным является “bc – язык калькулятора произвольной точности”.

Чтобы запустить интерактивный режим, вам просто нужно ввести bc в командной строке. Вы можете использовать параметр -q (quiet, тихий), чтобы удалить начальный bc баннер.

[andreyex@linux ~]$ bc
bc 1.06
Copyright 1991-1994, 1997, 1998, 2000 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
3*5.2+7/8
15.6
15.6+299.33*2.3/7.4
108.6

 

Конечно, вы также можете использовать bc в неинтерактивном режиме, используя STDIN для отправки вашей формулы, чтобы bc затем получить выходные данные на STDOUT, или используя нотацию here-doc.

# Пример переданного по конвейеру арифметического выражения для bc STDIN
[andreyex@linux ~]$ echo "15.6+299.33*2.3/7.4" | bc
108.6

# Пример использования обозначения heredoc
[andreyex@linux ~]$ bc <<< "15.6+299.33*2.3/7.4" 
108.6

 

Существуют четыре специальные переменные: scale, ibase, obase и last. масштаб определяет, как в некоторых операциях используются цифры после запятой. Значение по умолчанию для scale равно 0. ibase и obase определяют базу преобразования для входных и выходных чисел. По умолчанию как для ввода, так и для вывода используется база 10. last (расширение) – это переменная, имеющая значение последнего напечатанного числа.

 

Переменная “scale” необходима для точности ваших результатов, особенно при использовании только целых чисел.

 

👉 Примечание: вы также можете использовать bc -l для использования mathlib и увидеть результат в максимальном масштабе.

 

[andreyex@linux ~]$ echo "2/3" | bc
0
[andreyex@linux ~]$ echo "scale=2; 2/3" | bc
.66
[andreyex@linux ~]$ echo "(2/3)+(7/8)" | bc
0
[andreyex@linux ~]$ echo "scale=2;(2/3)+(7/8)" | bc
1.53
[andreyex@linux ~]$ echo "scale=4;(2/3)+(7/8)" | bc
1.5416
[andreyex@linux ~]$ echo "scale=6;(2/3)+(7/8)" | bc
1.541666
[andreyex@linux ~]$ echo "(2/3)+(7/8)" | bc -l
1.54166666666666666666

 

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

 

Подробные примеры и часто задаваемые вопросы

Как вычислить процент в Bash?

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

Подход с округлением использует поведение оболочки для округления к нулю (0). Сначала мы вычисляем удвоенный процент, затем вычитаем из него обычный процент. Это дает нам формулу:, roundup = (int) (2 * x) – (int) x где x – вычисление в процентах.

[andreyex@linux ~]$ fileProcessed=17; fileTotal=96;

# Точность с плавающей запятой с использованием встроенного метода printf и арифметического расширения
[andreyex@linux ~]$ printf %.2f%% "$((10**3 * 100 * $fileProcessed/$fileTotal))e-3"
17.71%

# Округление с использованием арифметического разложения и только целых чисел
[andreyex@linux ~]$ echo $((200 * $fileProcessed/$fileTotal))
35
[andreyex@linux ~]$ echo $((100 * $fileProcessed/$fileTotal ))
17
[andreyex@linux ~]$ echo $((200 * $fileProcessed/$fileTotal -  100 * $fileProcessed/$fileTotal ))%
18%

 

Как найти факториал в shell-скрипте?

Чтобы вычислить факториал числа в Bash или любой оболочке POSIX, вы можете использовать арифметическое расширение и рекурсивную функцию. Символом факториальной функции в mathematical является !, а факториал определяется формулой n! = (n-1)! * n.

[andreyex@linux ~]$ function f() {
  local x=$1
  if ((x<=1)); then
     echo 1
  else
     n=$(f $((x-1)))
     echo $((n*x))
  fi
}
[andreyex@linux ~]$ f 1
1
[andreyex@linux ~]$ f 2
2
[andreyex@linux ~]$ f 3
6
[andreyex@linux ~]$ f 6
720
[andreyex@linux ~]$ f 8
40320

 

⚠️ Это решение ограничено максимальным значением числовой переменной оболочки в вашей среде. Вы можете попробовать разрешить следующее арифметическое выражение echo $((2**64)). Если результат равен нулю, ваша платформа и среда оболочки не будут поддерживать более 20 factorial. Попытка разрешить факториал с большим числом с помощью этого метода приведет к неточным результатам. Следовательно, вы должны предпочесть использование bc для получения надежных результатов без таких ограничений. Пример рекурсивной факториальной функции с использованием bcсм. Как найти факториал числа в сценарии оболочки с использованием bc?.

 

Как создать простую функцию калькулятора bash?

В качестве мысленного эксперимента вы можете создать команду calculator для выполнения математических операций, используя функцию bash, арифметическое выражение bash и вывод переменной bash. Каждый раз, когда calculator вызывается функция, она обновляет имя переменной на заданное значение или по умолчанию 1 с помощью заданного арифметического оператора. Пример: counter <var_name> <operator> <value>. Вы также можете реализовать интерактивную версию простого калькулятора с помощью bc.

[andreyex@linux ~]$ A=0; B=0;
[andreyex@linux ~]$ calculator() ((${1}=${!1}${3:-+}${2:-1}))
[andreyex@linux ~]$ echo "Counter A=$A and B=$B"
Counter A=0 and B=0
[andreyex@linux ~]$ calculator A; calculator B
[andreyex@linux ~]$ echo "Counter A=$A and B=$B"
Counter A=1 and B=1
[andreyex@linux ~]$ calculator A 5; calculator B 2
[andreyex@linux ~]$ echo "Counter A=$A and B=$B"
Counter A=6 and B=3
[andreyex@linux ~]$ calculator A 5 /; calculator B 2 "*"
Counter A=1 and B=6

 

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

 

Как выполнить математические вычисления для даты, используя арифметическое разложение и printf?

Ниже приведен простой пример выполнения манипуляции с датой с помощью математического вычитания в shell script с использованием новой переменной $EPOCHSECONDS из GNU Bash версии 5 и форматирования даты printf. В примере показано текущее время минус 86400 секунд.

[andreyex@linux ~]$ echo $EPOCHSECONDS
1589153003
[andreyex@linux ~]$ printf 'Current day of the month is the %(%d)T\n' $EPOCHSECONDS
Current day of the month is the 10
[andreyex@linux ~]$ printf 'Previous day of the month was the %(%d)T\n' $((EPOCHSECONDS-86400))
Previous day of the month was the 09

 

Как использовать разные арифметические базы в арифметическом расширении?

С помощью расширения Bash Arithmetic вы можете выполнять вычисления между различными арифметическими базисами. Например, добавьте целое число по основанию 10 к целому числу по основанию 2. Для этого вы можете префиксить каждое число базовым идентификатором и символом хэштега #, используя форму base#number. Основание должно быть десятичным от 2 до 64, представляющим арифметическую базу. Базовое значение по умолчанию, используемое в расширении bash arithmetic, является base 10.

Обратите внимание, что вы также можете добавлять к числам начальный ноль 0 для представления восьмеричных чисел (base 8). Начальное 0x или 0X будет интерпретироваться как шестнадцатеричное. Кроме того, знак доллара $ обязателен перед переменной при указании базового идентификатора

[andreyex@linux ~]$ echo $(( 10#2 + 2#1 ))
3
[andreyex@linux ~]$ echo $(( 2 + 2#1 ))
3
[andreyex@linux ~]$ echo $(( 10#2 + 16#aa ))
172
[andreyex@linux ~]$ echo $(( 010 + 16#aa ))
178
[andreyex@linux ~]$ echo $(( 0x10 + 16#aa ))
186
[andreyex@linux ~]$ x=2 ; y=1a ; echo $(( x * 16#$y ))
52

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

 

[andreyex@linux ~]$ x=-08 ; echo $(( 10#$x ))
bash: -08: value too great for base (error token is "08")
[andreyex@linux ~]$ echo $(( ${x%%[!+-]*}10#${x#[-+]} ));
-8

 

Как устранить ошибку bash, значение которой слишком велико для base (токен ошибки …?

Как упоминалось выше, арифметическое выражение Bash автоматически учитывает числа с начальным нулем как восьмеричные числа (основание 8). Когда переменная используется для представления числа с начальными нулями и состоит из чисел, равных или превышающих 8, это приведет к арифметической ошибке bash, например bash: 08: value too great for base (error token is “08”).

[andreyex@linux ~]$ x=01 ; echo $((x+1))
2
[andreyex@linux ~]$ x=0105 ; echo $((x+1))
70
[andreyex@linux ~]$ x=08 ; echo $((x+1))
bash: 08: value too great for base (error token is "08")

 

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

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

# Пример использования расширенного glob
[andreyex@linux ~]$ shopt -s extglob
[andreyex@linux ~]$ x=08 ; x=${x##+(0)}; echo $((x+1))
9

# Пример использования идентификатора base 10
[andreyex@linux ~]$ x=08 ; echo $((10#$x+1))
9

 

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

 

Как устранить синтаксическую ошибку: недопустимый арифметический оператор?

При использовании арифметического расширения может возникнуть следующая ошибка:

syntax error: invalid arithmetic operator (error token is ...)

 

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

[andreyex@linux ~]$ echo "$((20.0+7))"
-bash: 20.0/7: syntax error: invalid arithmetic operator (error token is ".0+7")

 

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

[andreyex@linux ~]$ a=$' 3\r'
[andreyex@linux ~]$ echo "$((a+7))"
")syntax error: invalid arithmetic operator (error token is "

 

Вы заметите, что в таких случаях сообщение об ошибке даже искажается из-за символа возврата каретки \r в переменной. Чтобы предотвратить подобные проблемы, вам нужно убедиться, что используемые вами переменные правильно отформатированы, удалив управляющие символы, в основном те, которые имеют значения ASCII от 1 (восьмеричный 001) до 31 (восьмеричный 037). Вы можете сделать это с помощью расширения параметров оболочки.

[andreyex@linux ~]$ echo "$((${a//[ $'\001'-$'\037']}+7))"
10

 

👉 Перед выпуском версии Bash 5 вам может потребоваться обеспечить правильный порядок сортировки значений ASCII с помощью shopt -s globasciiranges. Хотя, начиная с версии Bash 5, вам не нужно беспокоиться об этой опции globasciiranges, поскольку теперь она установлена по умолчанию.

 

Как устранить ошибку bash, вызванную ожидаемым целочисленным выражением?

При использовании команд test или одной квадратной скобки [ с условным выражением может возникнуть ошибка integer expression expected. Эта ошибка возникает при попытке выполнить арифметическое сравнение строк, то есть не целых чисел. Она похожа на ошибку invalid arithmetic operator при использовании двойных квадратных скобок [[, как упоминалось выше. Убедитесь, что используемая вами переменная не содержит недопустимых символов; смотрите предыдущий вопрос о недопустимом арифметическом операторе.

Обратите внимание, что наилучшей практикой является использование составного выражения bash arithmetic с реальными арифметическими операторами вместо устаревших -lt, -gt, -le и -ge.

# ошибка
[andreyex@linux ~]$ [ 1 -lt 4.0 ] && echo "1 is smaller than 4.0"
bash: [: 4.0: integer expression expected

# правильный
[andreyex@linux ~]$ [ 1 -lt 4 ] && echo "1 is smaller than 4"
1 is smaller than 4

# лучший
[andreyex@linux ~]$ ((1<4)) && echo "1 is smaller than 4"
1 is smaller than 4
Exit mobile version