Что означает второй sh в sh -c 'some shell code' sh?

Вопрос или проблема

Вопрос

Я столкнулся со следующим фрагментом:

sh -c 'некоторый shell код' sh …

(где обозначает ноль или более дополнительных аргументов).

Я знаю, что первый sh – это команда. Я знаю, что sh -c должен выполнять предоставленный shell код (т.е. некоторый shell код). Какова цель второго sh?


Отказ от ответственности

Похожие или связанные вопросы иногда появляются как последующие вопросы после того, как sh -c правильно используется в ответе, и спрашивающий (или другой пользователь) хочет узнать в деталях, как работает ответ. Или это может быть частью более крупного вопроса типа “что делает этот код?”. Цель текущего вопроса – предоставить каноничный ответ ниже.

Главные вопросы, похожие или связанные с этими, следующие:

  1. Что такое второй sh в sh -c 'некоторый shell код' sh …?
  2. Что такое второй bash в bash -c 'некоторый shell код' bash …?
  3. Что такое find-sh в find . -exec sh -c 'некоторый shell код' find-sh {} \;?
  4. Если некоторый shell код находился в shell скрипте и мы вызвали ./myscript foo …, тогда foo будет обозначаться как $1 внутри скрипта. Но sh -c 'некоторый shell код' foo … (или bash -c …) ссылается на foo как $0. Почему такая разница?
  5. Что не так с использованием 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 кода, это меня не беспокоит. Что плохого может произойти?

В многих случаях это будет работать. Однако есть аргументы против такого подхода.

  1. Самый очевидный подводный камень – вы можете получить вводящие в заблуждение предупреждения или ошибки от вызываемого 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 код намеренный
    
  2. некоторый shell код не идентичен тому, что он был бы в скрипте. Это напрямую связано с предполагаемой разницей, изложенной выше. Это может не быть проблемой вообще; все же на определенном уровне shell-fu вы можете оценить последовательность.

  3. Аналогично, на определенном уровне вы можете обнаружить, что вам приятно программировать правильным образом. Тогда даже если вы можете обойтись использованием $0, вы не будете это делать, потому что это не так, как вещи должны работать.

  4. Если вы хотите передать более одного аргумента или если количество аргументов заранее неизвестно и вам нужно обработать их по порядку, тогда использование $0 для одного из них – плохая идея. $0 по замыслу отличается от $1 или $2. Этот факт проявит себя, если некоторый shell код использует один или несколько из следующих:

    • $# – Количество позиционных параметров не учитывает $0, потому что $0 не является позиционным параметром.

    • $@ или $*"$@" такое же, как "$1", "$2", …, в этой последовательности нет "$0".

    • for f do (что эквивалентно for f in "$@"; do) – $0 никогда не присваивается $f.

    • shift (shift [n] в общем) – Позиционные параметры сдвигаются, $0 остается неизменным.

    В частности, рассмотрите этот сценарий:

    1. Вы начинаете с кода, как этот:

      find . -exec sh -c 'некоторый shell код, ссылающийся на "$1"' find-sh {} \;
      
    2. Вы замечаете, что он запускает один sh на каждый файл. Это не оптимально.

    3. Вы знаете, что -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.

Вот как это работает на более детальном уровне:

  1. Ошибка и предупреждения: Основное назначение аргумента $0 заключается в том, чтобы предоставить более понятное имя, которое будет использоваться в сообщениях об ошибках и предупреждениях. Например, если код оболочки не найдет команду, сообщение будет выглядеть следующим образом:

    sh: command not found

    Если же передан второй аргумент, например sh, сообщение будет выглядеть так:

    sh: command not found

    Это помогает идентифицировать источник ошибки, особенно если вы выполняете много таких вызовов.

  2. Гибкость и привычная работа: Второй аргумент позволяет вам присвоить осмысленное имя. Это может быть важно в больших скриптах или при сложных вызовах через утилиты, такие как 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.

Оцените материал
Добавить комментарий

Капча загружается...