web-dev-qa-db-ja.com

Bashで配列をソートする方法

たとえば、Bashに配列があります。

array=(a c b f 3 5)

配列を並べ替える必要があります。ソートされた方法でコンテンツを表示するだけでなく、ソートされた要素を持つ新しい配列を取得します。新しいソートされた配列は、まったく新しい配列でも古い配列でもかまいません。

119
u32004

それほど多くのコードは必要ありません。

IFS=$'\n' sorted=($(sort <<<"${array[*]}"))
unset IFS

要素内の空白をサポートします(改行でない限り)。andはBash 3.xで機能します。

例えば。:

$ array=("a c" b f "3 5")
$ IFS=$'\n' sorted=($(sort <<<"${array[*]}")); unset IFS
$ printf "[%s]\n" "${sorted[@]}"
[3 5]
[a c]
[b]
[f]

注:@sorontarには 指摘 があり、要素に*?などのワイルドカードが含まれている場合は注意が必要です。

Sort =($(...))部分は「split and glob」演算子を使用しています。 globをオフにする必要があります:set -fまたはset -o noglobまたはshopt -op noglob、または*のような配列の要素は、ファイルのリストに展開されます。

何が起こっていますか:

その結果、次の6つのことがこの順序で行われます。

  1. IFS=$'\n'
  2. "${array[*]}"
  3. <<<
  4. sort
  5. sorted=($(...))
  6. unset IFS

まず、IFS=$'\n'

これは、2と5の結果に次のように影響する操作の重要な部分です。

与えられた:

  • "${array[*]}"は、IFSの最初の文字で区切られたすべての要素に展開されます
  • sorted=()は、IFSのすべての文字で分割して要素を作成します

IFS=$'\n'設定 区切り文字としてa new lineを使用して要素が展開され、その後各行が要素になるように作成されます。 (つまり、新しい行で分割します。)

新しい行で区切ることが重要です。なぜなら、sortがどのように動作するか(行ごとに並べ替える)からです。 onlyによる分割は重要ではありませんが、スペースまたはタブを含む要素を保持する必要があります。

IFSのデフォルト値はa spacea tab、それに続くa new lineであり、この操作には不適当です。

次に、sort <<<"${array[*]}"部分

here文字列 と呼ばれる<<<は、上記で説明したように"${array[*]}"を展開し、sortの標準入力に送ります。

この例では、sortに次の文字列が渡されます。

a c
b
f
3 5

sortsortsであるため、以下を生成します。

3 5
a c
b
f

次に、sorted=($(...))部分

コマンド置換 と呼ばれる$(...)部分は、その内容(sort <<<"${array[*]})を通常のコマンドとして実行し、結果の標準出力$(...)があった場所に行くリテラル。

この例では、これは単に次のように書くことに似ています。

sorted=(3 5
a c
b
f
)

sortedは、このリテラルを新しい行ごとに分割することで作成される配列になります。

最後に、unset IFS

これにより、IFSの値がデフォルト値にリセットされます。これは適切な方法です。

スクリプトの後半でIFSに依存するもので問題が発生しないようにするためです。 (そうでない場合は、物事をやり直したことを覚えておく必要があります。これは、複雑なスクリプトには実用的ではないかもしれません。)

171
antak

元の応答:

array=(a c b "f f" 3 5)
readarray -t sorted < <(for a in "${array[@]}"; do echo "$a"; done | sort)

出力:

$ for a in "${sorted[@]}"; do echo "$a"; done
3
5
a
b
c
f f

このバージョンは、特殊文字または空白を含む値を処理します(except改行)

readarrayはbash 4+でサポートされています。


編集 @Dimitreの提案に基づき、私はそれを次のように更新しました:

readarray -t sorted < <(printf '%s\0' "${array[@]}" | sort -z | xargs -0n1)

これには、改行文字が正しく埋め込まれたsorting要素を理解するという利点もあります。残念ながら、@ ruakhによって正しく通知されたように、これはreadarrayの代わりにreadarrayを使用するオプションがないため、NULの結果がcorrectになることを意味しませんでした通常のnewlines行区切りとして。

34
sehe

以下は、純粋なBashクイックソート実装です。

#!/bin/bash

# quicksorts positional arguments
# return is in array qsort_ret
qsort() {
   local pivot i smaller=() larger=()
   qsort_ret=()
   (($#==0)) && return 0
   pivot=$1
   shift
   for i; do
      if [[ $i < $pivot ]]; then
         smaller+=( "$i" )
      else
         larger+=( "$i" )
      fi
   done
   qsort "${smaller[@]}"
   smaller=( "${qsort_ret[@]}" )
   qsort "${larger[@]}"
   larger=( "${qsort_ret[@]}" )
   qsort_ret=( "${smaller[@]}" "$pivot" "${larger[@]}" )
}

として使用、例えば、

$ array=(a c b f 3 5)
$ qsort "${array[@]}"
$ declare -p qsort_ret
declare -a qsort_ret='([0]="3" [1]="5" [2]="a" [3]="b" [4]="c" [5]="f")'

この実装は再帰的です...したがって、反復クイックソートを次に示します。

#!/bin/bash

# quicksorts positional arguments
# return is in array qsort_ret
# Note: iterative, NOT recursive! :)
qsort() {
   (($#==0)) && return 0
   local stack=( 0 $(($#-1)) ) beg end i pivot smaller larger
   qsort_ret=("$@")
   while ((${#stack[@]})); do
      beg=${stack[0]}
      end=${stack[1]}
      stack=( "${stack[@]:2}" )
      smaller=() larger=()
      pivot=${qsort_ret[beg]}
      for ((i=beg+1;i<=end;++i)); do
         if [[ "${qsort_ret[i]}" < "$pivot" ]]; then
            smaller+=( "${qsort_ret[i]}" )
         else
            larger+=( "${qsort_ret[i]}" )
         fi
      done
      qsort_ret=( "${qsort_ret[@]:0:beg}" "${smaller[@]}" "$pivot" "${larger[@]}" "${qsort_ret[@]:end+1}" )
      if ((${#smaller[@]}>=2)); then stack+=( "$beg" "$((beg+${#smaller[@]}-1))" ); fi
      if ((${#larger[@]}>=2)); then stack+=( "$((end-${#larger[@]}+1))" "$end" ); fi
   done
}

どちらの場合でも、使用する順序を変更できます。文字列比較を使用しましたが、算術比較を使用したり、wrtファイルの変更時間を比較したりできます。適切なテストを使用するだけです。さらに汎用的にして、テスト関数で使用される最初の引数を使用することもできます。

#!/bin/bash

# quicksorts positional arguments
# return is in array qsort_ret
# Note: iterative, NOT recursive! :)
# First argument is a function name that takes two arguments and compares them
qsort() {
   (($#<=1)) && return 0
   local compare_fun=$1
   shift
   local stack=( 0 $(($#-1)) ) beg end i pivot smaller larger
   qsort_ret=("$@")
   while ((${#stack[@]})); do
      beg=${stack[0]}
      end=${stack[1]}
      stack=( "${stack[@]:2}" )
      smaller=() larger=()
      pivot=${qsort_ret[beg]}
      for ((i=beg+1;i<=end;++i)); do
         if "$compare_fun" "${qsort_ret[i]}" "$pivot"; then
            smaller+=( "${qsort_ret[i]}" )
         else
            larger+=( "${qsort_ret[i]}" )
         fi
      done
      qsort_ret=( "${qsort_ret[@]:0:beg}" "${smaller[@]}" "$pivot" "${larger[@]}" "${qsort_ret[@]:end+1}" )
      if ((${#smaller[@]}>=2)); then stack+=( "$beg" "$((beg+${#smaller[@]}-1))" ); fi
      if ((${#larger[@]}>=2)); then stack+=( "$((end-${#larger[@]}+1))" "$end" ); fi
   done
}

次に、この比較関数を使用できます。

compare_mtime() { [[ $1 -nt $2 ]]; }

そして使用:

$ qsort compare_mtime *
$ declare -p qsort_ret

現在のフォルダー内のファイルを変更時刻(最新のものから)でソートします。

注意。これらの関数は純粋なBashです!外部ユーティリティもサブシェルもありません!面白い記号(スペース、改行文字、グロブ文字など)に対して安全です。

32
gniourf_gniourf

配列要素で特殊なシェル文字を処理する必要がない場合:

array=(a c b f 3 5)
sorted=($(printf '%s\n' "${array[@]}"|sort))

bashを使用すると、外部ソートプログラムが必要になります。

zshを使用すると、外部プログラムは不要で、特殊なシェル文字を簡単に処理できます。

% array=('a a' c b f 3 5); printf '%s\n' "${(o)array[@]}" 
3
5
a a
b
c
f

kshにはset -sがあり、ソートできますASCIIbetically

26

ミュンヘンからフランクフルトへの3時間の列車旅行(オクトーバーフェストが明日から始まるので連絡が取れませんでした)で、最初の投稿を考えていました。グローバル配列を使用することは、一般的な並べ替え関数にとってはるかに優れたアイデアです。次の関数は、任意の文字列(改行、空白など)を処理します。

declare BSORT=()
function bubble_sort()
{   #
    # @param [ARGUMENTS]...
    #
    # Sort all positional arguments and store them in global array BSORT.
    # Without arguments sort this array. Return the number of iterations made.
    #
    # Bubble sorting lets the heaviest element sink to the bottom.
    #
    (($# > 0)) && BSORT=("$@")
    local j=0 ubound=$((${#BSORT[*]} - 1))
    while ((ubound > 0))
    do
        local i=0
        while ((i < ubound))
        do
            if [ "${BSORT[$i]}" \> "${BSORT[$((i + 1))]}" ]
            then
                local t="${BSORT[$i]}"
                BSORT[$i]="${BSORT[$((i + 1))]}"
                BSORT[$((i + 1))]="$t"
            fi
            ((++i))
        done
        ((++j))
        ((--ubound))
    done
    echo $j
}

bubble_sort a c b 'z y' 3 5
echo ${BSORT[@]}

これは印刷します:

3 5 a b c z y

同じ出力がから作成されます

BSORT=(a c b 'z y' 3 5) 
bubble_sort
echo ${BSORT[@]}

おそらくBashは内部的にスマートポインターを使用しているため、スワップ操作couldは安価であることに注意してください(疑いはありますが)。ただし、bubble_sortは、merge_sortなどのより高度な関数もシェル言語の範囲内にあることを示しています。

8

外部sortを使用し、any特殊文字(NULを除く:)に対処する別のソリューション。 bash-3.2およびGNUまたはBSD sortで動作するはずです(残念ながら、POSIXには-zは含まれません)。

local e new_array=()
while IFS= read -r -d '' e; do
    new_array+=( "${e}" )
done < <(printf "%s\0" "${array[@]}" | LC_ALL=C sort -z)

最後に入力のリダイレクトを見てください。 printf組み込みを使用して、ゼロで終了する配列要素を書き出します。引用は、配列要素がそのまま渡されることを確認し、Shell printfの詳細により、残りの各パラメーターに対してフォーマット文字列の最後の部分が再利用されます。つまり、次のようなものと同等です。

for e in "${array[@]}"; do
    printf "%s\0" "${e}"
done

次に、ヌル終了要素リストがsortに渡されます。 -zオプションを使用すると、NULLで終了する要素が読み取られ、ソートされ、NULLで終了する要素も出力されます。一意の要素のみを取得する必要がある場合は、-uよりも移植性が高いため、uniq -zを渡すことができます。 LC_ALL=Cは、ロケールに関係なく安定したソート順を保証します—スクリプトに役立つ場合があります。 sortにロケールを尊重させる場合は、それを削除します。

<()構造は、生成されたパイプラインから読み取る記述子を取得し、<whileループの標準入力をリダイレクトします。パイプ内の標準入力にアクセスする必要がある場合は、別の記述子を使用できます—読者のために練習してください:)。

さて、最初に戻りましょう。 readビルトインは、リダイレクトされたstdinから出力を読み取ります。空のIFSを設定すると、ここで不要なWord分割が無効になります。その結果、readは、指定された単一の変数への入力の「行」全体を読み取ります。 -rオプションは、ここでも望ましくないエスケープ処理を無効にします。最後に、-d ''は行区切り文字をNULに設定します。つまり、readにゼロで終わる文字列を読み取るように指示します。

その結果、ループは連続するゼロで終わる配列要素ごとに1回実行され、値はeに格納されます。この例では、アイテムを別の配列に配置するだけですが、それらを直接処理することもできます:)。

もちろん、それは同じ目標を達成する多くの方法の1つにすぎません。ご覧のとおり、bashで完全なソートアルゴリズムを実装するよりも簡単で、場合によっては高速になります。改行を含むすべての特殊文字を処理し、ほとんどの一般的なシステムで動作するはずです。最も重要なことは、bashについて新しくて素晴らしい何かを教えてくれるかもしれません:)。

6
Michał Górny

これを試して:

echo ${array[@]} | awk 'BEGIN{RS=" ";} {print $1}' | sort

出力は次のようになります。

 3 
 5 
 a 
 b 
 c 
 f 

問題が解決しました。

2
rsingh

最小ソート:

#!/bin/bash
array=(.....)
index_of_element1=0

while (( ${index_of_element1} < ${#array[@]} )); do

    element_1="${array[${index_of_element1}]}"

    index_of_element2=$((index_of_element1 + 1))
    index_of_min=${index_of_element1}

    min_element="${element_1}"

        for element_2 in "${array[@]:$((index_of_element1 + 1))}"; do
            min_element="`printf "%s\n%s" "${min_element}" "${element_2}" | sort | head -n+1`"      
            if [[ "${min_element}" == "${element_2}" ]]; then
                index_of_min=${index_of_element2}
            fi
            let index_of_element2++
        done

        array[${index_of_element1}]="${min_element}"
        array[${index_of_min}]="${element_1}"

    let index_of_element1++
done
2
MathQues
array=(a c b f 3 5)
new_array=($(echo "${array[@]}" | sed 's/ /\n/g' | sort))    
echo ${new_array[@]}

new_arrayのエコーコンテンツは次のようになります。

3 5 a b c f
1
blp

次のように、配列の各要素に対して一意の整数を計算できる場合:

tab='0123456789abcdefghijklmnopqrstuvwxyz'

# build the reversed ordinal map
for ((i = 0; i < ${#tab}; i++)); do
    declare -g ord_${tab:i:1}=$i
done

function sexy_int() {
    local sum=0
    local i ch ref
    for ((i = 0; i < ${#1}; i++)); do
        ch="${1:i:1}"
        ref="ord_$ch"
        (( sum += ${!ref} ))
    done
    return $sum
}

sexy_int hello
echo "hello -> $?"
sexy_int world
echo "world -> $?"

次に、これらの整数を配列インデックスとして使用できます。これは、Bashが常にスパース配列を使用するため、未使用のインデックスを心配する必要がないためです。

array=(a c b f 3 5)
for el in "${array[@]}"; do
    sexy_int "$el"
    sorted[$?]="$el"
done

echo "${sorted[@]}"
  • 長所速い。
  • 短所重複した要素はマージされ、コンテンツを32ビットの一意の整数にマッピングすることは不可能になる可能性があります。
1
Xiè Jìléi

スペースと改行の通常の問題に対する回避策があります。

元の配列にない文字($'\1'$'\4'など)を使用します。

この関数は仕事を完了させます:

# Sort an Array may have spaces or newlines with a workaround (wa=$'\4')
sortarray(){ local wa=$'\4' IFS=''
             if [[ $* =~ [$wa] ]]; then
                 echo "$0: error: array contains the workaround char" >&2
                 exit 1
             fi

             set -f; local IFS=$'\n' x nl=$'\n'
             set -- $(printf '%s\n' "${@//$nl/$wa}" | sort -n)
             for    x
             do     sorted+=("${x//$wa/$nl}")
             done
       }

これは配列をソートします:

$ array=( a b 'c d' $'e\nf' $'g\1h')
$ sortarray "${array[@]}"
$ printf '<%s>\n' "${sorted[@]}"
<a>
<b>
<c d>
<e
f>
<gh>

これにより、ソース配列に回避策の文字が含まれていると文句が言います。

$ array=( a b 'c d' $'e\nf' $'g\4h')
$ sortarray "${array[@]}"
./script: error: array contains the workaround char

説明

  • 2つのローカル変数wa(回避策文字)とnull IFSを設定します
  • 次に(ifs nullを使用して)配列全体$*をテストします。
  • Woraround char [[ $* =~ [$wa] ]]は含まれていません。
  • 存在する場合は、メッセージを生成してエラーを通知します:exit 1
  • ファイル名の展開を避ける:set -f
  • IFSの新しい値(IFS=$'\n')、ループ変数x、および改行変数(nl=$'\n')を設定します。
  • 受け取った引数のすべての値(入力配列$@)を出力します。
  • ただし、新しい行を回避策char "${@//$nl/$wa}"に置き換えます。
  • これらの値を送信してsort -nでソートします。
  • そして、ソートされたすべての値を位置引数set --に戻します。
  • 次に、各引数を1つずつ割り当てます(改行を保持するため)。
  • ループでfor x
  • 新しい配列:sorted+=(…)
  • 既存の改行を保持するための引用符内。
  • 回避策を改行"${x//$wa/$nl}"に復元します。
  • やった
0
sorontar

Bashに外部ソートプログラムが必要になるとは思いません。

これは、単純なバブルソートアルゴリズムの実装です。

function bubble_sort()
{   #
    # Sorts all positional arguments and echoes them back.
    #
    # Bubble sorting lets the heaviest (longest) element sink to the bottom.
    #
    local array=($@) max=$(($# - 1))
    while ((max > 0))
    do
        local i=0
        while ((i < max))
        do
            if [ ${array[$i]} \> ${array[$((i + 1))]} ]
            then
                local t=${array[$i]}
                array[$i]=${array[$((i + 1))]}
                array[$((i + 1))]=$t
            fi
            ((i += 1))
        done
        ((max -= 1))
    done
    echo ${array[@]}
}

array=(a c b f 3 5)
echo " input: ${array[@]}"
echo "output: $(bubble_sort ${array[@]})"

これは印刷します:

 input: a c b f 3 5
output: 3 5 a b c f
0
a=(e b 'c d')
shuf -e "${a[@]}" | sort >/tmp/f
mapfile -t g </tmp/f
0
Steven Penny