skip to content

You need one PowerShell 7 script to run on Windows, Linux and macOS. What does the engine give you for detecting the current operating system, and how do you build file paths and read environment variables so they work on all three?

level: juniorimportance: should knowfreq 52%

answer

  1. three booleans, exactly one true
  2. never concatenate with a backslash
  3. let a cmdlet insert the separator
  4. home directory differs by platform
  5. spelling of PATH matters on Unix

basics

~20 s

PowerShell 7 defines the automatic variables $IsWindows, $IsLinux and $IsMacOS for OS detection. Build paths with Join-Path rather than hardcoded backslashes, use $HOME instead of $env:USERPROFILE, and spell environment variable names exactly, since Unix names are case-sensitive.

solid answer

~40 s

PowerShell 7 ships three boolean automatic variables — `$IsWindows`, `$IsLinux` and `$IsMacOS` — and exactly one is true, so you branch on those rather than sniffing paths. For file paths, never concatenate with a backslash: use `Join-Path`, or `[System.IO.Path]::DirectorySeparatorChar` if you need the character itself, and `[System.IO.Path]::PathSeparator` when splitting `PATH`, which is `;` on Windows and `:` elsewhere. `$HOME` is defined on every platform; `$env:USERPROFILE` and `$env:APPDATA` are Windows-only. Two traps catch people: environment variable *names* are case-insensitive on Windows but case-sensitive on Linux and macOS, so `$env:Path` silently returns nothing on Linux while `$env:PATH` works; and the file system is case-sensitive on Linux, even though PowerShell's own `-eq` string comparison stays case-insensitive everywhere. Anything Windows-only — the registry provider, WMI, `Get-Service` — has to sit inside an `$IsWindows` branch.

code

powershell · 12 lines
powershell
$configDir = if ($IsWindows) {
    Join-Path $env:APPDATA 'myapp'
} else {
    Join-Path $HOME '.config/myapp'
}

New-Item -ItemType Directory -Path $configDir -Force | Out-Null
$logFile = Join-Path $configDir 'app.log'
"started $(Get-Date -Format o)" | Set-Content -Path $logFile -Encoding utf8NoBOM

[System.IO.Path]::DirectorySeparatorChar   # '\' on Windows, '/' on Linux and macOS
$env:PATH -split [System.IO.Path]::PathSeparator | Select-Object -First 3

go deeper

for a junior

Know the three OS variables by name and say that you build paths with Join-Path rather than gluing strings with a backslash. Mention $HOME as the portable home directory.

for a middle

Explain the mechanics behind the traps: case-sensitive environment variable names and file systems on Unix, PathSeparator versus DirectorySeparatorChar, and why $IsWindows misfires under 5.1.

for a senior

Show how you keep portability honest in practice — running the script on each target platform in CI, isolating Windows-only calls behind one branch, and pinning encoding so downstream tools do not break on a default change.

for a principal

Own the decision of whether a script should be portable at all. Weigh the maintenance cost of triple-platform branching against standardising the fleet's automation on one platform or moving the work into containers.

## The OS-detection variables PowerShell 6 introduced three read-only boolean automatic variables, and PowerShell 7 keeps them: ```powershell $IsWindows # $true only on Windows $IsLinux # $true only on Linux $IsMacOS # $true only on macOS ``` Exactly one is `$true` in any session. There is also `$IsCoreCLR`, which is `$true` on any Core-edition engine. Reading `$PSVersionTable.Platform` gives the coarser `Win32NT` or `Unix`, and `$PSVersionTable.OS` gives a descriptive string useful for logging but not for branching. The well-known trap: these variables do **not** exist in Windows PowerShell 5.1. There, `$IsWindows` is an undefined variable, which evaluates to `$null` — falsy. A script that says `if ($IsWindows) { Windows-path } else { Unix-path }` therefore takes the *Unix* branch when run by `powershell.exe` on Windows, which is the exact opposite of what the author meant. If the script must tolerate 5.1, gate on the edition first: ```powershell $onWindows = $PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows ``` ## Paths Hardcoded backslashes are the most common portability bug. Use `Join-Path`, which inserts the platform's separator: ```powershell $logDir = Join-Path $HOME 'logs' $logFile = Join-Path $logDir 'app.log' ``` When you need the raw characters, .NET exposes them: * `[System.IO.Path]::DirectorySeparatorChar` — `\` on Windows, `/` on Linux and macOS. * `[System.IO.Path]::PathSeparator` — `;` on Windows, `:` on Unix. This is the one to use when splitting `$env:PATH` into entries; splitting on the wrong character yields one giant nonsense entry. A mercy: PowerShell and Windows both accept forward slashes in most file paths, so `Join-Path $HOME 'logs/app.log'` works on Windows too. It is native tools and command-line arguments that are fussier. **Well-known locations differ.** `$HOME` is defined everywhere. `$env:USERPROFILE`, `$env:APPDATA` and `$env:LOCALAPPDATA` are Windows-only; on Unix the convention is `~/.config/<app>`. `$env:TEMP` exists on Windows; `[System.IO.Path]::GetTempPath()` works everywhere. ## Environment variables On Windows, environment variable names are case-insensitive, so `$env:Path`, `$env:PATH` and `$env:path` are the same variable. On Linux and macOS the process environment is case-sensitive, so `$env:Path` is a *different* (and almost certainly empty) variable from `$env:PATH`. This produces a silent `$null` rather than an error — the script simply behaves as though `PATH` were unset. Always spell environment variable names in their canonical case. Do not confuse this with PowerShell's own comparison semantics: `-eq`, `-like` and `-match` remain case-insensitive on every platform (use `-ceq`, `-clike`, `-cmatch` for case-sensitive ones). Only the OS-owned things — file names, environment variable names — inherit the platform's case rules. ## The file system Linux file systems are case-sensitive; Windows is case-insensitive but case-preserving; macOS is case-insensitive by default but *can* be formatted case-sensitive. So `Get-Content ./Config.json` may work on your Mac and fail on the Linux build agent where the file is `config.json`. Treat file names as case-sensitive when you author them, regardless of where you test. Line endings differ too — CRLF on Windows, LF elsewhere — which matters when a script generates a file another tool parses. ## Encoding PowerShell 6 unified the default output encoding to UTF-8 without a BOM across `Set-Content`, `Out-File`, `Add-Content` and friends. Windows PowerShell 5.1 was inconsistent: `Out-File` defaulted to UTF-16LE, `Set-Content` to the ANSI code page. If a file written by a script is consumed by a non-PowerShell tool, state the encoding explicitly with `-Encoding utf8NoBOM` rather than relying on the default matching between engines. ## What simply does not exist off Windows The registry provider (`HKLM:`, `HKCU:`, `Get-ItemProperty` against them), WMI/CIM queries against `Win32_*` classes, `Get-Service`, `Get-WinEvent`, COM objects, and NTFS ACL cmdlets are Windows-only. There is no shim. Put them behind `if ($IsWindows)` and provide a real alternative for the other platforms — or decide the script is Windows-only and say so with `#requires`. ## The shape of a good answer Name the three variables, mention that they are absent in 5.1, show `Join-Path` and the two separator members, and call out the case-sensitivity of environment variable names and file paths. That set covers the bugs that actually occur, which is what the interviewer is checking for.

  • Why is `if ($IsWindows)` unsafe in a script that might be run by Windows PowerShell 5.1?
    Because `$IsWindows` was introduced in PowerShell 6 and does not exist in 5.1. An undefined variable evaluates to `$null`, which is falsy, so on a Windows machine running `powershell.exe` the script takes the non-Windows branch. Gate on something both engines define, such as `$PSVersionTable.PSEdition -eq 'Desktop'`, or require PowerShell 7 outright.
  • On Linux, why can `$env:Path` come back empty while `$env:PATH` works?
    Process environment variable names are case-sensitive on Linux and macOS, so `Path` and `PATH` are distinct names; on Windows they are the same variable. PowerShell returns `$null` for the missing one rather than erroring, so the failure is silent. Note this is an OS rule — PowerShell's own `-eq` string comparison stays case-insensitive on every platform.
  • Which character should you split `$env:PATH` on in a portable script?
    `[System.IO.Path]::PathSeparator` — `;` on Windows and `:` on Linux and macOS. Splitting on a hardcoded `;` on Linux returns the entire value as a single entry, so lookups quietly find nothing. The directory separator inside each entry is the different member `[System.IO.Path]::DirectorySeparatorChar`.
  • What changed about default file encoding between Windows PowerShell 5.1 and PowerShell 7?
    PowerShell 6 standardised the default on UTF-8 without a BOM for `Set-Content`, `Out-File` and `Add-Content`. Windows PowerShell 5.1 was inconsistent — `Out-File` used UTF-16LE and `Set-Content` used the ANSI code page. If another tool parses the output, pass `-Encoding utf8NoBOM` explicitly instead of relying on the engine's default.

saying these in an interview costs you the question

  • Uses $IsWindows in Windows PowerShell 5.1
  • Builds paths by concatenating a backslash
  • Assumes $env:USERPROFILE exists on Linux
  • Thinks $env:Path resolves on Linux
  • Believes PowerShell's -eq becomes case-sensitive on Linux

context