diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs
new file mode 100644
index 00000000000..35be218ce5d
--- /dev/null
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/join-string.cs
@@ -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
+{
+ ///
+ /// Join-Object implementation.
+ ///
+ [Cmdlet(VerbsCommon.Join, "String", RemotingCapability = RemotingCapability.None, DefaultParameterSetName = "default")]
+ [OutputType(typeof(string))]
+ 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;
+
+ ///
+ /// Gets or sets the property name or script block to use as the value to join.
+ ///
+ [Parameter(Position = 0)]
+ [ArgumentCompleter(typeof(PropertyNameCompleter))]
+ public PSPropertyExpression Property { get; set; }
+
+ ///
+ /// Gets or sets the delimiter to join the output with.
+ ///
+ [Parameter(Position = 1)]
+ [ArgumentCompleter(typeof(JoinItemCompleter))]
+ [AllowEmptyString]
+ public string Separator
+ {
+ get => _separator ?? LanguagePrimitives.ConvertTo(GetVariableValue("OFS"));
+ set => _separator = value;
+ }
+
+ ///
+ /// Gets or sets text to include before the joined input text.
+ ///
+ [Parameter]
+ [Alias("op")]
+ public string OutputPrefix { get; set; }
+
+ ///
+ /// Gets or sets text to include after the joined input text.
+ ///
+ [Parameter]
+ [Alias("os")]
+ public string OutputSuffix { get; set; }
+
+ ///
+ /// Gets or sets if the output items should we wrapped in single quotes.
+ ///
+ [Parameter(ParameterSetName = "SingleQuote")]
+ public SwitchParameter SingleQuote { 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 a format string that is applied to each input object.
+ ///
+ [Parameter(ParameterSetName = "Format")]
+ [ArgumentCompleter(typeof(JoinItemCompleter))]
+ public string FormatString { get; set; }
+
+ ///
+ /// Gets or sets if the current culture should be used with formatting instead of the invariant culture.
+ ///
+ [Parameter]
+ public SwitchParameter UseCulture { get; set; }
+
+ ///
+ /// Gets or sets the input object to join into text.
+ ///
+ [Parameter(ValueFromPipeline = true)]
+ public PSObject InputObject { get; set; }
+
+ ///
+ protected override void BeginProcessing()
+ {
+ _quoteChar = SingleQuote ? '\'' : DoubleQuote ? '"' : char.MinValue;
+ _outputBuilder.Append(OutputPrefix);
+ if (UseCulture)
+ {
+ _cultureInfo = CultureInfo.CurrentCulture;
+ }
+ }
+
+ ///
+ 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(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);
+ }
+ else if (string.IsNullOrEmpty(FormatString))
+ {
+ _outputBuilder.Append(stringValue);
+ }
+ else
+ {
+ _outputBuilder.AppendFormat(_cultureInfo, FormatString, stringValue);
+ }
+ }
+ }
+
+ ///
+ protected override void EndProcessing()
+ {
+ _outputBuilder.Append(OutputSuffix);
+ WriteObject(_outputBuilder.ToString());
+ }
+ }
+
+ internal class JoinItemCompleter : IArgumentCompleter
+ {
+ public IEnumerable 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 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);
+
+ 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
+ }
+ }
+ }
+}
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..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,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",
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..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,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",
diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs
index c20057c397c..0b2a220f16e 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