디렉토리 이름, 기본 이름 및 매개변수 확장

디렉토리 이름, 기본 이름 및 매개변수 확장

우리가 한 형식을 다른 형식보다 선호하는 객관적인 이유가 있습니까? 성능, 신뢰성, 휴대성?

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는 외부 바이너리를 사용합니다.)

답변1

불행히도 둘 다 고유한 특징이 있습니다.

POSIX에는 두 가지가 모두 필요하므로 둘 사이의 차이점은 이식성 문제가 아닙니다.

이러한 유틸리티를 사용하는 쉬운 방법은 다음과 같습니다.

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

평소와 같이 변수 대체 주위에 큰따옴표가 있고 --파일 이름이 대시로 시작하는 경우 명령 뒤에 큰따옴표가 있습니다(그렇지 않으면 명령이 파일 이름을 옵션으로 해석합니다). 이것은 드물지만 악의적인 사용자에 의해 강제될 수 있는 극단적인 경우에는 여전히 실패합니다. 명령 대체는 후행 줄 바꿈을 제거합니다. 따라서 파일 이름이 이라고 하면 대신 로 설정 foo/bar␤됩니다 . 해결 방법은 줄바꿈이 아닌 문자를 추가하고 명령 대체 후에 제거하는 것입니다.basebarbar␤

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

매개변수 대체를 사용하면 이상한 문자 확장과 관련된 극단적인 경우가 발생하지 않지만 슬래시 문자에는 많은 어려움이 있습니다. 전혀 엣지 케이스가 아닌 한 가지는 no 에 대해 디렉토리 부분 계산을 수행해야 한다는 것입니다 /.

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

엣지 케이스는 뒤에 슬래시가 있는 케이스입니다(모두 슬래시인 루트 케이스 포함). basenameand 명령은 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/.foofoo/foo/.foofoofoofoo/.

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

빠르고 안정적인 방법은 zsh와 해당 기능을 사용하는 것입니다.역사적 수정(유틸리티처럼 후행 슬래시를 먼저 제거하십시오):

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

1 Solaris 10 및 이전 버전과 같은 사전 POSIX 셸을 사용하지 않는 한(아직 생산 중인 시스템에는 매개변수 확장 문자열 조작 기능이 없지만 POSIX 셸은 설치 시 항상 호출되지만 /bin/sh그렇지 않습니다) .sh/usr/xpg4/bin/sh/bin/sh
²예 : foo␤파일 업로드 서비스에 파일을 제출했지만 서비스가 이 문제로부터 보호하지 못한 후 해당 파일을 삭제하고 결과가 foo삭제 됩니다.

답변2

둘 다 POSIX이므로 이식성은 "걱정할 필요가 없습니다". 더 빠르게 실행하려면 셸 교체를 고려해야 합니다.

하지만 - 휴대용이 무엇을 의미하는지에 따라 다릅니다. 일부 (불필요하게) 오래된 시스템에서는 이러한 기능을 구현하지 않았 /bin/sh으며(Solaris 10 및 이전 버전), 반면에 얼마 전까지만 해도 개발자 dirname들은 basename.

참고로:

이식성을 고려할 때 고려해야 할 사항모두나는 프로그램의 시스템을 유지 관리합니다. 모두 POSIX가 아니므로 장단점이 있습니다. 귀하의 장단점은 다를 수 있습니다.

답변3

게다가:

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

0

이와 같은 이상한 일이 발생하는 이유는 두 프로세스가 통신할 때 발생해야 하는 많은 해석, 구문 분석 및 기타 작업이 있기 때문입니다. 명령 대체는 후행 줄 바꿈을 제거합니다. 그리고 널(분명히 여기서는 관련이 없지만). basename그리고 dirname후행 개행 문자는 어떤 경우에도 제거됩니다. 왜냐하면 다른 방법으로 그들과 대화할 수 있기 때문입니다. 어쨌든 파일 이름에 줄바꿈이 따라오는 것은 짜증나는 일이지만 여러분은 결코 알지 못할 것입니다. 다른 일을 할 수 있는데 잠재적으로 결함이 있는 접근 방식을 취하는 것은 의미가 없습니다.

그래도... ${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.

...댓글이 방해가 될 수도 있습니다...

답변4

프로세스 내에서 부스팅을 얻을 수 있지만 basename( dirname이것이 내장되어 있지 않은 이유를 이해할 수 없습니다. 후보가 아닌 경우 무엇인지 모르겠습니다) 구현에서는 다음과 같은 사항을 처리해야 합니다.

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 구현에는 다중 인수 처리 또는 접미사 제거와 같은 작업을 위한 몇 가지 특수 명령줄 스위치가 basename추가되었지만 dirname이는 셸에 추가하기가 매우 쉽습니다.)

bash(기본 시스템 구현을 활용하여) 내장 함수를 만드는 것도 덜 어렵지만 위 함수는 컴파일이 필요하지 않으며 몇 가지 개선 사항을 제공합니다.

관련 정보