dirname và basename vs mở rộng tham số


20

Có bất kỳ lý do khách quan để thích một hình thức khác? Hiệu suất, độ tin cậy, tính di động?

filename=/some/long/path/to/a_file

parentdir_v1="${filename%/*}"
parentdir_v2="$(dirname "$filename")"

basename_v1="${filename##*/}"
basename_v2="$(basename "$filename")"

echo "$parentdir_v1"
echo "$parentdir_v2"
echo "$basename_v1"
echo "$basename_v2"

Sản xuất:

/some/long/path/to
/some/long/path/to
a_file
a_file

(v1 sử dụng mở rộng tham số shell, v2 sử dụng nhị phân ngoài.)

Câu trả lời:


21

Cả hai đều có những điều kỳ quặc, thật không may.

Cả hai đều được POSIX yêu cầu, vì vậy sự khác biệt giữa chúng không phải là vấn đề về tính di động¹.

Cách đơn giản để sử dụng các tiện ích là

base=$(basename -- "$filename")
dir=$(dirname -- "$filename")

Lưu ý các dấu ngoặc kép xung quanh các thay thế thay đổi, như mọi khi, và cả --sau lệnh, trong trường hợp tên tệp bắt đầu bằng dấu gạch ngang (nếu không các lệnh sẽ hiểu tên tệp là một tùy chọn). Điều này vẫn thất bại trong trường hợp một cạnh, điều này rất hiếm nhưng có thể bị ép buộc bởi một người dùng độc hại²: thay thế lệnh sẽ loại bỏ các dòng mới. Vì vậy, nếu một tên tệp được gọi foo/bar␤thì basesẽ được đặt thành barthay vì bar␤. Một cách giải quyết là thêm một ký tự không phải dòng mới và loại bỏ nó sau khi thay thế lệnh:

base=$(basename -- "$filename"; echo .); base=${base%.}
dir=$(dirname -- "$filename"; echo .); dir=${dir%.}

Với việc thay thế tham số, bạn không gặp phải các trường hợp cạnh liên quan đến việc mở rộng các ký tự lạ, nhưng có một số khó khăn với ký tự gạch chéo. Một điều hoàn toàn không phải là trường hợp biên là việc tính toán phần thư mục yêu cầu mã khác nhau cho trường hợp không có /.

base="${filename##*/}"
case "$filename" in
  */*) dirname="${filename%/*}";;
  *) dirname=".";;
esac

Trường hợp cạnh là khi có một dấu gạch chéo (bao gồm cả trường hợp của thư mục gốc, tất cả là dấu gạch chéo). Các lệnh basenamedirnametước bỏ dấu gạch chéo trước khi chúng thực hiện công việc của mình. Không có cách nào để loại bỏ các dấu gạch chéo trong một lần nếu bạn sử dụng các cấu trúc POSIX, nhưng bạn có thể thực hiện theo hai bước. Bạn cần quan tâm đến trường hợp khi đầu vào không có gì ngoài dấu gạch chéo.

case "$filename" in
  */*[!/]*)
    trail=${filename##*[!/]}; filename=${filename%%"$trail"}
    base=${filename##*/}
    dir=${filename%/*};;
  *[!/]*)
    trail=${filename##*[!/]}
    base=${filename%%"$trail"}
    dir=".";;
  *) base="/"; dir="/";;
esac

Nếu bạn tình cờ biết rằng bạn không ở trong trường hợp cạnh (ví dụ: findkết quả không phải là điểm bắt đầu luôn chứa một phần thư mục và không có dấu vết /) thì thao tác chuỗi mở rộng tham số là đơn giản. Nếu bạn cần đối phó với tất cả các trường hợp cạnh, các tiện ích sẽ dễ sử dụng hơn (nhưng chậm hơn).

Đôi khi, bạn có thể muốn đối xử foo/như thế foo/.hơn là thích foo. Nếu bạn đang hành động trên một mục nhập thư mục thì foo/được cho là tương đương foo/., không foo; điều này tạo ra sự khác biệt khi foolà một liên kết tượng trưng đến một thư mục: foocó nghĩa là liên kết tượng trưng, foo/có nghĩa là thư mục đích. Trong trường hợp đó, tên cơ sở của một đường dẫn có dấu gạch chéo là thuận lợi .và đường dẫn có thể là tên thư mục riêng của nó.

case "$filename" in
  */) base="."; dir="$filename";;
  */*) base="${filename##*/}"; dir="${filename%"$base"}";;
  *) base="$filename"; dir=".";;
esac

Phương pháp nhanh và đáng tin cậy là sử dụng zsh với các bộ sửa đổi lịch sử của nó (dải đầu tiên này cắt các dấu gạch chéo, giống như các tiện ích):

dir=$filename:h base=$filename:t

¹ Trừ khi bạn đang sử dụng các vỏ POSIX trước như Solaris 10 và cũ hơn /bin/sh(thiếu các tính năng thao tác chuỗi mở rộng tham số trên các máy vẫn đang được sản xuất - nhưng luôn có vỏ POSIX được gọi shtrong cài đặt, chỉ có nó /usr/xpg4/bin/sh, không phải vậy /bin/sh).
² Ví dụ: gửi tệp được gọi foo␤đến dịch vụ tải lên tệp không bảo vệ chống lại điều này, sau đó xóa tệp đó và foothay vào đó sẽ bị xóa


Ồ Vì vậy, nó có vẻ như (trong bất kỳ vỏ POSIX) cách mạnh mẽ nhất là cách thứ hai bạn đề cập? base=$(basename -- "$filename"; echo .); base=${base%.}; dir=$(dirname -- "$filename"; echo .); dir=${dir%.}? Tôi đã đọc kỹ và tôi không nhận thấy bạn đề cập đến bất kỳ nhược điểm nào.
tự đại diện

1
@Wildcard Một nhược điểm là nó xử lý foo/như thế nào foo, không thích foo/., không phù hợp với các tiện ích tuân thủ POSIX.
Gilles 'SO- ngừng trở nên xấu xa'

Hiểu rồi, cảm ơn. Tôi nghĩ rằng tôi vẫn thích phương pháp đó bởi vì tôi sẽ biết nếu tôi đang cố gắng xử lý các thư mục và tôi chỉ có thể giải quyết (hoặc "giải quyết lại") một dấu vết /nếu tôi cần.
tự đại diện

"Ví dụ: một findkết quả, luôn chứa một phần thư mục và không có dấu /" Không hoàn toàn đúng, find ./sẽ xuất ra ./kết quả đầu tiên.
Tavian Barnes

@Gilles Ví dụ về nhân vật dòng mới chỉ thổi vào tâm trí của tôi. Cảm ơn câu trả lời
Sam Thomas

10

Cả hai đều nằm trong POSIX, vì vậy tính di động "nên" không đáng lo ngại. Các thay thế vỏ nên được coi là để chạy nhanh hơn.

Tuy nhiên - nó phụ thuộc vào những gì bạn có nghĩa là di động. Một số hệ thống cũ (không cần thiết) đã không triển khai các tính năng đó trong /bin/sh(Solaris 10 trở lên trong tâm trí), mặt khác, một thời gian trước, các nhà phát triển đã cảnh báo rằng nó dirnamekhông dễ mang theo như basename.

Để tham khảo:

Khi xem xét tính di động, tôi sẽ phải tính đến tất cả các hệ thống nơi tôi duy trì các chương trình. Không phải tất cả đều là POSIX, vì vậy có sự đánh đổi. Sự đánh đổi của bạn có thể khác nhau.


7

Ngoài ra còn có:

mkdir '
';    dir=$(basename ./'
');   echo "${#dir}"

0

Những điều kỳ lạ như thế xảy ra bởi vì có rất nhiều phiên dịch và phân tích cú pháp và phần còn lại cần phải xảy ra khi hai quá trình nói chuyện. Thay thế lệnh sẽ tước dòng mới. Và NUL (mặc dù điều đó rõ ràng không liên quan ở đây) . basenamedirnamecũng sẽ loại bỏ các dòng mới trong bất kỳ trường hợp nào vì bạn nói chuyện với họ như thế nào? Tôi biết, theo dõi các dòng mới trong một tên tệp là loại anathema nào, nhưng bạn không bao giờ biết. Và nó không có ý nghĩa để đi theo con đường có thể thiếu sót khi bạn có thể làm khác.

Vẫn ... ${pathname##*/} != basenamevà tương tự ${pathname%/*} != dirname. Những lệnh được quy định để thực hiện một phần lớn là dãy được xác định rõ các bước để đi đến kết quả cụ thể của họ.

Thông số kỹ thuật bên dưới, nhưng trước tiên đây là phiên bản terser:

basename()
    case   $1   in
    (*[!/]*/)     basename         "${1%"${1##*[!/]}"}"   ${2+"$2"}  ;;
    (*/[!/]*)     basename         "${1##*/}"             ${2+"$2"}  ;;
  (${2:+?*}"$2")  printf  %s%b\\n  "${1%"$2"}"       "${1:+\n\c}."   ;;
    (*)           printf  %s%c\\n  "${1##///*}"      "${1#${1#///}}" ;;
    esac

Đó là một tuân thủ POSIX hoàn toàn basenameđơn giản sh. Điều đó không khó thực hiện. Tôi đã hợp nhất một vài chi nhánh tôi sử dụng dưới đây vì tôi có thể mà không ảnh hưởng đến kết quả.

Đây là thông số kỹ thuật:

basename()
    case   $1 in
    ("")            #  1. If  string  is  a null string, it is 
                    #     unspecified whether the resulting string
                    #     is '.' or a null string. In either case,
                    #     skip steps 2 through 6.
                  echo .
     ;;             #     I feel like I should flip a coin or something.
    (//)            #  2. If string is "//", it is implementation-
                    #     defined whether steps 3 to 6 are skipped or
                    #     or processed.
                    #     Great. What should I do then?
                  echo //
     ;;             #     I guess it's *my* implementation after all.
    (*[!/]*/)       #  3. If string consists entirely of <slash> 
                    #     characters, string shall be set to a sin‐
                    #     gle <slash> character. In this case, skip
                    #     steps 4 to 6.
                    #  4. If there are any trailing <slash> characters
                    #     in string, they shall be removed.
                  basename "${1%"${1##*[!/]}"}" ${2+"$2"}  
      ;;            #     Fair enough, I guess.
     (*/)         echo /
      ;;            #     For step three.
     (*/*)          #  5. If there are any <slash> characters remaining
                    #     in string, the prefix of string up to and 
                    #     including the last <slash> character in
                    #     string shall be removed.
                  basename "${1##*/}" ${2+"$2"}
      ;;            #      == ${pathname##*/}
     ("$2"|\
      "${1%"$2"}")  #  6. If  the  suffix operand is present, is not
                    #     identical to the characters remaining
                    #     in string, and is identical to a suffix of
                    #     the characters remaining  in  string, the
                    #     the  suffix suffix shall be removed from
                    #     string.  Otherwise, string is not modi‐
                    #     fied by this step. It shall not be
                    #     considered an error if suffix is not 
                    #     found in string.
                  printf  %s\\n "$1"
     ;;             #     So far so good for parameter substitution.
     (*)          printf  %s\\n "${1%"$2"}"
     esac           #     I probably won't do dirname.

... có lẽ các ý kiến ​​đang gây mất tập trung ....


1
Wow, điểm hay về các dòng mới trong tên tập tin. Thật là một con giun. Tôi không nghĩ rằng tôi thực sự hiểu kịch bản của bạn, mặc dù. Tôi chưa từng thấy [!/]trước đây, có phải như [^/]vậy không? Nhưng bình luận của bạn bên cạnh đó dường như không khớp với nó ....
Wildcard

1
@Wildcard - tốt .. đó không phải là nhận xét của tôi. Đó là tiêu chuẩn . Thông số POSIX cho basenamelà một bộ hướng dẫn về cách thực hiện với vỏ của bạn. Nhưng [!charclass]có phải cách di động để làm điều đó với các khối [^class]là dành cho regex - và shell không phải là đặc trưng cho regex. Giới thiệu về phù hợp với những nhận xét ... casecác bộ lọc, vì vậy nếu tôi phù hợp với một chuỗi có chứa một dấu gạch chéo / một !/sau đó nếu mẫu case kế tiếp bên dưới các trận đấu bất kỳ dấu /gạch chéo ở tất cả họ chỉ có thể là tất cả các dấu gạch chéo. Và một cái bên dưới không thể có bất kỳ dấu vết nào
mikeerv

2

Bạn có thể nhận được sự thúc đẩy từ trong quá trình basenamedirname(Tôi không hiểu tại sao đây không phải là nội dung - nếu đây không phải là ứng cử viên, tôi không biết đó là gì) nhưng việc triển khai cần xử lý những việc như:

path         dirname    basename
"/usr/lib"    "/usr"    "lib"
"/usr/"       "/"       "usr"
"usr"         "."       "usr"
"/"           "/"       "/"
"."           "."       "."
".."          "."       ".."

^ Từ tên cơ sở (3)

và các trường hợp cạnh khác.

Tôi đã sử dụng:

basename(){ 
  test -n "$1" || return 0
  local x="$1"; while :; do case "$x" in */) x="${x%?}";; *) break;; esac; done
  [ -n "$x" ] || { echo /; return; }
  printf '%s\n' "${x##*/}"; 
}

dirname(){ 
  test -n "$1" || return 0
  local x="$1"; while :; do case "$x" in */) x="${x%?}";; *) break;; esac; done
  [ -n "$x" ] || { echo /; return; }
  set -- "$x"; x="${1%/*}"
  case "$x" in "$1") x=.;; "") x=/;; esac
  printf '%s\n' "$x"
}

(Việc triển khai GNU mới nhất của tôi basenamedirnamethêm một số công tắc dòng lệnh ưa thích đặc biệt cho các công cụ như xử lý nhiều đối số hoặc tước hậu tố, nhưng điều đó cực kỳ dễ dàng để thêm vào trình bao.)

Không khó để biến chúng thành các bashnội trang (bằng cách sử dụng triển khai hệ thống cơ bản), nhưng chức năng trên không cần phải được biên dịch và chúng cũng cung cấp một số tăng.


Danh sách các trường hợp cạnh thực sự rất hữu ích. Đó là tất cả những điểm rất tốt. Danh sách thực sự có vẻ khá đầy đủ; Có thực sự có trường hợp cạnh khác?
tự đại diện

Việc triển khai trước đây của tôi không xử lý x//chính xác mọi thứ , nhưng tôi đã sửa cho bạn trước khi trả lời. Tôi hy vọng đó là nó.
PSkocik

Bạn có thể chạy một kịch bản để so sánh các chức năng và các tệp thực thi làm gì trên các ví dụ này. Tôi nhận được một trận đấu 100%.
PSkocik

1
Hàm dirname của bạn dường như không loại bỏ các dấu gạch chéo lặp đi lặp lại. Ví dụ: dirname a///b//c//d////esản lượng a///b//c//d///.
codeforester
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.