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 (kuten lainausmerkkien puuttuminen muuttujan ympäriltä vaikka siinä voi olla erikoismerkkejä), eksoottisimmista ei mennyt virhettä lainkaan (erikoismerkkejä sisältävien tiedostonimien käsittelyä oikein ei vaadittu kuin tehtävässä 4).
Pikkuvirheeksi katsottiin myös syötön lukeminen väärästä paikasta (stdin vs. komentorivi) ym
Vähintään kokonainen piste meni kaikesta joka vaati skriptin muuttamista että se toimisi lainkaan (syntaksivirheistä jne)..
Piste meni myös Gnu-spesifeihin ominaisuuksiin olennaisesti nojautuvissa ratkaisuissa (kuten tail -qn1 "$@" 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.
Gnu-spesifisten ominaisuuksien käyttö silloin kun niillä ei ollut olennaista merkitystä (kuten tail --lines 1) katsottiin pikkuvirheeksi.
Ylimääräisistä koristuksista tai turhan monimutkaisista ratkaisuista ei sakotettu.
Seuraavassa yksityiskohtia tehtävittäin (alkuperäiset kysymykset kursivoituna):
Helpoin ratkaisu on varmaankin awk: määrittelemällä kenttäerottimeksi lainausmerkki riittää kun laskee kenttien määrän (pariton määrä kenttäerottimia tarkoittaa parillista määrää kenttiä). Siis esim. näin:
#! /bin/sh awk -F\" 'NF%2==0' "$@"Lainausmerkit voi myös laskea vaikka gsub()-funktiolla (korvaten lainausmerkin itsellään), esim. näin:
#! /usr/bin/awk -f gsub(/"/,"&")%2Kovin vaikeaa tämä ei ole grepilläkään: määritellään malli jossa on mielivaltainen määrä kaksi lainausmerkkiä sisältäviä jonoja ja sitten yksi jossa on tasan yksi sellainen (huom. ankkurit tai -x tarvitaan):
#! /bin/sh grep -E '^([^"]*"[^"]*")*[^"]*"[^"]*$' "$@"tai
#! /bin/sh grep -xE '([^"]*"[^"]*")*[^"]*"[^"]*' "$@"
Myös sed sopii tarkoitukseen: talletetaan rivi ensin, sitten poistetaan kaikki parilliset lainausmerkit ja jos niitä jää tasan yksi, palautetaan ja tulostetaan alkuperäinen rivi:
#! /usr/bin/sed -f h s/"[^"]*"//g /"/!d gTässä sai yhden pisteen jos vastaus oli korjattavissa yhdellä muutoksella, esim. jos se löysi rivit joilla on parillinen määrä lainausmerkkejä tai jos grep-mallista puuttui ankkurointi (^$ tai -x).
Tässä tarvitsi vain vaihtaa kauttaviivat pisteiksi ja kuukauden ja päivän järjestys. Nelinumeroisen vuoden lyhentämistä kaksinumeroiseksi ei vaadittu (toki sallittiin), riitti että se tulee oikeaan paikkaan alkuperäisessä muodossa.
Tässäkin ehkä helpoin on awk, esim:
#! /bin/sh awk -F/ '{print $2,$1,$3}' OFS=.tai
#! /usr/bin/awk -f BEGIN{FS="/" } { printf "%s.%s.%s\n", $2,$1,$3 }Lähes yhtä helposti se käy sedillä:
#! /usr/bin/sed -f s-\(.*\)/\(.*\)/\(.*\)-\2.\1.\3-tai read/printf-komennoilla (myös echo kelpasi koska syötön tarkistusta ei vaadittu):
#! /bin/sh IFS=/ read month day year printf "%d.%d.%d\n" $day $month $yearYhden pisteen sai pelkästä erotinmerkkien vaihtamisesta tai yhdellä muutoksella korjattavissa olevasta virheestä.
Tässä yksinkertaisin ratkaisu on tail -n 1 jokaiselle tiedostolle erikseen:
#! /bin/sh for i ; do tail -n 1 "$i"; doneMyös vanha syntaksi tail -1 kelpasi. Huom. standardi-tail ei osaa käsitellä kuin yhden tiedoston!
Myös sediä voi käyttää, mutta se ei paljoa asiaa helpota: sedissä $ vastaa vain viimeisen tiedoston viimeistä riviä jos tiedostoja on monta, joten silmukka tarvitaan kuitenkin:
#! /bin/sh for i ; do sed '$!d' "$i"; done
Paljon helpompi ei ole myöskään awk, vaikka sillä tiedoston vaihtumisen voikin havaita:
#! /usr/bin/awk -f f && f != FILENAME { print line } { line=$0; f=FILENAME } END { print line }Tiedostojen luettavuutta tai tyyppiä ei tarvinnut tarkistaa, eikä epäonnistuneistakaan tarkistusyrityksistä menettänyt pisteitä.
Tässä ei myöskään tarvinnut miettiä mitä tehdä jos tiedoston lopusta puuttuu rivinvaihto, eikä ylimääräisen tyhjän rivin tulostamisesta tiedostojen väliin sakotettu.
Gnu-spesifisistä ratkaisuista tyyliin tail -qn 1 "$@" sai yhden pisteen, pelkästään yhden tiedoston kanssa toimivasta ei yhtään.
Tässä helpoimmalla päässee ls -Rl:n ja awkin kanssa. Pistealkuisten tiedostojen laskemiseksi tarvitaan ls:lle myös optio -a ja rivinvaihtoja sisältävien tiedostonimien varalta -q; kumpaakaan ei kuitenkaan vaadittu:
#! /bin/sh ls -Rlaq "$@" | awk '/^-/ {t+=$5; n++} END{printf "count %d, avg. size %.2f\n",n,t/n}'Tässä find on turha koska ehto oli noin yksinkertainen ja tiedostojen koon saamiseksi tarvitaan kuitenkin ls -l, mutta toki sekin toimii:
#! /bin/sh find "$@" -type f -exec ls -lq {} \; | awk '{t+=$5} END{printf "count %d, avg. size %.2f\n",NR,t/NR}'Myös shellin aritmetiikan käyttö awkin asemesta käy, kunhan muistaa että siinä on vain kokonaisluvut (ja että toisin kuin awkin, printf-komento ei tunne %f-formaattia (Gnu-versio kylläkin)), ja että standardishellissä putkitus silmukalle luo alishellin. Tämä toimii vain ksh:lla (kelpasi jos se oli huomattu):
#! /bin/ksh count=0 sum=0 ls -Rlaq "$@" | grep '^-' | while read perm links user group size junk ;do count=$((count+1)) sum=$((sum+size)) done avg=$((sum*100/count)) echo "$count files, avg. ${avg%??}.${avg#${avg%??}} bytes"Standardi-sh:lla (tai bashilla) asian voi hoitaa esim. näin:
#! /bin/sh count=0 sum=0 ls -Rlaq "$@" | grep '^-' | ( while read perm links user group size junk ;do count=$((count+1)) sum=$((sum+size)) done avg=$((sum*100/count)) echo "$count files, avg. ${avg%??}.${avg#${avg%??}} bytes" )Myös aputiedoston käyttö käy, ja laskut voi tehdä bc:llä tai exprilläkin.
Tässä meni piste jos hukkasi desimaalit, samoin Gnu-spesifisistä tempuista. Yhden pisteen sai jo lukumäärän laskemisesta oikein.
Yksinkertaisin hyväksyttävä ratkaisu oli tällainen:
#! /bin/sh xargs -n 1 wc -l | sort -nTuossa on se vika että se särkyy tyhjiä sisältävillä tiedostonimillä. Asian voi korjata esim. näin:
#! /bin/sh while read -r filename; do wc -l <"$filename" done | sort -nRivinvaihtoja sisältäviä tiedostonimiä ei voinutkaan käsitellä - stdinistä luettaessa rivinvaihtojen merkitystä ei voi tietää, ellei siihen ole erikseen sovittu jotakin. (Ilman -r -optiota read tulkitsee kenoviivan jatkorivin merkiksi, ja se hyväksyttiin ilman kommenttiakin.)
Yleinen vika tässä oli option -n unohtaminen sort-komennosta, mistä meni yksi piste. Pikkuvirheestä kävi -g:n käyttö sen asemesta (se on vain Gnu sortissa).
Helpointa lienee ensin lisätä alkunolla tarvittaessa ja vaihtaa 12 tunneissa 00:ksi, lajitella niin että am/pm on merkitsevin, ja lopuksi muuntaa 00:t takaisin ja poistaa turhat alkunollat:
#! /bin/sh export LC_COLLATE=C sed -e 's/^.:/0&/' -e 's/^12/00/' "$@" | sort -k 1.7 | sed -e 's/^00/12/' -e 's/^0//'LC_COLLATE:n asettamista ei vaadittu ("a"<"p" pitänee paikkansa kaikissa normaaleissa localeissa).
Asian voi ratkaista myös erottelemalla aamu- ja iltapäivän esim. näin:
#! /bin/sh { sed -n '/am/s/^12/00/p' "$@" | sort -n sed -n '/pm/s/^12/00/p' "$@" | sort -n } | sed 's/^00/12/'
Huom. muutos pitää tehdä sekä aamu- että iltapäivällä: 12:30am = 00.30 ja 12:30pm = 13.30.
Tässä meni yksi piste 12:n väärinkäsittelystä. Pelkästä sort-komennosta ei saanut pisteitä.
Tämä käy esim. näin:
#! /bin/sh eval homedir=~$1 groups=$(id -G $1 | sed -e 's/[^0-9]/ /g' -e 's/\([0-9][0-9]*\)/-o -group \1/g' -e 's/^-o//' ) find "$homedir" -perm -g=w ! \( $groups \) -printKäyttäjä voi kuulua moneen ryhmään; jos tämä jäi huomaamatta, menetti yhden pisteen; melkein onnistuneesta yrityksestä selvisi puolen pisteen menetyksellä. Ryhmien samoin kuin kotihakemiston selvittäminen /etc/passwd- ja /etc/group-tiedostoja lukemalla kelpasi vaikkei olekaan tiukasti standardinmukaista (jos se oli tehty oikein).
Epästandardin groups-komennon käyttö katsottiin tässä pikkuvirheeksi, jos sitä oli käytetty oikein.
Tulostusformaatiksi hyväksyttiin melkein mitä tahansa, myös tiedostonimi ilman hakemistotietoa.
Tässä olennaista oli nimenomaan erikoismerkkien käsittely, ja esim. echon käytöstä, virheistä kenoviivan kanssa malleissa tms meni pisteitä. Rivinvaihtoja sisältävien tiedostonimien kanssa epäonnistuminen annettiin kuitenkin anteeksi.
Hakemistojen käsittelyä ei vaadittu, ei edes varautumista hakemistonimissä oleviin erikoismerkkeihin, tehtävä oli siinä ehkä tulkinnanvarainen.
Optioiden puuttumisesta meni vain yksi piste, ja osittaisesta käsittelystä puolikas.
Keskeneräisestä 'hyvästä yrityksestä' saattoi saada yhden tai kaksi pistettä.
Rivinvaihtoja sisältävien nimien käsittely lienee suurin vaikeus, se onnistuu helpoiten käyttämällä kahta skriptiä: ensimmäinen ("4") käsittelee optiot ja etsii muutettavat tiedostonimet ja kutsuu sitten toista find ... -execillä (huomaa että funktiota ei voi sillä tavoin kutsua, siksi tarvitaan toinen skripti):
#! /bin/sh export MOVE=mv scriptfile=/dev/null while getopts :tr: opt ;do case $opt in t) MOVE=fakemv ;; r) scriptfile="$OPTARG" ;; ?) printf "Usage %s: [-t] [-r scriptfile] dir\n" "${0##*/}" ; exit 1 ;; esac done shift $((OPTIND - 1)) find "$*" -type f \( -name '*[!-a-zA-Z0-9_.]*' -o -name '-*' \ -o -name '???????????????*' \) -exec "$0"-1 {} \; 3>"$scriptfile"
Toinen ("4-1") saa sitten argumenttinaan muutettavan tiedostonimen, sekä ympäristömuuttujassa MOVE joko "mv" tai "fakemv" option -t mukaan, sekä tiedostokahvan 3 suunnattuna palautusskriptitiedostoon:
#! /bin/sh fakemv() { printf 'mv %s %s %s\n' "$1" "$2" "$3" rm "$3" } badpath="$*" badname="${badpath##*/}" dir="${badpath%/*}" newname=$(printf "%s\n" "$badname" | tr -c 'a-zA-Z0-9_.\n-' '[_*]' | sed -e 's/^-/_/' -e 's/\(.\{14\}\).*/\1/') set -C count=0 until >"$dir/$newname" 2>/dev/null ;do count=$((count+1)) tmp=$newname newname=$newname$count while [ ${#newname} -gt 14 ] ;do tmp=${tmp%?} newname=$tmp$count done done ${MOVE:-fakemv} -f "$badpath" "$dir/$newname" printf "mv %s '" "$dir/$newname" >&3 printf "%s\n" "$badpath" | sed -e "s/'/'\\\\''/g" -e "\$s/\$/'/" >&3Tuosta on jätetty kaikki ylimääräiset virhetarkistukset pois (mistä seuraa mm. että se joutuu päättymättömään silmukkaan jos johonkin hakemistoon ei ole kirjoitusoikeutta...)
Huomaa option -r toteutus, erityisesti lainausmerkkien ja rivinvaihtojen käsittely.
Ohessa on viimeistellympi versio (fixnames), joka tekee saman asian yhdellä skriptillä, hoitaa myös hakemistonimet ja käsittelee virhetilanteita paremmin.
Tässä varsinainen ongelma oli vuosilukujen puuttuminen: tarkoitus oli, että kun aikaleimojen välissä on aina enintään vuosi, niin vuoden vaihtuminen huomataan siitä, että päiväys muuten menisi taaksepäin. Toinen vaikeus on kuukausien vertailu: standardi-sort ei tunne Gnu sortin optiota M, joka vertailee kuukausia suoraan (sen käytöstä meni piste).
Yksinkertaisin ratkaisu lienee ensin lisätä kumpaankin tiedostoon vuosiluku ja kuukausi numeerisessa muodossa, lajitella tulos yhteen ja poistaa lisäykset, esim. näin:
#! /bin/sh TMP=/tmp/.log1.tmp.$$ fixdate() { awk 'BEGIN{ year=lastmonth=0 Mon["Jan"]=1; Mon["Feb"]=2; Mon["Mar"]=3; Mon["Apr"]=4; Mon["May"]=5; Mon["Jun"]=6; Mon["Jul"]=7; Mon["Aug"]=8; Mon["Sep"]=9; Mon["Oct"]=10; Mon["Nov"]=11; Mon["Dec"]=12 } Mon[$1] < lastmonth { ++year } { lastmonth=Mon[$1] print year, Mon[$1], $0 } ' $1 } fixdate $1 > "$TMP" fixdate $2 | sort -m -k1,1n -k2,2n -k 4,4n -k 5,5 "$TMP" - | cut -d' ' -f 3- rm "$TMP"Huomaa option -m käyttö sort-komennossa: se nopeuttaa lajittelua merkittävästi kun tiedostojen tiedetään olevan ennestään järjestyksessä (vaikka toisaalta aiheuttaa aputiedoston käytön tarpeen). (Sitä ei kuitenkaan edellytetty.)
Myös tiedostojen lukeminen rinnakkain silmukassa ja kuukausien vertailu sitä mukaa onnistuu, mutta on olennaisesti hankalampaa.
Tässä pikkuvirheiksi tulkittiin ilmeisiä syntaksivirheitäkin, toimimattomasta saattoi saada neljäkin pistettä jos idea oli oikein ja se oli helposti korjattavissa, ja 1-2 pistettä saattoi saada hyvästä yrityksestä.