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):
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 dtai myös näin:
#!/bin/sed -f /^[[:blank:]]*\([^[:blank:]]\)\(.*\1\)*[[:blank:]]*$/!dTehtä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" } doneYritys tehdä tuo yhdellä read-silmukalla hukkaa alku- ja lopputyhjät tulostuksestakin, mistä meni yksi piste.
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 -f1tai
#! /bin/sh printf "%s\n" "$1" | cut -de -f1 | cut -dE -f1 | tr -d +-
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 == 0Myös pelkillä shellin komennoilla se onnistuu:
#! /bin/sh cat "$@" | while read n; do [ $(( n % 4 )) -eq 0 ] && printf "%d\n" "$n" doneTuossa 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 | bcmutta 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.)
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.
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 donetai
#! /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" doneHuom. case "$j;"* in... ei toimisi tuossa, casen valitsimelle ei tehdä tiedostonimilevitystä (muut levitykset kyllä, mukaanlukien tilde).
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.
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/_.//'
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))
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).