fck: Fake Construction Kit
Yeah it's christmas time again, and santa's elves are quite busy.
And when I say busy, I don't mean:
I mean busy like this:
So they decided to build some automation productivity tools, and they choose Santa's favorite language to do the job:
F# of course !
F# scripting
No body would seriously use a compiled language for automation tools. Requiring compilation or a CI server for this kind of things usually kills motivation.
Of course it is possible to write bash/batch files but the syntax if fugly once you start to make more advanced tools.
Python, JavaScript, Ruby or PowerShell are cool, but you end up as often with scripted languages with dynamic typing which you'll come to regret when you have to maintain it on the long term.
F# is a staticaly typed language that can be easily scripted. Type inference make it feel like shorter JavaScript but with far higher safety !
Writing F# script is easy and fast. Test it from the command line:
|
Then write:
printfn "Merry Christmas !"
press :q
to exit
now launch it on linux with:
|
or on windows:
|
Excellent.
The only problem is that typing the fshapi --exec
this is a bit tedious.
Bash/Batch to the rescue
We can create a bash/batch script to puth in the path that will launch the script (for linux):
|
|
|
or one windows
|
|
Done !
Better, but now we need to write a bash and/or a batch script for each F# script.
fck bash/batch dispatcher FTW !
We create a fck file (don't forget to chmod +x it) that takes a command [lang=bash] #!/usr/bin/env bash # #the fck tool path fckpath=\((readlink -f "\)0") #this fck tool dir dir=\((dirname\)fckpath) script="\(dir/fck-cmd/fck-\)1.fsx" shell="\(dir/fck-cmd/fck-\)1.sh" cmd="\(1" shift # #restore packages if needed if [ ! -d "\)dir/fck-cmd/packages" ] then pushd "\(dir/fck-cmd" > /dev/null mono "\)dir/fck-cmd/.paket/paket.bootstrapper.exe" --run restore popd > /dev/null fi # #execute script command if it exists if [ -e $script ] then mono "\(dir/fck-cmd/packages/FAKE/tools/FAKE.exe" "\)script" -- \(@ # #execute shell command if it exists elif [ -e\)shell ] then eval \(shell\)@ # #show help else pushd "\(dir/fck-cmd" > /dev/null mono "\)dir/fck-cmd/packages/FAKE/tools/FAKE.exe" "\(dir/fck-cmd/fck-help.fsx" --\)cmd $@ popd > /dev/null fi
and the batch version:
|
Forget the paket part for now.
The bash take a command argument, and check whether a fck-cmd/fck-$cmd.fsx file exists. If it does, run it ! It also works with shell scripts name fck-$cmd.sh or batch scripts fck-$cmd.cmd to integrate quickly with existing tools.
Fake for faster startups
When F# scripts start to grow big, especially with things like Json or Xml type providers, load time can start to raise above acceptable limits for a cli.
Using Fake to launch scripts takes adventage of it's compilation cache. We get the best of both world:
- scriptability for quick changes and easy deployment
- automaticly cached jit compilation for fast startup and execution
We could have written all commands in a single fsx file and pattern maching on the command name, but once we start to have more commands, the script becomes bigger and compilation longer. The problem is also that the pattern matching becomes a friction point in the source control.
FckLib
At some point we have recuring code in the tools. So we can create helper scripts that will be included by command scripts.
For instance parsing the command line is often useful so I created a helper:
#r "System.Xml.Linq"
#I "../packages/FAKE/tools"
#r "FakeLib.dll"
open Fake
open System
// culture invariant, case insensitive string comparison
let (==) x y = String.Equals(x,y, StringComparison.InvariantCultureIgnoreCase)
open System.Xml.Linq
module CommandLine =
// get the command line, fck style...
let getCommandLine() =
System.Environment.GetCommandLineArgs()
|> Array.toList
|> List.skipWhile ((<>) "--")
|> function
| [] -> []
| _ :: tail -> tail
// check whether the command line starts with specified command
let (|Cmd|_|) str cmdLine =
match cmdLine with
| s :: _ when s == str -> Some()
| _ -> None
We use the --
to delimit arguments reserved for the script.
Since Fake is used to launch scripts, we can also include FakeLib for all the fantastic helpers it contains.
Here is a sample fck-cmd/fck-hello.fsx script that can write hello.
#load "fcklib/FckLib.fsx"
#r "../packages/FAKE/tools/FakeLib.dll"
open FckLib.CommandLine
open Fake
let name =
match getCommandLine() with
| name :: _ -> name
| _ -> "you"
tracefn "Hello %s" name
It uses FakeLib for the tracefn
function and FckLib for getCommandLine
.
You can call it with (once fck is in your Path environment variable):
|
Help
A tool without help is just a nightmare, and writing help should be easy.
The last part of fck bash script lanch the fck-help.fsx script:
#load "fcklib/FckLib.fsx"
open System.IO
open Printf
open FckLib.CommandLine
let root = __SOURCE_DIRECTORY__
let cmd =
match getCommandLine() with
| cmd :: _ -> cmd
| _ -> "help"
let file cmd = sprintf "%s/fck-%s.txt" root cmd
let filename = file cmd
if File.Exists filename then
File.ReadAllText filename
|> printfn "%s"
else
printfn "The command %s doesn't exist" cmd
printfn ""
file "help"
|> File.ReadAllText
|> printfn "%s"
This script tries to find a fck-xxx.txt file and display it, or fallbacks to fck-help.txt.
For exemple the help for our fck hello command will be in fck-hello.txt:
|
Of course we can the pimp the fck-help.fsx to parse the txt help files and add codes for colors, verbosity etc.
Deployment
Deployment is really easy. We can clone the git repository, and add it to $PATH.
Run the commands, it will automatically restore packages if missing, and lanch the script.
To upgrade to a new version, call fck update, defined in fck-update.sh :
|
or batch fck-update.cmd: [lang=bash] git pull .paket\paket.bootstrapper.exe --run restore
Yep, that's that easy
Happy Christmas
Using Santa's elves tools, I hope you won't be stuck at work on xmas eve ! Enjoy !
<summary>Print to <c>stdout</c> using the given format, and add a newline.</summary>
<param name="format">The formatter.</param>
<returns>The formatted result.</returns>
type String = interface IEnumerable<char> interface IEnumerable interface ICloneable interface IComparable interface IComparable<string> interface IConvertible interface IEquatable<string> new: value: nativeptr<char> -> unit + 8 overloads member Clone: unit -> obj member CompareTo: value: obj -> int + 1 overload ...
<summary>Represents text as a sequence of UTF-16 code units.</summary>
--------------------
String(value: nativeptr<char>) : String
String(value: char[]) : String
String(value: ReadOnlySpan<char>) : String
String(value: nativeptr<sbyte>) : String
String(c: char, count: int) : String
String(value: nativeptr<char>, startIndex: int, length: int) : String
String(value: char[], startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: Text.Encoding) : String
String.Equals(a: string, b: string, comparisonType: StringComparison) : bool
<summary>Specifies the culture, case, and sort rules to be used by certain overloads of the <see cref="M:System.String.Compare(System.String,System.String)" /> and <see cref="M:System.String.Equals(System.Object)" /> methods.</summary>
<summary>Provides information about, and means to manipulate, the current environment and platform. This class cannot be inherited.</summary>
<summary>Provides methods for creating, manipulating, searching, and sorting arrays, thereby serving as the base class for all arrays in the common language runtime.</summary>
<summary>Builds a list from the given array.</summary>
<param name="array">The input array.</param>
<returns>The list of array elements.</returns>
<exception cref="T:System.ArgumentNullException">Thrown when the input array is null.</exception>
module List from Microsoft.FSharp.Collections
<summary>Contains operations for working with values of type <see cref="T:Microsoft.FSharp.Collections.list`1" />.</summary>
<namespacedoc><summary>Operations for collections such as lists, arrays, sets, maps and sequences. See also <a href="https://docs.microsoft.com/dotnet/fsharp/language-reference/fsharp-collection-types">F# Collection Types</a> in the F# Language Guide. </summary></namespacedoc>
--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T member IsEmpty: bool member Item: index: int -> 'T with get ...
<summary>The type of immutable singly-linked lists.</summary>
<remarks>Use the constructors <c>[]</c> and <c>::</c> (infix) to create values of this type, or the notation <c>[1;2;3]</c>. Use the values in the <c>List</c> module to manipulate values of this type, or pattern match against the values directly. </remarks>
<exclude />
<summary>Bypasses elements in a list while the given predicate returns True, and then returns the remaining elements of the list.</summary>
<param name="predicate">A function that evaluates an element of the list to a boolean value.</param>
<param name="list">The input list.</param>
<returns>The result list.</returns>
<summary>The representation of "Value of type 'T"</summary>
<param name="Value">The input value.</param>
<returns>An option representing the value.</returns>
<summary>The representation of "No value"</summary>
get the command line, fck style...
<summary>Extensible printf-style formatting for numbers and other datatypes</summary>
<remarks>Format specifications are strings with "%" markers indicating format placeholders. Format placeholders consist of <c>%[flags][width][.precision][type]</c>.</remarks>
<category index="4">Strings and Text</category>
<summary>Print to a string via an internal string buffer and return the result as a string. Helper printers must return strings.</summary>
<param name="format">The input formatter.</param>
<returns>The formatted string.</returns>
active recognizer File: FileSystemInfo -> Choice<FileInfo,(DirectoryInfo * Collections.Generic.IEnumerable<FileSystemInfo>)>
<summary> Active pattern which discriminates between files and directories. </summary>
--------------------
type File = static member AppendAllLines: path: string * contents: IEnumerable<string> -> unit + 1 overload static member AppendAllLinesAsync: path: string * contents: IEnumerable<string> * encoding: Encoding * ?cancellationToken: CancellationToken -> Task + 1 overload static member AppendAllText: path: string * contents: string -> unit + 1 overload static member AppendAllTextAsync: path: string * contents: string * encoding: Encoding * ?cancellationToken: CancellationToken -> Task + 1 overload static member AppendText: path: string -> StreamWriter static member Copy: sourceFileName: string * destFileName: string -> unit + 1 overload static member Create: path: string -> FileStream + 2 overloads static member CreateSymbolicLink: path: string * pathToTarget: string -> FileSystemInfo static member CreateText: path: string -> StreamWriter static member Decrypt: path: string -> unit ...
<summary>Provides static methods for the creation, copying, deletion, moving, and opening of a single file, and aids in the creation of <see cref="T:System.IO.FileStream" /> objects.</summary>
File.ReadAllText(path: string, encoding: Text.Encoding) : string
<summary>Formatted printing to stdout, adding a newline.</summary>
<param name="format">The input formatter.</param>
<returns>The return type and arguments of the formatter.</returns>