Shell-Skripte - Zeile aus Datei

Kamikaze

Warrior of Sunlight
Teammitglied
Kennt jemand eine zügige Möglichkeit einzelne Dateien aus Zeilen zu ermitteln?

Ich kenne nur folgende:
Code:
head -n10000 /usr/ports/INDEX|tail -n1

Das dauert jedoch viel zu Lange wenn ich ein paar Hundert Zeilen auslesen will. Am besten wäre etwas das mir Zeilen für eine Liste von Zeilennummern zurückgibt. Im Moment habe ich eine O(1) Alternative gefunden, die Konstant ca. 2 Sekunden braucht. Das ist in Ordnung so lange ich Hunderte von Zeilen fetchen will, aber es kann auch vorkommen, dass ich zumindest ein Dutzend mal einzelne Zeilen fetche, dann wird die Laufzeit ziemlich unangenehm und mit head und tail wäre es deutlich besser.

Natürlich kann ich eine Weiche einbauen, die bis zu einer gewissen Grenze head/tail verwendet und ab dann meine andere Lösung, das finde ich aber nicht sehr elegant.
 
Code:
#! /bin/sh

if [ "$#" -eq "0" ]; then exit 0; fi

I=0
while read LINE; do
  I=$(($I + 1))
  if [ "$I" -eq "$1" ]; then
    echo $LINE
    shift
    if [ "$#" -eq "0" ]; then exit 0; fi
  fi
done
exit 1 # line not found
Das Skript erwartet als Argumente die Zeilennummern (ab 1 gezählt), liest Zeilen von stdin und gibt die jeweiligen Zeilen auf stdout aus.
 
Uah, gerade getestet, hast recht. Eventuell ist tail -n +10000 $DATEI | head -n 1 etwas schneller, da nicht die 9999 vorher ausgegeben werden (man beachte das + bei tail). Ansonsten benutze $SKRIPTSPRACHE, die hoffentlich etwas schneller ist als ein Shellskript.
 
Bin glaub etwas begriffsstutzig:

Kennt jemand eine zügige Möglichkeit einzelne Dateien aus Zeilen zu ermitteln?
Ich kenne nur folgende:
Code:
head -n10000 /usr/ports/INDEX|tail -n1
Das dauert jedoch viel zu Lange wenn ich ein paar Hundert Zeilen auslesen will.
...
Das ist in Ordnung so lange ich Hunderte von Zeilen fetchen will, aber es kann auch vorkommen, dass ich zumindest ein Dutzend mal einzelne Zeilen fetche,
Dieser "Code" zeigt dir ja genau /eine/ Zeile aus einer Datei an (hier Zeile 10000) und nicht mehrere?

Also um sich eben z.B. die Zeile 10000 ausgeben zu lassen, würd ich eher das benutzen:
Code:
sed '10000q;d' /usr/ports/INDEX
(Für eine Zeile und große Datei schnell)

Und dann wird so (beide Arten) natürlich nur eine Zeile ausgegeben, und nicht einzelne Datei(namen).

Am besten wäre etwas das mir Zeilen für eine Liste von Zeilennummern zurückgibt.
Also z.B. Zeile 1000 und Zeile 10000:
Code:
sed -n '1000p;10000p' /usr/ports/INDEX

Im Moment habe ich eine O(1) Alternative gefunden,
Was ist (ein) "O(1)"?
 
Was ist (ein) "O(1)"?
O(1) heißt es dauert immer gleich lang, egal wie viele Zeilen ich haben will. Dieses gleich lang ist aber etwas über 2 Sekunden, was für 1000 Zeilen in Ordnung ist, aber nicht für eine.

Der Tip mit sed sieht gut aus, ich werde das gleich benchmarken.
 
Wie wäre es mit:
Code:
awk 'NR==ZEILENNUMMER { print $0 }' /usr/ports/INDEX
Ist bei mir recht fix…

edit: das lässt sich natürlich auch verodern um mehrere Zeilen zu bekommen.
 
Die sed-Lösung funktioniert super:
Code:
#
# This returns lines from the index where a column exactly matches the
# given strings.
#
# @param 1
#	The name of the variable to return results to.
# @param 2
#	The column to match against.
# @param 3
#	The line break delimited search strings.
#
bsda_pkg:Index.search() {
	local IFS index lines numbers
	IFS='
'

	$this.getIndex index
	# Get the line numbers of matching lines.
	numbers="$(cut -d\| -f$2 "$index" | grep -nFx "$3" | sed 's/:.*/p/1' | rs -C\;)"
	# Get the lines from the index.
	lines="$(sed -n "${numbers%;}" "$index")"

	# Return the matching index lines.
	$caller.setvar $1 "$lines"
	return 0
}

Wenn ich noch irgendwo Performance rauskitzeln will, dann nicht mehr hier.
 
Das sieht aber schon arg kompliziert aus.
Du willst du nur die Zeilen in denen ein Column matcht bekommen?
Wozu da die ganze Nummerierungssache?
Code:
awk -F "|" ' $NUM ~ TERM { print $0 }'
Wobei du NUM und TERM durch die $2 und $3 des sh-skripts ersetzen müsstest (mit awk -v oder direkt in dem String ersetzen).

Die Operation braucht bei mir Bruchteile von Sekunden und verwendet nur ein "basis-tool" (und einen Aufruf), statt fünf ;)

edit: Mist zu spät :D
edit2: wobei ich mit mit der ~ Operation Recht habe, denn es muss ja nur ein Substring matchen, wenn ich das richtig verstanden habe.
 
Ich verwende auch -F, das heißt es geht um eine Liste von Exakt identischen Strings.

Nichts davon darf als regulärer Ausdruck behandelt werden.

So wie ich das sehe müsst ich beide Beispiele auch für jeden Suchstring einzeln aufrufen. Also mal eben 800 Aufrufe statt 5.
 
Ich verwende auch -F, das heißt es geht um eine Liste von Exakt identischen Strings.

Nichts davon darf als regulärer Ausdruck behandelt werden.

So wie ich das sehe müsst ich beide Beispiele auch für jeden Suchstring einzeln aufrufen. Also mal eben 800 Aufrufe statt 5.
Ne, du kannst das verodern, wie oben geschrieben, also:

awk ' foo == bar1 || foo == bar2 || foo == bar3 { ....
 
Es wäre hilfreich, wenn du gleich dein ganzes Problem präsentierst, anstatt Salamitaktik zu betreiben.
Wenn du viele Wörter nachschlagen willst, dann willst du in einer Menge nachschauen (und nicht linear mit allen Wörter vergleichen). Das sieht dann mit awk in etwa so aus:
Code:
BEGIN {
  dict["WORT0"] = 1;
  dict["WORT1"] = 1;
  dict["WORT2"] = 1;
  /* usw. */
}

{ if (dict[$1]) print; } /* bzw. statt $1 die entsprechende Spalte */
 
Es wäre hilfreich, wenn du gleich dein ganzes Problem präsentierst, anstatt Salamitaktik zu betreiben.
Wenn du viele Wörter nachschlagen willst, dann willst du in einer Menge nachschauen (und nicht linear mit allen Wörter vergleichen). Das sieht dann mit awk in etwa so aus:
Code:
BEGIN {
  dict["WORT0"] = 1;
  dict["WORT1"] = 1;
  dict["WORT2"] = 1;
  /* usw. */
}

{ if (dict[$1]) print; } /* bzw. statt $1 die entsprechende Spalte */

Wozu if und print? Ein simples
Code:
dict[$1]
oder
Code:
$1 in dict
tut's doch auch. Ebenso die (langsamere) Variante, die SoulRebel glaube ich vorgeschlagen hatte, die koennte so aussehen:
Code:
/WORT1/ || /WORT2/ || /WORT3/

Ueberhaupt finde ich besonders awk-Einzeiler spannend, die nur aus Pattern bestehen mit Default-Action bestehen.

Ciao, Kili, der sich heute ueber ein Script wundern durfte, das eine Pipeline bestehend aus zwei mal cut(1), einmal grep(1), einmal sed(1) und einmal awk(1) enthielt, was garantiert ein Anzeichen fuer Overtooling ist ;-)
 
So, nach all den Kommentaren habe ich jetzt noch mal daran gebastelt. Bei einer einzigen Suche nach ein paar Hundert Paketen sind Zeitunterschiede zwischen beiden Lösungen deutlich unterhalb der Messtoleranz.

Bei ein paar Hundert Suchen nach einzelnen Paketen liegt awk jedoch um Faktor 1.5 vorn.

Code:
#
# This returns lines from the index where a column exactly matches the
# given strings.
#
# @param 1
#	The name of the variable to return results to.
# @param 2
#	The column to match against.
# @param 3
#	The line break delimited search strings.
#
bsda_pkg:Index.search() {
	local index lines

	$this.getIndex index
	lines="$(awk -F\| "
		BEGIN{
			$(echo "$3" | sed -e 's/^/search["/1' -e 's/$/"] = 1/1')
		}

		\$$2 in search
	" "$index")"

	# Return the matching index lines.
	$caller.setvar $1 "$lines"
	return 0
}

Also, danke noch mal für all die Anregungen.
 
Code:
#
		BEGIN{
			$(echo "$3" | sed -e 's/^/search["/1' -e 's/$/"] = 1/1')
		}
}

Nach laengerem Staunen und Quotingentwirren glaube ich, dass da noch Optimierungspotential bei der Lesbarkeit existiert ;-)

Das " = 1" wird jedenfalls nicht benoetigt. Ein lesender Zugriff auf einen Key eines Arrays sollte in awk(1) eigentlich ausreichen, um den Key fuer dieses Array existieren zu lassen. Beispiel:

Code:
BEGIN { foo["gaga"]; foo["gogo"]; foo["trallafiti"]} $2 in foo

Das funktioniert zumindest mit the one true awk(1), und aus dem Gedaechtnis sollte es auch mit allen anderen funktionieren. Ich habe aber nicht grossartig bei anderen Implementierungen oder bei der Opengroup nachgelesen, ob's wirklich so ist.
 
Inzwischen wurde ncoh ein wenig optimiert, was die Sache vor allem für Suchen nach wenigen Begriffen noch mal deutlich schneller macht. Wenn ich zu Hause bin poste ich das.

Das = 1 habe ich auch herausgeschmissen. Nur wie man das
Code:
$(echo "$3" | sed -e 's/^/search["/1' -e 's/$/"]')
lesbarer gestalten könnte ist mir nicht klar.

Ein
Code:
$(echo "$3" | sed -E 's/(.*)/search["\1"]/1')
ginge natürlich auch noch, aber ich denke das ist zwar kürzer aber auch nicht besser zu lesen und wahrscheinlich weniger effizient.

Die Verschachtelungstiefe ist in dieser Methode übrigens noch recht genügsam. Da gibt es ganz andere eval-Orgien.
 
Inzwischen wurde ncoh ein wenig optimiert, was die Sache vor allem für Suchen nach wenigen Begriffen noch mal deutlich schneller macht. Wenn ich zu Hause bin poste ich das.

Da bin ich schon mal gespannt.


Das = 1 habe ich auch herausgeschmissen. Nur wie man das
Code:
$(echo "$3" | sed -e 's/^/search["/1' -e 's/$/"]')
lesbarer gestalten könnte ist mir nicht klar.

Uebergib die Suchwoerter, wenn's nicht zu viel werden dem awk-Script in der Kommandozeile. Die Spaltennummer kann man dann auch gleich als erstes Argument mit angeben. Hier mal ein schlichtes Beispiel:

Code:
#!/usr/bin/awk -f
BEGIN {
        c = ARGV[++i]
        while (++i < ARGC)
                s[ARGV[i]]
        ARGC=1
}
$c in s

Das laesst sich sogar noch in einen Einzeiler giessen ;-)

Wenn's wegen zu vieler Suchbegriffe nicht reicht, koennte man in der BEGIN-Rule die Begriffe auch selbst aus einer Datei lesen.

Die Verschachtelungstiefe ist in dieser Methode übrigens noch recht genügsam. Da gibt es ganz andere eval-Orgien.

Hatte ich eigentlich schon mal mein cleancvs(1) Script irgendwo gepostet? Das sucht in einem CVS Sourcetree offline nach Files, fuer die ein cvs up `?' ausspucken wuerde, und das Script ist echt gruselig.
 
Also erst mal danke für die weiteren Anregungen. Inzwischen habe ich einen weiteren UseCase abgedeckt (nur eine bestimmte Spalte ausgeben, damit wurde die Wegoptimierung von print hinfällig). Außerdem mache ich mir zu Nutze das Paketnamen und Origins im INDEX eindeutig sind. Damit kann das Skript durch einfaches Mitzählen abbrechen sobald alle Einträge gefunden sind.

Die Übergabe der gesuchten Daten werde ich übrigens nicht per Parameter machen. Die Daten sind in einer einzigen Variable, die im theoretischen Worst-Case etwa 20000 Zeilen enthalten kann. Da wird es mit Parametern schon happig und ich denke Pipes sind für die möglichen Datenmengen deutlich besser geeignet.

So, jetzt ist hier für die Interessierten die aktuelle Version der Funktion. Im Moment arbeite ich übrigens daran die Informationen aus der MOVED-Datei in der Index-Klasse transparent mitzunutzen, so dass die eigentliche Programmlogik damit nichts zu tun haben wird.
Code:
#
# This returns lines from the index where a column exactly matches the
# given strings.
#
# @param 1
#	The name of the variable to return results to.
# @param 2
#	The column to match against.
# @param 3
#	The line break delimited search strings.
# @param 4
#	The optional column to output. If this is omitted the whole line
#	is returned.
#
bsda_pkg:Index.search() {
	local index lines length

	$this.getIndex index
	# The number of lines to search for.
	length=$(echo "$3" | wc -l)

	# Get the matching index lines.
	lines="$(echo "
		BEGIN {
			# Initialize result counter.
			count = 0
			# Create an array with the search strings as indices.
			$(echo "$3" | sed -e 's/^/search["/1' -e 's/$/"]/1')
		}

		# Check whether the current line matches an array element.
		\$$2 in search {
			# Print the requested line.
			print \$${4:-0}
			# Exit if all requested lines have been found.
			if (++count == $length)
				exit 0
		}
	" | awk -F\| -f - "$index")"

	# Return the matching index lines.
	$caller.setvar $1 "$lines"
	return 0
}

Edit: Ich habe es gerade mal ausprobiert. AWK scheint auf eine array-Länge von 6531 begrenzt zu sein. Oder es liegt an der größe des damit als Parameter übergebenen Skriptes. Da muss ich noch eine Lösung finden.

Edit: Lag daran, dass das Skript als Parameter übergeben wurde. Ich füttere das jetzt über stdin ein, damit geht es (code habe ich aktualisiert).
 
Zuletzt bearbeitet:
Zurück
Oben