This tutorial demonstrates how to build parsers for configuration files like INI, properties, and other structured text formats using SharpParser.Core. We'll focus on INI files as they're commonly used for application configuration.
- .NET 6.0 SDK or later
- SharpParser.Core NuGet package
- Basic understanding of F#
INI files have a simple structure:
[Section1]
key1=value1
key2=value2
[Section2]
key3=value3
; This is a comment
# This is also a commentKey characteristics:
- Sections:
[SectionName]groups related settings - Key-value pairs:
key=valuestores individual settings - Comments: Lines starting with
;or# - Whitespace: Usually ignored
Create a new F# console application:
dotnet new console -lang F# -n ConfigParserTutorial
cd ConfigParserTutorial
dotnet add package SharpParser.CoreLet's start by recognizing the basic elements of INI files:
open SharpParser.Core
// Basic INI parser that recognizes sections and key-value pairs
let createBasicIniParser () =
Parser.create ()
|> Parser.enableTokens ()
|> Parser.onError (fun ctx msg ->
printfn "Parse error: %s" msg
ctx)
// Section headers: [SectionName]
|> Parser.onPattern @"\[([^\]]+)\]" (fun ctx matched ->
let sectionName = matched.Trim('[', ']')
printfn "Found section: [%s]" sectionName
ParserContextOps.enterMode sectionName ctx)
// Key-value pairs: key=value
|> Parser.onPattern @"^([^=]+)=(.*)$" (fun ctx matched ->
let parts = matched.Split('=', 2)
if parts.Length = 2 then
let key = parts.[0].Trim()
let value = parts.[1].Trim()
printfn "Found setting: %s = %s" key value
ctx)
// Comments (lines starting with ; or #)
|> Parser.onPattern @"^[;#].*" (fun ctx matched ->
printfn "Found comment: %s" matched
ctx)
// Empty lines
|> Parser.onPattern @"^\s*$" (fun ctx _ ->
printfn "Empty line"
ctx)Test with a simple INI file:
[<EntryPoint>]
let main argv =
let parser = createBasicIniParser ()
let iniContent = """[Database]
host=localhost
port=5432
[Application]
name=MyApp
debug=true"""
printfn "Parsing INI content:\n%s\n" iniContent
let context = Parser.runString iniContent parser
printfn "Tokens: %d" (List.length (Parser.getTokens context))
printfn "Errors: %d" (List.length (Parser.getErrors context))
0To create a useful configuration parser, we need to build a data structure:
type Configuration = Map<string, Map<string, string>>
let createIniParser () =
Parser.create ()
|> Parser.enableTokens ()
|> Parser.onError (fun ctx msg ->
printfn "Parse error: %s" msg
ctx)
// ... (include all handlers from createBasicIniParser)
// Use user data to build configuration during parsing
|> Parser.onPattern @"\[([^\]]+)\]" (fun ctx matched ->
let sectionName = matched.Trim('[', ']')
printfn "Entering section: [%s]" sectionName
// Store current section in context
let ctxWithSection = ParserContextOps.setUserData "currentSection" sectionName ctx
ParserContextOps.enterMode sectionName ctxWithSection)
|> Parser.onPattern @"^([^=]+)=(.*)$" (fun ctx matched ->
let parts = matched.Split('=', 2)
if parts.Length = 2 then
let key = parts.[0].Trim()
let value = parts.[1].Trim()
// Get current section and build configuration
match ParserContextOps.getUserData "currentSection" ctx with
| Some sectionName ->
// Here you would update a configuration map
// For now, just print
printfn "Setting [%s] %s = %s" sectionName key value
| None ->
printfn "Setting (no section) %s = %s" key value
ctx)Here's a complete parser that builds a configuration object:
module ConfigParserTutorial
open SharpParser.Core
type IniConfig = Map<string, Map<string, string>>
let createIniParser () =
Parser.create ()
|> Parser.enableTokens ()
|> Parser.onError (fun ctx msg ->
printfn "INI Parse error: %s" msg
ctx)
// Section headers
|> Parser.onPattern @"\[([^\]]+)\]" (fun ctx matched ->
let sectionName = matched.Trim('[', ']')
printfn "Section: [%s]" sectionName
ParserContextOps.enterMode sectionName ctx)
// Key-value pairs
|> Parser.onPattern @"^([^=]+)=(.*)$" (fun ctx matched ->
let parts = matched.Split('=', 2)
if parts.Length = 2 then
let key = parts.[0].Trim()
let value = parts.[1].Trim()
printfn "Setting: %s = %s" key value
ctx)
// Comments
|> Parser.onPattern @"^[;#].*" (fun ctx matched ->
printfn "Comment: %s" matched
ctx)
// Empty lines and whitespace
|> Parser.onPattern @"^\s*$" (fun ctx _ -> ctx)
|> Parser.onPattern @"^\s+|\s+$" (fun ctx _ -> ctx)
[<EntryPoint>]
let main argv =
let parser = createIniParser ()
let iniData = """; Sample application configuration
[Database]
host = localhost
port = 5432
username = admin
password = secret123
[Application]
name = My Awesome App
version = 1.0.0
debug = true
; Feature flags
[Features]
logging = enabled
caching = disabled
experimental = false"""
printfn "Parsing INI configuration:\n%s\n" iniData
let context = Parser.runString iniData parser
let tokens = Parser.getTokens context
let errors = Parser.getErrors context
printfn "\nResults:"
printfn "Tokens found: %d" (List.length tokens)
printfn "Errors: %d" (List.length errors)
if not (List.isEmpty errors) then
printfn "\nErrors:"
errors |> List.iter (fun error ->
printfn " Line %d, Col %d: %s" error.Line error.Col error.Message)
printfn "\nSample tokens:"
tokens |> List.take 15 |> List.iter (fun token ->
printfn " %A" token)
0Add support for more advanced INI features:
// Support for quoted values
|> Parser.onPattern @"^([^=]+)=\s*""([^""]*)""" (fun ctx matched ->
// Handle quoted values with spaces
ctx)
// Support for multi-line values (non-standard but useful)
|> Parser.onPattern @"^([^=]+)=\s*<<(\w+)$" (fun ctx matched ->
// Start multi-line value capture
ctx)
// Support for environment variable substitution
|> Parser.onPattern @"\$\{([^}]+)\}" (fun ctx matched ->
// Replace with environment variable
ctx)
// Support for include directives
|> Parser.onPattern @"^include\s+(.+)$" (fun ctx matched ->
// Load additional config file
ctx)The same approach works for other formats:
let createPropertiesParser () =
Parser.create ()
// Similar to INI but no sections
|> Parser.onPattern @"^([^=]+)=(.*)$" (fun ctx matched ->
let parts = matched.Split('=', 2)
let key = parts.[0].Trim()
let value = parts.[1].Trim()
printfn "Property: %s = %s" key value
ctx)let createYamlParser () =
Parser.create ()
// Basic YAML structure
|> Parser.onPattern @"^(\s*)([^:]+):\s*(.*)$" (fun ctx matched ->
// Handle indentation for nesting
ctx)let createTomlParser () =
Parser.create ()
// TOML table headers
|> Parser.onPattern @"^\[([^\]]+)\]$" (fun ctx matched ->
let tableName = matched.Trim('[', ']')
printfn "Table: [%s]" tableName
ctx)
// Key-value pairs
|> Parser.onPattern @"^([^=]+)=(.*)$" (fun ctx matched ->
ctx)dotnet runYou should see the parser correctly identifying sections, key-value pairs, and comments in the INI file.
Here's how to use the parsed configuration in an application:
// Function to extract configuration values
let getConfigValue (config: IniConfig) section key defaultValue =
config
|> Map.tryFind section
|> Option.bind (Map.tryFind key)
|> Option.defaultValue defaultValue
// Usage example
let config = parseIniFile "app.config"
let dbHost = getConfigValue config "Database" "host" "localhost"
let appName = getConfigValue config "Application" "name" "MyApp"
let debugMode = getConfigValue config "Application" "debug" "false" = "true"- Section grouping:
[Section]headers group related settings - Key-value parsing:
key=valuewith proper trimming - Comments: Ignore lines starting with
;or# - Whitespace handling: Trim keys and values appropriately
- Empty lines: Skip blank lines gracefully
- Add support for array values:
key = [value1, value2] - Implement configuration validation against schemas
- Add support for environment variable substitution
- Create configuration watchers for hot reloading
- Add encryption support for sensitive values
See examples/SharpParser.Examples/IniExample.fs for a complete working implementation with additional features.