パス独立したshebang

パス独立したshebang

2台のコンピュータで実行できるスクリプトがあります。どちらのマシンも同じgitリポジトリからスクリプトのコピーを取得します。スクリプトは正しいインタプリタ(例えばzsh

残念ながら、両方 envzshローカルコンピュータとリモートコンピュータの異なる場所にあります。

リモートマシン

$ which env
/bin/env

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

ローカルマシン

$ which env
/usr/bin/env

$which zsh
/usr/local/bin/zsh

/path/to/script.sh常に利用可能なスクリプトを使用してZshスクリプトを実行するようにshebangを設定するにはどうすればよいですかPATH

答え1

shebangは純粋に静的であるため、shebangを介してこの問題を直接解決することはできません。あなたができることは、シェルの観点からいくつかの「最小共通乗数」をshebangに追加し、正しいシェルを使用してスクリプトを再実行することです(LCMがzshではない場合)。つまり、すべてのシステムにあるシェルでスクリプトを実行し、機能のみをzshテストします。テストがfalseの場合をexec使用してスクリプトを実行すると、テストはzsh成功し、続行されます。

zshたとえば、独自の特徴は$ZSH_VERSION変数があるということです。

#!/bin/sh -

[ -z "$ZSH_VERSION" ] && exec zsh - "$0" ${1+"$@"}

# zsh-specific stuff following here
echo "$ZSH_VERSION"

この簡単な例では、スクリプトは最初に実行されます/bin/sh(すべての80年代以降、UnixシリーズシステムはBourneまたはPOSIXを理解して#!保持します/bin/shが、構文は両方と互換性があります)。の$ZSH_VERSION場合いいえ設定に応じてスクリプトexec自体が渡されますzsh。設定されている場合$ZSH_VERSION(またはスクリプトがすでに実行されている場合zsh)、テストをスキップします。望むより。

zshこれはまったく存在しない場合にのみ失敗します$PATH

編集する:一般的な場所execにしかないことを確認するには、次のものを使用できます。zsh

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

これにより、予期しないことがexec偶然に発生するのを防ぐことができます。$PATHzsh

答え2

私は、スクリプトを実行する必要があるシステムのさまざまな場所で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シェルとして実行されることです。次に、2番目に自分自身を再帰的に呼び出しますが、### BEGIN解析コメントの上のすべてのエントリを使用しますsed

真珠

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

答え3

注:@jw013は次のことを行います。サポートされていません異議がある場合は、次のコメントをご覧ください。

自己修正コードは一般的に悪い習慣と見なされ、使用されなくなりました。以前の小規模なアセンブラでは、これは条件付き分岐を減らし、パフォーマンスを向上させる賢い方法でしたが、今ではセキュリティリスクが利点よりも大きくなっています。スクリプトを実行しているユーザーがスクリプトへの書き込み権限を持っていない場合、アプローチは機能しません。

私は彼のセキュリティの反対について次のように答えた。どの特別権限は次のとおりです。一度だけ必要すべてアップデートのインストールするための措置を講じるアップデートのインストールこれ自己設置スクリプト - 個人的には非常に安全だと思います。私も彼に次のことを指摘した。man sh同様の手段で同様の目標を達成することを指します。セキュリティ上の欠陥やその他の事項を指摘することには気にしませんでした。一般的に推奨されないこれは私の答えに現れるかもしれませんが、私の答えよりも質問自体に根ざしている可能性があります。

/path/to/script.shでスクリプトを実行すると、常にPATHで利用可能なZshを使用するようにshebangをどのように設定しますか?

@jw013は満足せず、ずっと反対し、さらに発展していきます。まだサポートされていませんこの主張は少なくともいくつかの誤った表明をします。

2つのファイルではなく単一のファイルを使用します。これ[man sh引用] パッケージに他のファイルを変更するファイルが1つあります。独自に変更されるファイルがあります。どちらの状況にも明らかな違いがあります。入力を受け入れて出力ファイルを生成するだけです。実行中に自分で変更される実行ファイルは、一般的に悪い考えです。あなたが言及した例はそうしません。

最初:

ただ実行可能ファイル任意のコード実行可能ファイルシェルスクリプトはこれ#!です

#!それでも正式に指定されていない)

{   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

シェルスクリプトは単なるテキストファイルです。動作するには、次のようにする必要があります。読むそのコマンドを受け取った他の実行可能ファイルを介して説明した他の実行ファイルによって、そして最後に他の実行ファイルの前に解釈の実装シェルスクリプト。これは不可能シェルスクリプトファイルの実行に使用されます。2つ未満のファイルが必要です。独自のコンパイラには例外があるかもしれませんが、zshこれにはほとんど経験がありません。

シェルスクリプト用ハッシュバン〜しなければならないその意図を示す通訳または関連性のないものとして廃棄されます。

シェルトークン認識/実行アクションは標準によって定義されます。

シェルには、入力を解析して解釈する 2 つの基本モードがあります。つまり、現在の入力がaを定義する<<here_documentか、aを定義することです{ ( command |&&|| list ) ; } &。つまり、シェルは次のいずれかを解釈します。トークン読み取り後に実行する必要があるコマンドの区切り記号またはファイルを生成し、それを他のコマンドのファイル記述子にマップするコマンドとして使用されます。それはすべてです。

シェルを実行するためにコマンドを解釈すると、トークンはセットに分割されます。予約語。})シェルが開始タグを検出した場合は、リストが終了タグ(たとえば、改行文字(該当する場合))または終了タグ(実行前の終了タグなど)で区切られるまで、コマンドリストを読み続ける必要があります({

シェルの区別簡単なコマンドそして複合コマンドこれ複合コマンド実行する前に読み取る必要がある一連のコマンドですが、シェルは$expansionそのコンポーネントを実行しません。簡単なコマンドそれぞれを個別に実行するまで。

したがって、以下の例では;semicolon 予約語個人を描く簡単なコマンド\newline2つを分離するエスケープ文字の代わりに複合コマンド:

{   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

これはガイドを簡素化したものです。考慮すると、状況がより複雑になります。シェル組み込みコマンド、サブシェル、現在の環境ちょっと待って、しかしここで私の目的にはそれで十分です。

話す組み込みそしてコマンドリスト、afunction() { declaration ; }は単にaを割り当てる方法です。複合コマンド簡単なコマンド。シェルは$expansions宣言文自体(含む<<redirections>)で何もしないでくださいが、定義を単一のリテラル文字列として保存し、呼び出し時に特別なシェル組み込みとして実行する必要があります。

したがって、実行可能シェルスクリプトで宣言されたシェル関数は、解釈するシェルのメモリにリテラル文字列形式で格納されます。これは追加の文書を入力として含めるように拡張されず、組み込み関数として呼び出されるたびに独立しています。シェルソースファイルの実行中 - シェルの現在の環境が持続する限り。

<<HERE-DOCUMENTインラインファイルです

リダイレクト演算子<<<<-その両方は、次のようなシェル入力ファイルに含まれる行リダイレクトを受け入れます。ここのドキュメント、コマンドに入力します。

これここのドキュメント\newline次の単語から始めて、次の単語のみを含む行が出るまで、続く単一の単語として扱う必要があります。区切り記号そして\newline間にsがないa。[: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

cmd <(cmd)

while実装は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. まずcat、シェルで生成されたすべてのファイルの内容をFILE入れて./file実行可能にしてから実行します。

  2. カーネルが解釈し#!/usr/bin/sh呼び出します。<read ファイル記述子割り当て./file

  3. sh次を含むメモリに文字列をマップします。複合コマンドで始まり、_fn()で終わりますSCRIPT

  4. _fn呼び出されると、sh最初に解釈され、次にファイルで定義された記述子にマップする必要があります。<<SCRIPT...SCRIPT 今後_fn特殊な組み込みユーティリティと呼ばれる理由は次のSCRIPTとおりです。_fn<input.

  5. printf出力文字列はscommandに書き込まれます。_fn標準出力 >&1- 現在のシェルにリダイレクトARGV0- または$0

  6. catつながる<&0 標準入力ファイル記述子SCRIPT- 現在のシェルの>切り捨てられた引数または。ARGV0$0

  7. 現在の読み取り完了複合コマンドsh execs実行可能で新しく書き換えられた$0パラメータ。

呼び出された時点から./file埋め込まれた命令がexec再び呼び出されるべきであることを指定する時点まで、sh単一の方法で読み取られます。複合コマンド実行中に./file独自に何もしないでください新しい内容を喜んで受け入れることを除いて。実際の作業ファイルは次のとおりです。/usr/bin/sh, /usr/bin/cat, /tmp/sh-something-or-another.

結局ありがとうございます

したがって、@jw013が次のように指定した場合:

入力を受け取り、出力を生成するファイルは問題ありません。

...この答えに対する誤った批判で、彼は実際にここで使用された唯一の方法を誤って黙認しました。これは基本的に次のようになります。

cat <new_file >old_file

回答

ここにあるすべての答えは素晴らしいですが、それらのどれも完全に正確ではありません。誰もがパスを動的に永続的に指定できないと主張しているようです#!bang。以下は、パスに依存しないshebangを設定するデモです。

デモ版

{   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コマンドを1行下に移動しました。今:

#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にあるからです。ただし、正しく計画しないと、ファイルストリームが切り捨てられる可能性があります。上記で私が経験したことはまさにこのことです。

答え4

自己修正スクリプトを使用して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

いくつかのコメント:

  • スクリプトを含むディレクトリにファイルを作成および削除する権限を持つ人が一度実行する必要があります。

  • /bin/shPOSIX準拠のオペレーティングシステムの場合でもPOSIXシェルは保証されていないことが一般的に認められていますが、従来のシェル構文のみを使用してください。

  • PATHをPOSIX互換パスに設定し、「偽」のzshを選択しないように可能なzshの場所のリストを設定します。

  • 何らかの理由で自己修正スクリプトが人気がない場合は、1つの代わりに2つのスクリプトを配布するのは簡単ではありません。最初のスクリプトはパッチを適用するスクリプトで、2番目のスクリプトは古いスクリプトを処理するために少し変更することをお勧めします。

関連情報