経路指定を最長の共通プレフィックス+サフィックスに分解する

経路指定を最長の共通プレフィックス+サフィックスに分解する

2つのUnix絶対パスが与えられた場合メガネ図1に示すように、各仕様は、最も長い共通接頭辞と特定の接尾辞の連結に分解することができます。例えば、

/abc/bcd/cdf     -> /abc/bcd + cdf
/abc/bcd/chi/hij -> /abc/bcd + chi/hij

そのような分解を計算するUnixユーティリティはありますか? (最も長い共通プレフィックス計算と相対パス計算のための別々のユーティリティがある場合は、「またはユーティリティ」を追加しました。)

(これらのユーティリティを書くことはあまり難しくありませんが、可能であればカスタムツールよりもやや標準的なツールを優先しようとします。)

1指定されたファイルシステムで(パス)の存在、リンクなどの問題を回避するために、「パス」の代わりに「パスの指定」を書き込みます

答え1

以下を使用して、行リストの最も長い共通の先行部分文字列を計算できます。

sed -e '1{h;d;}' -e 'G;s,\(.*\).*\n\1.*,\1,;h;$!d'

たとえば、

/abc/bcd/cdf
/abc/bcd/cdf/foo
/abc/bcd/chi/hij
/abc/bcd/cdd

返品:

/abc/bcd/c

パスコンポーネントに制限するには、次の手順を実行します。

sed -e 's,$,/,;1{h;d;}' -e 'G;s,\(.*/\).*\n\1.*,\1,;h;$!d;s,/$,,'

/abc/bcd上記の例に戻ります)。

答え2

シェルループでこれを行うことができます。以下のコードは、スラッシュが追加されたあらゆる種類の奇妙なパスに対して機能します。すべてのパスがこの形式の場合は、より/foo/bar簡単なものを使用できます。

split_common_prefix () {
  path1=$1
  path2=$2
  common_prefix=
  ## Handle initial // specially
  case $path1 in
    //[!/]*) case $path2 in
               //[!/]*) common_prefix=/ path1=${path1#/} path2=${path2#/};;
               *) return;;
             esac;;
    /*) case $path2 in
          /*) :;;
          *) return;;
        esac;;
    *) case $path2 in /*) return;; esac;;
  esac
  ## Normalize multiple slashes
  trailing_slash1= trailing_slash2=
  case $path1 in */) trailing_slash1=/;; esac
  case $path2 in */) trailing_slash2=/;; esac
  path1=$(printf %s/ "$path1" | tr -s / /)
  path2=$(printf %s/ "$path2" | tr -s / /)
  if [ -z "$trailing_slash1" ]; then path1=${path1%/}; fi
  if [ -z "$trailing_slash2" ]; then path2=${path2%/}; fi
  ## Handle the complete prefix case (faster, necessary for equality and
  ## for some cases with trailing slashes)
  case $path1 in
    "$path2")
      common_prefix=$path1; path1= path2=
      return;;
    "$path2"/*)
      common_prefix=$path2; path1=${path1#$common_prefix} path2=
      return;;
  esac
  case $path2 in
    "$path1"/*)
      common_prefix=$path1; path1= path2=${path2#$common_prefix}
      return;;
  esac
  ## Handle the generic case
  while prefix1=${path1%%/*} prefix2=${path2%%/*}
        [ "$prefix1" = "$prefix2" ]
  do
    common_prefix=$common_prefix$prefix1/
    path1=${path1#$prefix1/} path2=${path2#$prefix1/}
  done
}

または、2つの文字列の最も長い共通接頭辞の決定そしてこれを最後の文字まで切り捨てます/(共通プレフィックスがスラッシュでのみ構成されていない限り)。

答え3

私が知っている限り、そのようなツールはありません。ただし、最も長いコンポーネントのグループを決定する必要があるため、これらのプログラムを簡単に作成できます。

「一行」の例:

echo /abc/bcd/cdf | awk -vpath=/abc/bcd/chi/hij -F/ '{ OFS="\n";len=0; split(path, components); for (i=1; i<=NF; i++) if($i == components[i])len+=1+length($i);else break;print substr($0, 1, len - 1), substr($0, len + 1), substr(path, len + 1);exit;}

注釈付き形式のバージョン:

$ cat longest-path.awk
#!/usr/bin/awk -f
BEGIN {
    FS="/";   # split by slash
}
{
    len=0;                      # initially the longest path has length 1
    split(path, components);    # split by directory separator (slash)
    for (i=1; i<=NF; i++) {     # loop through all path components
        if ($i == components[i]) {
            len += 1 + length($i);
        } else {
            break;              # if there is a mismatch, terminate
        }
    }
    print substr($0, 1, len - 1);  # longest prefix minus slash
    print substr($0, len + 1);     # remainder stdin
    print substr(path, len + 1);   # remainder path
    exit;                          # only the first line is compared
}
$ echo  /abc/bcd/cdf | ./longest-path.awk -vpath=/abc/bcd/chi/hij
/abc/bcd
cdf
chi/hij

答え4

Stéphane Chazelasがsedベースのソリューションを実演しました。少し違う内容に触れましたackのsed式この質問に答えるために、以下にカスタマイズしました。具体的には、ルートコンポーネントに制限し、ルートコンポーネントで改行の可能性を処理しました。次に、これを使用してパス仕様を次のように分解する方法を示します。最も長い共通ブートパスコンポーネント+残りのパスコンポーネント

私たちは始めます。ackのsed式(私はERE構文に切り替えました。①

sed -E '$!{N;s/^(.*).*\n\1.*$/\1\n\1/;D;}' <<"EOF'
/abc/bcd/cdf
/abc/bcd/cdf/foo
/abc/bcd/chi/hij
/abc/bcd/cdd
EOF

/abc/bcd/c予想通りだ。 ✔️

パスコンポーネントに制限するには、次の手順を実行します。

sed -E '$!{N;s|^(.*/).*\n\1.*$|\1\n\1|;D;};s|/$||' <<'EOF'
/abc/bcd/cdf
/abc/bcd/cdf/foo
/abc/bcd/chi/hij
/abc/bcd/cdd
EOF

/abc/bcd予想通りだ。 ✔️

改行によるパスコンポーネントの処理

テスト目的で次のパス指定配列を使用します。

a=(
  $'/a\n/b/\nc  d\n/\n\ne/f'
  $'/a\n/b/\nc  d\n/\ne/f'
  $'/a\n/b/\nc  d\n/\ne\n/f'
  $'/a\n/b/\nc  d\n/\nef'
)

チェックにより、最も長い共通ブートパスコンポーネントが次のようになることがわかります。

$'/a\n/b/\nc  d\n'

これは次のように計算され、変数としてキャプチャできます。

longest_common_leading_path_component=$(
  printf '%s\0' "${a[@]}" \
    | sed -zE '$!{N;s|^(.*/).*\x00\1.*$|\1\x00\1|;D;};s|/$||' \
    | tr \\0 x # replace trailing NUL with a dummy character ②
)
# Remove the dummy character
longest_common_leading_path_component=${longest_common_leading_path_component%x} 
# Inspect result
echo "${longest_common_leading_path_component@Q}" # ③

結果:

$'/a\n/b/\nc  d\n'

予想通り。 ✔️


テストケースを続けて、パス仕様を次に分解する方法を示します。最も長い共通ブートパスコンポーネント+残りのパスコンポーネント次の内容があります。

for e in "${a[@]}"; do
  remainder=${e#"$longest_common_leading_path_component/"}
  printf '%-26s -> %s + %s\n' \
    "${e@Q}" \
    "${longest_common_leading_path_component@Q}" \
    "${remainder@Q}"
done

結果:

$'/a\n/b/\nc  d\n/\n\ne/f' -> $'/a\n/b/\nc  d\n' + $'\n\ne/f'
$'/a\n/b/\nc  d\n/\ne/f'   -> $'/a\n/b/\nc  d\n' + $'\ne/f'
$'/a\n/b/\nc  d\n/\ne\n/f' -> $'/a\n/b/\nc  d\n' + $'\ne\n/f'
$'/a\n/b/\nc  d\n/\nef'    -> $'/a\n/b/\nc  d\n' + $'\nef'

①私はいつも-Esedとgrepにオプションを追加して切り替えます。むしろawk、bash、perl、javascript、javaなど、私が使用している他のツール/言語とよりよく一致するように構文を使用します。

②このコマンド置換で末尾の改行を維持するために、以下を使用します。一般的に使用される技術その後に切り捨てられたダミーキャラクターを追加します。x1つのステップで、末尾のNUL削除と(選択した)ダミー文字の追加を組み合わせますtr \\0 x

${parameter@Q}拡張結果は、「入力で繰り返し使用できる形式で引用されたパラメータ値である文字列」です。 –バッシュリファレンスマニュアル。バッシュ4.4+が必要(議論する)。それ以外の場合は、次のいずれかの方法で結果を確認できます。

printf '%q' "$longest_common_leading_path_component"
printf '%s' "$longest_common_leading_path_component" | od -An -tc
od -An -tc < <(printf %s "$longest_common_leading_path_component")
od -An -tc <<<$longest_common_leading_path_component # ④

④ ここで文字列は改行を追加することに注意してください(議論する)。

関連情報