...

воскресенье, 18 июля 2021 г.

[Перевод] Bash-функции и их имена, которые могут быть практически всем чем угодно

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

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

function ++ { (( $1++ )); }

Точные правила, касающиеся именования bash-функций, весьма туманны. Возникает такое ощущение, что имена функций могут представлять собой абсолютно любую последовательность символов, которую оболочка Bash способна однозначно распознать. Например, следующий скрипт выводит все допустимые односимвольные имена функций:
#!/bin/bash
for n in {33..126}; do
  printf -v oct "%03o" "$n"
  printf -v chr \\"$oct"
  eval "function $chr { echo -n '$chr'; }; $chr" 2>/dev/null
done

Вот результаты его работы:
+,./:=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz

Тут, для получения всех десятичных кодов печатаемых ASCII-символов (хорошую справочную таблицу можно найти в man 7 ascii), используется механизм раскрытия скобок (brace expansion). После этого применяется команда printf, переводящая десятичный код в восьмеричное представление, а потом превращающая его в соответствующий символ. Затем тут применяется команда eval, в которой делается попытка объявить новую функцию с именем, соответствующим найденному символу, после чего осуществляется вызов этой функции.

Если в имени функции содержится буква, то набор других символов, которые можно использовать в её имени, меняется. Вот — список допустимых символов, которые могут находиться в имени функции на второй позиции, учитывая то, что первым символом имени является буква a:

!#%+,-./0123456789:?@ABCDEFGHIJKLMNOPQRSTUVWXYZ]^_abcdefghijklmnopqrstuvwxyz{}~

Все дополнительные символы имеют некое особое значение для парсера Bash, а то, что в начале имени находится буква, снимает вопрос их неоднозначного толкования системой. Это справедливо и для символов, которых нет во второй группе, например — это знак =, который сам по себе вполне подходит, но, судя по всему, последовательность символов a= выглядит как начало объявления переменной. То же касается и символа [, так как последовательность a[ похожа на начало конструкции для работы с массивом.

Имена функций, кстати, совершенно не ограничены ASCII-символами. Вот, например, функция, именем которой является смайлик:

function { echo «haha!»; }

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


Помимо того, что Bash позволяет объявлять функции с разными интересными именами, эта оболочка ещё и без проблем позволяет переопределять имена любых функций, в частности — имена встроенных команд. Вот как я переопределил команду echo:
function echo { echo "$@"; }

Правда, я сразу же должен попросить вас так не поступать. Вызов этой функции приведёт к бесконечной рекурсии, которая завершится «убийством» процесса. Но если удалось успешно переопределить встроенную команду — её исходный вариант можно вызвать с помощью команды command. В результате следующая конструкция оказывается вполне работоспособной:
function echo { command echo "$@"; }

Правда, рабочей такая функция окажется лишь до тех пор, пока не переопределят и command. Тогда всё будет основательно «поломано». Не знаю, хорошо это или плохо, но у меня такое ощущение, что ключевое слово function переопределить нельзя. И меня расстроило (хотя, скорее — успокоило) то, что переопределение exit не мешает процессам оболочки завершать работу, если только эта команда не вызывается явным образом.

Какая от этого польза?


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

Так как в Bash имеются лишь глобальная и функциональная области видимости, импортированный код может переопределить ранее объявленные конструкции, такие, как имена функций. Поэтому, если вы пишете библиотеку или то, что называется «modulino», вы можете предусмотреть использование в имени каждой из функций префикса $namespace, указывающего на пространство имён вашего проекта. После этого запуск подобного кода с помощью source не приведёт к непреднамеренному переопределению имён функций в вызывающем коде (естественно, если вызывающий код написан с использованием такого же соглашения об именовании сущностей).

Вот, например, функция из jp, выводящая сведения об ошибках:

function jp.error {
  echo "Error: $1 at line $JP_LINE, column $JP_COL" >&2
  return 1
}

Так как правила именования переменных строже правил именования функций, я добавил к именам глобальных переменных префикс JP_, но тут используется та же самая идея (я позаимствовал это правило из JSON.bash).

Можно ли счесть это удачным ходом? Если код рассчитан исключительно на Bash, то — да. Но такой код не отличается переносимостью (взгляните на стандарт POSIX). В частности — он не запустится в ash. Это — плохая новость для того, кто планирует запускать его в Docker-контейнерах, основанных на Busybox.

В Bash имеется полезный POSIX-режим. Его можно применять для запуска кода, в переносимости которого есть сомнения. Работа в этом режиме, кроме того, запрещает пользовательскому коду переопределение встроенных команд, определённых в POSIX.

Приходилось ли вам пользоваться Bash-функциями с необычными именами?

Adblock test (Why?)

Комментариев нет:

Отправить комментарий