X Tutup
The Wayback Machine - https://web.archive.org/web/20260202030752/https://github.com/PowerShell/PowerShell/pull/24648
Skip to content

Conversation

@ArmaanMcleod
Copy link
Contributor

@ArmaanMcleod ArmaanMcleod commented Dec 7, 2024

PR Summary

Add -Delimiter & -NoTrailingDelimiter parameters to Set-Content/Add-Content cmdlets.

PR Context

Fixes #23906.

I've added new parameters to Set-Content/Add-Content so a user can specify their own delimiters using -Delimiter when writing to files.

This included making changes to WriteContentCommandBase class to process all lines into a buffer in ProcessRecord() instead of writing one at a time, and do all the writing in EndProcessing() so we can append delimiters but also make sure the trailing delimiter can be suppressed using -NoTrailingDelimiter. This seemed much simpler to do since Value can be used with a pipeline which makes it difficult to detect the last line and suppress the final delimiter.

Update from above

When -NoTrailingDelimiter is specified, all delimiters are prepended to all lines except first line. Otherwise, we fall back to default behaviour and prepend delimiter after every line. This approach is more ideal than the previous buffering approach because streaming behaviour of input lines is maintained.

I also needed to make changes to the FileSystemProvider method IContentWriter GetContentWriter(string path) to handle new dynamic parameters in FileSystemContentWriterDynamicParameters for delimiters and also include a new overload for FileSystemContentReaderWriter. There is also some error handling if we use parameter combinations which are not supported.

Example 1

Write contents to file using delimiters:

> "a", "b", "c" | Set-Content -Path test.txt -Delimiter ","
> cat ./test.txt
a,b,c,

Example 2

Write contents to file using comma delimiter and remove trailing delimiter:

> "a", "b", "c" | Set-Content -Path test.txt -Delimiter "," -NoTrailingDelimiter
> cat ./test.txt
a,b,c

PR Checklist

@ArmaanMcleod
Copy link
Contributor Author

ArmaanMcleod commented Dec 7, 2024

@mklement0 Please review if you get a chance. I've added the delimiter parameters in, I will tackle the -NoTrailingNewLine parameter in another PR when we can get the other issue #5108 re-opened again 😄.

@mklement0
Copy link
Contributor

I'll leave the review to others, @ArmaanMcleod, but thank you for tackling this.

@microsoft-github-policy-service microsoft-github-policy-service bot added the Review - Needed The PR is being reviewed label Dec 15, 2024
@JunielKatarn
Copy link

Does this also apply to Out-File?
If not, why is it excluded?

@mklement0
Copy link
Contributor

@JunielKatarn, please see #23906 (comment)

@adityapatwardhan
Copy link
Member

Can the behavior of -Delimiter + -NoTrailingDelimiter be not achieved as follows

PS> "a", "b", "c" | Join-String -Separator "," | Set-Content test.txt
PS> cat test.txt
a,b,c

@daxian-dbw
Copy link
Member

This included making changes to WriteContentCommandBase class to process all lines into a buffer in ProcessRecord() instead of writing one at a time, and do all the writing in EndProcessing() so we can append delimiters but also make sure the trailing delimiter can be suppressed using -NoTrailingDelimiter.

This breaks the streaming behavior. What if tons of strings are fed into the pipeline? Holding them up in a buffer could drastically increase the memory usage and might even cause an out-of-memory exception.

@ArmaanMcleod
Copy link
Contributor Author

This included making changes to WriteContentCommandBase class to process all lines into a buffer in ProcessRecord() instead of writing one at a time, and do all the writing in EndProcessing() so we can append delimiters but also make sure the trailing delimiter can be suppressed using -NoTrailingDelimiter.

This breaks the streaming behavior. What if tons of strings are fed into the pipeline? Holding them up in a buffer could drastically increase the memory usage and might even cause an out-of-memory exception.

This effectively is what ConvertFrom-Json does as well.

@daxian-dbw
Copy link
Member

ConvertFrom-Json is different. You need to wait for all the JSON content to be available to do the parsing, or otherwise it will fail on an in-complete JSON text.

But still, ConvertFrom-Json could be improved with a -Stream switch for the scenarios where it gets JSON Lines fed from the pipeline.

@ArmaanMcleod
Copy link
Contributor Author

ConvertFrom-Json is different. You need to wait for all the JSON content to be available to do the parsing, or otherwise it will fail on an in-complete JSON text.

But still, ConvertFrom-Json could be improved with a -Stream switch for the scenarios where it gets JSON Lines fed from the pipeline.

Yeah its a similar issue with this feature as well since we need to detect last item which requires loading all lines into buffer. I wonder though if we can just keep streaming behaviour and only buffer if user wants to use these flags, unless I am missing something and this can all be achieved with streaming?

Might make command too complex for what its worth though, and still keeps performance issue around just to save some keystrokes and reduce pipelining.

Happy to close PR if this is not desired 🙂.

Copilot AI review requested due to automatic review settings December 5, 2025 01:04
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds new -Delimiter and -NoTrailingDelimiter parameters to the Set-Content and Add-Content cmdlets, allowing users to specify custom delimiters when writing content to files. The implementation includes a significant architectural change: content is now buffered in ProcessRecord() and written in EndProcessing() to enable proper handling of the trailing delimiter suppression. The PR includes comprehensive test coverage, parameter validation, and appropriate error messages for invalid parameter combinations.

Key Changes:

  • Added -Delimiter and -NoTrailingDelimiter parameters to enable custom delimiter control when writing content
  • Refactored content writing to use a buffering approach, accumulating content during pipeline processing and writing once in EndProcessing()
  • Added validation logic to prevent incompatible parameter combinations (Delimiter with NoNewLine/AsByteStream, NoTrailingDelimiter without Delimiter)

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
test/powershell/Modules/Microsoft.PowerShell.Management/Set-Content.Tests.ps1 Adds comprehensive test coverage for delimiter functionality including parameter validation, various delimiter types, and nested content handling
test/powershell/Modules/Microsoft.PowerShell.Management/Add-Content.Tests.ps1 Adds comprehensive test coverage for delimiter functionality with append operations, and removes trailing whitespace
src/System.Management.Automation/resources/FileSystemProviderStrings.resx Adds error message strings for invalid parameter combinations
src/System.Management.Automation/namespaces/FileSystemProvider.cs Adds dynamic parameters (Delimiter, NoTrailingDelimiter) and validation logic to GetContentWriter method
src/System.Management.Automation/namespaces/FileSystemContentStream.cs Adds new constructor overload for delimiter support and refactors Write method to handle delimiter insertion and trailing delimiter suppression
src/Microsoft.PowerShell.Commands.Management/commands/management/WriteContentCommandBase.cs Refactors ProcessRecord to buffer content and EndProcessing to perform actual writing, enabling proper last-item detection for delimiter suppression

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

string streamName = null;
bool suppressNewline = false;

string delimiter = "\n";
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

The default delimiter value is hardcoded as "\n" here, but FileSystemContentReaderWriter uses a constant DefaultDelimiter = '\n'. Consider using a consistent approach, either by using the same constant or ensuring the default values match. This helps maintain consistency and makes future changes easier.

Suggested change
string delimiter = "\n";
string delimiter = FileSystemContentReaderWriter.DefaultDelimiter;

Copilot uses AI. Check for mistakes.
ArmaanMcleod and others added 8 commits December 5, 2025 12:56
…ntent.Tests.ps1

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ntent.Tests.ps1

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…trings.resx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…trings.resx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…tream.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
ArmaanMcleod and others added 2 commits December 5, 2025 13:07
…ntent.Tests.ps1

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@ArmaanMcleod
Copy link
Contributor Author

@daxian-dbw I think I found a good solution here.

Instead of buffering all lines to handle last line trailing delimiter, we can maintain streaming behaviour if we just prepend delimiter after the first line if last delimiter needs to be suppressed with -NoTrailingDelimiter. Otherwise, we just append delimiter normally.

I am not sure why I didn't do this before, but I think there should be no performance issue now 🙂.

Comment on lines 1198 to 1202
// Always append delimter if we don't suppress last delimiter
if (!_suppressLastDelimiter)
{
_writer.Write(_delimiter);
}
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 assign _delimiter in Beginning as needed and write here unconditionally.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I kept same _usingDelimiter and _delimiter flags and set them in new FileSystemContentReaderWriter constructor. Seemed much simpler to manage it this way.

Logic is simpler in f6378d3 now. Default should be to append delimiter unless we need to prepend after first line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Apologise above actually doesn't work, I have split the code paths using _suppressLastDelimiter in 6255eb3.

/// Gets the status of the delimiter parameter. Returns true
/// if the delimiter was explicitly specified by the user, false otherwise.
/// </summary>
public bool DelimiterSpecified
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we really need this. It seems we could simply check for default value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Existing dynamic parameters code checked for this flag, I just maintained it as well for this to keep it consistent. I think it can be simplified but perhaps in another PR when both FileSystemContentWriterDynamicParameters and FileSystemContentReaderDynamicParameters is consolidated.

We also should also centralise the default delimiter "\n" in a better place, it seems it is also hardcoded in many places.

@iSazonov iSazonov added the CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log label Dec 5, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log Review - Needed The PR is being reviewed

Projects

Development

Successfully merging this pull request may close these issues.

Add Delimiter or NewLine argument to Set-Content

6 participants

X Tutup