Creating F# FSX Script Files from FS Source Files
As you may know over the past few months I have been doing some work on FSharpChart. One of the source code changes that has been implemented over this time is the breakdown of the original single FSX script file into more manageable FS source files. However in doing this I was left with the need to compose the individual FS source files back into a single FSX script file; as folks may still wish to use this single file. Rather than do this composition manually for each release I decided to write a little utility script to achieve this.
In writing this code to generate the composite script file I wanted to do achieve several things:
- Allow one to use a template to manage inclusion of reference and items like fsi.AddPrinter
- Allow for the management of the namespaces and open statements
- Easily allow for the inclusion of a multiple FS source files
For generating the FSharpChart FSX script file the template source I used is as follows:
Template File
#nowarn "40"
#if INTERACTIVE
#r "System.Windows.Forms.DataVisualization.dll"
#endif
namespace MSDN.FSharp.Charting
open System
open System.Collections
open System.Collections.Generic
open System.Drawing
open System.Reflection
open System.Runtime.InteropServices
open System.Windows.Forms
open System.Windows.Forms.DataVisualization.Charting
#file "Charting.ClipboardMetafileHelper.fs"
#file "Charting.ChartStyles.fs"
#file "Charting.ChartData.fs"
#file "Charting.ChartFormUtilities.fs"
#file "Charting.ChartTypes.fs"
#file "Charting.ChartControl.fs"
#file "Charting.FSharpChart.fs"
#file "Charting.ChartExtensions.fs"
#file "Charting.ChartStyleExtensions.fs"
#if INTERACTIVE
module InstallFsiAutoDisplay =
fsi.AddPrinter(fun (ch:GenericChart) ->
let frm = new Form(Visible = true, TopMost = true, Width = 700, Height = 500)
let ctl = new ChartControl(ch, Dock = DockStyle.Fill)
frm.Text <- ChartFormUtilities.ProvideTitle ch
frm.Controls.Add(ctl)
frm.Show()
ctl.Focus() |> ignore
"(Chart)")
#endif
The premise of this template file is that it is used to generate the FSX file where each #file tag is expanded to include the contents of the specified source file; where namespaces and open statements are modified to be consistent with the namespace of the template file.
In this example the template file defines the default namespace as “MSDN.FSharp.Charting”. As such when writing out the contents of a source file the base namespace element needed to be removed and the open statements modified to reflect this change. Thus in this case, with the default namespace being “MSDN.FSharp.Charting”, any open statement with this prefix needs to be transformed to exclude the default value; namely:
open MSDN.FSharp.Charting.ChartTypes
needs to be transformed to:
open ChartTypes
With this in mind here is the transform code:
Template Script
open System.Net
open System.Linq
open System.Collections.Generic
open System.IO
open System.Text.RegularExpressions
// --------------------------------------------------------------------------------------
// Utility for creating a single FSX file
// The code will parse through the template file and replace each #file with the appropriate file contents
let codeDir = __SOURCE_DIRECTORY__ + "\\..\\fsharpchart\\"
let templateName = __SOURCE_DIRECTORY__ + "\\FSharpChart.fstemplate"
let sourceName = __SOURCE_DIRECTORY__ + "\\..\\samples\\FSharpChart.fsx"
let startLine = "// ----------------------------------- "
let commentLine = "// "
let blankLine = ""
let filePattern = "^\#file\s+\"(?<filename>.*)\"$";
let codePattern = "^module|type|\[<.*>\]"
let openPattern = "^open MSDN\.FSharp\.Charting\.(?<openstatement>.*)$";
// Processes the contents of a code file
let isNotCodeStart (line:string) =
if not (Regex.Match(line, codePattern).Success) then true
else false
let getOpenStatements (lines:string list) (line:string) =
let m = Regex.Match(line, openPattern, RegexOptions.IgnoreCase)
if m.Success && m.Groups.Count = 2 then ("open " + m.Groups.["openstatement"].Value) :: lines
else lines
let openFileLines filename =
File.ReadAllLines(codeDir + filename)
|> Seq.takeWhile isNotCodeStart
|> Seq.fold getOpenStatements []
let codeFileLines filename =
File.ReadAllLines(codeDir + filename)
|> Seq.skipWhile isNotCodeStart
let processCode filename =
[ yield startLine
yield commentLine + filename
yield startLine
// yield any neccessary open statements
match openFileLines filename with
| [] -> ()
| _ ->
yield! (openFileLines filename)
yield blankLine
// yield the module/type code lines
yield! (codeFileLines filename)
yield blankLine]
// Define the output of the template file processing any #file elements
let (|MatchFileInclude|_|) (line:string) =
let m = Regex.Match(line, filePattern, RegexOptions.IgnoreCase)
if m.Success && m.Groups.Count = 2 then Some(m.Groups.["filename"].Value) else None
let templateFile =
[ for line in File.ReadAllLines(templateName) do
match line with
| MatchFileInclude filename -> yield! (processCode filename)
| _ -> yield line ]
// open and write the file contents
do
use codeFile = File.CreateText(sourceName)
templateFile
|> Seq.iter codeFile.WriteLine
So what does the script do?
The templateFile list, which is written as the single FSX script file, is generated by one of two lines. Either the template source line is written directly to the output file or a #file tag is matched (MatchFileInclude) and the contents are processed.
In processing a code file firstly the open statements are written, through the openFileLines function, and secondly the remaining code lines are written, using the codeFileLines function. When executing these functions, processing occurs according to a the determination of when all the open statements have all been read and the first code line has been encountered. In this case the determination is made of this point in the source code through the regular expression match of a module, type or attribute statement.
Most of the code inspection is done through regular expressions. A file tag is located using an Active Pattern and the RegEx pattern:
"^\#file\s+\"(?<filename>.*)\"$"
This way one can easily locate a #file tag and extract the source filename to be included. A similar mechanism is used for open statements where the captured expression is the new open statement value.
For the determination of the start of the code the RegEx pattern that is used for locating a module, type or attribute statement is:
"^module|type|\[<.*>\]"
This code is a little specialized, as it does depend on some well structured source code file, but hopefully it does demonstrate that one can easily manage separate source code file, yet if necessary still easily deliver a single script file.