Improving Your AppleScripts: Structured Programming

Apple provided a great programming language in Applescript, but even good things can go horribly wrong when applied incorrectly. To paraphrase the gun control slogan, “Applescript doesn’t kill programs. Bad code kills programs.” But most folks drop into Applescript programming by accident, because we want to “tweak” a program or create a workflow that simplifies our life. The next thing you know, we’re hooked and write scripts for all sorts of things, even things we could (probably) do easier ourselves. (OK, maybe that’s just me…)

Programming, despite what you may hear, is an art. Within that art, though, like any other artistic endeavor, there are techniques that have become “standard” over time. Just as Salvador Dali practiced many different styles of painting before he set off to create his surrealist masterpieces, it behooves anyone who is serious about writing good code to learn some of the standard techniques. Generally, these techniques have been called “structured” programming, which simply means that there is a method to the programmer’s madness.

Luckily, Applescript itself makes much of structured programming easy. Since it is basically an object-oriented language, much of the structure is already there for you. But you, as the programmer, can still do much to make things easier to understand, cleaner, and sometimes more elegant or shorter.

One of the standard techniques is called “pseudocode.” Basically, the word mean “false code.” But it’s a powerful tool, and will help you not only write better code faster, but you’ll see how it will also help you to comment your code.

Let’s say you want to take a list of file names in the Finder and convert each one to mixed case (first letter capitalized, the rest lower case). For our example, we want to do these things, in order:


--get a list of file names from the Finder
--loop through the file names
--get the displayed name of a file
--create a list to hold the new words of the name
--loop through the words in each name
--if the first letter is not capitalized, make it upper case
--make the rest of the letters lower case
--add the new word to the name list
--change the file's name to the new name

Notice that I made each line a comment. Let’s take the first line:


--get a list of file names from the Finder
tell application "Finder" to set myFiles to the selection

That was fairly easy. Let’s try the next line:


--loop through the file names
repeat with aFile in myFiles

OK, let’s do some more:


--get the displayed name of a file
tell application "Finder" to set myName to displayed name of aFile
--create a list to hold the new words of the name
set newName to {}
--loop through the words in each name
repeat with myWord in (words of myName)
	--if the first letter is not capitalized, make it upper case
	set firstLetter to character 1 of myWord
	if firstLetter is not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" then set firstLetter to makeUPPER(firstLetter)
	--make the rest of the letters lower case
	if length of myWord > 1 then
		set lowerletters to text 2 thru (length of myWord) of myWord
		set lowerletters to makelower(lowerletters)
	else
		set lowerletters to ""
	end if
	-- there's more to come --
end repeat

“Now wait a minute,” I can hear you say, “What is makeUPPER? Where did makelower come from?” The answer is, they don’t exist YET. But they will later, so we can pretend they already exist. It should seem obvious that makeUPPER will take a letter and return the upper case version of the letter. By the same token, the makelower handler will take letters and make them lower case. Let’s finish out our main routine:


repeat -- carried forward from script above
	
	--add the new word to the name list
	set end of newName to firstLetter & lowerletters
end repeat
set myName to newName as text
--change the file's name to the new name
tell application "Finder" to set name of aFile to (myName as Unicode text)

That’s our main routine. Now let’s flesh it out a bit, and make sure it does what we want it to. Pseudocode doesn’t take into account things that are peculiar to a particular programming language. For example, setting Applescript’s text item delimiters isn’t something we’d think of in pseudocode. A “Considering” block isn’t something you’d put in pseudocode either, but necessary when dealing with mixed case letters.

So by “coloring in” the sketch we made, we get this:


--get a list of file names from the Finder
tell application "Finder" to set myFiles to the selection
--loop through the file names
repeat with aFile in myFiles
	--get the displayed name of the file
	tell application "Finder" to set myName to displayed name of aFile
	--create a list to hold the new words of the name
	set newName to {}
	--loop through the words in each name
	repeat with myWord in (words of myName)
		--if the first letter is not capitalized, make it upper case
		set firstLetter to character 1 of myWord
		considering case
			if firstLetter is not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" then set firstLetter to makeUPPER(firstLetter)
			--make the rest of the letters lower case
			if length of myWord > 1 then
				set lowerletters to text 2 thru (length of myWord) of myWord
				set lowerletters to makelower(lowerletters)
			else
				set lowerletters to ""
			end if
		end considering
		--add the new word to the name list
		set end of newName to firstLetter & lowerletters
	end repeat
	set AppleScript's text item delimiters to {space}
	set myName to newName as text
	set AppleScript's text item delimiters to {}
	--change the file's name to the new name
	tell application "Finder" to set name of aFile to (myName as Unicode text)
end repeat

That was fairly painless, wasn’t it? All we did was think through the problem logically, write out a quick thumbnail sketch of the code, then code it from the sketch. And the added benefit is that we wrote well-documented code that anyone else (or ourselves sometime later) can understand!

Now, of course, we need to see two the two handlers, makeUPPER and makelower. Let’s take the first one and do it the same way we did the main routine:

--see if the letter is in list of lower case letters
--if so, then return the upper case version
--else return the original character (it might be a number!)

That translates in Applescript to:


on makeUPPER(aLetter)
	--see if the letter is in list of lower case letters
	considering case
		set myChar to offset of aLetter in "abcdefghijklmnopqrstuvwxyz"
		--if so, then return the upper case version
		if myChar > 0 then
			return character myChar of "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
		else
			--else return the original character (it might be a number!)
			return aLetter
		end if
	end considering
end makeUPPER

We’d like to think that we can just “flip” the makeUPPER handler to make the makelower handler, but there’s a glitch. We have to handle multiple letters in makelower. The pseudocode looks like this:


--loop through the letters
--convert them to lower case
--return the new text

which leads us to this new handler:


on makelower(theText)
	set newText to ""
	--loop through the letters
	repeat with loop from 1 to (length of theText)
		--convert them to lower case
		set newText to newText & lower(character loop of theText)
	end repeat
	--return the new text
	return newText
end makelower

“You did it again,” you’re saying. “You have a handler lower(myletter) in there that isn’t written yet.” You’re right! But heck, we’re getting the hang of this by now. Let’s just reuse the makeUPPER script but flip the upper and lower case strings. Since makeUPPER does almost the same thing, we won’t even pseudocode this one:


on lower(aLetter)
	--see if the letter is in list of upper case letters
	considering case
		set myChar to offset of aLetter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
		--if so, then return the lower case version
		if myChar > 0 then
			return character myChar of "abcdefghijklmnopqrstuvwxyz"
		else
			--else return the original character (it might be a number!)
			return aLetter
		end if
	end considering
end lower

See how easy that was? Now, don’t think that it is always this easy. There will still be lots of debugging and things you didn’t account for in your pseudocode. For example, this script doesn’t account for punctuation characters in your file names, so they will all get stripped out of the file names. But this technique will help you to throw together the basic skeleton of your script and then you can spend most of your time refining it, adding extra features, and generally building better scripts faster!

Our whole script in one block for downloading to your Script Editor:


--get a list of file names from the Finder
tell application "Finder" to set myFiles to the selection
--loop through the file names
repeat with aFile in myFiles
	--get the displayed name of the file
	tell application "Finder" to set myName to displayed name of aFile
	--create a list to hold the new words of the name
	set newName to {}
	--loop through the words in each name
	repeat with myWord in (words of myName)
		--if the first letter is not capitalized, make it upper case
		set firstLetter to character 1 of myWord
		considering case
			if firstLetter is not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" then set firstLetter to makeUPPER(firstLetter)
			--make the rest of the letters lower case
			if length of myWord > 1 then
				set lowerletters to text 2 thru (length of myWord) of myWord
				set lowerletters to makelower(lowerletters)
			else
				set lowerletters to ""
			end if
		end considering
		--add the new word to the name list
		set end of newName to firstLetter & lowerletters
	end repeat
	set AppleScript's text item delimiters to {space}
	set myName to newName as text
	set AppleScript's text item delimiters to {}
	--change the file's name to the new name
	tell application "Finder" to set name of aFile to (myName as Unicode text)
end repeat

on makeUPPER(aLetter)
	--see if the letter is in list of lower case letters
	considering case
		set myChar to offset of aLetter in "abcdefghijklmnopqrstuvwxyz"
		--if so, then return the upper case version
		if myChar > 0 then
			return character myChar of "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
		else
			--else return the original character (it might be a number!)
			return aLetter
		end if
	end considering
end makeUPPER

on makelower(theText)
	set newText to ""
	--loop through the letters
	repeat with loop from 1 to (length of theText)
		--convert them to lower case
		set newText to newText & lower(character loop of theText)
	end repeat
	--return the new text
	return newText
end makelower

on lower(aLetter)
	--see if the letter is in list of upper case letters
	considering case
		set myChar to offset of aLetter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
		--if so, then return the lower case version
		if myChar > 0 then
			return character myChar of "abcdefghijklmnopqrstuvwxyz"
		else
			--else return the original character (it might be a number!)
			return aLetter
		end if
	end considering
end lower

Hello,

Well it seems that the script you posted here just ignores characters other than letters – it deletes dots, parenthesis and whatever other character gets in its way to capitalize the words and replaces them with a space !

I guess it needs a little tweaking :stuck_out_tongue: