Путь независимых shebangs

У меня есть сценарий, который я хочу запустить на двух машинах. Эти две машины получают копии сценария из одного и того же репозитория git. Скрипт должен работать с правильным интерпретатором (например, zsh ).

К сожалению, как env и zsh живут в разных местах на локальных и удаленных машинах:

Удаленная машина

 $ which env /bin/env $ which zsh /some/long/path/to/the/right/zsh 

Локальная машина

 $ which env /usr/bin/env $which zsh /usr/local/bin/zsh 

Как настроить shebang так, чтобы запуск скрипта как /path/to/script.sh всегда использовал Zsh доступный в PATH ?

4 Solutions collect form web for “Путь независимых shebangs”

Вы не можете решить это через shebang напрямую, так как shebang является чисто статическим. То, что вы можете сделать, это иметь «наименьший общий множитель» (с точки зрения оболочки) в shebang и повторить выполнение скрипта с помощью правильной оболочки, если этот LCM не является zsh. Другими словами: выполните ли ваш скрипт оболочкой, найденной во всех системах, протестируйте функцию zsh -only, и если тест окажется ложным, попробуйте выполнить скрипт с zsh , где тест будет успешным, и вы просто продолжите.

Одной из уникальных особенностей zsh , например, является наличие переменной $ZSH_VERSION :

 #!/bin/sh - [ -z "$ZSH_VERSION" ] && exec zsh - "$0" ${1+"$@"} # zsh-specific stuff following here echo "$ZSH_VERSION" 

В этом простом случае сценарий сначала выполняется /bin/sh (все системы, подобные Unix-версии после 80-х, понимают #! И имеют /bin/sh , либо Bourne, либо POSIX, но наш синтаксис совместим с обоими). Если $ZSH_VERSION не установлен, скрипт exec сам по zsh . Если $ZSH_VERSION (например, скрипт уже запущен через zsh ), тест просто пропущен. Вуаля.

Это происходит только тогда, когда zsh вообще не находится в $PATH .

Изменить: Чтобы убедиться, вы выполняете только zsh в обычных местах, вы можете использовать что-то вроде

 for sh in /bin/zsh \ /usr/bin/zsh \ /usr/local/bin/zsh; do [ -x "$sh" ] && exec "$sh" - "$0" ${1+"$@"} done 

Это может спасти вас от случайного exec чего-то в вашей $PATH которая не является zsh которую вы ожидаете.

В течение многих лет я использовал что-то похожее, чтобы иметь дело с различными местоположениями Bash в системах, в которых мне нужны мои сценарии для запуска.

Bash / Zsh / и т.д..

 #!/bin/sh # Determines which OS and then reruns this script with approp. shell interp. LIN_BASH="/bin/sh"; SOL_BASH="/packages/utilities/bin/sun5/bash"; OS_TYPE=`uname -s`; if [ $OS_TYPE = "SunOS" ]; then $SOL_BASH -c "`sed -n '/\#\#\# BEGIN/,$p' $0`" $0 $*; elif [ $OS_TYPE = "Linux" ]; then $LIN_BASH -c "`sed -n '/\#\#\# BEGIN/,$p' $0`" $0 $*; else echo "UNKNOWN OS_TYPE, $OS_TYPE"; exit 1; fi exit 0; ### BEGIN ...script goes here... 

Вышеприведенное может быть легко адаптировано для различных переводчиков. Ключевым моментом является то, что этот скрипт первоначально запускается как оболочка Bourne. Затем он рекурсивно называет себя второй раз, но разбирает все выше комментария ### BEGIN используя sed .

Perl

Вот аналогичный трюк для Perl:

 #!/bin/sh LIN_PERL="/usr/bin/perl"; SOL_PERL="/packages/perl/bin/perl"; OS_TYPE=`uname -s`; if [ $OS_TYPE = "SunOS" ]; then eval 'exec $SOL_PERL -x -S $0 ${1+"$@"}'; elif [ $OS_TYPE = "Linux" ]; then eval 'exec $LIN_PERL -x -S $0 ${1+"$@"}'; else echo "$OS_TYPE: UNSUPORRTED OS/PLATFORM"; exit 0; fi exit 0; #!perl ...perl script goes here... 

Этот метод использует возможности Perl, когда заданный файл для запуска будет анализировать указанный файл, пропуская все строки, которые находятся перед строкой #! perl #! perl .

ПРИМЕЧАНИЕ: @ jw013 делает следующее неподдерживаемое возражение в комментариях ниже:

Сдвиг – это то, что самомодифицирующийся код обычно считается плохой практикой. Еще в старые времена крошечных программ сборки это был умный способ уменьшить условные ветви и повысить производительность, но в наши дни риски безопасности перевешивают преимущества. Ваш подход не будет работать, если пользователь, который запускал сценарий, не имел прав на запись в скрипте.

Я ответил на его возражения по поводу защиты, указав, что для установки / обновления сценария самонастройки, который я лично мог бы назвать довольно безопасным, требуется только один раз для каждого действия по установке / обновлению . Я также указал ему на ссылку на достижение аналогичных целей аналогичными средствами. В то время я не хотел указывать на то, что любые недостатки безопасности или вообще непривычные практики, которые могут быть или не могут быть представлены в моем ответе, скорее всего, были связаны с самим вопросом, чем они были в моем ответе на него:

Как настроить shebang так, чтобы запуск скрипта как /path/to/script.sh всегда использовал Zsh, доступный в PATH?

Не удовлетворен, @ jw013 продолжал возражать, продвигая его еще не подтвержденный аргумент, по крайней мере, с несколькими ошибочными утверждениями:

Вы используете один файл, а не два файла. Пакет [ man sh referenced] имеет один файл, который модифицирует другой файл. У вас есть файл, модифицирующий себя. Между этими двумя случаями существует четкое различие. Файл, который принимает входные данные и производит выходные данные, прекрасен. Исполняемый файл, который сам по себе изменяет, как правило, является плохой идеей. Пример, на который вы указали, не делает этого.

В первую очередь:

ТОЛЬКО ИСПОЛНИТЕЛЬНЫЙ КОД В ЛЮБОМ ИСПОЛНИТЕЛЬНОМ СКАНИРОВАНИИ SHELL #! САМ

(хотя даже #! официально не указано )

 { cat >|./file chmod +x ./file ./file } <<-\FILE #!/usr/bin/sh { ${l=lsof -p} $$ echo "$l \$$" | sh } | grep \ "COMMAND\|^..*sh\| [0-9]*[wru] " #END FILE ##OUTPUT COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME file 8900 mikeserv txt REG 0,33 774976 2148676 /usr/bin/bash file 8900 mikeserv mem REG 0,30 2148676 /usr/bin/bash (path dev=0,33) file 8900 mikeserv 0r REG 0,35 108 15496912 /tmp/zshUTTARQ (deleted) file 8900 mikeserv 1u CHR 136,2 0t0 5 /dev/pts/2 file 8900 mikeserv 2u CHR 136,2 0t0 5 /dev/pts/2 file 8900 mikeserv 255r REG 0,33 108 2134129 /home/mikeserv/file COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME sh 8906 mikeserv txt REG 0,33 774976 2148676 /usr/bin/bash sh 8906 mikeserv mem REG 0,30 2148676 /usr/bin/bash (path dev=0,33) sh 8906 mikeserv 0r FIFO 0,8 0t0 15500515 pipe sh 8906 mikeserv 1w FIFO 0,8 0t0 15500514 pipe sh 8906 mikeserv 2u CHR 136,2 0t0 5 /dev/pts/2 { sed -i \ '1c#!/home/mikeserv/file' ./file ./file sh -c './file ; echo' grep '#!' ./file } ##OUTPUT zsh: too many levels of symbolic links: ./file sh: ./file: /home/mikeserv/file: bad interpreter: Too many levels of symbolic links #!/home/mikeserv/file 

Сценарий оболочки – это просто текстовый файл – для того, чтобы он мог иметь какой-либо эффект, он должен быть прочитан другим исполняемым файлом, его инструкции затем интерпретируются другим исполняемым файлом, прежде чем, наконец, другой исполняемый файл затем выполнит свою интерпретацию сценарий оболочки. При выполнении файла сценария оболочки невозможно задействовать менее двух файлов. В собственном компиляторе zsh есть возможное исключение, но с этим у меня мало опыта, и он здесь никоим образом не представлен.

Хэш-код сценария оболочки должен указывать на предполагаемый интерпретатор или быть отброшен как несущественный.

ПРИЗНАНИЕ / ИСПОЛНЕНИЕ ПОТРЕБИТЕЛЯ ОБОЛОЧКИ ОПРЕДЕЛЯЕТСЯ СТАНДАРТЫ

Оболочка имеет два основных режима анализа и интерпретации его ввода: либо его текущий ввод определяет <<here_document либо он определяет { ( command |&&|| list ) ; } & { ( command |&&|| list ) ; } & – другими словами, оболочка интерпретирует токен как разделитель для команды, которую он должен выполнить, когда он прочитал его или как инструкции для создания файла и сопоставил его файловому дескриптору для другой команды. Вот и все.

При интерпретации команд для выполнения оболочки ограничивает токены в наборе зарезервированных слов. Когда оболочка встречает открывающий токен, она должна продолжать считываться в списке команд, пока список не будет разделен закрывающим токеном, таким как новая строка – если применимо, или закрывающий токен как }) для ({ перед исполнением.

Оболочка различает простую команду и составную команду. Составная команда – это набор команд, которые должны быть прочитаны перед выполнением, но оболочка не выполняет $expansion на любую из своих составных простых команд до тех пор, пока оно не будет выполняться отдельно.

Таким образом, в следующем примере зарезервированные слова с ;semicolon ограничивают отдельные простые команды, тогда как символ без \newline ограничивает между двумя составными командами:

 { cat >|./file chmod +x ./file ./file } <<-\FILE #!/usr/bin/sh echo "simple command ${sc=1}" ;\ : > $0 ;\ echo "simple command $((sc+2))" ;\ sh -c "./file && echo hooray" sh -c "./file && echo hooray" #END FILE ##OUTPUT simple command 1 simple command 3 hooray 

Это упрощение руководящих принципов. Это становится намного более сложным, если вы рассматриваете оболочки, подоболочки, текущую среду и т. Д., Но для моих целей здесь достаточно.

И, говоря о встроенных и командных списках, function() { declaration ; } function() { declaration ; } является просто средством назначения составной команды простой команде. Оболочка не должна выполнять никаких $expansions в самом объявлении объявления – включить <<redirections> – но вместо этого следует сохранить это определение как единую, литеральную строку и выполнить ее как специальную оболочку, встроенную при вызове.

Таким образом, функция оболочки, объявленная в исполняемом сценарии оболочки, хранится в памяти интерпретационной оболочки в своей строковой форме – нерасширенная, чтобы включать в себя прилагаемые сюда документы в качестве входных данных, и выполняться независимо от исходного файла каждый раз, когда она вызывается как встроенная оболочка, в течение до тех пор, пока текущая среда оболочки продолжается.

A <<HERE-DOCUMENT ВХОДНЫЙ ФАЙЛ

Операторы перенаправления << и <<- позволяют перенаправлять строки, содержащиеся в файле ввода оболочки, называемом здесь-документом, на ввод команды.

Этот документ должен рассматриваться как одно слово, которое начинается после следующей \newline и продолжается до тех пор, пока не будет строка, содержащая только разделитель и \newline , без промежутка между [:blank:] . Затем начинается следующий документ , если он есть. Формат выглядит следующим образом:

 [n]<<word here-document delimiter 

… где необязательный n обозначает номер дескриптора файла. Если номер опущен, здесь-документ ссылается на стандартный ввод (дескриптор файла 0).

 for shell in dash zsh bash sh ; do sudo $shell -c ' { readlink /proc/self/fd/3 cat <&3 } 3<<-FILE $0 FILE ' ; done #OUTPUT pipe:[16582351] dash /tmp/zshqs0lKX (deleted) zsh /tmp/sh-thd-955082504 (deleted) bash /tmp/sh-thd-955082612 (deleted) sh 

Ты видишь? Для каждой оболочки над оболочкой создается файл и сопоставляется с файловым дескриптором. В zsh, (ba)sh оболочка создает обычный файл в /tmp , выдает вывод, сопоставляет его с дескриптором, а затем удаляет файл /tmp так что копия дескриптора ядра остается неизменной. dash избегает всего этого глупости и просто выводит свою обработку вывода в анонимный файл |pipe нацеленный на перенаправление << цели.

Это делает dash :

 cmd <<HEREDOC $(cmd) HEREDOC 

функционально эквивалентный bash 's:

 cmd <(cmd) 

в то время как реализация dash по крайней мере POSIXly переносима.

КОТОРЫЙ ДЕЛАЕТ НЕСКОЛЬКИХ ФАЙЛОВ

Поэтому в ответе ниже, когда я это делаю:

 { cat >|./file chmod +x ./file ./file } <<\FILE #!/usr/bin/sh _fn() { printf '#!' ; command -v zsh ; cat } <<SCRIPT >$0 [SCRIPT BODY] SCRIPT _fn ; exec $0 FILE 

Происходит следующее:

  1. Сначала я ./file содержимое любого файла, созданного оболочкой для FILE в ./file , сделаю его исполняемым, а затем выполним.

  2. Ядро интерпретирует #! и вызывает /usr/bin/sh с <read file descriptor, назначенным ./file .

  3. sh отображает строку в память, состоящую из составной команды, начинающейся с _fn() и заканчивающейся на SCRIPT .

  4. Когда _fn , sh сначала должен интерпретировать, а затем отображать в дескриптор файл, определенный в « <<SCRIPT...SCRIPT прежде чем _fn как специальную встроенную утилиту, потому что SCRIPT является _fn <input.

  5. Строки, выводимые командой printf и command , выписываются в стандартную _fn >&1 которая перенаправляется на ARGV0 текущей версии оболочки или $0 .

  6. cat объединяет свой файл-дескриптор <&0 стандартного вводаSCRIPT – по аргументу ARGV0 усеченной текущей оболочкой или $0 .

  7. Завершая свою текущую командную команду для текущего считывания, sh exec – исполняемый файл – и вновь переписанный – $0 .

Начиная с момента ./file вызывается до тех пор, пока в его содержащихся инструкциях не указывается, что он должен быть снова exec d, sh читает его в одной составной команде одновременно, когда он их выполняет, а ./file сам ничего не делает, кроме как с радостью принимает его новый содержание. Файлы, которые на самом деле работают: /usr/bin/sh, /usr/bin/cat, /tmp/sh-something-or-another.

СПАСИБО, ПОСЛЕ ВСЕГО

Поэтому, когда @ jw013 указывает, что:

Файл, который принимает входные данные и производит выходные данные, отлично …

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

 cat <new_file >old_file 

ОТВЕТ

Все ответы здесь хороши, но ни один из них не является полностью правильным. Все, кажется, утверждают, что вы не можете динамически и навсегда идти по пути #!bang . Вот демонстрация создания независимого по трассе shebang:

DEMO

 { cat >|./file chmod +x ./file ./file } <<\FILE #!/usr/bin/sh _rewrite_me() { printf '#!' ; command -v zsh ${out+cat} ; ${out+:} . /dev/fd/0 >&2 } <<\SCRIPT >|${out-/dev/null} printf " \$0 :\t$0 lines :\t$((c=$(wc -l <$0))) !bang :\t$(sed 1q "$0") shell :\t"$(printf `ps -o args= -p $$`)\\n\\n sed -n "1,2{=;p};$((c-1)),\${=;p}" "$0" | sed -e 'N;s/\n/ >\t/' -e 4a\\... SCRIPT _rewrite_me ; out=$0 _rewrite_me ; exec $0 FILE 

ВЫВОД

  $0 : ./file lines : 13 !bang : #!/usr/bin/sh shell : /usr/bin/sh 1 > #!/usr/bin/sh 2 > _rewrite_me() { printf '#!' ; command -v zsh ... 12 > SCRIPT 13 > _rewrite_me ; out=$0 _rewrite_me ; exec $0 $0 : /home/mikeserv/file lines : 8 !bang : #!/usr/bin/zsh shell : /usr/bin/zsh 1 > #!/usr/bin/zsh 2 > printf " ... 7 > sed -n "1,2{=;p};$((c-1)),\${=;p}" "$0" | 8 > sed -e 'N;s/\n/ >\t/' -e 4a\\... 

Ты видишь? Мы просто перезаписываем скрипт. И это происходит только один раз после git синхронизации. С этого момента у него есть правильный путь в строке #! Bang.

Теперь почти все это просто пух. Для этого вам необходимо:

  1. Функция, определенная сверху и вызываемая внизу, которая выполняет запись. Таким образом мы сохраняем все, что нам нужно в памяти, и обеспечиваем чтение всего файла, прежде чем мы начнем его писать.

  2. Определить, каким должен быть путь. command -v очень хороша для этого.

  3. Heredocs действительно помогает, потому что они являются фактическими файлами. Тем временем они будут хранить ваш скрипт. Вы можете использовать строки, но …

  4. Вы должны убедиться, что оболочка читает в команде, которая перезаписывает ваш скрипт в том же списке команд, что и тот, который его запускает.

Посмотрите:

 { cat >|./file chmod +x ./file ./file } <<\FILE #!/usr/bin/sh _rewrite_me() { printf '#!' ; command -v zsh ${out+cat} ; ${out+:} . /dev/fd/0 >&2 } <<\SCRIPT >|${out-/dev/null} printf " \$0 :\t$0 lines :\t$((c=$(wc -l <$0))) !bang :\t$(sed 1q "$0") shell :\t"$(printf `ps -o args= -p $$`)\\n\\n sed -n "1,2{=;p};$((c-1)),\${=;p}" "$0" | sed -e 'N;s/\n/ >\t/' -e 4a\\... SCRIPT _rewrite_me ; out=$0 _rewrite_me exec $0 FILE 

Обратите внимание, что я только переместил команду exec на одну строку. Теперь:

 #OUTPUT $0 : ./file lines : 14 !bang : #!/usr/bin/sh shell : /usr/bin/sh 1 > #!/usr/bin/sh 2 > _rewrite_me() { printf '#!' ; command -v zsh ... 13 > _rewrite_me ; out=$0 _rewrite_me 14 > exec $0 

Я не получаю вторую половину вывода, потому что сценарий не может читать следующую команду. Тем не менее, поскольку единственная потерявшаяся команда была последней:

 cat ./file #!/usr/bin/zsh printf " \$0 :\t$0 lines :\t$((c=$(wc -l <$0))) !bang :\t$(sed 1q "$0") shell :\t"$(printf `ps -o args= -p $$`)\\n\\n sed -n "1,2{=;p};$((c-1)),\${=;p}" "$0" | sed -e 'N;s/\n/ >\t/' -e 4a\\... 

Скрипт прошел так, как должен был – в основном потому, что все было в heredoc, но если вы не планируете это правильно, вы можете урезать свой поток, что случилось со мной выше.

Вот один из способов иметь скрипт самоопределения, который фиксирует его shebang. Этот код должен быть добавлен к вашему фактическому скрипту.

 #!/bin/sh # unpatched PATH=`PATH=/bin:/usr/bin:$PATH getconf PATH` if [ "`awk 'NR==2 {print $2;exit;}' $0`" = unpatched ]; then [ -z "`PATH=\`getconf PATH\`:/usr/local/bin:/some/long/path/to/the/right:$PATH command -v zsh`" ] && { echo "zsh not found"; exit 1; } cp -- "$0" "$0.org" || exit 1 mv -- "$0" "$0.old" || exit 1 ( echo "#!`PATH=\`getconf PATH\`:$PATH command -v zsh`" sed -n '/^##/,$p' $0.old ) > $0 || exit chmod +x $0 rm $0.old sync exit fi ## Original script starts here 

Некоторые комментарии:

  • Он должен запускаться один раз тем, кто имеет права создавать и удалять файлы в каталоге, где находится скрипт.

  • Он использует только устаревший синтаксис оболочки bourne, поскольку, несмотря на распространенное мнение, /bin/sh не гарантированно является оболочкой POSIX, даже совместимой с POSIX операционными системами.

  • Он устанавливает PATH в соответствие с POSIX, за которым следует список возможных мест zsh, чтобы избежать выбора «фальшивого» zsh.

  • Если по какой-то причине самомодифицирующийся скрипт не приветствуется, было бы тривиально распространять два сценария вместо одного, первый из которых был бы исправленным, а второй – тем, который я предложил слегка модифицировать для обработки первого.

  • Почему ZSH заканчивает линию выделенным символом процента?
  • Могу ли я выборочно отключить завершение zsh?
  • Здание zsh без администратора: нет библиотеки обработки терминала
  • Пробелы как разрывы строк из команды inline for loop
  • встроенные условия для назначения
  • zsh globbing - поиск файлов с повторяющимися строками имен файлов
  • Источник .bashrc в zsh без печати какого-либо вывода
  • Двойная и тройная замена в bash и zsh
  • Конкатенация колонн горизонтально. Печать только строк, которые пересекаются в 1-й колонке
  • Как добавить / расширить zshell доработки?
  • Gnome-terminal и Zsh: сохранить текущий каталог на новой вкладке / окне
  • Interesting Posts

    Несколько источников IP на одном интерфейсе в Linux

    Настройка частной зоны контейнера OpenVZ

    Выполните условие IF из результата, который мы получаем с удаленного сервера

    Можно ли добавить пароль для перемещения или копирования определенных файлов в Linux?

    Почему sed не использует расширенный режим регулярного выражения по умолчанию?

    Linux-загрузчики, поддерживающие полное шифрование диска?

    cat .bash_history | grep ./configure

    Определите, какая версия бинарника выполняется в настоящий момент

    Почему корневой вход через SSH настолько плох, что каждый советует отключить его?

    Не удалось запустить ubuntu после установки обновлений – Stuck at 'Try (hd0,0)'

    если существует переменная env, выйдите из сценария

    имя файла в качестве аргумента и сортировать по убыванию в perl?

    Получение и установка размера экрана для Xorg

    Курсор в соответствие, редактирование, курсор на следующее совпадение, редактирование – как в каталоге?

    автозаполнение имен файлов с использованием compgen

    Linux и Unix - лучшая ОС в мире.