• Willkommen im Linux Club - dem deutschsprachigen Supportforum für GNU/Linux. Registriere dich kostenlos, um alle Inhalte zu sehen und Fragen zu stellen.

awk-Felder in bash-Variablen?

Moin Moin,

gibt es eine Möglichkeit mehr als ein awk-Feld in verschiedene bash-Variablen zu schreiben? Kleines Beispiel wie ich es unschön finde:
Code:
$eingang=EIN/VERZEICHNIS
IFS=$'\n'
for datei in `ls -lA --time-style=+%Y\ %m\ %d $eingang`
do

jahr=`echo $datei | awk '{print $6}'`
monat=`echo $datei | awk '{print $7}'`
tag=`echo $datei | awk '{print $8}'`
dateiname=`echo $datei | awk '{print $9}'`
test=`echo $datei | awk '{print $1}'`
Mich stört es das ich für jede Variable jedes mal den gleichen Ablauf habe mit nur einer geänderten Feld-Nummer. Schöner wäre es wenn ich irgendwie $1, $6, $7, $8 und $9 direkt den Variablen zuweisen könnte ohne jedes mal den echo-Aufruf mit dabei zu haben.
 
A

Anonymous

Gast
Geier0815 schrieb:
gibt es eine Möglichkeit mehr als ein awk-Feld in verschiedene bash-Variablen zu schreiben?
sicherlich geht auch das, aber muss ich etwas genauer erklären, ;) weil dort eine kleine Falle ist, die man verstehen muss, um sowas zu lösen.

um mehrere Bash-Variablen gleichzeitig zuzuweisen muss man so einen Befehl wie zB "read" verwenden.
Wenn man aber jetzt zum Beispiel schreibt:
Code:
echo "A B C D E F"   |  read V1 V2 V3 V4 V5 V6 
echo $V1
echo $V2
...
würde man feststellen die Variablen sind leer. :???:
Problem sind hier die Subprozesse, ich habe hier einmal einen Bash-Prozess der das "echo" ausführt, und eine Subprozess, der das "read" ausführt, beide sind über eine Pipe verbunden. Zwar werden im Subprozess die Variablen richtig gesetzt, aber wenn "read" fertig ist, dann wird sein Subprozess beendet und die Bash die den "echo" Befehl ausgeführt hat, läuft weiter. Aber dieser Prozess kann überhaupt nichts von den Variablen wissen die im Subprozess gesetzt oder verändert worden sind.
Um bei diesem Beispiel zu bleiben, man muss die Variablen in der Subshell auslesen, in der sie mit den richtigen Werten vorhanden sind. Also zB so hier.
Code:
echo "A B C D E F"   | ( read V1 V2 V3 V4 V5 V6 ; echo $V1; echo $V2)

Man muss also hier immer sehr genau aufpassen das man den read-Befehl in den richtige Prozess hat, also die Bashvariablen in dem Prozess hat in dem sie dann verarbeitet werden sollen, oder von wo ich sie exportieren kann, und nicht in einen Subprozess der anschließend mit den geänderten Variablen stirbt . Exportieren kann man sie, aber Werte von Variablen nicht rückwärts vererben, das ist eben Shell, dort gibt es keine globalen Variablen und auch keine Zeiger und Referenzen mit denen man in anderen Prammiersprachen dieses Problem erschlagen würde.

Es gibt mehrere Möglichkeiten,, ich möchte nicht hier alle verwirren in dem ich hier 10 mögliche Umgehungen dieses Problemes aufliste, wer hier im Forum sucht, wird sicherlich fündig, solche Probleme hatten wir schon gelegentlich mal gehabt.

Zurück zu Aufgabenstellung in diesem Thread. Die Ausgabe von awk in verschiedene Bash-Variablen setzen, ohne awk mehrfach aufzurufen.
Eine recht einfache Umgehung des Subprozessproblems "<<<" , eine Sonderform des Here-Dokuments, ist nachzulesen in der Manpage der bash, aber nur als ein winziger Satz.
bash(1) schrieb:
A variant of here documents, the format is:

<<<word

The word is expanded and supplied to the command on its standard input.
setzte ich für "word" eine Befehlsubstitution ein, dann wird dieser Befehl ausgeführt und dessen Ausgabe dem Standard Eingabekanal zugeführt.
Die Lösung in diesem Fall hier könnte das zB so aussehen. ( #auskommentiert was jetzt nicht mehr benögtigt wird.)
Code:
#!/bin/bash
eingang="/tmp"
IFS=$'\n'
for datei in $(ls -lA --time-style=+%Y\ %m\ %d "$eingang")
do
#       jahr=`echo "$datei" | awk '{print $6}'`
#       monat=`echo "$datei" | awk '{print $7}'`
#       tag=`echo "$datei" | awk '{print $8}'`
#       dateiname=`echo "$datei" | awk '{print $9}'`
#       test=`echo "$datei" | awk '{print $1}'`

        read jahr monat tag dateiname test <<< $(echo "$datei" | awk '{print $6,$7,$8,$9,$1}')

        echo "$jahr" "$monat" "$tag" "$dateiname" "$test"  
done

robi



PS für die Bastler hier im Forum:
es gibt neben der Bash aber noch andere Shells, die durchaus auch noch einige andere Umleitungs Methoden kennen, Erwähnen möche ich hier mal die ksh (Kornshell).
Wer eine echte Kornshell installiert hat, kann ja mal das hier testen, und versuchen herauszufinden warum das mit der ksh funktioniert, aber nicht mit der Bash ;)
Code:
#!/usr/bin/ksh
eingang="/tmp"

ls -lA "--time-style=+%Y %m %d " $eingang | awk '{print $6 , $7 , $8 , $9 , $1}' |&
echo "hier kann jetzt irgend was anderes gemacht werden" 

while read -p jahr monat tag dateiname test 
do
        echo "$jahr" "$monat" "$tag" "$dateiname" "$test"  
done
 
Eigentlich braucht man gar kein awk:
Code:
#!/bin/bash
while read -a Array; do
    echo "Jahr: ${Array[5]}"
    echo "Monat: ${Array[6]}"
    echo "Tag: ${Array[7]}"
    echo "Dateiname: ${Array[8]}"
    echo "Test: ${Array[0]}"
done < <(ls -lA --time-style=+%Y\ %m\ %d $eingang)
 
A

Anonymous

Gast
Faxxon schrieb:
Eigentlich braucht man gar kein awk:
Sehr schöner Vorschlag den du hier gepostet hast
Linux ist immer Multiple Choice. die Idee hier alles gleich als Array einzulesen ist gut, solange die Ausgabe sauber formatiert ist, ist das ok und wird gut funktionieren.

Aber viel interessanter finde ich die hier gezeigte Lösung des Subprozess Problems. ;) "read < <( befehl)"
Damit lassen sich auch wunderbar "Pipes" bauen, um dieses Problem zu umgehen.
Code:
< <( echo "A B C D"  )  read  V1 V2 V3 V4 
echo $V1
echo $V2

Allerdings ist diese Schreibweise (Process Substitution) nur wenig ausführlich in den Bashdokus zu finden. Diese Methode kommt aus anderen Shells, wo sie auch entsprechend gut beschrieben sind. Aber sie funktionieren auch in der Bash. In der Bash gibt es noch etwas ähnliches, mal nach "bash + coproc" suchen.

Die Beschreibung der Process Substitution Syntax aus der Manpage der ksh:
ksh(1) schrieb:
Process Substitution.
This feature is only available on versions of the UNIX operating system that support the /dev/fd directory for naming open files. Each command argument of the form
<(list) or >(list) will run process list asynchronously connected to some file in /dev/fd. The name of this file will become the argument to the command. If the form
with > is selected then writing on this file will provide input for list. If < is used, then the file passed as an argument will contain the output of the list
process. For example,

paste <(cut -f1 file1) <(cut -f3 file2) | tee >(process1) >(process2)

cuts fields 1 and 3 from the files file1 and file2 respectively, pastes the results together, and sends it to the processes process1 and process2, as well as putting
it onto the standard output. Note that the file, which is passed as an argument to the command, is a UNIX pipe(2) so programs that expect to lseek(2) on the file will
not work.

Process substitution of the form <(list) can also be used with the < redirection operator which causes the output of list to be standard input or the input for what-
ever file descriptor is specified.



Asynchrone Kommunikation mit einer Subshell mittels coproc zu schreiben ist dann schon ein klein wenig "Oberliega", und für dieses kleine Problemchen hier ziemlich überdimensioniert aber mal zur Demo:
Code:
#!/bin/bash
eingang="/tmp"
coproc zawk { awk '{print $6,$7,$8,$9,$1}'; }

ls -lA --time-style=+%Y\ %m\ %d $eingang >&"${zawk[1]}"
fd=${zawk[1]}
exec {fd}>&-
while read jahr monat tag dateiname test
do
        echo "$jahr" "$monat" "$tag" "$dateiname" "$test"  
done <&"${zawk[0]}"
Zur Erklärung:
  • coproc zawk { awk '{print $6,$7,$8,$9,$1}'; }
    * erzeugt einen Koprozess der hier mal den Namen "zawk" bekommen hat. Dieser Prozess ist in diesem Fall ein einfacher awk-Befehl den wir als Filter verwenden.
    Die Bash hat uns dabei Variablen zur Verfügung gestellt in der jetzt die Dateidiskriptoren enthalten sind, mit deren Hilfe wir diesen Prozess ansprechen können. zawk[1] ist sein Descriptor zum Standardeingang und zawk[0] ein Descriptor der vom Standardausgang diese Prozesses ließt.

    ls -lA --time-style=+%Y\ %m\ %d $eingang >&"${zawk[1]}"
    * Damit übergeben wir jetzt die Standardausgabe des ls Kommandos direkt dem Standardeingang des zawk Prozesses.

    while read ......do ......done <&"${zawk[0]}"
    * ist die Leseschleife die jetzt direkt vom Standardauskabekanal des zawk Prozesses befüllt wird. In dieser Schleife sollen jetzt auch unsere Variablen verarbeitet werden.

    fd=${zawk[1]}
    exec {fd}>&-

    * ohne diese beiden Zeilen würde unser Script auch funktionieren, aber es würde nicht enden. Der zawk Prozess würde immer weiterlaufen und auf neue Eingaben warten. Wir könnten also durchaus jetzt noch mehr Befehlsausgaben aus weitern Befehlen an diesen zawk Prozess zum filtern schicken. Die Readschleife würde auch weiterlaufen und auf weitere Ausgaben des zawk Prozesses warten. Um das zu beenden müssen wir entweder ein EOF an den Eingang des zawk Prozesses senden, oder den Deskriptor dorthin schließen. diese beiden Zeilen schließen diesen Deskriptor ( sieht ein bischen umständlich aus, aber ich habe auf die Schnelle hier keinen anderen Weg gefunden als über eine Hilfsvariable (fd) zu gehen)


!!! Warnung !!!
So großartige Möglichkeiten durch die Benutzung von asynchroner Subprozesse und ähnlicher Mehoden (zB über NamedPipe) auch sein mag, sie hat generell ein großes Problem, das bei der Erstellung von solchen Scripten dringend zu beachten ist: Deadlock

Das oben gezeigte Demoscript wird zB in einem kleinem Verzeichnis problemlos funktionieren. aber wahrscheinlich versagen wenn ein sehr großes Verzeichnis bearbeitet werden soll, oder wenn es rekursiv einen großen Dateibaum durchlaufen soll.
Warum :???:

"ls" produziert Daten und gibt sie gepuffert an den zawk Prozess weiter, dieser wartet bis genügend Daten am Eingang vorliegen und fängt an zu filtern. Die gefilterten Ergebnisse landen wiederum in einem Buffer am Ausgang und warten dort bis sie gelesen werden. Ist dieser Buffer am Ausgang des zawk Prozesses voll, muss awk die Arbeit unterbrechen bis dieser Buffer wieder Platz hat. Damit hält aber dar zawk-Prozess auch den "ls"-Befehl an, da auch dieser am Ausgang vollläuft. Wenn aber jetzt "ls" stehen bleibt und nicht beendet wird, dann wird niemals die while-read-Schleife gestartet, und drin ist der einzige Befehl der aus dem Buffer lesen kann, und auf den eigentlich jetzt alles wartet., und damit steckt das Script fest. Beide Prozesse warten gegenseitig darauf, das im anderem Prozess was vorwärts geht. Deadlock.

Dieses sind dann meistens Laufzeitfehler die sehr schwer zu finden und zu identifizieren sind. Man braucht einiges an Erfahrung um sowas bei der Programmierung eines Scriptes vorhersehen und berücksichtigen zu können.

Das ist einer der Gründe warum solche Scripte nicht sonderlich beliebt sind.


robi
 
Sehr gute Erfahrungen mit den schrägsten Dateinamen habe ich übrigens mit einer solchen Konstruktion gesammelt:
Code:
while read -rd '' Jahr Monat Tag Dateiname; do
    echo "$Jahr $Monat $Tag $Dateiname"
done < <(find -maxdepth 1 ! -name "." -printf "%TY %Tm %Td %p\000" )
Find stellt sicher, dass Jahr, Monat und Tag jeweils ein Wort sind. Der komplette Rest bis zum Nullbyte ist Dateiname (mit Pfad).
 
Oben