Как я могу защитить bash-скрипты от нанесения вреда при их изменении в будущем?

46

Итак, я удалил свою домашнюю папку (или, точнее, все файлы, к которым у меня был доступ для записи). Что случилось то, что у меня было

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

в bash-скрипте и, после того, как больше не нужно $build, удалить объявление и все его использования - кроме rm. Баш радостно расширяется rm -rf /*. Да.

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

Теперь я задаюсь вопросом: каковы методы написания сценариев bash, чтобы такие ошибки не возникали или, по крайней мере, менее вероятны? Например, если бы я написал

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

в сценарии на Ruby переводчик жаловался на то, что его buildне объявили, поэтому язык защищает меня.

То, что я рассмотрел в bash, помимо загона rm(что, как упоминают многие ответы в смежных вопросах, не является проблемой):

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

Это так или есть другие способы написания bash-скриптов, которые в этом смысле «надежны»?

Рафаэль
источник
3
Там нет причин, rm -rf "${build}/*независимо от того, куда идут кавычки. rm -rf "${build} будет делать то же самое из-за f.
Монти Хардер
1
Статическая проверка с shellcheck.net - очень хорошее начало. Доступна интеграция с редакторами, поэтому вы можете получить предупреждение от ваших инструментов, как только удалите определение того, что еще используется.
Чарльз Даффи
@CharlesDuffy добавить к моему стыду, я написал этот сценарий в IDE IDEA стиля с установленным BashSupport, который делает предупредит в этом случае. Так что да, правильное замечание, но мне действительно нужна была жесткая отмена.
Рафаэль
2
Попался. Обязательно обратите внимание на предостережения в BashFAQ # 112 . set -uне так хмурится, как set -eесть, но у него все еще есть свои ошибки.
Чарльз Даффи
2
Ответ на шутку: просто поместите #! /usr/bin/env rubyв начало каждого сценария оболочки и забудьте о bash;)
Pod

Ответы:

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стандартной ошибке будет отображаться символ, и оболочка будет рассматривать расширение как ошибку (команда не будет выполнена, и если она выполняется в неинтерактивной оболочке, это завершится). Выход из :out приведет к ошибке только для неустановленного значения, как показано ниже 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: мы знаем, что не хотим работать за пределами текущего каталога, поэтому мы делаем это явным образом, используя ограниченные команда. Вообще нет необходимости делать rm более безопасным.
Рафаэль
1
@ Рафаэль Извините, должно быть с:. Я исправлю эту опечатку сейчас и упомяну ее значение позже, когда вернусь к своему компьютеру.
Кусалананда
2
@Raphael Хорошо, я добавил короткое предложение о том, что происходит без :(это вызовет ошибку только для неустановленных переменных). Я не смею пытаться написать инструмент, который бы справлялся с удалением файлов исключительно по определенному пути при любых обстоятельствах. Разбор путей и забота / не забота о символических ссылках и т. Д. В настоящее время слишком сложна для меня.
Кусалананда
1
Спасибо, объяснение помогает! Относительно "местного rm": я в основном размышлял, возможно, ловил рыбу на "уверен, это <имя инструмента>" - но, конечно, не пытался помочь вампир в написании этого! 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.
Centimane
источник
Справедливо, но немного громоздко. Я что-то упустил, или это похоже на то, что ${variable:?word}предлагает @Kusalananda?
Рафаэль
@ Рафаэль работает аналогично в случае «имеет ли строка значение», но test(то есть [) имеет много других релевантных проверок, таких как -d(аргумент - это каталог), -f(аргумент - это файл), -O(файл принадлежит текущему пользователю) и так далее.
Сентиман
1
@ Рафаэль также, проверка должна быть нормальной частью любого кода / сценариев. Также обратите внимание, если бы вы удалили все экземпляры сборки, вы бы не удалили ее ${build:?word}вместе с ней? ${variable:?word}Формат не защитит вас , если вы удалите все экземпляры переменных.
Сентиман
1
1) Я думаю, что есть разница между проверкой предположений (файлы существуют и т. Д.) И проверкой, установлены ли переменные (imho работа компилятора / интерпретатора). Если мне придется делать последнее вручную, лучше использовать для этого сверхъестественный синтаксис, чего не скажешь if. 2) «Вы бы тоже не удалили $ {build:? Word} вместе с ним» - сценарий таков, что я пропустил использование переменной. Использование ${v:?w}защитило бы меня от повреждений. Если бы я удалил все обычаи, очевидно, даже простой доступ не был бы вредным.
Рафаэль
Это все сказанное, ваш ответ является справедливым и helful ответ на общий вопрос титульного: убедившись , что предположения держать это важно для сценариев , которые остаются вокруг. Имхо, на конкретный вопрос в теле вопроса лучше ответил Кусалананда. Спасибо!
Рафаэль
4

В вашем конкретном случае я переработал «удаление» в прошлом, чтобы вместо этого переместить файлы / каталоги (предполагая, что / tmp находится в том же разделе, что и ваш каталог):

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

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

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

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

user117529
источник
2
«Это обеспечивает намного более быструю очистку, когда система активно используется» - не так ли? Я бы подумал, что оба касаются только изменения затронутых инодов. Это, конечно , не быстрее, если /tmpнаходится на другом разделе (я думаю, что это всегда для меня, по крайней мере, для сценариев, которые выполняются в пространстве пользователя); затем необходимо изменить папку для мусора (и она не получит выгоды от обработки ОС /tmp).
Рафаэль
1
Вы можете использовать, mktemp -dчтобы получить этот временный каталог, не наступая на пальцы другого процесса (и чтобы правильно соблюдать $TMPDIR).
Тоби Спайт
Добавил ваши точные и полезные заметки от вас обоих, спасибо. Я думаю, что я изначально опубликовал это слишком быстро, не учитывая вопросы, которые вы подняли.
user117529
2

Используйте замену параметра bash, чтобы назначить значения по умолчанию, когда переменная неинициализирована, например:

rm -rf $ {переменная: - "/ несуществующая"}

rackandboneman
источник
1

Я всегда стараюсь начинать свои скрипты Bash со строки #!/bin/bash -ue.

-eозначает «провал на первом uncathed е rror»;

-uозначает «провал на первом использовании из U ndeclared переменной».

Более подробную информацию можно найти в большой статье. Используйте неофициальный строгий режим Bash (если вы не любите отладку) . Также автор рекомендует использовать, set -o pipefail; IFS=$'\n\t'но для моих целей это излишне.

niya3
источник
1

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

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

Пропуск трейлинга /также приведет rm ''к появлению сообщения об ошибке:

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

Это работает, даже если ваша команда rm не реализует защиту для /.

eschwartz
источник
-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}только немного сдвинул вопрос вниз.
Рафаэль
Гектометр Посмотрите, как это выглядит. «Спасибо, но это то, что я не раскрыл в первоначальном вопросе, поэтому я собираюсь не только отклонить ваш ответ, но и понизить его, несмотря на то, что он более применим к общему делу». Ух ты.
Рич
«Позвольте мне сделать дополнительные предположения помимо того, что вы написали в вопросе, а затем написать специализированный ответ». * пожимает плечами * (К вашему сведению, это не ваше дело: я не понизил голосование. Возможно, я должен был это сделать, потому что ответ был бесполезен (по крайней мере, для меня).)
Рафаэль
Гектометр Я не сделал никаких предположений и написал общий ответ. В этом нет ничего общего make. Написание ответа, который применим только к этому, makeбыло бы очень конкретным и удивительным предположением. Мой ответ работает для случаев make, отличных от разработки программного обеспечения, кроме вашей конкретной проблемы новичка, которая возникла при удалении ваших материалов.
Рич
1
Кусалананда научил меня ловить рыбу. Вы пришли и сказали: «Поскольку вы живете на равнинах, почему бы вам не съесть говядину?»
Рафаэль