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

가장 중요한 경우는 슬래시가있는 경우입니다 (모든 슬래시 인 루트 디렉토리의 경우 포함). basenamedirname그들의 일을하기 전에 명령이 슬래쉬를 벗겨. 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/의미하고 대상 디렉토리를 의미합니다. 이 경우, 슬래시가있는 경로의 기본 이름은 유리 .하며 경로는 고유 한 이름 일 수 있습니다.

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

빠르고 안정적인 방법은 zsh를 기록 수정 자 와 함께 사용하는 것입니다 (먼저 유틸리티와 같이 후행 슬래시를 제거합니다).

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

¹ 당신이 솔라리스 10 세의 같은 사전 POSIX 쉘을 사용하지 않는 /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 'SO- 악마 그만해'

감사합니다 나는 디렉토리를 다루려고 하는지 알기 때문에 내가 그 방법을 선호한다고 생각 /한다. 필요할 경우 후행을 추적 할 수있다.
와일드 카드

"예 : find항상 디렉토리 부분을 포함하고 후행이없는 결과 /"사실이 아닌 경우 첫 번째 결과로 find ./출력 ./됩니다.
타비 안 반즈

@Gilles 개행 문자 예제가 방금 떠 올랐습니다. 답변 주셔서 감사합니다
Sam Thomas

10

둘 다 POSIX에 있으므로 이식성에 대해서는 걱정할 필요가 없습니다. 쉘 대체는 더 빨리 실행되는 것으로 가정해야합니다.

그러나 그것은 휴대용의 의미에 달려 있습니다. (필수적이지 않은) 일부 오래된 시스템은 /bin/sh(Solaris 10 및 이전 버전을 염두에두고) 이러한 기능을 구현하지 않았지만 , 반면에 개발자는 이전 dirname과 같이 이식성 이 떨어 졌다고 경고했습니다 basename.

참고로 :

이식성을 고려할 때 프로그램을 유지 관리하는 모든 시스템 을 고려해야 합니다 . POSIX가 전부는 아니므로 장단점이 있습니다. 장단점이 다를 수 있습니다.


7

도 있습니다:

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

0

해석과 파싱이 많고 두 프로세스가 이야기 할 때 필요한 나머지가 있기 때문에 그런 이상한 일이 발생합니다. 명령 대체는 후행 줄 바꿈을 제거합니다. 그리고 NUL (여기서는 분명히 관련이 없지만) . basenamedirname 또한 당신이 그들에게 얼마나 다른 이야기 않기 때문에 어떤 경우에 줄 바꿈을 후행 제거 할 것인가? 파일 이름에서 줄 바꿈 문자는 어쨌든 일종의 혐오이지만, 당신은 결코 알지 못합니다. 그리고 당신이 달리 할 수있을 때 결함이있는 길을가는 것은 합리적이지 않습니다.

여전히 ... ${pathname##*/} != basename그리고 마찬가지로 ${pathname%/*} != dirname. 이러한 명령은 지정된 결과에 도달하기 위해 대부분 잘 정의 된 단계 시퀀스를 수행하도록 지정됩니다.

사양은 다음과 같지만 먼저 다음은 더 정확한 버전입니다.

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
파일 이름에서 줄 바꿈을 끝내는 것에 대한 좋은 지적입니다. 벌레의 깡통. 그래도 나는 당신의 대본을 정말로 이해한다고 생각하지 않습니다. 나는 [!/]전에 본 적이 없다 [^/]. 그러나 귀하의 의견과 일치하지 않는 것 같습니다 ....
Wildcard

1
@Wildcard-음 .. 내 의견이 아닙니다. 이것이 표준 입니다. POSIX 사양 basename은 셸을 사용하여 수행하는 방법에 대한 지침입니다. 그러나 [!charclass]globs를 사용하여 이식 할 수있는 휴대용 방법 [^class]은 정규식이며 쉘은 정규식으로 지정되지 않습니다. (가) 주석을 일치하는 정보 ... case필터, 그래서 나는 후행 슬래시가 포함 된 문자열과 일치하는 경우 / !/ 다음 경우 다음의 경우 패턴 아래 경기 후행 /전혀 슬래시 그들은 단지가 될 수 있는 모든 슬래시. 그리고 그중 하나에는 후행이 없습니다 /
mikeserv

2

당신의 프로세스에서 부스트를 얻을 수 basenamedirname(이러한 내장 명령하지 않은 이유를 이해하지 않는 -이없는 후보를하면, 나는 무엇인지 모른다) 그러나 핸들 것들 구현의 요구와 같은 :

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

^ 기본 이름에서 (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 구현 basenamedirname여러 인수 처리 또는 접미사 제거와 같은 특수 명령 줄 스위치를 추가했지만 셸에 추가하는 것은 매우 쉽습니다.)

bash기본 시스템 구현을 사용 하여 내장형 으로 만드는 것은 어렵지 않지만 위의 함수는 컴파일 할 필요가 없으며 약간의 향상도 제공합니다.


에지 사례 목록은 실제로 매우 유용합니다. 이것들은 모두 매우 좋은 포인트입니다. 이 목록은 실제로 상당히 완성 된 것 같습니다. 실제로 다른 엣지 케이스가 있습니까?
와일드 카드

내 이전 구현은 x//올바르게 처리하지 못했지만 대답하기 전에 해결했습니다. 그게 좋기를 바랍니다.
PSkocik

이 예제에서 기능과 실행 파일이 수행하는 작업을 비교하기 위해 스크립트를 실행할 수 있습니다. 100 % 일치합니다.
PSkocik

1
dirname 함수는 반복되는 슬래시를 제거하지 않는 것 같습니다. 예를 들어 dirname a///b//c//d////eyields a///b//c//d///입니다.
codeforester
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.