Hack 89 Use Perl from Word


This hack shows you two very different ways of
getting at Perl from within a Word macro.
Perl's hard to beat for
heavy-duty text processing, and if
you've already got some Perl scripts lying around
for performing certain tasks, you may want to use those from a Word
macro rather than starting from scratch in VBAa decidedly
lighter-weight contender when it comes to text processing.
|
This hack demonstrates two ways to get at Perl code from a Word
macro. The first is the more polished method and requires some
special software from ActiveState, which lets you create standalone
Windows .dll (dynamic link library) files. These
files contain libraries of functions used to perform specific tasks.
The second method is about as quick and dirty as they come and does
not require any special software.
9.11.1 Creating COM Objects with the Perl Dev Kit
In addition to the free
ActivePerl distribution, ActiveState sells
software to help Perl developers create Windows applications. The
Perl Dev Kit (PDK) lets you create standalone
Windows executables, Microsoft installation files, and even .NET
applications. You can try it free for 21 days, though any
applications you build will expire at the same time as the evaluation
period. You can, however, renew your trial period for an additional
21 days. The standard license costs $195. You can get the free trial
version from http://www.activestate.com/Products/Perl_Dev_Kit/.
|
One part of the PDK is PerlCTRL, which builds
Windows .dll files from a Perl script. It
involves a bit of setup work, but once you create the
.dll, using it as a COM object [Hack #84]
from VBA is a breeze.
This example is similar to [Hack #88] ; it
shows you how to build a standalone interface to
Perl's split function. A detailed
explanation of PerlCTRL is beyond the scope of
this hack, which demonstrates only a simple example.
|
The following is the base Perl code used to build the COM object.
It's a simple wrapper around Perl's
built-in split function:
package PerlSample;
sub Split {
my $pattern = shift;
my $string = shift;
my @list = split(/$pattern/, $string);
return \@list;
}
There are three main steps to turning this Perl code into a COM
object:
Create a template file using the PDK. The
template file contains boilerplate code and examples of the
information that PerlCTRL needs to generate the
.dll.
Modify the template file.
Generate the .dll from the template.
First, create a new folder on your system and name it
C:\PerlCOMSample\. Open a DOS command prompt and
navigate to the folder you created. At the DOS prompt, type the
following:
> PerlCtrl -t > template.pl
Now open the template.pl file in a text editor,
such as Notepad. The file will look like the one shown in Figure 9-13.
Figure 9-13. The template file generated by PerlCTRL

The template file also includes three unique identifiers that Windows
will use to keep track of your .dll.
PerlCtrl creates these identifiers when you
generate the template file. As the comments in the template file
indicate, do not edit those lines. Otherwise,
modify the template file as follows:
package PerlSample;
sub Split {
my $pattern = shift;
my $string = shift;
my @list = split(/$pattern/, $string);
return \@list;
}
=pod
=begin PerlCtrl
%TypeLib = (
PackageName => 'PerlSample',
TypeLibGUID => '{26798342-6F54-4271-9668-B4C0D31EB5C8}', # do NOT edit this line
ControlGUID => '{BD48D84F-C5C9-4E3B-8E36-24E019E4F48D}', # do NOT edit this line
DispInterfaceIID=> '{FF546B71-4492-4E07-BD44-1EDE507CB5A4}', # or this one
ControlName => 'PerlSample',
ControlVer => 1, # increment if new object with same ProgID
# create new GUIDs as well
ProgID => 'PerlSample.Split',
DefaultMethod => '',
Methods => {
'Split' => {
RetType => VT_ARRAY|VT_VARIANT,
TotalParams => 2,
NumOptionalParams => 0,
ParamList =>[ 'pattern' => VT_BSTR,
'string' => VT_BSTR ]
},
}, # end of 'Methods'
Properties => {
}, # end of 'Properties'
); # end of %TypeLib
=end PerlCtrl
=cut
Again, use the three lines generated in your template file,
not the ones shown in bold in this example.
Next, save this file as PerlCOMObject.ctrl in
the same directory, and then run the following command at a DOS
prompt:
> PerlCtrl PerlCOMObject.ctrl
You'll see the following output:
Created 'PerlCOMObject.dll'
Now you need to register the new .dll with
Windows. At the DOS prompt, enter the following:
> regsvr32 PerlCOMObject.dll
In a few seconds, you'll see the dialog shown in
Figure 9-14, indicating that the
.dll file was successfully registered.
Figure 9-14. Windows notifies you when your .dll file is registered successfully

Now you can call the .dll as a COM object from
within a Word macro. Open Word and create the following macro in the
template of your choice [Hack #50] :
Sub TestPerlObject( )
Dim pl As Object
Set pl = CreateObject("PerlSample.Split")
Dim str As String
Dim var( ) As Variant
Dim v As Variant
str = "Hello from Perl!"
var = pl.Split(" ", str)
For Each v In var
MsgBox v
Next v
End Sub
When you run the macro, you'll see three dialog
boxes displayed in sequence, each showing one of the words in the
string "Hello from Perl!"
If you'd like to remove the
.dll from your system, enter the following at a
DOS command prompt:
> regsvr32 /u PerlCOMObject.dll
9.11.2 Call Perl Directly with the VBA Shell Function
VBA includes a function
you can use to launch other Windows
applications. At its simplest, the Shell function
is roughly equivalent to entering a command at a DOS prompt. For
example, enter the following in the Visual Basic
Editor's Immediate window [Hack #2]
to launch the Notepad text editor:
Shell("notepad.exe")
Because Perl is an executable file, you can use
Shell to run Perl scripts. For example, if you had
a Perl script called C:\foo.pl, you could enter
the following in the Immediate window to run the script:
Shell("C:\perl\bin\wperl.exe C:\foo.pl")
After the Shell function executes and the
executable program starts, the VBA code continues.
|
You can use the clipboard to pass and return values between VBA and
Perl. For example, you can copy selected text to the clipboard and
then call a Perl script that reads the clipboard, processes the text,
and puts the result back on the clipboard to paste into your
document.
However, the VBA macro might try to paste from the clipboard before
the Perl script finishes. Thus, you also need a way to have VBA
"wait" for the Perl script to
finish. One solution is to use a semaphore; that
is, have the macro create a temporary folder on your computer, and
then have the Perl script delete it once it puts the script result on
the clipboard. All you need is a few lines of VBA to check to see if
the folder still exists and, if so, instruct the macro to wait a few
seconds until the Perl script finishes.
|
Since it's likely you'd want to access a variety of Perl scripts from
within a Word macro, it's worthwhile to create a reusable function to
act as a wrapper around the Shell function call to Perl. The
following function takes three arguments: the name of the Perl script
to run, the name of the semaphore folder the Perl script should
delete when it finishes, and finally the maximum time to wait for the
Perl script to run before giving up. The function returns a value of
True if the Perl script deleted the semaphore
folder, or False if the folder still exists when
the time limit is reached. Put this code into the template of your
choice [Hack #50] :
Function RunPerl(sPerlScriptToRun As String, _
sSemFolderName As String, _
sngWaitMax As Single) As Boolean
Dim sPerlPath As String
Dim sFullShellCommand As String
Dim sSemDir As String
Dim sSemDirFullName As String
Dim sngStartTime As Single
' Full path of "Windowless" Perl executable
sPerlPath = "C:\perl\bin\wperl.exe"
' Get the full path from the environment variable
sSemDirFullName = Environ("TEMP") & "\" & sSemFolderName
' Put quotes around full script path.
' This allows for spaces in script path names, common on Windows systems.
sFullShellCommand = sPerlPath & " " & _
Chr(34) & sPerlScriptToRun & Chr(34)
' Create semaphore directory, unless it already exists
If Not LCase(Dir(sSemDirFullName, vbDirectory)) = LCase(sSemFolderName) Then
MkDir (sSemDirFullName)
End If
' Start the countdown to timeout
sngStartTime = Timer
' Run Perl script
Shell (sFullShellCommand)
' The script will stay in this loop until either
' the semaphore directory is deleted, or until the
' time limit set by sngMaxWaitTime has passed
Do While LCase$(Dir$(sSemDirFullName, vbDirectory)) = _
sSemFolderName And _
((Timer - sngStartTime) < sngWaitMax)
' Display a countdown in status bar
StatusBar = "Waiting " & _
Int((sngWaitMax - (Timer - sngStartTime))) & _
" more seconds for Perl ..."
Loop
If LCase$(Dir$(sSemDirFullName, vbDirectory)) = sSemFolderName Then
' Gave up waiting.
RmDir (sSemDirFullName)
StatusBar = "Gave up waiting for Perl"
RunPerl = False
Else
' Perl script successfully deleted semaphore folder
StatusBar = "
RunPerl = True
End If
End Function
To see an example of this function in action, and to borrow Tim
Meadowcroft's example from Computer Science and Perl
Programming (O'Reilly), the following
code will demonstrate how to use Perl to standardize phone numbers
that are in a variety of formats. (Note: This example uses U.K. phone
numbers.)
For starters, the following Perl script called
FixPhoneNumbers.pl pulls the text off the
Windows clipboard, checks it using a series of regular expressions,
then either puts the modified number on the clipboard, or the
original, if it couldn't fix it. The standard ActiveState Windows
Perl distribution includes the Win32::Clipboard
module. Save this script as C:FixPhoneNumbers.pl:
use Win32::Clipboard;
my $TEMP = $ENV{"TMP"};
my $clipcontents = Win32::Clipboard( );
my $cliptext = $clipcontents->Get( );
my $num = PerlFixPhone($cliptext);
if ($num != '') {
$cliptext = $num
}
$clipcontents->Set($cliptext);
rmdir("$TEMP/vba_sem") || die "cannot rmdir $TEMP\\vba_sem: $!";
sub PerlFixPhone {
# Tests:
# 020 xxxx xxxx : fine as is
# xxx xxxx : assume 020 7xxx xxxx
# 2xxx : Building 1 extension, assume 020 7457 2xxx
# 8xxx : Building 2 extension, assume 020 7220 8xxx
# 0171 xxx xxxx : convert to 020 7xxx xxxx
# 0181 xxx xxxx : convert to 020 8xxx xxxx
# Anything else is an error and should be ignored...
#
local $_ = shift;
return $_ if /^020 \d{4} \d{4}$/;
return $_ if s/^\s*(\d{3})[-\s]+(\d{4})\s*$/020 7$1 $2/;
return $_ if s/^\s*(\d{3})[-\s]+(\d{4})[-\s]+(\d{4})\s*$/$1 $2 $3/;
return $_ if s/^\s*(2\d{3})\s*$/020 7457 $1/;
return $_ if s/^\s*(8\d{3})\s*$/020 7220 $1/;
return $_ if s/^\s*0171[-\s]+(\d{3})[-\s]+(\d{4})\s*$/020 7$1 $2/;
return $_ if s/^\s*0181[-\s]+(\d{3})[-\s]+(\d{4})\s*$/020 8$1 $2/;
return '';
}
The following macro uses the RunPerl function
shown above to run the FixPhoneNumbers.pl
script. Put this code in the same template as the
RunPerl function:
Sub UsePerlToFixSelectedPhoneNumber( )
' Pass selected text to a Perl program
' to format/normalize phone numbers
Dim sel As Selection
Set sel = Selection
' Exit if selection doesn't include some text
If sel.Type = wdSelectionIP Then
MsgBox "Please select some text first"
Exit Sub
End If
' Copy selected text to clipboard for Perl
sel.Copy
' Run Perl script. If successful,
' paste in changed text from Perl
If (RunPerl(sPerlScriptToRun:="C:\FixPhoneNumbers.pl", _
sSemFolderName:="vba_sem", _
sngWaitMax:=5)) = True Then
sel.Paste
Else
MsgBox "Gave up waiting for Perl"
End If
End Sub
To see this macro in action, type the following four (U.K.) phone
numbers into a Word document:
0171 123 6554
8000
220-8537
220 8537
Select each in turn and run the
UsePerlToFixSelectedPhoneNumber macro from Tools
the following format, according to the rules laid out in the
FixPhoneNumbers.pl script:
020 7123 6554
020 7220 8000
020 7220 8537
020 7220 8537
If your Perl program takes a long time to run, you may need to adjust
the value passed in the sngWaitMax argument to the
RunPerl function. This example sets it to five
seconds, more than enough time for Perl to finish this little bit of
text crunching.
Sean M. Burke, Andy Bruno, and Andrew
Savikas