DEV Community

Cover image for PowerShell Join-Path: Build File Paths Without Slash Bugs
arnostorg
arnostorg

Posted on

PowerShell Join-Path: Build File Paths Without Slash Bugs

Most broken PowerShell scripts I've debugged didn't fail on some clever logic. They failed on a path glued together with a string: "$dir\$file". One trailing backslash, one empty variable, or one run on Linux, and the script writes to the wrong place or nowhere at all.

The Join-Path cmdlet fixes that. It combines a parent path and a child path with exactly one separator, and it fails loudly when the input is wrong instead of quietly guessing.

For the official reference, see Microsoft Learn's Join-Path and Split-Path docs.

The basic join

Join-Path -Path 'C:\Apps' -ChildPath 'logs'
# C:\Apps\logs
Enter fullscreen mode Exit fullscreen mode

Positional works too, and it's how most people write it:

Join-Path C:\Apps logs
Enter fullscreen mode Exit fullscreen mode

It cleans up your slashes

This is the main reason to use it. Whether the parent ends with a separator or the child starts with one, you get a single backslash:

Join-Path 'C:\Apps\' 'logs'      # C:\Apps\logs
Join-Path 'C:\Apps'  '\logs'     # C:\Apps\logs
Join-Path 'C:\Apps\' '\logs'     # C:\Apps\logs
Enter fullscreen mode Exit fullscreen mode

String gluing doesn't do that. "C:\Apps\" + "\logs" gives you C:\Apps\\logs, which Windows usually tolerates, until that string ends up in a comparison, a log line, a registry value, or a tool that's pickier about it.

The empty-variable bug it saves you from

Here's the one that actually bites in production. Say $logDir comes from a config file and the key is missing:

$logDir = $config.LogDir     # $null, nobody noticed
$target = "$logDir\app.log"  # \app.log
Add-Content -Path $target -Value 'started'
Enter fullscreen mode Exit fullscreen mode

\app.log is the root of the current drive. Your script happily writes C:\app.log (or fails with access denied, depending on who runs it), and you spend an hour wondering where the log went.

With Join-Path, the same mistake stops the script on the spot:

$target = Join-Path -Path $logDir -ChildPath 'app.log'
# Cannot bind argument to parameter 'Path' because it is null.
Enter fullscreen mode Exit fullscreen mode

A clear error at the line that's wrong beats a file in the wrong place every time.

Join more than two parts

On PowerShell 7 (and 6), you can pass as many child parts as you want. Everything after the first child goes into -AdditionalChildPath:

Join-Path C:\Apps MyService logs 2026 app.log
# C:\Apps\MyService\logs\2026\app.log
Enter fullscreen mode Exit fullscreen mode

Windows PowerShell 5.1 only takes one child, so the same line fails there with "A positional parameter cannot be found". Two options that work everywhere:

# Nest the calls
Join-Path (Join-Path (Join-Path C:\Apps MyService) logs) app.log

# Or put the separators in the child yourself
Join-Path C:\Apps 'MyService\logs\app.log'
Enter fullscreen mode Exit fullscreen mode

The second one is fine. Join-Path still handles the seam between parent and child, which is where the bugs live.

Build the same child under many parents

-Path accepts an array, so you get one result per parent:

Join-Path -Path 'C:\Sites\shop', 'C:\Sites\blog', 'C:\Sites\api' -ChildPath 'web.config'
# C:\Sites\shop\web.config
# C:\Sites\blog\web.config
# C:\Sites\api\web.config
Enter fullscreen mode Exit fullscreen mode

That pipes straight into the next step, for example to check which sites are missing a config. If you haven't seen it yet, my Test-Path guide covers that half:

Join-Path -Path 'C:\Sites\shop', 'C:\Sites\blog', 'C:\Sites\api' -ChildPath 'web.config' |
  Where-Object { -not (Test-Path -Path $_ -PathType Leaf) }
Enter fullscreen mode Exit fullscreen mode

-Resolve: join and confirm it exists

Add -Resolve and Join-Path returns the full path only if it actually exists. If it doesn't, you get an error instead of a string pointing at nothing:

Join-Path -Path $env:windir -ChildPath 'System32\drivers\etc\hosts' -Resolve
# C:\WINDOWS\System32\drivers\etc\hosts
Enter fullscreen mode Exit fullscreen mode

-Resolve also expands wildcards, so it doubles as a quick finder:

Join-Path -Path C:\Apps\MyService\logs -ChildPath '*.log' -Resolve
Enter fullscreen mode Exit fullscreen mode

You get every matching log as a full path. For anything fancier (recursion, filtering by date), reach for Get-ChildItem.

The drive gotcha

Join-Path checks that the drive exists, even without -Resolve:

Join-Path Q:\backup nightly.zip
# Cannot find drive. A drive with the name 'Q' does not exist.
Enter fullscreen mode Exit fullscreen mode

It doesn't check folders or files, only the drive. That's usually helpful (a typo'd drive letter fails early), but it can surprise you when a script builds paths for a mapped drive that only exists on the target machine. In that case, build the string on the machine where the drive is mapped, or use a UNC path.

Why not [System.IO.Path]::Combine?

You'll see .NET's Combine in a lot of scripts. It mostly works, but it has a nasty rule: if the second part looks rooted, the first part is thrown away.

[System.IO.Path]::Combine('C:\Apps', '\logs')
# \logs
Enter fullscreen mode Exit fullscreen mode

No error, no warning, and your path now points at the root of the current drive. Join-Path gives C:\Apps\logs for the same input. Inside PowerShell scripts, I'd stick with the cmdlet.

Scripts that work from any folder

The most useful habit of all: build paths from the script's own location, not from wherever someone happened to cd before running it.

$configPath = Join-Path -Path $PSScriptRoot -ChildPath 'config.json'
$config     = Get-Content -Path $configPath -Raw | ConvertFrom-Json
Enter fullscreen mode Exit fullscreen mode

$PSScriptRoot is the folder the script lives in. Pair it with Join-Path and the script finds its files whether it's run from a scheduled task, a different drive, or a colleague's terminal.

Cross-platform for free

On PowerShell 7 for Linux and macOS, Join-Path uses / as the separator:

Join-Path -Path $HOME -ChildPath 'logs'
# /home/you/logs
Enter fullscreen mode Exit fullscreen mode

PowerShell's own cmdlets often forgive a hardcoded "$HOME\logs" on Linux, but the moment that string goes to a native tool, a config file, or a .NET method, the backslash is just a character in a file name. If a script might ever run in a container or CI runner, letting Join-Path pick the separator is worth the switch.

Practical ops example: a dated log next to the script

$logDir = Join-Path -Path $PSScriptRoot -ChildPath 'logs'
New-Item -ItemType Directory -Path $logDir -Force | Out-Null

$logFile = Join-Path -Path $logDir -ChildPath ('run-{0:yyyy-MM-dd}.log' -f (Get-Date))
Add-Content -Path $logFile -Value "$(Get-Date -Format s) job started"
Enter fullscreen mode Exit fullscreen mode

Every run appends to one file per day, in a folder next to the script, no matter where it's launched from.

The other direction: Split-Path

When you need to take a path apart instead, Split-Path is the mirror image:

$p = 'C:\Apps\MyService\logs\app.log'

Split-Path -Path $p -Parent   # C:\Apps\MyService\logs
Split-Path -Path $p -Leaf     # app.log
Enter fullscreen mode Exit fullscreen mode

On PowerShell 7 there's also -LeafBase (app) and -Extension (.log).

Quick checklist

  • Combining any two path pieces? Use Join-Path, not "$a\$b".
  • Worried about trailing or leading slashes? Join-Path collapses them to one.
  • Variable might be empty? Join-Path throws instead of writing to the drive root.
  • More than two parts? Pass them all on PowerShell 7, or nest calls on 5.1.
  • Need the path to really exist? Add -Resolve.
  • Script needs its own files? Start from $PSScriptRoot.
  • Avoid [System.IO.Path]::Combine when the second part can start with \.

Practice it live

CMD Master is a free online interactive learning platform for the command line. You can practice PowerShell file and path commands in a live browser terminal with instant feedback, no install or VM needed. It also covers Windows CMD and Bash, most lessons are free, and premium unlocks the deeper paths. If you're just getting comfortable with the shell, start with the free interactive command line lessons on CMD Master.

Top comments (0)