dirname ve basename ve parametre genişletmesi


20

Bir formu diğerine tercih etmek için nesnel bir neden var mı? Performans, güvenilirlik, taşınabilirlik?

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"

üretir:

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

(v1 kabuk parametresi genişletme kullanır, v2 harici ikili dosyalar kullanır.)

Yanıtlar:


21

Ne yazık ki her ikisinin de tuhaflıkları var.

Her ikisi de POSIX için gereklidir, bu nedenle aralarındaki fark bir taşınabilirlik sorunu değildir¹.

Yardımcı programları kullanmanın basit yolu

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

--Dosya adının bir tire işareti ile başlaması durumunda her zamanki gibi değişken ikameler ve komuttan sonra gelen çift tırnaklara dikkat edin (aksi takdirde komutlar dosya adını bir seçenek olarak yorumlar). Bu hala bir kenar durumunda başarısız olur, bu nadirdir, ancak kötü niyetli bir kullanıcı² tarafından zorlanabilir. Bir dosya adı denir Yani eğer foo/bar␤o zamanbase ayarlanır baryerine bar␤. Geçici bir çözüm, yeni satır olmayan bir karakter eklemek ve komutun değiştirilmesinden sonra onu kaldırmaktır:

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

Parametre ikamesi ile, garip karakterlerin genişlemesi ile ilgili son durumlarla karşılaşmazsınız, ancak eğik çizgi karakterinde bir takım zorluklar vardır. Bir uç durum olmayan bir şey, dizin parçasının hesaplanmasının, yok olduğu durumda farklı kod gerektirmesidir /.

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

Kenar durumu, bir eğik çizgi olduğunda (tüm eğik çizgiler olan kök dizinin durumu dahil). basenamevedirname onlar işlerini yapmadan önce komutlar eğik çizgileri kapalı şerit. POSIX yapılarına yapışırsanız sondaki eğik çizgileri tek seferde kesmenin bir yolu yoktur, ancak iki adımda yapabilirsiniz. Girdi eğik çizgilerden başka bir şey içermiyorsa, duruma dikkat etmeniz gerekir.

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

Uç bir durumda olmadığınızı biliyorsanız (ör. find . Başlangıç ​​noktasından başka sonuç her zaman bir dizin parçası içerir ve iz bırakmaz /), parametre genişletme dizesi manipülasyonu basittir. Tüm uç durumlarla başa çıkmanız gerekiyorsa, yardımcı programların kullanımı daha kolaydır (ancak daha yavaştır).

Bazen, foo/gibi davranmak foo/.yerine davranmak isteyebilirsiniz foo. Eğer bir dizin girişinde davranıyorsun o zaman foo/eşdeğer olması gerekiyordu foo/.değil foo; bu, bir foodizine sembolik bağlantı olduğunda fark yaratır : foosembolik bağlantı, foo/hedef dizin anlamına gelir. Bu durumda, sonunda eğik çizgi bulunan bir yolun taban adı avantajlıdır .ve yol kendi dizin adı olabilir.

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

Hızlı ve güvenilir yöntem, tarih değiştiricileri ile zsh kullanmaktır (bu, ilk olarak yardımcı programlar gibi eğik çizgileri çıkarır):

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

Solar Solaris 10 ve eskisi gibi POSIX öncesi mermileri kullanmadığınız sürece /bin/sh(hala üretimde olan makinelerde parametre genişletme dizesi manipülasyon özelliklerinden yoksundu - ancak her zaman shkurulumda çağrılan bir POSIX kabuğu var , sadece /usr/xpg4/bin/shdeğil /bin/sh).
² Örneğin: buna foo␤karşı korumasız bir dosya yükleme hizmetine çağrılan bir dosya gönderin , ardından silin ve foobunun yerine silinmesine neden olun


Vay. Yani (herhangi bir POSIX kabuğunda) kulağa en sağlam şekilde bahsettiğiniz ikinci şey mi? base=$(basename -- "$filename"; echo .); base=${base%.}; dir=$(dirname -- "$filename"; echo .); dir=${dir%.}? Dikkatle okuyordum ve herhangi bir dezavantajdan bahsettiğinizi fark etmedim.
Joker

1
@Wildcard bir dezavantajı, muamele olmasıdır foo/gibi foodeğil gibi, foo/.POSIX uyumlu programları ile değil tutarlıdır.
Gilles 'SO- kötü olmayı bırak'

Anladım, teşekkürler. Ben çünkü ben hala bu yöntemi tercih ederim biliyorum ben dizinleri ile başa çalışıyorum eğer ve sadece tack (ya da "çakmak geri") bir eğik olabilir /bunu gerekirse.
Joker

"örneğin find, her zaman bir dizin kısmı içeren ve sonda olmayan bir sonuç /" Tam olarak doğru değil , ilk sonuç olarak find ./çıktı alır ./.
Tavian Barnes

@Gilles Newline karakter örneği aklımı aldı. Cevabınız için teşekkürler
Sam Thomas

10

Her ikisi de POSIX'tedir, bu nedenle taşınabilirlik "endişe" olmamalıdır. Kabuk ikamelerinin daha hızlı çalıştığı varsayılmalıdır.

Ancak - taşınabilir ile ne demek istediğinize bağlıdır. Bazı (zorunlu olarak değil) eski sistemler bu özellikleri /bin/sh(Solaris 10 ve daha yaşlı akla gelen) uygulamadı, öte yandan, bir süre önce, geliştiriciler dirnamekadar taşınabilir olmayan uyarıldı basename.

Referans için:

Taşınabilirliği göz önünde bulundurarak, hepsini dikkate almalıyım programları sistemleri . Hepsi POSIX değil, bu yüzden ödünleşmeler var. Ödünleşmeleriniz farklı olabilir.


7

Ayrıca birde şu var:

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

0

Bunun gibi garip şeyler olur, çünkü birçok yorumlama ve ayrıştırma ve iki süreç konuşulduğunda gerçekleşmesi gereken geri kalanı vardır. Komut ikameleri, takip eden yeni satırları kaldırır. Ve NUL'lar (bu açıkça burada alakalı olmasa da) .basenameve dirnameher durumda sondaki satırları da kaldırır çünkü onlarla başka nasıl konuşuyorsunuz? Biliyorum, bir dosya adındaki yeni satırları zaten bir tür anatema, ama asla bilemezsin. Ve aksi halde yapabileceğiniz olası kusurlu yola gitmek mantıklı değil.

Yine de ... ${pathname##*/} != basenameve aynı şekilde ${pathname%/*} != dirname. Bu komutlar, belirtilen sonuçlarına ulaşmak için çoğunlukla iyi tanımlanmış bir adım dizisi gerçekleştirecek şekilde belirtilir.

Spesifikasyon aşağıda, ancak önce burada ters bir sürüm var:

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

Bu tamamen POSIX uyumlu basename basit içinde sh. Bunu yapmak zor değil. Aşağıda kullandığım birkaç dalı birleştirdim çünkü sonuçları etkilemeden yapabildim.

İşte özellikleri:

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.

... belki yorumlar dikkat dağıtıcıdır ....


1
Vay canına, dosya adlarında satır sonlarını izlemek için iyi bir nokta. Bir kutu solucan. Senaryonuzu gerçekten anladığımı sanmıyorum. Daha önce hiç görmedim [!/], öyle [^/]mi? Ancak bununla ilgili yorumunuz da buna uymuyor gibi görünüyor ....
Wildcard

1
@Wildcard - şey .. bu benim yorumum değil. Budur standart . POSIX spesifikasyonu basename, kabuğunuzla nasıl yapılacağına ilişkin bir dizi talimattır. Ancak glob [!charclass]ile bunu yapmanın taşınabilir yolu [^class]normal ifade içindir - ve kabuklar normal ifade için gerekli değildir. Yorumunu eşleşen Hakkında ... casefiltreler, bu yüzden bir bölü içeren bir dize eşleşirse / ve bir !/o takdirde bir sonraki durum deseni aşağıda maçları herhangi sondaki /hiç eğik çizgi onlar sadece olabilir tüm eğik çizgi. Ve aşağıda bir iz
bırakamaz

2

Süreçten bir destek alabilirsiniz basenameve dirname(bunların neden yerleşik olmadığını anlamıyorum - eğer bunlar aday değilse, ne olduğunu bilmiyorum) ama uygulamanın aşağıdaki gibi şeyleri işlemesi gerekir:

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

^ Basename'den (3)

ve diğer kenar durumlarda.

Ben kullanıyorum:

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

(En son GNU uygulamam basenamevedirname birden fazla argümanı ele alma veya sonek sıyırma gibi şeyler için bazı özel süslü komut satırı anahtarları ekliyor, ancak kabuk içine eklemek çok kolay.)

Bunları bashyerleşik yapılara dönüştürmek o kadar da zor değil (altta yatan sistem uygulamasını kullanarak), ancak yukarıdaki fonksiyonun derlenmesi gerekmez ve ayrıca biraz destek sağlarlar.


Uç vakaların listesi aslında çok faydalıdır. Bunların hepsi çok iyi noktalar. Liste aslında oldukça eksiksiz görünüyor; Gerçekten başka uç durumlar var mı?
Wildcard

Eski uygulamam gibi şeyleri x//doğru şekilde işlemedi , ama cevaplamadan önce sizin için düzelttim. Umarım budur.
PSkocik

Bu örneklerde işlevlerin ve yürütülebilir dosyaların ne yaptığını karşılaştırmak için bir komut dosyası çalıştırabilirsiniz. % 100 eşleşme alıyorum.
PSkocik

1
Dirname işleviniz, tekrarlanan eğik çizgi oluşumlarını kaldırmıyor. Örneğin: dirname a///b//c//d////everim a///b//c//d///.
codeforester
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.