ファイルのパス名の配列をベース名でソートします


8

配列に保存されているファイルのパス名のリストがあるとします

filearray=("dir1/0010.pdf" "dir2/0003.pdf" "dir3/0040.pdf" ) 

ファイル名のベース名に従って配列の要素を番号順に並べ替えたい

sortedfilearray=("dir2/0003.pdf" "dir1/0010.pdf" "dir3/0040.pdf") 

どうやってやるの?

私はそれらのベースネーム部分のみをソートできます:

basenames=()
for file in "${filearray[@]}"
do
    filename=${file##*/}
    basenames+=(${filename%.*})
done
sortedbasenamearr=($(printf '%s\n' "${basenames[@]}" | sort -n))

私は考えています

  • キーがベース名で値がパス名である連想配列を作成するため、パス名へのアクセスは常にベース名を介して行われます。
  • ベース名のみの別の配列を作成sortし、ベース名配列に適用します。

ありがとう。


1
それは良い考えではありません 、bashで並べ替える
ジェフシャラー

dir1 / 42.pdfとdir2 / 42.pdfがある場合は、ベース名をキーとする配列に注意してください
ジェフシャーラー

私の場合、それ(同じベース名を持つ異なるパス名)は起こりません。しかし、bashスクリプトがそれを処理できれば、それは素晴らしいことです。同じベース名でパス名をソートする方法について、適度な要件はありません。おそらく他の誰かがそうするかもしれません。dir1 dir2単に構成されており、実際には任意のパス名です。
Tim

回答:


4

kshまたはzshとは異なり、bashには、配列または任意の文字列のリストをソートするための組み込みサポートがありません。それはグロブかの出力をソートすることができaliasたりsetまたはtypeset(これらの最後の3は、ユーザーのロケールのソート順序ではないが)、それは事実上、ここで使用することはできません。

文字列の任意のリストを簡単に並べ替えることができるPOSIXツールチェストには何もありません¹(sort行を並べ替えるので、NULおよび改行以外の短い文字列(LINE_MAXは多くの場合PATH_MAXより短い)だけですが、ファイルパスは他の空でないバイトシーケンスです) 0より)。

あなたがあなた自身のソートアルゴリズムを実装することができるようにしながら、awk(使用して<文字列比較演算子を)かさえbash(使用[[ < ]]中の任意のパスのために、) bash、移植性、最も簡単にはに頼るかもしれperl

を使用するとbash4.4+、次のことができます。

readarray -td '' sorted_filearray < <(perl -MFile::Basename -l0 -e '
  print for sort {basename($a) cmp basename($b)} @ARGV' -- "${filearray[@]}")

これは-のstrcmp()ような順序になります。グロブやの出力のように、ロケールの照合規則に基づく順序の場合ls、に-Mlocale引数を追加しますperl。数値ソート(GNUのようなより多くの場合sort -g、それは次のように数字をサポートして+31.2e-5そしてない千セパレータ、ではないがhexadimals)、使用<=>の代わりに、cmp(そして再び-Mlocaleのためのように表彰されるユーザの小数点以下のマークのためのsortコマンド)。

コマンドの引数の最大サイズによって制限されます。これを回避するには、perl引数を使用する代わりに、ファイルのリストを標準入力に渡すことができます。

readarray -td '' sorted_filearray < <(
  printf '%s\0' "${filearray[@]}" | perl -MFile::Basename -0le '
    chomp(@files = <STDIN>);
    print for sort {basename($a) cmp basename($b)} @files')

の古いバージョンではbashwhile IFS= read -rd ''代わりにループを使用するreadarray -d ''perl、適切に引用されたパスのリストを出力して、それをに渡すことができましたeval "array=($(perl...))"

を使用zshすると、並べ替え順序を定義できるグロブ拡張を偽造できます。

sorted_filearray=(/(e{'reply=($filearray)'}oe{'REPLY=$REPLY:t'}))

ではreply=($filearray)、私たちは実際には(最初はちょうどだったグロブ展開を強制/配列の要素であることを)。次に、ファイル名の末尾に基づいてソート順を定義します。

strcmp()様オーダー、(GNUに似た数値ソートのためにCにロケール修正sort -Vではなく、sort -n比較する際の重要な違いは1.41.23ロケールで(.例えば)小数マークである)を、追加nグロブ修飾子を。

の代わりにoe{expression}、関数を使用して次のような並べ替え順序を定義することもできます。

by_tail() REPLY=$REPLY:t

またはより高度なもの:

by_numbers_in_tail() REPLY=${(j:,:)${(s:,:)${REPLY:t}//[^0-9]/,}}

(したがってa/foo2bar3.pdf(2,3の数値)はb/bar1foo3.pdf(1,3)の後、c/baz2zzz10.pdf(2,10)の前にソートされます)、次のように使用します。

sorted_filearray=(/(e{'reply=($filearray)'}no+by_numbers_in_tail))

もちろん、本来の目的である本当のグロブに適用できます。たとえば、pdfbasename / tailでソートされた、任意のディレクトリ内のファイルのリストの場合:

pdfs=(**/*.pdf(N.oe+by_tail))

a strcmp()ベースの並べ替えが受け入れ可能で、短い文字列の場合は、文字列を16進エンコーディングに変換してから、ソートawkに渡してsortから変換し直すことができます。


優れたbashワンライナーについては、この回答を参照してください:unix.stackexchange.com/a/394166/41735
kael

9

sortGNU coreutilsでは、カスタムフィールドセパレータとキーを使用できます。/パス全体ではなく、フィールドセパレータとして設定し、2番目のフィールドに基づいて並べ替え、ベース名で並べ替えます。

printf "%s\n" "${filearray[@]}" | sort -t/ -k2 生成されます

dir2/0003.pdf
dir1/0010.pdf
dir3/0040.pdf

4
これはの標準オプションsortであり、GNU拡張ではありません。これは、パスがすべて同じ長さの場合に機能します。
クサラナンダ

同じ答えを同時に:)
MiniMax

2
これは、パスにそれぞれ1つのディレクトリが含まれている場合にのみ機能します。どうsome/long/path/0011.pdfですか?そのmanページから見る限りsort、最後のフィールドでソートするオプションはありません。
フェデリコポローニ2017

5

でソートgawkの表現(でサポートされているbashの「S readarray):

空白を含むファイル名のサンプル配列:

filearray=("dir1/name 0010.pdf" "dir2/name  0003.pdf" "dir3/name 0040.pdf")

readarray -t sortedfilearr < <(printf '%s\n' "${filearray[@]}" | awk -F'/' '
   BEGIN{PROCINFO["sorted_in"]="@val_num_asc"}
   { a[$0]=$NF }
   END{ for(i in a) print i}')

出力:

echo "${sortedfilearr[*]}"
dir2/name 0003.pdf dir1/name 0010.pdf dir3/name 0040.pdf

単一のアイテムへのアクセス:

echo "${sortedfilearr[1]}"
dir1/name 0010.pdf

これは、ファイルパスに改行文字が含まれていないことを前提としています。の値の数値による並べ替えは、@val_num_ascキーの先頭の数値部分(この例ではなし)にのみ適用され、strcmp()タイの字句比較(ロケールの並べ替え順序ではなくに基づく)にフォールバックすることに注意してください。


4
oldIFS="$IFS"; IFS=$'\n'
if [[ -o noglob ]]; then
  setglob=1; set -o noglob
else
  setglob=0
fi

sorted=( $(printf '%s\n' "${filearray[@]}" |
            awk '{ print $NF, $0 }' FS='/' OFS='/' |
            sort | cut -d'/' -f2- ) )

IFS="$oldIFS"; unset oldIFS
(( setglob == 1 )) && set +o noglob
unset setglob

名前に改行を含むファイル名を並べ替えると、sortステップで問題が発生します。

最初の列にベース名を含み、残りの列として完全なパスを含む- /区切りリストを生成しawkます。

0003.pdf/dir2/0003.pdf
0010.pdf/dir1/0010.pdf
0040.pdf/dir3/0040.pdf

これはソートされたものでありcut、最初の-で/区切られた列を削除するために使用されます。結果は新しいbash配列に変わります。


@StéphaneChazelas少し毛深いが、大丈夫...
Kusalananda

間違いなく、のようなパスの間違ったベース名を計算することに注意してください/some/dir/
ステファンChazelas

@StéphaneChazelasはい、ただしOPはファイルのパスがあると具体的に述べたので、パスの最後に適切なベース名があると仮定します。
クサラナンダ

注一般的なGNU C以外のロケールで、ことa/x.c++ b/x.c-- c/x.c++にもかかわらず、その順にソートされる-ソート前+ため-+および/の主要量がIGNOREそう比較(x.c++/a/x.c++抗してx.c--/b/x.c++第一比較xcaxcに対してxcbxc、およびのみタイの場合だろう他の量(ここで-前に来るが+)と考えられる。
ステファンChazelasを

それは上参加することで回避することができ/x/代わりに/、それはASCIIベースのシステム上のCロケールでのケースに対処しないと、a/foo後に並べ替えるしまうa/foo.txtので、例えば/ソートした後.
ステファンChazelas

4

dir1dir2は任意のパス名」であるため、単一のディレクトリ(または同じ数のディレクトリ)で構成されるパスは当てにできません。そのため、パス名の最後のスラッシュを、パス名の他の場所では発生しないスラッシュに変換する必要があります。文字@がデータに含まれていないとすると、次のようにベース名でソートできます。

cat pathnames | sed 's|\(.*\)/|\1@|' | sort -t@ -k+2 | sed 's|@|/|'

最初のsedコマンドは、各パス名の最後のスラッシュを選択したセパレータで置き換え、2番目のコマンドは変更を元に戻します。(簡単にするために、パス名は1行に1つずつ配信できると想定しています。シェル変数内にある場合は、最初に1行に1つの形式に変換してください。)


ハ!これは素晴らしい!次のように非表示文字を下塗りすることで、少し堅牢に(そして少し醜く)しましたcat pathnames | sed 's|\(.*\)/|\1'$'\4''|' | sort -t$'\4' -k+2nr | sed 's|'$'\4''|/|'。(\4ASCIIテーブルから
取得しました

@kael \4^D(control-D)です。端末で自分で入力しない限り、通常の制御文字です。つまり、この方法で使用しても安全です。
アレクシス

3

短い(やや速い)解決策:配列インデックスをファイル名に追加してソートすることにより、ソートされたインデックスに基づいてソートされたバージョンを後で作成できます。

このソリューションは、bashビルトインとsortバイナリのみを必要とし、改行\n文字を含まないすべてのファイル名でも機能します。

index=0 sortedfilearray=()
while read -r line ; do
    sortedfilearray+=("${filearray[${line##* }]}")
done <<< "$(for i in "${filearray[@]}" ; do
    echo "$(basename "$i") $((index++))"
done | sort -n)"

すべてのファイルについて、次のように追加された初期インデックスでベース名をエコーし​​ます。

0010.pdf 0
0003.pdf 1
0040.pdf 2

から送信されますsort -n

0003.pdf 1
0010.pdf 0
0040.pdf 2

その後、出力行を反復処理し、bash変数展開を使用して古いインデックスを抽出し、${line##* }この要素を新しい配列の最後に挿入します。


1
並べ替えるために各ファイルのフルネームを渡す必要がないソリューションの+1
roaima

3

これは、ファイルパス名の前にベース名を付加し、それを数値でソートしてから、文字列の先頭からベース名を取り除いてソートします。

#!/bin/bash
#
filearray=("dir1/0010.pdf" "dir2/0003.pdf" "dir3/0040.pdf" "dir4/0003.pdf")

sortarray=($(
    for file in "${filearray[@]}"
    do
        echo "$file"
    done |
        sed -r 's!^(.*)/([[:digit:]]*)(.*)$!\2 \1/\2\3!' |
        sort -t $'\t' -n |
        sed -r 's![^ ]* !!'
))

for item in "${sortarray[@]}"
do
    echo "> $item <"
done

実際の作業はsed | sort | sed構造体によって行われるため、シェル配列としてではなくパイプを介して直接渡すことができるリストにファイル名がある場合は、より効率的ですが、これで十分です。

私が最初にこの手法に出会ったのは、Perlでコーディングしたときです。その言語では、シュワルツ変換と呼ばれていました。

Bashでは、ファイルのベース名に非数値が含まれていると、コードでここに示した変換が失敗します。Perlでは、はるかに安全にコーディングできます。


ありがとう。bashの「リスト」とは何ですか?bash配列とは異なりますか?私はそれを聞いたことがありません、それは素晴らしいでしょう。はい、ファイル名を「リスト」に保存するのは良い考えです。スクリプトを実行するためのコマンドライン引数として、$@またはファイル名を取得$*
Tim

ファイルにファイル名を保存すると、外部ユーティリティが可能になりますが、たとえば改行の誤解のリスクもあります。
Jeff Schaller

『Gang of Four』の「Design Pattern」で紹介されているように、シュワルツ変換はテンプレート、戦略、...パターンなどのデザインパターンのソートに使用されますか?
Tim

@JeffSchaller幸い、数字に改行はありません。完全に汎用のファイル名セーフなコードを書いていたら、おそらくbashを使用しないでしょう。
roaima 2017

3

深さが等しいファイル名の場合。

filearray=("dir1/0010.pdf" "dir2/0003.pdf" "dir3/0040.pdf" "dir3/0014.pdf")

sorted_file_array=($(printf "%s\n" "${filearray[@]}" | sort -n -t'/' -k2))

説明

-k POS1 [、POS2] -並べ替えフィールドを指定するための推奨されるPOSIXオプション。(POS2が省略された場合、またはラインの終わり)フィールドは、POS1とPOS2との間のラインの一部から成る包括。フィールドと文字位置には、1から始まる番号が付けられています。したがって、2番目のフィールドでソートするには、「-k 2,2」を使用します。

-t SEPARATOR各行でソートキー を見つけるときに、フィールドセパレータとして文字SEPARATORを使用します。デフォルトでは、フィールドは空白以外の文字と空白文字の間の空の文字列で区切られます。

情報はその種の人から取られます。

結果の配列の印刷

printf "%s\n" "${sorted_file_array[@]}"
dir2/0003.pdf
dir1/0010.pdf
dir3/0014.pdf
dir3/0040.pdf
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.