Настало время вспомнить и о пользователе — ведь это для него, собственно, создаются файлы и запускаются процессы, — а также о том, что для управления системой ему необходим пользовательский интерфейс. В этой статье речь пойдет о традиционном для UNIX-систем интерфейсе командной строки и о языке наиболее популярного в Linux командного интерпретатора bash.
Вы, возможно, удивитесь: есть же графические оболочки, такие как GNOME или KDE; именно благодаря им наблюдается сейчас бурный рост популярности Linux! Разве с их появлением командная строка не превратилась в никому не нужный анахронизм? Вовсе нет. Она по-прежнему остается самым удобным средством комбинирования программ и автоматизации рутинных процедур.
Что касается графических оболочек, то они одновременно и слишком просты, и слишком сложны. Элементарные действия вроде запуска отдельных программ и основных операций с файлами выполняются там очень похоже на то, как это происходит в Windows, и пользователь, знакомый с Windows, легко освоит их без посторонней помощи. Организация же взаимодействия программ, наоборот, требует довольно высокой программистской квалификации: например, среда GNOME основана на модели CORBA, а манипулировать CORBA-объектами весьма непросто. Командный интерпретатор предоставляет в наше распоряжение некую «золотую середину» — возможности весьма широкие и при этом относительно легко осваиваемые.
Примеры и иллюстрации, как и в предыдущих статьях, приводятся на материале дистрибутива KSI-Linux Release 3.0 (Manticore).
Скрипты и интерпретаторы
Командный интерпретатор, как следует из его названия, интерпретирует команды, т. е. выполняет их непосредственно (без предварительной компиляции). Он обрабатывает команды, вводимые пользователем в командной строке, а также скрипты — заранее подготовленные последовательности команд, хранящиеся в текстовом виде.
Надо сказать, что скрипты играют в GNU/Linux (и UNIX вообще) куда более важную роль, чем командные файлы в Windows и DOS. Например, из более чем тысячи (!) программ в каталоге /usr/bin того компьютера, на котором пишутся эти строки, примерно четверть является скриптами того или иного вида, а уж количество вспомогательных скриптов, используемых разными программами для внутренних нужд и не предназначенных для исполнения «широкой публикой» (а потому хранящихся в других каталогах), вообще не поддается учету. На плечи скриптов ложится и большая часть «тяжелой работы» по запуску системы. А если требуется автоматизировать какие-либо действия, то самый простой способ — опять-таки написать несложный скрипт.
В любой «полноценной» (не сокращенной для помещения в тостер или мобильный телефон) версии GNU/Linux имеется не менее двух командных интерпретаторов плюс еще три-четыре языка скриптов, не используемых в командной строке (таких как perl, tcl, python или scheme), и это не считая «мини-языков» типа sed или awk. Почему бы не ограничиться одним интерпретатором и его командным языком? Главным образом потому, что люди не похожи друг на друга и у них разные предпочтения. И чтобы учесть интересы максимального числа пользователей, создатели дистрибутивов включают в них по несколько интерпретаторов, а администраторы обычно предоставляют пользователям своих систем право выбрать по собственному вкусу язык для работы в командной строке.
Из всех командных интерпретаторов для UNIX-систем два являются «классическими». Это B Shell (Bourne Shell), созданный Стефеном Бурном (Stephen R. Bourne) для седьмой версии UNIX, и C Shell, разработанный в Беркли Уильямом Джоем (William N. Joy). Язык C Shell, основанный на командном интерпретаторе шестой версии UNIX, содержал ряд расширений, помогающих в интерактивной работе и в написании скриптов: историю команд, псевдонимы (aliases), массивы и многое другое. Однако при всех своих несомненных преимуществах он имел один очень серьезный недостаток — был несовместим с B Shell.
Поэтому, когда FSF разработал интерпретатор bash (Bourne-Again SHell), сочетающий синтаксис B Shell с мощью C Shell, привлекательность C Shell значительно снизилась. И хотя многие бывшие пользователи BSD или коммерческих версий UNIX используют C Shell при работе в GNU/Linux, стандартом де-факто в этой ОС является bash. (Впрочем, для аварийных дискет bash, занимающий «целых» 420 Кбайт, великоват, и на них часто помещают более компактный интерпретатор, например A Shell, вмещающийся в 62 Кбайт.)
Именно bash интерпретирует основную массу скриптов из /usr/bin и подавляющее большинство вспомогательных скриптов. (Поскольку B Shell не может быть включен в GNU/Linux по лицензионным соображениям, скрипты, изначально рассчитанные на B Shell, также интерпретируются посредством bash.) Поэтому из всех командных интерпретаторов пользователю GNU/Linux в первую очередь необходимо освоить bash.
Подробно описывать bash в журнальной статье невозможно, да, впрочем, и не слишком нужно: в конце концов, он снабжен весьма подробной документацией, которая вызывается командой info bash. Здесь же мы остановимся на наиболее характерных и интересных его особенностях.
Команды и метасимволы
Конечно, bash может выполнять любые команды, имеющиеся в системе, но некоторые из них являются подпрограммами интерпретатора (внутренние команды), а некоторые другие, хотя и представляют собой самостоятельные программы, специально предназначены для использования в командных скриптах (внешние команды). Внутренних и внешних команд bash насчитывается более сотни; перечень наиболее, на мой взгляд, часто применяемых с краткими описаниями приводится во врезке на с. 154. (Оценка употребительности, разумеется, чисто субъективная: я, например, обычно получаю имя файла без пути с помощью конструкции ${filename//*/}, а кто-то, возможно, использует для этого специальную команду basename, хотя она внешняя и из-за этого работает несколько медленнее.)
Как же устроена сама команда? Ее базовая структура во всех языках скриптов одинакова и весьма проста: сначала записывается имя команды, за ним может следовать определенное число опций (ключей) и аргументов (параметров), отделяемых от имени и друг от друга пробелами. Регистр символов существенен в любом месте команды; имена большинства команд записываются строчными буквами. Специальные символы — ‘*’, ‘$’, ‘?’ ‘!’, ‘>’, ‘<’ и др. — используются в командах в качестве метасимволов, т. е. служат для управления работой самого интерпретатора; именно поэтому их избегают употреблять в именах команд, файлов, каталогов и т. д. При необходимости вывести на экран строку, содержащую спецсимволы, ее обычно заключают в кавычки (существуют и другие способы вывода спецсимволов, но этот — самый распространенный).
Стандартно каждая команда записывается на отдельной строке, но можно поместить в одной строке и несколько команд: они отделяются друг от друга точками с запятой, если нужно, чтобы очередная команда ждала завершения работы предыдущей, и амперсандами, если команды должны выполняться параллельно. Длинную команду, которой не хватает одной строки, можно перенести на следующую с помощью символа ‘’.
Радости интерактивной работы
Надо сказать, что для интерактивной работы bash предоставляет массу удобств (на которые вы уже, скорее всего, обращали внимание, если работали в GNU/Linux). Он поддерживает такие средства редактирования командной строки, как повтор символов, макросы, «карман» (буфер) и т. д., а также историю (т. е. возможность повторить ранее введенную команду) и настраиваемое автоматическое дополнение.
Так, чтобы запустить, скажем, программу mysql_ convert_table_format, достаточно набрать в командной строке mysql_co и нажать клавишу табуляции: bash, зная названия доступных команд, сам «допишет» имя. (Если в системе есть несколько команд, начинающихся с заданного префикса, он выдаст их перечень, а если их более 100, то предварительно уточнит, действительно ли нужен такой огромный список. Кстати, с помощью данного свойства bash легко выяснить число доступных команд: для этого достаточно нажать клавишу табуляции, находясь в начале строки.) А когда название команды введено (и после него поставлен пробел), интерпретатор позволяет тем же способом ввести имя файла.
Автозаполнение также можно вызвать, нажав клавишу (в ее роли обычно выступает ) одновременно с одним из специальных символов: ‘/’ вызывает дополнение имени файла, ‘!’ — команды, $ — переменной, ‘~’ — пользователя, @ — машины. А при нажатии последовательно клавиш +x и соответствующего специального символа выдается список возможных вариантов дополнения.
Но и это еще не все. Например, автодополнение можно программировать... Вернемся, однако, к языку интерпретатора bash.
Шаблоны
Поскольку ряд идей B Shell был использован при создании командных интерпретаторов DOS и Windows NT (Windows 9X не имеет собственного интерпретатора), многие конструкции bash могут показаться вам знакомыми. Однако это сходство зачастую обманчиво и нередко вводит в заблуждение пользователя DOS/Windows. Хорошей иллюстрацией здесь могут послужить шаблоны.
Базовые правила задания шаблонов для имен файлов в bash довольно просты: ‘*’, как и в DOS, означает любое число любых символов, а ‘?’ — любой одиночный символ. Кроме того, можно перечислить символы в квадратных скобках (разрешается вставлять между ними пробелы); а символ ‘^’ или ‘!’ в начале такого списка будет указывать, что символы не должны встречаться в данной позиции.
Если интерпретатор DOS поручает обработку шаблонов конкретным командам, то bash выполняет ее самостоятельно и передает командам не шаблоны, а готовые списки подходящих файлов. В результате команды иногда ведут себя совсем не так, как ожидает пользователь DOS.
Рис. 1. Что же здесь произошло?
Посмотрите на рис. 1: исходя из опыта работы в DOS естественно было бы предположить, что каждый из файлов с именем вида ‘*.static’ будет скопирован в файл с таким же именем, но без расширения, а образовался файл с диким именем ‘*.’ (впрочем, это произошло только потому, что в каталоге имелся всего один файл, подходивший под шаблон; если бы их оказалось несколько, bash выдал бы сообщение об ошибке).
Как же тогда получить имена без расширений (точнее, с отсеченной частью после точки — в Linux нет расширений в понимании DOS)? Об этом вы узнаете в конце следующего раздела, посвященного переменным.
Переменные
Начнем с отличий между переменными bash и командного языка DOS. Их удобно продемонстрировать на примере.
Рассмотрим команду, которая «дописывает» в переменную окружения CLASSPATH путь к архиву JAVApackage (подобные команды часто вставляются в конфигурационные файлы инсталляторами различных программ). В DOS она имела бы приблизительно следующий вид:
set CLASSPATH=%CLASSPATH%;c:Program
FilesBig Program
JAVApackage.jar
Как видим, в DOS переменные ограничиваются с двух сторон символами ‘%’, а в bash маркируется только их начало — символом ‘$’: признаком конца служит первый символ, не разрешенный в именах переменных (разрешены буквы, цифры и символ подчеркивания). Кроме того, в bash нет команды, аналогичной SET: интерпретатор распознает присваивание значения переменной просто по наличию знака равенства.
Кстати, благодаря тому, что установка переменной окружения в bash не требует отдельной команды, ее можно сделать частью любой команды (рис. 2). Присвоенное таким образом значение действительно только для данной команды, а в среду bash изменения не вносятся.
Рис. 2. Присваивание значения переменной окружения может быть не только отдельной командой, но и частью другой команды
Вернемся, однако, к нашему примеру. Несмотря на более короткую форму записи самих переменных и операции присваивания, «Linux-версия» оказалась длиннее. Во-первых, там, где командный язык DOS обходится одним-единственным знаком ‘;’, она содержит устрашающего вида конструкцию ${CLASSPATH:+:}, а во-вторых, в ней присутствует несколько загадочная для пользователя DOS команда export.
И то, и другое — следствие заботы о безопасности. Команда export делает переменную доступной другим командам. В командных файлах DOS переменные всегда внешние, и в подавляющем большинстве случаев это действительно нужно, поскольку переменные служат почти исключительно для обмена данными между программами. В bash же широко используются внутренние переменные — в качестве счетчиков, для хранения промежуточных результатов вычислений или имен файлов и т. д. Поэтому переменные, которые должны быть доступны за пределами данного скрипта, специальным образом отмечаются. Конечно, маловероятно, что имя внутренней переменной случайно совпадет с именем переменной окружения, в результате чего значение последней окажется испорченным и какая-то программа начнет работать неправильно, но, как говорил Козьма Прутков, «лучше перебдеть, чем недобдеть».
Конструкция ${CLASSPATH:+:} вставляет в строку двоеточие (которое в bash служит разделителем элементов CLASSPATH, а также PATH, играющей ту же роль, что и в DOS), но лишь при условии, что строка CLASSPATH не является пустой. Без этой меры предосторожности результатом выполнения команды могла бы оказаться переменная CLASSPATH вида :/opt/big-program /JAVApackage. zip, т. е. с пустым элементом в начале. Такой элемент обозначает текущий каталог, который в Linux, в отличие от DOS, необходимо включать в CLASSPATH (и в PATH) в явной форме. Причем в большинстве дистрибутивов это не делается — из тех же соображений «как бы чего не вышло».
Данное обстоятельство часто сбивает с толку начинающих пользователей Linux:
— Как же так: я ведь и атрибуты executable for all на файл MyGreatCommand поставил, и в первой строке #!/bin/sh написал, а мне все равно говорят: command not found!
— Конечно! Ведь ты же ее в PATH не поместил!
— Какой PATH? Она у меня в текущем каталоге!
— А у тебя разве текущий каталог входит в PATH?
— ???
Впрочем, путь к команде, находящейся в текущем каталоге, можно указать в форме ./MyGreatCommand, и этого будет достаточно, чтобы она запустилась.
Если в DOS с переменной можно сделать, грубо говоря, две вещи — присвоить ей значение и извлечь значение, присвоенное ранее, — то в bash вариантов намного больше: скажем, извлечь значение можно десятком разных способов, включая условное извлечение (с которым мы познакомились на примере конструкции ${CLASSPATH:+:}), извлечение подстроки и извлечение с использованием шаблона. В частности, конструкция ${X##шаблон} позволяет, извлекая строку, удалить из нее максимально возможную соответствующую шаблону подстроку, считая от начала, а ${X%шаблон} — минимально возможную, считая с конца. Так что отсечь «хвосты» упомянутым в предыдущем разделе именам файлов можно было бы, например, следующим образом:
for i in /bin/*.static
do j=${i##*/}
cp ”$i” ”${j%.static}”
done
Подробнее о разных вариантах извлечения переменных рассказывается во врезке на с. 156. А операторами for и do мы займемся во второй части статьи.
Системные переменные
Помимо переменных, используемых различными программами, в Linux, как и в DOS, есть специальные, или «системные» переменные, значение которых определено заранее, причем их намного больше. Так, DOS имеет переменную PROMPT, содержащую приглашение командной строки, а в bash ей соответствуют четыре переменных: PS1 — основное приглашение; PS2 — «вспомогательное» приглашение, выдаваемое, когда команда не уместилась на одной строке; PS3 — приглашение «команды» select (на самом деле это не команда, а специальная конструкция bash, призванная облегчить выбор из нескольких вариантов; впрочем, она используется довольно редко); PS4 — приглашение перед командами скрипта, выводимыми в режиме трассировки (заметим, что в bash, в отличие от командного интерпретатора DOS, скрипты по умолчанию не трассируются).
В документации bash описано множество переменных, устанавливаемых интерпретатором или влияющих на его поведение. Назовем для примера $RANDOM, дающую доступ к простому генератору псевдослучайных чисел, и $!, значение которой равно PID последней команды, запущенной из данного экземпляра интерпретатора на асинхронное выполнение. Нам уже встречались системные переменные ${CLASSPATH, $PATH, а также $LC_ALL, определяющая страну и язык. С другими, такими как $? — возвращаемое значение — или $* — список параметров, — мы познакомимся в дальнейшем.
Виктор Хименко,
Окончание в следующем номере.
* В. Хименко. «Файлы, файлы, файлы». «Мир ПК», № 2/2000, с. 64; № 3/2000, с. 50.
«Процессы, задачи, потоки и нити». «Мир ПК», № 5/2000, с. 42; № 6/2000, с. 54.
Команды bash
Перед тем как перечислять в алфавитном порядке наиболее употребительные команды bash, необходимо назвать три «справочных» команды, используемых почти исключительно при интерактивной работе, — help, info и man. Команда help выдает краткое описание любой встроенной команды bash, info предоставляет доступ к входящему в состав системы GNU развернутому справочнику, в котором bash, разумеется, подробно описан. Команда man позволяет обратиться к другому, более старому справочнику, где есть информация о ряде команд, не описанных в справочнике info (вообще говоря, команда info, не найдя сведений в собственном справочнике, ищет их и в справочнике man, однако кое-что можно получить только с помощью самой man). Естественно, команда help help позволяет получить справку по help, команда info info — по info, а man man — по man.
В приводимом ниже списке каждая команда снабжена пометкой, указывающей, как получить ее более подробное описание: (b) означает, что команда встроенная и, следовательно, информацию о ней предоставляет команда help, (i) соответствует команде info, (m) — команде man.
. (b) — синоним для команды source
: (b) — синоним для команды true
[ (b) — сокращение для команды test, но, в отличие от нее, требует закрывающей квадратной скобки
(( (b) — соотносится с командой let так же, как [ соотносится с test
[[ (b) — не вполне команда, а особое выражение, очень похожее на команду [ (test)
alias(b) — позволяет задавать псевдонимы для других команд
at(m) — ставит программу в очередь на выполнение в заданное время
atq(m) — в заданное время проверяет очередь программ на выполнение
atrm(m) — в заданное время удаляет программу из очереди на выполнение
awk(i) — язык для построчного сканирования и обработки файлов: простой, маленький и быстрый, но притом достаточно мощный
batch(m) — выполняет программу, когда система не слишком загружена
builtin(b) — позволяет вызвать встроенную команду bash, даже когда ее имя перекрыто именем функции или псевдонимом
bzip2(i) — более новая, чем gzip, программа сжатия файлов; работает медленнее, чем gzip, но обеспечивает лучший коэффициент сжатия
cat(i) — «склеивает» заданные файлы и выдает их на стандартный выход
cd(b) — изменяет текущий каталог
chgrp(i), chmod(i), chown(i) — изменяют соответственно группу, права доступа и владельца файла
command(b) — позволяет вызвать команду — встроенную или внешнюю, даже когда ее имя перекрыто именем функции или псевдонимом
cp(i) — копирует файлы и каталоги
cpio(i) — CoPy In/Out — системная программа создания архивов; не содержит встроенной поддержки сжатия файлов, но может использоваться совместно с gzip или bzip2
crontab(m) — позволяет модифицировать список регулярных заданий пользователя
cut(i) — выдает на стандартный выход выбранные части строк текстового файла
dd(i) — копирует файл блоками, выполняя одновременно некоторые дополнительные действия
du(i) — вычисляет объем, занятый на диске указанными файлами
declare(b) — позволяет задать имя и тип переменной (применяется не слишком часто, так как bash допускает использование необъявленных переменных)
df(i) — сообщает количество свободного и занятого места на диске
diff(i) — находит различия между двумя файлами
dirs(b) — выводит список запомненных подкаталогов
echo(b/i) — выводит на стандартный выход заданное сообщение
enable(b) — позволяет разрешить или запретить использование встроенных команд
eval(b) — выполняет аргументы так, как если бы они были введены в командной строке (ранее часто использовалась для обращения к переменной, имя которой содержится в другой переменной)
exec(b) — выполняет системный вызов exec, т. е. замещает процесс, где исполняется скрипт, другим, заданным в качестве параметра; часто используется в так называемых «скриптах-обертках» (wrapper scripts), настраивающих среду для выполнения программ
exit(b) — завершает работу командного интерпретатора (и, стало быть, скрипта)
export(b) — делает переменные данного скрипта доступными для других процессов, запущенных из командного интерпретатора
file(m) — определяет тип файла (по содержимому; эвристический анализ выполняется на основе гибкой настраиваемой базы данных)
find(m/i) — ищет файлы по множеству признаков, но не по содержимому
false(b/i) — возвращает код ненормального завершения
getopts(b) — довольно сложная команда, похожая на аналогичное средство системной библиотеки; позволяет создавать скрипты, понимающие сложные опции
grep(m) — ищет строки в файлах; может использоваться совместно с командами find и xargs для поиска файлов по содержимому
gzip(i) — стандартная для GNU программа сжатия файлов; способна распаковывать (но не создавать) файлы в формате compress — более старой UNIX-программы сжатия
install(i) — копирует файлы, одновременно позволяя устанавливать их атрибуты
kill(b/m) — позволяет послать процессу сигнал; по умолчанию посылается сигнал SIGTERM, останавливающий процесс; отсюда такое устрашающее название
less(m) — улучшенная по сравнению с more программа просмотра файлов
let(b) — вычисляет арифметическое выражение; выражение может содержать многие операторы языка Си, а также переменные
logout(b) — завершает работу командного интерпретатора, являющегося основным (login shell)
ln(i) — создает ссылки на файлы (дополнительные жесткие или символические)
ls(i) — выводит список файлов (например, для заданного каталога)
md5sum(i) — подсчитывает для файлов 128-битовую контрольную сумму
mkdir(i) — создает подкаталог
mktemp(m) — создает временный файл (чтобы избежать «дыр» в безопасности, создавайте временные файлы только с помощью mktemp)
more(m) — постранично выводит файл на экран (служит для просмотра длинных файлов)
mv(i) — перемещает или переименовывает файлы (каталоги)
patch(i) — применяет diff-файл (см. diff) к исходному файлу
popd(b) — удаляет подкаталоги из списка запомненных подкаталогов
printf(b/i) — обеспечивает форматированную печать данных (имеет много общего с одноименной функцией стандартной библиотеки Си)
pushd(b) — добавляет подкаталог в список запомненных подкаталогов и перемещает подкаталоги внутри этого списка
pwd(b/i) — выводит путь к текущему каталогу
read(b) — считывает строку со стандартного ввода и присваивает прочитанные значения указанным переменным
readonly(b) — защищает переменную от случайных изменений
return(b) — выходит из функции и передает управление в вызвавшую программу
rm(i) — удаляет файлы (подкаталоги)
rmdir(i) — удаляет пустые подкаталоги
sed(i) — потоковый редактор (a Stream EDitor); дает возможность быстро производить с текстом простые операции (например, поиск с заменой)
select(b) — довольно сложная команда, позволяющая организовывать меню с выбором вариантов из списка (в действительности это даже не команда, а особая синтаксическая форма, родственная синтаксическим формам while и for)
set(b/i) — очень сложная команда:
без параметров выдает список всех определенных на данный момент переменных и функций
с параметрами вида +
все прочие параметры присваиваются последовательно переменным $1 $2 ... $N
(Команда help set не дает полного описания set; оно есть только в описании bash, получаемом командой info bash.)
shift(b) — сдвигает позиционные параметры ($1 становится равным $N, $2 — $N+1, $3 — $N+2 и т.д.)
sort(i) — сортирует файл
source(b) — читает и выполняет команды, содержащиеся в файле (часто используется для того, чтобы вынести определение переменных в отдельный файл конфигурации)
tar(i) — программа создания архивов (Tape ARchiver); не содержит встроенной поддержки сжатия файлов, но может использоваться совместно с gzip или bzip2
test(b/i) — вычисляет значение логического выражения; в основном проверяет атрибуты файлов (существует? пуст? исполняемый? подкаталог? и т. д.), однако может также сравнивать строки
tr(i) — заменяет одни символы на другие по заданной таблице подстановки
trap(b) — позволяет связать с сигналом особую обработку
true(b/i) — возвращает код успешного завершения
type(b) — возвращает «тип» слова, заданного в качестве аргумента (встроенная команда, псевдоним, функция и т. д.)
ulimit(b) — устанавливает или сообщает системные квоты для процесса (процессов)
umask(b) — назначение описано в статье «Файлы, файлы, файлы»
wc(i) — подсчитывает число символов, слов и строк в файле
xargs(m) — получает параметры со стандартного входа и вызывает с этими параметрами заданную программу (по умолчанию echo)
Как уже говорилось, здесь перечислены далеко не все команды. В типичной системе GNU/Linux их значительно больше: есть, например, команды, выводящие восьмеричный и шестнадцатеричный дамп памяти (od и hexdump), печатающие начало и конец файла (head и tail), а ко многим упомянутым командам есть дополнительные (например, diff3 позволяет сравнить три файла, а bzcat — просмотреть файл, упакованный программой bzip2). Не попали в наш обзор и системные переменные, имеющие для bash особый смысл. Обо всем этом и о многом другом вы сможете узнать, набрав в командной строке слова info bash.
${!X} — извлечь значение из переменной, имя которой хранится в переменной X. Вместе с массивами этого достаточно для создания и обработки весьма нетривиальных структур данных.
${X:-выражение} — условное извлечение: если переменная определена (как VAR1), используется ее значение, иначе — заданное альтернативное выражение (как в случае с VAR3).
${X#шаблон}, ${X##шаблон}, S{X%шаблон}, S{X%%шаблон} — извлечь строку, удалив из нее часть, соответствующую шаблону. Шаблон строится по тем же правилам, что и для имен файлов, т. е. ‘*[37]’ — это любая последовательность символов, а затем либо ‘3’, либо ‘7’, а ‘[37]*’ — это ‘3’ или ‘7’, а затем любая последовательность символов. Операции ‘#’ и ‘%’ удаляют минимальную возможную подстроку, ‘##’ и ‘%%’ — максимальную, причем ‘#’ и ‘##’ — с начала строки, а ‘%’ и ‘%%’ — с конца.
${X/шаблон/выражение}, ${X//шаблон/выражение} — извлечь строку, заменив в ней часть, соответствующую шаблону, заданным выражением (поиск с заменой). Операция ‘/’ выполняет замену однократно, а ‘//’ повторяет ее до победного конца.
аэрография авто | бюро ремонта телефонов из магазина