From 6997286fd8f072c1264dcc54cc8ed720b9c472f4 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Thu, 30 Aug 2018 00:11:42 +0200 Subject: [PATCH 01/24] Fix disposing of powershell in TypeInferenceContext if it was created internally. --- .../engine/parser/TypeInferenceVisitor.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index 8a2ea1a68c3..d5277e0bf03 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -79,8 +79,9 @@ public static IList InferTypeOf(Ast ast, PowerShell powerShell) /// List of inferred typenames. public static IList InferTypeOf(Ast ast, PowerShell powerShell, TypeInferenceRuntimePermissions evalPersmissions) { - var context = new TypeInferenceContext(powerShell); - return InferTypeOf(ast, context, evalPersmissions); + using (var context = new TypeInferenceContext(powerShell)) { + return InferTypeOf(ast, context, evalPersmissions); + } } /// @@ -121,14 +122,16 @@ public int GetHashCode(PSTypeName obj) } } - internal class TypeInferenceContext + internal class TypeInferenceContext : IDisposable { public static readonly PSTypeName[] EmptyPSTypeNameArray = Utils.EmptyArray(); + private readonly bool _ownsPowerShell; private readonly PowerShell _powerShell; public TypeInferenceContext() : this(PowerShell.Create(RunspaceMode.CurrentRunspace)) { + _ownsPowerShell = true; } /// @@ -470,6 +473,12 @@ private static bool IsConstructor(object member) var methodCacheEntry = psMethod?.adapterData as DotNetAdapter.MethodCacheEntry; return methodCacheEntry != null && methodCacheEntry.methodInformationStructures[0].method.IsConstructor; } + + public void Dispose() + { + if (_ownsPowerShell) + _powerShell?.Dispose(); + } } internal class TypeInferenceVisitor : ICustomAstVisitor2 From 1977dc25a427625dcb64e6e4d0c2384870d9b643 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Thu, 30 Aug 2018 00:13:03 +0200 Subject: [PATCH 02/24] Adding a PropertyNameCompleter custom completer. --- .../CommandCompletion/CompletionCompleters.cs | 93 +++++++++++++++++-- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index c20057c397c..28b4ec3611f 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -3746,7 +3746,7 @@ private static void NativeCompletionMemberName(CompletionContext context, List CompleteMember(CompletionContext context, if (inferredTypes != null && inferredTypes.Length > 0) { // Use inferred types if we have any - CompleteMemberByInferredType(context, inferredTypes, results, memberName, filter: null, isStatic: @static); + CompleteMemberByInferredType(context.TypeInferenceContext, inferredTypes, results, memberName, filter: null, isStatic: @static); } else { @@ -5126,7 +5126,7 @@ private static bool IsInDscContext(ExpressionAst expression) return Ast.GetAncestorAst(expression) != null; } - private static void CompleteMemberByInferredType(CompletionContext context, IEnumerable inferredTypes, List results, string memberName, Func filter, bool isStatic) + internal static void CompleteMemberByInferredType(TypeInferenceContext context, IEnumerable inferredTypes, List results, string memberName, Func filter, bool isStatic) { bool extensionMethodsAdded = false; HashSet typeNameUsed = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -5138,7 +5138,7 @@ private static void CompleteMemberByInferredType(CompletionContext context, IEnu continue; } typeNameUsed.Add(psTypeName.Name); - var members = context.TypeInferenceContext.GetMembersByInferredType(psTypeName, isStatic, filter); + var members = context.GetMembersByInferredType(psTypeName, isStatic, filter); foreach (var member in members) { AddInferredMember(member, memberNamePattern, results); @@ -5271,7 +5271,7 @@ private static bool IsWriteablePropertyMember(object member) return false; } - private static bool IsPropertyMember(object member) + internal static bool IsPropertyMember(object member) { return member is PropertyInfo || member is FieldInfo @@ -6149,7 +6149,7 @@ internal static List CompleteHashtableKey(CompletionContext co { var result = new List(); CompleteMemberByInferredType( - completionContext, AstTypeInference.InferTypeOf(typeAst, completionContext.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval), + completionContext.TypeInferenceContext, AstTypeInference.InferTypeOf(typeAst, completionContext.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval), result, completionContext.WordToComplete + "*", IsWriteablePropertyMember, isStatic: false); return result; } @@ -6260,7 +6260,7 @@ internal static List CompleteHashtableKey(CompletionContext co var inferredType = AstTypeInference.InferTypeOf(commandAst, completionContext.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); var result = new List(); CompleteMemberByInferredType( - completionContext, inferredType, + completionContext.TypeInferenceContext, inferredType, result, completionContext.WordToComplete + "*", IsWriteablePropertyMember, isStatic: false); return result; case "Select-Object": @@ -6905,4 +6905,83 @@ public object VisitParenExpression(ParenExpressionAst parenExpressionAst) return parenExpressionAst.Pipeline.Accept(this); } } + + + /// + /// Completes with the property names of the InputObject. + /// + public class PropertyNameCompleter : IArgumentCompleter + { + private readonly string _parameterNameOfInput; + + /// + /// Initializes a new instance of the class. + /// + public PropertyNameCompleter() + { + _parameterNameOfInput = "InputObject"; + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the property of the input object for witch to complete with property names. + public PropertyNameCompleter(string parameterNameOfInput) + { + _parameterNameOfInput = parameterNameOfInput; + } + + IEnumerable IArgumentCompleter.CompleteArgument( + string commandName, + string parameterName, + string wordToComplete, + CommandAst commandAst, + IDictionary fakeBoundParameters) + { + var pipelineAst = commandAst.Parent as PipelineAst; + if (pipelineAst == null) + { + return null; + } + + int i; + for (i = 0; i < pipelineAst.PipelineElements.Count; i++) + { + if (pipelineAst.PipelineElements[i] == commandAst) + { + break; + } + } + + using (var typeInferenceContext = new TypeInferenceContext()) + { + IEnumerable prevType; + if (i == 0) + { + var parameterAst = (CommandParameterAst)commandAst.Find(ast => ast is CommandParameterAst cpa && cpa.ParameterName == "PropertyName", false); + var pseudoBinding = new PseudoParameterBinder().DoPseudoParameterBinding(commandAst, null, parameterAst, PseudoParameterBinder.BindingType.ParameterCompletion); + if (!pseudoBinding.BoundArguments.TryGetValue(_parameterNameOfInput, out var pair) || !pair.ArgumentSpecified) + { + return null; + } + + var astPair = pair as AstPair; + if (astPair?.Argument == null) + { + return null; + } + + prevType = AstTypeInference.InferTypeOf(astPair.Argument, TypeInferenceRuntimePermissions.AllowSafeEval); + } + else + { + prevType = AstTypeInference.InferTypeOf(pipelineAst.PipelineElements[i - 1], TypeInferenceRuntimePermissions.AllowSafeEval); + } + + var result = new List(); + CompletionCompleters.CompleteMemberByInferredType(typeInferenceContext, prevType, result, wordToComplete + "*", filter: CompletionCompleters.IsPropertyMember, isStatic: false); + return result; + } + } + } } From ba17c6c3417af6a3b9253b3f57b2941a8df661fa Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Thu, 30 Aug 2018 00:13:55 +0200 Subject: [PATCH 03/24] Adding Join-Object command --- .../commands/utility/join-object.cs | 211 ++++++++++++++++++ .../Microsoft.PowerShell.Utility.psd1 | 2 +- .../Microsoft.PowerShell.Utility.psd1 | 2 +- 3 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs new file mode 100644 index 00000000000..f91c6539ee3 --- /dev/null +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Internal; +using System.Management.Automation.Language; +using System.Text; + +namespace Microsoft.PowerShell.Commands.Utility +{ + /// + /// Group-Object implementation. + /// + [Cmdlet(VerbsCommon.Join, "Object", RemotingCapability = RemotingCapability.None, DefaultParameterSetName = "default")] + [OutputType(typeof(string))] + public class JoinObjectCommand : PSCmdlet + { + // ReSharper disable once CollectionNeverQueried.Local + private readonly List _inputObjects = new List(50); + private DynamicPropertyGetter _propGetter = new DynamicPropertyGetter(); + + /// + /// Gets or sets the property name or script block to use as the value to join. + /// + [Parameter(Position = 0)] + [ArgumentCompleter(typeof(PropertyNameCompleter))] + public object PropertyName { get; set; } + + /// + /// Gets or sets the delimiter to join the output with. + /// + [Parameter(Position = 1)] + [ArgumentCompleter(typeof(JoinItemCompleter))] + [AllowEmptyString] + public string Delimiter { get; set; } + + /// + /// Gets or sets text to include before the joined input text. + /// + [Parameter] + public string PreScript { get; set; } + + /// + /// Gets or sets text to include after the joined input text. + /// + [Parameter] + public string PostScript { get; set; } + + /// + /// Gets or sets if the output items should we wrapped in single quotes. + /// + [Parameter(ParameterSetName = "Quote")] + public SwitchParameter Quote { get; set; } + + /// + /// Gets or sets if the output items should we wrapped in double quotes. + /// + [Parameter(ParameterSetName = "DoubleQuote")] + public SwitchParameter DoubleQuote { get; set; } + + /// + /// Gets or sets the input object to join into text. + /// + [Parameter(ValueFromPipeline = true, Mandatory = true)] + public PSObject InputObject { get; set; } + + /// + protected override void ProcessRecord() + { + _inputObjects.Add(InputObject); + } + + /// + protected override void EndProcessing() + { + var quoteChar = Quote ? '\'' : DoubleQuote ? '"' : char.MinValue; + + var builder = new StringBuilder(256); + builder.Append(PreScript); + + if (Delimiter == null) + { + Delimiter = LanguagePrimitives.ConvertTo(GetVariableValue("OFS")); + } + + + void AppendValue(string val) + { + if (quoteChar != char.MinValue) + { + builder.Append(quoteChar).Append(val).Append(quoteChar); + } + else + { + builder.Append(val); + } + } + + if (PropertyName == null) + { + if (_inputObjects.Count > 0) + { + AppendValue(LanguagePrimitives.ConvertTo(_inputObjects[0])); + } + + for (var index = 1; index < _inputObjects.Count; index++) + { + builder.Append(Delimiter); + AppendValue(LanguagePrimitives.ConvertTo(_inputObjects[index])); + } + } + else if (PropertyName is string propertyName) + { + string GetPropertyValueString(PSObject input, string name) + { + return LanguagePrimitives.ConvertTo(_propGetter.GetValue(input, name)); + } + + if (_inputObjects.Count > 0) + { + AppendValue(GetPropertyValueString(_inputObjects[0], propertyName)); + } + + for (var index = 1; index < _inputObjects.Count; index++) + { + builder.Append(Delimiter); + AppendValue(GetPropertyValueString(_inputObjects[index], propertyName)); + } + } + else if (PropertyName is ScriptBlock sb) + { + string GetScriptBlockResultString(PSObject input) + { + var result = sb.DoInvokeReturnAsIs( + useLocalScope: true, + errorHandlingBehavior: ScriptBlock.ErrorHandlingBehavior.WriteToCurrentErrorPipe, + dollarUnder: input, + input: AutomationNull.Value, + scriptThis: AutomationNull.Value, + args: Utils.EmptyArray()); + return LanguagePrimitives.ConvertTo(result); + } + + if (_inputObjects.Count > 0) + { + AppendValue(GetScriptBlockResultString(_inputObjects[0])); + } + + for (var index = 1; index < _inputObjects.Count; index++) + { + builder.Append(Delimiter); + AppendValue(GetScriptBlockResultString(_inputObjects[index])); + } + } + else + { + throw new ArgumentException(); + } + + builder.Append(PostScript); + WriteObject(builder.ToString(), false); + } + } + + internal class JoinItemCompleter : IArgumentCompleter + { + public IEnumerable CompleteArgument( + string commandName, + string parameterName, + string wordToComplete, + CommandAst commandAst, + IDictionary fakeBoundParameters) + { + var res = new List(10); + + void AddMatching(string completionText, string listText, string toolTip) + { + if (completionText.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) + { + res.Add(new CompletionResult(completionText, listText, CompletionResultType.ParameterValue, toolTip)); + } + } + + AddMatching("', '", "Comma-Space", "', ' - Comma-Space"); + AddMatching("';'", "Semi-Colon", "';' - Semi-Colon "); + AddMatching("'; '", "Semi-Colon-Space", "'; ' - Semi-Colon-Space"); + AddMatching($"\"{NewLineText}\"", "Newline", $"{NewLineText} - Newline"); + AddMatching("','", "Comma", "',' - Comma"); + AddMatching("'-'", "Dash", "'-' - Dash"); + AddMatching("' '", "Space", "' ' - Space"); + return res; + } + + public string NewLineText + { + get + { + switch (Environment.NewLine) + { + case "\r": return "`r"; + case "\n": return "`n"; + case "\r\n": return "`r`n"; + default: return Environment.NewLine.Replace("\r", "`r").Replace("\n", "`n"); + } + } + } + } +} \ No newline at end of file diff --git a/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 b/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 index 86e09756362..48cbbaff7f2 100644 --- a/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 +++ b/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 @@ -16,7 +16,7 @@ CmdletsToExport= "Format-List", "Format-Custom", "Format-Table", "Format-Wide", "Get-Unique", "Export-PSSession", "Import-PSSession", "Import-Alias", "Import-LocalizedData", "Select-String", "Measure-Object", "New-Alias", "New-TimeSpan", "Read-Host", "Set-Alias", "Set-Date", "Start-Sleep", "Tee-Object", "Measure-Command", "Update-TypeData", "Update-FormatData", - "Remove-TypeData", "Get-TypeData", "Write-Host", "Write-Progress", "New-Object", "Select-Object", + "Remove-TypeData", "Get-TypeData", "Write-Host", "Write-Progress", "New-Object", "Select-Object", "Join-Ojbect", "Group-Object", "Sort-Object", "Get-Variable", "New-Variable", "Set-Variable", "Remove-Variable", "Clear-Variable", "Export-Clixml", "Import-Clixml", "Import-PowerShellDataFile", "ConvertTo-Xml", "Select-Xml", "Write-Debug", "Write-Verbose", "Write-Warning", "Write-Error", "Write-Information", "Write-Output", "Set-PSBreakpoint", diff --git a/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 b/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 index ea7e3690574..d57a24238c4 100644 --- a/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 +++ b/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 @@ -16,7 +16,7 @@ CmdletsToExport= "Format-List", "Format-Custom", "Format-Table", "Format-Wide", "Get-Unique", "Export-PSSession", "Import-PSSession", "Import-Alias", "Import-LocalizedData", "Select-String", "Measure-Object", "New-Alias", "New-TimeSpan", "Read-Host", "Set-Alias", "Set-Date", "Start-Sleep", "Tee-Object", "Measure-Command", "Update-TypeData", "Update-FormatData", - "Remove-TypeData", "Get-TypeData", "Write-Host", "Write-Progress", "New-Object", "Select-Object", + "Remove-TypeData", "Get-TypeData", "Write-Host", "Write-Progress", "New-Object", "Select-Object", "Join-Object", "Group-Object", "Sort-Object", "Get-Variable", "New-Variable", "Set-Variable", "Remove-Variable", "Clear-Variable", "Export-Clixml", "Import-Clixml", "Import-PowerShellDataFile","ConvertTo-Xml", "Select-Xml", "Write-Debug", "Write-Verbose", "Write-Warning", "Write-Error", "Write-Information", "Write-Output", "Set-PSBreakpoint", From b34453ee1d0182acd62f5696771c7ac56d506ad8 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Thu, 30 Aug 2018 01:24:52 +0200 Subject: [PATCH 04/24] Adding tests --- .../Join-Object.Tests.ps1 | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 test/powershell/Modules/Microsoft.PowerShell.Utility/Join-Object.Tests.ps1 diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-Object.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-Object.Tests.ps1 new file mode 100644 index 00000000000..762bc299d85 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-Object.Tests.ps1 @@ -0,0 +1,87 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +Describe "Join-Object" -Tags "CI" { + + BeforeAll { + $testObject = Get-ChildItem + } + + It "Should be called using an object as piped without error with no switches" { + {$testObject | Join-Object } | Should -Not -Throw + } + + It "Should be called using the InputObject without error with no other switches" { + { Join-Object -InputObject $testObject } | Should -Not -Throw + } + + It "Should return a single string" { + $actual = $testObject | Join-Object + + $actual.Count | Should -Be 1 + $actual | Should -BeOfType System.String + } + + It "Should join property values with default delimiter" { + $expected = $testObject.Name -join $ofs + $actual = $testObject | Join-Object -PropertyName Name + $actual | Should -BeExactly $expected + } + + It "Should join property values positionally with default delimiter" { + $expected = $testObject.Name -join $ofs + $actual = $testObject | Join-Object Name + $actual | Should -BeExactly $expected + } + + It "Should join property values with custom delimiter" { + $expected = $testObject.Name -join "; " + $actual = $testObject | Join-Object -PropertyName Name -Delimiter "; " + $actual | Should -BeExactly $expected + } + + It "Should join property values Quoted" { + $expected = ($testObject.Name).Foreach{"'$_'"} -join "; " + $actual = $testObject | Join-Object -PropertyName Name -Delimiter "; " -Quote + $actual | Should -BeExactly $expected + } + + It "Should join property values DoubleQuoted" { + $expected = ($testObject.Name).Foreach{"""$_"""} -join "; " + $actual = $testObject | Join-Object -PropertyName Name -Delimiter "; " -DoubleQuote + $actual | Should -BeExactly $expected + } + + It "Should join script block results with default delimiter" { + $sb = {$_.Name + $_.Length} + $expected = ($testObject | ForEach-Object $sb) -join $ofs + $actual = $testObject | Join-Object -PropertyName $sb + $actual | Should -BeExactly $expected + } + + It "Should join script block results with custom delimiter" { + $sb = {$_.Name + $_.Length} + $expected = ($testObject | ForEach-Object $sb) -join "; " + $actual = $testObject | Join-Object -PropertyName $sb -Delimiter "; " + $actual | Should -BeExactly $expected + } + + It "Should join script block results Quoted" { + $sb = {$_.Name + $_.Length} + $expected = ($testObject | ForEach-Object $sb).Foreach{"'$_'"} -join $ofs + $actual = $testObject | Join-Object -PropertyName $sb -Quote + $actual | Should -BeExactly $expected + } + It "Should join script block results DoubleQuoted" { + $sb = {$_.Name + $_.Length} + $expected = ($testObject | ForEach-Object $sb).Foreach{"""$_"""} -join $ofs + $actual = $testObject | Join-Object -PropertyName $sb -DoubleQuote + $actual | Should -BeExactly $expected + } + + It "Should Handle PreScript and PostScript" { + $ofs = ',' + $expected = "A 1,2,3 B" + $actual = 1..3 | Join-Object -prescript "A " -PostScript " B" + $actual | Should -BeExactly $expected + } +} From cf646c15d68470dcddba50ea124f05f6e59570fa Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Fri, 31 Aug 2018 08:03:41 +0200 Subject: [PATCH 05/24] Reverting incorrect dispose in TypeInferenceVisitor --- .../engine/parser/TypeInferenceVisitor.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index d5277e0bf03..8a2ea1a68c3 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -79,9 +79,8 @@ public static IList InferTypeOf(Ast ast, PowerShell powerShell) /// List of inferred typenames. public static IList InferTypeOf(Ast ast, PowerShell powerShell, TypeInferenceRuntimePermissions evalPersmissions) { - using (var context = new TypeInferenceContext(powerShell)) { - return InferTypeOf(ast, context, evalPersmissions); - } + var context = new TypeInferenceContext(powerShell); + return InferTypeOf(ast, context, evalPersmissions); } /// @@ -122,16 +121,14 @@ public int GetHashCode(PSTypeName obj) } } - internal class TypeInferenceContext : IDisposable + internal class TypeInferenceContext { public static readonly PSTypeName[] EmptyPSTypeNameArray = Utils.EmptyArray(); - private readonly bool _ownsPowerShell; private readonly PowerShell _powerShell; public TypeInferenceContext() : this(PowerShell.Create(RunspaceMode.CurrentRunspace)) { - _ownsPowerShell = true; } /// @@ -473,12 +470,6 @@ private static bool IsConstructor(object member) var methodCacheEntry = psMethod?.adapterData as DotNetAdapter.MethodCacheEntry; return methodCacheEntry != null && methodCacheEntry.methodInformationStructures[0].method.IsConstructor; } - - public void Dispose() - { - if (_ownsPowerShell) - _powerShell?.Dispose(); - } } internal class TypeInferenceVisitor : ICustomAstVisitor2 From 32fd136820fa240060395604f73ab2a35b340ab3 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Fri, 31 Aug 2018 08:06:55 +0200 Subject: [PATCH 06/24] Addressing Pauls review comments. --- .../commands/utility/join-object.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs index f91c6539ee3..cab0ac5c90d 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs @@ -12,14 +12,16 @@ namespace Microsoft.PowerShell.Commands.Utility { /// - /// Group-Object implementation. + /// Join-Object implementation. /// [Cmdlet(VerbsCommon.Join, "Object", RemotingCapability = RemotingCapability.None, DefaultParameterSetName = "default")] [OutputType(typeof(string))] - public class JoinObjectCommand : PSCmdlet + public sealed class JoinObjectCommand : PSCmdlet { + private const int DefaultInputObjectBufferSize = 50; + // ReSharper disable once CollectionNeverQueried.Local - private readonly List _inputObjects = new List(50); + private readonly List _inputObjects = new List(DefaultInputObjectBufferSize); private DynamicPropertyGetter _propGetter = new DynamicPropertyGetter(); /// @@ -78,7 +80,8 @@ protected override void EndProcessing() { var quoteChar = Quote ? '\'' : DoubleQuote ? '"' : char.MinValue; - var builder = new StringBuilder(256); + const int defaultOutputStringCapacity = 256; + var builder = new StringBuilder(defaultOutputStringCapacity); builder.Append(PreScript); if (Delimiter == null) From acbdbf9a324553a64ac849060897009f961841ef Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Fri, 31 Aug 2018 08:15:47 +0200 Subject: [PATCH 07/24] Early out when inputobject count == 0. --- .../commands/utility/join-object.cs | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs index cab0ac5c90d..6daa9a8e4f7 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs @@ -18,10 +18,10 @@ namespace Microsoft.PowerShell.Commands.Utility [OutputType(typeof(string))] public sealed class JoinObjectCommand : PSCmdlet { - private const int DefaultInputObjectBufferSize = 50; + private const int Capacity = 50; // ReSharper disable once CollectionNeverQueried.Local - private readonly List _inputObjects = new List(DefaultInputObjectBufferSize); + private readonly List _inputObjects = new List(Capacity); private DynamicPropertyGetter _propGetter = new DynamicPropertyGetter(); /// @@ -102,12 +102,15 @@ void AppendValue(string val) } } + if (_inputObjects.Count == 0) + { + return; + } + if (PropertyName == null) { - if (_inputObjects.Count > 0) - { - AppendValue(LanguagePrimitives.ConvertTo(_inputObjects[0])); - } + + AppendValue(LanguagePrimitives.ConvertTo(_inputObjects[0])); for (var index = 1; index < _inputObjects.Count; index++) { @@ -122,10 +125,7 @@ string GetPropertyValueString(PSObject input, string name) return LanguagePrimitives.ConvertTo(_propGetter.GetValue(input, name)); } - if (_inputObjects.Count > 0) - { - AppendValue(GetPropertyValueString(_inputObjects[0], propertyName)); - } + AppendValue(GetPropertyValueString(_inputObjects[0], propertyName)); for (var index = 1; index < _inputObjects.Count; index++) { @@ -147,10 +147,7 @@ string GetScriptBlockResultString(PSObject input) return LanguagePrimitives.ConvertTo(result); } - if (_inputObjects.Count > 0) - { - AppendValue(GetScriptBlockResultString(_inputObjects[0])); - } + AppendValue(GetScriptBlockResultString(_inputObjects[0])); for (var index = 1; index < _inputObjects.Count; index++) { From bda1740b96d1bc177bcbe286ee9f15d260f13060 Mon Sep 17 00:00:00 2001 From: "Gustafsson, Staffan" Date: Fri, 31 Aug 2018 18:45:12 +0200 Subject: [PATCH 08/24] Fixing missing TypeInferenceVisitor Dispose change. --- .../CommandCompletion/CompletionCompleters.cs | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 28b4ec3611f..c77d6e0adc2 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -6953,35 +6953,35 @@ IEnumerable IArgumentCompleter.CompleteArgument( } } - using (var typeInferenceContext = new TypeInferenceContext()) + var typeInferenceContext = new TypeInferenceContext(); + IEnumerable prevType; + if (i == 0) { - IEnumerable prevType; - if (i == 0) + var parameterAst = (CommandParameterAst)commandAst.Find(ast => ast is CommandParameterAst cpa && cpa.ParameterName == "PropertyName", false); + var pseudoBinding = new PseudoParameterBinder().DoPseudoParameterBinding(commandAst, null, parameterAst, PseudoParameterBinder.BindingType.ParameterCompletion); + if (!pseudoBinding.BoundArguments.TryGetValue(_parameterNameOfInput, out var pair) || !pair.ArgumentSpecified) { - var parameterAst = (CommandParameterAst)commandAst.Find(ast => ast is CommandParameterAst cpa && cpa.ParameterName == "PropertyName", false); - var pseudoBinding = new PseudoParameterBinder().DoPseudoParameterBinding(commandAst, null, parameterAst, PseudoParameterBinder.BindingType.ParameterCompletion); - if (!pseudoBinding.BoundArguments.TryGetValue(_parameterNameOfInput, out var pair) || !pair.ArgumentSpecified) - { - return null; - } - - var astPair = pair as AstPair; - if (astPair?.Argument == null) - { - return null; - } - - prevType = AstTypeInference.InferTypeOf(astPair.Argument, TypeInferenceRuntimePermissions.AllowSafeEval); + return null; } - else + + var astPair = pair as AstPair; + if (astPair?.Argument == null) { - prevType = AstTypeInference.InferTypeOf(pipelineAst.PipelineElements[i - 1], TypeInferenceRuntimePermissions.AllowSafeEval); + return null; } - var result = new List(); - CompletionCompleters.CompleteMemberByInferredType(typeInferenceContext, prevType, result, wordToComplete + "*", filter: CompletionCompleters.IsPropertyMember, isStatic: false); - return result; + prevType = AstTypeInference.InferTypeOf(astPair.Argument, typeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); + } + else + { + prevType = AstTypeInference.InferTypeOf(pipelineAst.PipelineElements[i - 1], typeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); } + + var result = new List(); + + CompletionCompleters.CompleteMemberByInferredType(typeInferenceContext, prevType, result, wordToComplete + "*", filter: CompletionCompleters.IsPropertyMember, isStatic: false); + return result; } + } } From 2995f925124ae8929dfb46d7ac886d653e984b03 Mon Sep 17 00:00:00 2001 From: "Gustafsson, Staffan" Date: Fri, 31 Aug 2018 18:44:29 +0200 Subject: [PATCH 09/24] Renaming Join-Object => Join-String. PreScript to Prefix PostScript to Suffix. PropertyName to Property. --- .../commands/utility/join-object.cs | 20 +++++----- .../Microsoft.PowerShell.Utility.psd1 | 2 +- .../Microsoft.PowerShell.Utility.psd1 | 2 +- .../Join-Object.Tests.ps1 | 39 ++++++++++++------- 4 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs index 6daa9a8e4f7..2770859ea2b 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs @@ -14,9 +14,9 @@ namespace Microsoft.PowerShell.Commands.Utility /// /// Join-Object implementation. /// - [Cmdlet(VerbsCommon.Join, "Object", RemotingCapability = RemotingCapability.None, DefaultParameterSetName = "default")] + [Cmdlet(VerbsCommon.Join, "String", RemotingCapability = RemotingCapability.None, DefaultParameterSetName = "default")] [OutputType(typeof(string))] - public sealed class JoinObjectCommand : PSCmdlet + public sealed class JoinStringCommand : PSCmdlet { private const int Capacity = 50; @@ -29,7 +29,7 @@ public sealed class JoinObjectCommand : PSCmdlet /// [Parameter(Position = 0)] [ArgumentCompleter(typeof(PropertyNameCompleter))] - public object PropertyName { get; set; } + public object Property { get; set; } /// /// Gets or sets the delimiter to join the output with. @@ -43,13 +43,13 @@ public sealed class JoinObjectCommand : PSCmdlet /// Gets or sets text to include before the joined input text. /// [Parameter] - public string PreScript { get; set; } + public string Prefix { get; set; } /// /// Gets or sets text to include after the joined input text. /// [Parameter] - public string PostScript { get; set; } + public string Suffix { get; set; } /// /// Gets or sets if the output items should we wrapped in single quotes. @@ -82,7 +82,7 @@ protected override void EndProcessing() const int defaultOutputStringCapacity = 256; var builder = new StringBuilder(defaultOutputStringCapacity); - builder.Append(PreScript); + builder.Append(Prefix); if (Delimiter == null) { @@ -107,7 +107,7 @@ void AppendValue(string val) return; } - if (PropertyName == null) + if (Property == null) { AppendValue(LanguagePrimitives.ConvertTo(_inputObjects[0])); @@ -118,7 +118,7 @@ void AppendValue(string val) AppendValue(LanguagePrimitives.ConvertTo(_inputObjects[index])); } } - else if (PropertyName is string propertyName) + else if (Property is string propertyName) { string GetPropertyValueString(PSObject input, string name) { @@ -133,7 +133,7 @@ string GetPropertyValueString(PSObject input, string name) AppendValue(GetPropertyValueString(_inputObjects[index], propertyName)); } } - else if (PropertyName is ScriptBlock sb) + else if (Property is ScriptBlock sb) { string GetScriptBlockResultString(PSObject input) { @@ -160,7 +160,7 @@ string GetScriptBlockResultString(PSObject input) throw new ArgumentException(); } - builder.Append(PostScript); + builder.Append(Suffix); WriteObject(builder.ToString(), false); } } diff --git a/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 b/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 index 48cbbaff7f2..bb6694763b8 100644 --- a/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 +++ b/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 @@ -16,7 +16,7 @@ CmdletsToExport= "Format-List", "Format-Custom", "Format-Table", "Format-Wide", "Get-Unique", "Export-PSSession", "Import-PSSession", "Import-Alias", "Import-LocalizedData", "Select-String", "Measure-Object", "New-Alias", "New-TimeSpan", "Read-Host", "Set-Alias", "Set-Date", "Start-Sleep", "Tee-Object", "Measure-Command", "Update-TypeData", "Update-FormatData", - "Remove-TypeData", "Get-TypeData", "Write-Host", "Write-Progress", "New-Object", "Select-Object", "Join-Ojbect", + "Remove-TypeData", "Get-TypeData", "Write-Host", "Write-Progress", "New-Object", "Select-Object", "Join-String", "Group-Object", "Sort-Object", "Get-Variable", "New-Variable", "Set-Variable", "Remove-Variable", "Clear-Variable", "Export-Clixml", "Import-Clixml", "Import-PowerShellDataFile", "ConvertTo-Xml", "Select-Xml", "Write-Debug", "Write-Verbose", "Write-Warning", "Write-Error", "Write-Information", "Write-Output", "Set-PSBreakpoint", diff --git a/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 b/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 index d57a24238c4..c57aae9ad4c 100644 --- a/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 +++ b/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 @@ -16,7 +16,7 @@ CmdletsToExport= "Format-List", "Format-Custom", "Format-Table", "Format-Wide", "Get-Unique", "Export-PSSession", "Import-PSSession", "Import-Alias", "Import-LocalizedData", "Select-String", "Measure-Object", "New-Alias", "New-TimeSpan", "Read-Host", "Set-Alias", "Set-Date", "Start-Sleep", "Tee-Object", "Measure-Command", "Update-TypeData", "Update-FormatData", - "Remove-TypeData", "Get-TypeData", "Write-Host", "Write-Progress", "New-Object", "Select-Object", "Join-Object", + "Remove-TypeData", "Get-TypeData", "Write-Host", "Write-Progress", "New-Object", "Select-Object", "Join-String", "Group-Object", "Sort-Object", "Get-Variable", "New-Variable", "Set-Variable", "Remove-Variable", "Clear-Variable", "Export-Clixml", "Import-Clixml", "Import-PowerShellDataFile","ConvertTo-Xml", "Select-Xml", "Write-Debug", "Write-Verbose", "Write-Warning", "Write-Error", "Write-Information", "Write-Output", "Set-PSBreakpoint", diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-Object.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-Object.Tests.ps1 index 762bc299d85..c3eed113489 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-Object.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-Object.Tests.ps1 @@ -1,21 +1,21 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -Describe "Join-Object" -Tags "CI" { +Describe "Join-String" -Tags "CI" { BeforeAll { $testObject = Get-ChildItem } It "Should be called using an object as piped without error with no switches" { - {$testObject | Join-Object } | Should -Not -Throw + {$testObject | Join-String } | Should -Not -Throw } It "Should be called using the InputObject without error with no other switches" { - { Join-Object -InputObject $testObject } | Should -Not -Throw + { Join-String -InputObject $testObject } | Should -Not -Throw } It "Should return a single string" { - $actual = $testObject | Join-Object + $actual = $testObject | Join-String $actual.Count | Should -Be 1 $actual | Should -BeOfType System.String @@ -23,65 +23,76 @@ Describe "Join-Object" -Tags "CI" { It "Should join property values with default delimiter" { $expected = $testObject.Name -join $ofs - $actual = $testObject | Join-Object -PropertyName Name + $actual = $testObject | Join-String -Property Name $actual | Should -BeExactly $expected } It "Should join property values positionally with default delimiter" { $expected = $testObject.Name -join $ofs - $actual = $testObject | Join-Object Name + $actual = $testObject | Join-String Name $actual | Should -BeExactly $expected } It "Should join property values with custom delimiter" { $expected = $testObject.Name -join "; " - $actual = $testObject | Join-Object -PropertyName Name -Delimiter "; " + $actual = $testObject | Join-String -Property Name -Delimiter "; " $actual | Should -BeExactly $expected } It "Should join property values Quoted" { $expected = ($testObject.Name).Foreach{"'$_'"} -join "; " - $actual = $testObject | Join-Object -PropertyName Name -Delimiter "; " -Quote + $actual = $testObject | Join-String -Property Name -Delimiter "; " -Quote $actual | Should -BeExactly $expected } It "Should join property values DoubleQuoted" { $expected = ($testObject.Name).Foreach{"""$_"""} -join "; " - $actual = $testObject | Join-Object -PropertyName Name -Delimiter "; " -DoubleQuote + $actual = $testObject | Join-String -Property Name -Delimiter "; " -DoubleQuote $actual | Should -BeExactly $expected } It "Should join script block results with default delimiter" { $sb = {$_.Name + $_.Length} $expected = ($testObject | ForEach-Object $sb) -join $ofs - $actual = $testObject | Join-Object -PropertyName $sb + $actual = $testObject | Join-String -Property $sb $actual | Should -BeExactly $expected } It "Should join script block results with custom delimiter" { $sb = {$_.Name + $_.Length} $expected = ($testObject | ForEach-Object $sb) -join "; " - $actual = $testObject | Join-Object -PropertyName $sb -Delimiter "; " + $actual = $testObject | Join-String -Property $sb -Delimiter "; " $actual | Should -BeExactly $expected } It "Should join script block results Quoted" { $sb = {$_.Name + $_.Length} $expected = ($testObject | ForEach-Object $sb).Foreach{"'$_'"} -join $ofs - $actual = $testObject | Join-Object -PropertyName $sb -Quote + $actual = $testObject | Join-String -Property $sb -Quote $actual | Should -BeExactly $expected } It "Should join script block results DoubleQuoted" { $sb = {$_.Name + $_.Length} $expected = ($testObject | ForEach-Object $sb).Foreach{"""$_"""} -join $ofs - $actual = $testObject | Join-Object -PropertyName $sb -DoubleQuote + $actual = $testObject | Join-String -Property $sb -DoubleQuote $actual | Should -BeExactly $expected } It "Should Handle PreScript and PostScript" { $ofs = ',' $expected = "A 1,2,3 B" - $actual = 1..3 | Join-Object -prescript "A " -PostScript " B" + $actual = 1..3 | Join-String -Prefix "A " -Suffix " B" $actual | Should -BeExactly $expected } + + It "Should tabcomplete InputObject properties" { + $cmd = '[io.fileinfo]::new("c:\temp") | Join-String -Property ' + $res = tabexpansion2 $cmd $cmd.length + $completionTexts = $res.CompletionMatches.CompletionText + $Propertys = [io.fileinfo]::new($PSScriptRoot).psobject.properties.Name + foreach($n in $Propertys) { + $n -in $completionTexts | Should -BeTrue + } + } + } From 2a0b56ad6045301095528b012533c704c0fb5757 Mon Sep 17 00:00:00 2001 From: "Gustafsson, Staffan" Date: Fri, 31 Aug 2018 18:49:10 +0200 Subject: [PATCH 10/24] newline at eof. --- .../commands/utility/join-object.cs | 2 +- .../Modules/Microsoft.PowerShell.Utility/Join-Object.Tests.ps1 | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs index 2770859ea2b..c9c75d12890 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs @@ -208,4 +208,4 @@ public string NewLineText } } } -} \ No newline at end of file +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-Object.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-Object.Tests.ps1 index c3eed113489..99d1f011c2c 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-Object.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-Object.Tests.ps1 @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. + Describe "Join-String" -Tags "CI" { BeforeAll { From c68da1faaf5ee073699d2af44caa99c4ebb8107e Mon Sep 17 00:00:00 2001 From: "Gustafsson, Staffan" Date: Fri, 31 Aug 2018 18:50:25 +0200 Subject: [PATCH 11/24] Renaming files *-object -> *-string --- .../commands/utility/{join-object.cs => join-string.cs} | 0 .../{Join-Object.Tests.ps1 => Join-String.Tests.ps1} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/Microsoft.PowerShell.Commands.Utility/commands/utility/{join-object.cs => join-string.cs} (100%) rename test/powershell/Modules/Microsoft.PowerShell.Utility/{Join-Object.Tests.ps1 => Join-String.Tests.ps1} (100%) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs similarity index 100% rename from src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-object.cs rename to src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-Object.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-String.Tests.ps1 similarity index 100% rename from test/powershell/Modules/Microsoft.PowerShell.Utility/Join-Object.Tests.ps1 rename to test/powershell/Modules/Microsoft.PowerShell.Utility/Join-String.Tests.ps1 From 68c0f57ef1e3eceab9a11124b3da919f5e4f584d Mon Sep 17 00:00:00 2001 From: "Gustafsson, Staffan" Date: Fri, 31 Aug 2018 18:58:41 +0200 Subject: [PATCH 12/24] platform specific newline completion text. --- .../commands/utility/join-string.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs index c9c75d12890..c74fd111395 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs @@ -198,13 +198,11 @@ public string NewLineText { get { - switch (Environment.NewLine) - { - case "\r": return "`r"; - case "\n": return "`n"; - case "\r\n": return "`r`n"; - default: return Environment.NewLine.Replace("\r", "`r").Replace("\n", "`n"); - } +#if UNIX + return Platform.IsMacOS ? "`r" : "`n"; +#else + return "`r`n"; +#endif } } } From 6f39f8e1b37c30800ec0594439fc1cfd66b26449 Mon Sep 17 00:00:00 2001 From: "Gustafsson, Staffan" Date: Fri, 31 Aug 2018 19:08:24 +0200 Subject: [PATCH 13/24] Ignore null and automationnull input. --- .../commands/utility/join-string.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs index c74fd111395..3ed9028b12d 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs @@ -72,7 +72,10 @@ public sealed class JoinStringCommand : PSCmdlet /// protected override void ProcessRecord() { - _inputObjects.Add(InputObject); + if (InputObject != null && InputObject != AutomationNull.Value) + { + _inputObjects.Add(InputObject); + } } /// From 28224d9c5c19d2729e1d8e2c2394f0c6e425264b Mon Sep 17 00:00:00 2001 From: "Gustafsson, Staffan" Date: Fri, 31 Aug 2018 19:09:33 +0200 Subject: [PATCH 14/24] Moving the early exit to the top of the function. --- .../commands/utility/join-string.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs index 3ed9028b12d..0db07dd54b7 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs @@ -81,6 +81,11 @@ protected override void ProcessRecord() /// protected override void EndProcessing() { + if (_inputObjects.Count == 0) + { + return; + } + var quoteChar = Quote ? '\'' : DoubleQuote ? '"' : char.MinValue; const int defaultOutputStringCapacity = 256; @@ -92,7 +97,6 @@ protected override void EndProcessing() Delimiter = LanguagePrimitives.ConvertTo(GetVariableValue("OFS")); } - void AppendValue(string val) { if (quoteChar != char.MinValue) @@ -105,11 +109,6 @@ void AppendValue(string val) } } - if (_inputObjects.Count == 0) - { - return; - } - if (Property == null) { From 2fac926d2b95b5885ad3cf57764a462371f3defd Mon Sep 17 00:00:00 2001 From: "Gustafsson, Staffan" Date: Fri, 31 Aug 2018 19:13:13 +0200 Subject: [PATCH 15/24] Codefactor fixes. --- .../commands/utility/join-string.cs | 5 ++--- .../engine/CommandCompletion/CompletionCompleters.cs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs index 0db07dd54b7..fe25b7f1b0c 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs @@ -88,8 +88,8 @@ protected override void EndProcessing() var quoteChar = Quote ? '\'' : DoubleQuote ? '"' : char.MinValue; - const int defaultOutputStringCapacity = 256; - var builder = new StringBuilder(defaultOutputStringCapacity); + const int DefaultOutputStringCapacity = 256; + var builder = new StringBuilder(DefaultOutputStringCapacity); builder.Append(Prefix); if (Delimiter == null) @@ -111,7 +111,6 @@ void AppendValue(string val) if (Property == null) { - AppendValue(LanguagePrimitives.ConvertTo(_inputObjects[0])); for (var index = 1; index < _inputObjects.Count; index++) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index c77d6e0adc2..5fb3ee5ac00 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -5137,6 +5137,7 @@ internal static void CompleteMemberByInferredType(TypeInferenceContext context, { continue; } + typeNameUsed.Add(psTypeName.Name); var members = context.GetMembersByInferredType(psTypeName, isStatic, filter); foreach (var member in members) @@ -6906,7 +6907,6 @@ public object VisitParenExpression(ParenExpressionAst parenExpressionAst) } } - /// /// Completes with the property names of the InputObject. /// From 6493e7dada7b07379cf734428524b723c96ad407 Mon Sep 17 00:00:00 2001 From: "Gustafsson, Staffan" Date: Fri, 31 Aug 2018 19:41:14 +0200 Subject: [PATCH 16/24] Adding Join-String to defaultcommands test. --- test/powershell/engine/Basic/DefaultCommands.Tests.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 index cc60c28641f..bd1d2ae1f68 100644 --- a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 +++ b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 @@ -319,6 +319,7 @@ Describe "Verify approved aliases list" -Tags "CI" { "Cmdlet", "Invoke-WebRequest", , $($FullCLR -or $CoreWindows -or $CoreUnix) "Cmdlet", "Invoke-WmiMethod", , $($FullCLR ) "Cmdlet", "Invoke-WSManAction", , $($FullCLR -or $CoreWindows ) +"Cmdlet", "Join-String", , $( $CoreWindows -or $CoreUnix) "Cmdlet", "Join-Path", , $($FullCLR -or $CoreWindows -or $CoreUnix) "Cmdlet", "Limit-EventLog", , $($FullCLR ) "Cmdlet", "Measure-Command", , $($FullCLR -or $CoreWindows -or $CoreUnix) From e3d6fccacdeed0536e1d8b07fd387d396729808f Mon Sep 17 00:00:00 2001 From: "Gustafsson, Staffan" Date: Fri, 31 Aug 2018 20:58:07 +0200 Subject: [PATCH 17/24] sorting cmdlet names. --- test/powershell/engine/Basic/DefaultCommands.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 index bd1d2ae1f68..04781833081 100644 --- a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 +++ b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 @@ -319,8 +319,8 @@ Describe "Verify approved aliases list" -Tags "CI" { "Cmdlet", "Invoke-WebRequest", , $($FullCLR -or $CoreWindows -or $CoreUnix) "Cmdlet", "Invoke-WmiMethod", , $($FullCLR ) "Cmdlet", "Invoke-WSManAction", , $($FullCLR -or $CoreWindows ) -"Cmdlet", "Join-String", , $( $CoreWindows -or $CoreUnix) "Cmdlet", "Join-Path", , $($FullCLR -or $CoreWindows -or $CoreUnix) +"Cmdlet", "Join-String", , $( $CoreWindows -or $CoreUnix) "Cmdlet", "Limit-EventLog", , $($FullCLR ) "Cmdlet", "Measure-Command", , $($FullCLR -or $CoreWindows -or $CoreUnix) "Cmdlet", "Measure-Object", , $($FullCLR -or $CoreWindows -or $CoreUnix) From 0694362a669dccbc0372237e769357cb97cc6525 Mon Sep 17 00:00:00 2001 From: "Gustafsson, Staffan" Date: Fri, 31 Aug 2018 21:35:05 +0200 Subject: [PATCH 18/24] Addressing Ilya's feedback --- .../commands/utility/join-string.cs | 7 ++++--- .../engine/CommandCompletion/CompletionCompleters.cs | 10 ++++------ .../Microsoft.PowerShell.Utility/Join-String.Tests.ps1 | 3 +-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs index fe25b7f1b0c..0c37c23a560 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs @@ -18,10 +18,11 @@ namespace Microsoft.PowerShell.Commands.Utility [OutputType(typeof(string))] public sealed class JoinStringCommand : PSCmdlet { - private const int Capacity = 50; + /// 50 is an estimate of an upper range value for the number of inputs + /// that is used in the most common scenarios. + private const int InputObjectBufferDefaultCapacity = 50; - // ReSharper disable once CollectionNeverQueried.Local - private readonly List _inputObjects = new List(Capacity); + private readonly List _inputObjects = new List(InputObjectBufferDefaultCapacity); private DynamicPropertyGetter _propGetter = new DynamicPropertyGetter(); /// diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 5fb3ee5ac00..dfd6411a2e2 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -6938,8 +6938,7 @@ IEnumerable IArgumentCompleter.CompleteArgument( CommandAst commandAst, IDictionary fakeBoundParameters) { - var pipelineAst = commandAst.Parent as PipelineAst; - if (pipelineAst == null) + if (!(commandAst.Parent is PipelineAst pipelineAst)) { return null; } @@ -6964,13 +6963,12 @@ IEnumerable IArgumentCompleter.CompleteArgument( return null; } - var astPair = pair as AstPair; - if (astPair?.Argument == null) + if (pair is AstPair astPair && astPair.Argument != null) { - return null; + prevType = AstTypeInference.InferTypeOf(astPair.Argument, typeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); } - prevType = AstTypeInference.InferTypeOf(astPair.Argument, typeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); + return null; } else { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-String.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-String.Tests.ps1 index 99d1f011c2c..49fb84c581a 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-String.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-String.Tests.ps1 @@ -18,7 +18,7 @@ Describe "Join-String" -Tags "CI" { It "Should return a single string" { $actual = $testObject | Join-String - $actual.Count | Should -Be 1 + $actual.Count | Should -Be 1 $actual | Should -BeOfType System.String } @@ -95,5 +95,4 @@ Describe "Join-String" -Tags "CI" { $n -in $completionTexts | Should -BeTrue } } - } From 21eebc8ddbf88404617d9bc9967b787862387a1f Mon Sep 17 00:00:00 2001 From: "Gustafsson, Staffan" Date: Fri, 31 Aug 2018 21:52:57 +0200 Subject: [PATCH 19/24] Making PropertyNameCompleter internal. --- .../engine/CommandCompletion/CompletionCompleters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index dfd6411a2e2..1f8e1a91ca2 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -6910,7 +6910,7 @@ public object VisitParenExpression(ParenExpressionAst parenExpressionAst) /// /// Completes with the property names of the InputObject. /// - public class PropertyNameCompleter : IArgumentCompleter + internal class PropertyNameCompleter : IArgumentCompleter { private readonly string _parameterNameOfInput; From 1cd0a28f429526971e2138f651ded5f580a629cb Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 3 Sep 2018 23:29:21 +0200 Subject: [PATCH 20/24] Addressing most of Bruce's feedback. --- .../commands/utility/join-string.cs | 169 +++++++++--------- .../Microsoft.PowerShell.Utility.psd1 | 4 +- .../Microsoft.PowerShell.Utility.psd1 | 4 +- .../CommandCompletion/CompletionCompleters.cs | 1 - .../Join-String.Tests.ps1 | 48 +++-- 5 files changed, 122 insertions(+), 104 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs index 0c37c23a560..8f1855e892f 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs @@ -4,6 +4,8 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; +using System.Linq; using System.Management.Automation; using System.Management.Automation.Internal; using System.Management.Automation.Language; @@ -18,19 +20,19 @@ namespace Microsoft.PowerShell.Commands.Utility [OutputType(typeof(string))] public sealed class JoinStringCommand : PSCmdlet { - /// 50 is an estimate of an upper range value for the number of inputs - /// that is used in the most common scenarios. - private const int InputObjectBufferDefaultCapacity = 50; - - private readonly List _inputObjects = new List(InputObjectBufferDefaultCapacity); - private DynamicPropertyGetter _propGetter = new DynamicPropertyGetter(); + /// A bigger default to not get re-allocations in common use cases. + private const int DefaultOutputStringCapacity = 256; + private readonly StringBuilder _outputBuilder = new StringBuilder(DefaultOutputStringCapacity); + private string _separator; + private char _quoteChar; + private bool _firstInputObject = true; /// /// Gets or sets the property name or script block to use as the value to join. /// [Parameter(Position = 0)] [ArgumentCompleter(typeof(PropertyNameCompleter))] - public object Property { get; set; } + public PSPropertyExpression Property { get; set; } /// /// Gets or sets the delimiter to join the output with. @@ -38,25 +40,31 @@ public sealed class JoinStringCommand : PSCmdlet [Parameter(Position = 1)] [ArgumentCompleter(typeof(JoinItemCompleter))] [AllowEmptyString] - public string Delimiter { get; set; } + public string Separator + { + get => _separator ?? LanguagePrimitives.ConvertTo(GetVariableValue("OFS")); + set => _separator = value; + } /// /// Gets or sets text to include before the joined input text. /// [Parameter] - public string Prefix { get; set; } + [Alias("op")] + public string OutputPrefix { get; set; } /// /// Gets or sets text to include after the joined input text. /// [Parameter] - public string Suffix { get; set; } + [Alias("os")] + public string OutputSuffix { get; set; } /// /// Gets or sets if the output items should we wrapped in single quotes. /// - [Parameter(ParameterSetName = "Quote")] - public SwitchParameter Quote { get; set; } + [Parameter(ParameterSetName = "SingleQuote")] + public SwitchParameter SingleQuote { get; set; } /// /// Gets or sets if the output items should we wrapped in double quotes. @@ -64,106 +72,67 @@ public sealed class JoinStringCommand : PSCmdlet [Parameter(ParameterSetName = "DoubleQuote")] public SwitchParameter DoubleQuote { get; set; } + /// + /// Gets or sets a format string that is applied to each input object. + /// + [Parameter(ParameterSetName = "Format")] + [ArgumentCompleter(typeof(JoinItemCompleter))] + public string FormatString { get; set; } + /// /// Gets or sets the input object to join into text. /// - [Parameter(ValueFromPipeline = true, Mandatory = true)] + [Parameter(ValueFromPipeline = true)] public PSObject InputObject { get; set; } /// - protected override void ProcessRecord() + protected override void BeginProcessing() { - if (InputObject != null && InputObject != AutomationNull.Value) - { - _inputObjects.Add(InputObject); - } + _quoteChar = SingleQuote ? '\'' : DoubleQuote ? '"' : char.MinValue; } /// - protected override void EndProcessing() + protected override void ProcessRecord() { - if (_inputObjects.Count == 0) - { - return; - } - - var quoteChar = Quote ? '\'' : DoubleQuote ? '"' : char.MinValue; - - const int DefaultOutputStringCapacity = 256; - var builder = new StringBuilder(DefaultOutputStringCapacity); - builder.Append(Prefix); - - if (Delimiter == null) + if (InputObject != null && InputObject != AutomationNull.Value) { - Delimiter = LanguagePrimitives.ConvertTo(GetVariableValue("OFS")); - } + var inputValue = Property == null + ? InputObject + : Property.GetValues(InputObject, false, true).FirstOrDefault()?.Result; + var stringValue = LanguagePrimitives.ConvertTo(inputValue); - void AppendValue(string val) - { - if (quoteChar != char.MinValue) + if (_firstInputObject) { - builder.Append(quoteChar).Append(val).Append(quoteChar); + _outputBuilder.Append(OutputPrefix); + _firstInputObject = false; } else { - builder.Append(val); - } - } - - if (Property == null) - { - AppendValue(LanguagePrimitives.ConvertTo(_inputObjects[0])); - - for (var index = 1; index < _inputObjects.Count; index++) - { - builder.Append(Delimiter); - AppendValue(LanguagePrimitives.ConvertTo(_inputObjects[index])); - } - } - else if (Property is string propertyName) - { - string GetPropertyValueString(PSObject input, string name) - { - return LanguagePrimitives.ConvertTo(_propGetter.GetValue(input, name)); + _outputBuilder.Append(Separator); } - AppendValue(GetPropertyValueString(_inputObjects[0], propertyName)); - - for (var index = 1; index < _inputObjects.Count; index++) + if (_quoteChar != char.MinValue) { - builder.Append(Delimiter); - AppendValue(GetPropertyValueString(_inputObjects[index], propertyName)); + _outputBuilder.Append(_quoteChar); + _outputBuilder.Append(stringValue); + _outputBuilder.Append(_quoteChar); } - } - else if (Property is ScriptBlock sb) - { - string GetScriptBlockResultString(PSObject input) + else if (string.IsNullOrEmpty(FormatString)) { - var result = sb.DoInvokeReturnAsIs( - useLocalScope: true, - errorHandlingBehavior: ScriptBlock.ErrorHandlingBehavior.WriteToCurrentErrorPipe, - dollarUnder: input, - input: AutomationNull.Value, - scriptThis: AutomationNull.Value, - args: Utils.EmptyArray()); - return LanguagePrimitives.ConvertTo(result); + _outputBuilder.Append(stringValue); } - - AppendValue(GetScriptBlockResultString(_inputObjects[0])); - - for (var index = 1; index < _inputObjects.Count; index++) + else { - builder.Append(Delimiter); - AppendValue(GetScriptBlockResultString(_inputObjects[index])); + _outputBuilder.AppendFormat(CultureInfo.CurrentCulture, FormatString, stringValue); } } - else - { - throw new ArgumentException(); - } + } - builder.Append(Suffix); - WriteObject(builder.ToString(), false); + /// + protected override void EndProcessing() + { + _outputBuilder.Append(OutputSuffix); + WriteObject(_outputBuilder.ToString()); } } @@ -175,6 +144,36 @@ public IEnumerable CompleteArgument( string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters) + { + switch (parameterName) + { + case "Separator": return CompleteSeparator(wordToComplete); + case "FormatString": return CompleteFormatString(wordToComplete); + } + + return null; + } + + private IEnumerable CompleteFormatString(string wordToComplete) + { + var res = new List(); + void AddMatching(string completionText) + { + if (completionText.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) + { + res.Add(new CompletionResult(completionText)); + } + } + + AddMatching("'[{0}]'"); + AddMatching("'{0:N2}'"); + AddMatching("\"`r`n `${0}\""); + AddMatching("\"`r`n [string] `${0}\""); + + return res; + } + + private IEnumerable CompleteSeparator(string wordToComplete) { var res = new List(10); diff --git a/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 b/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 index bb6694763b8..e901694f4f9 100644 --- a/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 +++ b/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 @@ -14,9 +14,9 @@ CmdletsToExport= "Format-List", "Format-Custom", "Format-Table", "Format-Wide", "Export-Csv", "Import-Csv", "ConvertTo-Csv", "ConvertFrom-Csv", "Export-Alias", "Invoke-Expression", "Get-Alias", "Get-Culture", "Get-Date", "Get-Host", "Get-Member", "Get-Random", "Get-UICulture", "Get-Unique", "Export-PSSession", "Import-PSSession", "Import-Alias", "Import-LocalizedData", - "Select-String", "Measure-Object", "New-Alias", "New-TimeSpan", "Read-Host", "Set-Alias", "Set-Date", + "Join-String", "Select-String", "Measure-Object", "New-Alias", "New-TimeSpan", "Read-Host", "Set-Alias", "Set-Date", "Start-Sleep", "Tee-Object", "Measure-Command", "Update-TypeData", "Update-FormatData", - "Remove-TypeData", "Get-TypeData", "Write-Host", "Write-Progress", "New-Object", "Select-Object", "Join-String", + "Remove-TypeData", "Get-TypeData", "Write-Host", "Write-Progress", "New-Object", "Select-Object", "Group-Object", "Sort-Object", "Get-Variable", "New-Variable", "Set-Variable", "Remove-Variable", "Clear-Variable", "Export-Clixml", "Import-Clixml", "Import-PowerShellDataFile", "ConvertTo-Xml", "Select-Xml", "Write-Debug", "Write-Verbose", "Write-Warning", "Write-Error", "Write-Information", "Write-Output", "Set-PSBreakpoint", diff --git a/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 b/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 index c57aae9ad4c..316852c1728 100644 --- a/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 +++ b/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 @@ -14,9 +14,9 @@ CmdletsToExport= "Format-List", "Format-Custom", "Format-Table", "Format-Wide", "Export-Csv", "Import-Csv", "ConvertTo-Csv", "ConvertFrom-Csv", "Export-Alias", "Invoke-Expression", "Get-Alias", "Get-Culture", "Get-Date", "Get-Host", "Get-Member", "Get-Random", "Get-UICulture", "Get-Unique", "Export-PSSession", "Import-PSSession", "Import-Alias", "Import-LocalizedData", - "Select-String", "Measure-Object", "New-Alias", "New-TimeSpan", "Read-Host", "Set-Alias", "Set-Date", + "Join-String", "Select-String", "Measure-Object", "New-Alias", "New-TimeSpan", "Read-Host", "Set-Alias", "Set-Date", "Start-Sleep", "Tee-Object", "Measure-Command", "Update-TypeData", "Update-FormatData", - "Remove-TypeData", "Get-TypeData", "Write-Host", "Write-Progress", "New-Object", "Select-Object", "Join-String", + "Remove-TypeData", "Get-TypeData", "Write-Host", "Write-Progress", "New-Object", "Select-Object", "Group-Object", "Sort-Object", "Get-Variable", "New-Variable", "Set-Variable", "Remove-Variable", "Clear-Variable", "Export-Clixml", "Import-Clixml", "Import-PowerShellDataFile","ConvertTo-Xml", "Select-Xml", "Write-Debug", "Write-Verbose", "Write-Warning", "Write-Error", "Write-Information", "Write-Output", "Set-PSBreakpoint", diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 1f8e1a91ca2..0b2a220f16e 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -6980,6 +6980,5 @@ IEnumerable IArgumentCompleter.CompleteArgument( CompletionCompleters.CompleteMemberByInferredType(typeInferenceContext, prevType, result, wordToComplete + "*", filter: CompletionCompleters.IsPropertyMember, isStatic: false); return result; } - } } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-String.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-String.Tests.ps1 index 49fb84c581a..bbd1eefc9e4 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-String.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-String.Tests.ps1 @@ -22,54 +22,60 @@ Describe "Join-String" -Tags "CI" { $actual | Should -BeOfType System.String } - It "Should join property values with default delimiter" { + It "Should join property values with default separator" { $expected = $testObject.Name -join $ofs $actual = $testObject | Join-String -Property Name $actual | Should -BeExactly $expected } - It "Should join property values positionally with default delimiter" { + It "Should join property values positionally with default separator" { $expected = $testObject.Name -join $ofs $actual = $testObject | Join-String Name $actual | Should -BeExactly $expected } - It "Should join property values with custom delimiter" { + It "Should join property values with custom Separator" { $expected = $testObject.Name -join "; " - $actual = $testObject | Join-String -Property Name -Delimiter "; " + $actual = $testObject | Join-String -Property Name -Separator "; " $actual | Should -BeExactly $expected } - It "Should join property values Quoted" { + It "Should join property values SingleQuoted" { $expected = ($testObject.Name).Foreach{"'$_'"} -join "; " - $actual = $testObject | Join-String -Property Name -Delimiter "; " -Quote + $actual = $testObject | Join-String -Property Name -Separator "; " -SingleQuote $actual | Should -BeExactly $expected } It "Should join property values DoubleQuoted" { $expected = ($testObject.Name).Foreach{"""$_"""} -join "; " - $actual = $testObject | Join-String -Property Name -Delimiter "; " -DoubleQuote + $actual = $testObject | Join-String -Property Name -Separator "; " -DoubleQuote $actual | Should -BeExactly $expected } - It "Should join script block results with default delimiter" { + It "Should join property values Formatted" { + $expected = ($testObject.Name).Foreach{"[$_]"} -join "; " + $actual = $testObject | Join-String -Property Name -Separator "; " -Format "[{0}]" + $actual | Should -BeExactly $expected + } + + It "Should join script block results with default separator" { $sb = {$_.Name + $_.Length} $expected = ($testObject | ForEach-Object $sb) -join $ofs $actual = $testObject | Join-String -Property $sb $actual | Should -BeExactly $expected } - It "Should join script block results with custom delimiter" { + It "Should join script block results with custom separator" { $sb = {$_.Name + $_.Length} $expected = ($testObject | ForEach-Object $sb) -join "; " - $actual = $testObject | Join-String -Property $sb -Delimiter "; " + $actual = $testObject | Join-String -Property $sb -Separator "; " $actual | Should -BeExactly $expected } - It "Should join script block results Quoted" { + It "Should join script block results SingleQuoted" { $sb = {$_.Name + $_.Length} $expected = ($testObject | ForEach-Object $sb).Foreach{"'$_'"} -join $ofs - $actual = $testObject | Join-String -Property $sb -Quote + $actual = $testObject | Join-String -Property $sb -SingleQuote $actual | Should -BeExactly $expected } It "Should join script block results DoubleQuoted" { @@ -79,13 +85,26 @@ Describe "Join-String" -Tags "CI" { $actual | Should -BeExactly $expected } - It "Should Handle PreScript and PostScript" { + It "Should join script block results with Format and separator" { + $sb = {$_.Name + $_.Length} + $expected = ($testObject | ForEach-Object $sb).Foreach{"[{0}]" -f $_} -join "; " + $actual = $testObject | Join-String -Property $sb -Separator "; " -Format "[{0}]" + $actual | Should -BeExactly $expected + } + + It "Should Handle OutputPrefix and OutputSuffix" { $ofs = ',' $expected = "A 1,2,3 B" - $actual = 1..3 | Join-String -Prefix "A " -Suffix " B" + $actual = 1..3 | Join-String -OutputPrefix "A " -OutputSuffix " B" $actual | Should -BeExactly $expected } + It "Should handle null separator" { + $expected = -join 'hello'.tochararray() + $actual = "hello" | Join-String -separator $null + $actual | Should -BeExactly $expected + } + It "Should tabcomplete InputObject properties" { $cmd = '[io.fileinfo]::new("c:\temp") | Join-String -Property ' $res = tabexpansion2 $cmd $cmd.length @@ -95,4 +114,5 @@ Describe "Join-String" -Tags "CI" { $n -in $completionTexts | Should -BeTrue } } + } From 98c6edc7b0424b220db18908cb4300cb6c662b06 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Tue, 4 Sep 2018 16:46:19 +0200 Subject: [PATCH 21/24] Moving OutputPrefix to BeginProcessing --- .../commands/utility/join-string.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs index 8f1855e892f..c7140bf68b1 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs @@ -89,6 +89,7 @@ public string Separator protected override void BeginProcessing() { _quoteChar = SingleQuote ? '\'' : DoubleQuote ? '"' : char.MinValue; + _outputBuilder.Append(OutputPrefix); } /// @@ -103,7 +104,6 @@ protected override void ProcessRecord() if (_firstInputObject) { - _outputBuilder.Append(OutputPrefix); _firstInputObject = false; } else From 91d071ec47dd4b9fa477ec7dcfa784168897afa5 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 15 Oct 2018 23:49:12 +0200 Subject: [PATCH 22/24] Addressing Ilya's comments. --- .../commands/utility/join-string.cs | 2 +- .../Join-String.Tests.ps1 | 34 ++++++++----------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs index c7140bf68b1..c3e7102590e 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs @@ -200,7 +200,7 @@ public string NewLineText get { #if UNIX - return Platform.IsMacOS ? "`r" : "`n"; + return "`n"; #else return "`r`n"; #endif diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-String.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-String.Tests.ps1 index bbd1eefc9e4..ce979da8af2 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-String.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Join-String.Tests.ps1 @@ -7,10 +7,6 @@ Describe "Join-String" -Tags "CI" { $testObject = Get-ChildItem } - It "Should be called using an object as piped without error with no switches" { - {$testObject | Join-String } | Should -Not -Throw - } - It "Should be called using the InputObject without error with no other switches" { { Join-String -InputObject $testObject } | Should -Not -Throw } @@ -52,7 +48,7 @@ Describe "Join-String" -Tags "CI" { $actual | Should -BeExactly $expected } - It "Should join property values Formatted" { + It "Should join property values Formatted" { $expected = ($testObject.Name).Foreach{"[$_]"} -join "; " $actual = $testObject | Join-String -Property Name -Separator "; " -Format "[{0}]" $actual | Should -BeExactly $expected @@ -85,7 +81,7 @@ Describe "Join-String" -Tags "CI" { $actual | Should -BeExactly $expected } - It "Should join script block results with Format and separator" { + It "Should join script block results with Format and separator" { $sb = {$_.Name + $_.Length} $expected = ($testObject | ForEach-Object $sb).Foreach{"[{0}]" -f $_} -join "; " $actual = $testObject | Join-String -Property $sb -Separator "; " -Format "[{0}]" @@ -99,20 +95,20 @@ Describe "Join-String" -Tags "CI" { $actual | Should -BeExactly $expected } - It "Should handle null separator" { - $expected = -join 'hello'.tochararray() + It "Should handle null separator" { + $expected = -join 'hello'.tochararray() $actual = "hello" | Join-String -separator $null $actual | Should -BeExactly $expected - } - - It "Should tabcomplete InputObject properties" { - $cmd = '[io.fileinfo]::new("c:\temp") | Join-String -Property ' - $res = tabexpansion2 $cmd $cmd.length - $completionTexts = $res.CompletionMatches.CompletionText - $Propertys = [io.fileinfo]::new($PSScriptRoot).psobject.properties.Name - foreach($n in $Propertys) { - $n -in $completionTexts | Should -BeTrue - } - } + } + + It "Should tabcomplete InputObject properties" { + $cmd = '[io.fileinfo]::new("c:\temp") | Join-String -Property ' + $res = tabexpansion2 $cmd $cmd.length + $completionTexts = $res.CompletionMatches.CompletionText + $Propertys = [io.fileinfo]::new($PSScriptRoot).psobject.properties.Name + foreach ($n in $Propertys) { + $n -in $completionTexts | Should -BeTrue + } + } } From adddfde7c1bc1aa2c39b6aafaa1ca16e197325cb Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Sat, 27 Oct 2018 19:31:11 +0200 Subject: [PATCH 23/24] Adding UseCulture switch to the Format parameter set. --- .../commands/utility/join-string.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs index c3e7102590e..37b69a23f66 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs @@ -23,6 +23,7 @@ public sealed class JoinStringCommand : PSCmdlet /// A bigger default to not get re-allocations in common use cases. private const int DefaultOutputStringCapacity = 256; private readonly StringBuilder _outputBuilder = new StringBuilder(DefaultOutputStringCapacity); + private CultureInfo _cultureInfo = CultureInfo.InvariantCulture; private string _separator; private char _quoteChar; private bool _firstInputObject = true; @@ -79,6 +80,12 @@ public string Separator [ArgumentCompleter(typeof(JoinItemCompleter))] public string FormatString { get; set; } + /// + /// Gets of sets if the current culture should be used with formatting instead of the invariant culture. + /// + [Parameter(ParameterSetName = "Format")] + public SwitchParameter UseCulture { get; set; } + /// /// Gets or sets the input object to join into text. /// @@ -90,6 +97,10 @@ protected override void BeginProcessing() { _quoteChar = SingleQuote ? '\'' : DoubleQuote ? '"' : char.MinValue; _outputBuilder.Append(OutputPrefix); + if (UseCulture) + { + _cultureInfo = CultureInfo.CurrentCulture;; + } } /// @@ -123,7 +134,7 @@ protected override void ProcessRecord() } else { - _outputBuilder.AppendFormat(CultureInfo.CurrentCulture, FormatString, stringValue); + _outputBuilder.AppendFormat(_cultureInfo, FormatString, stringValue); } } } From 937d9b884721666070299d860b75b512d32cf13c Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Sat, 27 Oct 2018 20:48:31 +0200 Subject: [PATCH 24/24] Make TryConvertTo respect the passed formatting provider. --- .../commands/utility/join-string.cs | 13 +++++++---- .../engine/LanguagePrimitives.cs | 2 +- .../engine/MshObject.cs | 23 ++++++++++++++++++- .../engine/parser/Compiler.cs | 4 ++-- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs index 37b69a23f66..35be218ce5d 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs @@ -81,9 +81,9 @@ public string Separator public string FormatString { get; set; } /// - /// Gets of sets if the current culture should be used with formatting instead of the invariant culture. + /// Gets or sets if the current culture should be used with formatting instead of the invariant culture. /// - [Parameter(ParameterSetName = "Format")] + [Parameter] public SwitchParameter UseCulture { get; set; } /// @@ -99,7 +99,7 @@ protected override void BeginProcessing() _outputBuilder.Append(OutputPrefix); if (UseCulture) { - _cultureInfo = CultureInfo.CurrentCulture;; + _cultureInfo = CultureInfo.CurrentCulture; } } @@ -111,7 +111,12 @@ protected override void ProcessRecord() var inputValue = Property == null ? InputObject : Property.GetValues(InputObject, false, true).FirstOrDefault()?.Result; - var stringValue = LanguagePrimitives.ConvertTo(inputValue); + + // conversion to string always succeeds. + if (!LanguagePrimitives.TryConvertTo(inputValue, _cultureInfo, out var stringValue)) + { + throw new PSInvalidCastException("InvalidCastFromAnyTypeToString", ExtendedTypeSystem.InvalidCastCannotRetrieveString, null); + } if (_firstInputObject) { diff --git a/src/System.Management.Automation/engine/LanguagePrimitives.cs b/src/System.Management.Automation/engine/LanguagePrimitives.cs index 0c954de68d6..017eee6e308 100644 --- a/src/System.Management.Automation/engine/LanguagePrimitives.cs +++ b/src/System.Management.Automation/engine/LanguagePrimitives.cs @@ -3243,7 +3243,7 @@ private static string ConvertNonNumericToString(object valueToConvert, try { typeConversion.WriteLine("Converting object to string."); - return PSObject.ToStringParser(ecFromTLS, valueToConvert); + return PSObject.ToStringParser(ecFromTLS, valueToConvert, formatProvider); } catch (ExtendedTypeSystemException e) { diff --git a/src/System.Management.Automation/engine/MshObject.cs b/src/System.Management.Automation/engine/MshObject.cs index cf5c4defd3d..aaba1fc91d1 100644 --- a/src/System.Management.Automation/engine/MshObject.cs +++ b/src/System.Management.Automation/engine/MshObject.cs @@ -1207,10 +1207,31 @@ private static string ToStringEmptyBaseObject(ExecutionContext context, PSObject /// When there is a brokered ToString but it failed, or when the ToString on obj throws an exception. /// internal static string ToStringParser(ExecutionContext context, object obj) + { + return ToStringParser(context, obj, CultureInfo.InvariantCulture); + } + + /// + /// Returns the string representation of obj. + /// + /// ExecutionContext used to fetch the separator. + /// + /// object we are trying to call ToString on. If this is not an PSObject we try + /// enumerating and if that fails we call obj.ToString. + /// If this is an PSObject, we look for a brokered ToString. + /// If it is not present, and the BaseObject is null we try listing the properties. + /// If the BaseObject is not null we try enumerating. If that fails we try the BaseObject's ToString. + /// + /// The formatProvider to be passed to ToString. + /// A string representation of the object. + /// + /// When there is a brokered ToString but it failed, or when the ToString on obj throws an exception. + /// + internal static string ToStringParser(ExecutionContext context, object obj, IFormatProvider formatProvider) { try { - return ToString(context, obj, null, null, CultureInfo.InvariantCulture, true, true); + return ToString(context, obj, null, null, formatProvider, true, true); } catch (ExtendedTypeSystemException etse) { diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs index 41084c4db2b..d2814ed129b 100644 --- a/src/System.Management.Automation/engine/parser/Compiler.cs +++ b/src/System.Management.Automation/engine/parser/Compiler.cs @@ -368,7 +368,7 @@ internal static class CachedReflectionInfo internal static readonly FieldInfo PSObject_isDeserialized = typeof(PSObject).GetField(nameof(PSObject.isDeserialized), instanceFlags); internal static readonly MethodInfo PSObject_ToStringParser = - typeof(PSObject).GetMethod(nameof(PSObject.ToStringParser), staticFlags); + typeof(PSObject).GetMethod(nameof(PSObject.ToStringParser), staticFlags, null, new[]{typeof(ExecutionContext), typeof(object)}, null); internal static readonly PropertyInfo PSReference_Value = typeof(PSReference).GetProperty(nameof(PSReference.Value)); @@ -1535,7 +1535,7 @@ private static RuntimeDefinedParameter GetRuntimeDefinedParameter(ParameterAst p if (attribute is ExperimentalAttribute expAttribute) { - // Only honor the first seen experimental attribute, ignore the others. + // Only honor the first seen experimental attribute, ignore the others. if (!hasSeenExpAttribute && expAttribute.ToHide) { return null; } // Do not add experimental attributes to the attribute list.