Як я можу загартувати баш сценарії проти заподіяння шкоди при зміні в майбутньому?


46

Отже, я видалив свою домашню папку (або, точніше, всі файли, до яких я мав доступ для запису). Що сталося, що я мав

build="build"
...
rm -rf "${build}/"*
...
<do other things with $build>

у bash-скрипті та, після того, як більше не потрібно $build, видаляючи декларацію та всі її звичаї - але, окрім rm. Баш щасливо розширюється до rm -rf /*. Так.

Я почувався дурним, встановив резервну копію, переробив втрачену роботу. Намагаючись пройти повз сором.

Тепер мені цікаво: що це за методи написання баш-скриптів, щоб подібні помилки не траплялися, або принаймні рідше? Наприклад, чи писав я

FileUtils.rm_rf("#{build}/*")

у сценарії Ruby, перекладач скаржився, що buildне оголошувався, тому мова мене захищає.

Що я розглянув у баші, окрім корралінгу rm(який, як згадується у багатьох відповідях у споріднених питаннях, не є проблематичним):

  1. rm -rf "./${build}/"*
    Це вбило б мою нинішню роботу (Git repo), але нічого іншого.
  2. Варіант / параметризація rmцього вимагає взаємодії під час дії за межами поточного каталогу. (Не вдалося знайти жодного.) Подібний ефект.

Це чи це, чи є інші способи написання баш-скриптів, які є "надійними" в цьому сенсі?


3
Немає жодних підстав для rm -rf "${build}/*того, куди б дісталися лапки. rm -rf "${build} буде робити те ж саме через f.
Monty Harder

1
Статична перевірка з shellcheck.net - дуже солідне місце для початку. Доступна інтеграція редактора, тому ви можете отримати попередження від своїх інструментів, як тільки ви видалите визначення того, що все ще використовується.
Чарльз Даффі

@CharlesDuffy додати до мого сорому, я написав цей сценарій в IDE IDEA стилю з встановленим BashSupport, який робить попередить в цьому випадку. Так, так, дійсний пункт, але мені дуже потрібна була сувора скасування.
Рафаель

2
Готча. Не забудьте відзначити застереження в BashFAQ # 112 . set -uне настільки нахмурився, як set -eє, але все-таки має свої готчі.
Чарльз Даффі

2
жарт Відповідь: Просто помістіть #! /usr/bin/env rubyу верхній частині кожного скрипта і забути про баш;)
Під

Відповіді:


75
set -u

або

set -o nounset

Це призведе до того, що поточна оболонка розглядає розширення невстановлених змінних як помилку:

$ unset build
$ set -u
$ rm -rf "$build"/*
bash: build: unbound variable

set -uі set -o nounsetє параметрами оболонки POSIX .

Пусте значення буде НЕ викличе помилку , хоча.

Для цього використовуйте

$ rm -rf "${build:?Error, variable is empty or unset}"/*
bash: build: Error, variable is empty or unset

Розширення ${variable:?word}розшириться до значення, variableякщо воно не порожне чи не встановлено. Якщо він порожній або невідомий, показник wordвідображатиметься на стандартній помилці, а оболонка розглядає розширення як помилку (команда не виконується, а якщо працює в неінтерактивній оболонці, це припиняється). Вихід із :поля призведе до помилки лише для невстановленого значення, як і під set -u.

${variable:?word}є розширенням параметра POSIX .

Жоден з них не призведе до припинення інтерактивної оболонки, якщо set -e(або set -o errexit) також не діяв. ${variable:?word}викликає вихід сценаріїв, якщо змінна порожня або встановлена. set -uможе викликати вихід сценарію при використанні разом із set -e.


Щодо вашого другого питання. Немає способу обмежитись rmтим, що не працює поза поточним каталогом.

Реалізація GNU rmмає --one-file-systemопцію, яка зупиняє її від рекурсивного видалення змонтованих файлових систем, але це так близько, як я вважаю, ми можемо отримати, не rmперетворюючи виклик у функцію, яка фактично перевіряє аргументи.


Як бічна примітка: ${build}точно рівнозначно, $buildякщо тільки розширення не відбувається як частина рядка, де безпосередньо наступний символ є дійсним символом у назві змінної, наприклад у "${build}x".


Дуже добре, дякую! 1) Це ${build:?msg}чи ${build?msg}? 2) У контексті чогось типу сценаріїв збірки, я думаю, що використовувати інший інструмент, ніж rm для безпечного видалення, було б чудово: ми знаємо, що ми не хочемо працювати поза поточним каталогом, тому ми робимо це явним шляхом, використовуючи обмежений командування. Взагалі не потрібно робити РМ безпечнішими.
Рафаель

1
@Raphael Вибачте, має бути з:. Я зараз виправлю цю машинну помилку, і я зазначу її значення пізніше, коли я знову за своїм комп’ютером.
Кусалаланда

2
@Raphael Добре, я додав коротке речення про те, що відбувається без :(це призведе до помилки лише для невстановлених змінних). Я не наважуюся намагатися написати інструмент, який би впорався зі видаленням файлів виключно за певним шляхом, за будь-яких обставин. Розбір доріжок і турбота / не піклування про символічні посилання тощо на даний момент для мене занадто химерні.
Kusalananda

1
Дякую, пояснення допомагає! Щодо "місцевої телекомунікації": я здебільшого замислювався, можливо, полював на "впевнений, це <ім'я імені>" - але, безумовно, не намагаюся допомогти вампіру вам написати його! OO Все добре, ви досить допомогли! :)
Рафаель

2
@MateuszKonieczny Я не думаю, що буду, вибачте. Я вважаю, що такі заходи безпеки повинні застосовуватися у разі потреби . Як і у всьому, що робить навколишнє середовище безпечним, воно з часом зробить людей все більш недбалими та залежними від заходів безпеки. Краще знати, що робить кожен із заходів безпеки, а потім застосовувати їх вибірково за потребою.
Кусалаланда

12

Я пропоную звичайні перевірки перевірки, використовуючи test/[ ]

Ви б були в безпеці, якби ви написали свій сценарій як такий:

build="build"
...
[ -n "${build}" ] || exit 1
rm -rf "${build}/"*
...

У [ -n "${build}" ]перевіряє, "${build}"є ненульовий довжиною рядком .

||Є логічним АБО оператор в ударі. Це призводить до запуску іншої команди, якщо перша не вдалася.

Таким чином, ${build}було порожнім / невизначеним / тощо. сценарій вийшов би з кодом повернення 1, що є загальною помилкою.

Це також захистило б вас у випадку, якщо ви видалили будь-яке використання, ${build}оскільки [ -n "" ]завжди буде помилковим.


Перевага використання test/ [ ]є багато інших більш змістовних перевірок, які він також може використовувати.

Наприклад:

[ -f FILE ] True if FILE exists and is a regular file.
[ -d FILE ] True if FILE exists and is a directory.
[ -O FILE ] True if FILE exists and is owned by the effective user ID.

Справедливий, але неприступний. Чи щось мені не вистачає, чи це робиться майже так само, як ${variable:?word}пропонує @Kusalananda?
Рафаель

@Raphael працює аналогічно у випадку "чи має рядок значення", але test(тобто [) є багато інших перевірок, які є релевантними, такі як -d(аргумент - це каталог), -f(аргумент - файл), -O(файл належить поточному користувачеві) тощо.
Centimane

1
@Raphael також, перевірка повинна бути нормальною частиною будь-якого коду / сценарію. Також зауважте, якби ви видалили всі екземпляри збірки, ви б також не видалили його ${build:?word}разом з ним? ${variable:?word}Формат не захистить вас , якщо ви видалите всі екземпляри змінних.
Centimane

1
1) Я думаю, що існує різниця між тим, щоб переконатися, що припущення є (файли існують тощо), і перевірити, чи встановлені змінні (імхо робота компілятора / інтерпретатора). Якщо мені доведеться робити останнє вручну, краще для нього буде надто спритний синтаксис - який не є повноцінним if. 2) "Ви б також не видалили $ {build:? Word} разом з ним" - сценарій полягає в тому, що я пропустив використання змінної. Використання ${v:?w}захистило б мене від пошкоджень. Якби я видалив усі звичаї, навіть звичайний доступ не був би шкідливим, очевидно.
Рафаель

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

4

У вашому конкретному випадку я раніше переробляв "видалення", щоб замість цього переміщувати файли / каталоги (припустимо, що / tmp знаходиться на тому ж розділі, що і у вашому каталозі):

# mktemp -d is also a good, reliable choice
trashdir=/tmp/.trash-$USER/trash-`date`
mkdir -p "$trashdir"
...
mv "${build}"/* "$trashdir"
...

За лаштунками, це переміщує посилання на файл / реєстр топлевелів від джерела до $trashdirструктур каталогів призначення на одному і тому ж розділі, і не витрачає часу на прогулянку по структурі каталогів та звільнення блоків диска на один файл туди-сюди. Це призводить до набагато швидшого очищення, поки система активно використовується, в обмін на трохи повільніше перезавантаження (/ tmp очищається при перезавантаженнях).

Крім того, запис хронів для періодичного очищення /tmp/.trash-$USER не дозволить заповнити / tmp для заповнення процесів (наприклад, збірок), що займають багато дискового простору. Якщо ваш каталог знаходиться на іншому розділі, як / tmp, ви можете створити схожий / tmp-подібний каталог на своєму розділі і замість цього очистити cron.

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


2
"Це призводить до набагато швидшого очищення, поки система активно використовується" - це робить? Я б подумав, що обидва стосуються лише зміни уражених вузлів. Це, звичайно , не швидше, якщо /tmpє на іншому розділі (я думаю, це завжди для мене, принаймні, для скриптів, які працюють у користувальницькому просторі); тоді папку сміття потрібно змінити (і не отримає прибутку від роботи з ОС /tmp).
Рафаель

1
Ви можете використовувати mktemp -dцей тимчасовий каталог, не ступаючи пальцями іншого процесу (і правильно шануючи $TMPDIR).
Toby Speight

Додав ваші точні та корисні нотатки від вас обох, дякую. Я думаю, що спочатку я надрукував це занадто швидко, не зважаючи на точки, які ви підняли.
користувач117529

2

Використовуйте підстановку bash paramater, щоб призначити значення за замовчуванням, коли змінна неініціалізована, наприклад:

rm -rf $ {змінна: - "/ неіснуюча"}


1

Я завжди намагаюся запускати свої сценарії Bash рядком #!/bin/bash -ue.

-eозначає "невдача при першому непошкодженому e rror";

-uозначає «провал на першому використанні з U ndeclared змінної».

Дізнайтеся більше деталей у чудовій статті Використовуйте суворий режим неофіційного Bash (якщо ви не любите налагодження) . Також автор рекомендує використовувати, set -o pipefail; IFS=$'\n\t'але для моїх цілей це надмірність.


1

Загальна порада перевірити, чи встановлена ​​ваша змінна - корисний інструмент для запобігання подібних проблем. Але в цьому випадку було більш просте рішення.

Швидше за все, не потрібно глобалізувати вміст $buildкаталогу для того, щоб видалити їх, але не власне $buildсам каталог. Отже, якщо ви пропустили сторонне, *тоді невстановлене значення перетвориться на таке, rm -rf /за замовчуванням більшість реалізацій rm за останнє десятиліття відмовиться виконувати (якщо ви не відключите цей захист за допомогою параметра GNU rm --no-preserve-root).

Пропуск /також слід, rm ''що призведе до повідомлення про помилку:

rm: can't remove '': No such file or directory

Це працює, навіть якщо ваша команда rm не реалізує захист /.


-2

Ви думаєте з точки зору мови програмування, але bash - це сценарій мови :-) Отже, використовуйте зовсім іншу парадигму інструкцій для зовсім іншої мовної парадигми.

В цьому випадку:

rmdir ${build}

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

rm -rf ${build}/${parameter}
rmdir ${build}

Тепер, якщо ви помістите туди якісь інші файли чи каталоги, наприклад, тимчасовий файл чи щось, чого не слід, викличете rmdirпомилку. Потім поводьтеся з ним належним чином:

rmdir ${build} || build_dir_not_empty "${build}"

Такий спосіб мислення мені добре послужив, тому що… так, це робив.


6
Дякуємо за ваші зусилля, але це зовсім поза увагою. Припущення "ви знаєте, що таке члени" є невірним; придумайте вихід компілятора. make cleanвикористовуватиме декілька символів (якщо дуже старанний компілятор не створить список кожного створеного ним файлу). Крім того, мені здається, що маючи rm -rf ${build}/${parameter}лише трохи зрушити проблему вниз.
Рафаель

Гм. Подивіться, як це читається. "Дякую, але це за те, що я не розголошував у первісному запитанні, тому я збираюся не тільки відкинути вашу відповідь, але і спростувати її, незважаючи на те, що вона більше стосується загальної справи". Ого.
Багатий

"Дозвольте зробити додаткові припущення понад те, що ви написали у запитанні, а потім напишіть спеціалізовану відповідь." * знизуйте плечима * (FYI, не те, що це ваша справа: я не подав заявки. Можливо, мав би це зробити, тому що відповідь була не корисною (як мінімум для мене).)
Рафаель

Гм. Я не робив жодних припущень і написав загальну відповідь. У makeцьому питанні взагалі нічого немає . Написання відповіді, яка стосується лише цього, makeбуло б дуже конкретним і дивовижним припущенням. Моя відповідь працює в інших випадках make, окрім розробки програмного забезпечення, крім вашої конкретної проблеми-початківця, у вас виникли, видаляючи ваші речі.
Багатий

1
Кусалаланда навчив мене ловити рибу. Ви підійшли і сказали: "Оскільки ви живете на рівнинах, то чому б не їсти яловичину замість цього?"
Рафаель,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.