Main Help → Writing Extensions → Writing GENerel Scripts
The three tasks required before writing Internal Scripts. First, you need to know how to use GENerel, which is GEDitCOM II's built-in scripting language. Second, GEDitCOM II allows scripts to access all your genealogy data, but some access is done through the GEDCOM format. Thus, for some tasks, you need to be familiar with GEDCOM data structures. GEDCOM documentation can be found in the GEDitCOM II help or on line. Third, Internal Scripts work with commands and objects and you will often need to refer to available scripting commands and the dictionary of internal scripting objects.
Internal Scripts are preferred over AppleScripts in Python scripts for at least four reasons:
Internal Scripting was introduced in version 3.0 of GEDitCOM II. Many prior extensions have been rewritten (now known as "John's" Version) as well is many prior scripts embedded in browser styles. Whenever possible, more will be converted and future extensions will use Internal Scripting.
The help on this page covers the following topics for writing Internal Scripts:
When learning a new programming tool, the first step is often a trivial program to see something happen. Here is a trivial Internal Script for GEDitCOM II:
! look for open document @gdoc = "gcapp.frontDocument" ifndef gdoc exit "No documents are open." endif ! look for selected records @recs = "gdoc.selectedRecords" if @recs.count=0 exit "No record is selected." endif ! get first one and write it's name write "The selected record's name is "&@recs.0.name
To try this script do the following:
.gcscpt (note: if you want it to appear in the GEDitCOM II scripts menu, you should save it in ~/Library/Application Support/GEDitCOM II/Scripts or any folder in that folder (here ~/Library means the library folder in the home folder).When starting Internal Scripts, the object variable gcapp is automically defined and corresponds to the application class (see the next section to learn about variables and objects in Internal Scripts). The script uses the application object's "frontDocument" attribute to look up the front document and sets gdoc to that document object. If no document is opened, the script exits with a message to the scriting panel. If a document is open, the script fetches a list of selected records and stores the list in recs. If the list is empty, the script exits with a message to the scripting panel. If records are selected, the script writes the name of the first selected record and then ends. The name is found with an "@" expression (@recs.0.nameM). Note that "@" expressions in Internal Scripts change from getting GEDCOM data for interface customization to a method to reading properties of objects.
Note that assigning objects as in the first command above is a convenience method available since 3.5/1.0. The method used in earlier versions (and still supported) was the get command. Two commands with identical results are:
@gdoc = "gcapp.frontDocument" gcapp.get gdoc,"frontDocument"
This documentation will usually use the assignment method because it is easier to understand and more similar to other languages (although Python sometimes needs get() to assign objects).
To start developing this (or any other script), go back to the GEDitCOM Editor and add Internal Scripting code. Although the above steps work for writing scripts, it is much better to package scripts into a GEDitCOM II "Plug In" or "Extension". Finally, when developing Internal Scripts, it can be helpful to see example scripts. Open any of the GEDitCOM II "Plug In" or "Extension" files in the GEDitCOM Editor and expand the "Scripts" section. Any file listed in that section with extension .gcscpt is an Internal Script.
An advantage of Internal Scripts is that that they use the same GENerel language that is used to customize the user interface, but these two tasks have different needs and therefore have these differences:
#appName$ - it will be "GEDitCOM II" or "GEDitCOM" depending on which application the current user is running.#macOS$ - returns MacOS version as 1040 for Tiger, 1050 for Leopard, and 1060 for Snow Leopard or newer. Occasionally you need to know the MacOS to format the display correctly.#returnValue$ - the most recent result returned by a return command in a subroutine or "????" if nothing was returned.
#today$ - today's date formatted using the user-settings for dates.All Internal Scripts define gcapp as the object name for the GEDitCOM II Genealogy Application object (and the script should not change gcapp to something else). You can use gcapp to access currently opened documents using;
@docs = "gcapp.documents" if @docs.count>0 @doc = "docs.0" ! doc is opened front document (or pick another number) endif |
@doc = "gcapp.frontDocument" ifDef doc ! doc is opened front document endif |
The first block gets an ordered GC List of opened documents. If the list has any members, the first one (at index 0) is the front document. The second block uses a "to-single object" attribute of a Genealogy Application object to get just the front document. When a "to-single-object" attribute of any object finds nothing, it returns a special None object. The ifDef conditional will be true if a document was found or false if a "None" object was returned.
Almost all scripts work on one or more documents and some might need scripting features that only became available in certain versions of GEDitCOM II (or GEDitCOM if writing scripts that work in both). The best way to start such scripts is:
PreChecks "my script name",3.5,1,1,1 @doc = "gcapp.frontDocument"
The first line verifies the user is running GEDitCOM II, version 3.5, build 1 (2nd and 3rd parameters) or GEDitCOM, version 1.0, build 1 (4th and 5th parameters) and that at least one document is open. For more details, see the PreChecks command. If all checks are OK, the script continues and it is guaranteed to find an open document in the second line.
You can create a new document using:
CreateDocument @doc = "gcapp.frontDocument"
The second line sets doc to object name for the new document.
GEDitCOM II documents have many object relationships and properties as defined here.
You can read data from any GEDCOM record by getting the record's object and then using any attribute or property of GEDCOM records (or subclasses of GEDCOM records). You can get records from a list by number, name, or id as follows:
@records = "gdoc.gedcomRecords" write @records.6.id&return @rec = "records.@I34@" write @rec.name&return
the first line gets a GC List of records in the document. The second line uses record number, which is the order of the records in the file (numbering starts with 0). The third line gets a GEDCOM record by its GEDCOM record id.
IF you are focusing on one class of records (such as just individual or family records), it is faster to query just those records:
@indis = "gdoc.individuals" write @indis.5.name&return @fams = "gdoc.families" @fam = "fams.@F27@" write @fam.marriageDate&return
See the internal scripting objects for details on attributes and properties of GEDCOM records and all the subclasses of GEDCOM records.
You create records by adding them to a document with the addRecord command:
doc.addRecord rec,"INDI","male" @rec.name = "John Doe" @rec.birthDate = "12/12/1954"
The type of record requested is added to the document and stored in the object name used in the command (here it is rec). Once the record is added, you can set any available properties (see the internal scripting objects for details on properties of GEDCOM records and all the subclasses of GEDCOM records).
You can create custom record types by using a custom type in the second argument to the command.
The dropOn command moves any record to another record and it will mimic the drag and drop features of GEDitCOM II. In other words, the dropOn command is the same as dropping a record on the record browser of another record. Some examples are:
@indis = "gdoc.individuals" @fams = "gdoc.families" indis.@I7@.dropOn fams.@F27@ @sours = "gdoc.sources" sours.2.dropOn indis.@I41@ @objes = "gdoc.multimedia" #name1="Smith, John, Jr." #name2="John Smith Portrait" objes.#name2.dropOn indis.#name1
See the editing help in GEDitCOM II for each type of record to know which moves (analogous to drops) are supported. Some common moves are
If objName is a GEDCOM Record (or any subclass of GEDCOM Record), you can delete it with:
objName.delete
The record will be deleted from its document and removed from any GC list that contains it. It will not however, be removed from any lists created by the script. To delete and remove from any list, you can use instead:
mylist.2.delete
This approach fetches the record from a list and because it knows the list, it will remove from that list as well.
The AppleScripting feature to locate "every" record with some properties with simple coding, is not implemented for Internal Scripting. The same task, however, can be done by looping over records, such as:
@allindis = "gdoc.individuals"
SDNRange "#b","FROM 1920 TO 1929"
CreateList indis
RepeatWith indi from allindis.count
if @indi.birthSDN>=#b[1] && @indi.birthSDNMax<=#b[2]
indis.addObject indi
endif
EndRepeat
The list in indis will have all individuals born in the provided date range. This coding is more complex, but overall speed is still faster than an AppleScript.
The following options can read or change data in GEDCOM records:
The different types of GEDCOM records as well as GEDCOM structures have properties that can be "read" and "set." Properties are read using "@" expressions:
@doc = "gcapp.frontDocument" ! read birth date of one individual @indis = "doc.individuals" #bd = @indis.1.birthDate
Any property listed in the internal scripting objects can be read. Properties not specified as read only ("r/o") can also be changed with a setting expression or a set command. For example:
! change the name @indis.3.name = "Henry James"
Although only some properties can be set using the above method, all other data can be edited using methods in the following sections.
GEDCOM records are organized into GEDCOM structures . You can read any structure that is not hard-wired to a property by using the evaluateExpression command such as
indi.6.evaluateExpression "#cd","CENS.DATE"
This command will read the date for the first census event in the individual 6 by using a GEDCOM expression (without the "@" sign) and set the result to variable #cd. For more details on this command, see the evaluateExpression command.
You can directly access GEDCOM structures by number or name (where name refers to the GEDCOM tag word for the structure) and then work with the properties of that structure. For example, to access and change the NCHI structure (or number of children, if it exists) of a family record, you can:
@structs = "fams.@F23@.structures" @structs.NCHI.contents = 3
The second line means you can set the contents of any structure, where "contents" is the line value for the first line of the structure. You can also change the GEDCOM tag (using the name property), but you cannot change the level number. If there is more than one structure with the same name, accessing it by name method will return the first one. The best way to access multiple structures, or to access structures without using name or number, is to use the findStructures command.
GEDCOM structures themselves are organized into subordinate structures. You can access any substructure by number or name within the parent's structures. For example, the divorce date of an family might be set using
@structs = "fams.23.structures" @dsub = "structs.DIV.strutures" @dsub.DATE.contents = "12 JUL 1845"
The above method assumes the structure you want to access already exist. If they do not exist, you may want to create and add them.
You can add new structures to GEDCOM records or subordinate to other GEDCOM structures using the addStructure command:
indi.addStructure struct,"BIRT","",2 struct.addStructure bd,"DATE","4 Jul 1776"
The first addStructure adds a BIRT line to an individual with object name indi. The second one adds a DATE line subordinate to the BIRT line.
You can delete structures using the delete command:
@structs = "indi.structures" structs.2.delete
You can access raw GEDCOM data using the gedcom or gedcomArray property of GEDCOM records and GEDCOM structures. The first gives a string with GEDCOM data while the second gives a list with each element in the list having details about each line.
You can read data for any GEDCOM object, but you can only set gedcom property for GEDCOM records. Because setting GEDCOM data could corrupt the data in your file, such as to break existing linkages between records, tt should only be used by those very familiar with the GEDCOM format and usually only for special applications. To change GEDCOM data for the individual with object name indi:
#recText = @indi.gedcom (... use any commands to change #recText) @indi.gedcom = #recText
The first line of the new GEDCOM data must begin in "0" (to indicate it is a record), but the ID and record type in that first line will be ignored and replaced by values in the original record. In other words, this approach only changes subordinate data within the record.
A user can restrict editing of records in GEDitCOM II using a GEDCOM RESN tag. Such a restriction can prevent editing of the record in GEDitCOM II, but does not block scripts in extensions from changing the data. A robust extension should either check for restrictions before changing any data, or document in extension help that it might change locked records.
Internal scripts can check for restrictions using:
if @rec.restriction!="locked" ! it is "unlocked", "privacy", or "confidential" else ! it is locked so do not change it endif
where rec is the record object. Note that only individual or family records can be locked.
A common need in scripts is to work with dates in your file. The challenge in genealogy is that dates are often incomplete. For example you might know your great-grandfather was born in 1872 but not know the month or day. Similarly a date might be a month and a year (MAR 1842) or even a range of dates (BET 1900 and 1911). These "fuzzy" dates are confusing to many programming languages. The built-in date features of most programming languages expect all dates to be an exact date like "12 JUN 1949" and therefore do not know how to handle genealogy dates. Fortunately, GEDitCOM II scripting provides tools for working with "fuzzy" dates.
The basic task for all date calculations is to convert the date to a serial day number (SDN) which assigns an increasing number to all days with day 1 being a date around 4000 BC. When the date is a "fuzzy" date, the single SDN is represented by a range in day numbers spanning the earliest possible day represented by that date to the last possible day. For example the "fuzzy" date 1872 would correspond to the SDN range {2404794, 2405158} where 240474 is the SDN of 1 JAN 1872 and 2405158 is the SDN for 31 DEC 1872.
In date scripts, the first task is fetch the SDNs or SDN ranges for dates in your file. Many of the most common dates are available as properties of records. Some examples are:
#bdmin = @indi.birth #bdmax = @indi.birthSDNMax CreateList ddrange,@indi.deathSDN,@indi,deathSDNMax CreateList mrang,@fam.marriageSDN,@marriageSDNMax @structs = "indi.structures" #cd = @struct.CENS.eventSDN
Here indi and fam are assumed to be valid object names for an individual or a family record. The SDN property gets the minimum SDN for the given date; the SDN max property gets the maximum SDN for the given date. If the date is not available, the returned SDN is zero.
For dates not connected to a property or for dates just in a text string, you can use the SDNRange function. For example, you can get today's SDN:
SDNRange "#var",#today$
which on 13 May 2022 returns #var[1] = #var[2] = 2459713 (note that the SDN range for an exact date has the same minimum and maximum).
Once you have SDN's for dates, calculations are simple. For example:
#ageAtDeath = (@indi.deathSDN - @indi.birthSDN)/365.25 ! age based on minimum SDNs #minAge = (@indi.deathSDN - @indi.birthSDNMAX)/365.25 ! minimum possible age #maxAge to (@indi.deathSDNmax - @indi.birthSDN)/365.25 ! maximum possible age
To convert back to a date (using the user-preferred date style), you can use the DateText command:
DateText "#var",2455032 ! #var = "19 Jul 2009"
Lastly, for more advanced calculations, the Date Calculator application (which is provided with GEDitCOM II in the "GEDitCOM Extras" folder in your "Applications" folder) can do more date calculations.
You can get a list of albums currently in the document or create a new album with
@albums = "gdoc.albums" @album = "albums.0" or gdoc.addAlbum album,"Name of the Album"
Once you have an album, you can add or remove any GEDCOM Record to or from album using the addRecord and remove commands:
album.addRecord obj1,obj2,... album.remove objName
where objName is a GEDCOM Record. You can also add records to albums or manipulate them by other methods such as the populate, union, intersection, and difference.
You can delete and entire album with album.delete. This style of delete will delete the album, but will not remove it from the list of albums described above. To have it removed from that list automatically, reference the album to delete from that list, such as with albums.2.delete.
Many lists provided to Internal Scripts (such as a list of individuals) are immutable lists known as GC Lists. You can read from these lists, but cannot change them. For more list options, Internal Scripting can use Lists. You can create a List and add elements with
CreateList myList myList.addObject obj1,obj2,... myList.addString expr1,expr2,... myList.addList otherList,...
Once created, you can manipulate elements in a list using any of the commands supported by List
Warning: Although Internal Scripts work fine with very large lists, creating a list in an Internal Script and then filling it by repeated calls to addObject or addString can get slow as the list grows. This issue appears to be caused by MacOS handling of mutable lists. The scripts will still work. If they get too slow, try to find a different approach to the same problem (future versions of GEDitCOM II will look for faster list methods than standard MacOS list objects).
Internal Scripts can make use of dictionaries to store information in key-value pairs. The key is always a string. The values can be objects, strings, or numbers. You can create a dictionary and add key-value pairs with the following commands:
CreateDictionary myDict myDict.setObjectForKey obj,key myDict.setStringForKey expr,key myDict.setNumberForKey expr,key
Alternative short cuts for setStringForKey command are:
@myDict.key = expr myDict.set expr,key
These short cuts, however, will set that key as a string value even if expr evaluates to a number. The "@" option can either be a dictionary or an object specifier that evaluates to a dictionary (note: these methods cannot change a key reserved by any dictionary property and are only available since 3.5/1.0).
Once created, you can access or remove key value pairs with these two commands:
myDict.valueForKey ("#var" or objName),key
myDict.removeValueForKey key
where "#var" should be used to access strings or numbers and objName should be used to access objects. If a key's value is a string or a number, you can it read using an "@" expression or include within any expression:
#value=@myDict.key #explain="The value for key is "&@myDict.key
You can also access lists of all values, all keys, and keys sorted by various methods from the to-many attributes of the dictionary. A script can loop through these lists of keys to read values in the dictionary. For example, if a dictionary has a mix of strings, numbers, and objects, you can access them without needing to know their type as in the following example, which iterates through all keys:
@keys = "myDict.allKeys"
RepeatWith "#key" from keys
myDict.valueForKey "#var",#key
ifDef #var
write "Key ""ekey"e&" has a string or number"&return
else
myDict.valueForKey objVar,#key
write "Key ""ekey"e&" has an object"&return
endif
EndRepeat
GEDitCOM II has numerous built-in reports accessible through the reports menu. But you are not limited to those reports. By writing a custom script, you can define new reports and display them in a record browser the same way built-in reports are displayed.
Internal Scripts provide a special ScriptOutput object that simplifies the process of creating reports. It supports three report styles:
The following is an outline of an Internal Script to create an html report:
@gdoc = "gcapp.frontDocument" CreateScriptOutput rpt,"My Custom Report","html" rpt.out "<p>This is a report about document "&@gdoc.name&"</p>"&return ! insert more html content usingrpt.outcommandsrpt.head(text) ! add text to a <head> elementrpt.css(text) ! and CSS items to the reportrpt.writegdoc,"<h1>Introduction</h1>"&return
The first line assigns object name gdoc to the front document. The second line creates the ScriptOutput object that manages the report. The bulk of the script adds html content to the report using the rpt.out command. You need to know enough about using html to work with such reports, but you can use any valid html commands. The rpt.head and rpt.css can add custom options for the html content. Note that rpt.out does not parse for special characters such as "\n" in a string; it will just include those characters. You can use "return" in string expressions (as done above) to insert new lines in the report.
When the report is the done the final rpt.write compiles all report elements and opens it in a report window. The optional second argument to rpt.write inserts any text at the start of the report. Sometimes a report needs to include information at the start of the report that is not known before the report is compiled. This "preface" argument option solves that issue for such reports. See help on the rpt.write for a option to export the report to a file.
One nice feature of html content is providing links to other web sites. An additional feature in GEDitCOM II is that your report can provide links to open any record in the file. Links to records can use two different styles:
<a href='geditcom:@I128@'>Smith, John</a> <a href='//@I128@'>Smith, John</a>
These links are standard html "<a href=" link, but the reference must either start with "geditcom:", which stands for "GEDitCOM scheme," or start with two slashes "//". Although both work, the "geditcom:" scheme is recommended because it is the preferred method for custom URL links (note that version 3.5/1.0 is required for "geditcom:" scheme to work for any MacOS and for two slashes to work in MacOS 27; GEDitCOM II version 3.4 or older requires two slashes but that method stops working in MacOS 27 until upgrading 3.5/1.0 or newer). The rest of link is any record ID (which can be read with @rec.id where rec is a record's object name). The text inside the link is the clickable text that will appear in the report. Besides links to records or outside web sites, you can add internal links within the report by standard html methods. Such links are helpful in long reports.
By default, html reports use the default cascading style sheet (CSS) used for built-in GEDitCOM II reports. If needed, you can change them or create new cascading style sheet options using the rpt.css command. They can be added at any time and the rpt.write command handles inserting them in the proper place when the final report is created. Similarly, you can add html <head> elements, such as entities, by using the rpt.head command.
Some of the extensions provided with GEDitCOM II create custom reports using ScriptOutput. You can refer to documentation and scripts in those extensions for more examples of creating such reports. Although ScriptOutput objects have many properties, none of them affect html style reports.
Finally, documented MacOS methods for printing html content do not seem to work correctly. Html reports will always display well, but you may need some special metnods to print them (as documented in the GEDitCOM II help files).
The following is an outline of an Internal Script to create a plain text report (you can try it out as explained above):
@gdoc = "gcapp.frontDocument" CreateScriptOutput rpt,"My Custom Report","plain" rpt.out "A line of text for the report."&return ! insert more textrpt.outcommandsrpt.moveto0,25 rpt.out "Text starting in column 25"rpt.writegdoc
The first line assigns object name gdoc to the front document. The second line creates the ScriptOutput object that manages the report. The bulk of the script adds text to the report using the rpt.out command. To help aligning text (if using a monospaced font), you can use the rpt.moveto to move to a column on the the line. The next text added to the report will start at that column.
You can customize the report with ScriptOutput properties. The only properties used for plain text reports are monoFont, monoSize, printSize, and monoCol. Despite the property names, you do not need to use a monospaced font (unless you want to align text in columns).
When the report is done, the final rpt.write will output the results, which can be to a report window or exported to a file (see rpt.write for details).
A monospace report is a plain text report with extra features for lining up text in columns and outputing pages with headers and footers. The following is an outline of an Internal Script to create an monospace report :
@gdoc = "gcapp.frontDocument" CreateScriptOutput rpt,"My Custom Report","monospace" @rpt.headerText = "My Report" @rpt.footerText = "Page [#]" rpt.moveto 5,25 rpt.out "Report Title" rpt.newline 2 rpt.out "Name",30,"Birth Date",45,"Death Date",60 @recs = "gdoc.selectedRecordsINDI" RepeatWith rec from recs rpt.out @rec.name,30 rpt.out @rec.birthDateUser,45,@rec.deathDateUser EndRepeat rpt.write gdoc
The first line assigns object name gdoc to the front document. The second line creates the ScriptOutput object that manages the report. The rest of the commands define page style and add text content.
When the report is the done, the final rpt.write will output the results, which can be to a report window or exported to a file (see rpt.write for details).
Monospaced reports uses a page buffering method and is designed to use a monospaced font. The font for the report is set using ScriptOutput properties monoFont and monoSize. The number of rows and columns for a page are set using rowsPerPage and colsPerRow properties.
Each page can have a one-line header and/or footer. These are configured using ScriptOutput properties headerText, headerAlign, footerText, and footerAlign. Either one can display a page number, which is set with the monoPage property. You can control margins and gap between header and footer and page text using topMargin, bottomMargin, and textGap properties.
To have page style apply to the first page, set all needed properties before adding any text to the report. The properties can also be changed after adding text to the report but any changes will not take effect until the next page begins.
Text is added to a report using the rpt.out (see that command for more details). The two options are to use the rpt.moveto command to move to a position on the page and then use rpt.out to insert some text, or to use a multi-argument rpt.out command to output text in fields defined by column numbers. The text can he left justified, centered, or right-justified in each field.
The report automatically starts a new line when current column reaches the report's colsPerRow setting or an rpt.out command requests a column before the current column position. You cannot insert return characters into the report (they will be changed to spaces if you do). If you need to move to the next line, use the newline command.
Text added to a monospace report is adding to a page buffer. You can move around that buffer with the rpt.moveto. When you insert text it will be at the current location even if that means the new text will overwrite previously inserted text. Page breaks will automatically occur when the current row exceeds rowsPerPage. To induce a page break at other times use the rpt.newline command with the number new lines greater than the rowsPerPage property.
You can read the current row and column position with ScriptOutput read-only monoRow and monoCol properties.
Two reasons for using monospace reports instead of html reports is to easily align things without learning all html options and because the reports can print with better pagination (printing html reports normally requires extra steps and random page breaks). Getting printed reports with accurate page breaks and aligned columns requires three steps:
The last step above might need some trial and error reports to get exact values. The following table gives some results for rowsPerPage X columnsPerRow for two different fonts and either US Letter or A4 paper. These values assume printing using the default GEDitCOM II margins or 72 pixels on each edge of the page.
| Font and Size | US Letter | A4 | Font and Size | US Letter | A4 |
|---|---|---|---|---|---|
| Courier 9 | 60 X 88 | 65 X 84 | Monaco 9 | 56 X 88 | 61 X 84 |
| Courier 10 | 55 X 79 | 59 X 76 | Monaco 10 | 48 X 79 | 51 X 76 |
| Courier 11 | 51 X 71 | 55 X 69 | Monaco 11 | 44 X 71 | 48 X 69 |
| Courier 12 | 47 X 65 | 51 X 63 | Monaco 12 | 41 X 65 | 44 X 63 |
It is possible these values will need to changes in different MacOS software or with different printers. A ScriptOutput object defaults to "Courier,", display size 12, print size 9, and 60 X 88 rows and columns for US Letter paper
All extensions running in GEDitCOM II open the scripting panel while any scripts in those extensions are running. By default, this scripting panel disappears when the script is done, but you can also use that panel to provide information to the user or even to provided the primary output of the script. Here are the rules for the scripting panel: