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

Вопрос или проблема

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

Пример:

sleep 20

echo test

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

Итак, как прочитать весь shell скрипт перед его выполнением, чтобы последующие изменения не повлияли на запущенный экземпляр?

Да, оболочки, и 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.

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

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

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

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

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

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

Используйте:

{
  ... ваш код ...

  exit
}

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

Для скриптов, которые «вызываются», а не выполняются, используйте:

{
  ... ваш код ...

  return 2>/dev/null || exit
}

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

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

# эта следующая строка перезаписывает на диске копию скрипта
cat /tmp/scr2 > /tmp/scr
# эта строка в итоге изменится.
echo script content kept
EOF
chmod u+x /tmp/scr
/tmp/scr

мы видим, что измененная версия выводит

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

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

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

sed -i '' filename

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

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

# эта следующая строка разрывает связь с оригиналом и создает новую копию.
sed -i ''  /tmp/scr

# теперь перезапись не имеет немедленного эффекта
cat /tmp/scr2 > /tmp/scr
echo script content kept
EOF

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

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

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

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 не будет без необходимости проверять контрольные суммы файла для резервных копий, а жесткие ссылки будут работать как ожидалось.

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

Ответ или решение

Как прочитать весь shell-скрипт перед его выполнением: полное руководство

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

Почему это важно?

Когда вы запускаете shell-скрипт, оболочка, например bash, начинает исполнять команды одну за другой. Если во время выполнения скрипта вносятся изменения, это может привести к тому, что интерпретатор начнет обрабатывать новые строки, которые могут содержать ошибки или быть в разных условиях выполнения. Например:

sleep 20
echo "test"

Если во время 20-секундного паузы содержимое скрипта изменится, это может привести к выполнению неожиданного кода.

Способы гарантировать, что скрипт прочитан полностью

1. Использование блока {}

Один из наиболее надёжных способов – обернуть код скрипта в блоки {}. Это гарантирует, что вся логика будет прочитана до начала выполнения:

{
  sleep 20
  echo "test"
}; exit

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

2. Использование функции main

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

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

Этот метод также обеспечивает чтение всего скрипта перед его выполнением и минимизирует риск возникновения ошибок из-за изменения файла.

3. Копирование и переименование

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

cp script.sh script.sh.bak
# внесите изменения в script.sh

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

4. Использование временных файлов

Вы также можете использовать временный файл, чтобы обеспечить выполнение актуальной версии:

temp_file=$(mktemp)
cp script.sh "$temp_file"
bash "$temp_file"
rm "$temp_file"

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

5. Версионное управление

Вместо изменения файла на месте рассмотрите возможность использования системы управления версиями, такой как Git. Это позволит вам отслеживать изменения и откатываться к предыдущим версиям, если это необходимо.

6. Конфигурация редактора

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

Заключение

Чтение и выполнение shell-скриптов требует тщательной настройки и осознания возможных проблем. Использование блоков кода, функций, временных файлов и системой контроля версий поможет вам избежать неожиданных ошибок и сохранить целостность ваших скриптов. Всегда будьте осторожны при редактировании и выполняйте скрипты в безопасной среде, что позволит вам избавиться от проблем, связанных с изменением кода на лету.

Ваша работа будет более стабильной и предсказуемой при соблюдении этих рекомендаций.

Оцените материал
Добавить комментарий

Капча загружается...