![]() |
Vorlesung "UNIX"von Prof. Jürgen Plate |
Und damit es da gar kein Mißverständnis gibt: Sprechen Sie awk nicht wie das englische "awkward" oder "awful" aus. Sagen Sie einfach "a w k"!
Unter Linux wird die GNU Implementierung gawk benutzt, die außer allen im POSIX-Standard vorgesehenen Features auch die Erweiterungen aus SVR4 unterstützt.
Sie sollen zunächst einen Überblick der Sprache erhalten. In den
folgenden Abschnitten werden dann die einzelnen Sprachkonstrukte etwas detaillierter
behandelt.
Die Sprache awk ist recht einfach. Man kann Sie ohne weiteres aus der
ziemlich umfangreichen Man-Page erlernen. Da es sich um einen Interpreter
handelt, sind awk-Programme nicht besonders schnell. Man sollte also keine zu
großen und komplexen Anwendungen in awk formulieren. Für Prototypen und
kleine Toos ist awk aber bestens geeignet.
awk [Optionen] [--] Programmtext Datei ...
Folgendes Beispiel gibt Namen der Benutzer und das jeweilige Home-Verzeichnis aus:
awk -F: '{print $5, $6}' /etc/passwdDabei gibt die Option
-F:an, daß der Doppelpunkt als Trennzeichen zwischen Feldern aufgefaßt werden soll. Das eigentliche awk-Programm besteht aus
{print $1, $6}Das eigentliche awk-Programm muss vor der Auswertung durch die Shell geschützt werden, deshalb die Einbettung in einfache Hochkommata. Die geschweiften Klammern gehören zum Programm und sind auch wichtig, wie wir später noch sehen werden.
Im awk werden Anweisungen, ähnlich der Shell, durch Semikolon getrennt. Damit kann man auch mehrere Anweisungen auf der Kommandozeile unterbringen. Trotzdem ist die Grenze der Übersichtlichkeit schnell erreicht und man schreibt das awk-Skript lieber in eine Datei. Der Programmaufruf sieht dann so aus:
awk [Optionen] -f Programmdatei [--] Datei ...
Üblicherweise wird das Ergebnis eines awk-Aufrufs auf die Standardausgabe (was meistens der Bildschirm ist) geschrieben. Ist das nicht gewünscht, so muß die Ausgabe auf eine Ergebnisdatei umgeleitet oder in ein weiteres Programm gepipt werden.
Zur Demonstration wird im folgenden Programm das erste Feld der Passwortdatei ausgeben (der Feldseparator ist der Doppelpunkt), wobei die Zeilen zusätzlich numeriert werden:
awk -F : '{print NR,$1}' /etc/passwd 1 root 2 bin 3 daemon 4 lp 5 news ...
Bedingung1 { Aktion1 }
Bedingung2 { Aktion2 }
Bedingung3 { Aktion3 }
...
Bedingungn { Aktionn }
Als Bedingung wird meist ein Suchmuster (pattern) oder regulärer Ausdruck angegeben.
Aktionen sind immer in geschweifte Klammern eingeschlossen, wobei die öffnende Klammer immer in der gleichen Zeile stehen muß wie das Pattern. Mehrere Anweisungen werden durch Strichpunkt getrennt. Die Aktion wird in einer C-ähnlichen Sprache formuliert. Der Aktionsteil darf sich auch auch über mehrere Zeilen erstrecken. Im übrigen sind awk-Programme frei formatierbar (unter Berücksichtigung der obigen Einschränkungen).
In einer Programmzeile kann entweder die Bedingung oder der Aktionsteil weggelassen werden. Fehlt die Bedingung, wird die Aktion immer ausgeführt. Fehlt die Aktion, wird die Zeile komplett ausgegeben.
Die Kommandozeilenoption im letzten Beispiel hätte man umgehen können, indem vor Ausführung der Kommandos der interne Feldseparator ("FS") auf den Doppelpunkt gesetzt worden wäre. Eine solche "Initialisierung" ist die typische Aufgabe des BEGIN-Musters:
awk 'BEGIN {FS = ":"} {print NR,$1}' /etc/passwd 1 root 2 bin 3 daemon 4 lp 5 news ...
awk zerlegt eine Eingabezeile in Abhängigkeit vom Wert des Feldtrennzeichens FS (eine der awk-Standard-Variablen; siehe dazu 9.3.3) in einzelne Felder, die mit $1, $2, ..., $(NF-1) bezeichnet werden. NF ist eine andere awk-Standard-Variable, in der die Anzahl der Felder der Eingabezeile gespeichert wird. Auf die gesamte Eingabezeile kann mit $0 zugegriffen werden.
Außerdem gibt es die Möglichkeit, Funktionen zu definieren:
function Name(Parameterliste) { Anweisungen }
$1, $2, $3 usw. sind Zeichenketten und können dementsprechend im Aktionsteil manipuliert und verwendet werden.
Mit diesen Vorkenntnissen sollte die Funktionsweise des ersten Programms leicht nachvollziehbar sein; es zählt einfach nur die Zeilen der Eingabe und gibt das Resultat aus:
BEGIN { print "Zählen von Eingabezeilen"; zaehler = 0 } { zaehler++ } END { print "Ergebnis: " zaehler }
Ein weiteres einfaches Beispiel: Gegeben sei eine Datei, die Zeilen der Art
... Emil Hofer 23.08.1960 1234 175 cm Karl Müller 29.02.1957 1236 160 cm ...
mit den Angaben "Vorname", "Nachname", "Geburtsdatum", "Nebenstellen-Telefonnummer" und "Gewicht" enthält. Es soll eine Ausgabedatei generiert werden, die nur noch Geburtsdatum und Vorname (in dieser Reihenfolge) enthält. awk kennt folgenden AUsgabefunktionen:
{print $3, $1}Da ein Bedingungsteil nicht vorhanden ist, wird die aufgeführte Aktion unabhängig von Bedingungen für alle Eingabezeilen ausgeführt: Mitttels der awk-Standard-Funktion print wird der Inhalt der Felder $3 und $1 in einem Standard-Format ausgegeben.
Das folgende Beispiel druckt eine etwas schöner formatierte Liste aus der /etc/passwd, wobei die C-ähnliche awk-Standard-Funktion printf verwendet wird:
BEGIN { FS=":"; print "-------------------------------------------------------------------"; printf "%-15s %-30s %s\n", "user-id", "Name", "Home"; print "-------------------------------------------------------------------"; } {printf "%-15s %-30s %s\n", $1, $5, $6}
In obigen Beispielen ist die Aktion von keiner Bedingung abhängig. Als Bedingung können jedoch ohne weiteres Vergleiche und logische Ausdrücke angegeben werden, wie im folgenden Beispiel gezeigt wird:
$4>170 && $6<=80 {print $0}Angewendet auf die obige Eingabedatei bedeutet das, daß nur noch diejenigen Eingabezeilen ($0) aufgelistet werden, für die Größe>170 ($4>170) und (&&) Gewicht<=80 ($6<=80) gilt.
/M[ea][iy]e?r/ {print $2, $3}Als Bedingung ist hier ein regulärer Ausdruck aufgeführt, mit dem alle Eingabezeilen erkannt werden, die den Namen "Meier" (mit möglichen Varianten wie "Mayer", "Maier", "Meyr", "Meir", "Mayr" usw.) enthalten (Das Fragezeichen im regulären Ausdruck deutet an, daß das unmittelbar davor aufgeführte Element auch entfallen kann). Für die betreffenden Zeilen wird dann der Inhalt der Felder $2 und $3 ausgegeben.
$5 !~ /^[0-9][0-9][0-9]$/ {print $1, $2, $5}Mit Hilfe der hier verwendeten Bedingung kann überprüft werden, ob das Eingabefeld $5 (Körpergröße der Datei aus dem ersten Beispiel) einer Eingabezeile eine für ein bestimmtes Problem korrekt gebildete Zahl enthält. Genauer gesagt, es wird getestett, ob ein angegebenes Muster nicht (!~) auf das Eingabefeld $5 "paßt".
In den bisherigen Programmbeispielen wurden die awk-Standard-Funktionen print und printf verwendet. Eine fast ebenso häufig eingesetzte Funktion ist gsub, mit der Zeichenketten in einer Zeile gegen andere ausgetauscht werden können. Dazu ein Beispiel:
/ä/ {gsub(/ä/,"ä")} /Ä/ {gsub(/Ä/,"Ä")} /ö/ {gsub(/ö/,"ö")} /Ö/ {gsub(/Ö/,"Ö")} /ü/ {gsub(/ü/,"ü")} /Ü/ {gsub(/Ü/,"Ü")} /ß/ {gsub(/ß/,"ß")} {print}Mit Hilfe der in den Bedingungen angegebenen Muster für die deutschen Umlaute und das scharfe S und der awk-Funktion gsub werden in den betreffenden Zeilen diese Zeichen gegen die entsprechenden HTML-Umschreibungen ausgetauscht. Die letzte Programmzeile ist notwendig, damit das Ergebnis der vorgenommenen Umsetzungen auch die Standardausgabe ausgegeben wird.
Das folgende Beispiel zeigt ein Mini-Programm in der Kommandozeile:
awk ' {printf("%s-%4d %s\n", FILENAME, FNR, $0) }' eingabe*Es werden die Zeilen aller Dateien, deren Name mit "eingabe" beginnt, numeriert und mit dem Dateinamen auf der Standardausgabe aufgelistet.
Umbenennen von Dateien (append ".new" to "files_list"):
ls files_list | awk '{print "mv "$1" "$1".new"}' | sh
awk -v 'FS=^\"?|\"?,\"?|\"?$' '{print $3, $2}' dateiHier wird die Zuweisung der Variable FS auf der Kommandozeile vorgenommen. Der oben angegebene Ausdruck zerlegt CSV-Dateien, wie sie von Excel oder dBase exportiert werden, in einzelne Felder.
Ein Vektorelement wird durch vektor [index] angesprochen oder initialisiert.
Beispiel: Zählen von Worten
{ gsub(/[.,:;!?(){}-+]/,"") # Interpunktionszeichen weg for (i=1 ; i<=NF ; i++) feld[i]=feld[i]+1 } END { for (i in feld) print i, feld[i] }Mehrdimensionale Vektoren verhalten sich wie eindimensionale Vektoren. Intern werden sie zu solchen mit Hilfe von SUBSEP umgeformt. Die Indizes werden durch Kommata voneinander getrennt. Im Zusammenhang mit Operatoren sollten die Tupel mit runden Klammern eingeschlossen werden, z. B.:
if ((i,j,k) in vektor)
Neben den einfachen Strings kennt awk noch assoziative Arrays. Die Indizes eines solchen Arrays sind (im Gegensatz zu C) Strings.
^ | Beginn einer Zeichenfolge. | ||||||||||||
$ | Ende einer Zeichenfolge. | ||||||||||||
. | Jedes beliebige Zeichen. | ||||||||||||
* | Das vorangehende Muster beliebig oft (auch keinmal). | ||||||||||||
+ | Das vorangehende Muster einmal oder mehrmals. | ||||||||||||
? | Das vorangehende Muster keinmal oder einmal. | ||||||||||||
\ | Sonderbedeutung des nachfolgenden Metazeichens aufheben.
Dazu gehören auch folgende Escape-Sequenzen:
| ||||||||||||
| | Logisches oder. | ||||||||||||
[ab] | Zeichenmenge (a und b). | ||||||||||||
[a-z] | Zeichenmenge (alle Zeichen von a bis z). | ||||||||||||
[^ab] | Verneinte Zeichenmenge (nicht a oder b). | ||||||||||||
( ) | Klammerung für Teile komplexerer Muster |
A | B | Alternation (A oder B) |
A B | Konkatenation (A gefolgt von B) |
awk '/reg. Ausdruck/' Datei
Das Skript kann Kommentare enthalten. Diese beginnen mit dem Zeichen # und erstrecken sich bis zum Zeilenende. Leerzeilen sind zulässig. Ein Statement endet normalerweise am Zeilenende. Um Statements auf der nächsten Zeile fortzusetzen, maskiert man das Zeilenende mit einem Backslash.
Beispiel 1: Der Selektor
((NF > 2 ) || (NF < 8)) && ($0 !~ /^[^#]/ )wählt alle Zeilen aus, die zwischen 3 und 7 Feldern besitzen und keine Kommentarzeilen sind (also mit "#" beginnen).
Mit "Zeichenkette ~ /Muster/" lassen sich beliebige Zeichenketten vergleichen. So extrahiert nachfolgendes Awk-Programm alle Benutzernamen aus der Datei /etc/passwd, deren Heimatverzeichnis unterhalb von /home liegt:
awk -F ':' '$6 ~ /^\/home/ {print $1}' /etc/passwdDie zu vergleichende Zeichenkette ist hier das 6. Feld ($6) der Passwortdatei (Heimatverzeichnis).
#!/usr/bin/awk -f BEGIN { String = ÄÖÜäöü if (String ~ /[A-Za-z]/) print "A-Z funktioniert.";
if (String ~ /[[:alpha:]]/) print "Alpha funktioniert.";
}
[:alnum:] | Alphanumerische Zeichen |
[:alpha:] | Alphabetische Zeichen |
[:blank:] | Leerzeichen und Tabulatoren |
[:cntrl:] | Steuerzeichen |
[:digit:] | Numerische Zeichen |
[:graph:] | Druck- und sichtbare Zeichen (Ein Leerzeichen ist druckbar aber nicht sichtbar, wogegen ein "a" beides ist) |
[:print:] | Druckbare Zeichen (also keine Steuerzeichen) |
[:punct:] | Punktierungszeichen Punctuation characters (Zeichen die keine Buchstaben, Zahlen, Steuerzeichen oder Leerzeichen sind, z. B. ".", "," ":") |
[:space:] | Druckbare aber nicht sichtbaren Zeichen (Leerzeichen, Tabulatoren, Zeichenende, etc.) |
[:lower:] | Kleinbuchstaben |
[:upper:] | Großbuchstaben |
[:xdigit:] | Hexadezimale Zeichen (0-9,A-F,a-f) |
Um zu testen, ob in einer Variablen eine gültige Zahl (ganzzahlig) gespeichert ist, bietet sich folgendes Konstrukt an:
echo "4711" | awk '/^[[:digit:]]+$/ {print "eine Zahl"}'
Operator | Bedeutung |
---|---|
(...) | Gruppierung |
$ | Feldreferenz |
++ -- | Inkrement und Dekrement, in postfix- oder präfix-Notation |
^ | Potenzieren |
+ - ! | unäres plus, minus und logische Negation |
* / % | Multiplikation, Division, Modulo-Funktion |
+ - | Addition, Subtraktion |
space | Stringverkettung |
< > <= >= == != | Vergleiche |
~ !~ | Vergleich mit regulären Ausdrücken; rechter String im linken enthalten/nicht enthalten |
in | Test auf Enthaltensein in einem Array |
&& | Logisches Und |
|| | Logisches Oder |
? : | Bedingter Ausdruck, wie in C (A1 ? A2 : A3) |
= += -= *= /= %= ^= | Zuweisung, wie in C |
Ausdruck1; while (Ausdruck2) { Anweisung; Ausdruck3 }
Anweisung | Bedeutung |
---|---|
close(Datei) | Datei (oder Pipe) schließen. |
getline | nächste Zeile in $0 laden. |
getline <Datei | Nächste Zeile aus bestimmter Datei lesen. |
getline Variable | Nächste Zeile in Variable, statt $0, laden. |
getline Variable <Datei | Zeile aus Datei in Variable laden. |
next | Nächste Zeile lesen und ab Anfang des Skriptes bearbeiten. |
nextfile | Aktuelle Datei schließen und mit nächster fortfahren. |
Gibt den aktuellen Satz aus. | |
print Ausdrucksliste | Gibt Ergebnis der Ausdrücke aus. Auch: print (...). |
print Ausdrucksliste > Datei | Schreibt Ergebnis der Ausdrücke in Datei. |
print Ausdrucksliste | Programm | Schreibt Ergebnis der Ausdrücke in eine Pipe. |
printf Format, Ausdrucksliste | Gibt Ergebnisse formatiert aus. Auch: printf (...). |
printf Format, Ausdrucksliste > Datei | formatierte Ausgabe in Datei. |
printf Ausdrucksliste | Programm | Schreibt Ergebnis der Ausdrücke in eine Pipe. |
fflush([Datei]) | Erzwingt das Schreiben der Puffer |
Bei der Ausgabe werden die Variablen $0, OFS und ORS berücksichtigt.
Zur Ausgabe in Dateien werden die Zeichen der Ein- und Ausgabeumleitung
von UNIX verwendet. Dateinamen müssen in Anführungszeichen (" ")
eingeschlossen werden!
Beispiel: (Entsprechend des Wertes der Zahl in der ersten Spalte der Datei
soll die Datei in zwei Dateien zerlegt werden.
awk '$1 > 100 { print > "klein"}; $1 <= 100 {print > "gross"}'Die Ausgabe einer print- bzw. printf-Anweisung kann sogar in eine Pipe geschrieben werden. Das folgende Programm sortiert alle Zeilen einer Datei die den String "test" enthalten:
awk '/test/ { print | "sort" }'
Dateien, die Sie öffnen, sollten Sie auch wieder schließen. Zum einen dürfen Sie meistens nur eine endliche Zahl von Dateien gleichzeitig geöffnet haben, zum anderen kann es Probleme geben, wenn die Dateien auch von anderen Prozessen verwendet werden sollen (Logfiles, Infodateien etc.). Auch Pipes sollten wieder geschlossen werden! Sie erreichen dies mit:
close(dateiname)
close(kommando)
Das Format für printf stimmt im wesentlichen mit dem für die entsprechende C-Funktion überein. Die printf-Kontrollzeichen (werden immer mit einem Prozentzeichen eingeleitet):
Zeichen | drucke den entsprechenden Ausdruck als |
---|---|
c | ASCII-Zeichen |
d | Integerwert (dezimal) |
e | [-]d.ddddddE[+-}dd |
f | [-]ddd.dddddd |
g | e oder f Format (wählt das kürzere) |
o | Oktalwert (ohne Vorzeichen) |
s | Zeichenkette |
x | Hexadezimalwert (ohne Vorzeichen) |
% | Verwendet kein Argument! gibt ein % aus! |
Ausdruck | setzt folgende Variablen |
---|---|
getline | $0, NF, NR, FNR |
getline variable | variable, NR, FNR |
getline <dateiname | $0, NF |
getline variable <dateiname | variable |
kommando | getline | $0, NF |
kommando | getline variable | variable |
awk -F: 'match($7,"/bin/bash")' /etc/passwd
Die Funktion systime() liefert die Systemzeit in Sekunden seit dem 1.1.1970 0.00 Uhr.
Die Funktion strftime(Format [, Zeit]) liefert eine formatierte Zeitausgabe. Ausgabe der Systemzeit, wenn nur der Formatstring angegeben wird.
Beispiel:
BEGIN { start = systime } { for(i = 1; i > 100000; i++)} END { end = systime; print "Gesamtzeit betrug " end - start " Sekunden." }
Funktion | Beschreibung |
---|---|
atan2(x,y) | Arcustangens von x/y (Im Bereich von -Pi bis +Pi) |
cos(x) | Cosinus von x |
exp(x) | Exponentialfunktion von x (e hoch x) |
int(x) | ganzzahliger Anteil von x |
log(x) | natürlicher Logarithmus von x |
rand() | Zufahlszahl im Bereich 0 <= r < 1 |
sin(x) | Sinus von x |
sqrt(x) | Quadratwurzel von x |
srand([x]) | Ausgangswert für rand ändern |
function name(par1,par2, ...) { Anweisungen }Die öffnende Klammer muß direkt auf den Funktionsnamen folgen. Der Funktionsname muß eindeutig gewählt werden. Die Parameterliste inst eine Folge von Variablennamen, die durch Komma getrennt sind. Parameterübergabe erfolgt bei skalaren Variablen als call-by-value, bei Arrays hingegen als call-by-reference. Daher lassen sich die Array-Inhalte innerhalb der Funktion ändern. Parameter sind lokale Variablen, globale Variable gleichen Namnes sind daher in der Funktion nicht zugreifbar.
Benutzerdefinierte Funktionen sind typlos, die gleiche Funktion kann ganze Zahlen, Gleitpunktzahlen oder Strings zurückgeben. Die Funktione wird beendet, wenn die letzte Anweisung abgearbeitet wurde. Üblicherweise wir eine Funktion mit der Anweisung
return [Ausdruck]beendet. Ist ein Ausdruck angegeben, wird der Wert dieses Ausdrucks an das aufrufende Programm zurückgegeben.
Innerhalb der Funktion neu eingeführte Variablen sind global. Die Funktionsargumente sind nur im Funktionsrumpf gültig. Neue Variablen, die im Rumpf definiert werden, sind global gültig. Werden beim Aufruf weniger Parameter als vorgeschrieben angegeben, so werden die fehlenden Parameter mit "" initialisiert.
awk '...' flag=1 datei flag=0 datei
Wertzuweisung erfolgt zu dem Zeitpunkt, zu dem auf eine durch den Parameter angegebene Datei zugegriffen würde
-F Feldtrenner | awk arbeitet auf Feldern einer Eingabezeile. Normalerweise dient das Leerzeichen/Tabulator zur Trennung einzelner Felder. Mit der Option -F wird der Wert der internen Variable FS (field separator) verändert. |
---|
-v Variable=Wert | Eine im Programm verwendete Variable kann somit "von außen" initialisiert werden (eine interne Initialisierung wird damit nicht überschrieben). |
-f Programmdatei | awk liest den Quellcode aus der angegebenen Datei. |
-W compat | GNU awk verhält sich wie UNIX awk, d.h. die GNU-Erweiterungen werden nicht akzeptiert. |
-W help | Eine Kurzanleitung wird ausgegeben. |
-W posix | GNU awk hält sich exakt an den POSIX-Standard. |
Maier 3.3 Mueller 1.3 Huber 2.3 Maier 4.3 Huber 2.6 Maier 4.6Das folgende Programm berechnet die Durchschnittsnote für jeden Studenten:
{ sum[$1] += $2 ; oft[$1] += 1 } END { for (Student in sum) print Student, sum[Student]/oft[Student] }Die Programmausgabe für das Beispiel lautet:
Huber 2.45 Mueller 1.3 Maier 4.06667
BEGIN {FS="[^a-zA-Z0-9äÄöÖüÜáàâéèêíìîóòôúùû]"} {for (j=1; j<=NF; j++) {anzahl [$j] ++} } END {for (j in anzahl) {printf ("%-40s %4d\n", j, anzahl [j] ) } }
BEGIN { # Variablen setzen BEGIN_MSG = "From" BEGIN_BDY = "Precedence:" MAIN_KEY = "Subject:" VALIDATION = "[MONATSBERICHT]" HEAD = "NO"; BODY = "NO"; PRINT="NO" OUT_FILE = "Month_Reports" } { # keine Bedingung, also alle Zeilen bearbeiten if ( $1 == BEGIN_MSG ) { HEAD = "YES"; BODY = "NO"; PRINT="NO" } if ( $1 == MAIN_KEY ) { if ( $2 == VALIDATION ) { PRINT = "YES" $1 = ""; $2 = "" print "\n\n"$0"\n" >> OUT_FILE } } if ( $1 == BEGIN_BDY ) { getline if ( $0 == "" ) { HEAD = "NO"; BODY = "YES" } else { HEAD = "NO"; BODY = "NO"; PRINT="NO" } } if ( BODY == "YES" && PRINT == "YES" ) { print $0 >> OUT_FILE } }
expr -> term expr + term expr - term term -> factor term * factor term / factor factor -> number ( expr )Die obige Grammatik beinhaltet auch die unterschiedliche Wertigkeit und die Assoziativität der Operatoren. Das zugehörige Programm sieht folgendermaßen aus:
# Infix Rechner # Das Hauptprogramm wird nur auf die aktuelle Eingabezeile # angewendet, wenn sie mehr als ein Feld beinhaltet. D. h. # in jeder Zeile darf nur ein Ausdruck stehen und seine # Operanden muessen von Leerzeichen eingeschlossen werden. # z. B. ( 5 + 2 ) * 3 NF > 0 { f = 1 e = expr() if (f <= NF) printf("error at %s\n", $f) else printf("\t%.8g\n", e) } # Unterprogramme, die zur Erkennung der Struktur des # Ausdrucks verwendet werden. function expr( e) { # term | term [+-] term e = term() while ($f == "+" || $f == "-") e = $(f++) == "+" ? e + term() : e - term() return e } function term( e) { # factor | factor [*/] factor e = factor() while ($f == "*" || $f == "/") e = $(f++) == "*" ? e * factor() : e / factor() return e } function factor( e) { # number | (expr) if ($f ~ /^[+-]?([0-9]+[.]?[0-9]*|[.][0-9]+)$/) { return $(f++) } else if ($f == "(") { f++ e = expr() if ($(f++) != ")") printf("error: missing ) at %s\n", $f) return e } else { printf("error: expected number or ( at %s\n", $f) return 0 } }
![]() |
![]() |