Семантика для сценариев Bash?

87

Больше, чем какой-либо другой язык, который я знаю, я «выучил» Bash с помощью Google каждый раз, когда мне что-то нужно. Следовательно, я могу собирать вместе небольшие скрипты, которые, кажется, работают. Тем не менее, я не очень знаю , что происходит, и я надеялся на более формальное введение в Bash в качестве языка программирования. Например: каков порядок оценки? каковы правила определения объема работ? Какова дисциплина набора текста, например, все ли является строкой? Каково состояние программы - присвоение значения ключом строк именам переменных; есть еще что-то, например стек? Есть куча? И так далее.

Я подумал обратиться к руководству по GNU Bash для такого понимания, но, похоже, это не то, что мне нужно; это скорее подробный перечень синтаксического сахара, чем объяснение основной семантической модели. Миллион и один "учебник по bash" в сети только хуже. Возможно, мне следует сначала изучить shи понять Bash как синтаксический сахар поверх всего этого? Однако я не знаю, точна ли это модель.

Какие-либо предложения?

РЕДАКТИРОВАТЬ: Меня попросили привести примеры того, что в идеале я ищу. Довольно крайним примером того, что я бы назвал «формальной семантикой», является статья о «сути JavaScript» . Возможно, чуть менее формальным примером является отчет Haskell 2010 .

Jameshfisher
источник
3
Является ли Advanced Bash Scripting Guide одним из «миллиона и одного»?
choroba
2
Я не уверен, что у bash есть «базовая семантическая модель» (ну, может быть, «почти все является строкой»); Я думаю, что это действительно синтаксический сахар.
Гордон Дэвиссон
4
То, что вы называете «подробным списком синтаксического сахара», на самом деле является семантической моделью расширения - чрезвычайно важной частью выполнения. 90% ошибок и недоразумений происходят из-за непонимания модели расширения.
тот другой парень
4
Я могу понять, почему кто-то может подумать, что это широкий вопрос, если вы читаете его, например, как мне написать сценарий оболочки ? Но реальный вопрос заключается в том, какова формальная семантика и основы языка оболочки и bash в частности? , и это хороший вопрос с одним последовательным ответом. Голосование для открытия.
kojiro
1
Я довольно много узнал на linuxcommand.org, и есть даже бесплатный PDF-файл с более подробной книгой по командной строке и написанию сценариев оболочки
samrap

Ответы:

107

Оболочка - это интерфейс для операционной системы. Обычно это более или менее надежный сам по себе язык программирования, но с функциями, предназначенными для упрощения взаимодействия с операционной системой и файловой системой. Семантика оболочки POSIX (в дальнейшем именуемая просто «оболочка») представляет собой что-то вроде путаницы, объединяя некоторые функции LISP (s-выражения имеют много общего с разделением слов оболочки ) и C (большая часть арифметического синтаксиса оболочки семантика исходит из C).

Другой корень синтаксиса оболочки возник из-за того, что она выросла из мешанины отдельных утилит UNIX. Большинство из того, что часто является встроенным в оболочку, на самом деле может быть реализовано как внешние команды. Многие новички в области оболочки запутаются, когда осознают, что это /bin/[существует во многих системах.

$ if '/bin/[' -f '/bin/['; then echo t; fi # Tested as-is on OS X, without the `]`
t

Wat?

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

#!/usr/bin/env python

from __future__ import print_function
import os, sys

'''Hacky barebones shell.'''

try:
  input=raw_input
except NameError:
  pass

def main():
  while True:
    cmd = input('prompt> ')
    args = cmd.split()
    if not args:
      continue
    cpid = os.fork()
    if cpid == 0:
      # We're in a child process
      os.execl(args[0], *args)
    else:
      os.waitpid(cpid, 0)

if __name__ == '__main__':
  main()

Я надеюсь, что из приведенного выше ясно видно, что модель выполнения оболочки в значительной степени:

1. Expand words.
2. Assume the first word is a command.
3. Execute that command with the following words as arguments.

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

Не все команды fork. Фактически, есть несколько команд, которые не имеют особого смысла реализованы как внешние (так что они должны были бы это делать fork), но даже они часто доступны как внешние для строгого соответствия POSIX.

Bash опирается на эту основу, добавляя новые функции и ключевые слова для улучшения оболочки POSIX. Он почти совместим с sh, а bash настолько распространен, что некоторые авторы скриптов годами не осознают, что скрипт на самом деле может не работать в системе, строгой по POSIX. (Мне также интересно, как люди могут так сильно заботиться о семантике и стиле одного языка программирования и так мало о семантике и стиле оболочки, но я расходлюсь.)

Порядок оценки

Это немного хитрый вопрос: Bash интерпретирует выражения в своем основном синтаксисе слева направо, но в своем арифметическом синтаксисе он следует приоритету C. Однако выражения отличаются от расширений . Из EXPANSIONраздела руководства bash:

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

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

Объем

Объем функции

Как и старый ECMAscript, оболочка имеет динамическую область видимости, если вы явно не объявляете имена внутри функции.

$ foo() { echo $x; }
$ bar() { local x; echo $x; }
$ foo

$ bar

$ x=123
$ foo
123
$ bar

$ …

«Объем» среды и процесса

Подоболочки наследуют переменные своих родительских оболочек, но другие виды процессов не наследуют неэкспортированные имена.

$ x=123
$ ( echo $x )
123
$ bash -c 'echo $x'

$ export x
$ bash -c 'echo $x'
123
$ y=123 bash -c 'echo $y' # another way to transiently export a name
123

Вы можете комбинировать эти правила определения объема:

$ foo() {
>   local -x bar=123 # Export foo, but only in this scope
>   bash -c 'echo $bar'
> }
$ foo
123
$ echo $bar

$

Печатная дисциплина

Гм, типы. Да уж. В Bash действительно нет типов, и все раскрывается в строку (или, возможно, было бы более подходящим слово ). Но давайте рассмотрим различные типы расширений.

Струны

Практически все можно рассматривать как строку. Голые слова в bash - это строки, значение которых полностью зависит от применяемого к нему расширения.

Нет расширения

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

$ echo foo
foo
$ 'echo' foo
foo
$ "echo" foo
foo
Расширение подстроки
$ fail='echoes'
$ set -x # So we can see what's going on
$ "${fail:0:-2}" Hello World
+ echo Hello World
Hello World

Подробнее о расширениях читайте в Parameter Expansionразделе руководства. Это довольно мощно.

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

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

$ foo=10+10
$ echo $foo
10+10
$ declare -i foo
$ foo=$foo # Must re-evaluate the assignment
$ echo $foo
20
$ echo "${foo:0:1}" # Still just a string
2

Массивы

Аргументы и позиционные параметры

Прежде чем говорить о массивах, возможно, стоит обсудить позиционные параметры. Аргументы сценария оболочки можно получить с помощью пронумерованных параметров, $1, $2, $3и т.д. Вы можете получить доступ ко всем этим параметрам одновременно , используя "$@", что разложение имеет много общего с массивами. Вы можете установить и изменить позиционные параметры с помощью встроенных функций setили shiftили просто вызвав оболочку или функцию оболочки с этими параметрами:

$ bash -c 'for ((i=1;i<=$#;i++)); do
>   printf "\$%d => %s\n" "$i" "${@:i:1}"
> done' -- foo bar baz
$1 => foo
$2 => bar
$3 => baz
$ showpp() {
>   local i
>   for ((i=1;i<=$#;i++)); do
>     printf '$%d => %s\n' "$i" "${@:i:1}"
>   done
> }
$ showpp foo bar baz
$1 => foo
$2 => bar
$3 => baz
$ showshift() {
>   shift 3
>   showpp "$@"
> }
$ showshift foo bar baz biz quux xyzzy
$1 => biz
$2 => quux
$3 => xyzzy

В руководстве по bash также иногда упоминается $0как позиционный параметр. Я нахожу это запутанным, потому что он не включает его в счетчик аргументов $#, но это нумерованный параметр, так что я. $0это имя оболочки или текущего сценария оболочки.

Массивы

Синтаксис массивов смоделирован на основе позиционных параметров, поэтому в большинстве случаев полезно думать о массивах как о названном виде «внешних позиционных параметров», если хотите. Массивы можно объявлять следующими способами:

$ foo=( element0 element1 element2 )
$ bar[3]=element3
$ baz=( [12]=element12 [0]=element0 )

Вы можете получить доступ к элементам массива по индексу:

$ echo "${foo[1]}"
element1

Вы можете разрезать массивы:

$ printf '"%s"\n' "${foo[@]:1}"
"element1"
"element2"

Если рассматривать массив как нормальный параметр, вы получите нулевой индекс.

$ echo "$baz"
element0
$ echo "$bar" # Even if the zeroth index isn't set

$ …

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

$ foo=( 'elementa b c' 'd e f' )
$ echo "${#foo[@]}"
2

Основное различие между массивами и позиционными параметрами:

  1. Позиционные параметры не редкость. Если $12установлен, вы можете быть уверены $11, что он тоже установлен. (Это может быть пустая строка, но $#не меньше 12.) Если "${arr[12]}"установлено, нет гарантии, что "${arr[11]}"она установлена, и длина массива может быть меньше 1.
  2. Нулевой элемент массива однозначно является нулевым элементом этого массива. В позиционных параметрах нулевой элемент - это не первый аргумент , а имя оболочки или сценария оболочки.
  3. Для shiftмассива вы должны нарезать и переназначить его, например arr=( "${arr[@]:1}" ). Вы также можете это сделать unset arr[0], но это сделает первый элемент с индексом 1.
  4. Массивы могут неявно совместно использоваться функциями оболочки как глобальные объекты, но вы должны явно передать позиционные параметры функции оболочки, чтобы она их увидела.

Часто бывает удобно использовать расширения имен файлов для создания массивов имен файлов:

$ dirs=( */ )

Команды

Команды являются ключевыми, но они также описаны лучше, чем я могу в руководстве. Прочтите SHELL GRAMMARраздел. Различные виды команд:

  1. Простые команды (например $ startx)
  2. Трубопроводы (например $ yes | make config) (смеется)
  3. Списки (например $ grep -qF foo file && sed 's/foo/bar/' file > newfile)
  4. Составные команды (например $ ( cd -P /var/www/webroot && echo "webroot is $PWD" ))
  5. Сопроцессы (сложные, без примера)
  6. Функции (именованная составная команда, которую можно рассматривать как простую команду)

Модель исполнения

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

Ссылки:

  1. SHELL GRAMMARРаздел руководства Баша
  2. XCU Shell Command Language документация
  3. Руководство Bash на вики Greycat в.
  4. Расширенное программирование в среде UNIX

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

Кодзиро
источник
16
+1: Отличное объяснение. Оцените время, потраченное на написание этого с примерами.
jaypal Singh
+1 за yes | make config;-) А если серьезно, то очень хорошее описание.
Digital Trauma
только начал это читать .. приятно. оставлю несколько комментариев. 1) еще большее удивление возникает, когда вы видите это, /bin/[и /bin/testчасто это одно и то же приложение. 2) «Предположите, что первое слово - это команда». - ожидайте, когда вы выполните задание ...
Кароли Хорват
@KarolyHorvath Да, я намеренно исключил присваивание из моей демонстрационной оболочки, потому что переменные - это сложный беспорядок. Эта демонстрационная оболочка не была написана с учетом этого ответа - она ​​была написана намного раньше. Полагаю, я мог бы сделать это execleи вставить первые слова в окружение, но это все равно немного усложнит задачу.
kojiro
@kojiro: нет, это бы просто слишком усложнило бы, это определенно не было моим намерением! но назначение работает немного иначе (x), и IMHO вы должны упомянуть это где-нибудь в тексте. (x): и источник некоторой путаницы ... Я даже не могу больше сосчитать, сколько раз я видел, как люди жалуются, что a = 1не работают).
Кароли Хорват
5

Ответ на ваш вопрос «Какова дисциплина печати, например, все ли является строкой». Переменные Bash представляют собой символьные строки. Но Bash разрешает арифметические операции и сравнения переменных, когда переменные являются целыми числами. Исключением из правила Bash-переменные являются символьные строки, когда указанные переменные набраны или объявлены иначе.

$ A=10/2
$ echo "A = $A"           # Variable A acting like a String.
A = 10/2

$ B=1
$ let B="$B+1"            # Let is internal to bash.
$ echo "B = $B"           # One is added to B was Behaving as an integer.
B = 2

$ A=1024                  # A Defaults to string
$ B=${A/24/STRING01}      # Substitute "24"  with "STRING01".
$ echo "B = $B"           # $B STRING is a string
B = 10STRING01

$ B=${A/24/STRING01}      # Substitute "24"  with "STRING01".
$ declare -i B
$ echo "B = $B"           # Declaring a variable with non-integers in it doesn't change the contents.
B = 10STRING01

$ B=${B/STRING01/24}      # Substitute "STRING01"  with "24".
$ echo "B = $B"
B = 1024

$ declare -i B=10/2       # Declare B and assigning it an integer value
$ echo "B = $B"           # Variable B behaving as an Integer
B = 5

Объявите значения опций:

  • -a Переменная - это массив.
  • -f Использовать только имена функций.
  • -i Переменная должна рассматриваться как целое число; арифметическая оценка выполняется, когда переменной присваивается значение.
  • -p Показать атрибуты и значения каждой переменной. Когда используется -p, дополнительные параметры игнорируются.
  • -r Сделать переменные доступными только для чтения. Этим переменным не могут быть присвоены значения последующими операторами присваивания, и они не могут быть отменены.
  • -t Присвойте каждой переменной атрибут трассировки.
  • -x Пометить каждую переменную для экспорта в последующие команды через среду.
Кейт Рейнольдс
источник
1

Справочная страница bash содержит немного больше информации, чем большинство справочных страниц, и включает кое-что из того, что вы просите. Мое предположение после более чем десятилетнего использования сценариев bash состоит в том, что из-за своей истории как расширения sh, он имеет некоторый фанковый синтаксис (для обеспечения обратной совместимости с sh).

FWIW, мой опыт был похож на ваш; хотя различные книги (например, O'Reilly «Learning the Bash Shell» и подобные) действительно помогают с синтаксисом, существует множество странных способов решения различных проблем, и некоторых из них нет в книге, и их следует искать в Google.

прогулка
источник