Search code examples
c#.nettext-filesstreamwriterbackspace

Remove characters from text file when appending strings with backspaces


Is it possible to append a string containing backspaces to a text file and treat all the backspaces in it as a "remove last char" operation?

E.g. My text file:

This file has

two lines

Some sort of C# code like this:

string str = "...\b\b\b\b\b\b\b\b\b\b\b\b\b one line."
myFile.Append(str);

And after executing this code, the text file looks like this:

This file has one line.

The StreamWriter and the File classes don't seem to help much.

I could not figure out an optimal way to implement this without reading and writing the whole file on every single append operation, which would probably lead to terrible performance issues for large text files. The idea is to intensively write log statements to a text file with this new feature.

My second concern is how to deal with windows-style new line characters ("\r\n")? i.e. one backspace should remove a whole single newline character sequence ("\r\n").

Any ideas on how to implement this?

Source code would be highly appreciated.


Solution

  • Based on xanatos' answer, and on TheGeneral's comment, I wrote this prototype FileLogger class, which evaluates the string (to be appended) into a sequence of initial backspace characters plus a remaining string (without any backspaces).

    In case there are initial backspaces, the program truncates the FileStream object based on the number of initial backspaces (in a very naïve way), and then appends the remaining string.

    Unfortunately, this solution does NOT consider any \r\n newline sequence which should be be removed by a single backspace, both from the FileStream and from the appended string. As it is right now, two backspaces are required to remove a single windows-style newline character sequence.

    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Text;
    
    namespace Example
    {
        public static class FileLogger
        {
            public static bool IsStarted { get; private set; }
            public static Encoding Encoding { get; private set; }
            public static string LogFilePath { get; private set; }
    
            private static FileStream FS;
            private static int BytesPerChar;
            private static readonly object Locker = new object();
    
            public static void Start(string logFilePath, Encoding encoding = null)
            {
                lock (Locker)
                {
                    if (IsStarted) return;
                    LogFilePath = logFilePath;
                    Encoding = encoding ?? Encoding.UTF8;
                    if (File.Exists(LogFilePath)) File.SetAttributes(LogFilePath, FileAttributes.Normal);
                    FS = new FileStream(LogFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, 4096, FileOptions.RandomAccess);
                    FS.SetLength(0);
                    FS.Flush();
                    BytesPerChar = Encoding.UTF8.GetByteCount(new[] { 'A' });
                    IsStarted = true;
                }
            }
    
            public static void Close()
            {
                lock (Locker)
                {
                    if (!IsStarted) return;
                    try { FS?.Close(); } catch { }
                    FS = null;
                    IsStarted = false;
                }
            }
    
            public static void WriteToFile(string text)
            {
                lock (Locker)
                {
                    if (string.IsNullOrEmpty(text)) return;
    
                    if (!text.Contains('\b'))
                    {
                        FS.Position = FS.Length;
                        byte[] bytes = Encoding.GetBytes(text);
                        FS.Write(bytes, 0, bytes.Length);
                        FS.Flush();
                        return;
                    }
    
                    // Evaluates the the string into initial backspaces and remaining text to be appended:
                    EvaluateText(text, out int initialBackspaces, out string remainingText);
    
                    // If there are no initial backspaces after evaluating the string, just append it and return:
                    if (initialBackspaces <= 0)
                    {
                        if (string.IsNullOrEmpty(remainingText)) return;
    
                        FS.Position = FS.Length;
                        byte[] bytes = Encoding.GetBytes(remainingText);
                        FS.Write(bytes, 0, bytes.Length);
                        FS.Flush();
                        return;
                    }
    
                    // First process the initial backspaces:
                    long pos = FS.Length - initialBackspaces * BytesPerChar;
                    FS.Position = pos > 0 ? pos : 0;
                    FS.SetLength(FS.Position);
    
                    // Then write any remaining evaluated text:
                    if (!string.IsNullOrEmpty(remainingText))
                    {
                        byte[] bytes = Encoding.GetBytes(remainingText);
                        FS.Write(bytes, 0, bytes.Length);
                    }
                    FS.Flush();
                    return;
                }
            }
    
            public static void EvaluateText(string text, out int initialBackspaces, out string remainingTextToAppend)
            {
                initialBackspaces = 0;
                StringBuilder sb = new StringBuilder();
                foreach (char ch in text)
                {
                    if(ch == '\b')
                    {
                        if (sb.Length > 0) sb.Length--;
                        else initialBackspaces++;
                    }
                    else sb.Append(ch);
                }
                remainingTextToAppend = sb.ToString();
            }
        }
    }
    

    Test Code:

    FileLogger.Start("test.log");
    FileLogger.WriteToFile("aaa\r\n");
    FileLogger.WriteToFile("bbbb");
    FileLogger.WriteToFile("\b");
    FileLogger.WriteToFile("\b\b");
    FileLogger.WriteToFile("\b\b\b\b");
    FileLogger.WriteToFile("XXX");
    FileLogger.WriteToFile("\b\bYY\bZ");
    FileLogger.WriteToFile("\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b");
    FileLogger.WriteToFile("Done!");
    FileLogger.Close();
    

    Output (test.log file):

    aaXYZ