Korvaa stringi tiedostosta listaa käyttäen

Muutaman kerran minulla on tullut vastaan tilanne, jossa tiedostosta (ascii) pitäisi korvata useampi stringi. “Muokattava.txt”, ja siellä on siis “foo”, ja se pitää vaihtaa “bar”. Selvä. Tämä onnistuu Linuxin (esim. Ubuntu) komentoriviltä, onnistuu myös winkkarista, ja tähän käyttöön löytyy yksi ja miljoona erilaista softaa. Samoin, jos haluaa vaihtaa useammasta tiedostosta, homma on helppo.

Tänään tuli vastaan tilanne, että nyt on vain etsittävä työkalu. Linuxiin (ubuntu, tms), winkkariin, Winkkarin powershelliin.

Ongelma on se, että minulla on tuo “muokattava.txt”, ja sieltä pitäisi vaihtaa useita stringejä käyttäen “tietokantaa” eli korvaustiedostoa. Eli: etsi “muokattava.txt”-tiedostosta kaikki stringit “foo” ja vaihda ne “bar”, ja sitten etsi sieltä “fii” ja vaihda ne “bir” ja sitten etsi “faa” ja vaihda ne bor" jne. Tämähän on helppoa: korvataan ne yksi kerrallaan tuollaisella hienolla wysiwyg-softalla, tai sitten tehdään monta kymmentä riviä, eli “grep stringi 1 sed, grep stringi2 jne”.

Muokattava.txt sisältää siis hirvittävän määrän stringejä, siellä voi olla siis vaikkapa sata “foo” ja ne kaikki pitää vaihtaa “bar” jne.

korvaustiedosto.txt näyttäisi tietenkin tältä:
foo bir
fii bor
faa ber
fee bar
fyy bur
jne jne jne.

Milloin käyttäisit tätä? Yksi kaukaa haettu esimerkki voisi olla vaikkapa jonkun xml-tiedoston kääntäminen, vaikkapa italian finnvoicea, jonka tagit ovat italiaksi, kääntäminen finnvoiceksi, ja ensin tehtäisiin raaka konekäännös. korvaustiedosto.txt sisältäisi listan italialaisista stringeistä ja niiden finnvoice-vastineet. Tässä foo-bar taas “muokattava.txt” sisältäisi about tämän näköistä kamaa: foo tab 1 tab foo tab 2 tab fii tab enter fyy tab 4 fee tab 6 enter…

TÄMÄN PITÄISI kai toimia:
for line in $(cat korvaustaulu.txt);do korvaaja=$(echo $line|cut -f1);korvattava=$(echo $line|cut -f2);sed -i “s/$korvattava/$korvaaja/g” muokattava.txt;done

jossa korvaustaulu.txt sisältäisi mitä etsitään ja millä korvataan. f1, f2 kertoo sarakkeen, -d ei tarvita, koska defaulttina pitäisi olla tab.

Rivi ei kuitenkaan toimi, se ei korvaa yhtään mitään.

Eli:
a) mikä tuossa rivissä on vikana?
b) ja jos ei ole toteutettavissa riviltä, niin joku ohjelmisto, jolla tuon voisi tehdä?

Stackexchange on paras ystävä näissä asioissa, löysin tämän artikkelin:

, josta tein laiskan miehen version, jossa alkuperäisen ratkaisuskriptin luettavuus säilyy sillä alkuperäinen ratkaisuskripti oli tarkoitettu erillisistä tiedostoista koostuville listoilla.

cat Muokattava.txt > fileA
awk '{print $1}' korvaustiedosto.txt > fileB
awk '{print $2}' korvaustiedosto.txt > fileC

paste -ds///g /dev/null /dev/null \
<(sed 's|[[\.*^$/]|\\&|g' fileB) <(sed 's|[\&/]|\\&|g' fileC) \
/dev/null /dev/null | sed -f - fileA > Muokattava.txt

No niin, tutkin asiaa syvemmin, ja ongelmasi johtuvat esimerkki lausekkeessasi kahdesta eri tekijästä.

Ensinnäkin, mainitsit Windowsin, sekä esimerkeissäsi käytit mm. muokattava.txt sekä Muokattava.txt ristiin rastiin. Linux-ympäristössä ole tarkka mitä kirjoitat, ja täysin varma mihin tiedostoihin todella haluat viitata, sillä isoilla ja pienillä kirjaimilla on eroa. Lausekkeesi vaikuttaa silmäillen toimivalta, mutta sedin kohdalla ongelma nosti päätään, halusit käyttää "-lainausmerkkejä, sen sijaan käytit “- ”-lainausmerkkejä, jotka eivät toimi. Käytä Shift+2 tulevia standardilainausmerkkejä.

Toisena ongelmana tekstin silmukointi tapauksessasi vaatii Input Field Separatorin eli IFS:n käyttöä. Lausekkeesi pelkistettynä listaa korvaustaulun sanat yksittelen, välittämättä sanojen väliin jäävästä tyhjästä tilasta.

for line in $(cat korvaustaulu.txt);do echo $line ;done

foo
bir
fii
bor
faa
ber
fee
bar
fyy
bur

Tein Stackexchangin tekstin silmukointia käsittelevän ongelman pohjalta alkuperäisen lausekkeesi pohjalta toimivan version:

#!/bin/bash
while IFS='' read -r LINE || [ -n "${LINE}" ]; do
    korvaaja=$(echo "${LINE}" | awk '{print $1}');korvattava=$(echo "${LINE}" | awk '{print $2}'); sed -i "s~${korvattava}~${korvaaja}~g" muokattava.txt;
done < korvaustaulu.txt

Eiköhän tämä ole kotitehtävä (laiskanläksy), joka pitäisi ihan itse opetella ratkaisemaan, kyselemättä asiansaosaavilta ohjelmoijilta?

Oletan että “korvaustaulu.txt” sisältää samalla rivillä ensin korvattavan sanan ja sen perässä korvaavan sanan (katso alta), välilyönnillä eroteltuna, jolloin tämä alla oleva tulisi toimimaan halutulla tavalla.

while read line; do
  korvaaja=$( echo $line | cut -d ' ' -f2 );
  korvattava=$( echo $line | cut -d ' ' -f1 );
  sed -i "s/$korvattava/$korvaaja/g" muokattava.txt;
done < korvaustaulu.txt

muokattava.txt
Meikäläisten perheessä Liisa pesee pyykin
ja Liisa myös ulkoiluttaa koiran.

korvaustaulu.txt
Liisa Matti
koira kissa

Lopputulos olisi:
Meikäläisten perheessä Matti pesee pyykin
ja Matti myös ulkoiluttaa kissan.