-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Join-String cmdlet for creating text from pipeline input #7660
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6997286
1977dc2
ba17c6c
b34453e
cf646c1
32fd136
acbdbf9
bda1740
2995f92
2a0b56a
c68da1f
68c0f57
6f39f8e
28224d9
2fac926
6493e7d
e3d6fcc
0694362
21eebc8
1cd0a28
98c6edc
91d071e
adddfde
937d9b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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")] | ||
| [OutputType(typeof(string))] | ||
| public sealed class JoinStringCommand : PSCmdlet | ||
| { | ||
| /// <summary>A bigger default to not get re-allocations in common use cases.</summary> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; } | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be of type
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; } | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.)
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we'll add |
||
| /// <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> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; } | ||
iSazonov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// <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; | ||
powercode marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| else | ||
| { | ||
| _outputBuilder.Append(Separator); | ||
| } | ||
|
|
||
| if (_quoteChar != char.MinValue) | ||
| { | ||
| _outputBuilder.Append(_quoteChar); | ||
| _outputBuilder.Append(stringValue); | ||
| _outputBuilder.Append(_quoteChar); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
|---|---|---|
|
|
@@ -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); | ||
| } | ||
|
|
||
|
|
@@ -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 | ||
| { | ||
|
|
@@ -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) | ||
powercode marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| bool extensionMethodsAdded = false; | ||
| HashSet<string> typeNameUsed = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | ||
|
|
@@ -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); | ||
|
|
@@ -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 | ||
|
|
@@ -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; | ||
| } | ||
|
|
@@ -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": | ||
|
|
@@ -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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like a useful class. perhaps it should be public?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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++) | ||
powercode marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| 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; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :-)