Как я могу численно отсортировать одну строку элементов с разделителями?

Для улучшения ответа SLM вы также можете сделать эту работу для файлов, которые начинаются с числа, создав каталоги 0-9:

mkdir -p output/{A..Z}; mkdir -p output/{0..9}; for i in tstdir/*; do export FILE=$(basename "$i"); LTR=$(echo "${FILE:0:1}" | tr [a-z] [A-Z]); mv "$i" "output/$LTR/$FILE" ; done
11
07.04.2018, 03:58
13 ответов

Использование 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 МБ до чуть менее 900 КБ, а также значительно сокращает время выполнения 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'
4
27.01.2020, 19:56

Использование 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)'
12
27.01.2020, 19:56

Сgawk(GNUawk)для функцииasort():

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

замените *в качестве разделителя полей в SEP='*'на ваш разделитель .



Вы также можете выполнить следующую команду в случае одной строки (, потому что лучше не использовать циклы оболочки -для целей обработки текста -)

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

замените точки.вашим разделителем.
добавьте -uк команде sortвыше, чтобы удалить дубликаты.

Примечания:
Возможно, вам придется использовать опцию -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нет необходимости изменять, он по-прежнему будет обрабатывать их.

14
27.01.2020, 19:56

Разделение ввода на несколько строк

Используя 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

-2
27.01.2020, 19:56

Использование Python и аналогичная идея, как в ответе Стивена Харриса:

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

К сожалению, необходимость выполнять ввод-вывод вручную делает его гораздо менее элегантным, чем версия Perl.

6
27.01.2020, 19:56

Оболочка

Загрузка языка более высокого уровня требует времени.
Для нескольких строк сама оболочка может быть решением.
Мы можем использовать внешнюю команду 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 из-за использования только <<<. Если его заменить документом -, решение подходит для posix.
Это позволяет сортировать поля с помощью табуляции, пробелов или символов оболочки (*, ?, [). Не новые строки, потому что каждая строка сортируется.

Измените <<<"$2"на <"$2", чтобы обрабатывать имена файлов и называть их как:

shsort '.'    infile

Разделитель одинаков для всего файла. Если это ограничение, его можно улучшить.

Однако файл, содержащий всего 6000 строк, обрабатывается 15 секунд. Действительно, оболочка — не лучший инструмент для обработки файлов.

Ок

Для более чем нескольких строк (более нескольких десятков )лучше использовать настоящий язык программирования. Решение 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

Что занимает всего 0,2 секунды для того же файла из 6000 строк, упомянутого выше.

Поймите, что <"$2"для файлов может быть изменен обратно на <<<"$2"для строк внутри переменных оболочки.

Перл

Самое быстрое решение — 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
4
27.01.2020, 19:56

С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 -лайнеров

0
27.01.2020, 19:56

Сценарий 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

На основе

4
27.01.2020, 19:56

Вот какой-то 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возвращает ошибку ).

2
27.01.2020, 19:56

Нижеследующее является вариацией ответа Джеффа в том смысле, что он генерирует сценарий sed, который будет выполнять пузырьковую сортировку, но достаточно отличается, чтобы гарантировать собственный ответ.

Разница в том, что вместо O (n^2 )базовых регулярных выражений генерируется O (n )расширенных регулярных выражений. Результирующий скрипт будет иметь размер около 15 КБ. Время выполнения скрипта 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

Идея сгенерированных регулярных выражений заключается в сопоставлении с образцом чисел, которые меньше каждого целого числа; эти два числа будут отсутствовать -порядка -, поэтому они меняются местами. Регулярные выражения сгруппированы в несколько вариантов ИЛИ. Обратите особое внимание на диапазоны, добавленные к каждому элементу, иногда это {0}, что означает, что непосредственно -предыдущий элемент должен быть исключен из поиска. Параметры регулярного выражения слева от -до -справа соответствуют числам, которые меньше заданного числа на:

  • те места
  • разряд десятков
  • разряд сотен
  • (продолжение по мере необходимости для большего числа)
  • или меньшим по величине (числом цифр)

Чтобы пояснить пример, возьмите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
1
27.01.2020, 19:56

Этот ответ основан на неправильном понимании вопроса, но в некоторых случаях он все равно оказывается правильным. Если ввод полностью состоит из натуральных чисел и имеет только один разделитель на -строку, (, как и в примере данных в Q. ), он работает правильно.. Он также будет обрабатывать файлы со строками, каждая из которых имеет собственный разделитель, что немного больше, чем просили.

Эта функция оболочки readиз стандартного ввода использует подстановку параметров POSIX для поиска определенного разделителя в каждой строке, (сохраненную в $d), и использует trдля замены $dс новой строкой \nи sortс данными этой строки, затем восстанавливает исходные разделители каждой строки:

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 ; }

Применительно к данным, приведенным в ОП:

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
27.01.2020, 19:56

Это должно обрабатывать любой не -цифровой разделитель (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
27.01.2020, 19:56

Для произвольных разделителей:

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
27.01.2020, 19:56

Теги

Похожие вопросы