sed kérdés

Fórumok

Kedves fórumozók!

sed-del szeretnék egy syslog-ng konfigurációs fájlt feldolgozni. A koncepció az, hogy először kiszűröm a konfigfájlból a kommenteket, a különböző @include-szerű direktívákat, majd kitörlöm az újsor karaktereket.

Ezután pedig már relatíve könnyen elemezhetem a konfigurációt. A cél az lenne, hogy az engemet érdeklő kulcsszavakat kiírassam az stdout-ra, minden mást pedig elnyelessek.

Jelenleg egy ilyen paranccsal próbálkozom:

sed -nEe 's:\s+(source)\s+:\1\n:gp' eee

Mivel az egész konfiguráció ekkora már tulajdonképpen egy sor, így a -n-nek nincs sok értelme. Ellenben a sed - mivel van match az adott sorban - kiírja magát az egész sort, így le kell horgonyoznom a patternt a sor elejére és végére:

sed -nEe 's:^.*\s+(source)\s+.*$:\1\n:gp' eee

Ezzel a regex-szel viszont csak egy előfordulást talál, felhetőleg a legutolsót az adott sorban. Hogyan tudnám ezt a limitációt valahogy áthidalni?

Két lehetséges megoldás is működhet:

  • valahogy rávenni a sed-et, hogy a sor azon részeit amire a pattern nem match-cselt, ne írja ki
  • valahogy rávenni a sed-et, hogy több lehetőség esetén a substring csere minden lehetséges match-re fusson le

Per pillanat nem tudom melyik megközelítés lenne egyszerűbb.

Hozzászólások

Miert szeretnel mindenkepp sed-et hasznalni?

Valahogy nekem a grep jobban kezre allna ilyen feladatnal.

Alapvetően azért mert az adott source definíció több sort is igénybe vehet, és az én use-case-emben egy bizonyos source-t kellene kiválasztani, amiben szerepel egy bizonyos típusú source driver. Erre a célra pedig a sed alkalmasabbnak tűnik.

Ha csak simán a source neveket kellene kiszűrni, arra valóban elegendő lenne egy grep parancs is.

Az awk bennem is felmerült, viszont ott is valószínűleg fel kellene osztani több fázisra a feldolgozást. Arra például tökéletesen alkalmas lenne, hogy a kapcsos zárójelek szintjeit számolgassa, az inputot pedig eltegye bufferbe.

Sajnos az awk-hoz nem értek olyan mélyen így adott esetben szükségem lesz segítségre. Most például az érdekelne, hogy ha van egy FS=""-val karakterekre bontott inputom, akkor hogyan tudok rajta egy regex match-el végigiterálni, hogy csak a match-ektől kezdjem a karakterenkénti alaposabb elemzést.

       match(s, r [, a])       Return  the  position  in  s  where the regular
                               expression r  occurs,  or  zero  if  r  is  not
                               present,  and  set  the  values  of  RSTART and
                               RLENGTH.  Note that the argument order  is  the
                               same as for the ~ operator: str ~ re.  If array
                               a is provided, a is cleared and then elements 1
                               through  n  are  filled  with the portions of s
                               that match the corresponding parenthesized sub‐
                               expression in r.  The zero'th element of a con‐
                               tains the portion of s matched  by  the  entire
                               regular    expression   r.    Subscripts   a[n,
                               "start"], and a[n, "length"] provide the start‐
                               ing  index  in  the  string  and length respec‐
                               tively, of each matching substring.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Egyelőre sed-del eddig jutottam. Ez elvileg kiírja azokat a sorokat, ahol source definíció van, az első azt követő kapcsos zárójelig. Sajnos ez nem tökéletes megoldás, mivel ha van még a source definíción belül pár szint kapcsos zárójelekkel, akkor csak az első záró kapcsos zárójelig írja ki a sorokat. Nem tökéletes, de ez is egy lépés előre.

sed -nEe '/source\s[a-zA-Z0-9_\-]+\s\{/,/\};/ p'

Egyébként nem egysoros awk-ra gondoltam, hanem normális awk scriptre, ami valahogy úgy kezdődik, hogy

#!/usr/bin/awk -f

Nagyon komplett programot lehet awk-ban írni, a szövegfeldolgozás pedig kifejezetten hazai pálya az awk-nak, és mindemellett elég gyorsan fut.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Nem is neked írom, csak tódítom. ;)

Az awk a pattern - action halmaz miatt kiválóan alkalmas logok FSM (végesállapotú gép, automata) alapú feldolgozásának készítésére. Pl. egy call centerben van néhány száz operátorod és rengeteg hívásod, aminek a CTI logját lehet egy menetben feldolgozni. A végeredmény lehet az egyes hívások folyamatának a kigyűjtése - bármilyen szempont szerint.

Ezzel csak arra szeretnék utalni, hogy az algoritmus és regexp ismeretek mellé nem árt, ha az ember először meg tudja fogalmazni a feladatot és a célt is.

Wow! Igéretesnek tűnik! Egészen jól működik a source blokkok megkeresésére! Nem is tudtam, hogy van ilyen. Köszönöm!

Az egyetlen fura dolog, hogy a megtalált eredmények elé egy 1: sztringet fűz valamiért.

Igen, így már jobb. Csak két dolgot nem értek. Korábban honnan jött az a 1: prependált string, illetve, hogy a -z grep opciónak mi a konkrét szerepe? Nyilván átolvastam a man oldalt, és azt mondja, hogy az input és output-ról tételezzük fel, hogy ASCII NUL karakterrel terminálódik. Ugyan nem ez a helyzet, de ennek a mellékhatása vélhetőleg az, hogy a fájlt egy stream-nak tekinti a grep tőle, és nem az egyes sorokkal foglalkozik, hanem az egész fájllal.

az '1:'-ek a '-n' kapcsolótól jöttek, ami a sor számát írja ki, de a '-z' miatt ugye ez mindenhol egy volt...

az 1. hozzászólásra pedig (ezt valószínű lehetne szebben):

grep -zPo 'source\s[a-zA-Z0-9_\-]+\s(\{([^{}]++|(?1))*\});\n' asdf.txt | sed -z 's/\n[^s][^o]//g'

Visszakérdezek: miért kell feltétlenül és kizárólagosan a sed-et használni, és miért ragaszkodsz az egysoros megoldáshoz?

Én -- ötlet szintjén -- másképp közelíteném meg a problémát. Előbb a feladatot bontanám jól körülhatárolható részekre, majd azokhoz keresnék megoldó programokat -- itt több ötletet is említettek már a többiek --, ezután állítanám össze a megoldó szkriptet a teszteléshez. Ha kell, az optimalizálást a működő megoldáson utólag is el tudod végezni..

"Share what you know. Learn what you don't."

Az awk-s megoldásaimmal egész jól haladok időközben, de az egész script még nem állt össze, csak egyes részfeladatokat sikerült eddig befejezni.

Az egyik ilyen részfeldat a konfigurációs fájlból a kommentek eltávolítása. A legtöbb esetben ez elég triviális, azonban a naív megoldásomat sajnos kicselezte egy olyan sor, amiben a kettőskereszt/hashmark egy stringen belül volt. Ezt nyilván elegánsan kellene lekezelni, de a sima és dupla idézőjeles sztringek kezelését sed-ben nem teljesen triviális megoldani, hosszabb awk-scriptet pedig inkább nem írnék egy ilyen kicsi feladatra.

A neten körülnézve a stackexchange-en találtam egy egész jól működő megoldást, ami elég rövid is:

perl -ne 'if (/./) { s{\s*(?:#).*|("(?:\\.|[^"])*"|'"'(?:\\\\.|[^'])*'"'|.)}{$1}g; print if /./} else {print}'

Ezzel csak az a bajom, hogy ezt nem értem/látom át teljesen. El tudná magyarázni valaki mi is történik itt egészen pontosan, illetve azt is látni szeretném, hogy van-e valamilyen triviális módja hogy ezt átírjam mondjuk sed-re?

Így próbálkoztam eddig, de ezt a sed sajnos nem szereti:

$ sed -E -e 's/\s*(?:#).*|("(?:\\.|[^"])*"|'"'(?:\\\\.|[^'])*'"'|.)/\1/g' EEEE > eeee
sed: -e kifejezés #1, karakter 53: Érvénytelen megelőző szabályos kifejezés

Én csak azt nem értem, miért jó egysoros, nagyon tömör, ám tökéletesen érthetetlen scriptet írni egy minimálisan terjengősebb, szellősebb, külalakját tekintve is programnak kinéző script helyett. Az awk byte kódra fordít futás előtt, meglehetősen gyors lesz a futása. Az ilyen sorok nagyon jópofák, de már akkor bele lehet zavarodni, amikor írja az ember, két hét vagy két hónap múlva semmi esély a kód megértésére. Vagy legalább is aránytalanul nagy küzdelem.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

hosszabb awk-scriptet pedig inkább nem írnék egy ilyen kicsi feladatra

Hát ugye.

Ha egyszer képtelen vagy megoldani ezt a kis feladatot, akkor honnan tudod, hogy kis feladat? ;)

A hosszúságnak meg semmi köze a bonyolultsághoz.

A probléma a következő: Nem ismervén a választott eszközt, hibásan fogtál hozzá a megoldáshoz. (topicnyitó)

Ha nem vagy expert, akkor próbáld meg kis lépésekben megoldani a feladatot.

Alapvetően a sed, awk, stb. filter jellegű feldolgozást tesz lehetővé. Ez azt jelenti, hogy egy sor be, egy sor ki. Persze pont az a lényeg, hogy ezen az egyszerű algoritmuson kicsit bonyolítani kell, és kész a program.

Kezdőnek az awk a legjobb, mert egyszerű lépésekkel bővíthető a program.

Mivel nem barátod a man, kezdjük el az awk tanfolyamot!

Az awk program szerkezete az alábbi:

BEGIN { action }
pattern { action }
pattern { action }
...
END { action }

A felsorolt elemek bármelyike opcionális egészen addig, amíg csak egy pattern marad. Ekkor a default action a print. A BEGIN és END között elhelyezkedő program minden sorra végrehajtódik.

A fentiek alapján az első programod, amely kiszűri a kommenteket így néz ki.

awk '! /^#/' logfile

Szűrjük ki az @include mintákat is!

awk \
'/^#/ { next }
/@include/ { next }
{ print $0 }' logfile

A 3. actiontól kezdve jöhet a valódi munka. Közben szép lassan megtenulod a regexp használatát is.

A végén, ha már tudod mit csinálsz, akkor lehet egyszerűsíteni és összevonni a mintákat.

A minősítéseket figyelmen kívül hagyva szeretném mondani, hogy az első lépéseken túl vagyok már.

Csupán csak annyira gondoltam a hosszabb awk script alatt, hogy nem biztos, hogy most ezért írnék egy olyan scriptet közel nulláról, ami párba állítja az idézőjeleket, figyelembe veszi a stringeken belüli escape szekvenciákat, hogy meg tudja mondani, hogy melyik kettőskereszt comment és melyik nem.

Gondoltam, hogy ez egy olyan probléma amire lehet találni a neten konyhakész megoldást, hisz' más is bizonyára találkozott vele az évek során. Aztán az volt a terv, hogy ha találok valami életképes megoldást, akkor azt megpróbálom megérteni, majd adott esetben - ha látom értelmét - módosítani, kísérletezni vele.

Az eredeti scriptem erre a feladatra ez volt:

sed -e '/^#/ d' -e '/^@/ d' -e '/^$/ d' -e '/^\s+$/ d' -Ee 's:^([^#]+)#.*$:\1:'

De ez ugye sajnos nem kezeli jól azt az esetet, ha a kettőskereszt stringen belül van...

Már rég túl lennél rajta, ha magad írnád meg. Szerintem kell két állapotváltozó. Az egyik a „commentben vagyunk”, a másik pedig az „idézőjelen belül vagyunk”. Ha az aposztrof is ilyen, akkor kell egy harmadik is. Végigrohansz a soron karakterenként, ha találkozol kettőskereszttel, miközben nem vagy idézőjelben, akkor bebillented a kommentben vagyunk flag-et. Ha idézőjellel találkozol, s nem vagy kommentben, akkor ellenkezőjére változtatod az idézőjel flag-et. Lehet cifrázni, ha esetleg a backslash escape-el, és így tovább. Hagyd a fenébe a regexp-et! A regexp szerintem egyszerű esetekre jó, vagy akkor, ha többnyire működni kell valaminek, de nem mindig. Írj egy tisztességes szintaktikai elemzőt, ami nem lesz még mindig néhány tíz sornál hosszabb! Az elején a BEGIN {} szakaszban ne felejtsd el inicializálni a változóidat. Az egészet közel C szintaxissal meg tudod írni awk-ban.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Pár perc alatt írtam, bár ez nem kezeli a kukacot, de vázlatnak jó lehet. Illetve a több soron átívelő stringekkel sem törődik.

#!/usr/bin/awk -f

BEGIN {
    quote = 0;
    comment = 0;
    escape = 0;
    FS = "";
    norm = "\x1b[m";
    red = "\x1b[31;1m";
    green = "\x1b[32;1m";
}

{
    escape = 0;
    comment = 0;
    quote = 0;
    len = length();
    line = "";
    for (i=1; i<=len; i++) {
        c = substr($0, i, 1);
        if (escape) {
            escape = 0;
            line = line c;
            continue;
        }
        if (c=="\\") escape = 1;
        if (!quote && !comment && c=="#") {
            line = line green;
            comment = 1;
        }
        if (!comment && c=="\"") {
            if (!quote) {
                quote = 1;
                line = line red;
            } else {
                quote = 0;
                line = line c norm;
                continue;
            }
        }
        line = line c;
    }
    printf("%s%s\n", line, norm);
}

END {

}

Legegyszerűbb, ha önmagát adod át tesztfile-nak. :)

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Köszi a példakódot! Végül én is összeraktam saját awk-s kommenttelenítő scriptemet. Ez elvileg a sorokon átnyúló egyes és dupla idézőjeleket is kezeli. Kérlek vess rá egy pillantást, és "köpködd meg" ha esetleg benéztem valamit.
Köszi az eddigi kitartó segítségedet is!

function found_single_quote(input, input_length, position) {
	in_squote=1;
	for (i=position; i<=input_length; i++) {
		BUF=BUF input[i];
		switch (input[i]) {
			case "\x27":
				in_squote=0;
				return i+1;
			default:
				break;
		}
	}
	return input_length+1;
}
function found_double_quote(input, input_length, position) {
	in_dquote=1
	nbs=0;
	for (i=position; i<=input_length; i++) {
		BUF=BUF input[i];
		switch (input[i]) {
			case "\"":
				if ((nbs % 2) == 1) {
					nbs=0;
				}
				else {
					in_dquote=0;
					return i+1;
				}
				break;
			case "\\":
				nbs++;
				break;
			default:
				nbs=0;
				break;
		}
	}
	return input_length+1;
}
function process_line_normal(input, input_length, position) {
	for (i=position; i<=input_length; i++) {
			switch (input[i]) {
				case "\x27":
					BUF=BUF input[i];
					i=found_single_quote(input, input_length, i+1);
					break;
				case "\"":
					BUF=BUF input[i];
					i=found_double_quote(input, input_length, i+1);
					break;
				case "#":
					printf "%s\n", BUF;
					BUF="";
					return;
					break;
				default:
					BUF=BUF input[i];
					break;
			}
		}
		if ((in_dquote == 1) || (in_squote == 1)) {
			BUF=BUF "\n";
		}
		else {
			printf "%s\n", BUF;
			BUF="";
		}
}
function scan_line(input, input_length) {
	if (in_squote==1) {
		quote_end=found_single_quote(input, input_length, 1);
		process_line_normal(input, input_length, quote_end);
	}
	else if (in_dquote==1) {
		quote_end=found_double_quote(input, input_length, 1);
		process_line_normal(input, input_length, quote_end);
	}
	else {
		process_line_normal(input, input_length, 1);
	}
}
BEGIN {
	in_dquote=0;
	in_squote=0;
	BUF=""
}
{
	len=split($0, line, "");
	scan_line(line, len);	
}

Mélyrehatóbban még nem néztem, csak azt látom, hogy a return után felesleges a break, bár van benne koncepció, mert ha a return helyett mást írsz, akkor legalább nem fog hülyeséget csinálni. A másik, hogy az aposztrofot nem tudom, miért a kódjával adtad meg, s nem egyszerűen idézőjelek között: "'"

A string karaktereinek tömbbe robbantása tetszik, bár inkább nem akarom tudni, ez mennyi memóriát igényel. :) Igaz, ahol gigabyte-ok vannak, ott néhány kilobyte nem szempont. Majd még nézem, mit csináltál...

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Időközben felfedeztem benne egy bugot: időnként hajlamos volt lenyelni a sor utolsó karakterét, ha idézőjel volt az utolsó előtti karakter.
Ezt most megjavítottam. Az egyes idézőjelt pedig azért hexa kóddal adtam meg, mert az egész parancs egy bash függvényben fog lakni, és a kód egyes idézőjelek között lesz majd. Az bash pedig nem kezelte jól ha a maga valójában írtam oda az aposztrofot. Ez lett a kerülő megoldás, de minden esetre működik.

Ami a tömbbe robbantást illeti, ott pedig egyszerre csak egy sort bontok fel, nem az egész fájlt. Bináris fájlokat meg nem tervezek így feldolgozni ;)

A kód itt.

Na ugye, hogy nem mindent sed-del kell megoldani! :) Szerintem az awk kicsit addiktív, mert gyorsan fut, viszont script, gyengén típusos, nem kell annyi mindenre figyelni, mintha C-ben írná az ember, viszont eléggé C-közeli szintaxisa van.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

nem látok ebben hibát, miért nem kezeli, ha a kettőskereszt sztringen belül van?

~$ echo "asdf#aasdf" | sed -Ee 's:^([^#]+)#.*$:\1:'                                                    
asdf
 

Csak egyetlen komment:

-e '/^$/ d' -e '/^\s+$/ d'

helyett:

-e '/^\s*$/ d'

Szerintem nem arra gondolt, amikor a parancssorban idézőjelek közé teszed az Andráskeresztet, hanem amikor a keresett szövegben van idézőjel és azon belül kereszt.

Parancssorból helyesen:

$ echo '"asdf#aasdf"' | sed -Ee 's:^([^#]+)#.*$:\1:'

"asdf

Szerkesztve: 2019. 12. 11., sze – 13:45

syslog-ng --preprocess-into=/dev/stdout | while read _l ; do _l=${_l%%#*} && [[ ${_l} == *log*source* ]] && print -r -- "${_l}" ; done