using System.Collections.Generic;
using System.Text.RegularExpressions;
internal static class SqlCmdScriptParser
private static readonly Command[] ControlCommands =
Pattern = new(@":(?<command>setvar)\s+(?<name>\w+)\s+""(?<value>.*)"""),
variables.Add(match.Groups["name"].Value, match.Groups["value"].Value);
Pattern = new(@":(?<command>on error)\s+(?<value>exit|ignore)"),
MatchSubstitution = (_, _) => string.Empty,
Pattern = new(@"(?<command>\$)\((?<name>\w+)\)"),
variables.GetValueOrDefault(match.Groups["name"].Value) ?? string.Empty,
private static readonly IReadOnlyDictionary<string, Command> ControlCommandsMap =
ControlCommands.ToDictionary(c => c.Id, StringComparer.OrdinalIgnoreCase);
private static readonly Regex ControlCommandsPattern = GetControlCommandsPattern();
private static readonly Regex BatchSeparatorPattern = new Regex("GO").ToFullLine();
public static IReadOnlyCollection<string> Parse(
string input, IReadOnlyDictionary<string, string> variables = null) =>
.SubstituteControlCommands(variables)
private static Regex GetControlCommandsPattern()
var patterns = ControlCommands
.Select(c => c.IsFullLine ? c.Pattern.ToFullLine() : c.Pattern)
var combinedPattern = string.Join("|", patterns);
return new Regex(combinedPattern, RegexOptions.Multiline | RegexOptions.Compiled);
private static Regex ToFullLine(this Regex source) =>
new($@"^\s*{source}\s*$\n?", RegexOptions.Multiline | RegexOptions.Compiled);
private static string SubstituteControlCommands(
this string input, IReadOnlyDictionary<string, string> variables)
var establishedVariables = new Dictionary<string, string>(
variables ?? new Dictionary<string, string>(), StringComparer.OrdinalIgnoreCase);
return ControlCommandsPattern
.Replace(input, match => SubstituteControlCommandMatch(match, establishedVariables));
private static string SubstituteControlCommandMatch(
Match match, Dictionary<string, string> variables)
var commandId = match.Groups["command"].Value;
var command = ControlCommandsMap.GetValueOrDefault(commandId)
?? throw new InvalidOperationException($"Unknown command: {commandId}");
return command.MatchSubstitution(match, variables);
private static IReadOnlyCollection<string> SplitBatch(this string input) =>
BatchSeparatorPattern.Split(input)
.Where(s => !string.IsNullOrEmpty(s))
private sealed class Command
public string Id { get; init; } = string.Empty;
public Regex Pattern { get; init; } = new(string.Empty);
public bool IsFullLine { get; init; }
public Func<Match, Dictionary<string, string>, string> MatchSubstitution { get; init; } =
public static class Program
public static void Main()
:setvar DatabaseName "MessageQueue"
:setvar DefaultFilePrefix "MessageQueue"
:setvar __IsSqlCmdEnabled "True"
IF N'$(__IsSqlCmdEnabled)' NOT LIKE N'True'
PRINT N'SQLCMD mode must be enabled to successfully execute this script.';
PRINT N'Creating SqlSchema [MessageQueue]...';
var variables = new Dictionary<string, string>();
var batches = SqlCmdScriptParser.Parse(inputScript, variables);
foreach (var batch in batches)
$"------------------ Batch {i++} ---------------".Dump();
Console.WriteLine(batch);