경로 독립 세방 zsh로컬 및

두 대의 컴퓨터에서 실행할 수있는 스크립트가 있습니다. 이 두 머신은 동일한 자식 저장소에서 스크립트 사본을 가져옵니다. 스크립트는 올바른 인터프리터 (예 :)로 실행해야합니다 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있습니까?



답변

shebang은 순전히 정적이므로 shebang을 통해 직접 해결할 수 없습니다. 이 LCM이 zsh가 아닌 경우 shebang에 “최소한 공통 멀티 플라이어”(쉘 관점에서)를 가지고 올바른 쉘로 스크립트를 다시 실행하는 것이 가능합니다. 다시 말해서 : 모든 시스템에서 발견 된 쉘에 의해 스크립트가 실행되도록하고, zsh전용 기능을 테스트하고 테스트가 거짓으로 판명 되면 테스트가 성공하고 계속 진행할 스크립트 execzsh로 설정하십시오.

zsh예를 들어의 고유 한 기능 중 하나 는 $ZSH_VERSION변수 의 존재입니다 .

#!/bin/sh -

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

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

이 간단한 경우, 스크립트는 먼저 실행됩니다 /bin/sh(80 년대 유닉스 계열 시스템 은 Bourne 또는 POSIX를 이해 #!하고 /bin/sh있지만 구문은 둘 다 호환됩니다). 경우 $ZSH_VERSION입니다 하지 스크립트 exec자체를 통해 수행 zsh됩니다. $ZSH_VERSION이 설정된 경우 (대체로 스크립트가 이미 실행 됨 zsh) 테스트는 건너 뜁니다. Voilà.

이 경우에만 실패 zsh 전혀없는$PATH 합니다.

편집 : 확인하려면, 당신 만execzsh 평소 A 를 사용하려면 다음과 같은 것을 사용할 수 있습니다

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

이것은 당신이 의도 하지 않은 exec무언가 를 실수로 ‘들어가는’것을 막을 수 있습니다 .$PATHzsh


답변

몇 년 동안 필자는 스크립트를 실행해야하는 시스템에서 다양한 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의 기능을 사용하여 해당 파일을 구문 분석하여 line 앞에있는 모든 행을 건너 뜁니다 #! perl.


답변

참고 : @ jw013은 아래 주석에서 다음과 같은 지원되지 않는 이의 제기를합니다.

공감대는 자체 수정 코드가 일반적으로 나쁜 습관으로 간주되기 때문입니다. 예전에는 소규모 조립 프로그램으로 돌아가서 조건부 분기를 줄이고 성능을 향상시키는 영리한 방법 이었지만 오늘날 보안 위험이 장점보다 중요합니다. 스크립트를 실행 한 사용자에게 스크립트에 대한 쓰기 권한이 없으면 접근 방식이 작동하지 않습니다.

내가 지적으로 자신의 보안 반대 대답 어떤 특별한 권한이 있습니다 만되면 필요 에 따라 설치 / 업데이트를 위해 행동 설치 / 업데이트 자동 설치 개인적으로 확보 꽤 부를 것이다 – 스크립트. 나는 또한 man sh비슷한 방법으로 비슷한 목표를 달성 할 수 있다고 언급했다. 당시에는 답변에 표시되거나 표시되지 않을 수있는 보안 결함이나 일반적으로 조언 되지 않은 방법이 무엇이든 질문에 대한 답변보다 질문에 기반을 둔 것으로 나타났습니다.

스크립트를 /path/to/script.sh로 실행하면 항상 PATH에서 사용 가능한 Zsh를 사용하도록 shebang을 어떻게 설정할 수 있습니까?

만족스럽지 못한 @ jw013은 적어도 몇 가지 잘못된 진술 로 아직지지받지 못한 논증을 진전시켜 이의 제기를 계속했습니다 .

두 개의 파일이 아닌 단일 파일을 사용합니다. [ man sh참조]
패키지는 하나 개의 파일이 다른 파일을 수정 있습니다. 파일 자체가 수정되었습니다. 이 두 경우에는 뚜렷한 차이가 있습니다. 입력을 받고 출력을 생성하는 파일은 괜찮습니다. 실행될 때 자체적으로 변경되는 실행 파일은 일반적으로 나쁜 생각입니다. 당신이 지적한 예는 그렇게하지 않습니다.

우선 :

유일한 EXECUTABLE ANY IN CODE EXECUTABLE SHELL SCRIPT 인#! 자체

(하지만 심지어 #!입니다 공식적으로 지정되지 않은 )

{   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자체 컴파일러에는 예외가있을 수 있지만이 경우 경험이 거의 없으며 여기에 표시되지 않습니다.

쉘 스크립트의 해시 뱅 의도 한 인터프리터 를 가리켜 야합니다 거나 관련이없는 것으로 폐기 .

쉘의 토큰 인식 / 실행 동작은 표준 정의

쉘은 입력을 해석하고 해석하는 두 가지 기본 모드를 가지고 있습니다 : 현재 입력이 a를 정의 <<here_document하거나 정의 { ( 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 ; }는 단순한 명령복합 명령 을 지정하는 수단 일뿐 입니다. 쉘은 $expansions선언문 자체를 포함 <<redirections>하지 말고 정의를 단일 리터럴 문자열로 저장하고 호출 할 때 내장 된 특수 쉘로 실행해야합니다.

따라서 실행 가능한 셸 스크립트로 선언 된 셸 함수는 해석되는 셸 메모리에 리터럴 문자열 형식으로 저장됩니다. 여기에 추가 된 문서를 입력으로 포함하도록 확장되지 않으며 셸 빌드로 호출 될 때마다 소스 파일과 독립적으로 실행됩니다. 쉘의 현재 환경이 지속되는 한.

A는 <<HERE-DOCUMENT인라인 파일

경로 재 지정 연산자 <<<<-둘 다 여기에서 문서 로 알려진 쉘 입력 파일에 포함 된 행을 명령의 입력으로 경로 재 지정할 수 있습니다.

문서 는 다음 단어 이후에 시작 하여 사이에 s 가없는 구분 기호 와 a \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:

cmd <(cmd)

동안 dash의 구현은 적어도 POSIXly 휴대용이다.

이는도하지 SEVERAL 파일을

그래서 내가 할 때 아래 답변에서 :

{    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/shA를 <read 파일 기술자 에 할당을 ./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을 설정하는 데모는 다음과 같습니다.

데모

{   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

일부 의견 :

  • 스크립트가있는 디렉토리에서 파일을 작성하고 삭제할 권한이있는 사람이 한 번 실행해야합니다.

  • 널리 알려진 신념에도 불구하고 /bin/shPOSIX 셸, 심지어 POSIX 호환 OS로 보장되지 않으므로 레거시 Bourne 쉘 구문 만 사용합니다 .

  • “포니”zsh를 선택하지 않도록 PATH를 POSIX 호환 경로로 설정하고 가능한 zsh 위치 목록을 설정합니다.

  • 어떤 이유로 자체 수정 스크립트를 사용하는 것이 바람직하지 않은 경우 하나는 스크립트 대신 두 개의 스크립트를 배포하는 것이 쉽지 않습니다.