Вопрос или проблема
Вопрос
Я столкнулся со следующим фрагментом:
sh -c 'некоторый shell код' sh …
(где …
обозначает ноль или более дополнительных аргументов).
Я знаю, что первый sh
– это команда. Я знаю, что sh -c
должен выполнять предоставленный shell код (т.е. некоторый shell код
). Какова цель второго sh
?
Отказ от ответственности
Похожие или связанные вопросы иногда появляются как последующие вопросы после того, как sh -c
правильно используется в ответе, и спрашивающий (или другой пользователь) хочет узнать в деталях, как работает ответ. Или это может быть частью более крупного вопроса типа “что делает этот код?”. Цель текущего вопроса – предоставить каноничный ответ ниже.
Главные вопросы, похожие или связанные с этими, следующие:
- Что такое второй
sh
вsh -c 'некоторый shell код' sh …
? - Что такое второй
bash
вbash -c 'некоторый shell код' bash …
? - Что такое
find-sh
вfind . -exec sh -c 'некоторый shell код' find-sh {} \;
? - Если
некоторый shell код
находился в shell скрипте и мы вызвали./myscript foo …
, тогдаfoo
будет обозначаться как$1
внутри скрипта. Ноsh -c 'некоторый shell код' foo …
(илиbash -c …
) ссылается наfoo
как$0
. Почему такая разница? -
Что не так с использованием
sh -c 'некоторый shell код' foo …
, гдеfoo
является “случайным” аргументом? В частности:sh -c 'некоторый shell код' "$variable"
sh -c 'некоторый shell код' "$@"
find . -exec sh -c 'некоторый shell код' {} \;
find . -exec sh -c 'некоторый shell код' {} +
Я имею в виду, что я могу использовать
$0
вместо$1
внутринекоторого shell кода
, это меня не беспокоит. Что плохого может произойти?
Некоторые из вышеизложенного могут считаться дубликатами (возможно, межсайтовыми дубликатами) существующих вопросов (например, этот). Тем не менее, я не нашел вопроса/ответа, который нацелен на объяснение проблемы для новичков, которые хотят понять sh -c …
и его предполагаемо бесполезный дополнительный аргумент, наблюдаемый в ответах высокого качества. Этот вопрос заполняет этот пробел.
Предварительная запись
Довольно необычно видеть sh -c 'некоторый shell код'
, вызываемый непосредственно из shell. На практике, если вы находитесь в shell, вы, вероятно, решите использовать тот же shell (или его подсhell) для выполнения некоторого shell кода
. Довольно часто sh -c
вызывается из других инструментов, таких как find -exec
.
Тем не менее, большая часть этого ответа объясняет sh -c
, представляющий собой самостоятельную команду (чем он и является), потому что основная проблема зависит исключительно от sh
. Позже несколько примеров и подсказок используют find
, где это кажется полезным и/или образовательным.
Базовый ответ
Что такое второй
sh
вsh -c 'некоторый shell код' sh …
?
Это произвольная строка. Его цель – предоставить значимое имя для использования в предупреждениях и ошибках. Здесь это sh
, но это может быть foo
, shell1
или специальный shell
(правильно заключенный в кавычки, чтобы включать пробелы).
Bash и другие совместимые с POSIX оболочки работают аналогично в случае с -c
. Хотя я считаю документацию POSIX слишком формальной, чтобы здесь цитировать, отрывок из man 1 bash
довольно очевиден:
bash [options] [command_string | file]
-c
Если параметр-c
присутствует, команды читаются из первого не опционального аргументаcommand_string
. Если послеcommand_string
есть аргументы, первый аргумент присваивается$0
, а все оставшиеся аргументы присваиваются позиционным параметрам. Присвоение$0
устанавливает имя оболочки, которое используется в предупреждениях и ошибках.
В нашем случае некоторый shell код
– это command_string
, а этот второй sh
– “первый аргумент после”. Он присваивается $0
в контексте некоторого shell кода
.
Таким образом, ошибка от sh -c 'некоманду-не-существует' "специальный shell"
будет такой:
специальный shell: некокоманда-не-существует: команда не найдена
и вы сразу узнаете, из какого shell это произошло. Это полезно, если у вас много вызовов sh -c
. “Первый аргумент после command_string
” может вовсе не быть предоставлен; в таком случае sh
(строка) будет присвоен $0
, если shell – sh
, bash
, если shell – bash
. Таким образом, эти два варианта эквивалентны:
sh -c 'некоторый shell код' sh
sh -c 'некоторый shell код'
Но если вам необходимо передать хотя бы один аргумент после некоторого shell кода
(т.е. возможно, аргументы, которые должны быть присвоены $1
, $2
, …), то нельзя будет пропустить тот, который будет присвоен $0
.
Разница?
Если
некоторый shell код
был в shell скрипте и мы вызывали./myscript foo …
, тогдаfoo
будет обозначаться как$1
внутри скрипта. Ноsh -c 'некоторый shell код' foo …
(илиbash -c …
) ссылается наfoo
как$0
. Почему такая разница?
Обычно shell, интерпретирующий скрипт, получает имя скрипта (например, ./myscript
) как нулевую запись в массиве аргументов, доступных позже как $0
. Затем имя будет использоваться в предупреждениях и ошибках. Обычно это поведение вполне нормально, и нет необходимости вручную задавать $0
. С другой стороны, в sh -c
нет скрипта, из которого можно получить имя. Тем не менее, некоторое значимое имя полезно, и поэтому предусмотрен способ его предоставить.
Разница исчезнет, если вы перестанете рассматривать первый аргумент после некоторого shell кода
как (в некотором роде) позиционный параметр для кода. Если некоторый shell код
находится в скрипте с именем myscript
и вы вызываете ./myscript foo …
, тогда эквивалентный код с sh -c
будет:
sh -c 'некоторый shell код' ./myscript foo …
Здесь ./myscript
– это просто строка, она выглядит как путь, но этот путь может не существовать; строка может быть другой в первую очередь. Таким образом, тот же shell код может быть использован. Shell присвоит foo
$1
в обоих случаях. Нет разницы.
Подводные камни обращения с $0
как с $1
Что не так с использованием
sh -c 'некоторый shell код' foo …
, где foo является “случайным” аргументом? […] Я имею в виду, что я могу использовать$0
вместо$1
внутринекоторого shell кода
, это меня не беспокоит. Что плохого может произойти?
В многих случаях это будет работать. Однако есть аргументы против такого подхода.
-
Самый очевидный подводный камень – вы можете получить вводящие в заблуждение предупреждения или ошибки от вызываемого shell. Помните, что они начнутся с того, что
$0
расширяется в контексте оболочки. Учтите этот фрагмент:sh -c 'eecho "$0"' foo # опечатка намеренная
Ошибка будет следующей:
foo: eecho: команда не найдена
и вы можете задаться вопросом, был ли
foo
трактован как команда. Это не так уж плохо, еслиfoo
закодирован и уникален; по крайней мере, вы знаете, что ошибка имеет отношение кfoo
, так что это привлекает ваше внимание на эту самую строку кода. Это может быть хуже:# как обычный пользователь sh -c 'ls "$0" > "$1"/' "$HOME" "/root/foo"
Вывод:
/home/kamil: /root/foo: Доступ запрещен
Первая реакция: что произошло с моей домашней директорией? Другой пример:
find /etc/fs* -exec sh -c '<<EOF' {} \; # безумный shell код намеренный
Возможный вывод:
/etc/fstab: предупреждение: here-document на линии 0, завершающийся концом файла (ожидался `EOF')
Очень легко подумать, что что-то не так с
/etc/fstab
; или задаться вопросом, почему код хочет интерпретировать это как here-document.Теперь выполните эти команды и посмотрите, насколько точны ошибки, когда мы предоставляем значимые имена:
sh -c 'eecho "$1"' "shell с echo" foo # опечатка намеренная sh -c 'ls "$1" > "$2"/' my-special-shell "$HOME" "/root/foo" find /etc/fs* -exec sh -c '<<EOF' find-sh {} \; # безумный shell код намеренный
-
некоторый shell код
не идентичен тому, что он был бы в скрипте. Это напрямую связано с предполагаемой разницей, изложенной выше. Это может не быть проблемой вообще; все же на определенном уровне shell-fu вы можете оценить последовательность. -
Аналогично, на определенном уровне вы можете обнаружить, что вам приятно программировать правильным образом. Тогда даже если вы можете обойтись использованием
$0
, вы не будете это делать, потому что это не так, как вещи должны работать. -
Если вы хотите передать более одного аргумента или если количество аргументов заранее неизвестно и вам нужно обработать их по порядку, тогда использование
$0
для одного из них – плохая идея.$0
по замыслу отличается от$1
или$2
. Этот факт проявит себя, еслинекоторый shell код
использует один или несколько из следующих:-
$#
– Количество позиционных параметров не учитывает$0
, потому что$0
не является позиционным параметром. -
$@
или$*
–"$@"
такое же, как"$1", "$2", …
, в этой последовательности нет"$0"
. -
for f do
(что эквивалентноfor f in "$@"; do
) –$0
никогда не присваивается$f
. -
shift
(shift [n]
в общем) – Позиционные параметры сдвигаются,$0
остается неизменным.
В частности, рассмотрите этот сценарий:
-
Вы начинаете с кода, как этот:
find . -exec sh -c 'некоторый shell код, ссылающийся на "$1"' find-sh {} \;
-
Вы замечаете, что он запускает один
sh
на каждый файл. Это не оптимально. -
Вы знаете, что
-exec … \;
заменяет{}
одним именем файла, но-exec … {} +
заменяет{}
возможно множеством имен файлов. Вы пользуетесь этим и вводите цикл:find . -exec sh -c ' for f do некоторый shell код, ссылающийся на "$f" done ' find-sh {} +
Такая оптимизация – хорошая вещь. Но если вы начнете с этого:
# не совсем правильно, но вы сойдете с этим find . -exec sh -c 'некоторый shell код, ссылающийся на "$0"' {} \;
и превратите это в это:
# испорчено find . -exec sh -c ' for f do некоторый shell код, ссылающийся на "$f" done ' {} +
тогда вы введете ошибку: первый файл, приходящий из расширения
{}
, не будет обработаннекоторым shell кодом, ссылающимся на "$f"
. Обратите внимание, что-exec sh -c … {} +
запускаетsh
с максимальным возможным количеством аргументов, но для этого есть пределы, и если файлов много, то одинsh
не будет достаточен, будет запущен другой процессsh
find
(и возможно другой, и другой, …). С каждымsh
вы пропустите (т.е. не обработаете) один файл.Чтобы протестировать это на практике, замените строку
некоторый shell код, ссылающийся
наecho
и выполните полученные фрагменты кода в директории с несколькими файлами. Последний фрагмент не выведет.
.Все это не означает, что вы не должны использовать
$0
внекотором shell коде
вообще. Вы можете и должны использовать$0
для тех вещей, для которых он был предназначен. Например, если вы хотите, чтобынекоторый shell код
печатал (пользовательское) предупреждение или ошибку, сделайте так, чтобы сообщение начиналось с$0
. Предоставьте значимое имя посленекоторого shell кода
и наслаждайтесь значительными ошибками (если таковые имеются), вместо расплывчатых или вводящих в заблуждение. -
Финальные советы:
-
С
find … -exec sh -c …
никогда не встраивайте{}
в shell код. -
По тем же причинам
некоторый shell код
не должен содержать фрагменты, расширяемые текущей оболочкой, если вы действительно не знаете, что расширенные значения безопасны. Лучшая практика – заключить весь код в одинарные кавычки (как в приведенных выше примерах, это всегда'некоторый shell код'
) и передавать каждое непостоянное значение как отдельный аргумент. Такой аргумент можно безопасно получить из позиционного параметра внутри внутренней оболочки. Экспорт переменных также безопасен. Выполните это и проанализируйте вывод каждогоsh -c …
(желаемый вывод –foo';date'
):variable="foo';date'" # неверно sh -c "echo '$variable'" my-sh # правильно sh -c 'echo "$1"' my-sh "$variable" # также правильно export variable sh -c 'echo "$variable"' my-sh
-
Если вы запускаете
sh -c 'некоторый shell код' …
в оболочке, оболочка удалит одинарные кавычки, окружавшиенекоторый shell код
. Затем внутренняя оболочка (sh
) будет разбиратьнекоторый shell код
. Важно правильно цитировать и в этом контексте. Вы можете найти это полезным: Расширение параметров и кавычки внутри кавычек.
Ответ или решение
Вопрос о втором sh
в конструкции sh -c 'some shell code' sh ...
часто вызывает интерес у пользователей, изучающих работу с командной оболочкой и скриптами. Давайте разберемся, какую роль выполняет второй sh
в этом контексте.
Понимание конструкции sh -c 'some shell code'
Первое, что следует отметить, это то, что команда sh -c
предназначена для выполнения переданного ей кода оболочки, указанного в строке 'some shell code'
. Этот код будет выполнен в новом экземпляре оболочки. Важно понимать, что после основной строки команды могут передаваться дополнительные аргументы, и здесь мы сталкиваемся со вторым sh
.
Роль второго sh
Второй sh
в этой конструкции — это не что иное, как произвольная строка, которая назначается переменной $0
в контексте выполняемого кода. С точки зрения POSIX-совместимых оболочек, $0
представляет собой имя текущего скрипта или команды. Когда в вызове sh -c
указывается дополнительный аргумент (в данном случае sh
), этот аргумент становится значением $0
.
Вот как это работает на более детальном уровне:
-
Ошибка и предупреждения: Основное назначение аргумента
$0
заключается в том, чтобы предоставить более понятное имя, которое будет использоваться в сообщениях об ошибках и предупреждениях. Например, если код оболочки не найдет команду, сообщение будет выглядеть следующим образом:sh: command not found
Если же передан второй аргумент, например
sh
, сообщение будет выглядеть так:sh: command not found
Это помогает идентифицировать источник ошибки, особенно если вы выполняете много таких вызовов.
-
Гибкость и привычная работа: Второй аргумент позволяет вам присвоить осмысленное имя. Это может быть важно в больших скриптах или при сложных вызовах через утилиты, такие как
find -exec
, где вы запускаете множество команд, и логирование ошибок может стать критичным.
Примеры
Рассмотрим конкретные примеры для ясности:
-
Прямой вызов:
sh -c 'echo "Hello, World!"' sh
Если бы у нас была ошибка внутри выполненного кода, то
$0
в сообщении об ошибке указывало бы наsh
. -
Передача аргументов:
sh -c 'echo "First argument: $1"' custom_shell arg1
Здесь
$0
будетcustom_shell
, а$1
станетarg1
. Таким образом, обратная связь об ошибках или предупреждениях будет более понятной.
Заключение
В заключение, второй sh
в выражении sh -c 'some shell code' sh ...
служит для назначения имени текущему исполняемому окружению. Это важно для создания информативных сообщений об ошибках и улучшает читаемость и отладку вашего кода. Рассматривая этот аспект, вы улучшаете качество вашего скриптового кода и упрощаете процесс отладки, что очень ценно в IT.