dirname dan nama dasar vs perluasan parameter

20

Apakah ada alasan obyektif untuk lebih menyukai satu formulir daripada yang lain? Kinerja, keandalan, portabilitas?

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"

Menghasilkan:

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

(v1 menggunakan ekspansi parameter shell, v2 menggunakan binari eksternal.)

Wildcard
sumber

Jawaban:

21

Sayangnya, keduanya memiliki kebiasaan masing-masing.

Keduanya diperlukan oleh POSIX, jadi perbedaan di antara mereka bukan masalah portabilitas¹.

Cara sederhana untuk menggunakan utilitas adalah

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

Perhatikan tanda kutip ganda di sekitar substitusi variabel, seperti biasa, dan juga --setelah perintah, jika nama file dimulai dengan tanda hubung (jika tidak, perintah akan menafsirkan nama file sebagai opsi). Ini masih gagal dalam satu kasus tepi, yang jarang tetapi mungkin dipaksakan oleh pengguna yang berbahaya ²: substitusi perintah menghapus baris baru. Jadi jika nama file dipanggil foo/bar␤maka baseakan diatur ke barbukan bar␤. Solusinya adalah menambahkan karakter non-baris baru dan menghapusnya setelah penggantian perintah:

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

Dengan penggantian parameter, Anda tidak mengalami kasus tepi yang terkait dengan perluasan karakter aneh, tetapi ada sejumlah kesulitan dengan karakter garis miring. Satu hal yang bukan kasus tepi sama sekali adalah bahwa komputasi bagian direktori memerlukan kode yang berbeda untuk kasus di mana tidak ada /.

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

Kasus tepi adalah ketika ada garis miring (termasuk kasus direktori root, yang semuanya adalah garis miring). The basenamedan dirnameperintah menanggalkan garis miring sebelum mereka melakukan pekerjaan mereka. Tidak ada cara untuk menghapus garis miring trailing sekaligus jika Anda tetap menggunakan konstruksi POSIX, tetapi Anda bisa melakukannya dalam dua langkah. Anda harus mengurus kasing ketika input hanya terdiri dari garis miring.

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

Jika Anda mengetahui bahwa Anda tidak berada dalam kasus tepi (misalnya findhasil selain dari titik awal selalu berisi bagian direktori dan tidak memiliki trailing /) maka manipulasi string string ekspansi sangat mudah. Jika Anda perlu mengatasi semua kasing tepi, utilitas lebih mudah digunakan (tetapi lebih lambat).

Terkadang, Anda mungkin ingin memperlakukan foo/suka foo/.daripada suka foo. Jika Anda bertindak pada entri direktori maka foo/seharusnya setara dengan foo/., bukan foo; ini membuat perbedaan ketika footautan simbolik ke direktori: fooberarti tautan simbolik, foo/berarti direktori target. Dalam hal ini, nama dasar dari sebuah jalan dengan garis miring lebih menguntungkan ., dan jalur tersebut dapat menjadi dirname sendiri.

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

Metode cepat dan andal adalah menggunakan zsh dengan pengubah riwayatnya (strip pertama ini mengikuti garis miring, seperti utilitas):

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

¹ Kecuali jika Anda menggunakan shell pra-POSIX seperti Solaris 10 dan yang lebih lama /bin/sh(yang tidak memiliki fitur manipulasi string ekspansi parameter pada mesin yang masih dalam produksi - tetapi selalu ada shell POSIX yang dipanggil shdalam instalasi, hanya saja itu /usr/xpg4/bin/sh, bukan /bin/sh).
² Misalnya: mengirim file yang dipanggil foo␤ke layanan unggah file yang tidak melindungi dari ini, kemudian hapus dan menyebabkan foodihapus sebagai gantinya

Gilles 'SANGAT berhenti menjadi jahat'
sumber
Wow. Jadi kedengarannya seperti (di shell POSIX) cara yang paling kuat adalah yang kedua yang Anda sebutkan? base=$(basename -- "$filename"; echo .); base=${base%.}; dir=$(dirname -- "$filename"; echo .); dir=${dir%.}? Saya membaca dengan seksama dan saya tidak melihat Anda menyebutkan kekurangan.
Wildcard
1
@ Kartu Memori Kelemahannya adalah ia memperlakukan foo/suka foo, tidak suka foo/., yang tidak konsisten dengan utilitas yang sesuai dengan POSIX.
Gilles 'SO- stop being evil'
Terima kasih, terima kasih. Saya pikir saya masih lebih suka metode itu karena saya akan tahu jika saya mencoba untuk berurusan dengan direktori dan saya hanya bisa memperbaiki (atau "memperbaiki kembali") trailing /jika saya membutuhkannya.
Wildcard
"mis. suatu findhasil, yang selalu berisi bagian direktori dan tidak memiliki trailing /" Tidak sepenuhnya benar, find ./akan ditampilkan ./sebagai hasil pertama.
Tavian Barnes
@Gilles Contoh karakter baris baru hanya mengejutkan saya. Terima kasih atas jawabannya
Sam Thomas
10

Keduanya dalam POSIX, sehingga portabilitas "seharusnya" tidak menjadi perhatian. Penggantian shell harus dianggap berjalan lebih cepat.

Namun - itu tergantung pada apa yang Anda maksud dengan portabel. Beberapa sistem lama (yang tidak perlu) tidak mengimplementasikan fitur-fitur tersebut pada mereka /bin/sh(Solaris 10 dan yang lebih tua muncul di benak), sementara di sisi lain, beberapa waktu lalu, para pengembang diingatkan bahwa dirnametidak semudah portable basename.

Sebagai referensi:

Dalam mempertimbangkan portabilitas, saya harus memperhitungkan semua sistem tempat saya memelihara program. Tidak semua POSIX, jadi ada pengorbanan. Pengorbanan Anda mungkin berbeda.

Thomas Dickey
sumber
7

Ada juga:

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

0

Hal-hal aneh seperti itu terjadi karena ada banyak penafsiran dan penguraian dan sisanya yang perlu terjadi ketika dua proses berbicara. Pergantian perintah akan menghapus baris baru. Dan NUL (meskipun itu jelas tidak relevan di sini) . basenamedan dirnamejuga akan menelanjangi baris baru dalam kasus apa pun karena bagaimana lagi Anda berbicara dengan mereka? Saya tahu, membuntuti baris baru dalam nama file adalah semacam laknat, tetapi Anda tidak pernah tahu. Dan tidak masuk akal untuk menempuh jalan yang mungkin cacat ketika Anda bisa melakukan sebaliknya.

Masih ... ${pathname##*/} != basenamedan juga begitu ${pathname%/*} != dirname. Perintah-perintah tersebut ditentukan untuk melakukan urutan langkah yang sebagian besar didefinisikan dengan baik untuk sampai pada hasil yang ditentukan.

Spesifikasi di bawah, tetapi pertama-tama ini adalah versi 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

Itu sepenuhnya POSIX sepenuhnya basenamesederhana sh. Itu tidak sulit untuk dilakukan. Saya menggabungkan beberapa cabang yang saya gunakan di bawah sana karena saya bisa tanpa mempengaruhi hasil.

Berikut spesifikasinya:

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.

... mungkin komentarnya mengganggu ....

mikeserv
sumber
1
Wow, poin bagus tentang membuntuti baris baru dalam nama file. Apa yang bisa cacing. Saya pikir saya tidak begitu mengerti naskah Anda. Saya belum pernah melihat [!/], seperti itu [^/]? Tetapi komentar Anda yang sepertinya tidak cocok ....
Wildcard
1
@ Kartu Memori - yah .. itu bukan komentar saya. Itu standarnya . Spesifikasi POSIX basenameadalah seperangkat instruksi tentang cara melakukannya dengan shell Anda. Tetapi [!charclass]apakah cara portabel untuk melakukannya dengan gumpalan [^class]adalah untuk regex - dan cangkang tidak ditentukan untuk regex. Tentang pencocokan komentar ... casefilter, jadi jika saya cocok string yang berisi garis miring / dan sebuah !/kemudian jika pola kasus berikutnya di bawah pertandingan setiap Trailing /garis miring sama sekali mereka hanya bisa semua garis miring. Dan satu di bawah ini yang tidak dapat memiliki trailing /
mikeserv
2

Anda dapat memperoleh dorongan dari dalam proses basenamedan dirname(Saya tidak mengerti mengapa ini bukan bawaan - jika ini bukan kandidat, saya tidak tahu apa itu) tetapi implementasinya perlu menangani hal-hal seperti:

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

^ Dari basename (3)

dan kasus tepi lainnya.

Saya telah menggunakan:

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

(Implementasi terbaru saya dari GNU basenamedan dirnamemenambahkan beberapa saklar baris perintah mewah khusus untuk hal-hal seperti menangani beberapa argumen atau stripping suffix, tapi itu super mudah untuk ditambahkan di shell.)

Ini tidak terlalu sulit untuk membuatnya menjadi bashbuiltin baik (dengan memanfaatkan implementasi sistem yang mendasarinya), tetapi fungsi di atas tidak perlu dikompilasi, dan mereka memberikan beberapa dorongan juga.

PSkocik
sumber
Daftar kasus tepi sebenarnya sangat membantu. Itu semua adalah poin yang sangat bagus. Daftar itu tampaknya cukup lengkap; apakah benar-benar ada kasus tepi lainnya?
Wildcard
Implementasi saya sebelumnya tidak menangani hal-hal seperti x//dengan benar, tetapi saya telah memperbaiki untuk Anda sebelum menjawab. Saya harap itu saja.
PSkocik
Anda bisa menjalankan skrip untuk membandingkan apa fungsi dan yang dapat dieksekusi lakukan pada contoh ini. Saya mendapat kecocokan 100%.
PSkocik
1
Fungsi dirname Anda tampaknya tidak menghapus kemunculan berulang garis miring. Misalnya: dirname a///b//c//d////ehasil a///b//c//d///.
codeforester