Що станеться, якщо ви редагуєте сценарій під час виконання?


31

У мене є загальне питання, яке може бути наслідком нерозуміння того, як обробляються процеси в Linux.

Для своїх цілей я буду визначати "скрипт" як фрагмент bash-коду, збереженого в текстовому файлі, з дозволами на виконання, увімкненим для поточного користувача.

У мене є ряд сценаріїв, які дзвонять один одному в тандемі. Для простоти я буду називати їх сценаріями A, B і C. Сценарій A виконує ряд висловлювань, а потім робить паузу, він виконує сценарій B, потім робить паузу, потім виконує сценарій C. Іншими словами, серія кроків - це щось подібне:

Запустити сценарій A:

  1. Серія тверджень
  2. Пауза
  3. Запустіть сценарій B
  4. Пауза
  5. Запустіть сценарій C

З досвіду я знаю, що якщо я запускаю скрипт A до першої паузи, а потім вношу правки в сценарій B, ці зміни відображаються у виконанні коду, коли я дозволяю йому відновитись. Так само, якщо я вносити зміни до сценарію C, поки сценарій А все ще призупинено, тоді дозвольте йому продовжуватися після збереження змін, ці зміни відображаються у виконанні коду.

Ось тоді справжнє питання, чи є спосіб змінити сценарій A, поки він все ще працює? Або редагування неможливе, як тільки починається його виконання?


2
я думаю, це залежить від оболонки. хоча ви заявляєте, що використовуєте bash. видається, що це буде залежати від способу завантаження оболонки сценаріїв всередину.
strugee

поведінка може також змінитися, якщо ви надсилаєте файл, а не виконуєте його.
strugee

1
Я думаю, що Баш читає весь скрипт у пам'яті перед виконанням.
w4etwetewtwet

2
@handuel, ні, ні. Начебто він не чекає, поки ви введете "exit" у запиті, щоб почати інтерпретувати введені команди.
Стефан Шазелас

1
@StephaneChazelas Так, читання з терміналу не робить, однак це відрізняється від запуску сценарію.
w4etwetewtwet

Відповіді:


21

У Unix більшість редакторів працюють, створюючи новий тимчасовий файл, що містить відредагований вміст. Коли відредагований файл буде збережено, вихідний файл видаляється, а тимчасовий файл перейменовується на оригінальне ім'я. (Звичайно, існують різні гарантії для запобігання втраті даних.) Це, наприклад, стиль, який використовується sedабо perlколи викликається -iпрапором ("на місці"), який насправді взагалі не "на місці". Його слід було назвати "новим місцем зі старою назвою".

Це добре працює, тому що unix запевняє (принаймні, для локальних файлових систем), що відкритий файл продовжує існувати до його закриття, навіть якщо він "видалений" та створений новий файл з тим же ім'ям. (Не випадково виклик системи Unix для "видалення" файлу насправді називається "від’єднати".) Отже, загалом кажучи, якщо інтерпретатор оболонки відкриває якийсь вихідний файл, і ви "редагуєте" файл у спосіб, описаний вище , оболонка навіть не побачить зміни, оскільки в неї все ще відкритий оригінальний файл.

[Примітка: як і всі коментарі, що базуються на стандартах, вищезазначене піддається численним тлумаченням, і існують різні випадкові випадки, такі як NFS. Педани можуть залишити коментарі винятками.]

Звичайно, можна безпосередньо змінювати файли; це просто не дуже зручно для редагування, оскільки, хоча ви можете перезаписати дані у файл, ви не можете видалити або вставити, не змістивши всі наступні дані, що означало б дуже багато перезапису. Крім того, поки ви робили цю зміну, вміст файлу був би непередбачуваним, і процеси, які мали відкритий файл, зазнали б погіршення. Щоб уникнути цього (як, наприклад, у системах баз даних), вам потрібен складний набір протоколів модифікації та розподілених замків; матеріал, що виходить за рамки типової утиліти редагування файлів.

Отже, якщо ви хочете відредагувати файл, обробляючи його оболонкою, у вас є два варіанти:

  1. Ви можете додати файл. Це завжди має працювати.

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

Я не знаю жодного формулювання стандарту Posix, яке насправді вимагає можливості додавання до файлу сценарію під час виконання файлу, тому він може працювати не з усіма сумісними оболонками Posix, тим більше з поточною пропозицією майже - а іноді-сумісні з оболонкою оболонок. Так YMMV. Але наскільки я знаю, це працює надійно з bash.

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

#!/bin/bash
if [[ $1 == reset ]]; then
  printf "%s\n%-16s#\n" '####' 'next ${1:-99}' |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^#### $0 | cut -f1 -d:) bs=1 2>/dev/null
  exit
fi

step() {
  s=s
  one=one
  case $beer in
    2) beer=1; unset s;;
    1) beer="No more"; one=it;;
    "No more") beer=99; return 1;;
    *) ((--beer));;
  esac
}
next() {
  step ${beer:=$(($1+1))}
  refrain |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^next\  $0 | cut -f1 -d:) bs=1 conv=notrunc 2>/dev/null
}
refrain() {
  printf "%-17s\n" "# $beer bottles"
  echo echo ${beer:-No more} bottle$s of beer on the wall, ${beer:-No more} bottle$s of beer.
  if step; then
    echo echo Take $one down, pass it around, $beer bottle$s of beer on the wall.
    echo echo
    echo next abcdefghijkl
  else
    echo echo Go to the store, buy some more, $beer bottle$s of beer on the wall.
  fi
}
####
next ${1:-99}   #

Коли я запускаю це, він починається з "Більше", потім продовжується до -1 і в мінусові числа нескінченно.
Данило Гершкович

Якщо я export beer=100перед тим, як запустити сценарій, він працює як очікувалося.
Данило Гершкович

@DanielHershcovich: цілком правильно; неохайне тестування з мого боку. Я думаю, що я це виправив; тепер він приймає необов'язковий параметр підрахунку. Кращим і цікавішим виправленням буде автоматичне скидання, якщо параметр не відповідає кешованій копії.
rici

18

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

Наприклад у:

cmd1
cmd2

Оболонка буде читати скрипт по блоках, тому, ймовірно, читайте обидві команди, інтерпретуйте першу, а потім звертайтеся до кінця cmd1сценарію та ще раз читайте сценарій, щоб прочитати cmd2та виконати його.

Ви можете легко перевірити це:

$ cat a
echo foo | dd 2> /dev/null bs=1 seek=50 of=a
echo bar
$ bash a
foo

(хоча дивлячись на straceвисновок про це, здається, це робить деякі більш химерні речі (наприклад, прочитати дані кілька разів, повернутися назад ...), ніж коли я спробував те ж саме кілька років тому, тому моє твердження вище про огляд назад більше не застосовується для новіших версій).

Якщо ви пишете свій сценарій так:

{
  cmd1
  cmd2
  exit
}

Оболонку доведеться читати до закриття }, зберігати її в пам'яті та виконувати її. Через те exit, що оболонка більше не буде читати із сценарію, тож ви можете його безпечно редагувати, поки оболонка її інтерпретує.

Крім того, редагуючи сценарій, переконайтеся, що ви написали нову копію сценарію. Оболонка буде читати оригінальний (навіть якщо його видалено чи перейменовано).

Щоб зробити це, перейменувати the-scriptв the-script.oldі копіювати the-script.oldв the-scriptі редагувати його.


4

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

Часто, коли сценарій змінюється під час виконання, оболонка закінчує повідомлення про синтаксичні помилки. Це пов’язано з тим, що, коли оболонка закривається і знову відкриває файл сценарію, вона використовує зміщення байтів у файл, щоб змінити репозицію при поверненні.


4

Ви можете обійти це, встановивши пастку на свій скрипт, а потім скориставшись execновим вмістом сценарію. Зауважте, однак, execвиклик починає сценарій з нуля, а не з того місця, де він дійшов до запущеного процесу, і тому сценарій B буде викликаний (і так далі).

#! /bin/bash

CMD="$0"
ARGS=("$@")

trap reexec 1

reexec() {
    exec "$CMD" "${ARGS[@]}"
}

while : ; do sleep 1 ; clear ; date ; done

Це буде продовжувати відображати дату на екрані. Тоді я міг би відредагувати свій сценарій та змінити dateна echo "Date: $(date)". Після написання цього запущеного сценарію все ще просто відображається дата. Як ніколи, якщо я надішлю сигнал, який я встановив trapдля захоплення, скрипт exec(замінює поточний запущений процес на вказану команду), яка є командою $CMDта аргументами $@. Ви можете зробити це, видавши kill -1 PID- де PID - це PID запущеного сценарію - і результат зміни змінюється, щоб відобразитись Date:до dateвиведення команди.

Ви можете зберігати "стан" свого скрипту у зовнішньому файлі (in say / tmp) та читати вміст, щоб знати, де "відновити", коли програма повторно виконується. Потім ви можете додати додаткове припинення пастки (SIGINT / SIGQUIT / SIGKILL / SIGTERM), щоб очистити цей tmp-файл, щоб після перезавантаження після переривання "Script A" воно почнеться з початку. Державна версія буде чимось на зразок:

#! /bin/bash

trap reexec 1
trap cleanup 2 3 9 15

CMD="$0"
ARGS=("$@")
statefile='/tmp/scriptA.state'
EXIT=1

reexec() { echo "Restarting..." ; exec "$CMD" "${ARGS[@]}"; }
cleanup() { rm -f $statefile; exit $EXIT; }
run_scriptB() { /path/to/scriptB; echo "scriptC" > $statefile; }
run_scriptC() { /path/to/scriptC; echo "stop" > $statefile;  }

while [ "$state" != "stop" ] ; do

    if [ -f "$statefile" ] ; then
        state="$(cat "$statefile")"
    else
        state='starting'
    fi

    case "$state" in
        starting)         
            run_scriptB
        ;;
        scriptC)
            run_scriptC
        ;;
    esac
done

EXIT=0
cleanup

Я вирішив цю проблему, захопивши $0і $@на початку сценарію, і execзамість цього застосував ці змінні .
Drav Sloan
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.