3
\$\begingroup\$

I really like c#'s Using statement as it allows you to pair Open statements with their corresponding Close/Dispose statements in an elegant way which matches C#'s syntax. I realised that something similar can be achieved in PowerShell; e.g. Push-Location should be paired with a Pop-Location, and this can be enforced by creating a function which allows you to use a USING-like syntax.

e.g.

function PushDPopD {
    [CmdletBinding()]
    param (
        [string]$Path
        , 
        [scriptblock]$Code
    )
    process {
        if (Push-Location $Path -PassThru) { #only proceed with this function's logic if our pushd were successful
            try {
                $Code.Invoke()
            } finally { #ensure that whatever happens in user code, we always popd after our pushd
                Pop-Location
            }
        }
    }
}
function ShowCurrentLocation {
    ("You are in: {0}" -f (Get-Location).Path)
}

Clear-Host
[string]$localVariable = "We keep variables even in scriptblock context"

write-host "`n`n==Healthy Demo==`n`n" -ForegroundColor cyan
ShowCurrentLocation 
PushDPopD 'c:\Users' {
    "hello"
    ShowCurrentLocation
    $localVariable  
    "bye"
}
ShowCurrentLocation 

write-host "`n`n==Exception Demo==`n`n" -ForegroundColor cyan
ShowCurrentLocation 
PushDPopD 'c:\ThisPathDoesNotExist' {
    "hello"
    ShowCurrentLocation 
    $localVariable 
    "bye"
}
ShowCurrentLocation 

To me this seems like a logical approach / looks neater than manually checking that all pushes are correctly paired with pops. The only potential issue is if someone includes a popd in the scriptblock itself; but that's no worse than the original scenario, so isn't a big drawback. This also gives us the advantage that code won't blindly run on thinking it's in a different directory to the one specified if the pushd fails.

As a concept does this seem logical / have I missed anything.

NB: I'm aware it's not currently following verb-noun convention; that's because it's just proof of concept code, so I've not yet put any thought into suitable naming (suggestions welcome though).

\$\endgroup\$
  • \$\begingroup\$ One issue occurred; if the scriptblock throws an error (e.g. I add a new line with 1/0 somewhere in the Code parameter's value, the error will show as coming from the $Code.Invoke() line, rather than the actual line-in-error. \$\endgroup\$ – JohnLBevan Nov 12 '15 at 13:18
4
\$\begingroup\$

To be honest I feel this might be overcomplicating things.

I use Push-Location/Pop-Location in try/finally blocks when needed (like when loading the SQLPS module), but that's almost never.

For the most part I think one should avoid changing the current location and instead reference the desired path (either relative or absolute). So something like:

$p = $BasePath | Join-Path -ChildPath 'subfolder'
Get-ChildItem -Path $p

rather than:

$p = $BasePath | Join-Path -ChildPath 'subfolder'
Push-Location $p
try {
    Get-ChildItem
} finally {
    Pop-Location
}

As you've seen, there are also some issues with using scriptblocks in that way. You might have scope issues with variables, and call stack/execution context will be a bit different. This might get complicated further when you consider pipelines.

So in summary I'd rather avoid changing the working directory and then not obfuscate the process when I do so it's clear what's going on.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.