So sortieren Sie ein Array in Bash


139

Ich habe ein Array in Bash, zum Beispiel:

array=(a c b f 3 5)

Ich muss das Array sortieren. Nicht nur den Inhalt sortiert anzeigen, sondern auch ein neues Array mit den sortierten Elementen erhalten. Das neu sortierte Array kann ein völlig neues oder das alte sein.

Antworten:


208

Sie brauchen nicht wirklich so viel Code:

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

Unterstützt Leerzeichen in Elementen (solange es sich nicht um eine neue Zeile handelt) und funktioniert in Bash 3.x.

z.B:

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

Hinweis: @sorontar hat darauf hingewiesen, dass Vorsicht geboten ist, wenn Elemente Platzhalter wie *oder enthalten ?:

Der sortierte = ($ (...)) Teil verwendet den Operator "split and glob". Sie sollten glob deaktivieren: set -foder set -o nogloboder shopt -op nogloboder ein Element des Arrays wie *wird zu einer Liste von Dateien erweitert.

Was ist los:

Das Ergebnis ist ein Höhepunkt von sechs Dingen, die in dieser Reihenfolge geschehen:

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

Zuerst die IFS=$'\n'

Dies ist ein wichtiger Teil unserer Operation, der das Ergebnis von 2 und 5 folgendermaßen beeinflusst:

Gegeben:

  • "${array[*]}" Erweitert sich auf jedes Element, das durch das erste Zeichen von begrenzt ist IFS
  • sorted=() Erstellt Elemente durch Aufteilen auf jedes Zeichen von IFS

IFS=$'\n' Richtet die Dinge so ein, dass Elemente mit einer neuen Zeile als Trennzeichen erweitert und später so erstellt werden, dass jede Zeile zu einem Element wird. (dh Aufteilen in eine neue Zeile.)

Das Abgrenzen durch eine neue Zeile ist wichtig, da dies so sortfunktioniert (Sortieren pro Zeile). Das Teilen durch nur eine neue Zeile ist nicht so wichtig, muss jedoch Elemente beibehalten, die Leerzeichen oder Tabulatoren enthalten.

Der Standardwert von IFSist ein Leerzeichen , eine Registerkarte , gefolgt von einer neuen Zeile , und wäre für unseren Vorgang nicht geeignet.

Als nächstes das sort <<<"${array[*]}"Teil

<<<, hier Strings genannt , nimmt die Erweiterung von "${array[*]}", wie oben erläutert, und speist sie in die Standardeingabe von ein sort.

In unserem Beispiel sortwird folgende Zeichenfolge eingegeben:

a c
b
f
3 5

Da sort sortiert , produziert es:

3 5
a c
b
f

Als nächstes das sorted=($(...))Teil

Der $(...)Teil, der als Befehlssubstitution bezeichnet wird , bewirkt, dass content ( sort <<<"${array[*]}) wie ein normaler Befehl ausgeführt wird, während die resultierende Standardausgabe als Literal verwendet wird, das dorthin geht, wo immer es $(...)war.

In unserem Beispiel ergibt dies etwas Ähnliches wie das einfache Schreiben:

sorted=(3 5
a c
b
f
)

sorted wird dann zu einem Array, das durch Aufteilen dieses Literals in jede neue Zeile erstellt wird.

Endlich, das unset IFS

Dies setzt den Wert von IFSauf den Standardwert zurück und ist nur eine gute Vorgehensweise.

Es soll sicherstellen, dass wir keine Probleme mit irgendetwas verursachen, auf das wir uns IFSspäter in unserem Skript stützen. (Andernfalls müssten wir uns daran erinnern, dass wir die Dinge umgestellt haben - etwas, das für komplexe Skripte möglicherweise unpraktisch ist.)


2
@xxor ohne das IFS, es wird Ihre Elemente in kleine Teile teilen, wenn sie Leerzeichen enthalten. Versuchen Sie das zB mit IFS=$'\n' weggelassen und sehen Sie!
Antak

3
Sehr schön. Können Sie dem durchschnittlichen Bash-Benutzer erklären, wie diese Lösung funktioniert?
U32004

2
Mit dem IFSteilt es Ihre Elemente in kleine Teile auf, wenn sie nur eine bestimmte Art von Leerzeichen enthalten. Gut; nicht perfekt :-)
Begrenzte Versöhnung

7
Ist unset IFSnotwendig? Ich dachte, das Voranstellen IFS=eines Befehls umfasste nur die Änderung dieses Befehls und kehrte danach automatisch zum vorherigen Wert zurück.
Mark H

10
@MarkH Es ist notwendig, weil sorted=()es sich nicht um einen Befehl handelt, sondern um eine zweite Variablenzuweisung.
Antak

35

Ursprüngliche Antwort:

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

Ausgabe:

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

Beachten Sie, dass diese Version mit Werten umgeht , die Sonderzeichen oder Leerzeichen enthalten ( außer Zeilenumbrüche).

Hinweis: Readarray wird in Bash 4+ unterstützt.


Bearbeiten Basierend auf dem Vorschlag von @Dimitre hatte ich es aktualisiert auf:

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

Dies hat den Vorteil, dass Sortierelemente mit korrekt eingebetteten Zeilenumbruchszeichen sogar verstanden werden. Leider, wie korrekt signalisiert @ruakh bedeute dies nicht , das das Ergebnis readarraywäre richtig , weil readarraykeine andere Wahl hat zu verwenden , die NULanstelle der normalen Zeilenumbrüche als Line-Separatoren.


5
Schön, es sollte auch beachtet werden, dass Readarray seit Version 4 von Bash verfügbar ist. Es könnte ein wenig verkürzt werden:readarray -t sorted < <(printf '%s\n' "${array[@]}" | sort)
Dimitre Radoulov

1
@Dimitre: Ich habe Ihren Vorschlag angenommen und das Whitespace-Handling so korrigiert, dass es mit allem funktioniert (interne Verwendung von Nullzeichen-Trennzeichen). Prost
sehe

1
Ja, das sort -zist eine nützliche Verbesserung. Ich nehme an, die -zOption ist eine GNU-Sortiererweiterung.
Dimitre Radoulov

2
Wenn Sie eingebettete Zeilenumbrüche verarbeiten möchten, können Sie Ihr eigenes Readarray rollen. Zum Beispiel : sorted=(); while read -d $'\0' elem; do sorted[${#sorted[@]}]=$elem; done < <(printf '%s\0' "${array[@]}" | sort -z). Dies funktioniert auch, wenn Sie bash v3 anstelle von bash v4 verwenden, da Readarray in bash v3 nicht verfügbar ist.
Bob Bell

1
@ user1527227 Es ist Input Redirection ( <) kombiniert mit Prozessersetzung <(...) . Oder intuitiv ausgedrückt: weil (printf "bla")es keine Datei ist.
sehe

33

Hier ist eine reine Bash-Quicksort-Implementierung:

#!/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[@]}" )
}

Verwenden Sie als z.

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

Diese Implementierung ist rekursiv. Hier ist eine iterative Quicksortierung:

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

In beiden Fällen können Sie die Reihenfolge ändern, die Sie verwenden: Ich habe Zeichenfolgenvergleiche verwendet, aber Sie können arithmetische Vergleiche verwenden, die Änderungszeit der WRT-Datei vergleichen usw. Verwenden Sie einfach den entsprechenden Test. Sie können es sogar allgemeiner gestalten und ein erstes Argument verwenden lassen, das die Testfunktion verwendet, z.

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

Dann können Sie diese Vergleichsfunktion haben:

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

und verwenden:

$ qsort compare_mtime *
$ declare -p qsort_ret

Damit die Dateien im aktuellen Ordner nach Änderungszeit sortiert werden (neueste zuerst).

HINWEIS. Diese Funktionen sind reine Bash! Keine externen Dienstprogramme und keine Subshells! Sie sind sicher für alle lustigen Symbole, die Sie möglicherweise haben (Leerzeichen, Zeilenumbruchzeichen, Glob-Zeichen usw.).


1
Ein großes Lob für beeindruckendes Bashing, das große Flexibilität in Bezug auf Eingabeelemente und Sortierkriterien bietet. Wenn eine zeilenbasierte Sortierung mit den angebotenen Sortieroptionen sortausreicht, ist eine sort+ read -a-Lösung ab etwa 20 Elementen schneller und umso schneller, je mehr Elemente Sie bearbeiten. Beispiel: Auf meinem iMac Ende 2012 wird OSX 10.11.1 mit einem Fusion Drive ausgeführt: Array mit 100 Elementen: Ca. 0,03 s sek. ( qsort()) vs. 0,005 Sekunden. ( sort+ read -a); 1000-Elemente-Array: Ca. 0,375 Sekunden ( qsort()) vs. 0,014 Sekunden ( sort+ read -a).
mklement0

Nett. Ich erinnere mich an die schnelle Sortierung aus College-Tagen, werde aber auch die Blasensortierung erforschen. Für meine Sortieranforderungen habe ich das erste und das zweite Element, die den Schlüssel bilden, gefolgt von einem Datenelement (das ich später erweitern kann). Ihr Code könnte durch die Anzahl der Schlüsselelemente (parm1) und die Anzahl der Datenelemente (parm2) verbessert werden. Für OP wären die Parameter 1 und 0. Für mich wären die Parameter 2 und 1. In jeder Hinsicht ist Ihre Antwort am vielversprechendsten.
WinEunuuchs2Unix

1
Bei einem Datensatz mit nicht übertragenen Zeichenfolgen-Ganzzahlen, die ich gefunden habe, if [ "$i" -lt "$pivot" ]; thenwar andernfalls die aufgelöste "2" <"10" true zurückgegeben. Ich glaube, das ist POSIX vs. Lexicographical; oder vielleicht Inline Link .
Page2PagePro

27

Wenn Sie keine speziellen Shell-Zeichen in den Array-Elementen verarbeiten müssen:

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

Mit bash benötigen Sie ohnehin ein externes Sortierprogramm.

Mit zsh werden keine externen Programme benötigt und spezielle Shell-Zeichen sind einfach zu handhaben:

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

ksh muss ASCIIbetischset -s sortieren .


Sehr schöne Hintergrundinformationen. Ich würde fast nach einer Demo fragen, wie ksh das Set -s-Flag verwenden würde ... aber
andererseits

Dies sollte mit den meisten KornShell- Implementierungen (z. B. ksh88 und pdksh ) funktionieren : set -A array x 'a a' d; set -s -- "${array[@]}"; set -A sorted "$@" Und natürlich setzt der Befehl set die aktuellen Positionsparameter zurück, falls vorhanden.
Dimitre Radoulov

Sie sind eine wahre Quelle des Muschelwissens. Ich bin sicher, Sie müssen ein fotografisches Gedächtnis haben oder so, denn diese Art von subtilen Unterschieden entziehen sich den meisten anderen Mitgliedern der menschlichen Spezies :), +1 für das komplette Informationspaket
siehe

10

tl; dr :

Sortieren Sie das Array a_inund speichern Sie das Ergebnis in a_out(Elemente dürfen keine eingebetteten Zeilenumbrüche enthalten [1] ):

Bash v4 +:

readarray -t a_out < <(printf '%s\n' "${a_in[@]}" | sort)

Bash v3:

IFS=$'\n' read -d '' -r -a a_out < <(printf '%s\n' "${a_in[@]}" | sort)

Vorteile gegenüber der Lösung von Antak :

  • Sie müssen sich keine Gedanken über versehentliches Globbing machen (versehentliche Interpretation der Array-Elemente als Dateinamenmuster), sodass kein zusätzlicher Befehl erforderlich ist, um das Globbing zu deaktivieren ( set -fund set +fes später wiederherzustellen).

  • Sie müssen sich keine Gedanken über das Zurücksetzen IFSmit machen unset IFS. [2]


Optionales Lesen: Erklärung und Beispielcode

Der oben Mähdrescher Bash Code mit externem Dienstprogramm sortfür eine Lösung , die Arbeiten mit beliebigen einzelnen -Linie Elementen und entweder lexikalischer oder numerischer Sortier (gegebenenfalls durch Feld) :

  • Leistung : Bei etwa 20 Elementen oder mehr ist dies schneller als bei einer reinen Bash-Lösung - deutlich und zunehmend, sobald Sie mehr als 100 Elemente erreicht haben.
    (Die genauen Schwellenwerte hängen von Ihrer spezifischen Eingabe, Maschine und Plattform ab.)

    • Der Grund, warum es schnell ist, ist, dass es Bash-Schleifen vermeidet .
  • printf '%s\n' "${a_in[@]}" | sort führt die Sortierung durch (standardmäßig lexikalisch - siehe sortPOSIX-Spezifikation ):

    • "${a_in[@]}"Erweitert sich sicher auf die Elemente des Arrays a_inals einzelne Argumente , unabhängig davon, was sie enthalten (einschließlich Leerzeichen).

    • printf '%s\n' druckt dann jedes Argument - dh jedes Array-Element - unverändert in einer eigenen Zeile.

  • Beachten Sie die Verwendung einer Prozesssubstitution ( <(...)) , um die sortierte Ausgabe als Eingabe für read/ readarray(über die Umleitung zu stdin ) bereitzustellen <, da read/ readarrayin der aktuellen Shell ausgeführt werden muss (darf nicht in einer Subshell ausgeführt werden ), damit die Ausgabevariable a_outsichtbar ist auf die aktuelle Shell (damit die Variable im Rest des Skripts definiert bleibt).

  • Lesen sortder Ausgabe in eine Array-Variable :

    • Bash v4 +: readarray -t a_outLiest die einzelnen Zeilen aus, die von sortin die Elemente der Array-Variablen ausgegeben werden a_out, ohne die Nachfolge \nin jedes Element ( -t) aufzunehmen.

    • Bash v3: readarrayexistiert nicht, readmuss also verwendet werden:
      IFS=$'\n' read -d '' -r -a a_outweist readan, in die -aVariable array ( ) zu lesen a_out, die gesamte Eingabe über Zeilen ( -d '') zu lesen , sie jedoch durch Zeilenumbrüche ( IFS=$'\n'. ) In Array-Elemente aufzuteilen $'\n', wodurch eine wörtliche Zeilenumbruch (LF) erzeugt wird ) ist eine sogenannte ANSI C-Zeichenfolge ).
      ( -rEine Option, die praktisch immer verwendet werden sollte read, deaktiviert die unerwartete Behandlung von \Zeichen.)

Kommentierter Beispielcode:

#!/usr/bin/env bash

# Define input array `a_in`:
# Note the element with embedded whitespace ('a c')and the element that looks like
# a glob ('*'), chosen to demonstrate that elements with line-internal whitespace
# and glob-like contents are correctly preserved.
a_in=( 'a c' b f 5 '*' 10 )

# Sort and store output in array `a_out`
# Saving back into `a_in` is also an option.
IFS=$'\n' read -d '' -r -a a_out < <(printf '%s\n' "${a_in[@]}" | sort)
# Bash 4.x: use the simpler `readarray -t`:
# readarray -t a_out < <(printf '%s\n' "${a_in[@]}" | sort)

# Print sorted output array, line by line:
printf '%s\n' "${a_out[@]}"

Aufgrund der Verwendung von sortohne Optionen ergibt sich eine lexikalische Sortierung (Ziffernsortierung vor Buchstaben und Ziffernfolgen werden lexikalisch und nicht als Zahlen behandelt):

*
10
5
a c
b
f

Wenn Sie eine numerische Sortierung nach dem 1. Feld wünschen, verwenden Sie sort -k1,1nanstelle von nur sort, was ergibt (Nicht-Zahlen werden vor Zahlen sortiert und Zahlen werden korrekt sortiert):

*
a c
b
f
5
10

[1] Verwenden Sie zum Behandeln von Elementen mit eingebetteten Zeilenumbrüchen die folgende Variante (Bash v4 + mit GNU sort ) :
readarray -d '' -t a_out < <(printf '%s\0' "${a_in[@]}" | sort -z).
Die hilfreiche Antwort von Michał Górny enthält eine Bash v3-Lösung.

[2] Während IFS wird in der Schlag - v3 Variante gesetzt, wird die Änderung auf den Befehl scoped .
Im Gegensatz dazu folgt IFS=$'\n' in Antaks Antwort eher eine Zuweisung als ein Befehl. In diesem Fall ist die IFSÄnderung global .


8

Bei der dreistündigen Zugfahrt von München nach Frankfurt (die ich nur schwer erreichen konnte, weil das Oktoberfest morgen beginnt) dachte ich an meinen ersten Beitrag. Die Verwendung eines globalen Arrays ist eine viel bessere Idee für eine allgemeine Sortierfunktion. Die folgende Funktion verarbeitet beliebige Zeichenfolgen (Zeilenumbrüche, Leerzeichen usw.):

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[@]}

Dies druckt:

3 5 a b c z y

Die gleiche Ausgabe wird aus erstellt

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

Beachten Sie, dass wahrscheinlich Bash intern Smart-Pointer verwendet, so dass der Swap-Betrieb könnte billig sein (obwohl ich bezweifle es). Zeigt jedoch, bubble_sortdass erweiterte Funktionen wie merge_sortauch in der Reichweite der Shell-Sprache liegen.


5
Blasensorte? Wow .. Obama sagt, "
Blasensortierung

1
Nun, während der O-Typ schlau sein wollte, hatte er anscheinend nicht gespürt, dass dies keine 50/50-Zufallsfrage ist. Ein Vorgänger in der Position eines O-Mannes, sagen wir ihm, der B-Mann hat es einmal viel besser gemacht (Reynoldsburg, Ohio, Oktober 2000): "Ich denke, wenn Sie wissen, was Sie glauben, ist es viel einfacher, Fragen zu beantworten Ich kann deine Frage nicht beantworten. " Dieser B-Typ weiß also wirklich etwas über die Boolesche Logik. Der O-Typ tut es nicht.
Andreas Spindler

Die Funktion könnte einfacher portierbar gemacht werden, indem BSORT zu einem lokalen Array mit einem Namen für das zu sortierende Array gemacht wird. dh local -n BSORT="$1"zu Beginn der Funktion. Dann können Sie laufen bubble_sort myarray, um Myarray zu sortieren .
Johnraff

7

Eine weitere Lösung , die externe verwendet sortund meistert mit allen Sonderzeichen (außer NULs :)). Sollte mit bash-3.2 und GNU oder BSD sortfunktionieren (leider enthält POSIX nicht -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)

Schauen Sie sich zuerst die Eingabeumleitung am Ende an. Wir verwenden die printfeingebauten Array-Elemente mit Nullterminierung. Durch das Anführungszeichen wird sichergestellt, dass Array-Elemente unverändert übergeben werden. Durch die Besonderheiten der Shell wird printfder letzte Teil der Formatzeichenfolge für jeden verbleibenden Parameter wiederverwendet. Das heißt, es entspricht so etwas wie:

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

Die nullterminierte Elementliste wird dann an übergeben sort. Die -zOption bewirkt, dass nullterminierte Elemente gelesen, sortiert und auch nullterminiert ausgegeben werden. Wenn Sie nur die eindeutigen Elemente benötigen, können Sie übergeben, -uda es portabler ist als uniq -z. Das LC_ALL=Csorgt für eine stabile Sortierreihenfolge unabhängig von locale - manchmal nützlich für Skripte. Wenn Sie möchten, dass das sortGebietsschema respektiert wird, entfernen Sie das.

Das <()Konstrukt erhält den zu lesenden Deskriptor aus der erzeugten Pipeline und <leitet die Standardeingabe der whileSchleife an diese weiter. Wenn Sie auf die Standardeingabe in der Pipe zugreifen müssen, können Sie eine andere Deskriptorübung für den Leser verwenden :).

Nun zurück zum Anfang. Das readeingebaute liest die Ausgabe vom umgeleiteten Standard. Wenn Sie leer setzen, wird die IFSWortaufteilung deaktiviert, was hier nicht readerforderlich ist. Infolgedessen wird die gesamte Eingabezeile in die einzelne bereitgestellte Variable gelesen. -rOption deaktiviert die Escape-Verarbeitung, die auch hier unerwünscht ist. Setzt schließlich -d ''den Zeilenbegrenzer auf NUL - das heißt, es wird readangewiesen, Zeichenfolgen mit Nulltermin zu lesen.

Infolgedessen wird die Schleife einmal für jedes aufeinanderfolgende nullterminierte Array-Element ausgeführt, wobei der Wert in gespeichert wird e. Das Beispiel fügt die Elemente nur in ein anderes Array ein, aber Sie können es vorziehen, sie direkt zu verarbeiten :).

Dies ist natürlich nur eine der vielen Möglichkeiten, dasselbe Ziel zu erreichen. Aus meiner Sicht ist es einfacher als die Implementierung eines vollständigen Sortieralgorithmus in Bash und in einigen Fällen schneller. Es behandelt alle Sonderzeichen einschließlich Zeilenumbrüchen und sollte auf den meisten gängigen Systemen funktionieren. Am wichtigsten ist, dass es Ihnen etwas Neues und Fantastisches über Bash beibringen kann :).


Tolle Lösung und sehr hilfreiche Erklärung, danke. Eine Erweiterung: Ohne IFS auf leer zu setzen, werden auch führende Leerzeichen entfernt - auch wenn sonst keine Wortaufteilung durchgeführt wurde.
Dirk Herrmann

eVerwenden Sie die Variable REPLY, anstatt eine lokale Variable einzuführen und ein leeres IFS festzulegen.
Robin A. Meade

2

Versuche dies:

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

Ausgabe wird sein:

3
5
ein
b
c
f

Problem gelöst.


3
Sollte dies bearbeiten, um die Ausgabe in ein neues Array zu setzen, um seine Frage vollständig zu beantworten.
Peter Oram

2

Wenn Sie für jedes Element im Array eine eindeutige Ganzzahl berechnen können, gehen Sie wie folgt vor:

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 -> $?"

Dann können Sie diese Ganzzahlen als Array-Indizes verwenden, da Bash immer ein Array mit geringer Dichte verwendet, sodass Sie sich keine Gedanken über nicht verwendete Indizes machen müssen:

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

echo "${sorted[@]}"
  • Vorteile. Schnell.
  • Nachteile Doppelte Elemente werden zusammengeführt, und es kann unmöglich sein, Inhalte eindeutigen 32-Bit-Ganzzahlen zuzuordnen.

Interessante Technik, ich habe eine Variante verwendet, um Max / Min-Werte ohne expliziten Vergleich / Sortieren zu finden. Eine ungewichtete Addition ohne Berücksichtigung der Länge funktioniert jedoch nicht: "z" wird vor "aaaa" sortiert, sodass Sie dies nicht für Wörter verwenden können, wie oben gezeigt.
mr.spuratic

2

min sort:

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

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

Der Echo-Inhalt von new_array lautet:

3 5 a b c f

1

Es gibt eine Problemumgehung für das übliche Problem von Leerzeichen und Zeilenumbrüchen:

Verwenden , um ein Zeichen , das nicht in der ursprünglichen Anordnung ist (wie $'\1'oder $'\4'oder ähnliches).

Diese Funktion erledigt den Job:

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

Dadurch wird das Array sortiert:

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

Dadurch wird beanstandet, dass das Quellarray das Problemumgehungszeichen enthält:

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

Beschreibung

  • Wir setzen zwei lokale Variablen wa(Workaround-Zeichen) und ein Null-IFS
  • Dann (mit ifs null) testen wir das gesamte Array $*.
  • Enthält kein Woraround-Zeichen [[ $* =~ [$wa] ]].
  • Wenn dies der Fall ist, senden Sie eine Nachricht und signalisieren Sie einen Fehler: exit 1
  • Vermeiden Sie Dateinamenerweiterungen: set -f
  • Setzen Sie einen neuen Wert von IFS ( IFS=$'\n'), eine Schleifenvariable xund eine Newline var ( nl=$'\n').
  • Wir drucken alle Werte der empfangenen Argumente (das Eingabearray $@).
  • Wir ersetzen jedoch jede neue Zeile durch das Problemumgehungszeichen "${@//$nl/$wa}".
  • Senden Sie diese zu sortierenden Werte sort -n.
  • und setzen Sie alle sortierten Werte in die Positionsargumente zurück set --.
  • Dann weisen wir jedes Argument einzeln zu (um Zeilenumbrüche beizubehalten).
  • in einer Schleife for x
  • zu einem neuen Array: sorted+=(…)
  • in Anführungszeichen, um vorhandene Zeilenumbrüche beizubehalten.
  • Wiederherstellen der Problemumgehung in einer neuen Zeile "${x//$wa/$nl}".
  • getan

1

Diese Frage sieht eng verwandt aus. Übrigens, hier ist ein Mergesort in Bash (ohne externe Prozesse):

mergesort() {
  local -n -r input_reference="$1"
  local -n output_reference="$2"
  local -r -i size="${#input_reference[@]}"
  local merge previous
  local -a -i runs indices
  local -i index previous_idx merged_idx \
           run_a_idx run_a_stop \
           run_b_idx run_b_stop

  output_reference=("${input_reference[@]}")
  if ((size == 0)); then return; fi

  previous="${output_reference[0]}"
  runs=(0)
  for ((index = 0;;)) do
    for ((++index;; ++index)); do
      if ((index >= size)); then break 2; fi
      if [[ "${output_reference[index]}" < "$previous" ]]; then break; fi
      previous="${output_reference[index]}"
    done
    previous="${output_reference[index]}"
    runs+=(index)
  done
  runs+=(size)

  while (("${#runs[@]}" > 2)); do
    indices=("${!runs[@]}")
    merge=("${output_reference[@]}")
    for ((index = 0; index < "${#indices[@]}" - 2; index += 2)); do
      merged_idx=runs[indices[index]]
      run_a_idx=merged_idx
      previous_idx=indices[$((index + 1))]
      run_a_stop=runs[previous_idx]
      run_b_idx=runs[previous_idx]
      run_b_stop=runs[indices[$((index + 2))]]
      unset runs[previous_idx]
      while ((run_a_idx < run_a_stop && run_b_idx < run_b_stop)); do
        if [[ "${merge[run_a_idx]}" < "${merge[run_b_idx]}" ]]; then
          output_reference[merged_idx++]="${merge[run_a_idx++]}"
        else
          output_reference[merged_idx++]="${merge[run_b_idx++]}"
        fi
      done
      while ((run_a_idx < run_a_stop)); do
        output_reference[merged_idx++]="${merge[run_a_idx++]}"
      done
      while ((run_b_idx < run_b_stop)); do
        output_reference[merged_idx++]="${merge[run_b_idx++]}"
      done
    done
  done
}

declare -ar input=({z..a}{z..a})
declare -a output

mergesort input output

echo "${input[@]}"
echo "${output[@]}"

0

Ich bin nicht davon überzeugt, dass Sie in Bash ein externes Sortierprogramm benötigen.

Hier ist meine Implementierung für den einfachen Blasensortierungsalgorithmus.

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[@]})"

Dies soll drucken:

 input: a c b f 3 5
output: 3 5 a b c f

Blasensorte ist O(n^2). Ich erinnere mich an die meisten Sortieralgorithmen, die O(n lg(n))bis zum letzten Dutzend Elemente oder so verwenden. Für die letzten Elemente wird die Auswahlsortierung verwendet.
Jww


-1

sorted=($(echo ${array[@]} | tr " " "\n" | sort))

Im Geiste von Bash / Linux würde ich für jeden Schritt das beste Befehlszeilen-Tool verwenden. sorterledigt die Hauptaufgabe, benötigt jedoch eine durch Zeilenumbruch getrennte Eingabe anstelle von Leerzeichen, sodass die oben beschriebene sehr einfache Pipeline einfach Folgendes tut:

Echo-Array-Inhalt -> Leerzeichen durch Zeilenumbruch ersetzen -> sortieren

$() ist das Ergebnis zu wiederholen

($()) ist das "Echo-Ergebnis" in ein Array zu setzen

Hinweis : Wie @sorontar in einem Kommentar zu einer anderen Frage erwähnt hat:

Der sortierte = ($ (...)) Teil verwendet den Operator "split and glob". Sie sollten glob ausschalten: set -f oder set -o noglob oder shopt -op noglob oder ein Element des Arrays wie * wird zu einer Liste von Dateien erweitert.


Im Geiste von Bash / Linux : Ich denke, Sie haben den Geist überhaupt nicht verstanden. Ihr Code ist vollständig fehlerhaft (Pfadnamenerweiterung und Wortteilung). Dies wäre besser (Bash≥4): mapfile -t sorted < <(printf '%s\n' "${array[@]}" | sort)sonst sorted=(); while IFS= read -r line; do sorted+=( "$line" ); done < <(printf '%s\n' | sort).
gniourf_gniourf

Die von Ihnen verwendeten Antimuster sind :: echo ${array[@]} | tr " " "\n"Dies wird unterbrochen, wenn die Felder des Arrays Leerzeichen und Glob-Zeichen enthalten. Außerdem erzeugt es eine Unterschale und verwendet einen nutzlosen externen Befehl. Und aufgrund echoist stumm, wird es brechen , wenn Ihr Array mit beginnt -e, -Eoder -n. Verwenden Sie stattdessen : printf '%s\n' "${array[@]}". Das andere Antimuster ist: ($())Das "Echo-Ergebnis" wird in ein Array eingefügt . Sicherlich nicht! Dies ist ein schreckliches Antimuster, das aufgrund der Pfadnamenerweiterung (Globbing) und der Wortteilung zerbricht. Benutze niemals diesen Horror.
gniourf_gniourf

Die Top-Antwort hat das "schreckliche Antimuster". Und wie Sie die Antwort eines anderen auf die Frage, die Sie selbst beantwortet haben, ablehnen können.
Michael
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.