경로 독립적인 shebang

경로 독립적인 shebang

두 대의 컴퓨터에서 실행할 수 있는 스크립트가 있습니다. 두 머신 모두 동일한 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 쉘로 실행된다는 것입니다. 그런 다음 두 번째로 자신을 재귀적으로 호출하지만 ### 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은 만족하지 않고 계속 반대하며 더욱 발전해 나갑니다.아직 지원되지 않음이 주장은 최소한 몇 가지 잘못된 진술을 합니다.

두 개의 파일이 아닌 단일 파일을 사용합니다. 이것[ man sh인용하다] 패키지에 다른 파일을 수정하는 파일이 하나 있습니다. 자체적으로 수정되는 파일이 있습니다. 두 상황에는 분명한 차이가 있습니다. 입력을 수락하고 출력 파일을 생성하기만 하면 됩니다. 실행 중에 스스로 변경되는 실행 파일은 일반적으로 좋지 않은 생각입니다. 당신이 언급한 예는 그렇게 하지 않습니다.

첫 번째:

오직실행 가능 파일임의의 코드실행 가능 파일쉘 스크립트는 이것 #!이다

( #!그래도공식적으로 지정되지 않음)

{   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이에 대한 경험이 거의 없으며 여기에는 어떤 식으로든 표시되지 않습니다.

쉘 스크립트용 해시뱅~ 해야 하다그 의도를 나타낸다통역사또는 관련성이 없는 것으로 폐기됩니다.

껍데기토큰 인식/실행행동은 표준에 의해 정의됩니다.

쉘에는 입력을 구문 분석하고 해석하는 두 가지 기본 모드가 있습니다. 즉, 현재 입력이 a를 정의 <<here_document하거나 a를 정의하는 것입니다 { ( 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

이것은 가이드를 단순화한 것입니다. 고려하면 상황이 더 복잡해집니다.쉘 내장 명령, 서브쉘, 현재 환경잠깐만요, 하지만 여기서 제 목적에는 그것으로 충분합니다.

말하기내장그리고명령 목록,a function() { 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출력 문자열은 s command에 기록됩니다 ._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명령을 한 줄 아래로만 옮겼습니다 . 지금:

#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 위치 목록을 설정합니다.

  • 어떤 이유로 자체 수정 스크립트가 인기가 없는 경우 하나 대신 두 개의 스크립트를 배포하는 것이 쉽지 않습니다. 첫 번째 스크립트는 패치하려는 스크립트이고 두 번째 스크립트는 이전 스크립트를 처리하기 위해 약간 수정하는 것이 좋습니다.

관련 정보