Bash - Mehrdimensionale Arrays und Extrahieren von Variablen aus der Ausgabe

8

Ich versuche etwas Einfaches zu tun, bin mir aber nicht sicher, wie ich mein Ziel hier erreichen soll.

Ich versuche, die Werte USER, TTY und FROM zu extrahieren, die vom wBefehl auf der Konsole angegeben werden. In bash versuche ich, diese Ausgabe zu übernehmen und diese Werte in ein mehrdimensionales Array (oder nur ein Array mit einem Leerzeichen) zu übertragen.

#!/bin/bash
w|awk '{if(NR > 2) print $1,$2,$3}' | while read line
do
     USERS+=("$line")
     echo ${#USERS[@]}
done
echo ${#USERS[@]}

Ich habe den Weg gefunden, die Werte zeilenweise in einem einzelnen Array einzulesen, aber ich kann den USERS-Array-Wert scheinbar nicht aus dem Bereich der while-Schleife herausholen. Es gibt die Werte 1,2,3,4 und dann 0 nach der Schleife aus. Jedes Beispiel, das ich lese, verwendet die Variable außerhalb des Gültigkeitsbereichs vollkommen in Ordnung, aber ich kann es nicht scheinen.

russ
quelle
Die rechte Seite einer Pipeline wird in einer Subshell in Bash ausgeführt. Deshalb ist sie nach der Schleife nicht verfügbar.
Jordan
@ Jordanm gibt es also keine Möglichkeit, das USERS-Array zu extrahieren?
Russ
Verwenden Sie die Prozessersetzung. while read col1 col2 col3 _; do ...; done < <(w)
Jordan

Antworten:

5

Ihr Hauptproblem besteht darin, dass der letzte Befehl in einer Pipeline wie alle anderen Befehle in der Pipeline in einer Subshell ausgeführt wird. Dies ist bei den meisten Schalen der Fall. ATT ksh und zsh sind Ausnahmen: Sie führen den letzten Befehl der Pipeline in der übergeordneten Shell aus.

Seit Bash 4.2 können Sie Bash anweisen, sich wie ksh und zsh zu verhalten, indem Sie die lastpipe Option festlegen .

#!/bin/bash
USERS=()
shopt -s lastpipe
w | awk '{if(NR > 2) print $1,$2,$3}' | while read line; do
  USERS+=("$line")
done
echo ${#USERS[@]}

Alternativ können Sie die Prozessersetzung anstelle einer Pipe verwenden, sodass der readBefehl im Haupt-Shell-Prozess ausgeführt wird.

#!/bin/bash
USERS=()
while read line; do
  USERS+=("$line")
done < <(w | awk '{if(NR > 2) print $1,$2,$3}')
echo ${#USERS[@]}

Alternativ können Sie den portablen Ansatz verwenden, der in Shells funktioniert, die weder Prozesssusbtitution noch ksh / zsh-Verhalten aufweisen, wie z. B. Bourne, dash und pdksh. (Sie benötigen weiterhin (pd) ksh, bash oder zsh für Arrays.) Führen Sie alles aus, was die Daten aus der Pipeline innerhalb der Pipeline benötigt.

#!/bin/bash
USERS=()
shopt -s lastpipe
w | awk '{if(NR > 2) print $1,$2,$3}' | {
  while read line; do
    USERS+=("$line")
  done
  echo ${#USERS[@]}
}
Gilles 'SO - hör auf böse zu sein'
quelle
4

Mit shopt -s lastpipekönnen Sie den letzten Befehl einer Pipeline in die aktuelle Shell-Umgebung übernehmen. Das löst dein Problem. Ich denke, diese Funktion war nicht immer in Bash. Vermeiden Sie sie, wenn Sie weitgehend kompatiblen Code wünschen.

Die kompatible Alternative:

export_array="$(w | awk '{if(NR > 2) print $1,$2,$3}' | 
  { USERS=(); while read line; do
      USERS[]="$line"
    done
    declare -p USERS; } )"
eval "$export_array"
Hauke ​​Laging
quelle
Meinten Sie USERS+=("$line")statt USERS[]="$line"?
Runium
@Sukminder Natürlich nicht. Ich habe das ersetzt, weil AFAIR die +=Notation in neueren Versionen von bash hinzugefügt wurde. Beide Befehle machen dasselbe.
Hauke ​​Laging
4

Bash-Arrays sind eindimensional. Wenn Sie geordnete separate Werte für jede Zeile beibehalten möchten, besteht eine Lösung darin, assoziative Arrays zu verwenden. Ein grobes Beispiel:

Seien Sie auch vorsichtig mit diesen Variablennamen in Großbuchstaben, da sie mit Umgebungsvariablen in Konflikt geraten können.

#!/bin/bash

declare -i i=0 j=0
declare -A w

while read -r user tty from _;do
    ((++i > 2)) || continue
    w["$j.user"]="$user"
    w["$j.tty"]="$tty"
    w["$j.from"]="$from"
    ((++j))
done < <(w)

for ((i = 0; i < j; ++i)); do
    printf "entry %-2d {\n  %-5s: %s\n  %-5s: %s\n  %-5s: %s\n}\n" \
    "$i" \
    "user" "${w[$i.user]}" \
    "tty"  "${w[$i.tty]}" \
    "from" "${w[$i.from]}"
done
Runium
quelle
3

Für die Speicherung in Bash-Arrays ist die Verwendung eines anderen Trennzeichens als Leerzeichen häufig einfacher.

    readarray -s2 -t my_w_array < <(w | awk '{ print $1":"$2":"$3 }')

Sie können es dann beim Drucken aufteilen, z. B.:

    printf '%s\n' "${my_w_array[@]//:/ }"
Christopher Barry
quelle