Variable aus Funktion in Skript auslesen

Vom einfachen Programm zum fertigen Debian-Paket, Fragen rund um Programmiersprachen, Scripting und Lizenzierung.
Antworten
Benutzeravatar
speefak
Beiträge: 532
Registriert: 27.04.2008 13:54:20

Variable aus Funktion in Skript auslesen

Beitrag von speefak » 16.06.2021 14:47:18

Moin,

declare, export sind die gebräuchlichsten Befehle um Strings durchzureichen. Dies scheint aber nur in eine Richtung zu gehen und zwar vom Parent zum Child Prozeß, sprich vom Script in die Funktion. in die andere Richtung gestaltet sich das ganze recht eigensinnig. Entweder man führt die Funktion in einer Variable aus und hat so die Funktionsausgabe in der Variablen, oder die Ausgabe der Variable in der Funktion wird in eine Datei umgeleitet, die im Script dann wieder ausgelesen wird. Letzteres dauert aber länger und erfordert mehr Operationen ( Auslesen, auf SSD schrieben , einlesen , in variable schrieben ) In Schleifen mit einigen 10000 Zeilen summiert sich da jeder Sekundenbruchteil.

Konkret geht es um folgende Funktion :

Code: Alles auswählen

create_parsed_file () {

	echo "1" > $PIDFile

	SAVEIFS=$IFS
	IFS=$(echo -en "\n\b")
	for i in $(cat $GpxFile | grep -v time) ; do										# processing all lines exerpt timestamp lines
		if [[ -n $( grep ele <<< $i) ]] ; then
			echo "$i" 														# print elevation lines 
			echo -en "\t<time>$(date +%FT%H:%M:%S.835Z -d @$Timestamp)</time>\n"	# print timestamp line
			Timestamp=$[$Timestamp + $Timestampinterval]							# raise timestamp when adding timestamp line to file		
		else
			echo "$i"															# print all lines exerpt elevation
		fi

		# calculate progressbar values  # TODO write counter to var instead of file
		ProccessingLineCounter=$[$ProccessingLineCounter + 1]						# proccessing line counter for progressbar calculation	
		sed 's/.*/'$ProccessingLineCounter'/' -i $PIDFile

#		echo $ProccessingLineCounter > $PIDFile
#		echo $ProccessingLineCounter  >&2 export counter to parent script

	done
	IFS=$SAVEIFS

	sleep 0.2
	rm $PIDFile
}
Um eine Fortschrittsanzeige zu implementieren werden zwei Werte benötigt: 1. Gesamtzeilen der Quelldatei und 2. aktuell bearbeitete Zeile
Mit den beiden Werten sind prozentualer Fortschritt und Ladebalken recht einfach umzusetzen.

Der Knackpunkt ist allerdings das Schreiben des Schleifenzählers ( $ProccessingLineCounter ) aus der im Hintergrund laufenden Funktion ( & ) in eine Datei und das Auslesen der Datei im Script selbst um die Fortschrittsanzeige zu berechnen.

Code: Alles auswählen

 sleep 0.2
 GpxFileLineCount=$(cat "$GpxFile" | grep -cv time )
 while	[[ -f $PIDFile ]]; do
	#PercentProcessing=$(cat $PIDFile)
	#Percent100=$(cat $GpxFile | grep -v time)
	#BarLength=20
	progressbar $(cat $PIDFile) $GpxFileLineCount 20 && sleep 0.1
 done
 printf " ${CHECK_MARK}\n\n" 
Ich habe testweise mal einige Befehle in der Funktion auskommentiert und geschaut was am meisten Zeit kostet. Ich dachte erst es seinen die Subshells $(cmd) innerhalb der Schleife ( if [[ -n $( grep ele <<< $i) ]] ; then ). Das sind aber keine 5% Zeitersparnis. Merklich schneller läuft das Ganze wenn die E/A Prozesse auf die SSD reduziert werden.

Ich hatte erst die Ausgabe der Funktion (create_parsed_file) direkt in ein Datei umgeleitet und die Funktion später einfach aufgerufen: create_parsed_file &

Code: Alles auswählen

funktion () {
echo foo
echo bar 
} > result.file 
Dabei wird nach jeder Zeile ein E/A Prozeß ausgelöst. Lege ich die Schleifenausführung in eine Variable und schreibe diese dann einmalig in eine Datei macht das knapp 15% Zeitersparnis aus

Code: Alles auswählen

echo "$(create_parsed_file)" > "$GpxOutputFile" & 


Problem dabei : Die Variable kann erst weiterverarbeitet werden wenn die Funktion/Schleife komplett durchlaufen ist. Für die Fortschrittsanzeige wird aber ein aktueller, auslesbarer Wert nach jedem Schleifendurchlauf benötigt.

Ein anderer Versuch war über zwei versch. Ausgaben ( stdout und stderr ) die Counterausgabe von der eigentlichen Ausgabe zu trennen. Das geht aber wieder nicht weil ich stderr entweder direkt im Terminal sehe oder stderr erst nach Schleifenende auslesbar ist. Gibt es eine Möglichkeit stderr direkt auszulesen WÄHREND die Funktion noch läuft ? Sowas in der Art :

Code: Alles auswählen

echo "$(create_parsed_file 2> $VARforWHILELOOP)" > "$GpxOutputFile" & 
Oder hat sonst jemand eine Vorschlag wie ich eine in einer Funktion gesetzen Variable im Skript selbst auslesen kann OHNE den Umweg über eine Datei zu gehn ?

mludwig
Beiträge: 807
Registriert: 30.01.2005 19:35:04

Re: Variable aus Funktion in Skript auslesen

Beitrag von mludwig » 16.06.2021 15:25:45

und wenn du die Variable im Skript definierst, und dann in der Funktion lediglich modifizierst? Oder geht das in deinem Fall nicht?

Benutzeravatar
heinz
Beiträge: 535
Registriert: 20.12.2007 01:43:49

Re: Variable aus Funktion in Skript auslesen

Beitrag von heinz » 16.06.2021 18:29:07

Hallo speefak,

ich verstehe es so:
Alles, was in der Funktion ausgegeben wird, wird komplett umgeleitet sodas die Funktion keinerlei Ausgaben in die Konsole macht.
Du moechtest eine Fortschrittsanzeige, die anzeigt wie weit die Funktion ist.

Wenn dem so ist und Du nicht unbedingt die Fortschrittsanzeige selbst Coden moechtest, wuerde ich Dir einen kleinen Umbau vorschlagen:
(Ungetestet)

Code: Alles auswählen

#!/bin/bash

function create_parsed_file
{
	while read
	do
# print all lines exerpt elevation
		echo "$REPLY"
		if [[ -n $( grep ele <<< "$REPLY") ]] ; then
# print timestamp line
			echo -en "\t<time>$(date +%FT%H:%M:%S.835Z -d @$Timestamp)</time>\n"
# raise timestamp when adding timestamp line to file
			let Timestamp+=$Timestampinterval
		fi
	done
}


pv "$GpxFile" | grep -ve time | create_parsed_file > "$GpxOutputFile"
Fuer die Fortschrittsanzeige wuerde in diesem Fall das Programm pv sorgen.

Gruss,
heinz

JTH
Moderator
Beiträge: 3077
Registriert: 13.08.2008 17:01:41
Wohnort: Berlin

Re: Variable aus Funktion in Skript auslesen

Beitrag von JTH » 17.06.2021 13:56:57

Ich gebe mal meinen Senf dazu :) Wegen des Test mit doppelten [[ ]] geh ich mal davon aus, das du Bash, nicht Posix-Shell benutzt.

(Nachtrag: Hatte anfangs überlesen, dass du das Dateischreiben schon als Flaschenhals eingegrenzt hast.)

speefak hat geschrieben: ↑ zum Beitrag ↑
16.06.2021 14:47:18
In Schleifen mit einigen 10000 Zeilen summiert sich da jeder Sekundenbruchteil.
Du möchtest ja anscheinend die Performance optimieren. Aus meiner Erfahrung wirkt es sich bei langen Schleifen, großen Datenmengen negativ aus, wenn man für Kleinigkeiten unnötig immer wieder externe Kommandos aufruft (grep, cat, sed, …). Besser und schneller kann es oft sein, Shell-Interna zu benutzen:

speefak hat geschrieben: ↑ zum Beitrag ↑
16.06.2021 14:47:18

Code: Alles auswählen

	for i in $(cat $GpxFile | grep -v time) ; do

 GpxFileLineCount=$(cat "$GpxFile" | grep -cv time )
 
 	#Percent100=$(cat $GpxFile | grep -v time)
cat DATEI | grep SUCHMUSTER ist eigentlich immer ein Antipattern. grep kann selbst Dateien öffnen:

Code: Alles auswählen

grep SUCHMUSTER DATEI
Außerdem erzeugt die Pipe | dort für jeden Aufruf zusätzliche Subshells.


speefak hat geschrieben: ↑ zum Beitrag ↑
16.06.2021 14:47:18

Code: Alles auswählen

sed 's/.*/'$ProccessingLineCounter'/' -i $PIDFile
Ein sed, nur um den Inhalt einer einzeiligen Datei zu ersetzen, dürfte auch Kanonen auf Spatzen sein. An so einer Stelle dürfte doch folgendes reichen?!

Code: Alles auswählen

echo "$ProccessingLineCounter" >"$PIDFile"

speefak hat geschrieben: ↑ zum Beitrag ↑
16.06.2021 14:47:18

Code: Alles auswählen

echo -en "\t<time>$(date +%FT%H:%M:%S.835Z -d @$Timestamp)</time>\n"
Neuere Bash kann Datum/Zeit mit printf selbst formatieren – ein Fork weniger:

Code: Alles auswählen

printf "\t<time>%(%FT%H:%M:%S.835Z)T</time>\n" "$Timestamp"
Ich hoffe, das tut exakt das gleiche. Aber sehr wahrscheinlich benutzt date, wie bash es laut manpage tut, strftime.


speefak hat geschrieben: ↑ zum Beitrag ↑
16.06.2021 14:47:18

Code: Alles auswählen

		if [[ -n $( grep ele <<< $i) ]] ; then
Das könnte man auch – ohne Fork und Subshell – durch Patternmatching lösen:

Code: Alles auswählen

		if [[ "$i" == *ele* ]] ; then


Am Rande: Sowas

Code: Alles auswählen

echo "$(create_parsed_file 2> $VARforWHILELOOP)" > "$GpxOutputFile" & 
ist eher Käse. Das wäre (vereinfacht) ähnlich wie

Code: Alles auswählen

echo "$(echo foobar)" >datei
Mit extra, eigentlich ja ungewolltem Buffering durch die Commandsubstitution (glaube ich).


Zu deinem eigentlichen Problem:
speefak hat geschrieben: ↑ zum Beitrag ↑
16.06.2021 14:47:18
Letzteres dauert aber länger und erfordert mehr Operationen ( Auslesen, auf SSD schrieben , einlesen , in variable schrieben )
Solche Kleinigkeiten werden sehr wahrscheinlich nicht direkt auf die Festplatte geschrieben, sondern landen erstmal nur im Cache. Trotzdem gibts vielleicht bessere oder auch elegantere Möglichkeiten. Einfallen würde mir, wie du schon überlegt hast, geschicktes Nutzen von stdout und stderr verbunden mit Processsubstitution oder einer Named Pipe:

Ein vereinfachtes Beispiel für ersteres:

Code: Alles auswählen

#!/bin/bash
set -eu

M=$2
N=$1

create_parsed_file()
{
	for ((i = 0; i < N; ++i)); do
		if [[ $((RANDOM % M)) == 0 ]]; then
			# Nutzdaten nach stdout:
			printf "\t<time>%(%FT%H:%M:%S.835Z)T</time>\n" -1
		fi

		# Fortschritt nach stderr:
		echo "$i" >&2
	done
}

progress()
{
	# Mach irgendwas mit dem Fortschritt, die Ausgabe könnte hier auch problemlos nach stderr:
	while read n; do
		echo "$n/$N"
	done
}

# Nutzdaten in die Datei, Fortschritt als stdin für progress:
create_parsed_file >"$GpxOutputFile" 2> >(progress)

Alternative wäre eine Named Pipe, falls die letzte Zeile noch irgendwelche Probleme für das was du vorhast macht:

Code: Alles auswählen

fifo=$(mktemp -d)/progress
mkfifo "$fifo"

progress <"$fifo" &
create_parsed_file >"$GpxOutputFile" 2>"$fifo"

rm -f "$fifo"
rmdir "${fifo%/*}"
Manchmal bekannt als Just (another) Terminal Hacker.

Benutzeravatar
speefak
Beiträge: 532
Registriert: 27.04.2008 13:54:20

Re: Variable aus Funktion in Skript auslesen

Beitrag von speefak » 21.06.2021 09:43:16

Danke für die Tipps ;) In den nächsten Tagen habe ich Zeit das umzusetzen und schauen ob es schneller läuft.

sed ist in der Tat bei sowas nicht optimal. Sed editiert einen vorhandenen Wert , echo schriebt die ganze Datei neu. Das führt bei echo dazu, dass es beim Auslesen und einer unvollständig geschrieben Datei zu Fehlern kommt. Bei Sed nicht.

sed 's/.*/'$ProccessingLineCounter'/' -i $PIDFile => Wert in Datei immer vorhanden => kein Auslesefehler
echo "$ProccessingLineCounter" >"$PIDFile" => Datei / Wert nicht immer vollständig vorhanden => Lesefehler in der Countdownschleife => Abbruch der Schleife.

Darum die Überlegung mit echo statt sed direkt in eine Variable zu schreiben. Tritt o.g. Fehler dann wieder auf bleibt mir wohl nur sed oder die Fehlerausgabe in der While Schleife abzufangen. Mit echo statt sed läuft es bedeutend schneller - bis zum ungünstigen Fall dass echo gerade die Datei schreibt und cat die nicht fertig geschriebene Datei ausliest.

In den nächsten Tagen teste ich die Tips mal, mit grep hätte ich auch drauf kommen können :facepalm: Beim Testen waren sed und date die Zeitfresser.

Antworten