Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6997286
Fix disposing of powershell in TypeInferenceContext if it was created…
powercode Aug 29, 2018
1977dc2
Adding a PropertyNameCompleter custom completer.
powercode Aug 29, 2018
ba17c6c
Adding Join-Object command
powercode Aug 29, 2018
b34453e
Adding tests
powercode Aug 29, 2018
cf646c1
Reverting incorrect dispose in TypeInferenceVisitor
powercode Aug 31, 2018
32fd136
Addressing Pauls review comments.
powercode Aug 31, 2018
acbdbf9
Early out when inputobject count == 0.
powercode Aug 31, 2018
bda1740
Fixing missing TypeInferenceVisitor Dispose change.
powercode Aug 31, 2018
2995f92
Renaming Join-Object => Join-String.
powercode Aug 31, 2018
2a0b56a
newline at eof.
powercode Aug 31, 2018
c68da1f
Renaming files *-object -> *-string
powercode Aug 31, 2018
68c0f57
platform specific newline completion text.
powercode Aug 31, 2018
6f39f8e
Ignore null and automationnull input.
powercode Aug 31, 2018
28224d9
Moving the early exit to the top of the function.
powercode Aug 31, 2018
2fac926
Codefactor fixes.
powercode Aug 31, 2018
6493e7d
Adding Join-String to defaultcommands test.
powercode Aug 31, 2018
e3d6fcc
sorting cmdlet names.
powercode Aug 31, 2018
0694362
Addressing Ilya's feedback
powercode Aug 31, 2018
21eebc8
Making PropertyNameCompleter internal.
powercode Aug 31, 2018
1cd0a28
Addressing most of Bruce's feedback.
powercode Sep 3, 2018
98c6edc
Moving OutputPrefix to BeginProcessing
powercode Sep 4, 2018
91d071e
Addressing Ilya's comments.
powercode Oct 15, 2018
adddfde
Adding UseCulture switch to the Format parameter set.
powercode Oct 27, 2018
937d9b8
Make TryConvertTo<string> respect the passed formatting provider.
powercode Oct 27, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

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;
using System.Text;

namespace Microsoft.PowerShell.Commands.Utility
{
/// <summary>
/// Join-Object implementation.
/// </summary>
[Cmdlet(VerbsCommon.Join, "String", RemotingCapability = RemotingCapability.None, DefaultParameterSetName = "default")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need open a tracking issue for HelpUri after merge thePR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know much about this. Who does documentation?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well - it would be nice if the developer wrote it :-) But if not, then the powershell dev/pm team will end up writing it with the docs team providing editorial coverage and sanity checking.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the developer who wrote it excels at documentation in English. Rumor has it that he was willing to write it in the language of the Gods and the Heroes, though.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gods and Heroes are cool :-) Then we could run it through machine translation and - oh - never mind :-)

[OutputType(typeof(string))]
public sealed class JoinStringCommand : PSCmdlet
{
/// <summary>A bigger default to not get re-allocations in common use cases.</summary>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use file pattern:

/// <Summary>
/// A bigger default to not get re-allocations in common use cases.
/// </Summary> 

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;

/// <summary>
/// Gets or sets the property name or script block to use as the value to join.
/// </summary>
[Parameter(Position = 0)]
[ArgumentCompleter(typeof(PropertyNameCompleter))]
public PSPropertyExpression Property { get; set; }

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be of type Microsoft.PowerShell.Commands.PSPropertyExpression and use its capabilities.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/cc @mklement0 Interesting example for you. :-)

/// <summary>
/// Gets or sets the delimiter to join the output with.
/// </summary>
[Parameter(Position = 1)]
[ArgumentCompleter(typeof(JoinItemCompleter))]
[AllowEmptyString]
public string Separator
{
get => _separator ?? LanguagePrimitives.ConvertTo<string>(GetVariableValue("OFS"));
set => _separator = value;
}

/// <summary>
/// Gets or sets text to include before the joined input text.
/// </summary>
[Parameter]
[Alias("op")]
public string OutputPrefix { get; set; }

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in "prefixes the entire output string" - right? So maybe they should be -OuputPrefix (-op) and -OutputSuffix (-os) instead? (My first thought was that they prefixed/suffixed the individual elements rather than the whole string.)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we'll add -Format perhaps the properties could be removed at all.

/// <summary>
/// Gets or sets text to include after the joined input text.
/// </summary>
[Parameter]
[Alias("os")]
public string OutputSuffix { get; set; }

/// <summary>
/// Gets or sets if the output items should we wrapped in single quotes.
/// </summary>
[Parameter(ParameterSetName = "SingleQuote")]
public SwitchParameter SingleQuote { get; set; }

/// <summary>
/// Gets or sets if the output items should we wrapped in double quotes.
/// </summary>
[Parameter(ParameterSetName = "DoubleQuote")]
public SwitchParameter DoubleQuote { get; set; }

/// <summary>
/// Gets or sets a format string that is applied to each input object.
/// </summary>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd expand the comment with what is the format and/or add a link on docs.

[Parameter(ParameterSetName = "Format")]
[ArgumentCompleter(typeof(JoinItemCompleter))]
public string FormatString { get; set; }

/// <summary>
/// Gets or sets if the current culture should be used with formatting instead of the invariant culture.
/// </summary>
[Parameter]
public SwitchParameter UseCulture { get; set; }

/// <summary>
/// Gets or sets the input object to join into text.
/// </summary>
[Parameter(ValueFromPipeline = true)]
public PSObject InputObject { get; set; }

/// <inheritdoc />
protected override void BeginProcessing()
{
_quoteChar = SingleQuote ? '\'' : DoubleQuote ? '"' : char.MinValue;
_outputBuilder.Append(OutputPrefix);
if (UseCulture)
{
_cultureInfo = CultureInfo.CurrentCulture;
}
}

/// <inheritdoc />
protected override void ProcessRecord()
{
if (InputObject != null && InputObject != AutomationNull.Value)
{
var inputValue = Property == null
? InputObject
: Property.GetValues(InputObject, false, true).FirstOrDefault()?.Result;

// conversion to string always succeeds.
if (!LanguagePrimitives.TryConvertTo<string>(inputValue, _cultureInfo, out var stringValue))
{
throw new PSInvalidCastException("InvalidCastFromAnyTypeToString", ExtendedTypeSystem.InvalidCastCannotRetrieveString, null);
}

if (_firstInputObject)
{
_firstInputObject = false;
}
else
{
_outputBuilder.Append(Separator);
}

if (_quoteChar != char.MinValue)
{
_outputBuilder.Append(_quoteChar);
_outputBuilder.Append(stringValue);
_outputBuilder.Append(_quoteChar);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use

_outputBuilder.Append(_quoteChar).Append(stringValue).(_quoteChar); 

}
else if (string.IsNullOrEmpty(FormatString))
{
_outputBuilder.Append(stringValue);
}
else
{
_outputBuilder.AppendFormat(_cultureInfo, FormatString, stringValue);
}
}
}

/// <inheritdoc />
protected override void EndProcessing()
{
_outputBuilder.Append(OutputSuffix);
WriteObject(_outputBuilder.ToString());
}
}

internal class JoinItemCompleter : IArgumentCompleter
{
public IEnumerable<CompletionResult> CompleteArgument(
string commandName,
string parameterName,
string wordToComplete,
CommandAst commandAst,
IDictionary fakeBoundParameters)
{
switch (parameterName)
{
case "Separator": return CompleteSeparator(wordToComplete);
case "FormatString": return CompleteFormatString(wordToComplete);
}

return null;
}

private IEnumerable<CompletionResult> CompleteFormatString(string wordToComplete)
{
var res = new List<CompletionResult>();
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<CompletionResult> CompleteSeparator(string wordToComplete)
{
var res = new List<CompletionResult>(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
{
#if UNIX
return "`n";
#else
return "`r`n";
#endif
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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",
"Group-Object", "Sort-Object", "Get-Variable", "New-Variable", "Set-Variable", "Remove-Variable",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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",
"Group-Object", "Sort-Object", "Get-Variable", "New-Variable", "Set-Variable", "Remove-Variable",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3746,7 +3746,7 @@ private static void NativeCompletionMemberName(CompletionContext context, List<C
prevType = AstTypeInference.InferTypeOf(pipelineAst.PipelineElements[i - 1], context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval);
}

CompleteMemberByInferredType(context, prevType, result, context.WordToComplete + "*", filter: IsPropertyMember, isStatic: false);
CompleteMemberByInferredType(context.TypeInferenceContext, prevType, result, context.WordToComplete + "*", filter: IsPropertyMember, isStatic: false);
result.Add(CompletionResult.Null);
}

Expand Down Expand Up @@ -5022,7 +5022,7 @@ internal static List<CompletionResult> 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
{
Expand Down Expand Up @@ -5126,7 +5126,7 @@ private static bool IsInDscContext(ExpressionAst expression)
return Ast.GetAncestorAst<ConfigurationDefinitionAst>(expression) != null;
}

private static void CompleteMemberByInferredType(CompletionContext context, IEnumerable<PSTypeName> inferredTypes, List<CompletionResult> results, string memberName, Func<object, bool> filter, bool isStatic)
internal static void CompleteMemberByInferredType(TypeInferenceContext context, IEnumerable<PSTypeName> inferredTypes, List<CompletionResult> results, string memberName, Func<object, bool> filter, bool isStatic)
{
bool extensionMethodsAdded = false;
HashSet<string> typeNameUsed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
Expand All @@ -5137,8 +5137,9 @@ 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);
Expand Down Expand Up @@ -5271,7 +5272,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
Expand Down Expand Up @@ -6149,7 +6150,7 @@ internal static List<CompletionResult> CompleteHashtableKey(CompletionContext co
{
var result = new List<CompletionResult>();
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;
}
Expand Down Expand Up @@ -6260,7 +6261,7 @@ internal static List<CompletionResult> CompleteHashtableKey(CompletionContext co
var inferredType = AstTypeInference.InferTypeOf(commandAst, completionContext.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval);
var result = new List<CompletionResult>();
CompleteMemberByInferredType(
completionContext, inferredType,
completionContext.TypeInferenceContext, inferredType,
result, completionContext.WordToComplete + "*", IsWriteablePropertyMember, isStatic: false);
return result;
case "Select-Object":
Expand Down Expand Up @@ -6905,4 +6906,79 @@ public object VisitParenExpression(ParenExpressionAst parenExpressionAst)
return parenExpressionAst.Pipeline.Accept(this);
}
}

/// <summary>
/// Completes with the property names of the InputObject.
/// </summary>
internal class PropertyNameCompleter : IArgumentCompleter
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a useful class. perhaps it should be public?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the cmdlet already approved by PowerShell Committee? If so we can merge "as is" and consider this api later.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are gonna discuss that, please consider making AstTypeInference public too. I need it from EditorServices.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had it public in my initial commit, as I believed it to be generally useful. Changed it to internal after @PaulHigin feedback.

{
private readonly string _parameterNameOfInput;

/// <summary>
/// Initializes a new instance of the <see cref="PropertyNameCompleter"/> class.
/// </summary>
public PropertyNameCompleter()
{
_parameterNameOfInput = "InputObject";
}

/// <summary>
/// Initializes a new instance of the <see cref="PropertyNameCompleter"/> class.
/// </summary>
/// <param name="parameterNameOfInput">The name of the property of the input object for witch to complete with property names.</param>
public PropertyNameCompleter(string parameterNameOfInput)
{
_parameterNameOfInput = parameterNameOfInput;
}

IEnumerable<CompletionResult> IArgumentCompleter.CompleteArgument(
string commandName,
string parameterName,
string wordToComplete,
CommandAst commandAst,
IDictionary fakeBoundParameters)
{
if (!(commandAst.Parent is PipelineAst pipelineAst))
{
return null;
}

int i;
for (i = 0; i < pipelineAst.PipelineElements.Count; i++)
{
if (pipelineAst.PipelineElements[i] == commandAst)
{
break;
}
}

var typeInferenceContext = new TypeInferenceContext();
IEnumerable<PSTypeName> 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;
}

if (pair is AstPair astPair && astPair.Argument != null)
{
prevType = AstTypeInference.InferTypeOf(astPair.Argument, typeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval);
}

return null;
}
else
{
prevType = AstTypeInference.InferTypeOf(pipelineAst.PipelineElements[i - 1], typeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval);
}

var result = new List<CompletionResult>();

CompletionCompleters.CompleteMemberByInferredType(typeInferenceContext, prevType, result, wordToComplete + "*", filter: CompletionCompleters.IsPropertyMember, isStatic: false);
return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Loading