uniblab

Improved Sed.cs

Jul 24th, 2026
10,631
0
Never
2
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 11.77 KB | None | 0 0
  1. // Original behavior/reference: sed (Lee E. McMahon)
  2. // Ported to .NET by Timothy J. Bruce <[email protected]>
  3.  
  4. namespace Icod.CoreUtils.Sed;
  5.  
  6. using System;
  7. using System.Collections.Generic;
  8. using System.IO;
  9. using System.Text;
  10. using System.Text.RegularExpressions;
  11.  
  12. /// <summary>
  13. /// sed: stream editor (simplified).
  14. /// Supported features:
  15. ///   -n        suppress automatic printing
  16. ///   -e script add script to the commands to run
  17. ///   -i[SUFFIX] edit files in-place, optional backup SUFFIX (e.g. -i.bak or -i)
  18. ///   -?        display this help/usage text
  19. /// Script forms supported (subset):
  20. ///   s/old/new/g   substitute (supports regex) with optional g flag
  21. ///   N i TEXT      insert TEXT before line N (example: "1i this is foo")
  22. ///   N r FILE      read FILE and append its contents after line N (example: "2r file.txt")
  23. /// Multiple -e accepted; first script applied if none specified.
  24. /// This is a small, portable subset for common use cases.
  25. /// </summary>
  26. public static class Command {
  27.     private sealed class Script {
  28.         public enum KindT {
  29.             Substitute,
  30.             Insert,
  31.             Read
  32.         }
  33.  
  34.         public KindT Kind {
  35.             get;
  36.         }
  37.         public string? Text {
  38.             get;
  39.         }
  40.         public string? Pattern {
  41.             get;
  42.         }
  43.         public string? Replacement {
  44.             get;
  45.         }
  46.         public string? Flags {
  47.             get;
  48.         }
  49.         public int LineNumber {
  50.             get;
  51.         }
  52.         public string? FileName {
  53.             get;
  54.         }
  55.  
  56.         private Script( KindT k, string? text = null, string? pattern = null, string? replacement = null, string? flags = null, int lineNumber = 0, string? fileName = null ) {
  57.             Kind = k;
  58.             Text = text;
  59.             Pattern = pattern;
  60.             Replacement = replacement;
  61.             Flags = flags;
  62.             LineNumber = lineNumber;
  63.             FileName = fileName;
  64.         }
  65.  
  66.         public static Script Subst( string pattern, string replacement, string? flags ) =>
  67.             new Script( KindT.Substitute, pattern: pattern, replacement: replacement, flags: flags );
  68.  
  69.         public static Script Insert( int lineNumber, string text ) =>
  70.             new Script( KindT.Insert, text: text, lineNumber: lineNumber );
  71.  
  72.         public static Script Read( int lineNumber, string fileName ) =>
  73.             new Script( KindT.Read, lineNumber: lineNumber, fileName: fileName );
  74.     }
  75.  
  76.     public static int Run( string[] args, TextReader? stdin = null, TextWriter? stdout = null, TextWriter? stderr = null ) {
  77.         stdin ??= Console.In;
  78.         stdout ??= Console.Out;
  79.         stderr ??= Console.Error;
  80.  
  81.         var scripts = new List<Script>();
  82.         var suppress = false;
  83.         var files = new List<string>();
  84.         var inPlace = false;
  85.         string? backupSuffix = null;
  86.         var exitCode = 0;
  87.  
  88.         int i = 0;
  89.         for ( ; i < args.Length; i++ ) {
  90.             var a = args[ i ];
  91.             if ( !a.StartsWith( "-" ) ) {
  92.                 break;
  93.             }
  94.  
  95.             if ( a == "-n" ) {
  96.                 suppress = true;
  97.             } else if ( a == "-e" ) {
  98.                 if ( i + 1 < args.Length ) {
  99.                     i++;
  100.                     ParseAndAddScript( args[ i ], scripts );
  101.                 }
  102.             } else if ( a == "-i" ) {
  103.                 inPlace = true;
  104.                 backupSuffix = string.Empty; // no backup
  105.             } else if ( a.StartsWith( "-i" ) && a.Length > 2 ) {
  106.                 inPlace = true;
  107.                 backupSuffix = a.Substring( 2 ); // -iSUFFIX
  108.             } else if ( a == "-?" ) {
  109.                 PrintUsage( stdout );
  110.                 return 0;
  111.             } else {
  112.                 // ignore other options
  113.             }
  114.         }
  115.  
  116.         for ( ; i < args.Length; i++ ) {
  117.             files.Add( args[ i ] );
  118.         }
  119.  
  120.         // If no -e scripts provided, GNU sed treats first non-option arg as the script.
  121.         // Handle single-argument script and split forms (Ni TEXT, Nr FILE).
  122.         if ( scripts.Count == 0 && files.Count > 0 ) {
  123.             var candidate = files[ 0 ];
  124.  
  125.             if ( candidate.StartsWith( "s/" ) || Regex.IsMatch( candidate, @"^[0-9]+[ir]\b" ) ) {
  126.                 var simpleOnly = Regex.IsMatch( candidate, @"^[0-9]+[ir]$" );
  127.                 if ( simpleOnly && files.Count >= 3 ) {
  128.                     // assemble middle tokens as script payload, last token is filename (target)
  129.                     var middle = string.Join( " ", files.GetRange( 1, files.Count - 2 ) );
  130.                     var scriptText = candidate + " " + middle;
  131.                     ParseAndAddScript( scriptText, scripts );
  132.                     var last = files[ files.Count - 1 ];
  133.                     files.Clear();
  134.                     files.Add( last );
  135.                 } else {
  136.                     ParseAndAddScript( candidate, scripts );
  137.                     files.RemoveAt( 0 );
  138.                 }
  139.             }
  140.         }
  141.  
  142.         if ( scripts.Count == 0 ) {
  143.             stderr.WriteLine( "sed: no scripts provided" );
  144.             return 2;
  145.         }
  146.  
  147.         if ( files.Count == 0 ) {
  148.             files.Add( "-" );
  149.         }
  150.  
  151.         try {
  152.             foreach ( var path in files ) {
  153.                 if ( inPlace && path == "-" ) {
  154.                     stderr.WriteLine( "sed: cannot edit standard input in-place" );
  155.                     return 2;
  156.                 }
  157.  
  158.                 TextReader reader;
  159.                 if ( path == "-" ) {
  160.                     reader = stdin;
  161.                 } else {
  162.                     reader = new StreamReader( path, Encoding.UTF8, detectEncodingFromByteOrderMarks: true );
  163.                 }
  164.  
  165.                 if ( inPlace && path != "-" ) {
  166.                     var dir = Path.GetDirectoryName( path ) ?? ".";
  167.                     var tmp = Path.Combine( dir, $".sed.{Path.GetRandomFileName()}.tmp" );
  168.                     using ( reader )
  169.                     using ( var outStream = new FileStream( tmp, FileMode.CreateNew, FileAccess.Write, FileShare.None ) )
  170.                     using ( var writer = new StreamWriter( outStream, ( (StreamReader)reader ).CurrentEncoding ?? Encoding.UTF8 ) ) {
  171.                         int lineNo = 1;
  172.                         string? line;
  173.                         while ( ( line = reader.ReadLine() ) is not null ) {
  174.                             // insert scripts: before current line
  175.                             foreach ( var sc in scripts ) {
  176.                                 if ( sc.Kind == Script.KindT.Insert && sc.LineNumber == lineNo ) {
  177.                                     writer.WriteLine( sc.Text );
  178.                                 }
  179.                             }
  180.  
  181.                             // apply substitutions
  182.                             var outLine = line;
  183.                             foreach ( var sc in scripts ) {
  184.                                 if ( sc.Kind == Script.KindT.Substitute ) {
  185.                                     var oldPat = sc.Pattern ?? string.Empty;
  186.                                     var newText = sc.Replacement ?? string.Empty;
  187.                                     var flags = sc.Flags ?? string.Empty;
  188.                                     var regexOptions = RegexOptions.None;
  189.                                     var replaceCount = flags.Contains( 'g' ) ? -1 : 1;
  190.                                     outLine = RegexReplace( outLine, oldPat, newText, regexOptions, replaceCount );
  191.                                 }
  192.                             }
  193.  
  194.                             if ( !suppress ) {
  195.                                 writer.WriteLine( outLine );
  196.                             }
  197.  
  198.                             // read-file scripts: append file contents AFTER the current line
  199.                             foreach ( var sc in scripts ) {
  200.                                 if ( sc.Kind == Script.KindT.Read && sc.LineNumber == lineNo ) {
  201.                                     try {
  202.                                         using var rf = new StreamReader( sc.FileName ?? string.Empty, Encoding.UTF8 );
  203.                                         string? rline;
  204.                                         while ( ( rline = rf.ReadLine() ) is not null ) {
  205.                                             writer.WriteLine( rline );
  206.                                         }
  207.                                     } catch ( Exception ex ) {
  208.                                         stderr.WriteLine( $"sed: {sc.FileName}: {ex.Message}" );
  209.                                         exitCode = 1;
  210.                                     }
  211.                                 }
  212.                             }
  213.  
  214.                             lineNo++;
  215.                         }
  216.  
  217.                         writer.Flush();
  218.                     }
  219.  
  220.                     if ( backupSuffix is not null && backupSuffix.Length > 0 ) {
  221.                         var bak = path + backupSuffix;
  222.                         if ( File.Exists( bak ) ) {
  223.                             File.Delete( bak );
  224.                         }
  225.                         File.Move( path, bak );
  226.                     }
  227.                     if ( File.Exists( path ) ) {
  228.                         File.Delete( path );
  229.                     }
  230.                     File.Move( tmp, path );
  231.                 } else {
  232.                     using ( reader ) {
  233.                         int lineNo = 1;
  234.                         string? line;
  235.                         while ( ( line = reader.ReadLine() ) is not null ) {
  236.                             // insert scripts write before the current line
  237.                             foreach ( var sc in scripts ) {
  238.                                 if ( sc.Kind == Script.KindT.Insert && sc.LineNumber == lineNo ) {
  239.                                     stdout.WriteLine( sc.Text );
  240.                                 }
  241.                             }
  242.  
  243.                             var outLine = line;
  244.                             foreach ( var sc in scripts ) {
  245.                                 if ( sc.Kind == Script.KindT.Substitute ) {
  246.                                     var oldPat = sc.Pattern ?? string.Empty;
  247.                                     var newText = sc.Replacement ?? string.Empty;
  248.                                     var flags = sc.Flags ?? string.Empty;
  249.                                     var regexOptions = RegexOptions.None;
  250.                                     var replaceCount = flags.Contains( 'g' ) ? -1 : 1;
  251.                                     outLine = RegexReplace( outLine, oldPat, newText, regexOptions, replaceCount );
  252.                                 }
  253.                             }
  254.  
  255.                             if ( !suppress ) {
  256.                                 stdout.WriteLine( outLine );
  257.                             }
  258.  
  259.                             // read-file scripts append file contents after the current line
  260.                             foreach ( var sc in scripts ) {
  261.                                 if ( sc.Kind == Script.KindT.Read && sc.LineNumber == lineNo ) {
  262.                                     try {
  263.                                         using var rf = new StreamReader( sc.FileName ?? string.Empty, Encoding.UTF8 );
  264.                                         string? rline;
  265.                                         while ( ( rline = rf.ReadLine() ) is not null ) {
  266.                                             stdout.WriteLine( rline );
  267.                                         }
  268.                                     } catch ( Exception ex ) {
  269.                                         stderr.WriteLine( $"sed: {sc.FileName}: {ex.Message}" );
  270.                                         exitCode = 1;
  271.                                     }
  272.                                 }
  273.                             }
  274.  
  275.                             lineNo++;
  276.                         }
  277.                     }
  278.                 }
  279.             }
  280.  
  281.             return exitCode;
  282.         } catch ( Exception ex ) {
  283.             stderr.WriteLine( $"sed: {ex.Message}" );
  284.             return 1;
  285.         }
  286.     }
  287.  
  288.     private static void ParseAndAddScript( string scriptText, List<Script> scripts ) {
  289.         // substitution
  290.         var m = Regex.Match( scriptText, @"^s/(?<old>.*?)/(?<new>.*?)/(?<flags>.*)$" );
  291.         if ( !m.Success ) {
  292.             m = Regex.Match( scriptText, @"^s/(?<old>.*?)/(?<new>.*)$" );
  293.         }
  294.         if ( m.Success ) {
  295.             var oldPat = m.Groups[ "old" ].Value;
  296.             var newText = m.Groups[ "new" ].Value;
  297.             var flags = m.Groups[ "flags" ].Success ? m.Groups[ "flags" ].Value : string.Empty;
  298.             newText = UnescapeSedString( newText );
  299.             scripts.Add( Script.Subst( oldPat, newText, flags ) );
  300.             return;
  301.         }
  302.  
  303.         // insert: "<number>i TEXT"
  304.         var im = Regex.Match( scriptText, @"^(?<ln>[0-9]+)i(?:\s+)(?<text>.*)$" );
  305.         if ( im.Success ) {
  306.             if ( int.TryParse( im.Groups[ "ln" ].Value, out var ln ) ) {
  307.                 var text = UnescapeSedString( im.Groups[ "text" ].Value );
  308.                 scripts.Add( Script.Insert( ln, text ) );
  309.                 return;
  310.             }
  311.         }
  312.  
  313.         // read: "<number>r FILE"
  314.         var rm = Regex.Match( scriptText, @"^(?<ln>[0-9]+)r(?:\s+)(?<file>.*)$" );
  315.         if ( rm.Success ) {
  316.             if ( int.TryParse( rm.Groups[ "ln" ].Value, out var ln ) ) {
  317.                 var file = rm.Groups[ "file" ].Value;
  318.                 // do not unescape filenames
  319.                 scripts.Add( Script.Read( ln, file ) );
  320.                 return;
  321.             }
  322.         }
  323.  
  324.         // unsupported script: ignore
  325.     }
  326.  
  327.     private static string UnescapeSedString( string s ) {
  328.         if ( string.IsNullOrEmpty( s ) )
  329.             return s;
  330.         var sb = new StringBuilder( s.Length );
  331.         for ( var i = 0; i < s.Length; i++ ) {
  332.             var c = s[ i ];
  333.             if ( c != '\\' ) {
  334.                 sb.Append( c );
  335.                 continue;
  336.             }
  337.  
  338.             i++;
  339.             if ( i >= s.Length ) {
  340.                 sb.Append( '\\' );
  341.                 break;
  342.             }
  343.  
  344.             var esc = s[ i ];
  345.             switch ( esc ) {
  346.                 case 'n':
  347.                     sb.Append( '\n' );
  348.                     break;
  349.                 case 'r':
  350.                     sb.Append( '\r' );
  351.                     break;
  352.                 case 't':
  353.                     sb.Append( '\t' );
  354.                     break;
  355.                 case '\\':
  356.                     sb.Append( '\\' );
  357.                     break;
  358.                 case 'a':
  359.                     sb.Append( '\a' );
  360.                     break;
  361.                 case 'b':
  362.                     sb.Append( '\b' );
  363.                     break;
  364.                 case 'f':
  365.                     sb.Append( '\f' );
  366.                     break;
  367.                 case 'v':
  368.                     sb.Append( '\v' );
  369.                     break;
  370.                 default:
  371.                     sb.Append( '\\' );
  372.                     sb.Append( esc );
  373.                     break;
  374.             }
  375.         }
  376.         return sb.ToString();
  377.     }
  378.  
  379.     private static string RegexReplace( string input, string pattern, string replacement, RegexOptions options, int maxReplacements ) {
  380.         try {
  381.             if ( maxReplacements == -1 ) {
  382.                 return Regex.Replace( input, pattern, replacement, options );
  383.             }
  384.  
  385.             var regex = new Regex( pattern, options );
  386.             var count = 0;
  387.             return regex.Replace( input, m => {
  388.                 count++;
  389.                 if ( count <= maxReplacements ) {
  390.                     return replacement;
  391.                 }
  392.  
  393.                 return m.Value;
  394.             } );
  395.         } catch {
  396.             return input;
  397.         }
  398.     }
  399.  
  400.     private static void PrintUsage( TextWriter stdout ) {
  401.         stdout.WriteLine( "Usage: sed [-n] [-e script]... [-i[SUFFIX]] [file...]" );
  402.         stdout.WriteLine( "  -n           suppress automatic printing of pattern space" );
  403.         stdout.WriteLine( "  -e script    add the script to the commands to be executed" );
  404.         stdout.WriteLine( "  -i[SUFFIX]   edit files in-place, optional backup SUFFIX" );
  405.         stdout.WriteLine( "  -?           display this help and exit" );
  406.     }
  407. }
  408.  
Advertisement
Comments
  • uniblab
    7 days
    # text 0.11 KB | 0 0
    1. One of these days I'll break that code into smaller chunks and separate functions so it's easier on the eyes.
  • User was banned
Add Comment
Please, Sign In to add comment