상위 디렉터리도 목록에 있는 경우 목록에서 경로를 제거합니다.

상위 디렉터리도 목록에 있는 경우 목록에서 경로를 제거합니다.

내 제목 표현이 약간 이상할 수 있으므로 내 상황은 다음과 같습니다.

/a/b
/a/b/c
/a/b/c/d
/a/e/f/g/h
/a/e/f/g/h/i/j/k/l
/a/e/f/g/m/n/o
/a/e/f/g/m/n/p

목록에 이미 존재하는 항목의 하위 경로인 모든 줄을 필터링하고 싶습니다.

/a/b
/a/e/f/g/h
/a/e/f/g/m/n/o
/a/e/f/g/m/n/p

디렉토리 경로는 에서 가져오므 find하향식 순서로 안정적으로 정렬되어야 합니다. 배열이나 여러 줄 문자열로 구문 분석하는 솔루션은 모두 환영합니다.

답변1

나는 경로 이름 목록이 정렬되지 않을 수 있고 결과 경로 이름 목록이 입력과 동일한 순서여야 한다고 가정합니다. 또한 경로 이름에 개행 문자가 포함되어 있지 않다고 가정합니다.

사용 /bin/sh:

#!/bin/sh

set --
while IFS= read -r pathname; do
        for p do
                case $pathname in ("$p"/*) continue 2 ;; esac
        done

        set -- "$@" "$pathname"
done <list

printf '%s\n' "$@"

그러면 파일에서 list한 번에 한 줄씩 경로 이름을 읽습니다. 허용된 경로 이름(처음에는 빈 목록)은 내부 루프에서 한 번에 하나씩 읽은 각 경로 이름에 대해 테스트됩니다. 허용된 경로 이름이 현재 경로 이름의 디렉토리 경로 접두어인 경우 현재 경로 이름은 삭제됩니다(내부 루프는 외부 루프의 다음 반복으로 점프를 사용합니다 continue 2). 현재 경로 이름인 경로 이름을 허용하는 디렉터리 경로 접두어가 발견되지 않으면 현재 경로 이름이 허용됩니다.

허용되는 경로 이름 목록은 위치 매개변수에 보관됩니다.

쉘은 bash분명히 위의 스크립트를 실행할 수 있지만 해당 쉘을 위해 특별히 작성된 것을 원한다면 다음과 같이 말할 수 있습니다.

#!/bin/bash

accepted=()
while IFS= read -r pathname; do
        for p in "${accepted[@]}"; do
                [[ $pathname == "$p"/* ]] && continue 2
        done

        accepted+=("$pathname")
done <list

printf '%s\n' "${accepted[@]}"

awk위와 동일한 방법을 사용하십시오 .

$ awk '{ for (i=1; i<=n; ++i) if (index($0, accepted[i] "/") == 1) next; accepted[++n]=$0 } END { for (i=1; i<=n; ++i) print accepted[i] }' list
/a/b
/a/e/f/g/h
/a/e/f/g/m/n/o
/a/e/f/g/m/n/p

코드가 awk개선되었습니다.

{
        for (i = 1; i <= n; ++i)
                if (index($0, accepted[i] "/") == 1)
                        next

        accepted[++n] = $0
}

END {
        for (i = 1; i <= n; ++i)
                print accepted[i]
}

awk이 프로그램과 쉘 코드 변형 사이의 명백한 유사점을 처음부터 바로 볼 수 있어야 합니다 .

index()이는 허용된 경로 이름이 현재 경로 이름의 접두사인지 테스트 하는 데 사용됩니다 . 이것을 사용할 수도 있지만 if ($0 ~ "^" acceped[i] "/")이 방법의 단점은 경로 이름 자체가 정규식의 일부로 사용된다는 것입니다. 경로 이름에 와 같은 문자가 포함되면 이는 .중요해 집니다 *.

답변2

내가 정확하게 기억한다면, 정규화된(*) 목록 또는 적어도 일관성 있게 렌더링된 경로는 일반적인 사전순으로 정렬되며, 디렉터리의 하위 디렉터리는 해당 디렉터리 바로 뒤에(재귀적으로) 나타납니다. 따라서 이전 행(삭제되지 않은 행)만 살펴보는 것으로 충분합니다.

(* 정규화란 /foo/baror 를 의미합니다. /foo/bar/예를 들어 /foo/asdf/../baror /foo///bar//. 의 출력 find은 정규화되지 않은 시작 디렉토리가 주어지면 정규화되지 않은 출력을 제공하지만 출력은 적어도 일관성이 있기 때문에 문제가 되지 않습니다.)

/foo경로는 및 와 같이 부모가 아닌 형제 경로인 동시에 다른 경로의 접두사가 될 수 있습니다 /foobar. 이 상황을 처리하기 위해 아직 슬래시가 없는 경우 각 줄에 후행 슬래시를 추가할 수 있습니다.

따라서 ( 테스트에 /foo및를 추가하고 코드를 작성하려고 하지 않음):/foobar

$ sort paths.txt | awk '! /\/$/ { $0 = $0 "/" } 
                        last && last == substr($0, 1, length(last)) { next; } 
                        { last = $0; sub(/\/$/, "", $0); print }' 
/a/b
/a/e/f/g/h
/a/e/f/g/m/n/o
/a/e/f/g/m/n/p
/foo
/foobar

$0첫 번째 줄은 필요한 경우 현재 줄에 슬래시를 추가합니다. 두 번째 줄은 마지막으로 저장된 줄(있는 last경우)과 비교하고 세 번째 줄은 삭제되지 않은 모든 줄을 저장하고 인쇄합니다. 슬래시가 제거됩니다. ( sub(...)보존하려면 삭제하세요.)

답변3

짧은해결책:

<infile sort -u |awk 'NR==1 || index($0, pre"/")!=1{print; pre=$0}'

답변4

GNU sed확장된 정규식 패턴을 사용하십시오 -E. 하위 집합이 없는 이전 행은 예약된 공간에 저장됩니다.

< file sort \
| sed -En '
    G
    /^([^\n]+)\/.*\n\1$/d
    s/\n.*//p;h
'

< file sort \
| perl -lne '
    $prev //= $_;
    print($prev = $_)
       if index($_, "$prev/");
'

POSIX sed 허용되지 않으므로 [^\n]POSIX 호환 구조를 사용하여 다시 작성합니다.

< file sort \
| sed -e '
    H;x
    \|^\(..*\)\n\1/|{
      s/\n.*//;h;d
    }
    g
'

관련 정보