Beginner scripting

I've begun using GNU Smalltalk for scripting purposes. The more I use it, the more I like it. Scripts pass the "its six months later can I understand what I did here" test easily, and errors usually result in a stack trace or a line number so debugging is fine.

Anyway, as always with a new language it takes a little while to figure out how to read and write files, how to display integers etc. Like most people, I find examples are helpful.

I had occasion to write a script to parse a text file with a list of accounts, and from it generate an LDIF text file for upload to an LDAP directory server. I figured this would be a good example to post for other beginning Smalltalkers.

The actual work of parsing the text file is done in several lines in the main area.

Items of interest in the script are:

  • Dictionary initialisation and use
  • File reading and writing (streams)
  • Ordered Collections
  • Tokenizer
  • Objects

The code uses the "old" GNU Smalltalk syntax.

I have found the "terse guide to Squeak a handy reference. The Transcript, and also Array initialization have slightly different syntax in GST than Squeak.

The script below can be run from the command line; just paste it into a file, e.g. Then at the command prompt type "chmod u+x" and then run it, i.e. ./
Note the gst binary is in /usr/local/bin/gst on my machine.

#!/usr/local/bin/gst -f
"convert csv data to LDIF format

CSV file is in Mac OS X server import format as shown below:-

0x0A 0x5C 0x3A 0x2C dsRecTypeStandard:Users 13 dsAttrTypeStandard:RecordName dsAttrTypeStandard:AuthMethod  dsAttrTypeStandard:Password  dsAttrTypeStandard:UniqueID  dsAttrTypeStandard:PrimaryGroupID  dsAttrTypeStandard:Keywords  dsAttrTypeStandard:RealName  dsAttrTypeStandard:UserShell  dsAttrTypeStandard:FirstName  dsAttrTypeStandard:LastName  dsAttrTypeStandard:Street  dsAttrTypeStandard:City  dsAttrTypeStandard:EMailAddress 

Object subclass: #Person
        instanceVariableNames: 'uid attributes'
        classVariableNames: ''
        poolDictionaries: ''
        category: 'SG-Scripting'!

Person comment: 
'LDIF parsing: 
Attributes stored in a dictionary. Note: handling multivalued attributes
would require a dictionary of collections. Have not needed to go there
for the attributes we are handling here.' !

!Person methodsFor: 'instance creation'!
   attributes := Dictionary new. ! ! 

!Person methodsFor: 'accessing'!

    ^attributes at: 'uid'. !

    ^attributes !

getValue: anAttribute
    ^attributes at: anAttribute ifAbsent: [^nil] !

setValue: anAttribute value: aValue
    attributes at: anAttribute put: aValue !

    "For purposes of this example, if have 4 attributes then its OK."
    ^(attributes size >= 4) & (uid isNil not) !  

Object subclass: #LdifGen
        instanceVariableNames: 'dn base people'
        classVariableNames: ''
        poolDictionaries: ''
        category: 'SG-Scripting'!

LdifGen comment: 
'Write out LDIF text for a person object' !

!LdifGen methodsFor: 'instance creation'!
init: thePeople
        people := thePeople.    
        base := 'ou=users,dc=example,dc=com'. !

!LdifGen methodsFor: 'operation' !
printRecords: aStream
"print the ldif entries. Prints out all attribute values of the person"
        people do: [ :person |
                aStream display: ('dn: uid=%1,%2' bindWith: (person getValue: 'uid') with: base); nl.
                aStream display: 'objectClass: person'; nl.
                aStream display: 'objectClass: organizationalPerson'; nl.
                aStream display: 'objectClass: inetOrgPerson'; nl.
                person attributes keysAndValuesDo: [ :aKey :aValue |
                        aStream display: ('%1: %2' bindWith: aKey with: aValue); nl. ].
                aStream nl.
        ]. !
printPersonAttr: anAttr person: aPerson
        ^'%1: %2' bindWith: anAttr with: (aPerson getValue: anAttr).
        ! !

"main program starts here...
 import the csv file into a person collection, then iterate of the colleciton and write out
   the person attributes in ldif format"
| csvFile persons person ldifGen ldifPath ldifFile |
    osxcsv :=         '/project/LDAPImport.csv'.
    ldifPath :=         '/project/LDAPImport.ldif'.
    csvFile := File name: osxcsv.
    persons := OrderedCollection new.
    "prepare dictionary with names for csv fields - but only the fields we want"
    csvmap := Dictionary new.
    cvsmap := Dictionary from: {
            'uid' -> 1. 
            'userPassword '-> 4. 
        'gidNumber' -> 6.
        'cn' -> 8.
        'givenName' -> 10.
        'sn' -> 11.
        'mail' -> 14.
    csvFile readStream linesDo: [:line |
        Transcript nl; showCr: line.
        ( line =~ 'dsAuthMethodStandard') matched ifTrue: [
            tokens  := line tokenize: ':'.
            person  := Person new init.
            cvsmap  keysAndValuesDo: [ :key :value |
                    person  setValue: key value: (tokens at: value). "tokens is an Array; value  contains token index"
                    Transcript  showCr: ('%1: %2 - %3' bindWith: key  with: (tokens at: value) with: value ).                    
            person isMailPerson ifTrue: [
                    persons add: person ].                

    Transcript nl; nl; showCr: 'Output '; nl.
    Transcript showCr: 'persons size: ',persons size printString.
    ldifGen := LdifGen new init: persons.
    ldifFile := File name: ldifPath.
    ldifGen printRecords: ldifFile writeStream.

Again, just a few comments on how to make lines shorter (and to me, more readable too). Besides using %, you can use << instead of #display:.

For example,

aStream display: ('dn: uid=%1,%2' bindWith: (person getValue: 'uid') with: base); nl.

Haven't tested it, but I think the line above should turn into the one below:-

aStream << ('dn: uid=%1,%2' % {person getValue: 'uid'. base}); nl.

User login