Год назад вышла ShellCheck v0.7.1. Главным образом в ней были подчищены и исправлены имеющиеся проверки, но также появились и новые. Лично меня из всех новинок больше всех удивила та, что указывает на проблему, обсуждение которой я еще нигде не встречал:
In demo line 6: cat template/header.txt "$f" > archive/$((i++)).txt ^ SC2257: Arithmetic modifications in command redirections may be discarded. Do them separately. #Арифметические изменения при перенаправлениях в command могут быть #отброшены. Выполняйте их отдельно
А вот весь скрипт:
#!/bin/bashi=1for f in *.txtdo echo "Archiving $f as $i.txt" cat template/header.txt "$f" > archive/$((i++)).txtdone
Опытные сценаристы уже наверняка забежали вперед и повторили это в своей оболочке, выяснив, что изменение будет работать, по крайней мере в Bash 5.0.16(1):
bash-5.0$ i=0; echo foo > $((i++)).txt; echo "$i" 1
Исходя из этого вы можете ожидать беглого просмотра истории коммитов Bash, и, быть может, призыва сохранять благосклонность к нашим обездоленным собратьям на macOS, использующим Bash 3.
Но нет. Вот демо-скрипт на той же системе:
bash-5.0$ ./demoArchiving chocolate_cake_recipe.txt as 1.txtArchiving emo_poems.txt as 1.txtArchiving project_ideas.txt as 1.txt
То же самое верно для
source ./demo
, которая выполняет
скрипт в том же экземпляре оболочки, где мы только что проводили
проверку. Более того, происходит это только при перенаправлениях,
но не в аргументах.Так в чем же здесь дело?
Оказывается, что Bash, Ksh и BusyBox ash в процессе установки файловых дескрипторов также расширяют имя файла, из которого происходит перенаправление. Если вы знакомы с моделью процессов Unix, то псевдокод будет выглядеть так:
if command is external: fork child process: filename := expandString(command.stdout) # инкрементно увеличивает i fd[1] := open(filename) execve(command.executable, command.args)else: filename := expandString(command.stdout) # инкрементно увеличивает i tmpFd := open(filename) run_internal_command(command, stdout=tmpFD) close(tmpFD)
Говоря иначе, область изменения переменной зависит от того, произвела ли оболочка ответвление нового процесса в ожидании выполнения команды.
Для встроенных команд, которые не разветвляются, например
echo
, это означает, что изменение произойдет в текущей
оболочке. Именно такой тест мы и провели.Для внешних же команд вроде
cat
изменение видимо
только между моментом установки файлового дескриптора и вызовом
команды для выполнения процесса. Это и делает демо-скрипт.Конечно же, подоболочки хорошо известны опытным программистам, а также описаны в статье Why Bash is like that: Subshells. Но лично для меня это новый и, в частности, коварный их источник.
К примеру, этот скрипт отлично работает в busybox sh, где
cat
является встроенной:
$ busybox sh demoArchiving chocolate_cake_recipe.txt as 1.txtArchiving emo_poems.txt as 2.txtArchiving project_ideas.txt as 3.txt
Аналогичным образом эта область может зависеть от того, переопределяли ли вы какие-либо команды функции-обертки:
awk() { gawk "$@"; }# Инкрементируетawk 'BEGIN {print "hi"; exit;}' > $((i++)).txt# Не инкрементируетgawk 'BEGIN {print "hi"; exit;}' > $((i++)).txt
Либо, если вы хотите переопределить псевдоним, то результат будет зависеть от того, использовали ли вы
command
или
\
:
# Инкрементируетcommand git show . > $((i++)).txt# Не инкрементирует\git show . > $((i++)).txt
Чтобы избежать этой путаницы, обратите внимание на совет ShellCheck, и если при перенаправлении переменная является частью имени файла, то просто увеличивайте ее отдельно:
anything > "$((i++)).txt": $((i++))
Выражаю благодарность Strolls из #bash@Freenode за то, что указал на это поведение.
P.S. В процессе подготовки материала для статьи я выяснил, что dash всегда производит увеличение (хоть и с помощью
$((i=i+1)
), так как не поддерживает ++
).
ShellCheck v0.7.1 по-прежнему делает предупреждение, а код из
master-ветки этого уже не делает.