親ディレクトリもリストにある場合は、リストからパスを削除します。

親ディレクトリもリストにある場合は、リストからパスを削除します。

私のタイトル表現は少し奇妙かもしれないので、私の状況は次のようになります。

/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一度に1行ずつパス名を読み込みます。許可されたパス名(最初は空のリスト)は、内部ループから一度に1つずつ読み取られた各パス名に対してテストされます。許可されたパス名が現在のパス名のディレクトリパスプレフィックスの場合、現在のパス名は削除されます(内部ループは外部ループの次の反復としてジャンプを使用します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/たとえば、or の出力は、正規化されていない起動ディレクトリが与えられた場合は正規化されてい/foo/asdf/../barない出力を提供しますが、出力は少なくとも一貫性があるため問題ありません。)/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最初の行は、必要に応じて現在の行にスラッシュを追加します。 2行目は最後に保存された行(存在するlast場合)と比較し、3行目は削除されていないすべての行を保存して印刷します。スラッシュが削除されます。 (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
'

関連情報