Как прочитать весь сценарий оболочки перед его выполнением?

35

Обычно, если вы редактируете scrpit, все запущенные сценарии подвержены ошибкам.

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

Пример:

sleep 20

echo test

Если вы выполните этот скрипт, bash прочитает первую строку (скажем, 10 байт) и перейдет в спящий режим. Когда он возобновляется, в скрипте может быть другое содержимое, начиная с 10-го байта. Я могу быть в середине строки в новом сценарии. Таким образом, запущенный скрипт будет сломан.

VasyaNovikov
источник
Что вы подразумеваете под «внешним изменением сценария»?
Maulinglawns
1
Может быть, есть способ обернуть все содержимое функции или чего-то еще, чтобы оболочка сначала прочитала весь сценарий? Но как насчет последней строки, в которой вы вызываете функцию, она будет читаться до EOF? Может быть, пропуск последнего \nпозволил бы? Может подделать ()подойдет? Я не очень опытен с этим, пожалуйста, помогите!
ВасяНовиков
@maulinglawns, если в скрипте есть такое содержимое, как sleep 20 ;\n echo test ;\n sleep 20я, и я начинаю его редактировать, он может плохо себя вести. Например, bash может прочитать первые 10 байтов скрипта, понять sleepкоманду и перейти в спящий режим. После возобновления в файле будет другое содержимое, начиная с 10 байтов.
ВасяНовиков
1
Итак, что вы говорите, что вы редактируете выполняемый скрипт? Сначала остановите скрипт, внесите изменения, а затем запустите его снова.
maulinglawns
@maulinglawns да, вот и все. Проблема в том, что мне неудобно останавливать сценарии, и всегда сложно помнить об этом. Может быть, есть способ заставить bash сначала прочитать весь скрипт?
ВасяНовиков

Ответы:

43

Да, оболочки, bashв частности, внимательно читают файл по одной строке за раз, поэтому он работает так же, как при интерактивном использовании.

Вы заметите, что когда файл не доступен для поиска (например, канал), bashдаже читает по одному байту за раз, чтобы не читать после \nсимвола. Когда файл доступен для поиска, он оптимизирует, считывая полные блоки за раз, но возвращаясь к после \n.

Это означает, что вы можете делать такие вещи, как:

bash << \EOF
read var
var's content
echo "$var"
EOF

Или написать сценарии, которые обновляются сами. Что бы вы не смогли сделать, если бы это не дало вам такой гарантии.

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

Чтобы избежать этого, вы можете попробовать и убедиться , что вы не измените файл на месте (например, изменить копию и переместите копию на месте (например , sed -iили perl -piи некоторые редакторы делают, например)).

Или вы можете написать свой сценарий как:

{
  sleep 20
  echo test
}; exit

(обратите внимание, что важно, чтобы он exitнаходился на одной линии с }; хотя вы могли бы также поместить его в фигурные скобки непосредственно перед закрывающей).

или:

main() {
  sleep 20
  echo test
}
main "$@"; exit

Оболочке нужно будет прочитать скрипт до тех пор, пока он exitне начнет что-либо делать. Это гарантирует, что оболочка не будет читать из сценария снова.

Это означает, что весь скрипт будет храниться в памяти.

Это также может повлиять на синтаксический анализ сценария.

Например, в bash:

export LC_ALL=fr_FR.UTF-8
echo $'St\ue9phane'

Выведет, что U + 00E9 закодировано в UTF-8. Однако, если вы измените его на:

{
  export LC_ALL=fr_FR.UTF-8
  echo $'St\ue9phane'
}

\ue9Будет расширены в кодировке , которая была в действии в то время, когда команда была разобрана , который в этом случае , прежде чемexport команда будет выполнена.

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

Это не тот случай, bashкогда чья sourceкоманда полностью читает файл перед его интерпретацией. Если писать bashспециально, вы можете использовать это, добавив в начале сценария:

if [[ ! $already_sourced ]]; then
  already_sourced=1
  source "$0"; exit
fi

(Я бы не стал полагаться на это, хотя, как вы могли себе представить, будущие версии bashмогут изменить это поведение, которое в настоящее время можно рассматривать как ограничение (bash и AT & T ksh - единственные POSIX-подобные оболочки, которые, насколько можно судить, ведут себя так же) и already_sourcedхитрость немного хрупкая, так как она предполагает, что переменная находится вне среды, не говоря уже о том, что она влияет на содержимое переменной BASH_SOURCE)

Стефан Шазелас
источник
@VasyaNovikov, кажется, что-то не так с SE в данный момент (или, по крайней мере, для меня). Когда я добавил свой ответ, было только несколько ответов, и ваш комментарий, кажется, появился только сейчас, хотя в нем говорится, что он был опубликован 16 минут назад (или, может быть, я просто теряю шарики). В любом случае, обратите внимание на дополнительный «выход», который необходим здесь, чтобы избежать проблем при увеличении размера файла (как отмечено в комментарии, который я добавил к вашему ответу).
Стефан Шазелас
Стефан, я думаю, что нашел другое решение. Это использовать }; exec true. Таким образом, нет необходимости в новых строках в конце файла, что удобно для некоторых редакторов (например, emacs). Все тесты, с которыми я мог правильно работать,}; exec true
ВасяНовиков,
@VasyaNovikov, не уверен, что ты имеешь в виду. Чем это лучше чем }; exit? Вы также теряете статус выхода.
Стефан
Как уже упоминалось в другом вопросе: обычно сначала анализируют весь файл, а затем выполняют составной оператор в случае использования команды dot ( . script).
Шили
@schily, да, я упоминаю это в этом ответе как ограничение AT & T ksh и bash. Другие оболочки типа POSIX не имеют такого ограничения.
Стефан Шазелас
12

Вам просто нужно удалить файл (т.е. скопировать его, удалить его, переименовать копию обратно к исходному имени). На самом деле многие редакторы могут быть настроены для вас. Когда вы редактируете файл и сохраняете в нем измененный буфер, вместо перезаписи файла он переименует старый файл, создаст новый и поместит новое содержимое в новый файл. Следовательно, любой запущенный скрипт должен продолжаться без проблем.

Используя простую систему контроля версий, такую ​​как RCS, которая легко доступна для vim и emacs, вы получаете двойное преимущество - наличие истории ваших изменений, и система извлечения должна по умолчанию удалить текущий файл и воссоздать его с правильными режимами. (Остерегайтесь жестких ссылок на такие файлы, конечно).

meuh
источник
«Удалить» на самом деле не является частью процесса. Если вы хотите сделать его должным образом атомарным, вы делаете переименование поверх файла назначения - если у вас есть шаг удаления, есть риск того, что ваш процесс умрет после удаления, но до переименования, не оставив файла вообще на месте ( или читатель попытается получить доступ к файлу в этом окне и не найдет ни старые, ни новые версии).
Чарльз Даффи
11

Самое простое решение:

{
  ... your code ...

  exit
}

Таким образом, Bash будет читать все {} блок перед его выполнением, а exitдиректива будет следить за тем, чтобы ничего не читалось за пределами блока кода.

Если вы не хотите «исполнять» скрипт, а скорее «исходить» из него, вам нужно другое решение. Это должно работать тогда:

{
  ... your code ...

  return 2>/dev/null || exit
}

Или, если вы хотите прямой контроль над кодом выхода:

{
  ... your code ...

  ret="$?";return "$ret" 2>/dev/null || exit "$ret"
}

Вуаля! Этот скрипт безопасен для редактирования, создания и исполнения. Вы все еще должны быть уверены, что не измените его в те миллисекунды, когда он изначально читается.

VasyaNovikov
источник
1
Я обнаружил, что он не видит EOF и не прекращает чтение файла, но запутывается в обработке «буферизованного потока» и в конечном итоге ищет за концом файла, поэтому он выглядит хорошо, если размер файл увеличивается не сильно, но выглядит плохо, когда вы делаете файл более чем в два раза больше, чем раньше. Я сообщу об ошибке в Bash сопровождающих в ближайшее время.
Стефан Шазелас
1
ошибка сообщается сейчас , см. также патч .
Стефан Шазелас
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Тердон
5

Доказательство концепции. Вот скрипт, который модифицирует себя:

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line overwites the on disk copy of the script
cat /tmp/scr2 > /tmp/scr
# this line ends up changed.
echo script content kept
EOF
chmod u+x /tmp/scr
/tmp/scr

мы видим измененную версию печати

Это связано с тем, что bash load сохраняет дескриптор файла для открытия в сценарии, поэтому изменения в файле будут видны сразу.

Если вы не хотите обновлять копию в памяти, отсоедините исходный файл и замените его.

Один из способов сделать это - использовать sed -i.

sed -i '' filename

доказательство концепции

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line unlinks the original and creates a new copy.
sed -i ''  /tmp/scr

# now overwriting it has no immediate effect
cat /tmp/scr2 > /tmp/scr
echo script content kept
EOF

chmod u+x /tmp/scr
/tmp/scr

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

Jasen
источник
2
Нет, bashне открывает файл с mmap(). Просто нужно внимательно читать по одной строке за раз, так же, как когда он получает команды от терминального устройства в интерактивном режиме.
Стефан Шазелас
2

Обертывание вашего скрипта в блок {}, вероятно, является лучшим вариантом, но требует изменения ваших скриптов.

F=$(mktemp) && cp test.sh $F && bash $F; rm $F;

будет вторым лучшим вариантом (при условии tmpfs ) недостатком является то, что он ломает $ 0, если ваши сценарии используют это.

используя что-то вроде F=test.sh; tail -n $(cat "$F" | wc -l) "$F" | bash менее идеально, потому что оно должно держать весь файл в памяти и ломать $ 0.

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

замена файла при редактировании работала бы, но менее надежна, поскольку не применяется другими скриптами / пользователями / или кто-то может забыть. И снова это сломало бы жесткие ссылки.

user1133275
источник
все, что делает копию, будет работать. tac test.sh | tac | bash
Ясен