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


35

Зазвичай, якщо ви редагуєте підказку, усі запущені сценарії схильні до помилок.

Наскільки я зрозумів, bash (інші оболонки теж?) Читає сценарій поступово, тому якщо ви змінили файл сценарію зовні, він почне читати неправильні речі. Чи є спосіб запобігти це?

Приклад:

sleep 20

echo test

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


Що ви маєте на увазі під "зовнішньою зміною сценарію"?
maulinglawns

1
Можливо, є спосіб, як увесь вміст обернути у якусь функцію чи щось таке, тож оболонка спочатку прочитає весь сценарій? Але як щодо останнього рядка, де ви викликаєте функцію, чи буде вона прочитана до EOF? Можливо, опускання останнього \nзробить трюк? Може, підзарядка ()зробить? Я не дуже досвідчений з цим, будь ласка, допоможіть!
ВасяНовіков

@maulinglawns, якщо скрипт містить вміст, як sleep 20 ;\n echo test ;\n sleep 20і я почніть його редагувати, він може поводитися неправильно. Наприклад, bash міг прочитати перші 10 байт сценарію, зрозуміти sleepкоманду і перейти до сну. Після його відновлення у файлі з’явиться різний вміст, починаючи з 10 байт.
ВасяНовіков

1
Отже, що ви говорите, що ви редагуєте виконаний сценарій? Спершу зупиніть скрипт, виконайте зміни, а потім запустіть його знову.
maulinglawns

@maulinglawns так, це в основному все. Проблема в тому, що мені не зручно зупиняти сценарії, і важко завжди пам’ятати про це. Може бути, є спосіб змусити спочатку читати весь сценарій?
ВасяНовіков

Відповіді:


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 команда буде виконана.

Також зауважте, що якщо 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)


@VasyaNovikov, здається, що з SE зараз щось не так (або принаймні для мене). Коли я додав свою, було лише кілька відповідей, а ваш коментар, схоже, з'явився лише зараз, хоча там написано, що він був опублікований 16 хвилин тому (а може, це я просто втрачаю мармур). У будь-якому випадку, зверніть увагу на додатковий "вихід", який потрібен тут, щоб уникнути проблем, коли розмір файлу збільшується (як зазначено в коментарі, який я додав до вашої відповіді).
Стефан Шазелас

Стефане, я думаю, що я знайшов інше рішення. Це використовувати }; exec true. Таким чином, немає жодної вимоги до нових рядків у кінці файлу, що є дружнім для деяких редакторів (наприклад, emacs). Усі тести, з якими я міг би подумати, спрацювали правильно}; exec true
Вася Новиков

@VasyaNovikov, не впевнений, що ти маєш на увазі. Як це краще, ніж }; exit? Ви також втрачаєте статус виходу.
Стефан Шазелас

Як було сказано в іншому запитанні: спочатку загально проаналізувати весь файл, а потім виконати складений оператор у випадку, якщо використовується команда dot ( . script).
шилі

@schily, так, я зазначу, що у цій відповіді як обмеження AT&T ksh та bash. Інші оболонки типу POSIX не мають такого обмеження.
Стефан Шазелас

12

Вам просто потрібно видалити файл (тобто скопіювати його, видалити, перейменувати копію назад у вихідне ім’я). Насправді багато редакторів можуть бути налаштовані зробити це за вас. Коли ви редагуєте файл і зберігаєте до нього змінений буфер, замість того, щоб перезаписати файл, він перейменовує старий файл, створить новий і додасть новий вміст у новий файл. Отже, будь-який запущений сценарій повинен продовжуватися без проблем.

Використовуючи просту систему управління версіями, на зразок RCS, яка легко доступна для vim та emacs, ви отримуєте подвійну перевагу наявності історії змін, і система замовлення повинна за замовчуванням видалити поточний файл і відтворити його з правильними режимами. (Звичайно, остерігайтеся жорсткого зв’язування таких файлів).


"delete" насправді не є частиною процесу. Якщо ви хочете зробити його належним чином атомарним, ви перейменовуєте над цільовим файлом - якщо у вас є крок видалення, існує ризик загинути ваш процес після видалення, але перед перейменуванням, не залишаючи жодного файлу на місці ( або читач намагається отримати доступ до файлу у цьому вікні і не знайти жодної старої та нової версій).
Чарльз Даффі

11

Найпростіше рішення:

{
  ... your code ...

  exit
}

Таким чином, bash прочитає весь {}блок перед його виконанням, а exitдиректива гарантує, що нічого не буде прочитано поза блоком коду.

Якщо ви не хочете "виконувати" скрипт, а скоріше "джерело" його, вам потрібно інше рішення. Це має працювати тоді:

{
  ... your code ...

  return 2>/dev/null || exit
}

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

{
  ... your code ...

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

Вуаля! Цей сценарій безпечно для редагування, джерела та виконання. Ви все ще повинні бути впевнені, що ви не змінюєте його протягом тих мілісекунд, коли він спочатку читається.


1
Що я виявив, це те, що він не бачить EOF і не перестає читати файл, але він заплутується в обробці "буферизованого потоку" і закінчується пошуком минулого кінця файлу, і тому це виглядає чудово, якщо розмір файл збільшується не набагато, але виглядає погано, коли ви робите файл удвічі більший, ніж раніше. Невдовзі я повідомлю про помилку технічному обслуговувачам bash.
Стефан Шазелас


Коментарі не для розширеного обговорення; ця розмова переміщена до чату .
тердон

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 зберігає ручку файлу для відкриття до сценарію, тому зміни у файлі будуть помічені негайно.

Якщо ви не хочете оновлювати копію пам'яті, від’єднайте оригінальний файл та замініть його.

Один із способів зробити це - за допомогою 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

Якщо ви використовуєте редактор для зміни сценарію, увімкнення функції "зберігати резервну копію" може бути все необхідне, щоб редактор написав змінену версію в новий файл замість того, щоб перезаписати існуючий.


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

заміна файлу на редагування працюватиме, але є менш надійною, оскільки це не підлягає застосуванню для інших сценаріїв / користувачів /, або можна забути. І знову це порушило б міцні зв’язки.


все, що робить копію, спрацювало б. tac test.sh | tac | bash
Ясен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.