区切られたアイテムの単一行を数値的にソートするにはどうすればよいですか?


11

任意の文字で区切られた数字の行(または多くの行)があります。区切り文字を保持したまま、各行の項目を数値的に並べ替えるために使用できる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文字の区切り文字を使用して、回答を自由に提供(または拡張)してください。


8
codegolfに投稿してください:)
ivanivan

1
同様の質問がここにもあります私はそのリンクをファイル名内にソートを使用してアルファベット順に単語
αғsнιη

オプションでcut任意の区切り文字をサポートする単なるヒント-d
オレグLobachev

DSVの4つの例が同じファイルにあるのか、4つの異なるファイルのサンプルなのかを明確にしてください。
agc

2
他のコメントのいくつかを見てください:区切り文字は任意ですが、入力で一貫して使用されます。区切り文字としてカンマを使用したり、データ内でカンマを使用したりしないように、データプロデューサー側のインテリジェンスを想定します(たとえば、4,325 comma 55 comma 42,430発生しない、または発生しない1.5 period 4.2)。
ジェフシャラー

回答:


12

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

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

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


またはgawkGNU 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不要の変更、それはまだそれらを処理します。


10

使用するperl明白なバージョンがあります。データを分割、並べ替え、再度結合します。

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

たとえば ,

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)'

4
を使用する-l代わりにオプションを使用できますchomp。また、印刷時に改行が追加されます。分割部分については-a(と一緒に-F)も参照してください。
ステファンChazelas

1
-landを使用すると-F、さらにperl -F'/\./' -le 'print join(".", sort {$a <=> $b} @F)'
便利になります。– muru

@StéphaneChazelas -lオプションをありがとう。私はそれを見逃した!
スティーブンハリス

1
@muru -Fすべてのバージョンで正常に動作しないため、もともとフラグを使用しませんでした(たとえば、CentOS 7の行-perl 5.16.3-は、Debian 9では正常に動作しますが、空白の出力を返します)。しかし、-pそれと組み合わせると少し結果が小さくなるので、私はそれを答えの代わりとして追加しました。-F使用方法を示します。ありがとう!
スティーブンハリス

2
perlのの新しいバージョンが自動的に追加されるためだ@StephenHarris -a-nする場合のオプション-F使用され-nたときに-a使用されているが...これだけ変更する-leには-lane
Sundeep

4

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バージョンよりもはるかに洗練されていません。



3

シェル

高レベルの言語をロードするには時間がかかります。
数行では、シェル自体が解決策になる場合があります。
外部コマンドを使用できます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を必要とします。これがhere-docに置き換えられた場合、ソリューションは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"forファイル<<<"$2"をシェル変数内のfor行に戻すことができることを理解してください。

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"を単純にソートし、perlオプションに"$a"追加-iしてファイルの編集を「適切な場所」にする場合:

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

perlsort '.' infile; exit

2

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

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

sedオクテットの最初の二つのペアの一方(末尾の区切り文字を強制的に第三のオクテットの終わりをマークするために存在する)、およびA:スクリプト自体は、2つの検索・アンド・スワップ・アウト・オブ・オーダー番号の各ペアのためのコマンドを含みます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

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

#!/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'

1
面白いアイデアですが、少し複雑すぎるようです。
マット

1
@Mattそれはちょっとポイントです。で何かをソートするのsedはばかげています。そのため、これは興味深い課題です。
クサラナンダ

2

ここで、区切り文字をそれ自体で推測する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はエラーを返します)。


それが触発した
agc

2

この回答はQ.の誤解に基づいていますが、いずれにせよそれが正しい場合もあります。入力が完全に自然数で、1行に1つの区切り記号しかない場合(Qのサンプルデータの場合と同様)、正しく機能します。また、それぞれが独自の区切り文字を持つ行を含むファイルも処理します。これは、要求されたものよりも少し多くなります。

このシェル関数はread、標準入力からsを取得し、POSIXパラメーター置換を使用して各行の特定の区切り文字(に格納されている$d)を検索し、を使用して改行とその行のデータtrを置き換え、次に各行の元の区切り文字を復元します。$d\nsort

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

任意の区切り文字:

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

0

これは、数字以外(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

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​​ one-linersのこの便利なリストはすでにありました


0

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

違いは、O(n ^ 2)基本正規表現を生成する代わりに、O(n)拡張正規表現を生成することです。結果のスクリプトのサイズは約15 KBになります。sedスクリプトの実行時間は1秒未満です(スクリプトの生成には少し時間がかかります)。

ドットで区切られた正の整数の並べ替えに制限されていますが、整数のサイズ(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

-2

入力を複数の行に分割する

tr使用すると、任意の区切り文字を使用して入力を複数の行に分割できます。

次に、この入力を実行できますsort-n入力が数値の場合に使用)。

出力で区切り文字を保持する場合は、tr再度使用して区切り文字を追加し直すことができます。

例えば、スペースを区切り文字として使用する

cat input.txt | tr " " "\n" | sort -n | tr "\n" " "

入力:1 2 4 1 4 32 18 3 出力:1 1 2 3 4 4 18 32


数値アイテムを安全に想定できます。そうです。区切り文字を置き換える必要があります。
Jeff Schaller
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.