เส้นทาง shebangs อิสระ


20

ฉันมีสคริปต์ที่ฉันต้องการให้สามารถทำงานในสองเครื่อง เครื่องทั้งสองนี้รับสำเนาของสคริปต์จากที่เก็บ 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?


8
คุณแน่ใจหรือenvไม่ว่าทั้ง / bin และ / usr / bin ลองwhich -a envยืนยัน
grawity

คำตอบ:


22

คุณไม่สามารถแก้ปัญหานี้ผ่าน shebang ได้โดยตรงเนื่องจาก shebang นั้นมีความคงที่อย่างแท้จริง สิ่งที่คุณสามารถทำได้คือมีตัวคูณร่วมน้อยที่สุด« (จากมุมมองของเชลล์) ใน shebang และเรียกใช้สคริปต์ของคุณอีกครั้งด้วยเชลล์ที่ถูกต้องหาก LCM นี้ไม่ใช่ zsh กล่าวอีกนัยหนึ่ง: ให้สคริปต์ของคุณดำเนินการโดย shell ที่พบในทุกระบบทดสอบสำหรับzshคุณลักษณะเฉพาะและหากการทดสอบปรากฎเป็นเท็จให้สคริปต์execด้วย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ทั้งบอร์นหรือ POSIX แต่ไวยากรณ์ของเราเข้ากันได้กับทั้งสอง) ถ้า$ZSH_VERSIONจะไม่ได้ตั้งสคริปต์exec's zshตัวเองผ่าน หาก$ZSH_VERSIONมีการตั้งค่าไว้ (สคริปต์จะทำงานเสร็จแล้วzsh) การทดสอบจะถูกข้ามไป voila

สิ่งนี้จะล้มเหลวหากzshไม่ได้อยู่$PATHที่ทั้งหมด

แก้ไข:เพื่อให้แน่ใจว่าคุณจะในสถานที่ปกติคุณสามารถใช้สิ่งที่ต้องการexeczsh

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

การทำเช่นนี้ช่วยให้คุณไม่ต้องเผลอทำexecอะไรบางอย่างในสิ่ง$PATHที่zshคุณไม่คาดหวัง


ฉันยกระดับมันขึ้นมาเพื่อความสง่างาม แต่โดยหลักการแล้วมันมีปัญหาด้านความปลอดภัย / ความเข้ากันได้ถ้าหากสิ่งแรกzshใน$PATHนั้นไม่ใช่สิ่งที่คุณคาดหวัง
Ryan Reich

พยายามที่จะอยู่มัน คำถามคือคุณจะแน่ใจได้หรือไม่ว่าzshไบนารีในสถานที่มาตรฐานเป็นจริงzshหรือไม่
Andreas Wiese

คุณสามารถกำหนดเส้นทาง!! bang นอกจากนี้คุณยังสามารถถามตัวเองว่ามันอยู่ที่ไหนด้วยzsh zsh -c 'whence zsh'ยิ่งคุณทำได้แค่command -v zshนี้ #!bangดูคำตอบของฉันสำหรับวิธีการแบบไดนามิกเส้นทาง
mikeserv

1
การเรียกzshเลขฐานสองจาก$PATHการรับเส้นทางของzshไบนารีจะไม่แก้ไขปัญหา @RyanReich ชี้ให้เห็นใช่ไหม? :)
Andreas Wiese

ไม่ใช่ถ้าคุณทำตัวzshเองไม่ฉันเดาไม่ แต่ถ้าคุณฝังสตริงผลลัพธ์ลงในแฮชปังของคุณจากนั้นเรียกใช้สคริปต์ของคุณเองอย่างน้อยก็รู้ว่าคุณกำลังได้อะไร อย่างไรก็ตามมันจะทำให้การทดสอบง่ายกว่าการวนซ้ำ
mikeserv

7

เป็นเวลาหลายปีที่ฉันใช้สิ่งที่คล้ายกันเพื่อจัดการกับตำแหน่งต่างๆของ 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...

ด้านบนสามารถปรับได้อย่างง่ายดายสำหรับล่ามที่หลากหลาย ส่วนสำคัญคือสคริปต์นี้เริ่มทำงานในฐานะเชลล์เป้าหมาย จากนั้นจะเรียกตัวเองซ้ำเป็นครั้งที่สอง แต่แยกวิเคราะห์ทุกอย่างที่กล่าวมาข้างต้นแสดงความคิดเห็นโดยใช้### BEGINsed

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เมื่อได้รับไฟล์ไปยังการทำงานจะวิเคราะห์แล้วกล่าวว่าไฟล์ข้ามเส้นทั้งหมดที่มีก่อนที่จะสาย


จำนวนของปัญหาที่นั่น: คำพูดที่ขาดหายไป, การใช้งาน$*แทน"$@", การใช้ eval ที่ไร้ประโยชน์, ไม่ได้รายงานสถานะการออก (คุณไม่ได้ใช้execสำหรับสิ่งแรก), ที่ขาดหายไป-/ --, ข้อความแสดงข้อผิดพลาดไม่ได้อยู่ที่ stderr, 0 สถานะการออก ใช้ / bin / sh สำหรับ LIN_BASH เซมิโคลอนไร้ประโยชน์ (เครื่องสำอาง) ใช้ตัวพิมพ์ใหญ่ทั้งหมดสำหรับตัวแปรที่ไม่ใช่ env uname -sเป็นเหมือนuname(uname สำหรับชื่อ Unix) คุณลืมที่จะกล่าวถึงว่าข้ามจะถูกเรียกโดยตัวเลือกที่จะ-x perl
Stéphane Chazelas

4

หมายเหตุ: @ jw013 ทำให้การคัดค้านที่ไม่ได้รับการสนับสนุนดังต่อไปนี้ในความคิดเห็นด้านล่าง:

Downvote นั้นเป็นเพราะรหัสการแก้ไขตัวเองนั้นโดยทั่วไปถือว่าเป็นการปฏิบัติที่ไม่ดี ย้อนกลับไปในสมัยก่อนของโปรแกรมการประกอบเล็ก ๆ มันเป็นวิธีที่ชาญฉลาดในการลดสาขาที่มีเงื่อนไขและปรับปรุงประสิทธิภาพ แต่ปัจจุบันความเสี่ยงด้านความปลอดภัยมีมากกว่าข้อดี วิธีการของคุณจะไม่ทำงานหากผู้ใช้ที่เรียกใช้สคริปต์ไม่มีสิทธิ์ในการเขียนสคริปต์

ฉันตอบคัดค้านการรักษาความปลอดภัยของเขาโดยชี้ให้เห็นว่าการใด ๆสิทธิ์พิเศษที่จำเป็นต้องใช้เพียงครั้งเดียวต่อการติดตั้ง / การปรับปรุงการดำเนินการเพื่อที่จะติดตั้ง / อัปเดตตนเองติดตั้งสคริปต์ - ซึ่งผมเองจะเรียกที่เชื่อถือได้สวย ฉันยังชี้ให้เขาman shอ้างอิงถึงการบรรลุเป้าหมายที่คล้ายกันด้วยวิธีการที่คล้ายกัน ในเวลานั้นฉันไม่ได้สนใจที่จะชี้ให้เห็นว่าข้อบกพร่องด้านความปลอดภัยหรือการปฏิบัติที่ไม่สมควรซึ่งอาจจะใช่หรือไม่ใช่ในคำตอบของฉันพวกเขามีแนวโน้มที่จะฝังรากอยู่ในคำถามมากกว่าคำตอบของฉัน:

ฉันจะตั้งค่า shebang เพื่อให้เรียกใช้สคริปต์เป็น /path/to/script.sh จะใช้ Zsh ใน PATH ได้อย่างไร

ไม่พอใจ @ jw013 ยังคงคัดค้านโดยการโต้แย้งว่าเขายังไม่ได้รับการสนับสนุนโดยมีอย่างน้อยสองประโยคที่ผิดพลาด:

คุณใช้ไฟล์เดียวไม่ใช่สองไฟล์ [ man shอ้างอิง] แพคเกจได้หนึ่งไฟล์แก้ไขไฟล์อื่น คุณมีไฟล์ที่แก้ไขเอง มีความแตกต่างที่ชัดเจนระหว่างสองกรณีนี้ ไฟล์ที่รับอินพุตและสร้างเอาต์พุตนั้นใช้ได้ ไฟล์เรียกทำงานที่เปลี่ยนแปลงตัวเองในขณะที่ทำงานนั้นโดยทั่วไปจะเป็นความคิดที่ไม่ดี ตัวอย่างที่คุณชี้ไปไม่ได้ทำเช่นนั้น

ในที่แรก:

รหัสEXECUTABLEเดียวในสคริปต์EXECUTABLE SHELL ใด ๆ ก็คือ#!ITSELF

(แม้ว่า#!จะไม่ได้ระบุอย่างเป็นทางการ )

{   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คอมไพเลอร์ของตัวเอง แต่ด้วยสิ่งนี้ฉันมีประสบการณ์น้อยและมันไม่มีทางเป็นตัวแทนที่นี่

hashbang ของเชลล์สคริปต์จะต้องชี้ไปที่ล่ามที่ตั้งใจไว้หรือถูกลบทิ้งไปโดยไม่เกี่ยวข้อง

พฤติกรรมการรับรู้ / การปฏิบัติของเปลือกหอยถูกกำหนดตามมาตรฐาน

เชลล์มีสองโหมดพื้นฐานของการแยกวิเคราะห์และตีความอินพุต: ทั้งอินพุตปัจจุบันกำลังนิยาม a <<here_documentหรือนิยาม{ ( command |&&|| list ) ; } &- ในคำอื่น ๆ เชลล์จะตีความโทเค็นเป็นตัวคั่นสำหรับคำสั่งที่ควรดำเนินการเมื่ออ่านแล้ว ในหรือเป็นคำแนะนำในการสร้างไฟล์และแมปไปยังไฟล์ descriptor สำหรับคำสั่งอื่น แค่นั้นแหละ.

เมื่อตีความคำสั่งเพื่อดำเนินการเชลล์จะ จำกัด โทเค็นในชุดของคำที่สงวนไว้ เมื่อเปลือกพบการเปิดโทเค็นก็จะต้องดำเนินการต่อไปอ่านในรายการคำสั่งจนกว่ารายการจะถูกคั่นด้วยทั้งโดยการปิดโทเค็นเช่นการขึ้นบรรทัดใหม่ได้ - เมื่อบังคับ - หรือปิด token เช่น})สำหรับ({ก่อนที่จะดำเนินการ

เชลล์แยกความแตกต่างระหว่างคำสั่งง่าย ๆและคำสั่งผสม คำสั่งผสมคือชุดของคำสั่งที่ต้องอ่านก่อนที่จะดำเนินการ แต่เปลือกไม่ได้ดำเนินการ$expansionใด ๆ ของส่วนประกอบของคำสั่งง่ายๆจนกว่าจะดำเนินการโดยลำพังแต่ละคน

ดังนั้นในตัวอย่างต่อไปนี้;semicolon คำสงวนสงวนคั่นคำสั่งง่าย ๆแต่ละคำในขณะที่อักขระที่ไม่ใช้ Escape จะ\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

นั่นคือความเรียบง่ายของแนวทาง มันซับซ้อนมากขึ้นเมื่อคุณพิจารณาshell-buildins, subshells, สภาพแวดล้อมปัจจุบันและอื่น ๆ แต่สำหรับจุดประสงค์ของฉันที่นี่ก็เพียงพอแล้ว

และการพูดถึงบิวด์อินและรายการคำสั่ง a function() { declaration ; }เป็นเพียงวิธีการกำหนดคำสั่งผสมให้กับคำสั่งง่าย ๆ เชลล์จะต้องไม่ดำเนินการใด ๆ$expansionsกับคำสั่งการประกาศตัวเอง - เพื่อรวม<<redirections>- แต่จะต้องจัดเก็บคำจำกัดความเป็นสตริงตัวอักษรเดียวและดำเนินการมันเป็นเปลือกพิเศษในตัวเมื่อเรียกร้อง

ดังนั้นฟังก์ชั่นของเชลล์ที่ประกาศในเชลล์สคริปต์ที่รันได้จะถูกเก็บไว้ในหน่วยความจำของเชลล์ในรูปแบบสตริงตัวอักษรโดยไม่ขยายเพื่อรวมเอกสารต่อท้ายที่นี่เป็นอินพุตและดำเนินการอย่างอิสระจากไฟล์ต้นฉบับทุกครั้งที่มันเรียกว่า ในตราบเท่าที่สภาพแวดล้อมปัจจุบันของเชลล์คงอยู่

<<HERE-DOCUMENTเป็น INLINE FILE

ตัวดำเนินการเปลี่ยนเส้นทาง<<และ<<-ทั้งคู่อนุญาตการเปลี่ยนเส้นทางของบรรทัดที่มีอยู่ในไฟล์อินพุตเชลล์หรือที่เรียกว่าhere-documentไปยังอินพุตของคำสั่ง

นี่เอกสารให้ถือเป็นคำเดียวว่าจะเริ่มต้นหลังจากต่อไป\newlineและต่อไปจนกว่าจะมีการโฆษณาที่มีเพียงตัวคั่นและ\newlineไม่มี[:blank:]ในระหว่าง จากนั้นเอกสารต่อไปนี้จะเริ่มขึ้นถ้ามี รูปแบบดังต่อไปนี้:

[n]<<word
    here-document 
delimiter

... โดยที่ทางเลือกnแทนหมายเลขไฟล์ descriptor หากไม่ระบุหมายเลขเอกสารที่นี่จะอ้างถึงอินพุตมาตรฐาน (ตัวอธิบายไฟล์ 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ดัมพ์เอาต์พุตแม็พกับ descriptor จากนั้นลบ/tmpไฟล์เพื่อให้สำเนาของเคอร์เนล descriptor เป็นสิ่งที่เหลืออยู่ dashหลีกเลี่ยงเรื่องไร้สาระทั้งหมดและเพียงแค่หยดการประมวลผลของมันลงใน|pipeไฟล์ที่ไม่ระบุชื่อมุ่งเป้าไปที่<<เป้าหมายเปลี่ยนเส้นทาง

สิ่งนี้ทำให้dash:

cmd <<HEREDOC
    $(cmd)
HEREDOC

หน้าที่เทียบเท่ากับbash:

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. ครั้งแรกที่ผมcatเนื้อหาของสิ่งที่ยื่นเปลือกที่สร้างขึ้นสำหรับFILEเข้าไป./fileให้มันปฏิบัติการแล้วรันมัน

  2. เคอร์เนลตีความ#!และบริการโทร/usr/bin/shกับ<read อธิบายไฟล์./fileมอบหมายให้

  3. shแผนที่สตริงในหน่วยความจำประกอบด้วยคำสั่งผสมเริ่มต้นที่และสิ้นสุดที่_fn()SCRIPT

  4. เมื่อ_fnถูกเรียกshต้องตีความแล้ว map เพื่อบ่งไฟล์ที่กำหนดไว้ใน<<SCRIPT...SCRIPT ก่อนที่จะกล่าวอ้าง_fnเป็นพิเศษในตัวยูทิลิตี้เพราะSCRIPTเป็น_fn's<input.

  5. เอาท์พุทสตริงโดยprintfและcommandจะเขียนออกไป_fn's มาตรฐานออก >&1 - ซึ่งถูกเปลี่ยนเส้นทางไปเปลือกปัจจุบันของARGV0- $0หรือ

  6. catเชื่อมของ<&0 มาตรฐานการป้อนข้อมูลไฟล์บ่ง - SCRIPT- เหนือ>ตัดทอนเปลือกปัจจุบันของการโต้แย้งหรือARGV0$0

  7. เสร็จสิ้นการอ่านแล้วในปัจจุบันคำสั่งผสม , sh execs ปฏิบัติการ - - และเขียนใหม่ที่เพิ่ง$0โต้แย้ง

จากเวลาที่./fileถูกเรียกจนกระทั่งคำแนะนำที่มีอยู่ระบุว่ามันควรจะเป็นexecd อีกครั้ง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 line

ตอนนี้เกือบทั้งหมดมีเพียงปุย หากต้องการทำสิ่งนี้อย่างปลอดภัยคุณต้อง:

  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 - แต่ถ้าคุณไม่ได้วางแผนไว้อย่างถูกต้องคุณสามารถตัดทอนภาพยนตร์ของคุณซึ่งเป็นสิ่งที่เกิดขึ้นกับฉันข้างต้น


Downvote นั้นเป็นเพราะรหัสการแก้ไขตัวเองโดยทั่วไปแล้วถือว่าเป็นแนวปฏิบัติที่ไม่ดี ย้อนกลับไปในสมัยก่อนของโปรแกรมการประกอบเล็ก ๆ มันเป็นวิธีที่ชาญฉลาดในการลดสาขาที่มีเงื่อนไขและปรับปรุงประสิทธิภาพ แต่ปัจจุบันความเสี่ยงด้านความปลอดภัยมีมากกว่าข้อดี วิธีการของคุณจะไม่ทำงานหากผู้ใช้ที่เรียกใช้สคริปต์ไม่มีสิทธิ์ในการเขียนสคริปต์
jw013

@ jw013 เห็นได้ชัดว่าแนวทางของฉันในการติดตั้งหรืออัปเดตสคริปต์ที่ใช้งานได้จะไม่ทำงานหากบุคคลที่พยายามติดตั้งหรืออัปเดตสคริปต์ไม่ได้รับอนุญาตให้ติดตั้งหรืออัปเดตสคริปต์ ในความเป็นจริงนั้นเป็นสิ่งที่ทำให้คำตอบนี้ดีกว่าคำตอบอื่น ๆ ที่นี่ - มันสามารถให้ #! bang line ที่ถูกต้องตามความจำเป็นและต้องการเพียงสิทธิ์พิเศษใด ๆ ที่จะทำเมื่อมีการร้องขอครั้งแรก - ระหว่างการติดตั้ง และอีกครั้งฉันจะไม่เพียงแค่ใช้คำพูดของคุณเพราะมันเป็นรหัสการปรับเปลี่ยนด้วยตนเองคือการปฏิบัติที่ไม่ดี - โปรดดูman commandความคิดเห็นที่ขัดแย้ง
mikeserv

โปรดดูman commandความคิดเห็นที่ขัดแย้ง - ไม่พบ คุณสามารถพาฉันไปที่ส่วน / ย่อหน้าที่คุณกำลังพูดถึงได้หรือไม่
jw013

@ jw013 - ความผิดพลาดของฉันอยู่ในman sh- ค้นหา'command -v' ฉันรู้ว่ามันเป็นหนึ่งในmanหน้าที่ฉันดูในวันอื่น ๆ
mikeserv

ผมถือว่านี่เป็นตัวอย่างที่คุณพูดถึงจากcommand -v man shนั่นเป็นสคริปต์ตัวติดตั้งที่ดูเป็นปกติไม่ใช่สคริปต์ที่ปรับเปลี่ยนได้เอง แม้แต่ตัวติดตั้งที่มีในตัวเองก็มีเพียงอินพุตอินพุตที่ปรับเปลี่ยนล่วงหน้าเท่านั้นและส่งเอาต์พุตการแก้ไขที่อื่น พวกเขาไม่เขียนตัวเองใหม่ในแบบที่คุณแนะนำ
jw013

1

นี่คือวิธีหนึ่งในการมีสคริปต์แก้ไขด้วยตนเองที่แก้ไข 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 shell แบบดั้งเดิมเท่านั้นแม้ว่าจะเป็นความเชื่อที่ได้รับความนิยม แต่/bin/shก็ไม่รับประกันว่าจะเป็น POSIX เชลล์แม้กระทั่งระบบปฏิบัติการที่สอดคล้องกับ POSIX

  • มันตั้งค่า PATH เป็น POSIX ที่สอดคล้องกับที่ตามด้วยรายการตำแหน่ง zsh ที่เป็นไปได้เพื่อหลีกเลี่ยงการเลือก "phony" zsh

  • หากด้วยเหตุผลบางอย่างสคริปต์การแก้ไขตัวเองจะไม่ได้รับการยืนยันมันน่าสนใจที่จะแจกจ่ายสคริปต์สองบทแทนที่จะเป็นหนึ่งสคริปต์บทแรกเป็นสคริปต์ที่คุณต้องการแก้ไขและสคริปต์ที่สองที่ฉันแนะนำให้แก้ไขเล็กน้อยเพื่อประมวลผลอดีต


/bin/shจุดหนึ่งที่ดี - แต่ในกรณีที่คุณต้องใช้ premodified #!ที่ทั้งหมดหรือไม่ และไม่awkน่าจะเป็นของปลอมเหมือนอย่างzshนั้นหรือ
mikeserv

@mikeserv คำตอบได้รับการอัปเดตเพื่อเรียก POSIX awk Shebang ที่ปรับรูปแบบล่วงหน้านั้นอยู่ที่นั่นเพื่อป้องกันไม่ให้สคริปต์ตีความโดยเชลล์ที่เข้ากันไม่ได้ซึ่งควรเป็นเชลล์ล็อกอินของคุณ
jlliagre

มีเหตุผล. ฉัน upvoting เพราะใช้งานได้มันยึดติดกับหนังสือและแสดงให้เห็นถึงความเข้าใจที่ถูกต้องเกี่ยวกับสภาพแวดล้อมของเชลล์ / การจัดการไฟล์ - โดยเฉพาะไฟล์สำรองที่คุณใช้ซึ่ง GNU ทั้งหมดsed -iทำอยู่แล้ว ฉันเองคิดว่า$PATHปัญหาที่ระบุไว้ในความคิดเห็นในคำตอบอื่นและที่คุณอยู่อย่างปลอดภัยเท่าที่ฉันสามารถคิดที่นี่ในไม่กี่บรรทัดได้รับการจัดการที่ดีขึ้นโดยง่ายและชัดเจนการกำหนดอ้างอิงและ / หรือการทดสอบที่เข้มงวดและชัดเจน - เช่นตอนนี้getconfอาจของปลอมแต่โอกาสใกล้ศูนย์เช่นเดียวกับพวกเขาzshและawk.
mikeserv

@mikeserv สคริปต์ที่แก้ไขเพื่อลดความเสี่ยงในการเรียกใช้ getconf ปลอม
jlliagre

$(getconf PATH)ไม่ใช่ Bourne cp $0 $0.oldเป็นไวยากรณ์ zsh การเทียบเท่าบอร์นจะเป็นอย่างที่cp "$0" "$0.old"คุณต้องการcp -- "$0" "$0.old"
Stéphane Chazelas
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.