dirname และ basename เทียบกับการขยายพารามิเตอร์


20

มีเหตุผลใดที่จะชอบอีกรูปแบบหนึ่งหรือไม่ ประสิทธิภาพความน่าเชื่อถือความสะดวกในการพกพา?

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"

ผลิต:

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

(v1 ใช้การขยายพารามิเตอร์เชลล์ v2 ใช้ไบนารีภายนอก)

คำตอบ:


21

ทั้งคู่ต่างก็มีนิสัยแปลก ๆ

ทั้งสองอย่างจำเป็นต้องใช้โดย POSIX ดังนั้นความแตกต่างระหว่างทั้งสองจึงไม่ใช่เรื่องของการพกพา¹

วิธีการใช้งานที่เรียบง่ายคือ

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

สังเกตเครื่องหมายคำพูดคู่รอบ ๆ การแทนที่ตัวแปรเช่นเคยและ--คำสั่งหลังในกรณีที่ชื่อไฟล์เริ่มต้นด้วยเส้นประ (มิฉะนั้นคำสั่งจะตีความชื่อไฟล์เป็นตัวเลือก) สิ่งนี้ยังคงล้มเหลวในกรณีขอบหนึ่งซึ่งหายาก แต่อาจถูกบังคับโดยผู้ใช้ที่เป็นอันตราย²: การทดแทนคำสั่งลบบรรทัดใหม่ต่อท้าย ดังนั้นถ้าชื่อไฟล์ที่เรียกว่าfoo/bar␤แล้วbaseจะถูกตั้งค่าแทนbar bar␤วิธีแก้ปัญหาคือการเพิ่มตัวละครที่ไม่ใช่บรรทัดใหม่และตัดมันหลังจากการทดแทนคำสั่ง:

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

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

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

เคสขอบคือเมื่อมีเครื่องหมายสแลชต่อท้าย (รวมถึงกรณีของไดเร็กทอรีรูทซึ่งเป็นสแลชทั้งหมด) basenameและdirnameคำสั่งถอดท้ายทับก่อนที่พวกเขาทำผลงานของพวกเขา ไม่มีวิธีที่จะตัดเครื่องหมายทับต่อท้ายในครั้งเดียวถ้าคุณติดกับ POSIX งานสร้าง แต่คุณสามารถทำได้สองขั้นตอน คุณจำเป็นต้องดูแลเคสเมื่ออินพุตประกอบด้วยอะไรนอกจากสแลช

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

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

บางครั้งคุณอาจต้องการที่จะรักษาfoo/เหมือนมากกว่าเช่นfoo/. fooหากคุณกำลังทำหน้าที่ในรายการไดเรกทอรีนั้นfoo/ควรจะเทียบเท่ากับfoo/.ไม่ใช่foo; สิ่งนี้สร้างความแตกต่างเมื่อfooเป็นลิงก์สัญลักษณ์ไปยังไดเรกทอรี: fooหมายถึงลิงค์สัญลักษณ์foo/หมายถึงไดเรกทอรีเป้าหมาย ในกรณีดังกล่าว basename ของเส้นทางที่มีเครื่องหมายทับต่อท้ายจะได้รับการพิจารณาเป็น.พิเศษและเส้นทางนั้นอาจเป็นชื่อของมันเอง

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

วิธีที่รวดเร็วและเชื่อถือได้คือการใช้ zsh กับตัวดัดแปลงประวัติ (แถบแรกจะใช้เครื่องหมายทับต่อท้ายเช่นอรรถประโยชน์):

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

less นอกจากว่าคุณกำลังใช้เชลล์ pre-POSIX เช่น Solaris 10 และรุ่นเก่ากว่า/bin/sh(ซึ่งขาดคุณสมบัติการจัดการสตริงการขยายสตริงบนเครื่องที่ยังคงใช้งานอยู่ - แต่มี POSIX เชลล์ที่เรียกว่าshในการติดตั้ง/usr/xpg4/bin/shเสมอไม่ใช่/bin/sh)
² ตัวอย่าง: ส่งไฟล์ที่เรียกfoo␤ไปยังบริการอัปโหลดไฟล์ที่ไม่ได้ป้องกันสิ่งนี้จากนั้นให้ลบและทำให้fooถูกลบแทน


ว้าว. ดังนั้นดูเหมือน (ใน POSIX เชลล์ใด ๆ ) วิธีที่แข็งแกร่งที่สุดคืออันที่สองที่คุณพูดถึง? base=$(basename -- "$filename"; echo .); base=${base%.}; dir=$(dirname -- "$filename"; echo .); dir=${dir%.}? ฉันอ่านอย่างระมัดระวังและฉันไม่ได้สังเกตว่าคุณพูดถึงข้อเสียใด ๆ
สัญลักษณ์แทน

1
@Wildcard ข้อเสียเปรียบก็คือมันปฏิบัติfoo/เหมือนfooไม่ชอบfoo/.ซึ่งไม่สอดคล้องกับยูทิลิตี้ที่สอดคล้องกับ POSIX
Gilles 'หยุดความชั่วร้าย'

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

"เช่นfindผลลัพธ์ซึ่งมักจะมีส่วนไดเรกทอรีและไม่มีการต่อท้าย/" ไม่เป็นความจริงfind ./จะส่งออก./เป็นผลลัพธ์แรก
Tavian Barnes

@Gilles ตัวอย่างอักขระขึ้นบรรทัดใหม่แค่ทำให้ใจฉันสั่น ขอบคุณสำหรับคำตอบ
Sam Thomas

10

ทั้งสองอยู่ใน POSIX ดังนั้นความสะดวกในการพกพา "ควร" ไม่เกี่ยวข้อง การทดแทนเชลล์ควรสันนิษฐานว่ารันได้เร็วขึ้น

อย่างไรก็ตาม - มันขึ้นอยู่กับสิ่งที่คุณหมายถึงโดยพกพา ระบบเก่าบางระบบ (ไม่จำเป็น) ไม่ได้ใช้คุณสมบัติเหล่านั้นใน/bin/sh(Solaris 10 และรุ่นเก่ากว่าจะนึกถึง) ในขณะที่ในทางกลับกันผู้พัฒนาได้รับการเตือนว่าdirnameไม่พกพาbasenameได้

สำหรับการอ้างอิง:

ในการพิจารณาการพกพาผมจะต้องนำเข้าบัญชีทั้งหมดของระบบที่ผมรักษาโปรแกรม ไม่ใช่ POSIX ทั้งหมดจึงมีการแลกเปลี่ยน การแลกเปลี่ยนของคุณอาจแตกต่างกัน


7

นอกจากนี้ยังมี:

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

0

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

ยังคง ... และเช่นเดียวกัน${pathname##*/} != basename ${pathname%/*} != dirnameคำสั่งเหล่านั้นมีการระบุไว้เพื่อดำเนินการส่วนใหญ่ตามลำดับขั้นตอนกำหนดไว้อย่างดีเพื่อให้ได้ผลลัพธ์ที่ระบุ

สเป็คอยู่ด้านล่าง แต่ก่อนอื่นนี่เป็นรุ่น 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

นั่นเป็น POSIX ที่ใช้งานbasenameง่ายshเรียบง่ายมันไม่ยากที่จะทำ ฉันรวมสาขาสองสาขาที่ฉันใช้ด้านล่างนี้เพราะฉันสามารถทำได้โดยไม่กระทบต่อผลลัพธ์

นี่คือข้อมูลจำเพาะ:

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.

... บางทีความคิดเห็นอาจทำให้เสียสมาธิ ....


1
ว้าวจุดที่ดีเกี่ยวกับการขึ้นบรรทัดใหม่ในชื่อไฟล์ เวิร์มสามารถเป็นอะไรได้บ้าง ฉันไม่คิดว่าฉันเข้าใจสคริปต์ของคุณจริงๆ ฉันไม่เคยเห็น[!/]มาก่อนเป็นอย่างนั้น[^/]เหรอ? แต่ความคิดเห็นของคุณควบคู่ไปกับที่ดูเหมือนจะไม่ตรงกับมัน ....
ไวลด์การ์ด

1
@ Wildcard - ดี .. มันไม่ใช่ความคิดเห็นของฉัน นั่นเป็นมาตรฐาน POSIX spec for basenameเป็นชุดคำสั่งเกี่ยวกับวิธีการทำกับเชลล์ของคุณ แต่[!charclass]วิธีพกพาที่ทำด้วย globs [^class]นั้นมีไว้สำหรับ regex และเชลล์ไม่ได้ระบุไว้สำหรับ regex เกี่ยวกับการจับคู่การแสดงความคิดเห็น ... caseฟิลเตอร์ดังนั้นถ้าฉันตรงกับสตริงที่มีเครื่องหมายทับต่อท้าย/ และ!/แล้วถ้ารูปแบบกรณีต่อไปด้านล่างที่ตรงกันใด ๆ ต่อท้าย/ทับที่พวกเขาทั้งหมดจะไม่สามารถอยู่ทั้งหมดทับ และด้านล่างที่ไม่สามารถมีส่วนท้าย /
mikeserv

2

คุณสามารถรับการสนับสนุนจากในกระบวนการbasenameและdirname(ฉันไม่เข้าใจว่าทำไมสิ่งเหล่านี้ถึงไม่สร้างขึ้น - หากสิ่งเหล่านี้ไม่ใช่ผู้สมัครฉันไม่รู้ว่ามันคืออะไร) แต่การนำไปปฏิบัติต้องจัดการสิ่งต่าง ๆ เช่น:

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

^ จากbasename (3)

และกรณีขอบอื่น ๆ

ฉันใช้:

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"
}

(การนำไปใช้งานล่าสุดของ GNU basenameและdirnameเพิ่มสวิตช์บรรทัดคำสั่งแฟนซีพิเศษสำหรับสิ่งต่าง ๆ เช่นการจัดการอาร์กิวเมนต์หลายตัวหรือการถอดต่อท้าย แต่นั่นง่ายที่จะเพิ่มลงในเชลล์)

ไม่ใช่เรื่องยากที่จะทำให้สิ่งเหล่านี้เป็นแบบbashบิวด์อิน (โดยการใช้ประโยชน์จากการใช้งานระบบพื้นฐาน) แต่ฟังก์ชั่นด้านบนไม่จำเป็นต้องรวบรวม


รายการเคสขอบมีประโยชน์มากจริง ๆ นั่นเป็นจุดที่ดีมาก รายการดูเหมือนจะค่อนข้างสมบูรณ์จริง ๆ ; มีกรณีขอบอื่น ๆ จริง ๆ หรือไม่?
สัญลักษณ์แทน

การใช้งานในอดีตของฉันไม่ได้จัดการอะไรเช่น x//อย่างถูกต้อง แต่ฉันได้แก้ไขให้คุณก่อนที่จะตอบ ฉันหวังว่ามันจะ
PSkocik

คุณสามารถเรียกใช้สคริปต์เพื่อเปรียบเทียบสิ่งที่ฟังก์ชั่นและและไฟล์ที่เรียกทำงานทำบนตัวอย่างเหล่านี้ ฉันได้รับการแข่งขัน 100%
PSkocik

1
ฟังก์ชัน dirname ของคุณดูเหมือนจะไม่เกิดการทับซ้ำ ๆ ตัวอย่างเช่น:dirname a///b//c//d////ea///b//c//d///อัตราผลตอบแทน
codeforester
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.