Tập lệnh shell Unix tìm ra thư mục nào tập tin tập lệnh nằm?


511

Về cơ bản tôi cần chạy tập lệnh với các đường dẫn liên quan đến vị trí tệp tập lệnh shell, làm thế nào tôi có thể thay đổi thư mục hiện tại thành cùng thư mục với nơi tập tin tập lệnh nằm?


12
Đó thực sự là một bản sao? Câu hỏi này là về một "tập lệnh shell unix", cụ thể khác về Bash.
michaeljt

2
@BoltClock: Câu hỏi này đã được đóng không đúng cách. Câu hỏi liên kết là về Bash. Câu hỏi này là về lập trình shell Unix. Lưu ý rằng các câu trả lời được chấp nhận là khá khác nhau!
Dietrich Epp

@Dietrich Epp: Bạn nói đúng. Có vẻ như sự lựa chọn của người hỏi về câu trả lời được chấp nhận và việc thêm thẻ [bash] (có thể để đáp lại điều đó) đã khiến tôi đánh dấu câu hỏi là trùng lặp khi trả lời cờ.
BoltClock


Câu trả lời:


568

Trong Bash, bạn sẽ nhận được những gì bạn cần như thế này:

#!/usr/bin/env bash

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

17
Điều này không hoạt động nếu bạn đã gọi tập lệnh thông qua một liên kết tượng trưng trong một thư mục khác. Để thực hiện công việc đó, bạn cũng cần phải sử dụng readlink(xem câu trả lời của al bên dưới)
AndrewR

44
Trong bash, việc sử dụng $BASH_SOURCEthay thế sẽ an toàn hơn $0, bởi vì $0không phải lúc nào cũng chứa đường dẫn của tập lệnh được gọi, chẳng hạn như khi 'tìm nguồn cung ứng' một tập lệnh.
mkuity0

2
$BASH_SOURCElà đặc thù của Bash, câu hỏi là về kịch bản shell nói chung.
Hà-Dương Nguyễn

7
@auraham: CUR_PATH=$(pwd)hoặc pwdtrả về thư mục hiện tại (không phải là thư mục cha mẹ)!
Andreas Dietrich

5
Tôi đã thử phương pháp @ mkuity0 được khuyến nghị, sử dụng $BASH_SOURCEvà nó trả về những gì tôi cần. Kịch bản của tôi đang được gọi từ một kịch bản khác và $0trả về .trong khi $BASH_SOURCEtrả về thư mục con bên phải (trong trường hợp của tôi scripts).
David Rissato Cruz

401

Bài đăng gốc chứa giải pháp (bỏ qua các phản hồi, họ không thêm bất cứ điều gì hữu ích). Công việc thú vị được thực hiện bằng lệnh unix đã đề cập readlinkvới tùy chọn -f. Hoạt động khi tập lệnh được gọi bởi một đường dẫn tuyệt đối cũng như tương đối.

Đối với 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

Đối với 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

Xem thêm: https://stackoverflow.com/a/246128/59087


12
Lưu ý: Không phải tất cả các hệ thống đều có readlink. Đó là lý do tại sao tôi khuyên bạn nên sử dụng Pushd / popd (tích hợp cho bash).
docwhat

23
Các -ftùy chọn để readlinklàm điều gì đó khác nhau trên OS X (Lion) và có thể BSD. stackoverflow.com/questions/1055671/
Mạnh

9
Để làm rõ nhận xét của @ Ergwun: hoàn toàn -fkhông được hỗ trợ trên OS X (kể từ Lion); ở đó, bạn có thể bỏ qua -fđể giải quyết tối đa một mức độ gián tiếp, ví dụ pushd "$(dirname "$(readlink "$BASH_SOURCE" || echo "$BASH_SOURCE")")", hoặc bạn có thể cuộn đoạn mã theo dõi liên kết đệ quy của riêng mình như được trình bày trong bài được liên kết.
mkuity0

2
Tôi vẫn không hiểu, tại sao OP lại cần con đường tuyệt đối. Báo cáo "." sẽ hoạt động tốt nếu bạn muốn truy cập các tệp liên quan đến đường dẫn tập lệnh và bạn đã gọi tập lệnh như ./myscript.sh
Stefan Haberl

10
@StefanHaberl Tôi nghĩ sẽ có vấn đề nếu bạn chạy tập lệnh trong khi thư mục làm việc hiện tại của bạn khác với vị trí của tập lệnh (ví dụ: sh /some/other/directory/script.sh)trong trường hợp này .sẽ là pwd của bạn, không phải/some/other/directory
Jon z

51

Một nhận xét trước đó về một câu trả lời đã nói điều đó, nhưng rất dễ bỏ lỡ trong số tất cả các câu trả lời khác.

Khi sử dụng bash:

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

Tài liệu tham khảo Bash, 5.2 Biến Bash


3
Chỉ cái này hoạt động với tập lệnh trong đường dẫn môi trường, những cái được bình chọn nhiều nhất không hoạt động. cảm ơn bạn!
tháng

dirname "$BASH_SOURCE"Thay vào đó, bạn nên sử dụng để xử lý khoảng trắng trong $ BASH_SOURCE.
Mygod

1
Một cách rõ ràng hơn để in thư mục, theo công cụ ShellCheck, là: "$ (dirname" $ ​​{BASH_SOURCE {0}} ")" vì BASH_SOURCE là một mảng và không có phần tử con, phần tử đầu tiên được lấy theo mặc định .
Adrian M.

1
@AdrianM. , bạn muốn dấu ngoặc, không phải dấu ngoặc nhọn cho chỉ mục:"$(dirname "${BASH_SOURCE[0]}")"
Hashbrown

Không tốt cho tôi..prints chỉ là một dấu chấm (có lẽ là thư mục hiện tại, có lẽ)
JL_SO

40

Giả sử bạn đang sử dụng bash

#!/bin/bash

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

echo $current_dir
echo $script_dir

Tập lệnh này sẽ in thư mục mà bạn đang ở, và sau đó là thư mục mà tập lệnh nằm trong. Ví dụ: khi gọi nó từ /với tập lệnh trong /home/mez/, nó xuất ra

/
/home/mez

Hãy nhớ rằng, khi gán các biến từ đầu ra của lệnh, hãy bọc lệnh vào $()- hoặc bạn sẽ không nhận được đầu ra mong muốn.


3
Điều này sẽ không hoạt động khi tôi gọi kịch bản từ thư mục hiện tại.
Eric Wang

1
@EricWang bạn luôn ở trong thư mục hiện tại.
ctrl-alt-delor

Đối với tôi, $ current_dir thực sự là con đường tôi gọi kịch bản từ đó. Tuy nhiên, $ script_dir không phải là thư mục của tập lệnh, nó chỉ là một dấu chấm.
Michael

31

Nếu bạn đang sử dụng bash ....

#!/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
Bạn có thể thay thế pushd/ popdbằng cd $(dirname "${0}")cd -để làm cho nó hoạt động trên các shell khác, nếu chúng có pwd -L.
docwhat

Tại sao bạn lại sử dụng Pushd và popd ở đây?
qodeninja

1
Vì vậy, tôi không phải lưu trữ thư mục gốc trong một biến. Đó là một mẫu tôi sử dụng rất nhiều trong các chức năng và như vậy. Nó làm tổ thực sự tốt, đó là tốt.
docwhat

Nó vẫn đang được lưu trữ trong bộ nhớ - trong một biến - cho dù một biến có được tham chiếu trong tập lệnh của bạn hay không. Ngoài ra, tôi tin rằng chi phí cho việc thực hiện Pushd và popd vượt xa sự tiết kiệm của việc không tạo biến Bash cục bộ trong tập lệnh của bạn, cả về chu kỳ CPU và khả năng đọc.
vào

20

Như theMarko gợi ý:

BASEDIR=$(dirname $0)
echo $BASEDIR

Điều này hoạt động trừ khi bạn thực thi tập lệnh từ cùng thư mục chứa tập lệnh, trong trường hợp đó bạn nhận được giá trị '.'

Để giải quyết vấn đề đó, hãy sử dụng:

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

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

Bây giờ bạn có thể sử dụng biến current_dir trong toàn bộ tập lệnh của mình để tham chiếu đến thư mục tập lệnh. Tuy nhiên điều này vẫn có thể có vấn đề liên kết tượng trưng.


20

Câu trả lời tốt nhất cho câu hỏi này đã được trả lời ở đây:
Lấy thư mục nguồn của tập lệnh Bash từ bên trong

Và đó là:

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

One-liner sẽ cung cấp cho bạn tên thư mục đầy đủ của tập lệnh bất kể nó được gọi từ đâu.

Để hiểu cách thức hoạt động, bạn có thể thực thi đoạn mã sau:

#!/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

Hãy biến nó thành một POSIX oneliner:

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

Đã thử nghiệm trên nhiều hệ vỏ tương thích Bourne, bao gồm cả các vỏ BSD.

Theo như tôi biết tôi là tác giả và tôi đưa nó vào phạm vi công cộng. Để biết thêm thông tin, hãy xem: https://www.jasan.tk/posts/2017-05-11-poseix_shell_dirname_Vplocation/


1
như được viết, cd: too many argumentsnếu khoảng trắng trong đường dẫn và trả về $PWD. (một sửa chữa rõ ràng, nhưng chỉ cho thấy có bao nhiêu trường hợp thực sự có)
michael

1
Sẽ upvote. Ngoại trừ nhận xét từ @michael rằng nó thất bại với khoảng trắng trên đường dẫn ... Có cách khắc phục nào không?
spechter

1
@spechter vâng, có một sửa chữa cho điều đó. Xem cập nhật jasan.tk/poseix/2017/05/11/poseix_shell_dirname_Vplocation
Ján Sáreník

@Der_Meister - vui lòng cụ thể hơn. Hoặc viết (và có thể mã hóa) một email đến jasan tại jasan.tk
Ján Sáreník

2
ln -s / home / der / 1 / test / home / der / 2 / test && / home / der / 2 / test => / home / der / 2 (nên hiển thị đường dẫn đến tập lệnh gốc thay thế)
Der_Meister

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

3
cách đáng tin cậy nhất không bash cụ thể mà tôi biết.
eddygeek

10

Nếu bạn muốn lấy thư mục tập lệnh thực tế (bất kể bạn đang gọi tập lệnh bằng liên kết tượng trưng hay trực tiếp), hãy thử:

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

Điều này hoạt động trên cả linux và macOS. Tôi không thể thấy ai ở đây đề cập đếnrealpath . Không chắc chắn liệu có bất kỳ nhược điểm trong phương pháp này.

trên macOS, bạn cần cài đặt coreutilsđể sử dụng realpath. Vd : brew install coreutils.


6

GIỚI THIỆU

Câu trả lời này sửa câu trả lời được bình chọn rất hàng đầu nhưng gây sốc của chủ đề này (được viết bởi TheMarko):

#!/usr/bin/env bash

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

TẠI SAO SỬ DỤNG dirname "$ 0" TRÊN NÓ KHÔNG LÀM VIỆC?

tên thư $ 0 sẽ chỉ hoạt động nếu người dùng khởi chạy tập lệnh theo một cách rất cụ thể. Tôi đã có thể tìm thấy một số tình huống trong đó câu trả lời này không thành công và làm hỏng kịch bản.

Trước hết, hãy hiểu câu trả lời này hoạt động như thế nào. Anh ấy nhận được thư mục kịch bản bằng cách làm

dirname "$0"

$ 0 đại diện cho phần đầu tiên của lệnh gọi tập lệnh (về cơ bản đó là lệnh được nhập mà không có đối số:

/some/path/./script argument1 argument2

$ 0 = "/ some / path /./ script"

Về cơ bản, dirname tìm thấy cuối cùng / trong một chuỗi và cắt nó ở đó. Vì vậy, nếu bạn làm:

  dirname /usr/bin/sha256sum

bạn sẽ nhận được: / usr / bin

Ví dụ này hoạt động tốt vì / usr / bin / sha256sum là một đường dẫn được định dạng đúng nhưng

  dirname "/some/path/./script"

sẽ không hoạt động tốt và sẽ cung cấp cho bạn:

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

Giả sử bạn đang ở cùng thư mục với tập lệnh của mình và bạn khởi chạy nó bằng lệnh này

./script   

$ 0 trong tình huống này sẽ là ./script và dirname $ 0 sẽ cung cấp:

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

Sử dụng:

sh script

Không nhập vào đường dẫn đầy đủ cũng sẽ cho BASEDIR = "."

Sử dụng các thư mục tương đối:

 ../some/path/./script

Cung cấp một dirname $ 0 trong số:

 ../some/path/.

Nếu bạn đang ở trong thư mục / some và bạn gọi script theo cách này (lưu ý sự vắng mặt của / ở đầu, lại là một đường dẫn tương đối):

 path/./script.sh

Bạn sẽ nhận được giá trị này cho dirname $ 0:

 path/. 

và ./path/./script (một dạng khác của đường dẫn tương đối) đưa ra:

 ./path/.

Hai tình huống duy nhất dựa trên $ 0 sẽ hoạt động là nếu người dùng sử dụng sh hoặc chạm để khởi chạy tập lệnh vì cả hai sẽ dẫn đến $ 0:

 $0=/some/path/script

Nó sẽ cung cấp cho bạn một đường dẫn bạn có thể sử dụng với dirname.

GIẢI PHÁP

Bạn sẽ có tài khoản và phát hiện mọi tình huống được đề cập ở trên và áp dụng cách khắc phục nếu xảy ra:

#!/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

Lớp lót này cho biết tập lệnh shell nằm ở đâu, không quan trọng nếu bạn chạy nó hoặc nếu bạn có nguồn gốc nó . Ngoài ra, nó giải quyết bất kỳ liên kết tượng trưng liên quan, nếu đó là trường hợp:

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

Nhân tiện, tôi cho rằng bạn đang sử dụng / bin / bash .


2

Rất nhiều câu trả lời, tất cả đều hợp lý, mỗi câu hỏi đều có mục tiêu ủng hộ và khác biệt (có lẽ nên được nêu rõ cho từng mục tiêu). Đây là một giải pháp khác đáp ứng mục tiêu chính là cả rõ ràng và hoạt động trên tất cả các hệ thống, trên tất cả các bash (không có giả định về các phiên bản bash, hoặc readlinkhoặcpwd tùy chọn) và hợp lý là những gì bạn mong muốn xảy ra (ví dụ: giải quyết các liên kết tượng trưng là một vấn đề thú vị, nhưng thường không phải là những gì bạn thực sự muốn), xử lý các trường hợp cạnh như khoảng trắng trong đường dẫn, v.v., bỏ qua mọi lỗi và sử dụng mặc định lành mạnh nếu có bất kỳ vấn đề nào.

Mỗi thành phần được lưu trữ trong một biến riêng biệt mà bạn có thể sử dụng riêng lẻ:

# 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

Điều đó sẽ làm các trick:

echo `pwd`/`dirname $0`

Nó có thể trông xấu xí tùy thuộc vào cách nó được gọi và cwd nhưng sẽ đưa bạn đến nơi bạn cần đến (hoặc bạn có thể điều chỉnh chuỗi nếu bạn quan tâm nó trông như thế nào).


1
Vấn đề thoát stackoverflow ở đây: nó chắc chắn sẽ giống như thế này: `pwd`/`dirname $0`nhưng vẫn có thể thất bại trên các liên kết tượng trưng
Andreas Dietrich
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.