Что такое второй sh в sh -c некоторого шелл-кода sh?

Вопрос

Я обнаружил следующий фрагмент:

       sh -c 'some shell code' sh …

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

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


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

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

Основные вопросы, похожие или связанные вопросы, затронутые здесь:

  1. Какой второй sh в sh -c 'some shell code' sh …?
  2. Какой второй bash в bash -c 'some shell code' bash …?
  3. Что такое find-sh в find . -exec sh -c 'some shell code' find-sh {} \;?
  4. Если some shell code был в сценарии оболочки, и мы позвонили ./myscript foo …, тогда foo будет называться $1внутри скрипта. Но sh -c 'some shell code' foo … (или же bash -c …) относится к foo в виде $0. Почему несоответствие?
  5. Что не так с использованием sh -c 'some shell code' foo … где fooтакое "случайный" аргумент? В частности:

    • sh -c 'some shell code' "$variable"
    • sh -c 'some shell code' "$@"
    • find . -exec sh -c 'some shell code' {} \;
    • find . -exec sh -c 'some shell code' {} +

    Я имею в виду, что могу использовать $0 вместо $1 внутри some shell code, меня это не беспокоит. Что плохого может случиться?

Некоторые из вышеперечисленных могут рассматриваться как дубликаты (возможно, межсайтовые дубликаты) существующих вопросов (например, этого). Тем не менее я не нашел вопроса / ответа, который был бы направлен на объяснение проблемы новичкам, которые хотят понять sh -c …и его якобы бесполезный дополнительный аргумент, наблюдаемый в высококачественных ответах. Этот вопрос восполняет пробел.

1 ответ

Решение

Предварительное примечание

Это довольно необычно видеть sh -c 'some shell code'вызывается непосредственно из оболочки. На практике, если вы находитесь в оболочке, вы, вероятно, предпочтете использовать ту же оболочку (или ее подоболочку) для выполнения some shell code. Довольно часто можно увидеть sh -c вызывается из другого инструмента, например find -exec.

Тем не менее, большая часть этого ответа подробно описывает sh -c представлена ​​как отдельная команда (что и есть), потому что основная проблема зависит исключительно от sh. Позже несколько примеров и подсказок используют find где это кажется полезным и / или поучительным.


Основной ответ

Какой второй sh в sh -c 'some shell code' sh …?

Это произвольная строка. Его цель - предоставить содержательное имя для использования в предупреждениях и сообщениях об ошибках. Вот это sh но это могло быть foo, shell1 или же special purpose shell (правильные кавычки, чтобы включать пробелы).

Bash и другие совместимые с POSIX оболочки работают одинаково, когда дело доходит до -c. Хотя я считаю документацию POSIX слишком формальной, чтобы ее цитировать здесь, отрывок из man 1 bash довольно просто:

bash [options] [command_string | file]

-c
Если -c опция присутствует, то команды читаются из первого аргумента без опции command_string. Если есть аргументы после command_string, первый аргумент присваивается $0а любые оставшиеся аргументы присваиваются позиционным параметрам. Поручение $0 устанавливает имя оболочки, которое используется в предупреждениях и сообщениях об ошибках.

В нашем случае some shell code это command_string и в этот второй shэто "первый аргумент после". Он закреплен за $0 в контексте some shell code.

Таким образом, ошибка от sh -c 'nonexistent-command' "special purpose shell" является:

special purpose shell: nonexistent-command: command not found

и вы сразу узнаете, из какой он оболочки. Это полезно, если у вас много sh -cпризывы. "Первый аргумент после command_string"может не поставляться вообще; в таком случае sh (строка) будет присвоена $0 если оболочка sh, bash если оболочка bash. Следовательно, они эквивалентны:

sh -c 'some shell code' sh
sh -c 'some shell code'

Но если вам нужно передать хотя бы один аргумент после some shell code (т.е., возможно, аргументы, которые следует назначить $1, $2,…), То нельзя пропустить тот, который будет назначен $0.


Несоответствие?

Если some shell code был в сценарии оболочки, и мы позвонили ./myscript foo …, тогда foo будет называться $1внутри скрипта. Но sh -c 'some shell code' foo … (или же bash -c …) относится к foo в виде $0. Почему несоответствие?

Оболочка, интерпретирующая сценарий, назначает имя сценария (например, ./myscript) к $0. Затем имя будет использоваться в предупреждениях и сообщениях об ошибках. Обычно такое поведение совершенно нормально, и нет необходимости предоставлять $0вручную. С другой стороны, с sh -cнет сценария, из которого можно было бы получить имя. Тем не менее, какое-то значащее имя полезно, отсюда и способ его предоставления.

Расхождение исчезнет, ​​если вы перестанете рассматривать первый аргумент после some shell codeкак (своего рода) позиционный параметр для кода. Если some shell code находится в сценарии с именем myscript и ты звонишь ./myscript foo …, то эквивалентный код с sh -c будет:

sh -c 'some shell code' ./myscript foo …

Вот ./myscriptэто просто строка, выглядит как путь, но этот путь может не существовать; Во-первых, строка может быть другой. Таким образом можно использовать тот же шелл-код. Оболочка назначит foo к $1в обоих случаях. Нет противоречий.


Подводные камни лечения $0 нравиться $1

Что не так в использовании sh -c 'some shell code' foo …где foo - "случайный" аргумент? […] То есть я могу использовать $0 вместо $1 внутри some shell code, меня это не беспокоит. Что плохого может случиться?

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

  1. Самая очевидная ошибка - вы можете получить вводящие в заблуждение предупреждения или ошибки от вызванной оболочки. Помните, они начнут с чего угодно $0расширяется до в контексте оболочки. Рассмотрим этот фрагмент:

    sh -c 'eecho "$0"' foo    # typo intended
    

    Ошибка:

    foo: eecho: command not found
    

    и вы можете спросить, если fooрассматривался как команда. Это не так уж плохо, если fooжестко запрограммирован и уникален; по крайней мере, вы знаете, что ошибка как-то связана с foo, поэтому он привлекает ваше внимание именно к этой строке кода. Бывает и хуже:

    # as regular user
    sh -c 'ls "$0" > "$1"/' "$HOME" "/root/foo"
    

    Выход:

    /home/kamil: /root/foo: Permission denied
    

    Первая реакция: что случилось с моим домашним каталогом? Другой пример:

    find /etc/fs* -exec sh -c '<<EOF' {} \;    # insane shell code intended
    

    Возможный выход:

    /etc/fstab: warning: here-document at line 0 delimited by end-of-file (wanted `EOF')
    

    Очень легко думать, что что-то не так с /etc/fstab; или задаться вопросом, почему код хочет интерпретировать это как здесь-документ.

    Теперь запустите эти команды и посмотрите, насколько точны ошибки, когда мы даем значимые имена:

    sh -c 'eecho "$1"' "shell with echo" foo    # typo intended
    sh -c 'ls "$1" > "$2"/' my-special-shell "$HOME" "/root/foo"
    find /etc/fs* -exec sh -c '<<EOF' find-sh {} \;    # insane shell code intended
    
  2. some shell codeне идентично тому, что было бы в сценарии. Это напрямую связано с указанным выше предполагаемым несоответствием. Это может не быть проблемой; все же на каком-то уровне шелл-фу вы можете оценить последовательность.

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

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

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

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

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

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

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

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

      find . -exec sh -c 'some shell code referring "$1"' find-sh {} \;
      
    2. Вы замечаете, что он работает shна файл. Это неоптимально.

    3. Тебе известно -exec … \; заменяет {} с одним именем файла, но -exec … {} + заменяет {}с возможно многими именами файлов. Вы пользуетесь преимуществом последнего и вводите цикл:

      find . -exec sh -c '
         for f do
            some shell code referring "$f"
         done
      ' find-sh {} +
      

    Такая оптимизация - дело хорошее. Но если начать с этого:

    # not exactly right but you will get away with this
    find . -exec sh -c 'some shell code referring "$0"' {} \;
    

    и превратите его в это:

    # flawed
    find . -exec sh -c '
       for f do
          some shell code referring "$f"
       done
    ' {} +
    

    тогда вы введете ошибку: самый первый файл, полученный из расширения {} не будет обработан some shell code referring "$f". Запись -exec sh -c … {} + бежит sh с максимально возможным количеством аргументов, но для этого есть ограничения, и если файлов много-много, то один sh не хватит, другой sh процесс будет порожден find(и, возможно, еще и еще, …). С каждым sh вы пропустите (т.е. не обработаете) один файл.

    Чтобы проверить это на практике, замените строку some shell code referring с участием echoи запустите полученные фрагменты кода в каталоге с несколькими файлами. Последний фрагмент не печатается ..

    Все это не означает, что вы не должны использовать $0 в some shell codeвообще. Вы можете и должны использовать $0для вещей, для которых он был разработан. Например, если вы хотите some shell code чтобы напечатать (настраиваемое) предупреждение или ошибку, затем начните сообщение с $0. Введите значимое имя после some shell code и наслаждайтесь значимыми ошибками (если таковые имеются) вместо расплывчатых или вводящих в заблуждение.


Заключительные подсказки:

  • С участием find … -exec sh -c … никогда не вставлять {}в коде оболочки.

  • По тем же причинам some shell codeне должны содержать фрагменты, расширенные текущей оболочкой, если вы действительно не знаете, что расширенные значения безопасны. Лучше всего заключать весь код в одинарные кавычки (как в приведенных выше примерах, всегда 'some shell code') и передайте каждое нефиксированное значение как отдельный аргумент. Такой аргумент можно безопасно получить из позиционного параметра во внутренней оболочке. Экспорт переменных также безопасен. Запустите это и проанализируйте вывод каждого sh -c … (желаемый результат foo';date'):

    variable="foo';date'"
    
    # wrong
    sh -c "echo '$variable'" my-sh
    
    # right
    sh -c 'echo "$1"' my-sh "$variable"
    
    # also right
    export variable
    sh -c 'echo "$variable"' my-sh
    
  • Если ты бежишь sh -c 'some shell code' … в оболочке оболочка удалит одинарные кавычки, заключающие some shell code; затем внутренняя оболочка (sh) будет разбирать some shell code. В этом контексте важно правильно цитировать. Вы можете найти это полезным: Расширение параметров и кавычки внутри кавычек.

Другие вопросы по тегам