web-dev-qa-db-ja.com

区切られたアイテムの1行を数値で並べ替えるにはどうすればよいですか?

任意の文字で区切られた1行(または複数行)の数字があります。区切り文字を保持しながら、各行の項目を数値でソートするためにどのUNIXツールを使用できますか?

例は次のとおりです。

  • 番号のリスト;入力:10 50 23 42;並べ替え:10 23 42 50
  • IPアドレス;入力:10.1.200.42;並べ替え:1.10.42.200
  • CSV;入力:1,100,330,42;並べ替え:1,42,100,330
  • パイプ区切り;入力:400|500|404;並べ替え:400|404|500

区切り文字は任意なので、任意の1文字の区切り文字を使用して回答を提供(または拡張)してください。

11
Jeff Schaller

あなたはこれを達成することができます:

tr '.' '\n' <<<"$aline" | sort -n | paste -sd'.' -

dots.を区切り文字に置き換えます。
上記のsortコマンドに -uを追加して、重複を削除します。


またはgawk[〜#〜] gnu [〜#〜]awk)を使用すると、多くの行を処理できますが、上記も拡張できます。

gawk -v SEP='*' '{ i=0; split($0, arr, SEP); 
    while ( ++i<=asort(arr) ){ printf("%s%s", i>1?SEP:"", arr[i]) }; 
        print "" 
}' infile

*のフィールド区切り文字としてのSEP='*'区切り文字に置き換えます。


メモ:
数値のクラス(整数、浮動小数点、科学、16進数など)を処理するには、-g, --general-numeric-sortではなくsort-n, --numeric-sortオプションを使用する必要がある場合があります。

$ aline='2e-18,6.01e-17,1.4,-4,0xB000,0xB001,23,-3.e+11'
$ tr ',' '\n' <<<"$aline" |sort -g | paste -sd',' -
-3.e+11,-4,2e-18,6.01e-17,1.4,23,0xB000,0xB001

awkでは変更する必要はありませんが、それらは引き続き処理されます。

12
αғsнιη

Perlを使用すると、明らかなバージョンがあります。データを分割し、並べ替え、再び結合します。

区切り文字は2回リストする必要があります(splitに1回、joinに1回)。

たとえば,

Perl -lpi -e '$_=join(",",sort {$a <=> $b} split(/,/))'

そう

echo 1,100,330,42 | Perl -lpi -e '$_=join(",",sort {$a <=> $b} split(/,/))'
1,42,100,330

splitは正規表現であるため、文字を引用する必要がある場合があります。

echo 10.1.200.42 | Perl -lpi -e '$_=join(".",sort {$a <=> $b} split(/\./))'
1.10.42.200

-aおよび-Fオプションを使用することで、分割を削除できます。前と同じように-pループを使用して、結果を$_に設定すると、自動的に印刷されます。

Perl -F'/\./' -aple '$_=join(".", sort {$a <=> $b} @F)'
10
Stephen Harris

Pythonおよび Stephen Harrisの回答)と同様のアイデアを使用してください

python3 -c 'import sys; c = sys.argv[1]; sys.stdout.writelines(map(lambda x: c.join(sorted(x.strip().split(c), key=int)) + "\n", sys.stdin))' <delmiter>

だから次のようなもの:

$ cat foo
10.129.3.4
1.1.1.1
4.3.2.1
$ python3 -c 'import sys; c = sys.argv[1]; sys.stdout.writelines(map(lambda x: c.join(sorted(x.strip().split(c), key=int)) + "\n", sys.stdin))' . < foo
3.4.10.129
1.1.1.1
1.2.3.4

悲しいことに、手動でI/Oを行う必要があるため、これはPerlバージョンよりもはるかに洗練されていません。

4
muru

シェル

より高いレベルの言語のロードには時間がかかります。
数行については、シェル自体が解決策になる場合があります。
外部コマンドsortおよびコマンドtrを使用できます。 1つは行のソートに非常に効率的で、もう1つは1つの区切り文字を改行に変換するのに効果的です。

#!/bin/bash
shsort(){
           while IFS='' read -r line; do
               echo "$line" | tr "$1" '\n' |
               sort -n   | paste -sd "$1" -
           done <<<"$2"
    }

shsort ' '    '10 50 23 42'
shsort '.'    '10.1.200.42'
shsort ','    '1,100,330,42'
shsort '|'    '400|500|404'
shsort ','    '3 b,2       x,45    f,*,8jk'
shsort '.'    '10.128.33.6
128.17.71.3
44.32.63.1'

<<<のみを使用するため、これにはbashが必要です。それがヒアドキュメントに置き換えられた場合、解決策はposixに対して有効です。
これは、タブ、スペース、またはシェルグロブ文字(*?[)でフィールドを並べ替えることができます。各行がソートされているため、改行ではありません。

ファイル名を処理して次のように呼び出すには、<<<"$2"<"$2"に変更します。

shsort '.'    infile

デリミタはファイル全体で同じです。それが制限である場合は、改善することができます。

ただし、6000行しかないファイルの処理には15秒かかります。確かに、シェルはファイルを処理するための最良のツールではありません。

Awk

数行を超える(数十行を超える)場合は、実際のプログラミング言語を使用することをお勧めします。 awkソリューションは次のとおりです。

#!/bin/bash
awksort(){
           gawk -v del="$1" '{
               split($0, fields, del)
               l=asort(fields)
               for(i=1;i<=l;i++){
                   printf( "%s%s" , (i==0)?"":del , fields[i] )
               }
               printf "\n"
           }' <"$2"
         }

awksort '.'    infile

上記と同じ6000行のファイルで0.2秒しかかかりません。

ファイルの<"$2"を、シェル変数内の行の<<<"$2"に戻すことができることを理解してください。

Perl

最速の解決策はPerlです。

#!/bin/bash
perlsort(){  Perl -lp -e '$_=join("'"$1"'",sort {$a <=> $b} split(/['"$1"']/))' <<<"$2";   }

perlsort ' '    '10 50 23 42'
perlsort '.'    '10.1.200.42'
perlsort ','    '1,100,330,42'
perlsort '|'    '400|500|404'
perlsort ','    '3 b,2       x,45    f,*,8jk'
perlsort '.'    '10.128.33.6
128.17.71.3
44.32.63.1'

ファイルを並べ替える場合は、<<<"$a"を単純に"$a"に変更し、Perlオプションに-iを追加して、ファイルエディションを「適切な場所」にします。

#!/bin/bash
perlsort(){  Perl -lpi -e '$_=join("'"$1"'",sort {$a <=> $b} split(/['"$1"']/))' "$2"; }

perlsort '.' infile; exit
3
Isaac

Bashスクリプト:

#!/usr/bin/env bash

join_by(){ local IFS="$1"; shift; echo "$*"; }

IFS="$1" read -r -a tokens_array <<< "$2"
IFS=$'\n' sorted=($(sort -n <<<"${tokens_array[*]}"))
join_by "$1" "${sorted[@]}"

例:

$ ./sort_delimited_string.sh "." "192.168.0.1"
0.1.168.192

に基づく

3

sedを使用してIPアドレスのオクテットをソートする

sedにはsort関数が組み込まれていませんが、データが範囲内で(IPアドレスなどで)十分に制約されている場合は、手動で簡単に実装するsedスクリプトを生成できます- バブルソート 。基本的なメカニズムは、順序が乱れている隣接する数値を探すことです。番号が順番になっていない場合は、入れ替えます。

sedスクリプト自体には、順不同番号のペアごとに2つの検索およびスワップコマンドが含まれています。1つはオクテットの最初の2つのペア用です(末尾の区切り文字を強制して、 3番目のオクテット)、3番目のオクテットのペアの2番目(最後はEOL)。スワップが発生すると、プログラムはスクリプトの先頭に分岐し、順序が狂っている数値を探します。それ以外の場合は終了します。

生成されたスクリプトは、部分的には次のとおりです。

$ head -n 3 generated.sed
:top
s/255\.254\./254.255./g; s/255\.254$/254.255/
s/255\.253\./253.255./g; s/255\.253$/253.255/

# ... middle of the script omitted ...

$ tail -n 4 generated.sed
s/2\.1\./1.2./g; s/2\.1$/1.2/
s/2\.0\./0.2./g; s/2\.0$/0.2/
s/1\.0\./0.1./g; s/1\.0$/0.1/
ttop

このアプローチでは、ピリオドをエスケープする必要のある区切り文字としてハードコードします。そうしないと、正規表現構文に「特別」になり、任意の文字を使用できます。

このようなsedスクリプトを生成するために、このループは次のことを行います。

#!/bin/bash

echo ':top'

for (( n = 255; n >= 0; n-- )); do
  for (( m = n - 1; m >= 0; m-- )); do
    printf '%s; %s\n' "s/$n\\.$m\\./$m.$n./g" "s/$n\\.$m\$/$m.$n/"
  done
done

echo 'ttop'

そのスクリプトの出力を別のファイル、たとえばsort-ips.sedにリダイレクトします。

サンプルの実行は次のようになります。

ip=$((RANDOM % 256)).$((RANDOM % 256)).$((RANDOM % 256)).$((RANDOM % 256))
printf '%s\n' "$ip" | sed -f sort-ips.sed

次の生成スクリプトのバリエーションでは、Word境界マーカー\<および\>を使用して、2番目の置換の必要性を取り除きます。これにより、生成されたスクリプトのサイズも1.3 MBから900 KB未満に削減され、sed自体の実行時間が大幅に短縮されます(元のsed実装が使用されています):

#!/bin/bash

echo ':top'

for (( n = 255; n >= 0; --n )); do
  for (( m = n - 1; m >= 0; --m )); do
      printf '%s\n' "s/\\<$n\\>\\.\\<$m\\>/$m.$n/g"
  done
done

echo 'ttop'
2
Jeff Schaller

ここでは、区切り文字をそれ自体で推測するbashをいくつか示します。

#!/bin/bash

delimiter="${1//[[:digit:]]/}"
if echo $delimiter | grep -q "^\(.\)\1\+$"
then
  delimiter="${delimiter:0:1}"
  if [[ -z $(echo $1 | grep "^\([0-9]\+"$delimiter"\([0-9]\+\)*\)\+$") ]]
  then
    echo "You seem to have empty fields between the delimiters."
    exit 1
  fi
  if [[ './\' == *$delimiter* ]]
  then
    n=$( echo $1 | sed "s/\\"$delimiter"/\\n/g" | sort -n | tr '\n' ' ' | sed -e "s/\\s/\\"$delimiter"/g")
  else
    n=$( echo $1 | sed "s/"$delimiter"/\\n/g" | sort -n | tr '\n' ' ' | sed -e "s/\\s/"$delimiter"/g")
  fi
  echo ${n%$delimiter}
  exit 0
else
  echo "The string does not consist of digits separated by one unique delimiter."
  exit 1
fi

効率的でもクリーンでもないかもしれませんが、機能します。

bash my_script.sh "00/00/18/29838/2"のように使用します。

同じ区切り文字が一貫して使用されていない場合、または2つ以上の区切り文字が連続している場合は、エラーを返します。

使用されている区切り文字が特殊文字の場合、エスケープされます(それ以外の場合、sedはエラーを返します)。

2
jkd

この回答はQ.の誤解に基づいていますが、いずれにせよそれが正しい場合もあります。入力が完全に 自然数 であり、1行のみの区切り文字がある場合(( Q.)、それは正しく動作します。また、それぞれが独自の区切り文字を持つ行を含むファイルも処理します。これは、要求されたものよりも少し多くなります。

このシェル関数readsは標準入力から、POSIXパラメーター置換を使用して、各行の特定の区切り文字を検索します($d)を使用し、trを使用して$dを改行\nおよびsortsでその行のデータに置き換え、各行の元の区切り文字を復元します。

sdn() { while read x; do
            d="${x#${x%%[^0-9]*}}"   d="${d%%[0-9]*}"
            x=$(echo -n "$x" | tr "$d" '\n' | sort -g | tr '\n' "$d")
            echo ${x%?}
        done ; }

[〜#〜] op [〜#〜]で指定されたデータに適用されます:

printf "%s\n" "10 50 23 42" "10.1.200.42" "1,100,330,42" "400|500|404" | sdn

出力:

10 23 42 50
1.10.42.200
1,42,100,330
400|404|500
2
agc

任意の区切り文字の場合:

Perl -lne '
  @list = /\D+|\d+/g;
  @sorted = sort {$a <=> $b} grep /\d/, @list;
  for (@list) {$_ = shift@sorted if /\d/};
  print @list'

次のような入力で:

5,4,2,3
6|5,2|4
There are 10 numbers in those 3 lines

それは与えます:

2,3,4,5
2|4,5|6
There are 3 numbers in those 10 lines
2

以下は、バブルソートを実行するsedスクリプトを生成するという意味での Jeffの回答 のバリエーションですが、独自の回答を保証するのに十分な違いがあります。

違いは、O(n ^ 2)基本正規表現を生成する代わりに、O(n)拡張正規表現を生成することです。結果のスクリプトは約15 KBの大きさになります。実行時間はsedスクリプトは、ほんの一瞬です(スクリプトの生成には少し時間がかかります)。

ドットで区切られた正の整数の並べ替えに制限されていますが、整数のサイズに制限されていません(255(メインループ)、または整数の数。区切り文字は、delim='.'コード内。

正規表現を正しくするために頭を使ったので、別の日の詳細を説明します。

#!/bin/bash

# This function creates a extended regular expression
# that matches a positive number less than the given parameter.
lt_pattern() {
    local n="$1"  # Our number.
    local -a res  # Our result, an array of regular expressions that we
                  # later join into a string.

    for (( i = 1; i < ${#n}; ++i )); do
        d=$(( ${n: -i:1} - 1 )) # The i:th digit of the number, from right to left, minus one.

        if (( d >= 0 )); then
            res+=( "$( printf '%d[0-%d][0-9]{%d}' "${n:0:-i}" "$d" "$(( i - 1 ))" )" )
        fi
    done

    d=${n:0:1} # The first digit of the number.
    if (( d > 1 )); then
        res+=( "$( printf '[1-%d][0-9]{%d}' "$(( d - 1 ))" "$(( ${#n} - 1 ))" )" )
    fi

    if (( n > 9 )); then
        # The number is 10 or larger.
        res+=( "$( printf '[0-9]{1,%d}' "$(( ${#n} - 1 ))" )" )
    fi

    if (( n == 1 )); then
        # The number is 1. The only thing smaller is zero.
        res+=( 0 )
    fi

    # Join our res array of expressions into a '|'-delimited string.
    ( IFS='|'; printf '%s\n' "${res[*]}" )
}

echo ':top'

delim='.'

for (( n = 255; n > 0; --n )); do
    printf 's/\\<%d\\>\\%s\\<(%s)\\>/\\1%s%d/g\n' \
        "$n" "$delim" "$( lt_pattern "$n" )" "$delim" "$n"
done

echo 'ttop'

スクリプトは次のようになります。

$ bash generator.sh >script.sed
$ head -n 5 script.sed
:top
s/\<255\>\.\<(25[0-4][0-9]{0}|2[0-4][0-9]{1}|[1-1][0-9]{2}|[0-9]{1,2})\>/\1.255/g
s/\<254\>\.\<(25[0-3][0-9]{0}|2[0-4][0-9]{1}|[1-1][0-9]{2}|[0-9]{1,2})\>/\1.254/g
s/\<253\>\.\<(25[0-2][0-9]{0}|2[0-4][0-9]{1}|[1-1][0-9]{2}|[0-9]{1,2})\>/\1.253/g
s/\<252\>\.\<(25[0-1][0-9]{0}|2[0-4][0-9]{1}|[1-1][0-9]{2}|[0-9]{1,2})\>/\1.252/g
$ tail -n 5 script.sed
s/\<4\>\.\<([1-3][0-9]{0})\>/\1.4/g
s/\<3\>\.\<([1-2][0-9]{0})\>/\1.3/g
s/\<2\>\.\<([1-1][0-9]{0})\>/\1.2/g
s/\<1\>\.\<(0)\>/\1.1/g
ttop

生成された正規表現の背後にある考え方は、各整数より小さい数値のパターンマッチングです。これらの2つの数値は順不同であるため、入れ替えられます。正規表現はいくつかのORオプションにグループ化されています。各項目に追加された範囲に細心の注意を払ってください。時には{0}は、直前のアイテムが検索から除外されることを意味します。正規表現オプションは、左から右に、指定された数値よりも小さい数値を次のように照合します。

  • ものの場所
  • 十の位
  • 何百もの場所
  • (必要に応じて続き、より大きな数の場合)
  • または大きさ(桁数)を小さくする

例を詳しく説明するには、101(読みやすくするためにスペースを追加):

s/ \<101\> \. \<(10[0-0][0-9]{0} | [0-9]{1,2})\> / \1.101 /g

ここでは、最初の代替では100から100までの数値を許可しています。 2番目の代替では、0〜99を使用できます。

別の例は154

s/ \<154\> \. \<(15[0-3][0-9]{0} | 1[0-4][0-9]{1} | [0-9]{1,2})\> / \1.154 /g

ここで、最初のオプションは150から153を許可します。 2番目は100から149を許可し、最後は0から99を許可します。

ループで4回テストする:

for test_run in {1..4}; do
    nums=$(( RANDOM%256 )).$(( RANDOM%256 )).$(( RANDOM%256 )).$(( RANDOM%256 ))
    printf 'nums=%s\n' "$nums"
    sed -E -f script.sed <<<"$nums"
done

出力:

nums=90.19.146.232
19.90.146.232
nums=8.226.70.154
8.70.154.226
nums=1.64.96.143
1.64.96.143
nums=67.6.203.56
6.56.67.203
0
Kusalananda

Perlの場合:

$ # -a to auto-split on whitespace, results in @F array
$ echo 'foo baz v22 aimed' | Perl -lane 'print join " ", sort @F'
aimed baz foo v22
$ # {$a <=> $b} for numeric comparison, {$b <=> $a} will give descending order
$ echo '1,100,330,42' | Perl -F, -lane 'print join ",", sort {$a <=> $b} @F'
1,42,100,330

Rubyを使用すると、Perlと多少似ています。

$ # -a to auto-split on whitespace, results in $F array
$ # $F is sorted and then joined using the given string
$ echo 'foo baz v22 aimed' | Ruby -lane 'print $F.sort * " "'
aimed baz foo v22

$ # (&:to_i) to convert string to integer
$ echo '1,100,330,42' | Ruby -F, -lane 'print $F.sort_by(&:to_i) * ","'
1,42,100,330

$ echo '10.1.200.42' | Ruby -F'\.' -lane 'print $F.sort_by(&:to_i) * "."'
1.10.42.200


カスタムコマンドと区切り文字列のみを渡します(正規表現ではありません)。入力にも浮動データがある場合に機能します

$ # by default join uses value of $,
$ sort_line(){ Ruby -lne '$,=ENV["d"]; print $_.split($,).sort_by(&:to_f).join' ; }

$ s='103,14.5,30,24'
$ echo "$s" | d=',' sort_line
14.5,24,30,103
$ s='10.1.200.42'
$ echo "$s" | d='.' sort_line
1.10.42.200

$ # for file input
$ echo '123--87--23' > ip.txt
$ echo '3--12--435--8' >> ip.txt
$ d='--' sort_line <ip.txt
23--87--123
3--8--12--435


Perlのカスタムコマンド

$ sort_line(){ Perl -lne '$d=$ENV{d}; print join $d, sort {$a <=> $b} split /\Q$d/' ; }
$ s='123^[]$87^[]$23'
$ echo "$s" | d='^[]$' sort_line 
23^[]$87^[]$123


さらに読む-私はすでにPerl/Rubyのワンライナーのこの便利なリストを持っていました

0
Sundeep

これは、数字以外の(0-9)区切り文字を処理する必要があります。例:

x='1!4!3!5!2'; delim=$(echo "$x" | tr -d 0-9 | cut -b1); echo "$x" | tr "$delim" '\n' | sort -g | tr '\n' "$delim" | sed "s/$delim$/\n/"

出力:

1!2!3!4!5
0
Alexander