Unix ja shell-ohjelmointi, tentti 16.3.2001: Unix ja shell-ohjelmointi, tentti 16.3.2001: arvosteluperusteista, malliratkaisuja

Yleistä:

Seuraavassa "pikkuvirhe" tarkoittaa "puolen pisteen" virhettä, josta ei sakotettu jos niitä oli tehtävässä vain yksi. Tällaisia olivat mm. joidenkin erikoistapausten käsittely väärin (eksoottisimmista ei mennyt virhettä lainkaan, tavallisemmista koko piste), Gnu-spesifisten ominaisuuksien käyttö silloin kun niillä ei ollut olennaista merkitystä (hakemistoargumentin puuttuminen findista tms), jne.

Liian suurella datamäärällä kaatuvat komennot kuten for i in $(find ...) katsottiin myös pikkuvirheiksi.

Vähintään kokonainen piste meni kaikesta joka vaati skriptin muuttamista että se toimisi lainkaan (syntaksivirheistä jne), epäonnistumista ilmeisissä erikoistapauksissa tai tehtävässä erikseen mainituissa tilanteissa.

Piste meni myös Gnu-spesifeihin ominaisuuksiin nojautuvissa ratkaisuissa (kuten find -printf, find -cnewer, grep -E ... \1 jne): tehtäväpaperissakin sanottiin näin:

Huom. Ratkaisuissa saa käyttää vain luennoilla esiteltyjä komentoja, ei perliä tai komentojen Gnu- tai Linux-spesifisiä ominaisuuksia.

Ylimääräisistä koristuksista tai turhan monimutkaisista ratkaisuista ei sakotettu.

Seuraavassa yksityiskohtia tehtävittäin (alkuperäiset kysymykset kursivoituna):

  1. Kirjoita skripti, joka

    1. etsii tiedostosta rivit, joiden ensimmäinen ja viimeinen ei-tyhjä merkki on sama. (Tyhjä tarkoittaa välilyöntiä tai tabulaattoria.) (2p)

      Myös sellaiset rivit olisi pitänyt löytää, joilla on vain yksi ei-tyhjä merkki. Tässä epäonnistuminen katsottiin pikkuvirheeksi.

      Yhden pisteen sai jos tyhjien käsittelyssä oli selvä virhe, mutta skripti toimi muuten oikein.

      Tehtävässä ei täsmennetty miten tiedosto annetaan, joten kaikki toimivat ratkaisut siltä osin hyväksyttiin (tiedostonimi argumenttina, stdin, "$file"). Komentoriviargumentin tutkiminen sellaisenaan katsottiin pikkuvirheeksi. Seuraavat malliratkaisut on kaikki tehty niin, että ne ottavat tiedostonimiä (useitakin) komentoriviltä.

      Selkein ratkaisu tähän oli varmaankin awkin käyttö, joka syö tyhjät automaattisesti oikein:

      #!/bin/awk -f
      substr($1,1,1) == substr($NF,length($NF))
      
      Mitään if- tai print-käskyjä ei tarvita, mutta ei niistä sakotettukaan (jos olivat oikein).

      Ensin luontevalta tuntuva grep on selvästi hankalampi, yksimerkkisten rivien käsittely tuo siihen oman lisänsä:

      #!/bin/sh
      grep '^[[:blank:]]*\([^[:blank:]]\)\(.*\1\)*[[:blank:]]*$' "$@"
      
      Välilyönnin ja tabulaattorin voi kirjoittaa sellaisenaankin [:blank:]:n paikalle; [:space:] ei ole ihan sama (sisältää myös ''pystysuoran tyhjän'' kuten rivin- ja sivunvaihtomerkit) mutta sekin hyväksyttiin. Samoin ''ei-tyhjän'' paikalle olisi kelvannut esim. [:graph:], mutta ei [:print:] koska tämä vastaa myös välilyöntiä.

      Huom. standardi-grep ei tunne osalausekeviittausta \1 option -E kanssa, se toimii vain Gnu grepissä. Niinpä esim. seuraava ei ole standardinmukainen:

      #! /bin/sh
      # nonstandard - works with Gnu grep only
      grep -E '^[[:blank:]]*([^[:blank:]])(.*\1|)[[:blank:]]*$' "$@"
      
      Tässä tuo meni kuitenkin pikkuvirheestä koska se on tarpeen vain yksimerkkisten rivien käsittelyssä.

      Toisaalta seuraava on standardinmukainen mutta ei toimi Gnu grepillä (ainakaan vielä versiolla 2.4):

      #!/bin/sh
      # does not work with Gnu grep 2.4 or earlier (it has a bug)
      grep -e '^[[:blank:]]*[^[:blank:]][[:blank:]]*$' \
      -e '^[[:blank:]]*\([^[:blank:]]\).*\1[[:blank:]]*$' "$@"
      

      Seuraava sen sijaan toimii kaikilla:

      #!/bin/sh
      grep '^[[:blank:]]*\([^[:blank:]]\).*\1[[:blank:]]*$' "$@"
      grep '^[[:blank:]]*[^[:blank:]][[:blank:]]*$' "$@"
      
      Myös sedillä voi tehdä saman asian:
      #!/bin/sed -f
      /^[[:blank:]]*[^[:blank:]][[:blank:]]*$/p
      /^[[:blank:]]*\([^[:blank:]]\).*\1[[:blank:]]*$/p
      d
      
      tai myös näin:
      #!/bin/sed -f
      /^[[:blank:]]*\([^[:blank:]]\)\(.*\1\)*[[:blank:]]*$/!d
      
      Tehtävän voi ratkaista myös sh:n read-komentoa ja muuttujaeditointiominaisuuksia hyväksikäyttäen, mutta se on aika hankalaa: yksinkertaisessa ratkaisussa tyhjät katoavat kokonaan. Näin sen kuitenkin voi tehdä (cat on tarpeen vain monen tiedoston käsittelemiseksi):
      #! /bin/sh
      cat "$@" |
      while IFS='' read RAW ;do
        printf "%s\n" "$RAW" | {
          read COOKED 
          [ "x${COOKED#${COOKED%?}}" = "x${COOKED%${COOKED#?}}" ] && 
              printf "%s\n" "$RAW"
        }
      done
      
      Yritys tehdä tuo yhdellä read-silmukalla hukkaa alku- ja lopputyhjät tulostuksestakin, mistä meni yksi piste.

    2. tulostaa argumenttina annetusta liukuluvusta mantissan itseisarvon (poistaa mahdollisen etumerkin ja eksponentin, +12.34e-57:sta tulisi 12.34). Eksponenttimerkki voi olla iso tai pieni e. Luvun oikeellisuutta ei tarvitse tarkistaa. Testausapuna ohessa tiedosto floats.txt jossa on liukulukuja ja niiden mantissoja. (2p)

      Tutkittava luku piti ottaa argumenttina eikä lukea tiedostosta. Huolimattomuus tässä katsottiin pikkuvirheeksi.

      Tässä oli hiukan tulkinnanvaraista täytyykö liukuluvussa aina olla desimaalipilkku (kelpaako "12e56"); sellaisiin kompastumisesta ei sakotettu, ei myöskään jos oletti desimaalierottimen olevan aina piste.

      Sen sijaan eksponentiton tai etumerkitön luku piti käsitellä oikein; jommassa kummassa epäonnistuneesta skriptistä sai yhden pisteen.

      Ehkä luontevin ratkaisu tässä oli shellin muuttujaeditointikomentojen käyttö:

      #! /bin/sh
      tmp=${1%%[Ee]*}; printf "%s\n" "${tmp#[-+]}"
      
      tai expr:
      #! /bin/sh
      expr "$1" : "[-+]*\([0-9.]*\)[eE]*.*"
      
      Myös sed sopii tarkoitukseen:
      #! /bin/sh
      printf "%s\n" "$1" | sed -e 's/[-+]//' -e 's/[eE].*//'
      
      Noissa echokin kelpasi koskapa luvun oikeellisuutta ei tarvinnut tarkistaa.

      Myös awkilla asian voi tehdä monellakin tavalla:

      #! /bin/sh
      printf "%s\n" "$1" | awk -F'[eE]' '{sub("[-+]","");print $1}'
      
      tai
      #! /bin/sh
      awk -v x="$1" 'BEGIN{sub("[-+]","",x); sub("[Ee].*","",x); print x}'
      
      tai
      #! /bin/awk -f
      BEGIN { x=ARGV[1]; ARGV[1]=""
      sub("[-+]","",x); sub("[Ee].*","",x); print x }
      

      Muitakin vaihtoehtoja löytyy, esim. tr ja cut:

      #! /bin/sh
      printf "%s\n" "$1" | tr -d +- | tr e E | cut -dE -f1
      
      tai
      #! /bin/sh
      printf "%s\n" "$1" | cut -de -f1 | cut -dE -f1 | tr -d +-
      

    3. etsii pelkkiä numeroita (yksi kokonaisluku per rivi) sisältävistä tiedostoista kaikki neljällä tasan jaolliset luvut. Tiedostossa mahdollisesti oleviin muunlaisiin riveihin ei tarvitse varautua. Testausavuksi ohessa on tiedosto integers.txt. (2p)

      Tässäkään ei syöttötiedoston lukutapaan puututtu, vaikka tehtävässä onkin monikko (''tiedostoista''), ja argumentin tutkiminen tiedoston asemesta katsottiin pikkuvirheeksi.

      Tehtävänmäärittelyssä sanottiin että tiedostossa on pelkkiä numeroita, joten tulkinta että siellä ei ole etumerkkejä hyväksyttiin eikä toimintaa negatiivisilla luvuilla vaadittu.

      Helpoin ja ilmeisin ratkaisu tähän oli awk:

      #! /bin/awk -f
      $1 % 4 == 0
      
      Myös pelkillä shellin komennoilla se onnistuu:
      #! /bin/sh
      cat "$@" |
      while read n; do
        [ $(( n % 4 )) -eq 0 ] && printf "%d\n" "$n"
      done
      
      Tuossa cat on tarpeen vain monen tiedoston käsittelemiseksi (edellisessä awk osaa tehdä sen itse).

      Tarkoitukseen sopii myös bc, joka osaa käsitellä isompiakin lukuja:

      #! /bin/sh
      cat "$@" |
      while read n; do
        echo "if ($n % 4== 0) $n"
      done | bc
      
      mutta jos tosi isoja lukuja halutaan käsitellä, paras on grep:
      #! /bin/sh
      grep -E '([02468][048]|[13579][26])$' "$@"
      
      Lukuhan on jaollinen neljällä jos sen kahden viimeisen numeron muodostama luku on sitä. (Tuossa tulostusformaatti on erilainen jos tiedostoja on useita, mutta sitähän ei tehtävässä rajattu.)

  2. Kirjoita skripti, joka

    1. etsii annetun hakemiston alta tavalliset tiedostot, joita on muutettu tänään (skriptin ajopäivänä) kello 08:00 jälkeen. (3p)

      Tässä monelta jäi huomaamatta tavalliset tiedostot, eli hakemistoja jne ei pitänyt löytää. Siitä meni yksi piste.

      Pikkuvirheeksi katsottiin haun aloittaminen oletushakemistosta (huom. annetusta hakemistosta). Sen sijaan tulkinnanvaraista oli pitikö alihakemistot käydä läpi rekursiivisesti vai tutkia vain suoraan annetussa hakemistossa olevat tiedostot; molemmat hyväksyttiin.

      Ilmeinen ratkaisu oli jokseenkin suoraan monisteesta löytyvä touch/find -esimerkki, tässä täydennettynä referenssitiedoston luonnin tarkistuksella:

      #! /bin/sh
      i=$$
      set -C	# saa jäädä voimaan tämän skriptin loppuun asti
      until MARK=/tmp/.mark$i >/tmp/.mark$i ;do i=$((i+1)) ;done 2>&-
      trap "rm $MARK" 0
      touch -t $(date +%Y%m%d0800) "$MARK"
      find "${1:-}" -type f -newer "$MARK"
      
      Huom. -cnewer on Gnu-spesifinen epästandardi optio, eikä se edes tee sitä mitä haluttiin (se tutkii hakemistotiedon muutosaikaa). Siitä sakotettiin yksi piste.

      Standardi find myös vaatii hakemistoargumentin; find -newer... ei ole sallittu, vaikka sekin Gnu-versiossa toimii. Tämä katsottiin pikkuvirheeksi.

      Referenssitiedoston luominen vakionimellä, sen teko oletushakemistoon (jonne ei välttämättä ole kirjoitusoikeutta!) tai sen poistamatta jättäminen lopuksi annettiin anteeksi, eikä luonnin onnistumistarkistusta edellytetty.

      Ei-rekursiivinen ratkaisu kävi esimerkiksi näin:

      #! /bin/sh
      eval $(date '+MONTH=%b DAY=%d')
      ls -l "$@" | 
      awk '/^-/ && $6=="'$MONTH'" && $7=='$DAY' && $8~/:/ && $8>"08:00"'
      
      
      Tehtävässä ei sanottu miten löydetyt tiedostot pitää tulostaa, joten em. koko hakemistotiedon tulostava versio riittäisi. Myös { print $9 } kelpasi, vaikka se ei toimikaan oikein tyhjiä sisältävien tiedostonimien kanssa.

      Huomaa että 6kk vanhemmissa ls tulostaa kellonajan asemesta vuoden, mutta koska sitä vanhempia ei haluta, formaatin tarkistus riittää (vuosiluvuissa ei ole kaksoispistettä).

      Tuossa on eval siksi että daten kutsuminen useaan kertaan aiheuttaisi aikariippuvuuden (race condition); siitä ei kuitenkaan sakotettu.

    2. poistaa tiedostonimien lopusta puolipisteellä erotetut versionumerot (esim. file1.txt;1:sta tulisi file1.txt). Jos versionumeroa lukuunottamatta samannimisiä tiedostoja on useita, tulostetaan sopiva virheilmoitus ja jätetään ko. tiedostot sikseen. Voit olettaa että viimeisen puolipisteen perässä ei voi olla muuta kuin versionumero. (3p)

      Tehtävästä ei käy ilmi miten tiedostonimet annetaan; sekä niiden antaminen komentorivillä, annetusta hakemistosta tai vaikka oletushakemistosta etsiminen kelpasi.

      Tilanteessa jossa versioita on useita mutta puolipisteetöntä ei ollut lainkaan, hyväksyttiin yhden nimeäminen sellaiseksi jos lopuista tuli virheilmoitus, tällainen (tai toiminnallisesti ekvivalentti) siis hyväksyttiin:

      #! /bin/sh
      for i in ${1:-.}/*\;* 
      do j=${i%;*}
         if [ -f $j ]
         then printf "both %s and %s exist, not changed\n" "$i" "$j" >&2
         else mv "$i" "$j"
         fi
      done
      

      Törmäystestin puuttuminen tai ilmeinen virheellisyys (kuten mv:n onnistumisen testaaminen) sen sijaan maksoi pisteen.

      Tiedostolistan purkaminen komentoriville tyyliin for i in $(find ...) (mikä särkyy jos tiedostoja on liikaa) katsottiin pikkuvirheeksi, samoin huolimattomuus ''tavallisten erikoisten'' nimien kanssa tyyliin echo "$file" |... (joka särkyy mm. nimellä "-n"): viivalla alkavat ja useita puolipisteitä sisältävät nimet olisi pitänyt hallita.

      Sen sijaan välilyöntejä, rivinvaihtoja tai jokerimerkkejä sisältävien nimien käsittelyä täydellisesti ei vaadittu. Se onkin yllättävän vaikeaa.

      Seuraavissa malliratkaisuissa kummassakin on sama idea: käydään läpi puolipisteitä sisältävät nimet, poistetaan versio lopusta ja tutkitaan ensin onko versioton olemassa, sitten onko erilaisia puolipisteellisiä useita katsomalla montaako tiedostoa "$j;"* vastaa.

      #! /bin/ksh
      for i in ${1:-.}/*\;* ;do
         j=${i%;*}
         [ -e "$j" ] && printf "%s exists, %s not changed\n" "$j" "$i" && continue 
         OLDIFS="$IFS"
         IFS=''
         set -- "$j;"*
         IFS="$OLDIFS"
         case $# in
          1) mv "$i" "$j" ;;
          *) printf "other versions of '%s' exist, not changed\n" "$i" >&2 ;;
         esac
      done
      
      tai
      #! /bin/ksh
      for i in ${1:-.}/*\;* ;do
         j=${i%;*}
         [ -e "$j" ] && printf "%s exists, %s not changed\n" "$j" "$i" && continue 
         for k in "$j;"* ;do
           case "$k" in
             "$i") : ;;
             *)    printf "other versions of %s exist, not changed\n" "$i" >&2
      	     continue 2 ;;
           esac
         done
         echo mv "$i" "$j" 
      done
      
      Huom. case "$j;"* in... ei toimisi tuossa, casen valitsimelle ei tehdä tiedostonimilevitystä (muut levitykset kyllä, mukaanlukien tilde).

  3. Kirjoita skripti, joka

    1. lajittelee henkilötunnuksia sisältävän tiedoston syntymäajan mukaiseen järjestykseen, samana päivänä syntyneet järjestysnumeron mukaan. Tunnusten oikeellisuutta ei tarvitse tarkistaa. (Henkilötunnus on muotoa ppkkvvsnnnt, missä pp on syntymäpäivä, kk kuukausi, vv vuosi, s vuosisata, +=1800, -=1900, A=2000, nnn järjestysnumero joka on naisilla parillinen ja miehillä pariton, ja tarkistusmerkki t jolla ei tässä ole merkitystä.) Testaukseen voit käyttää tiedostoja hetuja.txt ja hetuja.sorted-a (samat tunnukset oikein lajiteltuina). (3p)

      Tässä vaikeutena oli lähinnä sort-komennon avainoptioiden hallinta. Vuosisatamerkin saaminen oikeaan järjestykseen onnistui suoraan sortilla kunhan locale on oikein:

      #! /bin/sh
      LC_COLLATE=C sort -k 1.7,1.7 -k 1.5,1.6 -k 1.3,1.4 -k 1.1,1.2 -k 1.8 "$@"
      

      Kielimuuttujan asettamatta jättäminen katsottiin kuitenkin vain pikkuvirheeksi. Hyväksyttävä ratkaisu oli myös vuosisatojen erotteleminen esim. näin:

      #! /bin/sh
      grep '+' "$@" | sort -k 1.5,1.6 -k 1.3,1.4 -k 1.1,1.2 -k 1.8
      grep '-' "$@" | sort -k 1.5,1.6 -k 1.3,1.4 -k 1.1,1.2 -k 1.8 
      grep 'A' "$@" | sort -k 1.5,1.6 -k 1.3,1.4 -k 1.1,1.2 -k 1.8 
      

      Myös kenttien järjestely edestakaisin sedillä kelpasi:

      #! /bin/sh
      sed -e 's/\(..\)\(..\)\(..\)\(.\)\(....\)/\4\3\2\1\5/' \
          -e 's/^[+]/1800/' -e 's/^[-]/1900/' -e 's/^A/2000/' "$@" | 
      sort | 
      sed -e 's/^1800/+/' -e 's/^1900/-/' -e 's/^2000/A/' \
          -e 's/\(.\)\(..\)\(..\)\(..\)\(....\)/\4\3\2\1\5/'
      

      Syöttötiedoston välitystapaan ei taaskaan kiinnitetty huomiota. Sen sijaan lajitteluavainten käsittelyssä lähti pienestäkin virheestä piste ja jos sitä ei ollut edes yritetty pisteitä ei herunut.

    2. kuten edellä mutta samana päivänä syntyneet pitää lajitella ensisijaisesti sukupuolen mukaan, naiset ensin. Testiaineistoksi käy sama hetuja.txt kuin edellä, haluttu tulos löytyy nyt tiedostosta hetuja.sorted-b. (3p)

      Tässä pisteitä sai vain siltä osin kuin se paransi edellistä, eli sukupuolta lukuunottamatta oikein lajitellusta ei pisteitä tullut (turha kopioida edellisen vastausta).

      Tehtävässä nimenomaan sanottiin että vain samana päivänä syntyneet piti lajitella sukupuolen mukaan, ei siis niin että ensin kaikki naiset ja sitten kaikki miehet. Oikea tulkinta olisi ollut nähtävissä mallitulosteestakin. Tällä tavoin väärin tulkitusta sai kuitenkin yhden pisteen jos toteutus oli toimiva.

      Yksinkertaisin ratkaisuidea on lisätä sukupuolta esittävä kenttä ennen lajittelua sopivaan paikkaan ja poistaa se sen jälkeen:

      #! /bin/sh
      sed -e 's/..[02468].$/_0&/' -e 's/..[13579].$/_1&/' "$@" |
      LC_COLLATE=C sort -k 1.7,1.7 -k 1.5,1.6 -k 1.3,1.4 -k 1.1,1.2 -k 1.9 |
      sed 's/_.//'
      

  4. Kirjoita skripti, joka testaa käyttäjän laskutaitoja pienillä kokonaisluvuilla (esim. enintään 3-numeroisilla) siten, että se arpoo satunnaisen laskutehtävän (yhteen-, vähennys-, kerto- tai jakolasku), kysyy sitä käyttäjältä ja tarkistaa vastauksen. Vastauksen ollessa väärä samaa kysytään uudestaan, jos vastaus on toistamiseen väärä kerrotaan käyttäjälle oikea vastaus. Argumenttina annetaan kysymysten määrä, lopussa tulostetaan onnistumisprosentti. Jakolaskutehtävät pitää muodostaa niin, että jako menee tasan.

    Satunnaislukujen generoimiseen voi käyttää ksh:n ja bash:in satunnaismuuttujaa $RANDOM, joka palauttaa satunnaisen kokonaisluvun väliltä 0... 32767. (6p)

    Tässä ei sakotettu virheilmoituksista käyttäjän vastatessa pelkällä rivinvaihdolla tai kirjaimilla tms. Sen sijaan suojaamattomista *-merkeistä lähti piste, samoin jakolaskuista jotka eivät menneet tasan kuten piti.

    Yleinen virhe oli myös onnistumisprosentin laskeminen väärin: $(( oikein/yht*100 )) tuottaa vain tasan 100 tai tasan 0 koska jakolasku tehdään ensin ja kokonaisluvuilla. Riittävä ratkaisu oli $(( oikein*100/yht )), myös awkin tai bc:n käyttö kelpasi.

    Vaikeuksista $RANDOMin kanssa ei sakotettu, jos esim. aina tuli sama lasku sen takia että $RANDOM evaluoitiin alishellissä. Pienen satunnaisluvun muodostaminen yrittämällä yhä uudestaan (mikä saattaa kestää toivottoman kauan) katsottiin pikkuvirheeksi.

    Ilmeisen aikapulan vuoksi kesken jääneistä sai pisteitä siten, että jokaisesta olennaisesti eri muutoksesta, joka skriptiin piti tehdä ennen kuin se toimi, lähti yksi piste (käytännössä tällaisesta saattoi saada enintään kolme pistettä).

    Koristeita ei vaadittu, riittävä ratkaisu oli esim. tällainen:

    #! /bin/sh
    count=${1:-10}
    left=$count
    
    while [ $left -gt 0 ] ;do
      left=$((left-1))
    
      arg1=$(( $RANDOM % 100 ))
      arg2=$(( $RANDOM % 100 ))
      case $(( $RANDOM % 4 )) in
        0)  op="+"; res=$(( arg1 + arg2 )) ;;
        1)  op="-"; res=$(( arg1 - arg2 )) ;;
        2)  op="*"; res=$(( arg1 * arg2 )) ;;
        3)  op="/"; res=$arg1; arg1=$(( res * arg2 ));
      esac
    
      for i in 1 2 ;do
        printf "%d%c%d= " "$arg1" "$op" "$arg2"
        read
        if [ "$REPLY" = "$res" ] ;then
          printf "Oikein!\n"
          good=$((good+1))
          break
        fi
        case $i in
          1) printf "pieleen meni, yritä uudestaan\n" ;;
          2) printf "ei vieläkään onnistunut, no se oli %s\n" "$res" ;;
        esac
      done
    
    done
    
    printf "Onnistumisprosentti %s\n" $((good * 100 / $count))
    

  5. Kirjoita skripti, joka muuttaa annetuissa tiedostoissa tai stdinissä tekstin seassa olevat %xx -koodatut merkit, missä xx on merkin heksadesimaalikoodi, vastaavaksi merkiksi. Testausapuna tiedosto heksaa.txt ja haluttu tulos unhexed.txt. (6p)

    Tässä helpoin tapa lienee rakentaa ensin korvaustaulukko kaikille heksamerkeille. Myös heksamuunnoksen tekeminen aina niiden tullessa vastaan käy. Kumpikin onnistunee helpoiten awkilla:

    #! /bin/awk -f
    BEGIN { for (i=0; i<256; ++i) c[sprintf("%2X",i)]=sprintf("%c",i) }
    { while (match($0,/%[0-9A-Fa-f][0-9A-Fa-f]/)) {
        h=toupper(substr($0,RSTART+1,2))
        printf "%s",substr($0,1,RSTART-1) c[h] 
        $0=substr($0,RSTART+3)
      }
      print
    }
    
    tai
    #! /bin/awk -f
    BEGIN { hex="123456789ABCDEF" }
    { while (match($0,/%[0-9A-Fa-f][0-9A-Fa-f]/)) {
        h=toupper(substr($0,RSTART+1,2))
        c=sprintf("%c",16*index(hex,substr(h,1,1))+index(hex,substr(h,2)))
        printf "%s",substr($0,1,RSTART-1) c 
        $0=substr($0,RSTART+3)
      }
      print
    }
    
    Huom. lyhyempi mallivaihtoehto [0-9A-Fa-f]{2} toimii Gnu awkin kanssa vain optiolla --posix tai --re-interval tai asettamalla ympäristömuuttuja POSIXLY_CORRECT. Standardinmukaisen awkin kanssa (esim. HP-UX:ssä) se toimii, vanhemmissa yleensä ei.

    Ilman awkiakin pärjää, esim. rakentamalla sed-skriptin:

    #! /bin/sh
    
    SEDFILE=/tmp/unhex.sed
    
    i=1
    while [ $i -le 255 ] ;do
      hex=$(printf "%02X" $i)
      oct=$(printf "%o" $i)
      case $i in 10) char='\
    ' ;;
                 *)  char=$(printf '%b' '\0'"$oct") ;;
      esac
      case "$char" in '/'|'&'|'\') char='\'"$char" ;; esac
      printf "s/%s/%s/g\n" "%$hex" "$char"
      i=$((i+1)) 
    done >"$SEDFILE"
    
    sed -f "$SEDFILE" "$@"
    # jätetään sed-tiedosto paikalleen jatkokäyttöä varten
    

    Tuossa on tosin se ongelma että monet sed-versiot eivät huoli sataa komentoa pitempiä skriptejä; siirrettävämpi tapa olisi rakentaa awk-skripti. Standardinmukainen tuo kuitenkin on ja toimii Gnu sedillä joten se oli OK.

    Myös sed-skriptin rakentaminen awkilla kelpasi.

    Toimimista null-merkin kanssa ei vaadittu (sitä ei voi standardikomennoilla siirrettävästi tehdä; em. malliratkaisuilla se toimii jos käytetty awk-versio sen osaa).

    Hyvistä aluista sai 1-2 pistettä, pelkästä raaka voima -ratkaisun ideasta ei mitään (valmiiksi kirjoitettuna se olisi kelvannut).


Kurssikysely löytyy täältä.