Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
F# script to add the files for a new F# problem to an .fsproj file
I am using Visual Studio to solve the F# problems on, and I found it tedious
to always add the same files (test file, new code file, readme, plus a folder for better
organization) after each `exercism fetch`, so I automated that.
As it is, this assumes that the .fsproj file is in `exercism\fsharp`. When run, it compares
the subdirectories present with the folders in the .fsproj file, and for any that are
missing in the project file, it adds the `*Test(s).fs` and `` files as well as
creating a new .fs file for the problem solution. It also adds a `module` line with the
respective file name (which is correct most of the time; only occasionally, the module the
test file will attempt to open has a different name).
Add this to the F# project, Alt-Enter it after every `exercism fetch`, and you should be
ready to go for solving the next problem without fiddling with the project.
#r "System.Xml.Linq"
open System.IO
open System.Xml.Linq
let asFst second first = first, second
let asSnd first second = first, second
let xname = XName.Get
let elements (element : XElement) = element.Elements()
let elementsNamed (name : string) (element : XElement) =
|> Seq.filter (fun element -> element.Name.LocalName = name)
let directory = __SOURCE_DIRECTORY__
// This may need to be adjusted.
let projectFileName = "ExercismFsharp.fsproj"
let project = Path.Combine [| directory; projectFileName |]
let loadDocument (fileName : string) = (XDocument.Load fileName).Root
let getCodeFilesItemGroup doc =
|> elementsNamed "ItemGroup"
|> Seq.find (fun itemGroup ->
|> elementsNamed "Compile"
|> Seq.tryHead
|> function
| Some node ->
// This assumes that the first 'Compile' node of the ItemGroup containing the .fs
// files will be that for AssemblyInfo.fs. If that isn't already the case, it is
// easily achieved by manually editing the .fsproj file.
node.Attribute(xname "Include").Value = "AssemblyInfo.fs"
| None -> false)
let directoriesToExclude =
// All subdirectories of the project directory that are *not* exercism problem diretories
[ ".paket"; ".vs"; "_NCrunch_ExercismFsharp"; "bin"; "obj"; "packages" ]
let getSubDirectories exclude directory =
let exclude = exclude |> Set.ofList
Directory.GetDirectories directory
|> Path.GetFileName
|> Seq.filter (not << exclude.Contains)
let getProblemDirectories (itemGroup : XElement) =
|> elementsNamed "Compile"
|> Seq.choose (fun element ->
match element.Attribute(xname "Include").Value.Split '\\' with
| [| directory; _ |] -> Some directory
| _ -> None)
|> Seq.distinct
let getNewProblems problemsInProject problemsOnDisk =
Set.ofSeq problemsOnDisk - Set.ofSeq problemsInProject
let createCodeFile problemDirectory fileName =
[ sprintf "module %s" (Path.GetFileNameWithoutExtension fileName); "" ]
|> asSnd (Path.Combine [| directory; problemDirectory; fileName |])
|> File.WriteAllLines
let getCodeFileName problemDirectory =
let testFileName =
// These are what I have come across so far for test file names.
[| "*Test.fs"; "*Tests.fs" |]
|> Array.collect (fun pattern ->
Directory.GetFiles(Path.Combine [| directory; problemDirectory |], pattern))
|> Array.head
|> Path.GetFileName
let codeFileName =
let testFileName = Path.GetFileNameWithoutExtension testFileName
let offset =
match testFileName with
| name when name.EndsWith "Tests" -> 5
| _ -> 4
testFileName.Substring(0, testFileName.Length - offset) + ".fs"
codeFileName, testFileName
let addProblemFiles (itemGroup : XElement) (``namespace`` : XNamespace) problemDirectory fileNames =
|> (asFst "Compile")
|> List.append [ "", "None" ]
|> List.iter (fun (fileName, tag) ->
let element = ``namespace`` + tag |> XElement
[| problemDirectory; fileName |]
|> Path.Combine
|> asSnd (xname "Include")
|> XAttribute
|> element.Add
itemGroup.Add element)
let projectFilePath = Path.Combine [| directory; projectFileName |]
let projectDocument = loadDocument projectFilePath
let ``namespace`` = projectDocument.GetDefaultNamespace()
let itemGroup = getCodeFilesItemGroup projectDocument
let problemsInProject = getProblemDirectories itemGroup
let problemsOnDisk = getSubDirectories directoriesToExclude directory
let newProblems = getNewProblems problemsInProject problemsOnDisk
for problem in newProblems do
let codeFileName, testFileName = getCodeFileName problem
createCodeFile problem codeFileName
addProblemFiles itemGroup ``namespace`` problem [ codeFileName; testFileName ]
projectDocument.Save projectFilePath
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment