neurogsynth / CrossShell

Practical conversion guide

How to convert a Bash script to PowerShell without changing what it does

A reliable port preserves inputs, outputs, side effects, failures, and operational assumptions. It does not merely replace command names.

Start with a behavior contract

Before editing code, write down how the Bash script is invoked, which environment variables it reads, which files and services it changes, what it writes to standard output and standard error, and which exit codes callers rely on. Record the supported operating systems and every external executable. That short contract is the test oracle for the PowerShell version.

Run the original only in a safe fixture environment. Capture normal input, empty input, missing files, permission failures, spaces and Unicode in paths, interrupted operations, and repeated runs. If you cannot state the expected behavior, a syntactically valid conversion can still be dangerously wrong.

Common Bash-to-PowerShell mappings

Bash concernPowerShell approachWhat to verify
set -euo pipefailSet-StrictMode, $ErrorActionPreference = 'Stop', explicit native exit checksPowerShell does not treat every native nonzero exit as a terminating error.
$1, $2, defaultsA typed param(...) block with validationRequired versus optional values, empty strings, arrays, and pipeline input.
export NAME=value$env:NAME = 'value'Process scope and whether child processes or later sessions need the value.
$(command)$(command) or direct assignmentWhether the result is one string, a string array, or rich objects.
source file.shDot-source with . ./file.ps1Which functions and variables intentionally persist in the caller's scope.
find ... | grep ...Get-ChildItem, Where-Object, Select-StringRecursion, links, hidden files, binary content, case sensitivity, and duplicate paths.
trap ... EXITtry/finally; sometimes Register-EngineEventCleanup after ordinary errors, cancellation, process termination, and host shutdown.
chmod, chownWindows ACL cmdlets or .NET security APIsThere is no universal one-to-one mapping from Unix mode bits and ownership.
/tmp, /etc, ~[IO.Path]::GetTempPath(), known folders, $HOME, configurationService accounts, scheduled tasks, cleanup, and path portability.
curl, wgetInvoke-RestMethod, Invoke-WebRequest, or the native executableRedirects, TLS, proxy use, authentication, binary output, and error status handling.

Pipelines are the largest semantic trap

Bash pipelines normally pass bytes or lines. PowerShell pipelines pass .NET objects between cmdlets, but still pass text when a native executable is involved. A chain that looks nearly identical can therefore sort, filter, quote, and serialize differently.

Choose one boundary deliberately. Either keep a native text pipeline intact and document its dependencies, or convert the whole section to objects. Avoid repeatedly crossing between text and objects. At every native command, inspect $LASTEXITCODE when failure matters; $? alone is often too vague for automation.

Paths and quoting need tests, not confidence

Use -LiteralPath for user-supplied file paths, Join-Path to construct paths, and argument arrays instead of building one command string. Test spaces, brackets, apostrophes, Unicode, long paths, missing parents, and a working directory different from the script directory. Use $PSScriptRoot when an asset is relative to the script itself.

Do not translate eval into Invoke-Expression. Model the intended operation and pass validated arguments directly. This is both safer and easier to test.

PowerShell 7 or Windows PowerShell 5.1?

Prefer PowerShell 7 for new work: it has current language behavior, active support, better cross-platform consistency, and modern .NET APIs. Use Windows PowerShell 5.1 only when a required Windows-only module has not moved forward. State the target version in the script header and test on that exact version. A script that works in 7 may fail in 5.1 because parameters, encodings, or APIs differ.

A repeatable conversion workflow

  1. Freeze the behavior contract and representative fixtures.
  2. Run the CrossShell preflight to inventory conversion hotspots.
  3. Separate platform-neutral intent from Unix-specific implementation.
  4. Port parameters, path handling, and pure logic first.
  5. Replace or isolate each external command one at a time.
  6. Add explicit error handling and cleanup.
  7. Run both versions against the same safe fixtures and compare outputs, file trees, exit codes, and repeat-run behavior.
  8. Document every intentional difference and retained dependency.

When not to translate

A script built around /proc, Linux namespaces, Unix permissions, package managers, systemd, or a dense chain of GNU tools may need a Windows-native redesign. Sometimes the honest answer is to run the original under a supported Linux environment instead of maintaining a fragile imitation. The preflight calls these dependencies out so that decision happens before the port grows expensive.

Need one reviewed port?

The CrossShell starter covers one Bash script up to 150 nonblank lines, with conversion notes, a verification checklist, and one revision.

See the $29 fixed scope Download the free preflight