如何用数字对一行分隔的项目进行排序?


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

由于定界符是任意的,因此可以使用选择的单字符定界符随意提供(或扩展)答案。


8
您应该将其发布在codegolf上:)
ivanivan '18

1
这里也有一个类似的问题,我想使用排序
αғsнιη

只是一个提示,cut它的-d选项支持任意定界符。
奥列格·洛巴乔夫

请说明这四个DSV示例是在同一文件中,还是来自四个不同文件的样本。
AGC

2
看到其他一些评论:分隔符是任意的,但是将在输入中一致使用。假设数据生成器方面具有智能,以便它们不会在数据中使用逗号作为定界符(例如,4,325 comma 55 comma 42,430也不会发生,也不会1.5 period 4.2)。
Jeff Schaller

Answers:


12

您可以使用以下方法实现此目的:

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

用您的定界符替换 .
添加-usort上面的命令中以删除重复项。


或使用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='*'分隔符


注意:
您可能需要使用-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一个明显的版本;拆分数据,对其进行排序,然后再次将其重新备份。

定界符需要列出两次(一次在中split,一次在中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
随着-l-F,它甚至更好:perl -F'/\./' -le 'print join(".", sort {$a <=> $b} @F)'
穆鲁

@StéphaneChazelas感谢您的-l选择;我错过了!
Stephen Harris

1
@muru我最初没有使用该-F标志,因为它不能在所有版本中正常工作(例如,您的CentOS 7中的行-perl 5.16.3- 返回空白输出,尽管在Debian 9上可以正常工作)。但是,-p与之结合使用时,得到的结果略小一些,因此我将其添加为答案的替代方法。显示如何-F使用。谢谢!
史蒂芬·哈里斯

2
@StephenHarris,因为新版本的perl会在使用时和使用时自动添加-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。一种有效地对行进行排序,另一种有效地将一个定界符转换为换行符:

#!/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秒才能处理。确实,shell并不是处理文件的最佳工具。

Awk

对于多行(多于10行),最好使用一种真正的编程语言。一个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"shell变量内的for文件改回for行。

佩尔

最快的解决方案是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"并添加-i到perl选项以使文件版本“就地”:

#!/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脚本本身包含对于每对乱序数的两个搜索并交换命令:一个用于前两对的八位位组(迫使尾定界符为存在标记第三个八位字节的末端),和一个第三对八位位组第二个(以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

生成脚本的以下变体使用了单词边界标记\<\>从而摆脱了第二次替换的需要。这也将生成的脚本的大小从1.3 MB减小到了900 KB以下,同时大大减少了其sed自身的运行时间(取决于原始脚本的运行时间的50%-75%,具体取决于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'

1
一个有趣的想法,但似乎确实使事情变得有些复杂。
马特

1
@Matt有点意思。对任何内容进行排序sed都是荒谬的,这就是为什么这是一个有趣的挑战。
Kusalananda

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"

当未始终使用相同的定界符或两个或多个定界符互相跟随时,将返回错误。

如果使用的分隔符是特殊字符,则将其转义(否则sed返回错误)。


这启发了这一点
AGC

2

这个答案是基于对Q.的误解,但是在某些情况下,它还是正确的。如果输入是完全自然数,并且每行只有一个定界符(与Q中的样本数据一样),则它可以正常工作。它还将处理带有行的文件,每个行都有自己的定界符,这比要求的要多。

这个shell函数read是从标准输入中获取的,使用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​​一线清单


0

以下是对Jeff答案的一种变体,从某种意义上说,它生成了一个sed可以进行冒泡排序的脚本,但是有足够的差异来保证自己的答案。

不同之处在于,它生成O(n)扩展正则表达式,而不是生成O(n ^ 2)基本正则表达式。生成的脚本大约为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

生成的正则表达式背后的思想是对小于每个整数的数字进行模式匹配。这两个数字将乱序,因此被交换。正则表达式分为几个OR选项。请密切注意附加到每个项目的范围,有时它们是{0},这意味着搜索中将忽略前一个项目。regex选项从左到右通过以下方式匹配小于给定数字的数字:

  • 那个地方
  • 十位
  • 数百个地方
  • (根据需要继续,以供更大数量使用)
  • 或幅度较小(数字位数)

为了说明一个例子,请采取以下步骤101(为了便于阅读,还有其他空格):

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

在此,第一个替换项允许数字100到100;第二个交替允许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;第二个允许100到149,最后一个允许0到99。

循环测试四次:

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


您可以放心地假设为数字项,是的:应该替换定界符。
杰夫·谢勒
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.