-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Add -Extension parameter to Join-Path cmdlet #26482
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
Add -Extension parameter to Join-Path cmdlet #26482
Conversation
This adds a new -Extension parameter to Join-Path that allows changing the file extension of the resulting path, providing parity with Split-Path's extension handling capabilities. Features: - Change extension: Join-Path -Path C:\folder -ChildPath file.txt -Extension .log - Add extension: Join-Path -Path C:\folder -ChildPath file -Extension .txt - Remove extension: Join-Path -Path C:\folder -ChildPath file.txt -Extension '' - Supports pipeline input via ValueFromPipelineByPropertyName Fixes PowerShell#20724
src/Microsoft.PowerShell.Commands.Management/commands/management/CombinePathCommand.cs
Outdated
Show resolved
Hide resolved
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.
Pull Request Overview
This PR adds a new -Extension parameter to the Join-Path cmdlet, enabling users to modify file extensions when joining paths. This provides parity with Split-Path's extension handling and eliminates the need for separate extension manipulation after path joining.
Key changes:
- Added
-Extensionparameter with pipeline by property name support toJoinPathCommand - Implemented extension modification using
System.IO.Path.ChangeExtensionwith special handling for empty string extensions - Added 6 test cases covering basic extension operations and pipeline input
Reviewed Changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| src/Microsoft.PowerShell.Commands.Management/commands/management/CombinePathCommand.cs | Adds the -Extension parameter property and implements the extension change logic in ProcessRecord() after path joining but before resolution |
| test/powershell/Modules/Microsoft.PowerShell.Management/Join-Path.Tests.ps1 | Adds 6 test cases covering extension change, addition, removal, multiple paths, dot handling, and pipeline input |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
test/powershell/Modules/Microsoft.PowerShell.Management/Join-Path.Tests.ps1
Outdated
Show resolved
Hide resolved
src/Microsoft.PowerShell.Commands.Management/commands/management/CombinePathCommand.cs
Show resolved
Hide resolved
src/Microsoft.PowerShell.Commands.Management/commands/management/CombinePathCommand.cs
Outdated
Show resolved
Hide resolved
test/powershell/Modules/Microsoft.PowerShell.Management/Join-Path.Tests.ps1
Outdated
Show resolved
Hide resolved
src/Microsoft.PowerShell.Commands.Management/commands/management/CombinePathCommand.cs
Outdated
Show resolved
Hide resolved
test/powershell/Modules/Microsoft.PowerShell.Management/Join-Path.Tests.ps1
Outdated
Show resolved
Hide resolved
test/powershell/Modules/Microsoft.PowerShell.Management/Join-Path.Tests.ps1
Outdated
Show resolved
Hide resolved
…nt/CombinePathCommand.cs Co-authored-by: Copilot <[email protected]>
…nt/CombinePathCommand.cs Co-authored-by: Copilot <[email protected]>
- Use string.TrimEnd('.') instead of Substring for cleaner code
- Remove redundant EndsWith check
- Add tests for multiple dots, directory-like paths, and multiple child paths
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.
Pull Request Overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
test/powershell/Modules/Microsoft.PowerShell.Management/Join-Path.Tests.ps1
Outdated
Show resolved
Hide resolved
src/Microsoft.PowerShell.Commands.Management/commands/management/CombinePathCommand.cs
Outdated
Show resolved
Hide resolved
…nt/CombinePathCommand.cs Co-authored-by: Copilot <[email protected]>
Use $SepChar variable instead of hardcoded backslashes in test expectations to ensure tests pass on both Windows and Unix platforms.
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.
Pull Request Overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
test/powershell/Modules/Microsoft.PowerShell.Management/Join-Path.Tests.ps1
Show resolved
Hide resolved
|
@brianary Please review. |
|
@yotsuda Please open new issue in https://github.com/MicrosoftDocs/PowerShell-Docs
|
| // Remove trailing dot when extension is empty string | ||
| if (Extension.Length == 0) | ||
| { | ||
| joinedPath = joinedPath.TrimEnd('.'); | ||
| } |
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.
@mklement0 What is your opinion about this?
@yotsuda Have we test for the code path?
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.
@iSazonov Yes, this code path is tested at line 95-97 in Join-Path.Tests.ps1.
However, I'm considering adding another test case to document the behavior of TrimEnd('.') with multiple trailing dots (e.g., file..txt → file). This would clarify the intentional difference from the original Substring approach.
Should I add this test, or is the current coverage sufficient?
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.
@yotsuda Lets wait @mklement0 feedback.
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.
Technically, it amounts to a deviation from the underlying .NET API, because [io.path]::ChangeExtension('x.txt', '') yields 't.', i.e. retains the trailing .; by contrast, the trailing . is removed if you pass a true null ([io.path]::ChangeExtension('x.txt', [nullstring]::value)).
That said, making a '' argument exhibit the latter behavior - i.e. removal of the (last) extension - makes more sense to me, not least because you cannot really files with names that end in . on Windows (the trailing . is in effect ignored; not on Unix-like platforms, though).
The simpler and more robust implementation, though, is to simply translate the empty string to null and let .ChangeExtension() deal with it - it only removes the last extension present, and each lone . is considered its own extension; e.g. [io.path]::ChangeExtension('x...', [nullstring]::value) yields 'x..'.
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.
Note that if we wanted to be consistent with .NET, users would have to - awkwardly - pass ([NullString]::Value) as the -Extension argument in order to achieve extension removal.
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.
-Extension [NullString]::Value effectively ignore the parameter.
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.
-Extension [NullString]::Value would pass verbatim [NullString]::Value as the parameter value, so I assume you mean -Extension ([NullString]::Value)
Yes, the current code ignores a null value, so IF we wanted to support that, we'd have to modify that - but I don't think we should.
To summarize what I'm suggesting:
-
Prevent passing
null(both[NullString]::Valueand$null) as the parameter value, which is consistent with what most cmdlets with[string]parameters do. -
Translate
""as an argument value tonullwhen calling.ChangeExtension(), resulting in removal of the (last) extension. -
Document that
"."must be passed in order to retain the (last) trailing.
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.
To spell it out:
Inside the if (Extension != null && joinedPath != null) branch, the only code should be:
joinedPath = System.IO.Path.ChangeExtension(joinedPath, Extension.Length == 0 ? null : Extension);Plus, the Extension parameter property must be declared not to accept null, though I'm confused about that: I thought that is the case by default and that you must conversely opt in to support null, via [AllowNull()]. However, in quick tests it seems that null is accepted by default. So perhaps a [ValidateNotNull] attribute is needed too.
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.
@mklement0 Thank you for the review! I've simplified the code as suggested in 9b084f2:
joinedPath = System.IO.Path.ChangeExtension(joinedPath, Extension.Length == 0 ? null : Extension);Question about [ValidateNotNull]: When -Extension "" is specified, we internally pass null to ChangeExtension(). However, this is unrelated to the behavior when -Extension $null is passed (or when -Extension is not specified at all). Therefore, the [ValidateNotNull] attribute doesn't seem necessary, but please let me know if it is indeed required.
I've also added test cases for preserving dots in base names ("file...txt" → "file.." when removing, "file...txt" → "file...md" when changing).
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.
Thanks, @yotsuda.
The goal is to ensure that neither -Extension $null nor -Extension ([NullString]::Value) are accepted.
If this is already the case: great.
If not, [ValidateNotNull] is needed.
(As noted, I'm unclear on whether the non-null constraint is enforced by default or not.)
brianary
left a comment
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.
Looks great!
|
Minor brief comment from inital look at this PR's intent. I feel there's no real need for the So if that scenario works too, I'll be more than happy with this being added. |
|
@kilasuit, yes, a leading |
- Simplify extension logic: use ternary operator to pass null when empty
- Remove TrimEnd('.'): fixes issue where dots in base name were removed
- Add test cases for preserving dots in base name:
* When removing extension (file...txt -> file..)
* When changing extension (file...txt -> file...md)
src/Microsoft.PowerShell.Commands.Management/commands/management/CombinePathCommand.cs
Show resolved
Hide resolved
src/Microsoft.PowerShell.Commands.Management/commands/management/CombinePathCommand.cs
Outdated
Show resolved
Hide resolved
| It "should change extension when -Extension parameter is specified" { | ||
| $result = Join-Path -Path "folder" -ChildPath "file.txt" -Extension ".log" | ||
| $result | Should -BeExactly "folder${SepChar}file.log" | ||
| } | ||
| It "should add extension to file without extension" { | ||
| $result = Join-Path -Path "folder" -ChildPath "file" -Extension ".txt" | ||
| $result | Should -BeExactly "folder${SepChar}file.txt" | ||
| } |
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 can use TestCases for all tests.
Also I'd add cases - extension without dot ("txt"), double extension (".tar.gz", "tar.gz")
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.
Thank you for the review! I've refactored the tests to use TestCases and added the requested test cases for extension without dot ("txt") and double extensions (".tar.gz", "tar.gz") in f06c5bc.
All tests pass.
…nt/CombinePathCommand.cs Co-authored-by: Ilya <[email protected]>
- Consolidate individual extension tests into TestCases for better maintainability
- Add test cases for extension without leading dot ('txt')
- Add test cases for double extensions ('.tar.gz', 'tar.gz')
- Remove duplicate test that is now covered by TestCases
Addresses review feedback from @iSazonov
- Add [ValidateNotNull] attribute to prevent explicit null values - Update XML comment to clarify default behavior when Extension is not specified Addresses review feedback from @iSazonov
| Path = "folder" | ||
| ChildPath = "file.txt" | ||
| Extension = ".log" | ||
| ExpectedResult = "folder${SepChar}file.log" |
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.
For readability I'd simplify these cases.
Path is the same in all tests so I believe we can remove it here and use directly in test body.
ExpectedResult I'd rename to ExpectedChildPath, leave only file name with new extension and build the full value in test body.:
| ExpectedResult = "folder${SepChar}file.log" | |
| ExpectedChildPath = "file.log" |
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.
- Remove Path from TestCases as it's constant across all tests - Rename ExpectedResult to ExpectedChildPath for clarity - Build full path in test body using 'folder' directly Addresses review feedback from @iSazonov
|
@iSazonov Thank you for the guidance. I've opened an issue in PowerShell-Docs to request documentation for the new |
| It "should remove extension when empty string is specified" { | ||
| $result = Join-Path -Path "folder" -ChildPath "file.txt" -Extension "" | ||
| $result | Should -BeExactly "folder${SepChar}file" | ||
| } | ||
| It "should preserve dots in base name when removing extension with empty string" { | ||
| $result = Join-Path -Path "folder" -ChildPath "file...txt" -Extension "" | ||
| $result | Should -BeExactly "folder${SepChar}file.." | ||
| } |
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.
The tests can be in TestCases too. Below there are more.
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.
- Consolidate six individual tests into TestCases arrays - Tests for removing/changing extensions are now part of parameterized tests - Created separate TestCases group for multiple child path segments - This improves consistency and maintainability Addresses review feedback from @iSazonov
d6aa244 to
11ff363
Compare
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.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/Microsoft.PowerShell.Commands.Management/commands/management/CombinePathCommand.cs
Show resolved
Hide resolved
|
The PowerShell Maintainer's discussed this PR and we have decided to not take this in v7.6. This will be part of the next v7.7 preview. |
PR Summary
This PR adds a new
-Extensionparameter toJoin-Pathcmdlet that allows changing the file extension of the resulting path, providing parity withSplit-Path's extension handling capabilities.Fixes #20724
PR Context
The
Split-Pathcmdlet has an-Extensionparameter that retrieves the file extension from a path. However,Join-Pathlacks the ability to set or change extensions when joining paths. This enhancement adds an-Extensionstring parameter toJoin-Paththat modifies the extension of the final joined path.Changes
CombinePathCommand.cs: Added-Extensionparameter withValueFromPipelineByPropertyNamesupportSystem.IO.Path.ChangeExtension()to modify the extensionJoin-Path.Tests.ps1Testing
New tests added:
Examples:
Test results:
PR Checklist
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright header-Extensionparameter should be documented inJoin-Pathcmdlet help Document new-Extensionparameter forJoin-Pathcmdlet MicrosoftDocs/PowerShell-Docs#12535