Skip to content
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

Improving Test-Path for when conditionally creating a new path #18957

Open
Andrew74L opened this issue Jan 17, 2023 · 17 comments
Open

Improving Test-Path for when conditionally creating a new path #18957

Andrew74L opened this issue Jan 17, 2023 · 17 comments
Labels
Issue-Enhancement the issue is more of a feature request than a bug Needs-Triage The issue is new and needs to be triaged by a work group. WG-Cmdlets-Utility cmdlets in the Microsoft.PowerShell.Utility module

Comments

@Andrew74L
Copy link

Summary of the new feature / enhancement

Test-Path script often looks like this:
if (-not (Test-Path 'L:\some\long\path\')) {New-Item 'L:\some\long\path\'}
Having to specify the path twice, is not very efficient.
This is a small improvement:
$p = 'L:\some\long\path\'
if (-not (Test-Path $p)) {New-Item $p}
I'm wondering if Test-Path could be improved in a way that streamlines code when conditionally creating a new path.

Proposed technical implementation details (optional)

Suppose Test-Path could pass a PSProvider object down the pipeline, based on the -Path argument. Hypothetically:
Test-Path 'L:\some\long\path\' -PassThru || New-Item
This wouldn't work because the command hasn't failed. However, suppose the result of Test-Path could be easily negated with a False switch, like this:
Test-Path 'L:\some\long\path\' -False -PassThru && New-Item
This still isn't going to work because the object would be passed-through regardless of the $true/$false outcome. So what about this:
Test-Path 'L:\some\long\path\' -PassThruIfFalseElseFail && New-Item
I'm not sure this would be particularly "intuitive", so what would be a better way, or is it a non-issue?

@Andrew74L Andrew74L added Issue-Enhancement the issue is more of a feature request than a bug Needs-Triage The issue is new and needs to be triaged by a work group. labels Jan 17, 2023
@Andrew74L
Copy link
Author

Andrew74L commented Jan 17, 2023

Perhaps a more elegant solution would be to add a switch to New-Item (and New-ItemProperty):
New-Item 'L:\some\long\path\' -TestPath
Which would test for the existence of the path before creating it.

@237dmitry
Copy link

Test-Path and New-Item are on the different spaces. Write function for your own, PowerShell give you wide possibilities.

@MartinGC94
Copy link
Contributor

I just use New-Item -Force. If it's there, nothing happens and if it's not then the command creates the folder. Note that this only applies to the filesystem provider, other providers may have undesirable side effects when you do this (I know the Registry provider deletes all the existing properties in the key).

@mklement0
Copy link
Contributor

-Force works great with directories (return preexisting, if present, otherwise create), but with files it truncates existing ones.

A switch such as -NoClobber would indeed be helpful for files (and for directories should behave the same as -Force); this has been suggested before:

The problem with registry keys losing their properties (values) with -Force, even though they should be conceived of as containers, just like directories, and therefore exhibit the return-preexisting-or-create behavior:

The backward-compatible way to resolve this issues is to introduce a -NoClobber switch, which should exhibit consistent nondestructive behavior irrespective of item type (the need for the nondescript -Force switch would then go away; -Force, due to the many different purposes it serves across different cmdlets, is inherently problematic).

@Andrew74L
Copy link
Author

The backward-compatible way to resolve this issues is to introduce a -NoClobber switch, which should exhibit consistent nondestructive behavior irrespective of item type (the need for the nondescript -Force switch would then go away; -Force, due to the many different purposes it serves across different cmdlets, is inherently problematic).

I like the -NoClobber solution, but I also like the idea of something that has more of a flow-chart feel. Such as:
Test-Path 'L:\some\long\path\' -Invoke New-Item
Conditionally invokes (or calls) New-Item 'L:\some\long\path\'

This sort of syntax might be useful if this were possible:
Test-Path 'L:\some\long\path\' -OlderThan $date -Invoke Touch-Item

@mklement0
Copy link
Contributor

It's an interesting idea, and while I like the DRY aspect of it, the fact that it would only work with commands that expect exactly one argument (namely the path being tested) makes this too narrow a feature in my view.

A more flexible alternative could be to add a -PassThru switch, along with something like -Not to allow negating the test (the latter would make sense on its own), which would enable the following:

# WISHFUL THINKING
Test-Path -PassThru 'L:\some\long\path\' | % { "$_ exists." }
Test-Path -Not -PassThru 'L:\some\long\path\' | % { "$_ does NOT exist." }

That is, -PassThru would produce no output if the test fails (meaning that the script block won't get executed), and pass the input path through if it succeeds, which can be referenced via $_ in the script block for further processing.

@mklement0
Copy link
Contributor

mklement0 commented Jan 18, 2023

@Andrew74L, re-reading your initial post I see that you had already proposed similar ideas.

Tor recap re && and ||: These operators would afford the most flexibility, but, given that even a negative test outcome - signaled by Boolean output - is considered successful execution of Test-Path, that isn't currently an option.

A debate about whether Test-Path should work with && and || is here (see this comment in paticular):

@Andrew74L
Copy link
Author

Thanks MK.
I think the syntax should be simpler than when specifying the path twice. How about a conditionally run scriptblock:
Test-Path 'L:\some\long\path\' -TrueCondition {"$_ exists"} -FalseCondition {New-Item $_}

In considering this I'm trying to dream up a syntax that has more of a natural language feel to it. Will Verb-Noun | Verb-Noun seem a bit passé in a few years?

@Andrew74L
Copy link
Author

I think the syntax should be simpler than when specifying the path twice. How about a conditionally run scriptblock: Test-Path 'L:\some\long\path\' -TrueCondition {"$_ exists"} -FalseCondition {New-Item $_}

Perhaps Resolve-Path is the better candidate for this concept, as it already returns paths if exist. The idea would be to execute a scriptblock if path not found, but is valid.

Resolve-Path 'L:\some\long\path\that\does\not\exist\but\is\a\valid\path\' -Catch {Resolve-Path (mkdir $_)}

Path
----
L:\some\long\path\that\does\not\exist\but\is\a\valid\path

@mklement0
Copy link
Contributor

Honestly, for the use case at hand, New-Item -NoClobber strikes me as the most concise and robust solution, employing desired-state logic.

It has the additional advantage that, should an error then occur, it signals a true error condition.

@Andrew74L
Copy link
Author

Honestly, for the use case at hand, New-Item -NoClobber strikes me as the most concise and robust solution, employing desired-state logic.

It has the additional advantage that, should an error then occur, it signals a true error condition.

Okay.

I'm curious about what the process would be for getting New-Item -NoClobber into the product. If this switch were accepted by the team, would user testing or feedback occur before it was included?

@mklement0
Copy link
Contributor

I have very little insight into what ultimately guides the decisions - I'm just a fellow user - but, generally speaking:

  • If, in the team's mind, the value of a feature request is without question, it will usually be green-lit by tagging it as "up for grabs", meaning that any community member is free to submit a PR to implement it - though on occasion a team member may take it on themselves.

  • If it's unclear whether implementing a requested features adds value, it may be implemented as an experimental feature.

Publicly signaling interest in a feature is an important part of the process, so I suggest you give a 👍 to those feature requests you would personally like to see implemented.

@Andrew74L
Copy link
Author

Andrew74L commented Jan 26, 2023

I have very little insight into what ultimately guides the decisions - I'm just a fellow user - but, generally speaking:

  • If, in the team's mind, the value of a feature request is without question, it will usually be green-lit by tagging it as "up for grabs", meaning that any community member is free to submit a PR to implement it - though on occasion a team member may take it on themselves.

  • If it's unclear whether implementing a requested features adds value, it may be implemented as an experimental feature.

Thanks for that info.

Regarding experimental features discovery, for preview versions I would make the logo something like:
PowerShell 7.4.0-preview.1 (Experimental features enabled. Use 'Get-ExperimentalFeature' for info.)
Anything multi-line is not a 'logo'.
For release versions I would have a runonce Get-ExperimentalFeature output with appropriate title, after the logo.

Publicly signaling interest in a feature is an important part of the process, so I suggest you give a 👍 to those feature requests you would personally like to see implemented.

I'll keep that in mind.

@mklement0
Copy link
Contributor

Re experimental-feature discovery: sounds like a good suggestion, I encourage you to post it at #14862

@daxian-dbw daxian-dbw added the WG-Cmdlets-Utility cmdlets in the Microsoft.PowerShell.Utility module label Jan 27, 2023
@iRon7
Copy link

iRon7 commented Oct 5, 2023

Similar thoughts (wishful thinking):

$Profile.PSObject.Properties | Where-Object value -Like *profile.ps1 |
    Test-Item -PassThru | Rename-Item $_.Value "$($_.Value).Bak" -Force

@RiverHeart
Copy link

RiverHeart commented Jan 4, 2024

I think the syntax should be simpler than when specifying the path twice. How about a conditionally run scriptblock: Test-Path 'L:\some\long\path\' -TrueCondition {"$_ exists"} -FalseCondition {New-Item $_}

Perhaps Resolve-Path is the better candidate for this concept, as it already returns paths if exist. The idea would be to execute a scriptblock if path not found, but is valid.

Resolve-Path 'L:\some\long\path\that\does\not\exist\but\is\a\valid\path\' -Catch {Resolve-Path (mkdir $_)}

Path
----
L:\some\long\path\that\does\not\exist\but\is\a\valid\path

Hey, that's a pretty good idea. Works out alright and while a Test-Path -PassThru would be my preferred syntax this is not significantly more verbose and functions similarly.

New-Item foo1, foo2
"foo1", "foo2", "foo3" | Resolve-Path -ErrorAction Ignore | Remove-Item

Doing the reverse is a bit more of a pain

"foo1", "foo2", "foo3" | 
    Where-Object { -not (Resolve-Path $_ -ErrorAction Ignore) } |
    New-Item -Path { $_ }

Now that I think about it you can also do

"foo1", "foo2", "foo3" | ? { Test-Path $_ } | Remove-Item -Path { $_ }

@RiverHeart
Copy link

RiverHeart commented Jan 4, 2024

It would be nice if this functionality was native but in the meantime, this might be helpful. You may want to verify the logic before using it if you're deleting files though.

Edit: Changed "Where-TestPath" to "Filter-OnTestPath" because "Where" is a reserved verb. "Filter" is unapproved but what's the approved alternative to "Where" anyway?

function Filter-OnTestPath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string[]] $Path,

        [ValidateSet('Existing', 'NonExisting')]
        [string] $Status = 'Existing',

        [switch] $Name
    )

    process {
        foreach ($Item in $Path) {
            $FileExists = Test-Path $Item
            $DoesExist  = $Status -eq 'Existing'    -and      $FileExists
            $IsAbsent   = $Status -eq 'NonExisting' -and -not $FileExists
            if ($DoesExist -or $IsAbsent) {
                if ($Name) { $Item }
                else { [PSCustomObject] @{ Path = $Item } } 
            }
        }
    }
}

@('foo', 'foo2', 'foo3') | Filter-OnTestPath -Status NonExisting | New-Item -Verbose | Out-Null
@('foo', 'foo2', 'foo3') | Filter-OnTestPath -Status Existing | Remove-Item -Verbose

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Issue-Enhancement the issue is more of a feature request than a bug Needs-Triage The issue is new and needs to be triaged by a work group. WG-Cmdlets-Utility cmdlets in the Microsoft.PowerShell.Utility module
Projects
None yet
Development

No branches or pull requests

7 participants