diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2e67cda..ae6907e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [windows-latest, ubuntu-18.04, macos-latest] + os: [windows-2022, ubuntu-20.04, macos-12] env: AZURE_PASSWORD: ${{ secrets.AZURE_PASSWORD }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5fd6345..acf4e77 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -17,7 +17,7 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 strategy: fail-fast: false diff --git a/.github/workflows/dependabot-cake.yml b/.github/workflows/dependabot-cake.yml index 304c519..6edc17a 100644 --- a/.github/workflows/dependabot-cake.yml +++ b/.github/workflows/dependabot-cake.yml @@ -7,7 +7,7 @@ on: jobs: dependabot-cake: - runs-on: ubuntu-18.04 # linux, because this is a docker-action + runs-on: ubuntu-20.04 # linux, because this is a docker-action steps: - name: check/update cake dependencies uses: nils-org/dependabot-cake-action@v1 \ No newline at end of file diff --git a/.github/workflows/publishDocs.yml b/.github/workflows/publishDocs.yml index 8852c27..45d0e74 100644 --- a/.github/workflows/publishDocs.yml +++ b/.github/workflows/publishDocs.yml @@ -11,7 +11,7 @@ env: jobs: cake: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 steps: - name: checkout diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml index 5f94248..9e7988d 100644 --- a/.github/workflows/release-notes.yml +++ b/.github/workflows/release-notes.yml @@ -7,7 +7,7 @@ jobs: draft-stable: env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 steps: - name: Checkout the requested branch diff --git a/src/Cake.FileHelpers.Tests/FileHelperTests.cs b/src/Cake.FileHelpers.Tests/FileHelperTests.cs index f2f34a4..85dd7b1 100644 --- a/src/Cake.FileHelpers.Tests/FileHelperTests.cs +++ b/src/Cake.FileHelpers.Tests/FileHelperTests.cs @@ -3,6 +3,7 @@ using Cake.Xamarin.Tests.Fakes; using Cake.Core.IO; using Xunit; +using System.Text; namespace Cake.FileHelpers.Tests { @@ -82,6 +83,17 @@ public void FindTextInFiles() Assert.Single (monkeyFiles); } + [Fact] + public void FindTextInFilesWithUTF8Encoding() + { + SetupFilesUTF8(); + + var files = context.CakeContext.FindTextInFiles("./testdata/*-utf8.txt", Encoding.UTF8, "Monkey🐒"); + + Assert.NotNull(files); + Assert.Single(files); + } + [Fact] public void FindRegexInFiles() { @@ -93,6 +105,16 @@ public void FindRegexInFiles() Assert.Single (files); } + [Fact] + public void FindRegexInFilesWithUTF8Encoding() + { + SetupFilesUTF8(); + + var files = context.CakeContext.FindRegexInFiles("./testdata/*-utf8.txt", Encoding.UTF8, @"\s{1}Monkey🐒\s{1,}"); + + Assert.NotNull(files); + Assert.Single(files); + } [Fact] public void ReplaceTextInFiles() @@ -110,6 +132,23 @@ public void ReplaceTextInFiles() } } + [Fact] + public void ReplaceTextInFilesWithUTF8Encoding() + { + SetupFilesUTF8(); + + var files = context.CakeContext.ReplaceTextInFiles("./testdata/*.txt", Encoding.UTF8, "Monkey🐒", "Tamarin"); + + Assert.NotNull(files); + Assert.Single(files); + + foreach (var f in files) + { + var contents = context.CakeContext.FileReadText(f, Encoding.UTF8); + Assert.Equal(string.Format(PATTERN_FILE_BASE_VALUE, "Tamarin"), contents); + } + } + [Fact] public void ReplaceRegexInFiles() { @@ -126,9 +165,29 @@ public void ReplaceRegexInFiles() } } + [Fact] + public void ReplaceRegexInFilesWithUTF8Encoding() + { + SetupFilesUTF8(); + + var files = context.CakeContext.ReplaceRegexInFiles("./testdata/*-utf8.txt", Encoding.UTF8, @"\s{1}Monkey🐒\s{1,}", " Tamarin "); + + Assert.NotNull(files); + Assert.Single(files); + + foreach (var f in files) + { + var contents = context.CakeContext.FileReadText(f, Encoding.UTF8); + Assert.Equal(string.Format(PATTERN_FILE_BASE_VALUE, "Tamarin"), contents); + } + } + public const string GROUPS_FILE = "./testdata/Groups.txt"; + public const string GROUPS_FILE_UTF8 = "./testdata/Groups-utf8.txt"; public const string GROUPS_FILE_CONTENT = "Hello World! This is A quick Test to Capture multiple Groups."; + public const string GROUPS_FILE_CONTENT_UTF8 = "🐒Hello 🐒World! 🐒This is A quick 🐒Test to 🐒Capture multiple 🐒Groups."; public const string GROUPS_PATTERN = "([A-Z])(\\w+)"; + public const string GROUPS_PATTERN_UTF8 = "(🐒[A-Z])(\\w+)"; [Fact] public void FindRegexMatchesGroupsInFile() @@ -144,6 +203,20 @@ public void FindRegexMatchesGroupsInFile() Assert.Equal (3, g.Count); } + [Fact] + public void FindRegexMatchesGroupsInFileWithUTF8Encoding() + { + context.CakeContext.FileWriteText(GROUPS_FILE_UTF8, GROUPS_FILE_CONTENT_UTF8); + + var matchesGroups = context.CakeContext.FindRegexMatchesGroupsInFile(GROUPS_FILE_UTF8, GROUPS_PATTERN_UTF8, RegexOptions.None); + + Assert.NotNull(matchesGroups); + Assert.Equal(6, matchesGroups.Count); + + foreach (var g in matchesGroups) + Assert.Equal(3, g.Count); + } + [Fact] public void FindRegexMatchGroupsInFile() { @@ -155,6 +228,18 @@ public void FindRegexMatchGroupsInFile() Assert.Equal (3, matchGroups.Count); } + + [Fact] + public void FindRegexMatchGroupsInFileWithUTF8Encoding() + { + context.CakeContext.FileWriteText(GROUPS_FILE_UTF8, GROUPS_FILE_CONTENT_UTF8); + + var matchGroups = context.CakeContext.FindRegexMatchGroupsInFile(GROUPS_FILE_UTF8, GROUPS_PATTERN_UTF8, RegexOptions.None); + + Assert.NotNull(matchGroups); + Assert.Equal(3, matchGroups.Count); + } + [Fact] public void FindRegexMatchGroupInFile() { @@ -168,6 +253,19 @@ public void FindRegexMatchGroupInFile() Assert.Equal ("ello", matchGroup.Value); } + [Fact] + public void FindRegexMatchGroupInFileWithUTF8Encoding() + { + context.CakeContext.FileWriteText(GROUPS_FILE_UTF8, GROUPS_FILE_CONTENT_UTF8); + + var matchGroup = context.CakeContext.FindRegexMatchGroupInFile(GROUPS_FILE_UTF8, GROUPS_PATTERN_UTF8, 2, RegexOptions.None); + var invalidMatchGroup = context.CakeContext.FindRegexMatchGroupInFile(GROUPS_FILE_UTF8, GROUPS_PATTERN_UTF8, 8, RegexOptions.None); + + Assert.NotNull(matchGroup); + Assert.Null(invalidMatchGroup); + Assert.Equal("ello", matchGroup.Value); + } + public const string PATTERN_FILE_BASE_VALUE = "The {0} makes great software.\nThis is not a surprise."; void SetupFiles() @@ -179,6 +277,18 @@ void SetupFiles() string.Format (PATTERN_FILE_BASE_VALUE, i == 2 ? "Monkey" : "Ape")); } } + + void SetupFilesUTF8() + { + // Setup files + for (int i = 1; i < 5; i++) + { + context.CakeContext.FileWriteText( + string.Format("./testdata/{0}-utf8.txt", i), + Encoding.UTF8, + string.Format(PATTERN_FILE_BASE_VALUE, i == 2 ? "Monkey🐒" : "Ape🦧")); + } + } } } diff --git a/src/Cake.FileHelpers/FileHelpers.cs b/src/Cake.FileHelpers/FileHelpers.cs index 664bd0c..b557fb4 100644 --- a/src/Cake.FileHelpers/FileHelpers.cs +++ b/src/Cake.FileHelpers/FileHelpers.cs @@ -7,6 +7,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Text; namespace Cake.FileHelpers { @@ -26,9 +27,22 @@ public static class FileHelperAliases [CakeMethodAlias] public static string FileReadText (this ICakeContext context, FilePath file) { - var filename = file.MakeAbsolute (context.Environment).FullPath; + using var streamReader = CreateStreamReader(context, file); + return streamReader.ReadToEnd(); + } - return File.ReadAllText (filename); + /// + /// Reads all text from a file + /// + /// The file's text. + /// The context. + /// The file to read. + /// The encoding to use for the file. + [CakeMethodAlias] + public static string FileReadText(this ICakeContext context, FilePath file, Encoding encoding) + { + using var streamReader = CreateStreamReader(context, file, encoding); + return streamReader.ReadToEnd(); } /// @@ -40,9 +54,22 @@ public static string FileReadText (this ICakeContext context, FilePath file) [CakeMethodAlias] public static string[] FileReadLines (this ICakeContext context, FilePath file) { - var filename = file.MakeAbsolute (context.Environment).FullPath; + using var streamReader = CreateStreamReader(context, file); + return ReadLines(streamReader); + } - return File.ReadAllLines (filename); + /// + /// Reads all lines from a file + /// + /// The file's text lines. + /// The context. + /// The file to read. + /// The encoding to use for the file. + [CakeMethodAlias] + public static string[] FileReadLines(this ICakeContext context, FilePath file, Encoding encoding) + { + using var streamReader = CreateStreamReader(context, file, encoding); + return ReadLines(streamReader); } /// @@ -54,9 +81,22 @@ public static string[] FileReadLines (this ICakeContext context, FilePath file) [CakeMethodAlias] public static void FileWriteText (this ICakeContext context, FilePath file, string text) { - var filename = file.MakeAbsolute (context.Environment).FullPath; + using var streamWriter = CreateStreamWriter(context, file, FileMode.Create); + streamWriter.Write(text); + } - File.WriteAllText (filename, text); + /// + /// Writes all text to a file + /// + /// The context. + /// The file to write to. + /// The encoding to use for the file. + /// The text to write. + [CakeMethodAlias] + public static void FileWriteText(this ICakeContext context, FilePath file, Encoding encoding, string text) + { + using var streamWriter = CreateStreamWriter(context, file, FileMode.Create, encoding); + streamWriter.Write(text); } /// @@ -68,9 +108,22 @@ public static void FileWriteText (this ICakeContext context, FilePath file, stri [CakeMethodAlias] public static void FileWriteLines (this ICakeContext context, FilePath file, string[] lines) { - var filename = file.MakeAbsolute (context.Environment).FullPath; + using var streamWriter = CreateStreamWriter(context, file, FileMode.Create); + WriteLines(streamWriter, lines); + } - File.WriteAllLines (filename, lines); + /// + /// Writes all text lines to a file + /// + /// The context. + /// The file to write to. + /// The encoding to use for the file. + /// The text lines to write. + [CakeMethodAlias] + public static void FileWriteLines(this ICakeContext context, FilePath file, Encoding encoding, string[] lines) + { + using var streamWriter = CreateStreamWriter(context, file, FileMode.Create, encoding); + WriteLines(streamWriter, lines); } /// @@ -82,9 +135,22 @@ public static void FileWriteLines (this ICakeContext context, FilePath file, str [CakeMethodAlias] public static void FileAppendText (this ICakeContext context, FilePath file, string text) { - var filename = file.MakeAbsolute (context.Environment).FullPath; + using var streamWriter = CreateStreamWriter(context, file, FileMode.OpenOrCreate); + streamWriter.Write(text); + } - File.AppendAllText (filename, text); + /// + /// Appends all text to a file + /// + /// The context. + /// The file to append text to. + /// The encoding to use for the file. + /// The text to append. + [CakeMethodAlias] + public static void FileAppendText(this ICakeContext context, FilePath file, Encoding encoding, string text) + { + using var streamWriter = CreateStreamWriter(context, file, FileMode.OpenOrCreate, encoding); + streamWriter.Write(text); } /// @@ -96,9 +162,22 @@ public static void FileAppendText (this ICakeContext context, FilePath file, str [CakeMethodAlias] public static void FileAppendLines (this ICakeContext context, FilePath file, string[] lines) { - var filename = file.MakeAbsolute (context.Environment).FullPath; + using var streamWriter = CreateStreamWriter(context, file, FileMode.OpenOrCreate); + WriteLines(streamWriter, lines); + } - File.AppendAllLines (filename, lines); + /// + /// Appends all text lines to a file + /// + /// The context. + /// The file to append text to. + /// The encoding to use for the file. + /// The text lines to append. + [CakeMethodAlias] + public static void FileAppendLines(this ICakeContext context, FilePath file, Encoding encoding, string[] lines) + { + using var streamWriter = CreateStreamWriter(context, file, FileMode.OpenOrCreate, encoding); + WriteLines(streamWriter, lines); } /// @@ -143,6 +222,37 @@ public static FilePath[] ReplaceTextInFiles (this ICakeContext context, string g return results.ToArray (); } + /// + /// Replaces the text in files matched by the given globber pattern + /// + /// The files that had text replaced in them. + /// The context. + /// The globber pattern to match files to replace text in. + /// The encoding to use for the file. + /// The text to find. + /// The replacement text. + [CakeMethodAlias] + public static FilePath[] ReplaceTextInFiles(this ICakeContext context, string globberPattern, Encoding encoding, string findText, string replaceText) + { + var files = context.Globber.GetFiles(globberPattern); + + var results = new ConcurrentBag(); + + Parallel.ForEach(files, f => { + var contents = FileReadText(context, f, encoding); + + if (contents.Contains(findText)) + { + contents = contents.Replace(findText, replaceText); + FileWriteText(context, f, encoding, contents); + + results.Add(f); + } + }); + + return results.ToArray(); + } + /// /// Replaces the regex pattern in files matched by the given globber pattern. /// @@ -157,6 +267,21 @@ public static FilePath[] ReplaceRegexInFiles (this ICakeContext context, string return ReplaceRegexInFiles (context, globberPattern, rxFindPattern, replaceText, RegexOptions.None); } + /// + /// Replaces the regex pattern in files matched by the given globber pattern. + /// + /// The files that had text replaced in them. + /// The context. + /// The globber pattern to match files to replace text in. + /// The encoding to use for the file. + /// The regular expression to find. + /// The replacement text. + [CakeMethodAlias] + public static FilePath[] ReplaceRegexInFiles(this ICakeContext context, string globberPattern, Encoding encoding, string rxFindPattern, string replaceText) + { + return ReplaceRegexInFiles(context, globberPattern, encoding, rxFindPattern, replaceText, RegexOptions.None); + } + /// /// Replaces the regex pattern in files matched by the given globber pattern. /// @@ -186,6 +311,38 @@ public static FilePath[] ReplaceRegexInFiles (this ICakeContext context, string return results.ToArray (); } + /// + /// Replaces the regex pattern in files matched by the given globber pattern. + /// + /// The files that had text replaced in them. + /// The context. + /// The globber pattern to match files to replace text in. + /// The encoding to use for the file. + /// The regular expression to find. + /// The replacement text. + /// The regular expression options to use. + [CakeMethodAlias] + public static FilePath[] ReplaceRegexInFiles(this ICakeContext context, string globberPattern, Encoding encoding, string rxFindPattern, string replaceText, RegexOptions rxOptions) + { + var rx = new Regex(rxFindPattern, rxOptions); + var files = context.Globber.GetFiles(globberPattern); + + var results = new ConcurrentBag(); + + Parallel.ForEach(files, f => + { + var contents = FileReadText(context, f, encoding); + if (rx.IsMatch(contents)) + { + contents = rx.Replace(contents, replaceText); + FileWriteText(context, f, encoding, contents); + results.Add(f); + } + }); + + return results.ToArray(); + } + /// /// Finds files with regular expression pattern in files matching the given globber pattern. /// @@ -199,6 +356,20 @@ public static FilePath[] FindRegexInFiles (this ICakeContext context, string glo return FindRegexInFiles (context, globberPattern, rxFindPattern, RegexOptions.None); } + /// + /// Finds files with regular expression pattern in files matching the given globber pattern. + /// + /// The files which match the regular expression and globber pattern. + /// The context. + /// The globber pattern to match files to replace text in. + /// The encoding to use for the file. + /// The regular expression to find. + [CakeMethodAlias] + public static FilePath[] FindRegexInFiles(this ICakeContext context, string globberPattern, Encoding encoding, string rxFindPattern) + { + return FindRegexInFiles(context, globberPattern, encoding, rxFindPattern, RegexOptions.None); + } + /// /// Finds files with regular expression pattern in files matching the given globber pattern. /// @@ -224,6 +395,33 @@ public static FilePath[] FindRegexInFiles (this ICakeContext context, string glo return results.ToArray (); } + /// + /// Finds files with regular expression pattern in files matching the given globber pattern. + /// + /// The files which match the regular expression and globber pattern. + /// The context. + /// The globber pattern to match files to replace text in. + /// The encoding to use for the file. + /// The regular expression to find. + /// The regular expression options to use. + [CakeMethodAlias] + public static FilePath[] FindRegexInFiles(this ICakeContext context, string globberPattern, Encoding encoding, string rxFindPattern, RegexOptions rxOptions) + { + var rx = new Regex(rxFindPattern, rxOptions); + var files = context.Globber.GetFiles(globberPattern); + + var results = new ConcurrentBag(); + + Parallel.ForEach(files, f => + { + var contents = FileReadText(context, f, encoding); + if (rx.IsMatch(contents)) + results.Add(f); + }); + + return results.ToArray(); + } + /// /// Finds files with the given text in files matching the given globber pattern. /// @@ -238,6 +436,31 @@ public static FilePath[] FindTextInFiles (this ICakeContext context, string glob return FindTextInFiles(context, files, substring); } + /// + /// Finds files with the given text in files matching the given globber pattern. + /// + /// The files which match the regular expression and globber pattern. + /// The context. + /// The globber pattern to match files to replace text in. + /// The encoding to use for the file. + /// The regular expression to find. + [CakeMethodAlias] + public static FilePath[] FindTextInFiles(this ICakeContext context, string globberPattern, Encoding encoding, string findPattern) + { + var files = context.Globber.GetFiles(globberPattern); + + var results = new ConcurrentBag(); + + Parallel.ForEach(files, f => + { + var contents = FileReadText(context, f, encoding); + if (contents.Contains(findPattern)) + results.Add(f); + }); + + return results.ToArray(); + } + /// /// Finds files with the given text in files matching the given collection of files. /// @@ -259,6 +482,29 @@ public static FilePath[] FindTextInFiles(this ICakeContext context, IEnumerable< return results.ToArray(); } + /// + /// Finds files with the given text in files matching the given collection of files. + /// + /// The files which match the regular expession in the files. + /// The context. + /// The files to find text in. + /// The encoding to use for the file. + /// The substring to find. + [CakeMethodAlias] + public static FilePath[] FindTextInFiles(this ICakeContext context, IEnumerable files, Encoding encoding, string substring) + { + var results = new ConcurrentBag(); + + Parallel.ForEach(files, f => + { + var contents = FileReadText(context, f, encoding); + if (contents.Contains(substring)) + results.Add(f); + }); + + return results.ToArray(); + } + /// /// Finds the regex matches in a text file. /// @@ -286,6 +532,34 @@ public static List FindRegexMatchesInFile (this ICakeContext context, Fi return values; } + /// + /// Finds the regex matches in a text file. + /// + /// The regex matches in file. + /// Context. + /// The text file. + /// The encoding to use for the file. + /// The regex pattern to search for. + /// The regex options. + [CakeMethodAlias] + public static List FindRegexMatchesInFile(this ICakeContext context, FilePath file, Encoding encoding, string rxFindPattern, RegexOptions rxOptions) + { + if (!context.FileSystem.Exist(file)) + return null; + + var rx = new Regex(rxFindPattern, rxOptions); + var contents = FileReadText(context, file, encoding); + + var values = new List(); + + var matches = rx.Matches(contents); + foreach (Match m in matches) + if (m.Success && m.Value != null) + values.Add(m.Value); + + return values; + } + /// /// Finds the first regex match in a textfile. /// @@ -305,6 +579,26 @@ public static string FindRegexMatchInFile (this ICakeContext context, FilePath f return null; } + /// + /// Finds the first regex match in a textfile. + /// + /// The first regex match in the file. + /// The context. + /// The file. + /// The encoding to use for the file. + /// The regex pattern to search for. + /// The regex options. + [CakeMethodAlias] + public static string FindRegexMatchInFile(this ICakeContext context, FilePath file, Encoding encoding, string rxFindPattern, RegexOptions rxOptions) + { + var values = FindRegexMatchesInFile(context, file, encoding, rxFindPattern, rxOptions); + + if (values != null) + return values.FirstOrDefault(); + + return null; + } + /// /// Finds regex matches in a file and returns all match groups. /// @@ -332,20 +626,65 @@ public static List> FindRegexMatchesGroupsInFile (this ICakeContext return values; } + /// + /// Finds regex matches in a file and returns all match groups. + /// + /// The matches with their groups. + /// The context. + /// The file. + /// The encoding to use for the file. + /// The regex pattern to search for. + /// The regex options. + [CakeMethodAlias] + public static List> FindRegexMatchesGroupsInFile(this ICakeContext context, FilePath file, Encoding encoding, string rxFindPattern, RegexOptions rxOptions) + { + if (!context.FileSystem.Exist(file)) + return null; + + var rx = new Regex(rxFindPattern, rxOptions); + var contents = FileReadText(context, file, encoding); + + var values = new List>(); + + var matches = rx.Matches(contents); + foreach (Match m in matches) + if (m.Success) + values.Add(m.Groups.Cast().ToList()); + + return values; + } + + /// + /// Finds the first regex match in a file and returns all match groups. + /// + /// The match groups. + /// The context. + /// The file. + /// The regex pattern to search for. + /// The regex options. + [CakeMethodAlias] + public static List FindRegexMatchGroupsInFile(this ICakeContext context, FilePath file, string rxFindPattern, RegexOptions rxOptions) + { + var groups = FindRegexMatchesGroupsInFile(context, file, rxFindPattern, rxOptions); + + return groups?.FirstOrDefault(); + } + /// /// Finds the first regex match in a file and returns all match groups. /// /// The match groups. /// The context. /// The file. + /// The encoding to use for the file. /// The regex pattern to search for. /// The regex options. [CakeMethodAlias] - public static List FindRegexMatchGroupsInFile (this ICakeContext context, FilePath file, string rxFindPattern, RegexOptions rxOptions) + public static List FindRegexMatchGroupsInFile(this ICakeContext context, FilePath file, Encoding encoding, string rxFindPattern, RegexOptions rxOptions) { - var groups = FindRegexMatchesGroupsInFile (context, file, rxFindPattern, rxOptions); + var groups = FindRegexMatchesGroupsInFile(context, file, encoding, rxFindPattern, rxOptions); - return groups?.FirstOrDefault (); + return groups?.FirstOrDefault(); } /// @@ -368,6 +707,65 @@ public static Group FindRegexMatchGroupInFile (this ICakeContext context, FilePa return null; } + + /// + /// Finds the first regex match in a file and returns a specific match group. + /// + /// The matches with their groups. + /// The context. + /// The file. + /// The encoding to use for the file. + /// The regex pattern to search for. + /// The specific match group. + /// The regex options. + [CakeMethodAlias] + public static Group FindRegexMatchGroupInFile(this ICakeContext context, FilePath file, Encoding encoding, string rxFindPattern, int groupIndex, RegexOptions rxOptions) + { + var matchesGroups = FindRegexMatchesGroupsInFile(context, file, encoding, rxFindPattern, rxOptions); + var matchGroups = matchesGroups?.FirstOrDefault(); + + if (matchGroups != null && matchGroups.Count > groupIndex) + return matchGroups[groupIndex]; + + return null; + } + + private static StreamReader CreateStreamReader(ICakeContext context, FilePath file, Encoding encoding = null) + { + var stream = context.FileSystem.GetFile(file.MakeAbsolute(context.Environment)).OpenRead(); + return encoding is null + ? new StreamReader(stream, leaveOpen: false) + : new StreamReader(stream, encoding, leaveOpen: false); + } + + private static StreamWriter CreateStreamWriter(ICakeContext context, FilePath file, FileMode mode, Encoding encoding = null) + { + var stream = context.FileSystem.GetFile(file.MakeAbsolute(context.Environment)).Open(mode); + return encoding is null + ? new StreamWriter(stream, leaveOpen: false) + : new StreamWriter(stream, encoding, leaveOpen: false); + } + + private static string[] ReadLines(StreamReader streamReader) + { + var lines = new List(); + + string line; + while ((line = streamReader.ReadLine()) is not null) + { + lines.Add(line); + } + + return lines.ToArray(); + } + + private static void WriteLines(StreamWriter streamWriter, string[] lines) + { + foreach (var line in lines) + { + streamWriter.WriteLine(line); + } + } } }