// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.IO; using System.Management.Automation; using System.Management.Automation.Internal; using System.Text; using System.Text.RegularExpressions; namespace Microsoft.PowerShell.Commands { /// /// Context information about a match. /// public sealed class MatchInfoContext : ICloneable { internal MatchInfoContext() { } /// /// Gets or sets the lines found before a match. /// public string[] PreContext { get; set; } /// /// Gets or sets the lines found after a match. /// public string[] PostContext { get; set; } /// /// Gets or sets the lines found before a match. Does not include /// overlapping context and thus can be used to /// display contiguous match regions. /// public string[] DisplayPreContext { get; set; } /// /// Gets or sets the lines found after a match. Does not include /// overlapping context and thus can be used to /// display contiguous match regions. /// public string[] DisplayPostContext { get; set; } /// /// Produce a deep copy of this object. /// /// A new object that is a copy of this instance. public object Clone() { return new MatchInfoContext() { PreContext = (string[])PreContext?.Clone(), PostContext = (string[])PostContext?.Clone(), DisplayPreContext = (string[])DisplayPreContext?.Clone(), DisplayPostContext = (string[])DisplayPostContext?.Clone() }; } } /// /// The object returned by select-string representing the result of a match. /// public class MatchInfo { private static readonly string s_inputStream = "InputStream"; /// /// Gets or sets a value indicating whether the match was done ignoring case. /// /// True if case was ignored. public bool IgnoreCase { get; set; } /// /// Gets or sets the number of the matching line. /// /// The number of the matching line. public int LineNumber { get; set; } /// /// Gets or sets the text of the matching line. /// /// The text of the matching line. public string Line { get; set; } = string.Empty; /// /// Gets or sets a value indicating whether the matched portion of the string is highlighted. /// /// Whether the matched portion of the string is highlighted with the negative VT sequence. private readonly bool _emphasize; /// /// Stores the starting index of each match within the line. /// private readonly IReadOnlyList _matchIndexes; /// /// Stores the length of each match within the line. /// private readonly IReadOnlyList _matchLengths; /// /// Initializes a new instance of the class with emphasis disabled. /// public MatchInfo() { this._emphasize = false; } /// /// Initializes a new instance of the class with emphasized matched text. /// Used when virtual terminal sequences are supported. /// /// Sets the matchIndexes. /// Sets the matchLengths. public MatchInfo(IReadOnlyList matchIndexes, IReadOnlyList matchLengths) { this._emphasize = true; this._matchIndexes = matchIndexes; this._matchLengths = matchLengths; } /// /// Gets the base name of the file containing the matching line. /// /// It will be the string "InputStream" if the object came from the input stream. /// This is a readonly property calculated from the path . /// /// /// The file name. public string Filename { get { if (!_pathSet) { return s_inputStream; } return _filename ??= System.IO.Path.GetFileName(_path); } } private string _filename; /// /// Gets or sets the full path of the file containing the matching line. /// /// It will be "InputStream" if the object came from the input stream. /// /// /// The path name. public string Path { get => _pathSet ? _path : s_inputStream; set { _path = value; _pathSet = true; } } private string _path = s_inputStream; private bool _pathSet; /// /// Gets or sets the pattern that was used in the match. /// /// The pattern string. public string Pattern { get; set; } /// /// Gets or sets context for the match, or null if -context was not specified. /// public MatchInfoContext Context { get; set; } /// /// Returns the path of the matching file truncated relative to the parameter. /// /// For example, if the matching path was c:\foo\bar\baz.c and the directory argument was c:\foo /// the routine would return bar\baz.c . /// /// /// The directory base the truncation on. /// The relative path that was produced. public string RelativePath(string directory) { if (!_pathSet) { return this.Path; } string relPath = _path; if (!string.IsNullOrEmpty(directory)) { if (relPath.StartsWith(directory, StringComparison.OrdinalIgnoreCase)) { int offset = directory.Length; if (offset < relPath.Length) { if (directory[offset - 1] == '\\' || directory[offset - 1] == '/') { relPath = relPath.Substring(offset); } else if (relPath[offset] == '\\' || relPath[offset] == '/') { relPath = relPath.Substring(offset + 1); } } } } return relPath; } private const string MatchFormat = "{0}{1}:{2}:{3}"; private const string SimpleFormat = "{0}{1}"; // Prefixes used by formatting: Match and Context prefixes // are used when context-tracking is enabled, otherwise // the empty prefix is used. private const string MatchPrefix = "> "; private const string ContextPrefix = " "; private const string EmptyPrefix = ""; /// /// Returns the string representation of this object. The format /// depends on whether a path has been set for this object or not. /// /// If the path component is set, as would be the case when matching /// in a file, ToString() would return the path, line number and line text. /// If path is not set, then just the line text is presented. /// /// /// The string representation of the match object. public override string ToString() { return ToString(null); } /// /// Returns the string representation of the match object same format as ToString() /// but trims the path to be relative to the argument. /// /// Directory to use as the root when calculating the relative path. /// The string representation of the match object. public string ToString(string directory) { return ToString(directory, Line); } /// /// Returns the string representation of the match object with the matched line passed /// in as and trims the path to be relative to /// the argument. /// /// Directory to use as the root when calculating the relative path. /// Line that the match occurs in. /// The string representation of the match object. private string ToString(string directory, string line) { string displayPath = (directory != null) ? RelativePath(directory) : _path; // Just return a single line if the user didn't // enable context-tracking. if (Context == null) { return FormatLine(line, this.LineNumber, displayPath, EmptyPrefix); } // Otherwise, render the full context. List lines = new(Context.DisplayPreContext.Length + Context.DisplayPostContext.Length + 1); int displayLineNumber = this.LineNumber - Context.DisplayPreContext.Length; foreach (string contextLine in Context.DisplayPreContext) { lines.Add(FormatLine(contextLine, displayLineNumber++, displayPath, ContextPrefix)); } lines.Add(FormatLine(line, displayLineNumber++, displayPath, MatchPrefix)); foreach (string contextLine in Context.DisplayPostContext) { lines.Add(FormatLine(contextLine, displayLineNumber++, displayPath, ContextPrefix)); } return string.Join(System.Environment.NewLine, lines.ToArray()); } /// /// Returns the string representation of the match object same format as ToString() /// and inverts the color of the matched text if virtual terminal is supported. /// /// Directory to use as the root when calculating the relative path. /// The string representation of the match object with matched text inverted. public string ToEmphasizedString(string directory) { if (!_emphasize) { return ToString(directory); } return ToString(directory, EmphasizeLine()); } /// /// Surrounds the matched text with virtual terminal sequences to invert it's color. Used in ToEmphasizedString. /// /// The matched line with matched text inverted. private string EmphasizeLine() { string invertColorsVT100 = VTUtility.GetEscapeSequence(VTUtility.VT.Inverse); string resetVT100 = VTUtility.GetEscapeSequence(VTUtility.VT.Reset); char[] chars = new char[(_matchIndexes.Count * (invertColorsVT100.Length + resetVT100.Length)) + Line.Length]; int lineIndex = 0; int charsIndex = 0; for (int i = 0; i < _matchIndexes.Count; i++) { // Adds characters before match Line.CopyTo(lineIndex, chars, charsIndex, _matchIndexes[i] - lineIndex); charsIndex += _matchIndexes[i] - lineIndex; lineIndex = _matchIndexes[i]; // Adds opening vt sequence invertColorsVT100.CopyTo(0, chars, charsIndex, invertColorsVT100.Length); charsIndex += invertColorsVT100.Length; // Adds characters being emphasized Line.CopyTo(lineIndex, chars, charsIndex, _matchLengths[i]); lineIndex += _matchLengths[i]; charsIndex += _matchLengths[i]; // Adds closing vt sequence resetVT100.CopyTo(0, chars, charsIndex, resetVT100.Length); charsIndex += resetVT100.Length; } // Adds remaining characters in line Line.CopyTo(lineIndex, chars, charsIndex, Line.Length - lineIndex); return new string(chars); } /// /// Formats a line for use in ToString. /// /// The line to format. /// The line number to display. /// The file path, formatted for display. /// The match prefix. /// The formatted line as a string. private string FormatLine(string lineStr, int displayLineNumber, string displayPath, string prefix) { return _pathSet ? StringUtil.Format(MatchFormat, prefix, displayPath, displayLineNumber, lineStr) : StringUtil.Format(SimpleFormat, prefix, lineStr); } /// /// Gets or sets a list of all Regex matches on the matching line. /// public Match[] Matches { get; set; } = Array.Empty(); /// /// Create a deep copy of this MatchInfo instance. /// /// A new object that is a copy of this instance. internal MatchInfo Clone() { // Just do a shallow copy and then deep-copy the // fields that need it. MatchInfo clone = (MatchInfo)this.MemberwiseClone(); if (clone.Context != null) { clone.Context = (MatchInfoContext)clone.Context.Clone(); } // Regex match objects are immutable, so we can get away // with just copying the array. clone.Matches = (Match[])clone.Matches.Clone(); return clone; } } /// /// A cmdlet to search through strings and files for particular patterns. /// [Cmdlet(VerbsCommon.Select, "String", DefaultParameterSetName = ParameterSetFile, HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2097119")] [OutputType(typeof(bool), typeof(MatchInfo), ParameterSetName = new[] { ParameterSetFile, ParameterSetObject, ParameterSetLiteralFile })] [OutputType(typeof(string), ParameterSetName = new[] { ParameterSetFileRaw, ParameterSetObjectRaw, ParameterSetLiteralFileRaw })] public sealed class SelectStringCommand : PSCmdlet { private const string ParameterSetFile = "File"; private const string ParameterSetFileRaw = "FileRaw"; private const string ParameterSetObject = "Object"; private const string ParameterSetObjectRaw = "ObjectRaw"; private const string ParameterSetLiteralFile = "LiteralFile"; private const string ParameterSetLiteralFileRaw = "LiteralFileRaw"; /// /// A generic circular buffer. /// /// The type of items that are buffered. private sealed class CircularBuffer : ICollection { // Ring of items private readonly T[] _items; // Current length, as opposed to the total capacity // Current start of the list. Starts at 0, but may // move forwards or wrap around back to 0 due to // rotation. private int _firstIndex; /// /// Initializes a new instance of the class. /// /// The maximum capacity of the buffer. /// If is negative. public CircularBuffer(int capacity) { if (capacity < 0) { throw new ArgumentOutOfRangeException(nameof(capacity)); } _items = new T[capacity]; Clear(); } /// /// Gets the maximum capacity of the buffer. If more items /// are added than the buffer has capacity for, then /// older items will be removed from the buffer with /// a first-in, first-out policy. /// public int Capacity => _items.Length; /// /// Whether or not the buffer is at capacity. /// public bool IsFull => Count == Capacity; /// /// Convert from a 0-based index to a buffer index which /// has been properly offset and wrapped. /// /// The index to wrap. /// If is out of range. /// /// The actual index that /// maps to. /// private int WrapIndex(int zeroBasedIndex) { if (Capacity == 0 || zeroBasedIndex < 0) { throw new ArgumentOutOfRangeException(nameof(zeroBasedIndex)); } return (zeroBasedIndex + _firstIndex) % Capacity; } #region IEnumerable implementation. public IEnumerator GetEnumerator() { for (int i = 0; i < Count; i++) { yield return _items[WrapIndex(i)]; } } IEnumerator IEnumerable.GetEnumerator() { return (IEnumerator)GetEnumerator(); } #endregion #region ICollection implementation public int Count { get; private set; } public bool IsReadOnly => false; /// /// Adds an item to the buffer. If the buffer is already /// full, the oldest item in the list will be removed, /// and the new item added at the logical end of the list. /// /// The item to add. public void Add(T item) { if (Capacity == 0) { return; } int itemIndex; if (IsFull) { itemIndex = _firstIndex; _firstIndex = (_firstIndex + 1) % Capacity; } else { itemIndex = _firstIndex + Count; Count++; } _items[itemIndex] = item; } public void Clear() { _firstIndex = 0; Count = 0; } public bool Contains(T item) { throw new NotImplementedException(); } public void CopyTo(T[] array, int arrayIndex) { if (array == null) { throw new ArgumentNullException(nameof(array)); } if (arrayIndex < 0) { throw new ArgumentOutOfRangeException(nameof(arrayIndex)); } if (Count > (array.Length - arrayIndex)) { throw new ArgumentException("arrayIndex"); } // Iterate through the buffer in correct order. foreach (T item in this) { array[arrayIndex++] = item; } } public bool Remove(T item) { throw new NotImplementedException(); } #endregion /// /// Create an array of the items in the buffer. Items /// will be in the same order they were added. /// /// The new array. public T[] ToArray() { T[] result = new T[Count]; CopyTo(result, 0); return result; } /// /// Access an item in the buffer. Indexing is based off /// of the order items were added, rather than any /// internal ordering the buffer may be maintaining. /// /// The index of the item to access. /// The buffered item at index . public T this[int index] { get { if (!(index >= 0 && index < Count)) { throw new ArgumentOutOfRangeException(nameof(index)); } return _items[WrapIndex(index)]; } } } /// /// An interface to a context tracking algorithm. /// private interface IContextTracker { /// /// Gets matches with completed context information /// that are ready to be emitted into the pipeline. /// IList EmitQueue { get; } /// /// Track a non-matching line for context. /// /// The line to track. void TrackLine(string line); /// /// Track a matching line. /// /// The line to track. void TrackMatch(MatchInfo match); /// /// Track having reached the end of the file, /// giving the tracker a chance to process matches with /// incomplete context information. /// void TrackEOF(); } /// /// A state machine to track display context for each match. /// private sealed class DisplayContextTracker : IContextTracker { private enum ContextState { InitialState, CollectPre, CollectPost, } private ContextState _contextState = ContextState.InitialState; private readonly int _preContext; private readonly int _postContext; // The context leading up to the match. private readonly CircularBuffer _collectedPreContext; // The context after the match. private readonly List _collectedPostContext; // Current match info we are tracking postcontext for. // At any given time, if set, this value will not be // in the emitQueue but will be the next to be added. private MatchInfo _matchInfo = null; /// /// Initializes a new instance of the class. /// /// How much preContext to collect at most. /// How much postContext to collect at most. public DisplayContextTracker(int preContext, int postContext) { _preContext = preContext; _postContext = postContext; _collectedPreContext = new CircularBuffer(preContext); _collectedPostContext = new List(postContext); _emitQueue = new List(); Reset(); } #region IContextTracker implementation public IList EmitQueue => _emitQueue; private readonly List _emitQueue; // Track non-matching line public void TrackLine(string line) { switch (_contextState) { case ContextState.InitialState: break; case ContextState.CollectPre: _collectedPreContext.Add(line); break; case ContextState.CollectPost: // We're not done collecting post-context. _collectedPostContext.Add(line); if (_collectedPostContext.Count >= _postContext) { // Now we're done. UpdateQueue(); } break; } } // Track matching line public void TrackMatch(MatchInfo match) { // Update the queue in case we were in the middle // of collecting postcontext for an older match... if (_contextState == ContextState.CollectPost) { UpdateQueue(); } // Update the current matchInfo. _matchInfo = match; // If postContext is set, then we need to hold // onto the match for a while and gather context. // Otherwise, immediately move the match onto the queue // and let UpdateQueue update our state instead. if (_postContext > 0) { _contextState = ContextState.CollectPost; } else { UpdateQueue(); } } // Track having reached the end of the file. public void TrackEOF() { // If we're in the middle of collecting postcontext, we // already have a match and it's okay to queue it up // early since there are no more lines to track context // for. if (_contextState == ContextState.CollectPost) { UpdateQueue(); } } #endregion /// /// Moves matchInfo, if set, to the emitQueue and /// resets the tracking state. /// private void UpdateQueue() { if (_matchInfo != null) { _emitQueue.Add(_matchInfo); if (_matchInfo.Context != null) { _matchInfo.Context.DisplayPreContext = _collectedPreContext.ToArray(); _matchInfo.Context.DisplayPostContext = _collectedPostContext.ToArray(); } Reset(); } } // Reset tracking state. Does not reset the emit queue. private void Reset() { _contextState = (_preContext > 0) ? ContextState.CollectPre : ContextState.InitialState; _collectedPreContext.Clear(); _collectedPostContext.Clear(); _matchInfo = null; } } /// /// A class to track logical context for each match. /// /// /// The difference between logical and display context is /// that logical context includes as many context lines /// as possible for a given match, up to the specified /// limit, including context lines which overlap between /// matches and other matching lines themselves. Display /// context, on the other hand, is designed to display /// a possibly-continuous set of matches by excluding /// overlapping context (lines will only appear once) /// and other matching lines (since they will appear /// as their own match entries.). /// private sealed class LogicalContextTracker : IContextTracker { // A union: string | MatchInfo. Needed since // context lines could be either proper matches // or non-matching lines. private sealed class ContextEntry { public readonly string Line; public readonly MatchInfo Match; public ContextEntry(string line) { Line = line; } public ContextEntry(MatchInfo match) { Match = match; } public override string ToString() => Match?.Line ?? Line; } // Whether or not early entries found // while still filling up the context buffer // have been added to the emit queue. // Used by UpdateQueue. private bool _hasProcessedPreEntries; private readonly int _preContext; private readonly int _postContext; // A circular buffer tracking both precontext and postcontext. // // Essentially, the buffer is separated into regions: // | prectxt region (older entries, length = precontext) | // | match region (length = 1) | // | postctxt region (newer entries, length = postcontext) | // // When context entries containing a match reach the "middle" // (the position between the pre/post context regions) // of this buffer, and the buffer is full, we will know // enough context to populate the Context properties of the // match. At that point, we will add the match object // to the emit queue. private readonly CircularBuffer _collectedContext; /// /// Initializes a new instance of the class. /// /// How much preContext to collect at most. /// How much postContext to collect at most. public LogicalContextTracker(int preContext, int postContext) { _preContext = preContext; _postContext = postContext; _collectedContext = new CircularBuffer(preContext + postContext + 1); _emitQueue = new List(); } #region IContextTracker implementation public IList EmitQueue => _emitQueue; private readonly List _emitQueue; public void TrackLine(string line) { ContextEntry entry = new(line); _collectedContext.Add(entry); UpdateQueue(); } public void TrackMatch(MatchInfo match) { ContextEntry entry = new(match); _collectedContext.Add(entry); UpdateQueue(); } public void TrackEOF() { // If the buffer is already full, // check for any matches with incomplete // postcontext and add them to the emit queue. // These matches can be identified by being past // the "middle" of the context buffer (still in // the postcontext region. // // If the buffer isn't full, then nothing will have // ever been emitted and everything is still waiting // on postcontext. So process the whole buffer. int startIndex = _collectedContext.IsFull ? _preContext + 1 : 0; EmitAllInRange(startIndex, _collectedContext.Count - 1); } #endregion /// /// Add all matches found in the specified range /// to the emit queue, collecting as much context /// as possible up to the limits specified in the constructor. /// /// /// The range is inclusive; the entries at /// startIndex and endIndex will both be checked. /// /// The beginning of the match range. /// The ending of the match range. private void EmitAllInRange(int startIndex, int endIndex) { for (int i = startIndex; i <= endIndex; i++) { MatchInfo match = _collectedContext[i].Match; if (match != null) { int preStart = Math.Max(i - _preContext, 0); int postLength = Math.Min(_postContext, _collectedContext.Count - i - 1); Emit(match, preStart, i - preStart, i + 1, postLength); } } } /// /// Add match(es) found in the match region to the /// emit queue. Should be called every time an entry /// is added to the context buffer. /// private void UpdateQueue() { // Are we at capacity and thus have enough postcontext? // Is there a match in the "middle" of the buffer // that we know the pre/post context for? // // If this is the first time we've reached full capacity, // hasProcessedPreEntries will not be set, and we // should go through the entire context, because it might // have entries that never collected enough // precontext. Otherwise, we should just look at the // middle region. if (_collectedContext.IsFull) { if (_hasProcessedPreEntries) { // Only process a potential match with exactly // enough pre and post-context. EmitAllInRange(_preContext, _preContext); } else { // Some of our early entries may not // have enough precontext. Process them too. EmitAllInRange(0, _preContext); _hasProcessedPreEntries = true; } } } /// /// Collects context from the specified ranges. Populates /// the specified match with the collected context /// and adds it to the emit queue. /// /// /// Context ranges must be within the bounds of the context buffer. /// /// The match to operate on. /// The start index of the preContext range. /// The length of the preContext range. /// The start index of the postContext range. /// The length of the postContext range. private void Emit(MatchInfo match, int preStartIndex, int preLength, int postStartIndex, int postLength) { if (match.Context != null) { match.Context.PreContext = CopyContext(preStartIndex, preLength); match.Context.PostContext = CopyContext(postStartIndex, postLength); } _emitQueue.Add(match); } /// /// Collects context from the specified ranges. /// /// /// The range must be within the bounds of the context buffer. /// /// The index to start at. /// The length of the range. /// String representation of the collected context at the specified range. private string[] CopyContext(int startIndex, int length) { string[] result = new string[length]; for (int i = 0; i < length; i++) { result[i] = _collectedContext[startIndex + i].ToString(); } return result; } } /// /// A class to track both logical and display contexts. /// private sealed class ContextTracker : IContextTracker { private readonly IContextTracker _displayTracker; private readonly IContextTracker _logicalTracker; /// /// Initializes a new instance of the class. /// /// How much preContext to collect at most. /// How much postContext to collect at most. public ContextTracker(int preContext, int postContext) { _displayTracker = new DisplayContextTracker(preContext, postContext); _logicalTracker = new LogicalContextTracker(preContext, postContext); EmitQueue = new List(); } #region IContextTracker implementation public IList EmitQueue { get; } public void TrackLine(string line) { _displayTracker.TrackLine(line); _logicalTracker.TrackLine(line); UpdateQueue(); } public void TrackMatch(MatchInfo match) { _displayTracker.TrackMatch(match); _logicalTracker.TrackMatch(match); UpdateQueue(); } public void TrackEOF() { _displayTracker.TrackEOF(); _logicalTracker.TrackEOF(); UpdateQueue(); } #endregion /// /// Update the emit queue based on the wrapped trackers. /// private void UpdateQueue() { // Look for completed matches in the logical // tracker's queue. Since the logical tracker // will try to collect as much context as // possible, the display tracker will have either // already finished collecting its context for the // match or will have completed it at the same // time as the logical tracker, so we can // be sure the matches will have both logical // and display context already populated. foreach (MatchInfo match in _logicalTracker.EmitQueue) { EmitQueue.Add(match); } _logicalTracker.EmitQueue.Clear(); _displayTracker.EmitQueue.Clear(); } } /// /// ContextTracker that does not work for the case when pre- and post context is 0. /// private sealed class NoContextTracker : IContextTracker { private readonly IList _matches = new List(1); IList IContextTracker.EmitQueue => _matches; void IContextTracker.TrackLine(string line) { } void IContextTracker.TrackMatch(MatchInfo match) => _matches.Add(match); void IContextTracker.TrackEOF() { } } /// /// Gets or sets a culture name. /// [Parameter] [ValidateSet(typeof(ValidateMatchStringCultureNamesGenerator))] [ValidateNotNull] public string Culture { get { switch (_stringComparison) { case StringComparison.Ordinal: case StringComparison.OrdinalIgnoreCase: { return OrdinalCultureName; } case StringComparison.InvariantCulture: case StringComparison.InvariantCultureIgnoreCase: { return InvariantCultureName; } case StringComparison.CurrentCulture: case StringComparison.CurrentCultureIgnoreCase: { return CurrentCultureName; } default: { break; } } return _cultureName; } set { _cultureName = value; InitCulture(); } } internal const string OrdinalCultureName = "Ordinal"; internal const string InvariantCultureName = "Invariant"; internal const string CurrentCultureName = "Current"; private string _cultureName = CultureInfo.CurrentCulture.Name; private StringComparison _stringComparison = StringComparison.CurrentCultureIgnoreCase; private CompareOptions _compareOptions = CompareOptions.IgnoreCase; private delegate int CultureInfoIndexOf(string source, string value, int startIndex, int count, CompareOptions options); private CultureInfoIndexOf _cultureInfoIndexOf = CultureInfo.CurrentCulture.CompareInfo.IndexOf; private void InitCulture() { _stringComparison = default; switch (_cultureName) { case OrdinalCultureName: { _stringComparison = CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; _compareOptions = CaseSensitive ? CompareOptions.Ordinal : CompareOptions.OrdinalIgnoreCase; _cultureInfoIndexOf = CultureInfo.InvariantCulture.CompareInfo.IndexOf; break; } case InvariantCultureName: { _stringComparison = CaseSensitive ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase; _compareOptions = CaseSensitive ? CompareOptions.None : CompareOptions.IgnoreCase; _cultureInfoIndexOf = CultureInfo.InvariantCulture.CompareInfo.IndexOf; break; } case CurrentCultureName: { _stringComparison = CaseSensitive ? StringComparison.CurrentCulture : StringComparison.CurrentCultureIgnoreCase; _compareOptions = CaseSensitive ? CompareOptions.None : CompareOptions.IgnoreCase; _cultureInfoIndexOf = CultureInfo.CurrentCulture.CompareInfo.IndexOf; break; } default: { var _cultureInfo = CultureInfo.GetCultureInfo(_cultureName); _compareOptions = CaseSensitive ? CompareOptions.None : CompareOptions.IgnoreCase; _cultureInfoIndexOf = _cultureInfo.CompareInfo.IndexOf; break; } } } /// /// Gets or sets the current pipeline object. /// [Parameter(ValueFromPipeline = true, Mandatory = true, ParameterSetName = ParameterSetObject)] [Parameter(ValueFromPipeline = true, Mandatory = true, ParameterSetName = ParameterSetObjectRaw)] [AllowNull] [AllowEmptyString] public PSObject InputObject { get => _inputObject; set => _inputObject = LanguagePrimitives.IsNull(value) ? PSObject.AsPSObject(string.Empty) : value; } private PSObject _inputObject = AutomationNull.Value; /// /// Gets or sets the patterns to find. /// [Parameter(Mandatory = true, Position = 0)] public string[] Pattern { get; set; } private Regex[] _regexPattern; /// /// Gets or sets files to read from. /// Globbing is done on these. /// [Parameter(Position = 1, Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = ParameterSetFile)] [Parameter(Position = 1, Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = ParameterSetFileRaw)] [FileinfoToString] public string[] Path { get; set; } /// /// Gets or sets literal files to read from. /// Globbing is not done on these. /// [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = ParameterSetLiteralFile)] [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = ParameterSetLiteralFileRaw)] [FileinfoToString] [Alias("PSPath", "LP")] public string[] LiteralPath { get => Path; set { Path = value; _isLiteralPath = true; } } private bool _isLiteralPath; /// /// Gets or sets a value indicating if only string values containing matched lines should be returned. /// If not (default) return MatchInfo (or bool objects, when Quiet is passed). /// [Parameter(Mandatory = true, ParameterSetName = ParameterSetObjectRaw)] [Parameter(Mandatory = true, ParameterSetName = ParameterSetFileRaw)] [Parameter(Mandatory = true, ParameterSetName = ParameterSetLiteralFileRaw)] public SwitchParameter Raw { get; set; } /// /// Gets or sets a value indicating if a pattern string should be matched literally. /// If not (default) search using pattern as a Regular Expression. /// [Parameter] public SwitchParameter SimpleMatch { get; set; } /// /// Gets or sets a value indicating if the search is case sensitive.If true, then do case-sensitive searches. /// [Parameter] public SwitchParameter CaseSensitive { get; set; } /// /// Gets or sets a value indicating if the cmdlet will stop processing at the first successful match and /// return true. If both List and Quiet parameters are given, an exception is thrown. /// [Parameter(ParameterSetName = ParameterSetObject)] [Parameter(ParameterSetName = ParameterSetFile)] [Parameter(ParameterSetName = ParameterSetLiteralFile)] public SwitchParameter Quiet { get; set; } /// /// Gets or sets a value indicating if matching files should be listed. /// This is the Unix functionality this switch is intended to mimic; /// the actual action of this option is to stop after the first match /// is found and returned from any particular file. /// [Parameter] public SwitchParameter List { get; set; } /// /// Gets or sets a value indicating if highlighting should be disabled. /// [Parameter] public SwitchParameter NoEmphasis { get; set; } /// /// Gets or sets files to include. Files matching /// one of these (if specified) are included. /// /// Invalid wildcard pattern was specified. [Parameter] [ValidateNotNullOrEmpty] public string[] Include { get => _includeStrings; set { // null check is not needed (because of ValidateNotNullOrEmpty), // but we have to include it to silence OACR _includeStrings = value ?? throw PSTraceSource.NewArgumentNullException(nameof(value)); _include = new WildcardPattern[_includeStrings.Length]; for (int i = 0; i < _includeStrings.Length; i++) { _include[i] = WildcardPattern.Get(_includeStrings[i], WildcardOptions.IgnoreCase); } } } private string[] _includeStrings; private WildcardPattern[] _include; /// /// Gets or sets files to exclude. Files matching /// one of these (if specified) are excluded. /// [Parameter] [ValidateNotNullOrEmpty] public string[] Exclude { get => _excludeStrings; set { // null check is not needed (because of ValidateNotNullOrEmpty), // but we have to include it to silence OACR _excludeStrings = value ?? throw PSTraceSource.NewArgumentNullException("value"); _exclude = new WildcardPattern[_excludeStrings.Length]; for (int i = 0; i < _excludeStrings.Length; i++) { _exclude[i] = WildcardPattern.Get(_excludeStrings[i], WildcardOptions.IgnoreCase); } } } private string[] _excludeStrings; private WildcardPattern[] _exclude; /// /// Gets or sets a value indicating whether to only show lines which do not match. /// Equivalent to grep -v/findstr -v. /// [Parameter] public SwitchParameter NotMatch { get; set; } /// /// Gets or sets a value indicating whether the Matches property of MatchInfo should be set /// to the result of calling System.Text.RegularExpressions.Regex.Matches() on /// the corresponding line. /// Has no effect if -SimpleMatch is also specified. /// [Parameter] public SwitchParameter AllMatches { get; set; } /// /// Gets or sets the text encoding to process each file as. /// [Parameter] [ArgumentToEncodingTransformationAttribute()] [ArgumentEncodingCompletionsAttribute] [ValidateNotNullOrEmpty] public Encoding Encoding { get { return _encoding; } set { EncodingConversion.WarnIfObsolete(this, value); _encoding = value; } } private Encoding _encoding = ClrFacade.GetDefaultEncoding(); /// /// Gets or sets the number of context lines to collect. If set to a /// single integer value N, collects N lines each of pre- /// and post- context. If set to a 2-tuple B,A, collects B /// lines of pre- and A lines of post- context. /// If set to a list with more than 2 elements, the /// excess elements are ignored. /// [Parameter] [ValidateNotNullOrEmpty] [ValidateCount(1, 2)] [ValidateRange(0, Int32.MaxValue)] public new int[] Context { get => _context; set { // null check is not needed (because of ValidateNotNullOrEmpty), // but we have to include it to silence OACR _context = value ?? throw PSTraceSource.NewArgumentNullException("value"); if (_context.Length == 1) { _preContext = _context[0]; _postContext = _context[0]; } else if (_context.Length >= 2) { _preContext = _context[0]; _postContext = _context[1]; } } } private int[] _context; private int _preContext = 0; private int _postContext = 0; // When we are in Raw mode or pre- and postcontext are zero, use the _noContextTracker, since we will not be needing trackedLines. private IContextTracker GetContextTracker() => (Raw || (_preContext == 0 && _postContext == 0)) ? _noContextTracker : new ContextTracker(_preContext, _postContext); // This context tracker is only used for strings which are piped // directly into the cmdlet. File processing doesn't need // to track state between calls to ProcessRecord, and so // allocates its own tracker. The reason we can't // use a single global tracker for both is that in the case of // a mixed list of strings and FileInfo, the context tracker // would get reset after each file. private IContextTracker _globalContextTracker; private IContextTracker _noContextTracker; /// /// This is used to handle the case were we're done processing input objects. /// If true, process record will just return. /// private bool _doneProcessing; private int _inputRecordNumber; /// /// Read command line parameters. /// protected override void BeginProcessing() { if (this.MyInvocation.BoundParameters.ContainsKey(nameof(Culture)) && !this.MyInvocation.BoundParameters.ContainsKey(nameof(SimpleMatch))) { InvalidOperationException exception = new(MatchStringStrings.CannotSpecifyCultureWithoutSimpleMatch); ErrorRecord errorRecord = new(exception, "CannotSpecifyCultureWithoutSimpleMatch", ErrorCategory.InvalidData, null); this.ThrowTerminatingError(errorRecord); } InitCulture(); string suppressVt = Environment.GetEnvironmentVariable("__SuppressAnsiEscapeSequences"); if (!string.IsNullOrEmpty(suppressVt)) { NoEmphasis = true; } if (!SimpleMatch) { RegexOptions regexOptions = CaseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase; _regexPattern = new Regex[Pattern.Length]; for (int i = 0; i < Pattern.Length; i++) { try { _regexPattern[i] = new Regex(Pattern[i], regexOptions); } catch (Exception e) { this.ThrowTerminatingError(BuildErrorRecord(MatchStringStrings.InvalidRegex, Pattern[i], e.Message, "InvalidRegex", e)); throw; } } } _noContextTracker = new NoContextTracker(); _globalContextTracker = GetContextTracker(); } private readonly List _inputObjectFileList = new(1) { string.Empty }; /// /// Process the input. /// /// Regular expression parsing error, path error. /// A file cannot be found. /// A file cannot be found. protected override void ProcessRecord() { if (_doneProcessing) { return; } // We may only have directories when we have resolved wildcards var expandedPathsMaybeDirectory = false; List expandedPaths = null; if (Path != null) { expandedPaths = ResolveFilePaths(Path, _isLiteralPath); if (expandedPaths == null) { return; } expandedPathsMaybeDirectory = true; } else { if (_inputObject.BaseObject is FileInfo fileInfo) { _inputObjectFileList[0] = fileInfo.FullName; expandedPaths = _inputObjectFileList; } } if (expandedPaths != null) { foreach (var filename in expandedPaths) { if (expandedPathsMaybeDirectory && Directory.Exists(filename)) { continue; } var foundMatch = ProcessFile(filename); if (Quiet && foundMatch) { return; } } // No results in any files. if (Quiet) { var res = List ? null : Boxed.False; WriteObject(res); } } else { // Set the line number in the matched object to be the record number _inputRecordNumber++; bool matched; MatchInfo result; MatchInfo matchInfo = null; if (_inputObject.BaseObject is string line) { matched = DoMatch(line, out result); } else { matchInfo = _inputObject.BaseObject as MatchInfo; object objectToCheck = matchInfo ?? (object)_inputObject; matched = DoMatch(objectToCheck, out result, out line); } if (matched) { // Don't re-write the line number if it was already set... if (matchInfo == null) { result.LineNumber = _inputRecordNumber; } // doMatch will have already set the pattern and line text... _globalContextTracker.TrackMatch(result); } else { _globalContextTracker.TrackLine(line); } // Emit any queued up objects... if (FlushTrackerQueue(_globalContextTracker)) { // If we're in quiet mode, go ahead and stop processing // now. if (Quiet) { _doneProcessing = true; } } } } /// /// Process a file which was either specified on the /// command line or passed in as a FileInfo object. /// /// The file to process. /// True if a match was found; otherwise false. private bool ProcessFile(string filename) { var contextTracker = GetContextTracker(); bool foundMatch = false; // Read the file one line at a time... try { // see if the file is one the include exclude list... if (!MeetsIncludeExcludeCriteria(filename)) { return false; } using (FileStream fs = new(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { using (StreamReader sr = new(fs, Encoding)) { string line; int lineNo = 0; // Read and display lines from the file until the end of // the file is reached. while ((line = sr.ReadLine()) != null) { lineNo++; if (DoMatch(line, out MatchInfo result)) { result.Path = filename; result.LineNumber = lineNo; contextTracker.TrackMatch(result); } else { contextTracker.TrackLine(line); } // Flush queue of matches to emit. if (contextTracker.EmitQueue.Count > 0) { foundMatch = true; // If -list or -quiet was specified, we only want to emit the first match // for each file so record the object to emit and stop processing // this file. It's done this way so the file is closed before emitting // the result so the downstream cmdlet can actually manipulate the file // that was found. if (Quiet || List) { break; } FlushTrackerQueue(contextTracker); } } } } // Check for any remaining matches. This could be caused // by breaking out of the loop early for quiet or list // mode, or by reaching EOF before we collected all // our postcontext. contextTracker.TrackEOF(); if (FlushTrackerQueue(contextTracker)) { foundMatch = true; } } catch (System.NotSupportedException nse) { WriteError(BuildErrorRecord(MatchStringStrings.FileReadError, filename, nse.Message, "ProcessingFile", nse)); } catch (System.IO.IOException ioe) { WriteError(BuildErrorRecord(MatchStringStrings.FileReadError, filename, ioe.Message, "ProcessingFile", ioe)); } catch (System.Security.SecurityException se) { WriteError(BuildErrorRecord(MatchStringStrings.FileReadError, filename, se.Message, "ProcessingFile", se)); } catch (System.UnauthorizedAccessException uae) { WriteError(BuildErrorRecord(MatchStringStrings.FileReadError, filename, uae.Message, "ProcessingFile", uae)); } return foundMatch; } /// /// Emit any objects which have been queued up, and clear the queue. /// /// The context tracker to operate on. /// Whether or not any objects were emitted. private bool FlushTrackerQueue(IContextTracker contextTracker) { // Do we even have any matches to emit? if (contextTracker.EmitQueue.Count < 1) { return false; } if (Raw) { foreach (MatchInfo match in contextTracker.EmitQueue) { WriteObject(match.Line); } } else if (Quiet && !List) { WriteObject(true); } else if (List) { WriteObject(contextTracker.EmitQueue[0]); } else { foreach (MatchInfo match in contextTracker.EmitQueue) { WriteObject(match); } } contextTracker.EmitQueue.Clear(); return true; } /// /// Complete processing. Emits any objects which have been queued up /// due to -context tracking. /// protected override void EndProcessing() { // Check for a leftover match that was still tracking context. _globalContextTracker.TrackEOF(); if (!_doneProcessing) { FlushTrackerQueue(_globalContextTracker); } } private bool DoMatch(string operandString, out MatchInfo matchResult) { return DoMatchWorker(operandString, null, out matchResult); } private bool DoMatch(object operand, out MatchInfo matchResult, out string operandString) { MatchInfo matchInfo = operand as MatchInfo; if (matchInfo != null) { // We're operating in filter mode. Match // against the provided MatchInfo's line. // If the user has specified context tracking, // inform them that it is not allowed in filter // mode and disable it. Also, reset the global // context tracker used for processing pipeline // objects to use the new settings. operandString = matchInfo.Line; if (_preContext > 0 || _postContext > 0) { _preContext = 0; _postContext = 0; _globalContextTracker = new ContextTracker(_preContext, _postContext); WarnFilterContext(); } } else { operandString = (string)LanguagePrimitives.ConvertTo(operand, typeof(string), CultureInfo.InvariantCulture); } return DoMatchWorker(operandString, matchInfo, out matchResult); } /// /// Check the operand and see if it matches, if this.quiet is not set, then /// return a partially populated MatchInfo object with Line, Pattern, IgnoreCase set. /// /// The result of converting operand to a string. /// The input object in filter mode. /// The match info object - this will be null if this.quiet is set. /// True if the input object matched. private bool DoMatchWorker(string operandString, MatchInfo matchInfo, out MatchInfo matchResult) { bool gotMatch = false; Match[] matches = null; int patternIndex = 0; matchResult = null; List indexes = null; List lengths = null; bool shouldEmphasize = !NoEmphasis && Host.UI.SupportsVirtualTerminal; // If Emphasize is set and VT is supported, // the lengths and starting indexes of regex matches // need to be passed in to the matchInfo object. if (shouldEmphasize) { indexes = new List(); lengths = new List(); } if (!SimpleMatch) { while (patternIndex < Pattern.Length) { Regex r = _regexPattern[patternIndex]; // Only honor allMatches if notMatch is not set, // since it's a fairly expensive operation and // notMatch takes precedent over allMatch. if (AllMatches && !NotMatch) { MatchCollection mc = r.Matches(operandString); if (mc.Count > 0) { matches = new Match[mc.Count]; ((ICollection)mc).CopyTo(matches, 0); if (shouldEmphasize) { foreach (Match match in matches) { indexes.Add(match.Index); lengths.Add(match.Length); } } gotMatch = true; } } else { Match match = r.Match(operandString); gotMatch = match.Success; if (match.Success) { if (shouldEmphasize) { indexes.Add(match.Index); lengths.Add(match.Length); } matches = new Match[] { match }; } } if (gotMatch) { break; } patternIndex++; } } else { while (patternIndex < Pattern.Length) { string pat = Pattern[patternIndex]; int index = _cultureInfoIndexOf(operandString, pat, 0, operandString.Length, _compareOptions); if (index >= 0) { if (shouldEmphasize) { indexes.Add(index); lengths.Add(pat.Length); } gotMatch = true; break; } patternIndex++; } } if (NotMatch) { gotMatch = !gotMatch; // If notMatch was specified with multiple // patterns, then *none* of the patterns // matched and any pattern could be picked // to report in MatchInfo. However, that also // means that patternIndex will have been // incremented past the end of the pattern array. // So reset it to select the first pattern. patternIndex = 0; } if (gotMatch) { // if we were passed a MatchInfo object as the operand, // we're operating in filter mode. if (matchInfo != null) { // If the original MatchInfo was tracking context, // we need to copy it and disable display context, // since we can't guarantee it will be displayed // correctly when filtered. if (matchInfo.Context != null) { matchResult = matchInfo.Clone(); matchResult.Context.DisplayPreContext = Array.Empty(); matchResult.Context.DisplayPostContext = Array.Empty(); } else { // Otherwise, just pass the object as is. matchResult = matchInfo; } return true; } // otherwise construct and populate a new MatchInfo object matchResult = shouldEmphasize ? new MatchInfo(indexes, lengths) : new MatchInfo(); matchResult.IgnoreCase = !CaseSensitive; matchResult.Line = operandString; matchResult.Pattern = Pattern[patternIndex]; if (_preContext > 0 || _postContext > 0) { matchResult.Context = new MatchInfoContext(); } // Matches should be an empty list, rather than null, // in the cases of notMatch and simpleMatch. matchResult.Matches = matches ?? Array.Empty(); return true; } return false; } /// /// Get a list or resolved file paths. /// /// The filePaths to resolve. /// True if the wildcard resolution should not be attempted. /// The resolved (absolute) paths. private List ResolveFilePaths(string[] filePaths, bool isLiteralPath) { List allPaths = new(); foreach (string path in filePaths) { Collection resolvedPaths; ProviderInfo provider; if (isLiteralPath) { resolvedPaths = new Collection(); string resolvedPath = SessionState.Path.GetUnresolvedProviderPathFromPSPath(path, out provider, out _); resolvedPaths.Add(resolvedPath); } else { resolvedPaths = GetResolvedProviderPathFromPSPath(path, out provider); } if (!provider.NameEquals(base.Context.ProviderNames.FileSystem)) { // "The current provider ({0}) cannot open a file" WriteError(BuildErrorRecord(MatchStringStrings.FileOpenError, provider.FullName, "ProcessingFile", null)); continue; } allPaths.AddRange(resolvedPaths); } return allPaths; } private static ErrorRecord BuildErrorRecord(string messageId, string argument, string errorId, Exception innerException) { return BuildErrorRecord(messageId, new object[] { argument }, errorId, innerException); } private static ErrorRecord BuildErrorRecord(string messageId, string arg0, string arg1, string errorId, Exception innerException) { return BuildErrorRecord(messageId, new object[] { arg0, arg1 }, errorId, innerException); } private static ErrorRecord BuildErrorRecord(string messageId, object[] arguments, string errorId, Exception innerException) { string fmtedMsg = StringUtil.Format(messageId, arguments); ArgumentException e = new(fmtedMsg, innerException); return new ErrorRecord(e, errorId, ErrorCategory.InvalidArgument, null); } private void WarnFilterContext() { string msg = MatchStringStrings.FilterContextWarning; WriteWarning(msg); } /// /// Magic class that works around the limitations on ToString() for FileInfo. /// private sealed class FileinfoToStringAttribute : ArgumentTransformationAttribute { public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) { object result = inputData; if (result is PSObject mso) { result = mso.BaseObject; } FileInfo fileInfo; // Handle an array of elements... if (result is IList argList) { object[] resultList = new object[argList.Count]; for (int i = 0; i < argList.Count; i++) { object element = argList[i]; mso = element as PSObject; if (mso != null) { element = mso.BaseObject; } fileInfo = element as FileInfo; resultList[i] = fileInfo?.FullName ?? element; } return resultList; } // Handle the singleton case... fileInfo = result as FileInfo; if (fileInfo != null) { return fileInfo.FullName; } return inputData; } } /// /// Check whether the supplied name meets the include/exclude criteria. /// That is - it's on the include list if there is one and not on /// the exclude list if there was one of those. /// /// The filename to test. /// True if the filename is acceptable. private bool MeetsIncludeExcludeCriteria(string filename) { bool ok = false; // see if the file is on the include list... if (_include != null) { foreach (WildcardPattern patternItem in _include) { if (patternItem.IsMatch(filename)) { ok = true; break; } } } else { ok = true; } if (!ok) { return false; } // now see if it's on the exclude list... if (_exclude != null) { foreach (WildcardPattern patternItem in _exclude) { if (patternItem.IsMatch(filename)) { ok = false; break; } } } return ok; } } /// /// Get list of valid culture names for ValidateSet attribute. /// public class ValidateMatchStringCultureNamesGenerator : IValidateSetValuesGenerator { string[] IValidateSetValuesGenerator.GetValidValues() { var cultures = CultureInfo.GetCultures(CultureTypes.AllCultures); var result = new List(cultures.Length + 3); result.Add(SelectStringCommand.OrdinalCultureName); result.Add(SelectStringCommand.InvariantCultureName); result.Add(SelectStringCommand.CurrentCultureName); foreach (var cultureInfo in cultures) { result.Add(cultureInfo.Name); } return result.ToArray(); } } }