ubuntuusers.de

bash Skript Standardausgabe in while Zeilenweise bearbeiten und zählen Fehler

Status: Gelöst | Ubuntu-Version: Nicht spezifiziert
Antworten |

twohead

Anmeldungsdatum:
31. Januar 2009

Beiträge: Zähle...

Hi,

was für ein toller Titel ^^. Folgendes Problem in einem Bashskript

1
2
3
4
5
6
7
8
#!/bin/bash
COUNTER=0
ls -1 | while read LINE
do
  let COUNTER+=1
  # verarbeite Zeile
done
echo $COUNTER

In der while wird COUNTER fleissig hoch gezählt. Kann man mit echo/ set -vx gut nachvollziehen. Aber nach der while ist COUNTER plötzlich wieder 0. Wieso das? Wie kann ich das verhindern?

LG, PS: Ich arbeite gerne mit set -e. Wenn ich aber eine Subtraktion (mit let VAR=3-3) habe, die zu 0 führt, dann gibt es einen Fehler ($? = 1), weiß jemand wie man zu 0 subtrahiert ohne auf set -e verzichten zu können?

Antiqua Team-Icon

Avatar von Antiqua

Anmeldungsdatum:
30. Dezember 2008

Beiträge: 4534

willst du wc -l in bash implementieren? 😉

COUNTER=$(ls -l |wc -l); let COUNTER-=1; echo $COUNTER

das macht genau, was du willst, inklusive Abzug von 1 und ohne while-Schleife - oder willst du die erste Zeile der Ausgabe von ls -l auch mitgezählt haben?

twohead

(Themenstarter)

Anmeldungsdatum:
31. Januar 2009

Beiträge: Zähle...

Hey,

nein das will ich eigentlich nicht. Ich hab mit dem Kommentar # verarbeite Zeile versucht anzudeuten das ich da schon etwas sinnvolleres mehr mache. Aber das hat ja mit meinem eigentlich Problem gar nichts zu tun. Deshalb hab ich das der Einfachheit halber (Minimalbeispiel...) mal weggelassen. Hast du das Verhalten denn erwartet und kannst es reproduzieren?

LG

Antiqua Team-Icon

Avatar von Antiqua

Anmeldungsdatum:
30. Dezember 2008

Beiträge: 4534

twohead schrieb:

Hey,

nein das will ich eigentlich nicht. Ich hab mit dem Kommentar # verarbeite Zeile versucht anzudeuten das ich da schon etwas sinnvolleres mehr mache.

sorry, hab ich so nicht verstanden

Aber das hat ja mit meinem eigentlich Problem gar nichts zu tun.

da hast du allerdings recht

Deshalb hab ich das der Einfachheit halber (Minimalbeispiel...) mal weggelassen. Hast du das Verhalten denn erwartet und kannst es reproduzieren?

ich hab es nicht erwartet (verwende eigentlich so gut wie nie while), kann es aber reproduzieren...

eventuell sorgt aber das ange-pipte while-Konstrukt dafür, daß bei FALSE eben die Variable COUNTER wieder auf den ursprünglichen Wert gestellt wird (ka. ob das stimmt, sieht aber zumindest so aus)

LG

Gz

Greebo

Avatar von Greebo

Anmeldungsdatum:
21. November 2006

Beiträge: 3443

Wohnort: 97070 Würzburg

Ja ihr seid auf dem richtigen Weg (Pipe/Subshell) ☺. Unter folgendem Link hatte jemand schonmal dasselbe Problem, dort wirds nochmal erklärt und eine mögliche Lösung angegeben.

twohead

(Themenstarter)

Anmeldungsdatum:
31. Januar 2009

Beiträge: 51

Genau so etwas hatte ich befürchtet. Vielen Dank für den Link. Leider funktioniert die Lösung dort nur für Dateien. Ich will ja aber die Ausgabe von einem Befehl zeilenweise einlesen, also die Standardeingabe. Die lässt sich leider nicht exakt so (überhaupt?) in die while biegen, dass keine Subshell gestartet wird. Weiß jemand wie man das macht?

Damit nicht jeder im Link suche muss, die vorgeschlagene Lösung dort ist:

1
2
3
4
5
counter=0
while read line; do
  let ++counter
done < tmpfile.tmp
echo $counter

LG

Greebo

Avatar von Greebo

Anmeldungsdatum:
21. November 2006

Beiträge: 3443

Wohnort: 97070 Würzburg

Die einzige Lösung die ich gerade sehe ist auf die Subshell zu verzichten und dein while umzuformen, also statt der Pipe sowas wie

COUNTER=0
for LINE in `ls -l`; do
  let ++COUNTER
done
echo $COUNTER

twohead

(Themenstarter)

Anmeldungsdatum:
31. Januar 2009

Beiträge: 51

Nur das die Eingabe dann nicht Zeilenweise eingelesen wird, sondern "Whitespace-weise". Deshalb benutz ich ja diese while Konstruktion, weil ich explizit Zeilenweise vorgehen will...

Antiqua Team-Icon

Avatar von Antiqua

Anmeldungsdatum:
30. Dezember 2008

Beiträge: 4534

das mit dem zeilenweise lesen bei for-Schleifen dürfte ein

IFS=$'\012' # entspricht einem $'\n' Newline

lösen...

also sowas dürfte funktionieren (tuts in der Tat bei mir):

#!/bin/sh
LS=`ls -l`
IFS=$'\012'
COUNTER=0

for i in $LS ;
do
        let ++COUNTER
        echo    $i
        echo    $COUNTER
        echo    "neue Zeile, oder was immer du machen willst"
done

echo $COUNTER

twohead

(Themenstarter)

Anmeldungsdatum:
31. Januar 2009

Beiträge: 51

Das ist ja hochinteressant. Wofür genau steht denn dieses Zeichen? Ich hab mich letzens schon nach einer Tabelle totgesucht, in der die Tasten den Escapesequencen zugeorndet werden (^M für Return glaube ich). Ich hab übrigens nix gefunden.

Ich habs jetzt noch ausführlicher getestet. Bei ls -l passt alles. Aber bei dem Skript das ich eigentlich bearbeite kommt die Standardausgabe nicht von ls, sondern von einem cplex Skript. Und dort stimmt dann am Ende die Zeilenanzahl von der neuen Variante nicht mit der neuen Variante überein!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash

/homes/combi3/Software/ILOG/ilm/ilmlist | uniq | sed 1,9d > /tmp/ls.txt
rm -f /tmp/1.txt
rm -f /tmp/2.txt

COUNTER=0
while read LINE;
do
        let ++COUNTER
        echo "$LINE" >> /tmp/1.txt
done < /tmp/ls.txt
echo $COUNTER

IFS=$'\012'
COUNTER=0
for LINE in `cat /tmp/ls.txt`;
do
        let ++COUNTER
        echo "$LINE" >> /tmp/2.txt
done
echo $COUNTER

Die Ausgabe ist:

% ./test1
13
11

D.h. die Zeilen werden anders gezählt!

% diff /tmp/1.txt /tmp/2.txt
4,5c4
<
< * PRODUCT CPLEX: 10 any
---
> * PRODUCT CPLEX: 10 any
8,9c7
< available tokens: 7
<
---
> available tokens: 7

Die neue Variante überspringt Leerzeilen!

Antiqua Team-Icon

Avatar von Antiqua

Anmeldungsdatum:
30. Dezember 2008

Beiträge: 4534

in /tmp/ls.txt scheinen 3 Leerzeilen zu sein. An einem bash-Promt gibt

for LINE in `cat test.txt`; do echo $LINE; done

nur Zeilen aus, in denen mehr als ein /n steht, im Gegensatz zu

cat test.txt

das gibt auch die (bis auf das Newline leeren) Zeilen aus

es ist übrigens mE. eine gute Idee, daß man das Umbiegen von $IFS nur direkt vor der Funktion macht und das gleich wieder zurückstellt.
Also z.B.

...
OLDIFS=$IFS
IFS=$'\012'
for LINE in bla ... ;
do
    ...
done
IFS=$OLDIFS
...




eventuell kannst du ja mal eine Beispieldatei geben und mal genau sagen, was du überhaupt bezweckst... Ich bin mir sicher, daß sich das Problem irgendwie einfacher, bzw. ohne so üble Hacks wie $IFS verbiegen, lösen lässt. Zur Not mit einer anderen Programiersprache 😉

twohead

(Themenstarter)

Anmeldungsdatum:
31. Januar 2009

Beiträge: 51

Jo klar wäre das in Perl schöner. Ich brauche sowas aber in letzter Zeit relativ häufig in kleinsten Bash Skripten. Kannte das noch gar nicht und das wird doch höchstens für den Aufruf des Skript verbogen. Finde das ne interessante Sache. Wofür genau steht denn nun das Zeichen IFS=$'\012'?

LG

Greebo

Avatar von Greebo

Anmeldungsdatum:
21. November 2006

Beiträge: 3443

Wohnort: 97070 Würzburg

twohead schrieb:

Jo klar wäre das in Perl schöner. Ich brauche sowas aber in letzter Zeit relativ häufig in kleinsten Bash Skripten. Kannte das noch gar nicht und das wird doch höchstens für den Aufruf des Skript verbogen. Finde das ne interessante Sache. Wofür genau steht denn nun das Zeichen IFS=$'\012'?

LG

Hat Antiqua doch schon hier geschrieben als Separator wird das Zeichen \012 (oktal?) angegeben, was nem Linefeed entspricht. Damit trennt das for seine Datensätze Zeilenweise statt Whitespaceweise. Falls AWK zur Verfügung steht würde ich übrigens das vorschlagen. AWK ist genau für solche Sachen gedacht.

Antiqua Team-Icon

Avatar von Antiqua

Anmeldungsdatum:
30. Dezember 2008

Beiträge: 4534

$IFS ist eine Builtin Variable der bash (s. http://tldp.org/LDP/abs/html/internalvariables.html#IFSREF)

man bash

IFS    The Internal Field Separator that is used for word splitting after expansion
              and to split lines into words with the read builtin command. The default
              value is ``<space><tab><newline>''.

und das \012 ist einfach die oktale Schreibweise von \n und das ist das ASCII-Steuerzeichen für LineFeed (s. ASCII control characters) und wird durch IFS=$'\012' eben der Internal-Field-Separator-Variablen zugewiesen. Dadurch wird das <leerzeichen> und \t als Feldseparator entfernt und nur noch \n "scharfgeschaltet".

twohead

(Themenstarter)

Anmeldungsdatum:
31. Januar 2009

Beiträge: 51

Greebo schrieb:

Hat Antiqua doch schon hier geschrieben als Separator wird das Zeichen \012 (oktal?) angegeben, was nem Linefeed entspricht. Damit trennt das for seine Datensätze Zeilenweise statt Whitespaceweise. Falls AWK zur Verfügung steht würde ich übrigens das vorschlagen. AWK ist genau für solche Sachen gedacht.

Das hab ich glatt übersehen. Hatte bloss den kompletten Codeblock darunter im Blick. Danke.

Großes Danke auch an Antiqua für die nochmals ausführlichere Erklärung. Super Hilfe, Dankeschön!

Awk steht zur Verfügung. Hab ich auch mal sehr kurz drauf geguckt. Aber hab ich noch nie richtig benutzt. Ich will die ersten X Zeilen ignorieren. Ich identifiziere Zeile X an dem Vorhandensein eines String am Anfang

if [ ${LINE:0:9} = "ABSCHNITT" ] ; then
  break;
fi

Dann steht der Counter auf der entsprechenden Zeilennummer und ich kann mit sed 1,${COUNTER}d den ersten Abschnitt wegschmeißen. Wie würde das mit awk gehen?

Antworten |