F# Scripting

Read Time: 5 minutes

Today I want to provide a quick overview of F#‘s scripting capabilities and how they can integrate into your daily workflow. Depending on your current perspective, you may not have considered using F# as a daily scripting tool. If not, then you should. Why leave all the untapped power just setting there?

The fact is F# is a great language to bring to the table when you are dealing with either adhoc or periodic scripting jobs. It doesn’t need to be relegated to just compiled apps. Taking a step back, in the earlier days F# had a reasonable scripting story. It was cross-platform, you just needed Mono and you could do interactive F# or run .fsx files for scripting. When .NET Core entered the scene things got a bit muddled. In the initial incarnation, .NET Core didn’t support interactive F#. This meant you still needed Mono for scripting, even if you were writing compiled apps for .NET Core. In the grand scheme of things, not the largest problem in the world, but still a pain. It also didn’t feel like a very good story to tell, dealing with multiple runtime targets, depending on what you needed to do. Thankfully we’re past that now. .NET Core has supported dotnet fsi to run interactive F# and scripting for awhile now. For me, this closes one of those annoying gaps.

Where that leaves us today is writing F# scripts for general tasks is again a breeze. I love bash as much as the next person, but F# brings way more power to the table when I need to do specialized work. To show what I mean, I have a fictional scenario. I want a script to do some simple loading of data into my Redis instance. For this I use the StackExchange.Redis package. This example is pretty simple. I’ll read some data (config) files from a directory and load them into Redis for server consumption. I’ll also read them back out, just to make sure things look as expected.

The first thing to notice in the script below is the #!usr/bin/env line at the top. If you’re a Windows user, ignore this. But for the Linux users, this allows me to run the script directly instead of needing to run dotnet fsi load-config.fsx from the command line. The second item to notice is the #r line. This is how I load packages for the script. Here I use nuget to grab the StackExchange.Redis package (specifically version 2.2.4). There are other options for how resources can be loaded and you should checkout the F# Interactive docs for more detail. I’ve also choosen to load a custom library script with #load. Beyond that, it’s a typical F# script.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env -S dotnet fsi
#r "nuget: StackExchange.Redis, 2.2.4"
#load "/home/codesuji/toolkit/codesuji.os.fsx"

open System
open StackExchange.Redis

let redis = ConnectionMultiplexer.Connect("localhost");
let db = redis.GetDatabase()

if fsi.CommandLineArgs.Length < 2 then
printfn "Error: Missing config directory"
Environment.Exit 1

let configPath = fsi.CommandLineArgs.[1]

// Load configs
printfn "Loading configs from: %s" configPath
IO.Directory.GetFiles(configPath, "config*")
|> Array.iter(fun filename -> let key = filename.Replace(configPath, "").Replace("/", "")
let data = IO.File.ReadAllText(filename)
let success = db.StringSet(RedisKey(key), RedisValue(data))
if not success then printfn "Error loading: %s" filename)

// Show configs
[ "config-website-dev"; "config-website-qa"; "config-website-prod"]
|> List.iter(fun key -> let value = db.StringGet(RedisKey(key))
printfn "%s =\n%s" key (if value.HasValue then value.ToString() else "undefined"))

This can be run one of two ways (depending on your environment). One is dotnet fsi redis-load-configs.fsx. The second (my preference) is ./redis-load-configs.fsx. Below I show the simple script in action.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
~/examples$ ./redis-load-configs.fsx ./configs
Loading configs from: ./configs
config-website-dev =
{
"debug": true,
"timeout": 30000,
"suffix": "dev",
"features": {
"foo": true,
"bar": true
}
}

config-website-qa =
{
"debug": false,
"timeout": 10000,
"suffix": "qa",
"features": {
"foo": true,
"bar": false
}
}

config-website-prod =
{
"debug": false,
"timeout": 10000,
"suffix": "prod",
"features": {
"foo": false,
"bar": false
}
}

There isn’t much else to show. The intent was to provide a quick little post of F# scripting and how you can use it to enhance your daily workflow. So check it out. With that, I say go forth and script!