Thomas lager verktøy [Luke 24, 2012]


mandag 24. desember 2012 Julekalender Linux Ruby

Thomas Kjeldahl Nilsson (@thomanil) er blant annet mannen bak Mine Verktøy, hvor han i drøyt et år publiserte en rekke intervjuer hvor norske geeks fortalte hvilket verktøysoppsett de hadde. Dagens blogpost – på selveste julaften – følger i det samme sporet. Thomas snakker om noe som er veldig viktig for en effektiv utvikler; kunsten å lage sine egne verktøy!

Thomas blogger også på kjeldahlnilsson.net.

colorPortrait

Hvem er du?
Trodde jeg skulle bli kjemiker på Blindern - men fant ut at jeg hadde mye lettere for programmering, gitt!

Hva er jobben din?
Partner og programmerer hos Gitorious AS - vi utvikler en fullstendig open source løsning for Git-hosting, og lever av å selge hosting og knallgod support til kunder over hele verden.

Hva kan du?
Pragmatisk allrounder som er spesielt glad i entreprenørskap, web-teknologi... og Emacs.

Hva liker du best med yrket ditt?
Å kunne skape noe ut av ingenting.


Verktøy for å lage verktøy: en Unix-safari

2239102681_bb9ca36abc_z

Jeg er fascinert av smiekunst.

Tradisjonelle smeder jobber med enkle, grunnleggende verktøy: esse, ambolt, hammer og tang. Smeden kan imidlertid lage mer effektive og spesialiserte verktøy ved behov. Det er såvidt jeg vet ingen andre håndtverkere som kan trekke seg selv opp etter bukseselene på denne måten…

unntatt oss programmerere. Med gode basis-verktøy kan vi bygge alt vi trenger for å jobbe raskere og mer effektivt.

I denne artikkelen skal vi se hvordan Unix-miljøer enkelt lar oss bruke basisverktøy til å bygge nye verktøy og arbeidsflyter. Vi jobber på tre nivåer: rett på kommandolinja, med shellscripting, og med Ruby.

Unix-idealene

Et av de beste utvikler-verktøyskrinene finner du i Unix-systemene (Linux, OS X, BSD, mfl): der får du mange små, fokuserte og gjenbrukbare byggeklosser du kan sette sammen til nye programmer. Med andre ord: de fleste verktøy du trenger enten finnes allerede, eller kan lages ved å kombinere allerede eksisterende verktøy.

Det er nyttig å omfavne — eller i det minste forstå — Unix-kulturen, for å absorbere en del prinsipper som har gjennomsyret denne tradisjonen gjennom flere tiår. Eric S Raymond oppsummerer disse prinsippene slik:

  • Modularitet: bygg enkle deler, koblet sammen med ryddige grensesnitt.
  • Klarhet: lettforståelige løsninger er bedre enn "lure knep".
  • Komposisjon: design programmer slik at de kan kobles sammen med andre programmer.
  • Løskobling: skill kontrakt fra mekanisme, grensesnitt fra implementasjon.
  • Enkelhet: etterstreb enkelhet. Tillat bare kompleksitet der du er nødt.
  • Tilbakeholdenhet: bygg bare et stort program når det er helt nødvendig.
  • Transparens: gjør programmer synlige og åpne — det gjør testing og feilsøking enklere.
  • Robusthet: programmer som er enkle og åpenbare blir også mer robuste.
  • Representasjon: legg domenelogikk i dataen din, slik at selve programmet kan forbli rettfrem og robust.
  • Forutsigbarhet: grensesnittene dine bør være innlysende, ikke "overraskende".
  • Stillhet: hvis et program ikke har noe nyttig å si, bør det holde kjeft og bare gjøre jobben sin.
  • Feilhåndtering: dersom programmet må feile, feil så tidlig og høylydt som mulig.
  • Økonomi: utvikler-tid er den mest verdifulle ressurssen din, så stress maskinen istedet for programmereren.
  • Generering: unngå å skrive ting for hånd. Forsøk istedet å skrive programmer som skriver programmer.
  • Optimalisering: prototyping før polering. Få det til å fungere før du optimaliserer det.
  • Mangfold: det er mange veier til Rom. Vær skeptisk til vedtatte sannheter og fasiter.
  • Utvidbarhet: design for fremtiden, den er her før du tror.

Disse prinsippene gjør deg til en bedre utvikler selv om du ellers ikke bruker Unix-systemer i det hele tatt.

Scenario: vi bygger en arbeidsbenk for email

Den beste måten å få dette til å synke inn er å bruke Unix-verktøy til å bygge noe konkret. Vi skal derfor bruke grunnleggende Unix-programmer til å bygge en enkel arbeidsflyt for å jobbe med Gmail-innboksen din.

Infrastruktur: installasjon av offlineimap og msmtp

Fundamentet for emailklienten vår blir verktøyene offlineimap (for å hente mail) og msmtp (for å sende mail).

Når disse er installert kommer vi til å ha en Maildir katalog som er synkronisert med Gmail-kontoen vår via IMAP, og vi kommer til å ha en enkel kommando for å sende mail rett fra kommandolinja. Begge verktøyene er tilgjengelig på både Linux, OS X og andre Unix-varianter.

Hvis du vil følge eksemplene mine på din egen maskin så bør du få dette til å fungere først. Hvis du bare vil lese resten av artikkelen kan du hoppe over denne seksjonen.

Merk: Jeg har bare testet oppsettet beskrevet nedenfor i Ubuntu --- andre Unix-varianter kan være litt forskjellige.

offlineimap

I Ubuntu installerer du offlineimap slik:

sudo apt-get install offlineimap

Vi oppretter så en lokal Maildir katalog der vi vil at offlineimap skal legge den lokale kopien av gmail kontoen vår:

mkdir ~/Maildir

For å bruke verktøyet må vi konfigurere det til å snakke med vår egen gmail-konto. Vi oppretter fila ~/.offlineimaprc, og setter korrekte skrive/lese-rettigheter:

touch ~/.offlineimaprc && chmod 600 ~/.offlineimaprc

Vi fyller den med følgende innhold (oppdater til å matche din egen Gmail-konto):

[general]
accounts = gmail

# Kutt ut all output, med unntak av feilmeldinger
ui = quiet

[Account gmail]
localrepository = gmail-local
remoterepository = gmail-remote
status_backend = sqlite

[Repository gmail-local]
type = Maildir
localfolders = ~/Maildir/Gmail

[Repository gmail-remote]
type = Gmail
remoteuser = DIN_GMAIL_EMAIL_ADRESSE
remotepass = DITT_GMAIL_PASSORD

# Vi henter kun ned selve innboksen, ikke resten av folderne
folderfilter = lambda folder: folder in ['INBOX']

# Sletting lokalt skal ikke medføre sletting i Gmail-kontoen
realdelete = no

La oss teste om verkøyet fungerer som det skal. Kjør kommandoen offlineimap. Hvis konfigurasjonen din er korrekt så vil hele innboksen din plukkes ned og legges i katalogen Maildir i rota av hjemmeområdet ditt. Vær tålmodig: dette kan ta en stund hvis du har mye liggende i selve innboksen i gmail.

msmtp

I Ubuntu installerer du msmtp-pakken slik:

sudo apt-get install msmtp

Også dette verktøyet må konfigureres: vi oppretter fila ~/.msmtprc, og gir den korrekte skrive/lese rettigheter:

touch ~/.msmtprc && chmod 600 ~/.msmtprc

Vi fyller den med følgende innhold (oppdater til å matche din egen Gmail-konto):

defaults
auth on
tls on

account         gmail
host            smtp.gmail.com
port            587
from            DIN_GMAIL_EMAIL_ADRESSE
user            DIN_GMAIL_EMAIL_ADRESSE
password        DITT_GMAIL_PASSORD
tls_trust_file  /etc/ssl/certs/ca-certificates.crt

La oss teste om verkøyet fungerer som det skal. Kjør følgende kommando (bare bruk din egen mailadresse):

echo 'tester msmtp...' | msmtp -a gmail 'thomas@kjeldahlnilsson.net'

Sjekk så emailen din som vanlig. Dersom msmpt ble konfigurert riktig så skal du ha mottatt en mail fra deg selv nå med innholdet "tester msmtp…"

Fungerer alt som det skal? Flott, da fortsetter vi.

Kataloger, filer og strenger er alt vi trenger

Etter at vi har kjørt offlineimap første gang ender vi opp med en katalog ~/MailDir/Gmail, som inneholder underkataloger som representerer mapper og tilstander i hver mailfolder. Hver mail i disse folderne er lagret som en vanlig flatfil.

Folderstrukturen ser omtrent slik ut:

Maildir/
└── Gmail
    └── INBOX
        ├── cur
        │   └── 1352560840_1.25056.localhost,U=2,FMD5=7e33429f656f1e6e9d79b29c3f82c57e:2,S
        ├── new
        │   ├── 1352560840_0.25056.localhost,U=1,FMD5=7e33429f656f1e6e9d79b29c3f82c57e:2,
        │   └── 1352560841_0.25056.localhost,U=3,FMD5=7e33429f656f1e6e9d79b29c3f82c57e:2,
        └── tmp

Filnavnene ser litt kryptiske ut, fordi offlineimap bruker filnavnene til å lagre en del informasjon om hver email: unike IDer, checksums etc. Filene under cur katalogen inneholder leste email, mens uleste mailer ligger under new.

Tilstand i denne lokale mailboksen synkroniseres til gmail-kontoen vår hver gang vi kjører offlineimap kommandoen. For eksempel kan vi flytte en email fra new til cur mappen og så kjøre offlineimap. Dette vil sette statusen på email til "lest" i Gmail-kontoen sentralt også.

Innholdet av en email ser ut omtrent slik:

MIME-Version: 1.0
Received: by 10.112.4.227; Sat, 10 Nov 2012 07:18:48 -0800 (PST)
Date: Sat, 10 Nov 2012 07:18:48 -0800
Message-ID: <CABQd01N+VFn89guaFCcBu+x=rJudno+yarZPZF1=CqWDz=sQnw@mail.gmail.com>
Subject: Import your contacts and old email
From: Gmail Team <mail-noreply@google.com>
To: Kensei Test Account <kensei.test@gmail.com>
Content-Type: multipart/alternative; boundary=00151748de9ec3315804ce259570

--00151748de9ec3315804ce259570
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: quoted-printable

You can import your contacts and mail from Yahoo!, Hotmail, AOL, and many
other web mail or POP accounts. If you want, we'll even keep importing your
mail for the next 30 days.
     Import contacts and mail
=BB<https://mail.google.com/mail/#settings/accounts>

We know it can be a pain to switch email accounts, and we hope this makes
the transition to Gmail a bit easier.

- The Gmail Team

Please note that importing is not available if you're using Internet
Explorer 6.0. To take advantage of the latest Gmail features, please upgrad=
e
to a fully supported
browser<http://support.google.com/mail/bin/answer.py?answer=3D6557&hl=3Den&=
utm_source=3Dwel-eml&utm_medium=3Deml&utm_campaign=3Den>
.

--00151748de9ec3315804ce259570
Content-Type: text/html; charset=ISO-8859-1

<html>
<font face="Arial, Helvetica, sans-serif">
<p>You can import your contacts and mail from Yahoo!, Hotmail, AOL, and many
other web mail or POP accounts. If you want, we'll even keep importing your
mail for the next 30 days.</p>

<table cellpadding="0" cellspacing="0">
<col style="width: 1px" /><col /><col style="width: 1px" />
<tr>
  <td></td>
  <td height="1px" style="background-color: #ddd"></td>
  <td></td>
</tr>
<tr>
  <td style="background-color: #ddd"></td>
  <td background="https://mail.google.com/mail/images/welcome-button-background.png"
      style="background-color: #ddd; background-repeat: repeat-x"
    ><a href="https://mail.google.com/mail/#settings/accounts"
        style="font-weight: bold; color: #000; text-decoration: none; display: block; padding: 0.5em 1em"
      >Import contacts and mail &#187;</a></td>
  <td style="background-color: #ddd"></td>
</tr>
<tr>
  <td></td>
  <td height="1px" style="background-color: #ddd"></td>
  <td></td>
</tr>
</table>

<p>We know it can be a pain to switch email accounts, and we hope this makes
the transition to Gmail a bit easier.</p>

<p>- The Gmail Team</p>

<p><font size="-2" color="#999">Please note that importing is not available if
you're using Internet Explorer 6.0. To take advantage of the latest Gmail
features, please
<a href="http://support.google.com/mail/bin/answer.py?answer=6557&hl=en&utm_source=wel-eml&utm_medium=eml&utm_campaign=en"><font color="#999">
upgrade to a fully supported browser</font></a>.</font></p>

</font>
</html>

--00151748de9ec3315804ce259570--

Hvorfor er dette nyttig?

Siden mailboksen vår er representert med vanlige kataloger, filer og strenger så kan vi bruke standard Unix-verktøy for å lese og manipulere email fra kommandolinja. Nå kan vi begynne å leke med verktøykassa vår!

"In The Beginning was the Command Line"

Unix-verktøy følger noen felles konvensjoner for å ta imot data og og sende resultater ut.

Programmer kjørt på kommandolinja tar data inn på STDIN strømmen, og spytter resultater ut igjen på strømmen STDOUT (med unntak av feilmeldinger , som spyttes ut på STDERR strømmen). Program-output ender enten i terminalen, eller kan sendes videre til et annet program.

Siden disse felles konvensjonene finnes kan vi kombinere programmer: Ved å kjede sammen flere verktøy på samme linje med | (pipe) mellom hver av dem, kan vi la data flyte gjennom dem etter tur som i et vannrør. Vi kan med andre ord enkelt bygge verktøy som består av pipelines av andre kommandoer.

Et eksempel: denne linja skrev jeg forrige uke for å finne alle sannsynlige synkroniserings-konflikter i Dropbox-mappa mi.

find ~/Dropbox | grep conflicted

Find-kommandoen lister opp alle filer, rekursivt, under den angitte katalogen. Hvert filnavn pipes videre til grep-kommandoen, som beholder de filstiene som har inneholder 'conflicted'. Listen av filnavn ender så i terminalen min slik at jeg kan se hvilke filer jeg antagelig bør rydde vekk. Det finnes mer optimale måter å gjøre denne oppgaven, men poenget er at det tok meg bare noen sekunder å lage denne automatiseringen.

Å kjede sammen pipelines av filtere, transformasjoner etc føles svært naturlig dersom du allerede er komfortabel med å bruke map, filter og reduce-operasjoner i "ordentlige" programmeringsspråk.

La oss bygge noen verktøy for email.

Terminal-snutt: Send en mail

Denne har du kanskje allerede kjørt for å teste at msmtp fungerer.

echo 'Dette er body i en mail jeg sender fra terminalen' | msmtp -a gmail 'dinadresse@gmail.com'

echo skriver ut den påfølgende teksten til STDOUT. Hvis den står for seg selv så får vi teksten tilbake ut i terminalen. Her piper vi istedet teksten til msmtp, som bruker teksten den får inn på STDIN til som body i en ny mail.

Terminal-snutt: Sjekk antall uleste email

Følgende linje teller hvor mange uleste email vi har.

find ~/Maildir/Gmail/INBOX/new -type f | wc -l

Vi finner alle filer i new folderen (kun filer, ikke kataloger etc), og bruker wc til å telle hvor mange av dem det var. Jeg synes det er litt knapt å bare dumpe ut tallet, så jeg legger til litt beskrivende tekst også:

echo "Unread emails: $(find ~/Maildir/Gmail/INBOX/new -type f | wc -l)"

Nå har vi alt vi trenger for å lage en liten "widget" for å vise antall uleste email. Vi gjør det ved å ha et lite terminalvindu på desktopen vår, der vi kjører følgende kommando for å sette den igang:

watch -n10 'offlineimap && echo "Unread emails: $(find ~/Maildir/Gmail/INBOX/new -type f | wc -l)"'

watch kjører de påfølgende kommandoene hvert tiende sekund — vi synkroniserer først mailen vår, så kjører vi programsnutten vår for å telle uleste meldinger.

Nå har vi et terminalvindu som hele tiden ser slik ut, og blir oppdatert hvert 10 sekund:

Every 10.0s: offlineimap && echo "Uleste mail: $(find ~/Maildir/Gmail/INBOX/new -type f | wc -l)"          Fri Nov 30 21:14:11 2012

Uleste mail: 1

Terminal-snutt: Innboks-oversikt

En naturlig start er å lage oss et sammendrag av hvilke mail vi har liggende i innboksen vår. Her er en måte å skrive ut en liste over mailene våre:

grep -Rh ^Subject: ~/Maildir/Gmail/INBOX

Vi søker rekursivt (R) i innboks-folderen vår etter tilfeller av <Linjestart> Subject:. H-en er der for å kun skrive ut subjekt-linjen, ikke det kryptiske filnavnet i tillegg (grep skriver ellers ut både filnavn og hvordan treffet ser ut).

Dette bør treffe en gang i hver fil (vi går ut fra at dette er standardformatet på mailfilene og at det forhåpentligvis bare er en slik linje i hver fil). Disse matchende emne-linjene skrives ut til terminalen vår, og ser omtrent slik ut når vi kjører kommandoen vår:

➜  ~  grep -Rh ^Subject:  ~/Maildir/Gmail/INBOX
Subject: Get Gmail on your mobile phone
Subject: Import your contacts and old email
Subject: Customize Gmail with colors and themes

Nå har vi oversikt over innboksen vår.

Terminal-snutt: Les en mail

Vi bør også kunne lese en bestemt mail. Denne linja gjør det mulig å dumpe ut innholdet av mail nummer N fra lista ovenfor, telt fra toppen. Denne er noe mer innfløkt:

find ~/Maildir/Gmail/INBOX -type f | sed -n 2p | xargs cat

Vi finner alle filer rekursivt under innboksen vår. Vi plukker den nte linja i fil-lista (i dette tilfelle nummer to), og sender fil-stien til cat kommandoen, som bare dumper ut innholdet av den gitte fila i terminalen vår.

Disse email-kommandoene vi har laget her fungerer jo helt greit, men de er ikke så smidige å jobbe med hvis du trenger å bruke dem raskt og ofte. Det er på tide å ty til shellscripting for å forenkle og gjenbruke.

Sidespor: ta vare på småting du lærer og bygger

Jeg har problemer med å huske hendige terminal-snutter den første gangen jeg bruker dem. Her er noen grep som hjelper:

  • Du kan søke bakover i historikken din ved å trykke Ctrl-r. Hvis du begynner å skrive får du opp første mulige treff, og kan trykke pil-opp for å hoppe til neste kandidat som matcher søket bakover i historikken din. Og sørg for at terminalen din er satt til å bevare mye, eller all, historikk bakover i tid!
  • Personlige "cheatsheets". Jeg har en orgmode-fil der jeg skriver ned kjekke one-liners, verktøy, program-snutter etc jeg kommer over — mens jeg jobber, fra artikler og bøker, fra kollegaer og så videre. Jeg er ikke så flink til å ta til meg nye greier på første forsøk, så jeg liker å skrive ting opp og vende tilbake til dem senere for å repetere og gjenoppdage.
  • Definer aliaser i terminal-miljøet ditt. Hvis du bruker Bash så oppretter eller oppdaterer du fila ~/.bashrc, og kan legge til linjer på denne formen:
alias helloworld="echo 'hello world'"

Når du har lastet miljøet på nytt vil du kunne bruke aliaset som en hvilken som helst annen kommando/script. Vi kan for eksempel forenkle en av linjene vi skrev ovenfor:

alias min_innboks="grep -Rh ^Subject: ~/Maildir/Gmail/INBOX"

Nå blir det litt enklere å sjekke mailboksen:

➜  ~  min_innboks
Subject: Get Gmail on your mobile phone
Subject: Import your contacts and old email
Subject: Customize Gmail with colors and themes

Når oneliners ikke strekker til banker shellscripting på døra

Vi kommer til et punkt der vi trenger mer faktisk programmering for å få ting gjort. Med andre ord: variable, conditionals, løkker, og ikke minst muligheten til å spre logikken over flere linjer.

La oss lage bash-scripts av operasjonene vi gikk gjennom i forrige seksjon. Da kan vi forbedre dem ved å gjøre dem tilgjengelige som kortere, parametriserte kommandoer.

All kode vi skriver her og i de neste seksjonene er forøvrig tilgjengelig for nedlasting.

Shellscript: Send en mail

Vi lager et script som heter send-email, som tar mottaker og mail-tekst inn som parametre.

#!/bin/sh

RECIPIENT=$1
TEXT=$2
echo $TEXT | msmtp -a gmail $RECIPIENT

Den aller første linja er en såkalt shebang som forteller hvordan scriptet skal eksekveres (i dette tilfellet sier vi at programmet er et shellscript). $1, $2 etc er variabler som bindes til å inneholde til shellscriptet. For lesbarhetens skyld tilegner vi dem til nye variable, før vi kjører samme kommando som vi gjorde ovenfor for å sende emailen.

Hvis du putter send-email i din egen path kan du kjøre den slik:

send-email thomanil@gmail.com "Sent from a tiny shellscript"

Dette verktøyet vårt ble litt mer brukervennlig nå, ikke sant?

Shellscript: Sjekk antall uleste email

Kommandoen vår for å holde styr på antall uleste mail gjør vi om til et script som heter watch-unread-emails, og ser slik ut:

#!/bin/sh

POLLING_INTERVAL=$1
watch -n$POLLING_INTERVAL 'offlineimap && echo "Unread emails: $(find ~/Maildir/Gmail/INBOX/new -type f | wc -l)"'

Kommandoen vår tar en parameter: antall sekunder mellom hver gang kommandoen skal kjøres/oppdateres. Slik kjøres den når scriptet er i pathen din:

watch-unread-emails 10

Shellscript: Innboks-oversikt

Vi lager en kommando som heter display-inbox for å liste opp innholdet i innboksen vår.

#!/bin/sh

grep -Rh ^Subject: ~/Maildir/Gmail/INBOX

Denne er bare den samme pipelinen vi tidligere skrev direkte inn i terminalen — bare mer brukervennlig siden vi slipper å huske den grep-kommandoen.

Når scriptet er tilgjengelig i pathen din kan du eksekvere det slik:

display-inbox

Shellscript: Les en mail

Linja vi tidligere lagde for å lese innhold av en bestemt mail var rimelig knotete, så det blir fint å forenkle litt.

#!/bin/sh

MAIL_NUMBER=$1
SED_COMMAND=$(printf "sed -n %sp" $MAIL_NUMBER)
find ~/Maildir/Gmail/INBOX -type f | $SED_COMMAND | xargs cat

Scriptet tar "mail nummer N fra toppen" som argument. Vi velger å bygge opp sed-kommandoen separat underveis for å gjøre det hele mer lesbart.

Når scriptet ligger i pathen din kan du kjøre det på følgende måte.

read-email 2

Helt klart enklere enn den kryptiske linja vi måtte skrive inn før.

Sidespor: gjør det enkelt for deg selv å scripte

Hvis du gjør terskelen for å skrive nye scripts så lav som mulig, så lager du også fler av dem og gjør derfor mer for å forbedre arbeidsflyten din. Her er to grep som hjelper:

  • Lag en katalog i hjemmeområdet ditt som er dedikert til nye scripts, f.eks ~/bin eller ~/scripts. Legg til denne katalogen i pathen slik at scriptene dine er tilgjengelige i miljøet ditt overalt. Bonus-poeng: lag en git-repo av script-katalogen din slik at har versjonskontroll, og hvis du jobber på flere maskiner, legg katalogen i Dropbox-folderen din og symlink dit fra alle maskinene du sitter på.
  • Lag et program som enkelt lar deg generere nye scripts. Under ser du mitt ~/script/generatescript bash-script. Det tar navn på nytt script som argument, oppretter det i standard-katalogen min (med riktige rettigheter), og fyrer opp standard-editoren min slik at jeg umiddelbart kan begynne å arbeide på det nye scriptet.
#!/bin/sh

SCRIPTPATH=~/scripts/$1
echo '#!/bin/sh
# Generated, add code here
' >> $SCRIPTPATH

touch $SCRIPTPATH
chmod a+x $SCRIPTPATH
$EDITOR $SCRIPTPATH

Når shellscripting blir for stygt sier skjønne Ruby hei

Perl kom til verden fordi Larry Wall syntes rå shellscripting ble for primitivt og lite ekspressivt. Etterhvert fikk vi også Ruby, Python, Groovy med fler, som sammen med Perl gjorde Unix-scripting langt mer behagelig.

Vi skal skrive om kommandoene våre til Ruby. Dette gir oss to umiddelbare gevinster: mer lesbare og utvidbare scripts og, for read-email kommandoen vår, parsing av email via et eksternt Ruby-bibliotek.

Ingenting av det vi gjør i Ruby-koden er umulig å få til i vanlige shellscripts — men Ruby er mer lettlest og vedlikeholdbart.

Ruby-script: Send en mail

Vi skriver om send-email shellscriptet vårt til Ruby:

#!/usr/bin/env ruby

if ARGV.length != 2
  puts "Usage: send-email TO_ADDRESS EMAIL_BODY"
  exit 1
end

recipient = ARGV[0]
text = ARGV[1]
puts `echo #{text} | msmtp -a gmail #{recipient}`

Vi starter scriptet med en ny "shebang"-linje, denne sier at systemet skal bruke ruby kommandoen i brukerens miljø til å eksekvere koden.

Vi legger også til en validering av at riktig antall parametre sendes inn til kommandoen. Argumenter til Ruby-programmer blir lagt i en konstant, global array som heter ARGV. Dersom det er for få eller for mange parametre skriver vi ut en kortfattet bruksanvisning og stopper videre kjøring.

Selve kjøringen av msmtp sheller vi ut til systemet. Dette er fordelen med å bruke Ruby og lignende språk: vi kan delegere ned til det underliggende systemet når som helst. Vi kan derfor velge hvor mye vi vil lene oss på vanlige Unix-verktøy kontra scriptspråkets egne biblioteker — det er ikke en "enten-eller" situasjon.

Ruby-script: Sjekk antall uleste email

Nestemann ut er watch-unread-emails scriptet vårt.

#!/usr/bin/env ruby

if ARGV.length != 1
  puts "Usage: watch-unread-emails POLLING_INTERVAL_SECONDS"
  exit 1
end

polling_interval = ARGV[0].to_i

while true
  new_mail_dir = File.expand_path("~/Maildir/Gmail/INBOX/new/*")
  unread_count = Dir[new_mail_dir].count { |file| File.file?(file) }
  puts `clear && offlineimap`
  puts "Unread emails: #{unread_count}"
  sleep polling_interval
end

Istedet for å lene oss på watch kommandoen så implementerer vi bare tilsvarende logikk selv i Ruby: skriv ut antall uleste email hvert nte sekund.

Vi tømmer terminalen for innhold og synkroniserer email ved å shelle ut til clear kommandoen og offlineimap. Selve antallet email finner vi ved å bruke Rubys egne file-apier istedet for find-kommandoen direkte.

Ruby-scriptet ble her en del lenger enn en det tilsvarende shellscriptet, fordi jeg kapper operasjonene opp i flere steg, variable og linjer. For meg føles dette noe mer utvidbart og lettlest enn det opprinnelige shellscriptet.

Ruby-script: Innboks-oversikt

display-inbox scriptet skriver vi bare om til Ruby for å være konsekvente: Ruby-versjonen sheller ut samme operasjon som tidligere. Jeg syntes at en enkelt grep-linje var helt greit, og det illustrerer at Ruby også kan fungere som en veldig tynn wrapper rundt ordinær shellscripting.

#!/usr/bin/env ruby

puts `grep -Rh ^Subject: ~/Maildir/Gmail/INBOX`

Ruby-script: Les en mail

Til slutt porter vi read-email til Ruby-kode.

#!/usr/bin/env ruby

if ARGV.length != 1
  puts "Usage: read-email EMAIL_NO"
  exit 1
end

#depends on the 'mail' gem, install like this: gem install mail
require 'mail'

maildir = File.expand_path("~/Maildir/Gmail/INBOX")
all_email_filepaths = Dir["#{maildir}/**/*"].select { |f| File.file?(f) }
mail_number = (ARGV[0].to_i)-1
mail_path = all_email_filepaths[mail_number]
mail = Mail.read(mail_path)
puts mail.text_part

Vi bruker Rubys fil-apier til å finne stien som inneholder den nte mailen fra toppen i innboksen. Så drar vi nytte av et eksternt Ruby-bibliotek (en såkalt gem) som heter Mail til å parse email-fila. Til slutt skrives mailen ut som html.

<html>
<font face="Arial, Helvetica, sans-serif">
<p>You can import your contacts and mail from Yahoo!, Hotmail, AOL, and many
other web mail or POP accounts. If you want, we'll even keep importing your
mail for the next 30 days.</p>

<table cellpadding="0" cellspacing="0">
<col style="width: 1px" /><col /><col style="width: 1px" />
<tr>
  <td></td>
  <td height="1px" style="background-color: #ddd"></td>
  <td></td>
</tr>
<tr>
  <td style="background-color: #ddd"></td>
  <td background="https://mail.google.com/mail/images/welcome-button-background.png"
      style="background-color: #ddd; background-repeat: repeat-x"
    ><a href="https://mail.google.com/mail/#settings/accounts"
        style="font-weight: bold; color: #000; text-decoration: none; display: block; padding: 0.5em 1em"
      >Import contacts and mail &#187;</a></td>
  <td style="background-color: #ddd"></td>
</tr>
<tr>
  <td></td>
  <td height="1px" style="background-color: #ddd"></td>
  <td></td>
</tr>
</table>

<p>We know it can be a pain to switch email accounts, and we hope this makes
the transition to Gmail a bit easier.</p>

<p>- The Gmail Team</p>

<p><font size="-2" color="#999">Please note that importing is not available if
you're using Internet Explorer 6.0. To take advantage of the latest Gmail
features, please
<a href="http://support.google.com/mail/bin/answer.py?answer=6557&hl=en&utm_source=wel-eml&utm_medium=eml&utm_campaign=en"><font color="#999">
upgrade to a fully supported browser</font></a>.</font></p>

</font>
</html>

Rå html-kode er ikke så lesbar, men vi kan kanskje bruke Firefox til å lese mailen?

read-email 2 > email.html && firefox email.html

Sidespor: shellscripting eller høynivå språk?

Er det lurt å holde seg så til så enkle verktøy som mulig, eller bør du alltid hoppe rett til det høyeste abstraksjonsnivået i verktøykassa?

Vel, du kan bygge hva som helst bare du har et turing-komplett språk — se for eksempel denne implementasjonen av Tetris i sed — men det er fint å kunne ta steget opp til mer ekspressive språk når vi har behov for det.

Fordelen med moderne scriptspråk som Ruby, Python etc er som nevnt over at de har mer behagelig syntax, og mange hendige eksterne biblioteker. De er også mer kryssplatform enn shellscripting, noe som gjør at du brått kan støtte mer enn bare Unix. For eksempel: ved å bruke Rubys fil-apier så kan scriptet ditt potensielt fungere på Windows også.

Ulempen med de moderne scriptspråkene er at de innfører flere eksterne avhengigheter: hvis du holder deg til shellscripting og standard Unix-verktøy så kan scriptet ditt fungere i minimale Unix-systemer uten å måtte installere flere eksterne pakker.

Det jeg ofte gjør når jeg skal skrive små programmer er å starte med noen enkle Unix-kommandoer i terminalen, og så trekke inn Ruby med en gang jeg ser at ting blir for komplekst for Bash.

Fjellvett: vend i tide, det er ingen skam å snu

Når du plukker opp nye byggeklosser på denne måten så ser du stadig flere løsninger på problemer, og det er fristende å bygge masse greier selv hele tiden. Men: bare fordi du kan bygge hva du vil fra bunnen av, er det ikke alltid lurt å gjøre det.

Vi må velge kampene våre. Noen ganger er det mer pragmatisk å gå ut og kjøpe et kanskje komplekst, suboptimalt, lukket verktøy… som faktisk gjør jobben uten at du trenger å bygge alt selv.

Det kommer an på situasjonen, tenk deg om før du hopper!


comments powered by Disqus