We’re ready to start writing our actual program! Because we’re exciting and creative folk, we’ll call our single procedure main
.
We’ll go ahead and use a let
statement to grab all but the first element of the program-arguments
list and stick it in the args
variable for brevity (the first element is the name of the executable file). This use of let
isn’t really required in such a simple program, but I find that it makes things easier to read, and if I expand the program later, it’s easier to modify.
(define (main)
(let ((args (cdr (program-arguments))))
We can’t just assume that the args
list is going to have anything in it, however, so we’ll print a short message and exit the program if it’s empty. If it’s not empty, we travel on to the `else’ clause of the if
expression.
(if (null? args)
(begin (display "No arguments, exiting...")
(newline)
(exit))
Now that we’ve invoked the interpreter with the right incantations, loaded our required module, and checked the program arguments list to make sure that we have something there to process, we can write the part of the program that actually does something. Sweet!
In the `else’ clause of the if
expression, we iterate over args
using the for-each
procedure. We use for-each
in this case (rather than our beloved map
) because we don’t want to build a new list by transforming each element of args
, we just want to iterate over our list being all “side-effect-y” (a technical term that in this case means “affecting the state of stuff on disk”).
The best way to read Lisp code is usually “inside-out”. Begin with the innermost element, figure out what argument(s) it takes, and see what it passes along as a return value. That return value is then an input for something else. This is true in most computer languages, but in Lisp it becomes especially necessary to read things this way.
Therefore we’ll start inside the innermost expression, at regexp-substitute/global
. The documentation says that it needs a port, a regular expression, and a string to match that regular expression against. Since regexp-substitute/global
isn’t writing its output to a port, but passing its arguments out to string-downcase
, we specify “no port” as #f
. Post
has to do with making regexp-substitute/global
recur on any unmatched parts of the string in arg
, and the literal -
is what we’d like to replace our matches with. For more comprehensive information on pre
and post
, I actually needed to consult the documentation on regexp-substitute
, since regexp-substitute/global
is apparently a special case of the former (and is perhaps implemented using regexp-substitute
? I didn’t check, but it would be easy enough to do so).
Let’s look at that regex, [,'!_ \t]+
. In English, it means “match any commas, apostrophes, exclamation points, underscores, blank spaces or tabs”. As noted above, we want to replace any occurrences of these characters with -
.
For example, a string like Hey Kids I Have Spaces.txt
would become Hey-Kids-I-Have-Spaces.txt
. We then pass it out to the string-downcase
procedure, which transforms it into hey-kids-i-have-spaces.txt
.
That value is then passed as the second argument to the rename-file
procedure, which renames arg
(our original, uncool filename) to hey-kids-i-have-spaces.txt
.
It’s all wrapped in a lambda
expression, which does the job of creating and invoking a one-argument procedure out of the several we’ve discussed; this procedure is then applied to every item in our argument list args
.
(for-each (lambda (arg)
(rename-file arg
(string-downcase
(regexp-substitute/global
#f "[,'!_ \t]+" arg
'pre "-" 'post))))
args))))