Unix shell script ค้นหาว่าไฟล์สคริปต์อยู่ที่ไหน?


511

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


12
มันซ้ำกันจริงเหรอ? คำถามนี้เกี่ยวกับ "unix shell script", อื่น ๆ โดยเฉพาะเกี่ยวกับ Bash
michaeljt

2
@BoltClock: คำถามนี้ถูกปิดอย่างไม่เหมาะสม คำถามที่เชื่อมโยงเป็นเรื่องเกี่ยวกับ Bash คำถามนี้เกี่ยวกับการเขียนโปรแกรม Unix shell ขอให้สังเกตว่าคำตอบที่ได้รับการยอมรับแตกต่างกันมาก!
Dietrich Epp

@Dietrich Epp: ถูกต้อง ดูเหมือนว่าทางเลือกของผู้ถามสำหรับคำตอบที่ได้รับการยอมรับและการเพิ่มแท็ก [bash] (อาจเป็นการตอบกลับ) ทำให้ฉันทำเครื่องหมายคำถามว่าซ้ำซ้อนเพื่อตอบสนองต่อการตั้งค่าสถานะ
BoltClock


1
มีความเป็นไปได้ที่ซ้ำกันของการรับไดเรกทอรีแหล่งของสคริปต์ Bash จากภายใน
dulgan

คำตอบ:


568

ใน Bash คุณควรได้รับสิ่งที่คุณต้องการเช่นนี้:

#!/usr/bin/env bash

BASEDIR=$(dirname "$0")
echo "$BASEDIR"

17
วิธีนี้ใช้ไม่ได้หากคุณเรียกใช้สคริปต์ผ่านลิงก์สัญลักษณ์ในไดเรกทอรีอื่น เพื่อให้งานที่คุณต้องใช้readlinkเช่นกัน (ดูคำตอบของอัลด้านล่าง)
AndrewR

44
ในการทุบตีมันจะปลอดภัยกว่าที่จะใช้$BASH_SOURCEแทน$0เพราะ$0ไม่ได้มีเส้นทางของสคริปต์ที่ถูกเรียกเช่นเคยเช่นเมื่อ 'sourcing' สคริปต์
mklement0

2
$BASH_SOURCEเฉพาะ Bash คำถามเกี่ยวกับเชลล์สคริปต์โดยทั่วไป
Ha-Duong Nguyen

7
@auraham: CUR_PATH=$(pwd)หรือpwdส่งคืนไดเรกทอรีปัจจุบัน (ซึ่งไม่จำเป็นต้องเป็นสคริปต์หลัก dir)!
Andreas Dietrich

5
ฉันลองวิธีที่ @ mklement0 แนะนำให้ใช้$BASH_SOURCEและจะคืนสิ่งที่ฉันต้องการ สคริปต์ของฉันถูกเรียกใช้จากสคริปต์อื่นแล้ว$0ส่งกลับ.ขณะที่$BASH_SOURCEส่งคืนไดเรกทอรีย่อยที่ถูกต้อง (ในกรณีของฉันscripts)
David Rissato Cruz

401

โพสต์ต้นฉบับมีการแก้ปัญหา (ไม่สนใจคำตอบพวกเขาไม่ได้เพิ่มอะไรที่เป็นประโยชน์ งานที่น่าสนใจทำโดยคำสั่ง unix ที่กล่าวถึงreadlinkพร้อมตัวเลือก-fที่มีตัวเลือกทำงานเมื่อสคริปต์ถูกเรียกโดยสัมบูรณ์เช่นเดียวกับโดยเส้นทางสัมพัทธ์

สำหรับ bash, sh, ksh:

#!/bin/bash 
# Absolute path to this script, e.g. /home/user/bin/foo.sh
SCRIPT=$(readlink -f "$0")
# Absolute path this script is in, thus /home/user/bin
SCRIPTPATH=$(dirname "$SCRIPT")
echo $SCRIPTPATH

สำหรับ tcsh, csh:

#!/bin/tcsh
# Absolute path to this script, e.g. /home/user/bin/foo.csh
set SCRIPT=`readlink -f "$0"`
# Absolute path this script is in, thus /home/user/bin
set SCRIPTPATH=`dirname "$SCRIPT"`
echo $SCRIPTPATH

ดูเพิ่มเติมที่: https://stackoverflow.com/a/246128/59087


12
หมายเหตุ: readlinkไม่ได้ทุกระบบมี นั่นเป็นเหตุผลที่ฉันแนะนำให้ใช้ pushd / popd (บิวด์อินสำหรับทุบตี)
docwhat

23
-fตัวเลือกที่จะreadlinkทำอะไรบางอย่างที่แตกต่างกันใน OS X (สิงโต) และอาจ BSD stackoverflow.com/questions/1055671/…
Ergwun

9
เพื่อชี้แจงความคิดเห็นของ @ Ergwun: -fไม่รองรับ OS X เลย (เหมือนของ Lion); คุณสามารถปล่อยสิ่ง-fที่ต้องทำด้วยการแก้ไขได้ในระดับหนึ่งทางอ้อมเช่นpushd "$(dirname "$(readlink "$BASH_SOURCE" || echo "$BASH_SOURCE")")"หรือคุณสามารถม้วนสคริปต์ตามลิงก์ symbian ซ้ำตามที่แสดงในโพสต์ที่เชื่อมโยง
mklement0

2
ฉันยังไม่เข้าใจว่าทำไม OP ถึงต้องการเส้นทางที่สมบูรณ์ การรายงาน "." ควรทำงานได้ดีถ้าคุณต้องการเข้าถึงไฟล์ที่เกี่ยวข้องกับเส้นทางสคริปต์และคุณเรียกว่าสคริปต์เช่น. /myscript.sh
Stefan

10
@StefanHaberl ฉันคิดว่ามันจะเป็นปัญหาถ้าคุณเรียกใช้สคริปต์ในขณะที่ไดเรกทอรีการทำงานปัจจุบันของคุณแตกต่างจากที่ตั้งของสคริปต์ (เช่นsh /some/other/directory/script.sh)ในกรณีนี้.จะเป็น pwd ของคุณไม่ใช่/some/other/directory
Jon z

51

ความคิดเห็นก่อนหน้านี้เกี่ยวกับคำตอบบอกว่ามันเป็นเรื่องง่ายที่จะพลาดในทุกคำตอบอื่น ๆ

เมื่อใช้ทุบตี:

echo this file: "$BASH_SOURCE"
echo this dir: "$(dirname "$BASH_SOURCE")"

คู่มืออ้างอิง Bash, 5.2 ตัวแปร Bash


3
เฉพาะอันนี้ใช้ได้กับสคริปต์ในเส้นทางสภาพแวดล้อมคนที่โหวตมากที่สุดไม่ทำงาน ขอบคุณ!
กรกฎาคม

คุณควรใช้dirname "$BASH_SOURCE"แทนเพื่อจัดการช่องว่างใน $ BASH_SOURCE
Mygod

1
วิธีที่ชัดเจนมากขึ้นในการพิมพ์ไดเรกทอรีจะตาม ShellCheck เครื่องมือเป็น: "$ (dirname" $ ​​{BASH_SOURCE {0}} ")" เพราะ BASH_SOURCE เป็นอาร์เรย์และไม่มีตัวห้อยองค์ประกอบแรกจะถูกนำมาใช้โดยค่าเริ่มต้น .
Adrian M.

1
@AdrianM คุณต้องการเครื่องหมายวงเล็บไม่ใช่เครื่องหมายวงเล็บ: สำหรับดัชนี:"$(dirname "${BASH_SOURCE[0]}")"
Hashbrown

ไม่ดีสำหรับฉัน .. พิมพ์แค่จุด (เช่นไดเรกทอรีปัจจุบันสมมุติ)
JL_SO

40

สมมติว่าคุณกำลังใช้ bash

#!/bin/bash

current_dir=$(pwd)
script_dir=$(dirname $0)

echo $current_dir
echo $script_dir

สคริปต์นี้ควรพิมพ์ไดเรกทอรีที่คุณอยู่แล้วไดเรกทอรีที่สคริปต์นั้นอยู่ตัวอย่างเช่นเมื่อเรียกจาก/ด้วยสคริปต์ใน/home/mez/นั้นจะส่งออก

/
/home/mez

โปรดจำไว้ว่าเมื่อกำหนดตัวแปรจากผลลัพธ์ของคำสั่งให้ล้อมคำสั่ง$(และ)- หรือคุณจะไม่ได้ผลลัพธ์ที่ต้องการ


3
สิ่งนี้จะไม่ทำงานเมื่อฉันเรียกใช้สคริปต์จาก dir ปัจจุบัน
Eric Wang

1
@EricWang คุณอยู่ในไดเรกทอรีปัจจุบันเสมอ
ctrl-alt-delor

สำหรับฉันแล้ว $ current_dir เป็นเส้นทางที่ฉันเรียกใช้จากสคริปต์ อย่างไรก็ตาม $ script_dir ไม่ใช่ dir ของสคริปต์มันเป็นเพียงจุด
ไมเคิล

31

หากคุณใช้ทุบตี ....

#!/bin/bash

pushd $(dirname "${0}") > /dev/null
basedir=$(pwd -L)
# Use "pwd -P" for the path without links. man bash for more info.
popd > /dev/null

echo "${basedir}"

4
คุณสามารถเปลี่ยนpushd/ popdด้วยcd $(dirname "${0}")และcd -จะทำให้มันทำงานบนเปลือกหอยอื่น ๆ pwd -Lถ้าพวกเขามี
docwhat

ทำไมคุณถึงใช้ pushd และ popd ที่นี่?
qodeninja

1
ดังนั้นฉันไม่ต้องเก็บไดเรกทอรีดั้งเดิมไว้ในตัวแปร เป็นรูปแบบที่ฉันใช้ในฟังก์ชั่นมากมายและเช่นนั้น มันสร้างรังได้ดีมากซึ่งก็ดี
docwhat

มันยังคงถูกเก็บไว้ในหน่วยความจำ - ในตัวแปร - ไม่ว่าจะอ้างอิงตัวแปรในสคริปต์ของคุณหรือไม่ นอกจากนี้ฉันเชื่อว่าค่าใช้จ่ายในการดำเนินการ pushd และ popd นั้นสูงกว่าการประหยัดที่ไม่ได้สร้างตัวแปร Bash แบบโลคอลในสคริปต์ของคุณทั้งในรอบ CPU และการอ่านได้
ingyhere

20

ในฐานะที่เป็น theMarko แนะนำ:

BASEDIR=$(dirname $0)
echo $BASEDIR

วิธีนี้ใช้งานได้เว้นแต่คุณจะเรียกใช้งานสคริปต์จากไดเรกทอรีเดียวกันกับที่สคริปต์อยู่ซึ่งในกรณีนี้คุณจะได้รับ '' '

ในการหลีกเลี่ยงปัญหานั้นให้ใช้:

current_dir=$(pwd)
script_dir=$(dirname $0)

if [ $script_dir = '.' ]
then
script_dir="$current_dir"
fi

ตอนนี้คุณสามารถใช้ตัวแปร current_dir ตลอดทั้งสคริปต์เพื่ออ้างถึงไดเรกทอรีสคริปต์ อย่างไรก็ตามเรื่องนี้อาจยังมีปัญหา symlink


20

คำตอบที่ดีที่สุดสำหรับคำถามนี้ตอบที่นี่:
รับไดเรกทอรีแหล่งของสคริปต์ทุบตีจากภายใน

และมันคือ:

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

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

เพื่อให้เข้าใจถึงวิธีการทำงานคุณสามารถรันสคริปต์ต่อไปนี้:

#!/bin/bash

SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  TARGET="$(readlink "$SOURCE")"
  if [[ $TARGET == /* ]]; then
    echo "SOURCE '$SOURCE' is an absolute symlink to '$TARGET'"
    SOURCE="$TARGET"
  else
    DIR="$( dirname "$SOURCE" )"
    echo "SOURCE '$SOURCE' is a relative symlink to '$TARGET' (relative to '$DIR')"
    SOURCE="$DIR/$TARGET" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
  fi
done
echo "SOURCE is '$SOURCE'"
RDIR="$( dirname "$SOURCE" )"
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
if [ "$DIR" != "$RDIR" ]; then
  echo "DIR '$RDIR' resolves to '$DIR'"
fi
echo "DIR is '$DIR'"


10

มาทำให้เป็น POSIX oneliner:

a="/$0"; a=${a%/*}; a=${a#/}; a=${a:-.}; BASEDIR=$(cd "$a"; pwd)

ทดสอบกับกระสุนที่เข้ากันได้กับ Bourne จำนวนมากรวมถึง BSD

เท่าที่ฉันรู้ว่าฉันเป็นผู้เขียนและฉันใส่ไว้ในโดเมนสาธารณะ สำหรับข้อมูลเพิ่มเติมโปรดดูที่: https://www.jasan.tk/posts/2017-05-11-posix_shell_dirname_replacement/


1
เป็นลายลักษณ์อักษรหากช่องว่างในเส้นทางและผลตอบแทนcd: too many arguments $PWD(การแก้ไขที่ชัดเจน แต่ก็แสดงให้เห็นว่าหลายกรณีมีขอบเป็นจริง)
ไมเคิล

1
อยากโหวต ยกเว้นความคิดเห็นจาก @michael ว่ามีช่องว่างในเส้นทาง ... มีการแก้ไขไหม
spechter

1
@spechter ใช่มีการแก้ไขสำหรับสิ่งนั้น ดูjasan.tk/posix/2017/05/11/posix_shell_dirname_replacement ที่
JánSáreník

@Der_Meister - โปรดเจาะจงมากขึ้น หรือเขียน (และอาจเข้ารหัส) อีเมลถึง jasan ที่ jasan.tk
JánSáreník

2
ln -s / home / der / 1 / test / home / der / 2 / test && / home / der / 2 / test => / home / der / 2 (ควรแสดงเส้นทางไปยังสคริปต์ต้นฉบับแทน)
Der_Meister

10
BASE_DIR="$(cd "$(dirname "$0")"; pwd)";
echo "BASE_DIR => $BASE_DIR"

3
ไม่ใช่วิธีทุบตีเฉพาะที่น่าเชื่อถือที่สุดที่ฉันรู้
eddygeek

9

หากคุณต้องการรับไดเรกทอรีสคริปต์จริง (ไม่ว่าคุณจะเรียกใช้สคริปต์โดยใช้ symlink หรือโดยตรง) ให้ลอง:

BASEDIR=$(dirname $(realpath "$0"))
echo "$BASEDIR"

สิ่งนี้ใช้ได้กับทั้ง linux และ macOS realpathผมมองไม่เห็นคนที่นี่พูดถึงเกี่ยวกับ ไม่แน่ใจว่ามีข้อบกพร่องในวิธีการนี้หรือไม่

บน MacOS คุณจะต้องติดตั้งเพื่อใช้งานcoreutils เช่น:realpathbrew install coreutils


6

บทนำ

คำตอบนี้แก้ไขคำตอบที่ได้รับการโหวตแตกหัก แต่ตกตะลึงมากที่สุดของกระทู้นี้ (เขียนโดย TheMarko):

#!/usr/bin/env bash

BASEDIR=$(dirname "$0")
echo "$BASEDIR"

ทำไมถึงใช้ dirname "$ 0" เพราะไม่ได้ผล?

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

ก่อนอื่นเรามาทำความเข้าใจว่าคำตอบนี้ใช้ได้อย่างไร เขาได้รับไดเรกทอรีสคริปต์โดยทำ

dirname "$0"

$ 0หมายถึงส่วนแรกของคำสั่งที่เรียกใช้สคริปต์ (โดยพื้นฐานแล้วมันคือคำสั่งอินพุตที่ไม่มีอาร์กิวเมนต์:

/some/path/./script argument1 argument2

$ 0 = "/ บาง / เส้นทาง /./ สคริปต์"

โดยทั่วไป dirname จะค้นหาตัวสุดท้าย / ในสตริงและตัดมันที่นั่น ดังนั้นถ้าคุณ:

  dirname /usr/bin/sha256sum

คุณจะได้รับ: / usr / bin

ตัวอย่างนี้ทำงานได้ดีเพราะ / usr / bin / sha256sum เป็นเส้นทางที่จัดรูปแบบอย่างถูกต้อง แต่

  dirname "/some/path/./script"

จะไม่ทำงานได้ดีและจะให้คุณ:

  BASENAME="/some/path/." #which would crash your script if you try to use it as a path

สมมติว่าคุณอยู่ใน dir เดียวกับสคริปต์ของคุณและคุณเปิดใช้งานด้วยคำสั่งนี้

./script   

$ 0 ในสถานการณ์นี้จะเป็น. /script และ dirname $ 0 จะให้:

. #or BASEDIR=".", again this will crash your script

โดยใช้:

sh script

โดยไม่ต้องป้อนเส้นทางแบบเต็มจะให้ BASEDIR = "."

ใช้ไดเรกทอรีญาติ:

 ../some/path/./script

ให้ dirname $ 0 จาก:

 ../some/path/.

หากคุณอยู่ในไดเรกทอรี / some และคุณเรียกสคริปต์ในลักษณะนี้ (สังเกตว่าไม่มี / ในตอนเริ่มต้นให้ใช้พา ธ สัมพัทธ์อีกครั้ง):

 path/./script.sh

คุณจะได้รับค่านี้สำหรับ dirname $ 0:

 path/. 

และ ./path/./script (รูปแบบอื่นของเส้นทางแบบสัมพัทธ์) ให้:

 ./path/.

มีเพียงสองสถานการณ์เท่านั้นที่ฐานของ$ 0จะทำงานถ้าผู้ใช้ใช้ sh หรือ touch เพื่อเรียกใช้สคริปต์เนื่องจากทั้งคู่จะส่งผลให้มีค่า $ 0:

 $0=/some/path/script

ซึ่งจะให้เส้นทางที่คุณสามารถใช้กับ dirname

การแก้ไขปัญหา

คุณมีบัญชีและตรวจสอบทุกสถานการณ์ดังกล่าวข้างต้นและใช้การแก้ไขหากเกิดขึ้น:

#!/bin/bash
#this script will only work in bash, make sure it's installed on your system.

#set to false to not see all the echos
debug=true

if [ "$debug" = true ]; then echo "\$0=$0";fi


#The line below detect script's parent directory. $0 is the part of the launch command that doesn't contain the arguments
BASEDIR=$(dirname "$0") #3 situations will cause dirname $0 to fail: #situation1: user launches script while in script dir ( $0=./script)
                                                                     #situation2: different dir but ./ is used to launch script (ex. $0=/path_to/./script)
                                                                     #situation3: different dir but relative path used to launch script
if [ "$debug" = true ]; then echo 'BASEDIR=$(dirname "$0") gives: '"$BASEDIR";fi                                 

if [ "$BASEDIR" = "." ]; then BASEDIR="$(pwd)";fi # fix for situation1

_B2=${BASEDIR:$((${#BASEDIR}-2))}; B_=${BASEDIR::1}; B_2=${BASEDIR::2}; B_3=${BASEDIR::3} # <- bash only
if [ "$_B2" = "/." ]; then BASEDIR=${BASEDIR::$((${#BASEDIR}-1))};fi #fix for situation2 # <- bash only
if [ "$B_" != "/" ]; then  #fix for situation3 #<- bash only
        if [ "$B_2" = "./" ]; then
                #covers ./relative_path/(./)script
                if [ "$(pwd)" != "/" ]; then BASEDIR="$(pwd)/${BASEDIR:2}"; else BASEDIR="/${BASEDIR:2}";fi
        else
                #covers relative_path/(./)script and ../relative_path/(./)script, using ../relative_path fails if current path is a symbolic link
                if [ "$(pwd)" != "/" ]; then BASEDIR="$(pwd)/$BASEDIR"; else BASEDIR="/$BASEDIR";fi
        fi
fi

if [ "$debug" = true ]; then echo "fixed BASEDIR=$BASEDIR";fi

4

นี้หนึ่งซับบอกที่เชลล์สคริปต์คือไม่ว่าคุณจะวิ่งหรือถ้าคุณมามัน นอกจากนี้ยังแก้ไขลิงก์สัญลักษณ์ที่เกี่ยวข้องหากเป็นกรณีนี้:

dir=$(dirname $(test -L "$BASH_SOURCE" && readlink -f "$BASH_SOURCE" || echo "$BASH_SOURCE"))

โดยวิธีการที่ฉันคิดว่าคุณกำลังใช้/ bin / ทุบตี


2

มีคำตอบมากมายที่เป็นไปได้ทั้งหมดโดยมีวัตถุประสงค์ของโปรและคอนและวัตถุประสงค์ที่แตกต่างกันเล็กน้อย (ซึ่งควรระบุไว้ในแต่ละข้อ) นี่เป็นอีกวิธีการหนึ่งที่ตรงตามวัตถุประสงค์หลักของทั้งความชัดเจนและการทำงานในทุกระบบใน bash ทั้งหมด (ไม่มีข้อสันนิษฐานเกี่ยวกับ bash version หรือreadlinkหรือpwdตัวเลือก) และสิ่งที่คุณคาดหวังว่าจะเกิดขึ้น (เช่นการแก้ไข symlink) ปัญหาที่น่าสนใจ แต่ไม่ใช่สิ่งที่คุณต้องการจริง ๆ ) จัดการกับตัวพิมพ์ขอบเช่นช่องว่างในพา ธ ฯลฯ ละเว้นข้อผิดพลาดใด ๆ และใช้ค่าเริ่มต้นที่มีสติหากมีปัญหาใด ๆ

แต่ละองค์ประกอบถูกเก็บไว้ในตัวแปรแยกต่างหากที่คุณสามารถใช้ทีละรายการ:

# script path, filename, directory
PROG_PATH=${BASH_SOURCE[0]}      # this script's name
PROG_NAME=${PROG_PATH##*/}       # basename of script (strip path)
PROG_DIR="$(cd "$(dirname "${PROG_PATH:-$PWD}")" 2>/dev/null 1>&2 && pwd)"


-4

ที่ควรทำเคล็ดลับ:

echo `pwd`/`dirname $0`

มันอาจดูน่าเกลียดขึ้นอยู่กับว่ามันถูกเรียกใช้และ cwd แต่ควรให้คุณไปที่ที่คุณต้องการไป (หรือคุณสามารถปรับแต่งสตริงถ้าคุณสนใจว่ามันดู)


1
ปัญหาการหลบหนีของ stackoverflow ที่นี่: แน่นอนควรมีลักษณะเช่นนี้: `pwd`/`dirname $0`แต่อาจยังคงล้มเหลวใน symlinks
Andreas Dietrich
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.