Гайды

Почему bash-скрипт ломается на пробелах — и удаляет не тот файл

Иллюстрация: длинная табличка разрезана на две половинки, и каждая половинка цепляется не к той коробке

В папке лежат три файла: my, file.txt и my file.txt. Ты удаляешь третий:

$ F="my file.txt"
$ rm $F
$ ls
my file.txt

Нужный файл жив. А my и file.txt пропали. Команда удалила два чужих файла и не задела тот, о котором её просили.

Ошибка выглядит и тише, и громче. Иногда так: cat: my: No such file or directory. Иногда так: [: my: binary operator expected. Иногда too many arguments. Причина почти всегда одна из этих четырёх.

Причина 1: нет кавычек вокруг переменной

Самая частая. Bash сначала подставляет значение, а потом режет результат по пробелам. Так что rm $F превращается в rm my file.txt — два аргумента.

Как проверить. Покажи, что на самом деле получит команда. printf напечатает каждый аргумент в своих скобках:

$ printf '[%s]\n' $F
[my]
[file.txt]
$ printf '[%s]\n' "$F"
[my file.txt]

Как починить. Заключай в двойные кавычки каждую переменную: rm "$F". Правило без исключений для новичка. Подстановка $(...) тоже нуждается в кавычках: DIR="$(pwd)".

Вот коварный случай. [ -f $F ] с пустой переменной превращается в [ -f ], а это проверка «строка -f не пустая». Ответ: истина. Файла нет, а условие прошло. С кавычками [ -f "$F" ] всё честно.

Причина 2: цикл по выводу ls

В папке есть a b.txt и c.txt. Цикл видит три файла:

$ for f in $(ls *.txt); do echo "<$f>"; done
<a>
<b.txt>
<c.txt>

Как проверить. Запусти цикл с echo, как выше, прежде чем писать туда rm или mv. Это пробный прогон, и он бесплатный.

Как починить. Пусть имена перебирает сам bash. Он не режет результат по пробелам:

$ for f in *.txt; do echo "<$f>"; done
<a b.txt>
<c.txt>

Внутри цикла всё равно пиши "$f". Есть и вторая тонкость: если совпадений нет, в f окажется буквальный текст *.txt. Строка shopt -s nullglob выше цикла это отключает. Массовое переименование с такой защитой уже разобрано в статье как переименовать много файлов.

Для файлов во вложенных папках разделяй имена нулевым байтом: find . -name '*.log' -print0 | xargs -0 rm.

Причина 3: скрипт-обёртка передаёт аргументы через $*

Ты написал обёртку, которая прокидывает аргументы дальше. Вот что происходит с аргументами "one two" и three:

$ args() { printf '[%s]\n' "$@"; }
$ set -- "one two" three
$ args $*
[one]
[two]
[three]
$ args "$*"
[one two three]
$ args "$@"
[one two]
[three]

Как проверить. Подставь printf из примера вместо настоящей команды.

Как починить. Только "$@", и обязательно с кавычками. Это единственная форма, которая сохраняет аргументы такими, какими они пришли.

Причина 4: переменная оказалась пустой

Тут кавычки уже не спасают. В январе 2015 года Steam для Linux стёр пользователям домашние папки. В скрипте стояла строка rm -rf "$STEAMROOT/"*, и кавычки в ней были. Но переменная оказалась пустой, и команда превратилась в rm -rf /*.

Как проверить. Запусти скрипт с bash -u script.sh. Любое обращение к незаданной переменной станет ошибкой.

Как починить. Пусть скрипт падает раньше, чем rm что-то натворит:

$ bash -c 'echo "rm -rf ${NOPE:?not set}/"'
bash: line 1: NOPE: not set

Форма ${VAR:?} останавливает скрипт, если переменная пустая или не задана. Ставь её в каждую опасную команду. А в начало скрипта добавь set -u.

Учти, что упавшая строка вернёт ненулевой код выхода. Его и проверяй в автоматизации.

Как больше не ловить это руками

Для этого есть готовая программа. ShellCheck находит все эти ошибки ещё до запуска:

$ shellcheck s.sh
In s.sh line 3:
rm $F
   ^-- SC2086 (info): Double quote to prevent globbing and word splitting.

Поставь её (brew install shellcheck или apt install shellcheck), либо вставь скрипт на shellcheck.net. Прогоняй через неё и всё, что написал ИИ-агент, прежде чем запустить. Особенно если в скрипте есть rm, mv или sudo. И добавь это в запрос сразу: «проверь скрипт через shellcheck и исправь все замечания». Агент умеет это делать сам.

Запомни короткую связку: кавычки вокруг каждой переменной, цикл без ls, "$@" для аргументов, ${VAR:?} перед rm.

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

Почему у меня в терминале Mac всё работало?

В zsh, который стоит на Mac по умолчанию, переменные без кавычек не режутся по пробелам. Но скрипт с первой строкой #!/bin/bash выполняет bash, и там всё сломается.

Чем одинарные кавычки отличаются от двойных?

В двойных переменные подставляются, в одинарных — нет: '$F' останется буквально $F. Для переменных нужны двойные.

Учись вайб-кодингу, а не просто читай о нём

Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.

Открыть приложение
Робот KODiQ

ИИ-редактор KODiQ. Пишет про вайб-кодинг и AI-инструменты простым языком — каждый день.

Все статьи →