
2台のコンピュータで実行できるスクリプトがあります。どちらのマシンも同じ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
/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
偶然に発生するのを防ぐことができます。$PATH
zsh
答え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
予約語個人を描く簡単なコマンド\newline
2つを分離するエスケープ文字の代わりに複合コマンド:
{ 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
次のようなことが発生します。
まず
cat
、シェルで生成されたすべてのファイルの内容をFILE
入れて./file
実行可能にしてから実行します。カーネルが解釈し
#!
て/usr/bin/sh
呼び出します。<read
ファイル記述子割り当て./file
。sh
次を含むメモリに文字列をマップします。複合コマンドで始まり、_fn()
で終わりますSCRIPT
。_fn
呼び出されると、sh
最初に解釈され、次にファイルで定義された記述子にマップする必要があります。<<SCRIPT...SCRIPT
今後_fn
特殊な組み込みユーティリティと呼ばれる理由は次のSCRIPT
とおりです。_fn
<input.
printf
出力文字列はscommand
に書き込まれます。_fn
標準出力>&1
- 現在のシェルにリダイレクトARGV0
- または$0
。cat
つながる<&0
標準入力ファイル記述子SCRIPT
- 現在のシェルの>
切り捨てられた引数または。ARGV0
$0
現在の読み取り完了複合コマンド、
sh exec
s実行可能で新しく書き換えられた$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 行のパスが正しいです。
最近ではほぼすべてがふわふわです。これを安全に行うには、次のものが必要です。
上部に定義され、下部に呼び出され、書き込みを実行する関数です。このようにして、必要なすべてをメモリに保存し、書き込みを開始する前にファイル全体を読み込んだことを確認します。
パスが何であるかを決定する方法。
command -v
これにぴったりです。Heredocsは実際のドキュメントなので本当に便利です。同時に、彼らはあなたのスクリプトを保存します。文字列を使用することもできますが...
シェルから読み取ったコマンドが、スクリプトを実行するコマンドのリストと同じコマンドのリストにあるスクリプトを上書きしていることを確認する必要があります。
望むより:
{ 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/sh
POSIX準拠のオペレーティングシステムの場合でもPOSIXシェルは保証されていないことが一般的に認められていますが、従来のシェル構文のみを使用してください。PATHをPOSIX互換パスに設定し、「偽」のzshを選択しないように可能なzshの場所のリストを設定します。
何らかの理由で自己修正スクリプトが人気がない場合は、1つの代わりに2つのスクリプトを配布するのは簡単ではありません。最初のスクリプトはパッチを適用するスクリプトで、2番目のスクリプトは古いスクリプトを処理するために少し変更することをお勧めします。