// Original behavior/reference: sed (Lee E. McMahon)
// Ported to .NET by Timothy J. Bruce <[email protected]>
namespace Icod.CoreUtils.Sed;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Text;
/// <summary>
/// sed: stream editor (simplified).
/// Supported features:
/// -n suppress automatic printing
/// -e script add script to the commands to run
/// Script forms supported (simple subset):
/// s/old/new/g substitute (supports regex) with optional g flag
/// Multiple -e accepted; first script applied if none specified.
/// This is a small, portable subset for common use cases.
/// </summary>
public static class Command
{
public static int Run(string[] args, TextReader? stdin = null, TextWriter? stdout = null, TextWriter? stderr = null)
{
stdin ??= Console.In;
stdout ??= Console.Out;
stderr ??= Console.Error;
var scripts = new List<string>();
var suppress = false;
var files = new List<string>();
var i = 0;
for (; i < args.Length; i++)
{
var a = args[i];
if (!a.StartsWith("-"))
{
break;
}
if (a == "-n")
{
suppress = true;
}
else if (a == "-e")
{
if (i + 1 < args.Length)
{
i++;
scripts.Add(args[i]);
}
}
else
{
// ignore other options
}
}
for (; i < args.Length; i++)
{
files.Add(args[i]);
}
if (scripts.Count == 0)
{
stderr.WriteLine("sed: no scripts provided");
return 2;
}
if (files.Count == 0)
{
files.Add("-");
}
try
{
foreach (var path in files)
{
TextReader reader;
if (path == "-")
{
reader = stdin;
}
else
{
reader = new StreamReader(path, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
}
using (reader)
{
string? line;
while ((line = reader.ReadLine()) is not null)
{
var outLine = line;
foreach (var script in scripts)
{
if (script.StartsWith("s/"))
{
// parse s/old/new/flags
var m = Regex.Match(script, @"^s/(?<old>.*?)/(?<new>.*?)/(?<flags>.*)$");
if (!m.Success)
{
m = Regex.Match(script, @"^s/(?<old>.*?)/(?<new>.*)$");
}
if (m.Success)
{
var oldPat = m.Groups["old"].Value;
var newText = m.Groups["new"].Value;
var flags = m.Groups["flags"].Success ? m.Groups["flags"].Value : string.Empty;
var regexOptions = RegexOptions.None;
var replaceCount = flags.Contains("g") ? -1 : 1;
outLine = RegexReplace(outLine, oldPat, newText, regexOptions, replaceCount);
}
}
else
{
// unsupported script: ignore
}
}
if (!suppress)
{
stdout.WriteLine(outLine);
}
}
}
}
return 0;
}
catch (Exception ex)
{
stderr.WriteLine($"sed: {ex.Message}");
return 1;
}
}
private static string RegexReplace(string input, string pattern, string replacement, RegexOptions options, int maxReplacements)
{
try
{
if (maxReplacements == -1)
{
return Regex.Replace(input, pattern, replacement, options);
}
var regex = new Regex(pattern, options);
var count = 0;
return regex.Replace(input, m =>
{
count++;
if (count <= maxReplacements)
{
return replacement;
}
return m.Value;
});
}
catch
{
return input;
}
}
}
Comments