GeezerVison is showing github.com — Add cmdlet 'Join-String' for creating text from pipeline input (#7660) · PowerShell/PowerShell@877b9a9
File treeExpand file treeCollapse file tree Open diff view settings Microsoft.PowerShell.Commands.Utility/commands/utility Unix/Microsoft.PowerShell.Utility Windows/Microsoft.PowerShell.Utility
System.Management.Automation/engine
Modules/Microsoft.PowerShell.Utility
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboard +226Lines changed: 226 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +// Copyright (c) Microsoft Corporation. All rights reserved. | | 2 | +// Licensed under the MIT License. | | 3 | + | | 4 | +using System; | | 5 | +using System.Collections; | | 6 | +using System.Collections.Generic; | | 7 | +using System.Globalization; | | 8 | +using System.Linq; | | 9 | +using System.Management.Automation; | | 10 | +using System.Management.Automation.Internal; | | 11 | +using System.Management.Automation.Language; | | 12 | +using System.Text; | | 13 | + | | 14 | +namespace Microsoft.PowerShell.Commands.Utility | | 15 | +{ | | 16 | + /// <summary> | | 17 | + /// Join-Object implementation. | | 18 | + /// </summary> | | 19 | + [Cmdlet(VerbsCommon.Join, "String", RemotingCapability = RemotingCapability.None, DefaultParameterSetName = "default")] | | 20 | + [OutputType(typeof(string))] | | 21 | + public sealed class JoinStringCommand : PSCmdlet | | 22 | + { | | 23 | + /// <summary>A bigger default to not get re-allocations in common use cases.</summary> | | 24 | + private const int DefaultOutputStringCapacity = 256; | | 25 | + private readonly StringBuilder _outputBuilder = new StringBuilder(DefaultOutputStringCapacity); | | 26 | + private CultureInfo _cultureInfo = CultureInfo.InvariantCulture; | | 27 | + private string _separator; | | 28 | + private char _quoteChar; | | 29 | + private bool _firstInputObject = true; | | 30 | + | | 31 | + /// <summary> | | 32 | + /// Gets or sets the property name or script block to use as the value to join. | | 33 | + /// </summary> | | 34 | + [Parameter(Position = 0)] | | 35 | + [ArgumentCompleter(typeof(PropertyNameCompleter))] | | 36 | + public PSPropertyExpression Property { get; set; } | | 37 | + | | 38 | + /// <summary> | | 39 | + /// Gets or sets the delimiter to join the output with. | | 40 | + /// </summary> | | 41 | + [Parameter(Position = 1)] | | 42 | + [ArgumentCompleter(typeof(JoinItemCompleter))] | | 43 | + [AllowEmptyString] | | 44 | + public string Separator | | 45 | + { | | 46 | + get => _separator ?? LanguagePrimitives.ConvertTo<string>(GetVariableValue("OFS")); | | 47 | + set => _separator = value; | | 48 | + } | | 49 | + | | 50 | + /// <summary> | | 51 | + /// Gets or sets text to include before the joined input text. | | 52 | + /// </summary> | | 53 | + [Parameter] | | 54 | + [Alias("op")] | | 55 | + public string OutputPrefix { get; set; } | | 56 | + | | 57 | + /// <summary> | | 58 | + /// Gets or sets text to include after the joined input text. | | 59 | + /// </summary> | | 60 | + [Parameter] | | 61 | + [Alias("os")] | | 62 | + public string OutputSuffix { get; set; } | | 63 | + | | 64 | + /// <summary> | | 65 | + /// Gets or sets if the output items should we wrapped in single quotes. | | 66 | + /// </summary> | | 67 | + [Parameter(ParameterSetName = "SingleQuote")] | | 68 | + public SwitchParameter SingleQuote { get; set; } | | 69 | + | | 70 | + /// <summary> | | 71 | + /// Gets or sets if the output items should we wrapped in double quotes. | | 72 | + /// </summary> | | 73 | + [Parameter(ParameterSetName = "DoubleQuote")] | | 74 | + public SwitchParameter DoubleQuote { get; set; } | | 75 | + | | 76 | + /// <summary> | | 77 | + /// Gets or sets a format string that is applied to each input object. | | 78 | + /// </summary> | | 79 | + [Parameter(ParameterSetName = "Format")] | | 80 | + [ArgumentCompleter(typeof(JoinItemCompleter))] | | 81 | + public string FormatString { get; set; } | | 82 | + | | 83 | + /// <summary> | | 84 | + /// Gets or sets if the current culture should be used with formatting instead of the invariant culture. | | 85 | + /// </summary> | | 86 | + [Parameter] | | 87 | + public SwitchParameter UseCulture { get; set; } | | 88 | + | | 89 | + /// <summary> | | 90 | + /// Gets or sets the input object to join into text. | | 91 | + /// </summary> | | 92 | + [Parameter(ValueFromPipeline = true)] | | 93 | + public PSObject InputObject { get; set; } | | 94 | + | | 95 | + /// <inheritdoc /> | | 96 | + protected override void BeginProcessing() | | 97 | + { | | 98 | + _quoteChar = SingleQuote ? '\'' : DoubleQuote ? '"' : char.MinValue; | | 99 | + _outputBuilder.Append(OutputPrefix); | | 100 | + if (UseCulture) | | 101 | + { | | 102 | + _cultureInfo = CultureInfo.CurrentCulture; | | 103 | + } | | 104 | + } | | 105 | + | | 106 | + /// <inheritdoc /> | | 107 | + protected override void ProcessRecord() | | 108 | + { | | 109 | + if (InputObject != null && InputObject != AutomationNull.Value) | | 110 | + { | | 111 | + var inputValue = Property == null | | 112 | + ? InputObject | | 113 | + : Property.GetValues(InputObject, false, true).FirstOrDefault()?.Result; | | 114 | + | | 115 | + // conversion to string always succeeds. | | 116 | + if (!LanguagePrimitives.TryConvertTo<string>(inputValue, _cultureInfo, out var stringValue)) | | 117 | + { | | 118 | + throw new PSInvalidCastException("InvalidCastFromAnyTypeToString", ExtendedTypeSystem.InvalidCastCannotRetrieveString, null); | | 119 | + } | | 120 | + | | 121 | + if (_firstInputObject) | | 122 | + { | | 123 | + _firstInputObject = false; | | 124 | + } | | 125 | + else | | 126 | + { | | 127 | + _outputBuilder.Append(Separator); | | 128 | + } | | 129 | + | | 130 | + if (_quoteChar != char.MinValue) | | 131 | + { | | 132 | + _outputBuilder.Append(_quoteChar); | | 133 | + _outputBuilder.Append(stringValue); | | 134 | + _outputBuilder.Append(_quoteChar); | | 135 | + } | | 136 | + else if (string.IsNullOrEmpty(FormatString)) | | 137 | + { | | 138 | + _outputBuilder.Append(stringValue); | | 139 | + } | | 140 | + else | | 141 | + { | | 142 | + _outputBuilder.AppendFormat(_cultureInfo, FormatString, stringValue); | | 143 | + } | | 144 | + } | | 145 | + } | | 146 | + | | 147 | + /// <inheritdoc /> | | 148 | + protected override void EndProcessing() | | 149 | + { | | 150 | + _outputBuilder.Append(OutputSuffix); | | 151 | + WriteObject(_outputBuilder.ToString()); | | 152 | + } | | 153 | + } | | 154 | + | | 155 | + internal class JoinItemCompleter : IArgumentCompleter | | 156 | + { | | 157 | + public IEnumerable<CompletionResult> CompleteArgument( | | 158 | + string commandName, | | 159 | + string parameterName, | | 160 | + string wordToComplete, | | 161 | + CommandAst commandAst, | | 162 | + IDictionary fakeBoundParameters) | | 163 | + { | | 164 | + switch (parameterName) | | 165 | + { | | 166 | + case "Separator": return CompleteSeparator(wordToComplete); | | 167 | + case "FormatString": return CompleteFormatString(wordToComplete); | | 168 | + } | | 169 | + | | 170 | + return null; | | 171 | + } | | 172 | + | | 173 | + private IEnumerable<CompletionResult> CompleteFormatString(string wordToComplete) | | 174 | + { | | 175 | + var res = new List<CompletionResult>(); | | 176 | + void AddMatching(string completionText) | | 177 | + { | | 178 | + if (completionText.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) | | 179 | + { | | 180 | + res.Add(new CompletionResult(completionText)); | | 181 | + } | | 182 | + } | | 183 | + | | 184 | + AddMatching("'[{0}]'"); | | 185 | + AddMatching("'{0:N2}'"); | | 186 | + AddMatching("\"`r`n `${0}\""); | | 187 | + AddMatching("\"`r`n [string] `${0}\""); | | 188 | + | | 189 | + return res; | | 190 | + } | | 191 | + | | 192 | + private IEnumerable<CompletionResult> CompleteSeparator(string wordToComplete) | | 193 | + { | | 194 | + var res = new List<CompletionResult>(10); | | 195 | + | | 196 | + void AddMatching(string completionText, string listText, string toolTip) | | 197 | + { | | 198 | + if (completionText.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) | | 199 | + { | | 200 | + res.Add(new CompletionResult(completionText, listText, CompletionResultType.ParameterValue, toolTip)); | | 201 | + } | | 202 | + } | | 203 | + | | 204 | + AddMatching("', '", "Comma-Space", "', ' - Comma-Space"); | | 205 | + AddMatching("';'", "Semi-Colon", "';' - Semi-Colon "); | | 206 | + AddMatching("'; '", "Semi-Colon-Space", "'; ' - Semi-Colon-Space"); | | 207 | + AddMatching($"\"{NewLineText}\"", "Newline", $"{NewLineText} - Newline"); | | 208 | + AddMatching("','", "Comma", "',' - Comma"); | | 209 | + AddMatching("'-'", "Dash", "'-' - Dash"); | | 210 | + AddMatching("' '", "Space", "' ' - Space"); | | 211 | + return res; | | 212 | + } | | 213 | + | | 214 | + public string NewLineText | | 215 | + { | | 216 | + get | | 217 | + { | | 218 | +#if UNIX | | 219 | + return "`n"; | | 220 | +#else | | 221 | + return "`r`n"; | | 222 | +#endif | | 223 | + } | | 224 | + } | | 225 | + } | | 226 | +} |
Collapse file Copy file name to clipboardExpand all lines: src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -14,7 +14,7 @@ CmdletsToExport= "Format-List", "Format-Custom", "Format-Table", "Format-Wide", | 14 | 14 | "Export-Csv", "Import-Csv", "ConvertTo-Csv", "ConvertFrom-Csv", "Export-Alias", "Invoke-Expression", | 15 | 15 | "Get-Alias", "Get-Culture", "Get-Date", "Get-Host", "Get-Member", "Get-Random", "Get-UICulture", | 16 | 16 | "Get-Unique", "Export-PSSession", "Import-PSSession", "Import-Alias", "Import-LocalizedData", | 17 | | - "Select-String", "Measure-Object", "New-Alias", "New-TimeSpan", "Read-Host", "Set-Alias", "Set-Date", | | 17 | + "Join-String", "Select-String", "Measure-Object", "New-Alias", "New-TimeSpan", "Read-Host", "Set-Alias", "Set-Date", | 18 | 18 | "Start-Sleep", "Tee-Object", "Measure-Command", "Update-TypeData", "Update-FormatData", | 19 | 19 | "Remove-TypeData", "Get-TypeData", "Write-Host", "Write-Progress", "New-Object", "Select-Object", | 20 | 20 | "Group-Object", "Sort-Object", "Get-Variable", "New-Variable", "Set-Variable", "Remove-Variable", | |
Collapse file Copy file name to clipboardExpand all lines: src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -14,7 +14,7 @@ CmdletsToExport= "Format-List", "Format-Custom", "Format-Table", "Format-Wide", | 14 | 14 | "Export-Csv", "Import-Csv", "ConvertTo-Csv", "ConvertFrom-Csv", "Export-Alias", "Invoke-Expression", | 15 | 15 | "Get-Alias", "Get-Culture", "Get-Date", "Get-Host", "Get-Member", "Get-Random", "Get-UICulture", | 16 | 16 | "Get-Unique", "Export-PSSession", "Import-PSSession", "Import-Alias", "Import-LocalizedData", | 17 | | - "Select-String", "Measure-Object", "New-Alias", "New-TimeSpan", "Read-Host", "Set-Alias", "Set-Date", | | 17 | + "Join-String", "Select-String", "Measure-Object", "New-Alias", "New-TimeSpan", "Read-Host", "Set-Alias", "Set-Date", | 18 | 18 | "Start-Sleep", "Tee-Object", "Measure-Command", "Update-TypeData", "Update-FormatData", | 19 | 19 | "Remove-TypeData", "Get-TypeData", "Write-Host", "Write-Progress", "New-Object", "Select-Object", | 20 | 20 | "Group-Object", "Sort-Object", "Get-Variable", "New-Variable", "Set-Variable", "Remove-Variable", | |
Collapse file Copy file name to clipboardExpand all lines: src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +83-7Lines changed: 83 additions & 7 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -3746,7 +3746,7 @@ private static void NativeCompletionMemberName(CompletionContext context, List<C | 3746 | 3746 | prevType = AstTypeInference.InferTypeOf(pipelineAst.PipelineElements[i - 1], context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); | 3747 | 3747 | } | 3748 | 3748 |
| 3749 | | - CompleteMemberByInferredType(context, prevType, result, context.WordToComplete + "*", filter: IsPropertyMember, isStatic: false); | | 3749 | + CompleteMemberByInferredType(context.TypeInferenceContext, prevType, result, context.WordToComplete + "*", filter: IsPropertyMember, isStatic: false); | 3750 | 3750 | result.Add(CompletionResult.Null); | 3751 | 3751 | } | 3752 | 3752 |
| @@ -5027,7 +5027,7 @@ internal static List<CompletionResult> CompleteMember(CompletionContext context, | 5027 | 5027 | if (inferredTypes != null && inferredTypes.Length > 0) | 5028 | 5028 | { | 5029 | 5029 | // Use inferred types if we have any | 5030 | | - CompleteMemberByInferredType(context, inferredTypes, results, memberName, filter: null, isStatic: @static); | | 5030 | + CompleteMemberByInferredType(context.TypeInferenceContext, inferredTypes, results, memberName, filter: null, isStatic: @static); | 5031 | 5031 | } | 5032 | 5032 | else | 5033 | 5033 | { | @@ -5131,7 +5131,7 @@ private static bool IsInDscContext(ExpressionAst expression) | 5131 | 5131 | return Ast.GetAncestorAst<ConfigurationDefinitionAst>(expression) != null; | 5132 | 5132 | } | 5133 | 5133 |
| 5134 | | - private static void CompleteMemberByInferredType(CompletionContext context, IEnumerable<PSTypeName> inferredTypes, List<CompletionResult> results, string memberName, Func<object, bool> filter, bool isStatic) | | 5134 | + internal static void CompleteMemberByInferredType(TypeInferenceContext context, IEnumerable<PSTypeName> inferredTypes, List<CompletionResult> results, string memberName, Func<object, bool> filter, bool isStatic) | 5135 | 5135 | { | 5136 | 5136 | bool extensionMethodsAdded = false; | 5137 | 5137 | HashSet<string> typeNameUsed = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | @@ -5142,8 +5142,9 @@ private static void CompleteMemberByInferredType(CompletionContext context, IEnu | 5142 | 5142 | { | 5143 | 5143 | continue; | 5144 | 5144 | } | | 5145 | + | 5145 | 5146 | typeNameUsed.Add(psTypeName.Name); | 5146 | | - var members = context.TypeInferenceContext.GetMembersByInferredType(psTypeName, isStatic, filter); | | 5147 | + var members = context.GetMembersByInferredType(psTypeName, isStatic, filter); | 5147 | 5148 | foreach (var member in members) | 5148 | 5149 | { | 5149 | 5150 | AddInferredMember(member, memberNamePattern, results); | @@ -5276,7 +5277,7 @@ private static bool IsWriteablePropertyMember(object member) | 5276 | 5277 | return false; | 5277 | 5278 | } | 5278 | 5279 |
| 5279 | | - private static bool IsPropertyMember(object member) | | 5280 | + internal static bool IsPropertyMember(object member) | 5280 | 5281 | { | 5281 | 5282 | return member is PropertyInfo | 5282 | 5283 | || member is FieldInfo | @@ -6154,7 +6155,7 @@ internal static List<CompletionResult> CompleteHashtableKey(CompletionContext co | 6154 | 6155 | { | 6155 | 6156 | var result = new List<CompletionResult>(); | 6156 | 6157 | CompleteMemberByInferredType( | 6157 | | - completionContext, AstTypeInference.InferTypeOf(typeAst, completionContext.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval), | | 6158 | + completionContext.TypeInferenceContext, AstTypeInference.InferTypeOf(typeAst, completionContext.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval), | 6158 | 6159 | result, completionContext.WordToComplete + "*", IsWriteablePropertyMember, isStatic: false); | 6159 | 6160 | return result; | 6160 | 6161 | } | @@ -6265,7 +6266,7 @@ internal static List<CompletionResult> CompleteHashtableKey(CompletionContext co | 6265 | 6266 | var inferredType = AstTypeInference.InferTypeOf(commandAst, completionContext.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); | 6266 | 6267 | var result = new List<CompletionResult>(); | 6267 | 6268 | CompleteMemberByInferredType( | 6268 | | - completionContext, inferredType, | | 6269 | + completionContext.TypeInferenceContext, inferredType, | 6269 | 6270 | result, completionContext.WordToComplete + "*", IsWriteablePropertyMember, isStatic: false); | 6270 | 6271 | return result; | 6271 | 6272 | case "Select-Object": | @@ -6910,4 +6911,79 @@ public object VisitParenExpression(ParenExpressionAst parenExpressionAst) | 6910 | 6911 | return parenExpressionAst.Pipeline.Accept(this); | 6911 | 6912 | } | 6912 | 6913 | } | | 6914 | + | | 6915 | + /// <summary> | | 6916 | + /// Completes with the property names of the InputObject. | | 6917 | + /// </summary> | | 6918 | + internal class PropertyNameCompleter : IArgumentCompleter | | 6919 | + { | | 6920 | + private readonly string _parameterNameOfInput; | | 6921 | + | | 6922 | + /// <summary> | | 6923 | + /// Initializes a new instance of the <see cref="PropertyNameCompleter"/> class. | | 6924 | + /// </summary> | | 6925 | + public PropertyNameCompleter() | | 6926 | + { | | 6927 | + _parameterNameOfInput = "InputObject"; | | 6928 | + } | | 6929 | + | | 6930 | + /// <summary> | | 6931 | + /// Initializes a new instance of the <see cref="PropertyNameCompleter"/> class. | | 6932 | + /// </summary> | | 6933 | + /// <param name="parameterNameOfInput">The name of the property of the input object for witch to complete with property names.</param> | | 6934 | + public PropertyNameCompleter(string parameterNameOfInput) | | 6935 | + { | | 6936 | + _parameterNameOfInput = parameterNameOfInput; | | 6937 | + } | | 6938 | + | | 6939 | + IEnumerable<CompletionResult> IArgumentCompleter.CompleteArgument( | | 6940 | + string commandName, | | 6941 | + string parameterName, | | 6942 | + string wordToComplete, | | 6943 | + CommandAst commandAst, | | 6944 | + IDictionary fakeBoundParameters) | | 6945 | + { | | 6946 | + if (!(commandAst.Parent is PipelineAst pipelineAst)) | | 6947 | + { | | 6948 | + return null; | | 6949 | + } | | 6950 | + | | 6951 | + int i; | | 6952 | + for (i = 0; i < pipelineAst.PipelineElements.Count; i++) | | 6953 | + { | | 6954 | + if (pipelineAst.PipelineElements[i] == commandAst) | | 6955 | + { | | 6956 | + break; | | 6957 | + } | | 6958 | + } | | 6959 | + | | 6960 | + var typeInferenceContext = new TypeInferenceContext(); | | 6961 | + IEnumerable<PSTypeName> prevType; | | 6962 | + if (i == 0) | | 6963 | + { | | 6964 | + var parameterAst = (CommandParameterAst)commandAst.Find(ast => ast is CommandParameterAst cpa && cpa.ParameterName == "PropertyName", false); | | 6965 | + var pseudoBinding = new PseudoParameterBinder().DoPseudoParameterBinding(commandAst, null, parameterAst, PseudoParameterBinder.BindingType.ParameterCompletion); | | 6966 | + if (!pseudoBinding.BoundArguments.TryGetValue(_parameterNameOfInput, out var pair) || !pair.ArgumentSpecified) | | 6967 | + { | | 6968 | + return null; | | 6969 | + } | | 6970 | + | | 6971 | + if (pair is AstPair astPair && astPair.Argument != null) | | 6972 | + { | | 6973 | + prevType = AstTypeInference.InferTypeOf(astPair.Argument, typeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); | | 6974 | + } | | 6975 | + | | 6976 | + return null; | | 6977 | + } | | 6978 | + else | | 6979 | + { | | 6980 | + prevType = AstTypeInference.InferTypeOf(pipelineAst.PipelineElements[i - 1], typeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); | | 6981 | + } | | 6982 | + | | 6983 | + var result = new List<CompletionResult>(); | | 6984 | + | | 6985 | + CompletionCompleters.CompleteMemberByInferredType(typeInferenceContext, prevType, result, wordToComplete + "*", filter: CompletionCompleters.IsPropertyMember, isStatic: false); | | 6986 | + return result; | | 6987 | + } | | 6988 | + } | 6913 | 6989 | } |
Collapse file Copy file name to clipboardExpand all lines: src/System.Management.Automation/engine/LanguagePrimitives.cs +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -3244,7 +3244,7 @@ private static string ConvertNonNumericToString(object valueToConvert, | 3244 | 3244 | try | 3245 | 3245 | { | 3246 | 3246 | typeConversion.WriteLine("Converting object to string."); | 3247 | | - return PSObject.ToStringParser(ecFromTLS, valueToConvert); | | 3247 | + return PSObject.ToStringParser(ecFromTLS, valueToConvert, formatProvider); | 3248 | 3248 | } | 3249 | 3249 | catch (ExtendedTypeSystemException e) | 3250 | 3250 | { | |
|