diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Csv.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Csv.cs deleted file mode 100644 index aba6043d2bb..00000000000 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Csv.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.ObjectModel; - -namespace Microsoft.PowerShell.Commands -{ - /// - /// This class is used to parse CSV text. - /// - internal class CSVHelper - { - internal CSVHelper(char delimiter) - { - Delimiter = delimiter; - } - - /// - /// Gets or sets the delimiter that separates the values. - /// - internal char Delimiter { get; } = ','; - - /// - /// Parse a CSV string. - /// - /// - /// String to be parsed. - /// - internal Collection ParseCsv(string csv) - { - Collection result = new Collection(); - string tempString = string.Empty; - csv = csv.Trim(); - if (csv.Length == 0 || csv[0] == '#') - { - return result; - } - - bool inQuote = false; - for (int i = 0; i < csv.Length; i++) - { - char c = csv[i]; - if (c == Delimiter) - { - if (!inQuote) - { - result.Add(tempString); - tempString = string.Empty; - } - else - { - tempString += c; - } - } - else - { - switch (c) - { - case '"': - if (inQuote) - { - // If we are at the end of the string or the end of the segment, create a new value - // Otherwise we have an error - if (i == csv.Length - 1) - { - result.Add(tempString); - tempString = string.Empty; - inQuote = false; - break; - } - - if (csv[i + 1] == Delimiter) - { - result.Add(tempString); - tempString = string.Empty; - inQuote = false; - i++; - } - else if (csv[i + 1] == '"') - { - tempString += '"'; - i++; - } - else - { - inQuote = false; - } - } - else - { - inQuote = true; - } - - break; - - default: - tempString += c; - break; - } - } - } - - if (tempString.Length > 0) - { - result.Add(tempString); - } - - return result; - } - } -} diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/ImportAliasCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/ImportAliasCommand.cs index 8ccd9a3a834..c401f5311a1 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/ImportAliasCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/ImportAliasCommand.cs @@ -8,6 +8,7 @@ using System.Management.Automation; using System.Management.Automation.Internal; using System.Security; +using System.Text; namespace Microsoft.PowerShell.Commands { @@ -99,6 +100,93 @@ public SwitchParameter Force #endregion Parameters #region Command code + + private static bool OnlyContainsWhitespace(string line) + { + bool result = true; + + foreach (char c in line) + { + if (char.IsWhiteSpace(c)) + { + continue; + } + + result = false; + break; + } + + return result; + } + + private static Collection ParseCsvLine(string csv) + { + ReadOnlySpan csvTrimmed = csv.Trim(); + Collection result = new Collection(); + StringBuilder wordBuffer = new StringBuilder(); + + for (int i = 0; i < csvTrimmed.Length; i++) + { + char nextChar = csvTrimmed[i]; + + // if next character was delimiter or we are at the end, add string to result and clear wordBuffer + // else if next character was quote, perform reading until next quote and add it to wordBuffer + // else read and add it to wordBuffer + if (nextChar == ',') + { + result.Add(wordBuffer.ToString()); + wordBuffer.Clear(); + } + else if (nextChar == '"') + { + bool inQuotes = true; + + // if we are within a quote section, read and append to wordBuffer until we find a next quote that is not followed by another quote + // if it is a single quote, escape the quote section + // if the quote is followed by an other quote, do not escape and add a quote character to wordBuffer + while (i + 1 < csvTrimmed.Length && inQuotes) + { + i++; + nextChar = csvTrimmed[i]; + + if (nextChar == '"') + { + if (i + 1 < csvTrimmed.Length && csvTrimmed[i + 1] == '"') + { + wordBuffer.Append(nextChar); + i++; + } + else + { + inQuotes = false; + } + } + else + { + wordBuffer.Append(nextChar); + } + } + } + else + { + wordBuffer.Append(nextChar); + } + } + + string lastWord = wordBuffer.ToString(); + if (lastWord != string.Empty) + { + result.Add(lastWord); + } + + return result; + } + + private static bool LineShouldBeSkipped(string line) + { + // if line is empty or a comment, return true + return line.Length == 0 || line[0] == '#' || OnlyContainsWhitespace(line); + } /// /// The main processing loop of the command. @@ -289,104 +377,100 @@ private bool VerifyShadowingExistingCommandsAndWriteError(string aliasName) private Collection GetAliasesFromFile(bool isLiteralPath) { Collection result = new Collection(); - string filePath = null; using (StreamReader reader = OpenFile(out filePath, isLiteralPath)) { - CSVHelper csvHelper = new CSVHelper(','); - - Int64 lineNumber = 0; + long lineNumber = 0; string line = null; while ((line = reader.ReadLine()) != null) { ++lineNumber; - // Ignore blank lines - if (line.Length == 0) + if (LineShouldBeSkipped(line)) { continue; } - // Ignore lines that only contain whitespace - if (OnlyContainsWhitespace(line)) - { - continue; - } + Collection parsedLine = ParseCsvLine(line); - // Ignore comment lines - if (line[0] == '#') + if (IsValidParsedLine(parsedLine, lineNumber, filePath)) { - continue; + ScopedItemOptions options = CreateItemOptions(parsedLine, filePath, lineNumber); + result.Add(ConstructAlias(parsedLine, options)); } + } + } - Collection values = csvHelper.ParseCsv(line); - - if (values.Count != 4) - { - string message = StringUtil.Format(AliasCommandStrings.ImportAliasFileInvalidFormat, filePath, lineNumber); - - FormatException formatException = - new FormatException(message); - - ErrorRecord errorRecord = - new ErrorRecord( - formatException, - "ImportAliasFileFormatError", - ErrorCategory.ReadError, - filePath); - - errorRecord.ErrorDetails = new ErrorDetails(message); + return result; + } - ThrowTerminatingError(errorRecord); - } + private ScopedItemOptions CreateItemOptions(Collection parsedLine, string filePath, long lineNumber) + { + ScopedItemOptions options; + if (!Enum.TryParse(parsedLine[3], out options)) + { + // if parsing is no succes + string message = StringUtil.Format(AliasCommandStrings.ImportAliasOptionsError, filePath, lineNumber); + ErrorRecord errorRecord = + new ErrorRecord( + new ArgumentException(), + "ImportAliasOptionsError", + ErrorCategory.ReadError, + filePath); + + errorRecord.ErrorDetails = new ErrorDetails(message); + WriteError(errorRecord); + } - ScopedItemOptions options = ScopedItemOptions.None; + return options; + } - try - { - options = (ScopedItemOptions)Enum.Parse(typeof(ScopedItemOptions), values[3], true); - } - catch (ArgumentException argException) - { - string message = StringUtil.Format(AliasCommandStrings.ImportAliasOptionsError, filePath, lineNumber); + private AliasInfo ConstructAlias(Collection parsedLine, ScopedItemOptions options) + { + AliasInfo newAlias = + new AliasInfo( + parsedLine[0], + parsedLine[1], + this.Context, + options); + string aliasDescription = parsedLine[2]; + if (!string.IsNullOrEmpty(aliasDescription)) + { + newAlias.Description = aliasDescription; + } - ErrorRecord errorRecord = - new ErrorRecord( - argException, - "ImportAliasOptionsError", - ErrorCategory.ReadError, - filePath); + return newAlias; + } - errorRecord.ErrorDetails = new ErrorDetails(message); - WriteError(errorRecord); - continue; - } + private bool IsValidParsedLine(Collection parsedLine, long lineNumber, string filePath) + { + if (parsedLine.Count != 4) + { + // if not four values, do ThrowTerminatingError(errorRecord) with ImportAliasFileFormatError, just like old implementation + string message = StringUtil.Format(AliasCommandStrings.ImportAliasFileInvalidFormat, filePath, lineNumber); - AliasInfo newAlias = - new AliasInfo( - values[0], - values[1], - Context, - options); + FormatException formatException = + new FormatException(message); - if (!string.IsNullOrEmpty(values[2])) - { - newAlias.Description = values[2]; - } + ErrorRecord errorRecord = + new ErrorRecord( + formatException, + "ImportAliasFileFormatError", + ErrorCategory.ReadError, + filePath); - result.Add(newAlias); - } + errorRecord.ErrorDetails = new ErrorDetails(message); - reader.Dispose(); + ThrowTerminatingError(errorRecord); + return false; } - return result; + return true; } private StreamReader OpenFile(out string filePath, bool isLiteralPath) { StreamReader result = null; - filePath = null; ProviderInfo provider = null; Collection paths = null; @@ -459,24 +543,6 @@ private void ThrowFileOpenError(Exception e, string pathWithError) errorRecord.ErrorDetails = new ErrorDetails(message); this.ThrowTerminatingError(errorRecord); } - - private static bool OnlyContainsWhitespace(string line) - { - bool result = true; - - foreach (char c in line) - { - if (char.IsWhiteSpace(c) && c != '\n' && c != '\r') - { - continue; - } - - result = false; - break; - } - - return result; - } #endregion Command code } }