Profiles, Configuration, Environment Overrides, and CLI Design
Keep PowerShell profiles as personal session customization while designing production scripts with explicit configuration precedence and a stable CLI contract.
Learning objectives
- Explain PowerShell profiles and discover the profile paths exposed by $PROFILE.
- Keep production automation independent of personal profiles and test with pwsh -NoProfile.
- Define a clear configuration precedence across parameters, environment variables, config files, and defaults.
- Use environment overrides carefully and convert/validate their string values.
- Explain $PSDefaultParameterValues as a preference mechanism that can create hidden environment dependencies.
- Build a configurable CLI-style tool whose machine-readable output remains structured.
1. A PowerShell profile customizes a user/host session; it is not an application dependency file
A PowerShell profile is a script that can run when PowerShell starts. It is useful for personal prompt customization, aliases, helper functions, variables, and interactive modules. Microsoft documents multiple profile files based on user and host. PowerShell does not create those files automatically.
Profiles are intentionally ambient session customization. Production automation should not require a developer’s profile to succeed. A CI runner, container, remote session, or another engineer may have a different profile—or none at all.
# Inspect the profile object and its named paths
$PROFILE | Format-List *
# Test whether the current-user/current-host profile exists
Test-Path -LiteralPath $PROFILE.CurrentUserCurrentHost2. Use $PROFILE properties instead of memorizing platform-specific profile paths
$PROFILE is a string-like automatic variable representing the current user/current host profile, and it also exposes properties for the supported profile combinations. The actual path depends on operating system and host, so discover it from the running environment.
[pscustomobject]@{
CurrentUserCurrentHost = $PROFILE.CurrentUserCurrentHost
CurrentUserAllHosts = $PROFILE.CurrentUserAllHosts
AllUsersCurrentHost = $PROFILE.AllUsersCurrentHost
AllUsersAllHosts = $PROFILE.AllUsersAllHosts
}If you choose to edit a personal profile, create its parent directory first and keep the contents focused on interactive convenience. Do not put production credentials, machine-specific deployment configuration, or required business logic there.
$profilePath = $PROFILE.CurrentUserCurrentHost
$profileDir = Split-Path -Parent $profilePath
# Safe preview of what would be needed:
[pscustomobject]@{ Profile = $profilePath; ParentExists = Test-Path -LiteralPath $profileDir }3. Test scripts without profiles to reveal hidden dependencies
The pwsh executable supports -NoProfile. Running automation this way is a useful diagnostic: if a script fails only when profiles are disabled, it depends on customization that was never declared as part of the tool.
pwsh -NoProfile -File ./New-DeploymentPlan.ps1 -Environment test -Target api-01A profile can still be valuable for your interactive workflow—for example, importing a personal prompt theme or defining an alias. The key separation is that the repository script must remain correct without it. Microsoft also documents that profiles are not automatically run in remote sessions, which reinforces this design rule.
4. Configuration needs a documented precedence order
A reusable tool often receives configuration from several sources. Without a precedence rule, two correct-looking environments can produce different behavior. A common explicit order is: command-line parameter first, then an environment variable, then a configuration file, then a built-in default. The exact order can differ, but it must be documented and testable.
| Priority | Source | Why |
|---|---|---|
| 1 | Explicit parameter | The caller intentionally selected a value for this run |
| 2 | Environment variable | Useful injection point for CI/container environments |
| 3 | Config file | Versioned or deployed persistent settings |
| 4 | Built-in default | Safe fallback when nothing else specifies the value |
param([string]$Region)
$resolvedRegion = if ($PSBoundParameters.ContainsKey('Region')) {
$Region
} elseif ($env:DEPLOY_REGION) {
$env:DEPLOY_REGION
} else {
'eu-central'
}
[pscustomobject]@{ Region = $resolvedRegion }Chapter 11 will add structured JSON/YAML configuration. The important concept now is source precedence, not file-format complexity.
5. Environment variables are convenient overrides, but they are strings and ambient state
Environment variables work well in CI and containers because orchestration systems can inject them without editing the script. They are nevertheless strings, inherit according to process rules, may be absent, and can vary by platform/casing behavior. Convert and validate them before treating them as typed configuration.
$retryText = $env:DEPLOY_RETRY_COUNT
$retryCount = 3
if ($retryText) {
$parsed = 0
if (-not [int]::TryParse($retryText, [ref]$parsed)) {
throw 'DEPLOY_RETRY_COUNT must be an integer.'
}
if ($parsed -lt 1 -or $parsed -gt 10) {
throw 'DEPLOY_RETRY_COUNT must be between 1 and 10.'
}
$retryCount = $parsed
}
$retryCountDo not store secrets in plain profile code merely because environment variables are also imperfect. Chapter 16 covers credential and secret-management boundaries.
6. $PSDefaultParameterValues can improve interactive ergonomics—and hide assumptions
$PSDefaultParameterValues is a preference dictionary that can provide default parameter values for cmdlets, advanced functions, and scripts that use CmdletBinding. Microsoft notes that it has no default value; users can persist assignments by placing them in profiles.
# Interactive preference example
$PSDefaultParameterValues['*:ErrorAction'] = 'Stop'
# Inspect configured defaults
$PSDefaultParameterValuesThis is useful for a personal shell, but production behavior should not silently depend on it. A script that needs terminating error behavior should request -ErrorAction Stop where required or set a documented script-level policy. Test important entry points with a clean profile/environment.
7. A user-facing PowerShell script needs a small, explicit CLI contract
Treat a script invoked by people or automation as a command-line interface. The contract should tell callers what inputs exist, what validation applies, what output is intended for the pipeline, what human messages may appear, and what exit statuses mean. Stable interfaces reduce the cost of putting the script into CI later.
- Use comment-based help and examples.
- Prefer named parameters in durable automation.
- Validate input before mutation.
- Emit structured objects for machine composition.
- Use warning/error/information streams intentionally instead of encoding status into ordinary strings.
- Document exit codes if external callers depend on them.
- Avoid prompts unless the user opted into an interactive mode.
Chapter 09 will turn these rules into cmdlet-like advanced functions. The script entry point remains valuable as a thin boundary around those reusable functions.
8. Combine explicit parameters, environment overrides, and defaults without involving the profile
The following pattern resolves configuration in one place and records the source. That makes behavior explainable during incidents. The tool does not read or modify $PROFILE; profile customization remains a user concern.
param(
[string]$Region,
[ValidateRange(1,10)] [int]$RetryCount
)
$regionSource = 'default'
if ($PSBoundParameters.ContainsKey('Region')) {
$resolvedRegion = $Region
$regionSource = 'parameter'
} elseif ($env:DEPLOY_REGION) {
$resolvedRegion = $env:DEPLOY_REGION
$regionSource = 'environment'
} else {
$resolvedRegion = 'eu-central'
}
if ($PSBoundParameters.ContainsKey('RetryCount')) {
$resolvedRetry = $RetryCount
$retrySource = 'parameter'
} elseif ($env:DEPLOY_RETRY_COUNT) {
$resolvedRetry = [int]$env:DEPLOY_RETRY_COUNT
$retrySource = 'environment'
} else {
$resolvedRetry = 3
$retrySource = 'default'
}
[pscustomobject]@{
Region = $resolvedRegion; RegionSource = $regionSource
RetryCount = $resolvedRetry; RetrySource = $retrySource
}For production code, validate environment-derived conversions as carefully as command-line inputs. The lab below does that while keeping all state process-local.
9. Lab: build a configurable tool and prove the precedence order
Create a temporary script that resolves Region from explicit parameter → environment variable → default. Use a process-scoped environment variable only, then remove it. Run through the precedence cases and finally execute the script through pwsh -NoProfile if pwsh is available on your machine.
$root = Join-Path ([System.IO.Path]::GetTempPath()) 'ps-cli-config-lab'
New-Item -ItemType Directory -Path $root -Force | Out-Null
$script = Join-Path $root 'Get-ToolConfig.ps1'
@'
param([string]$Region)
if ($PSBoundParameters.ContainsKey('Region')) {
$value = $Region; $source = 'parameter'
} elseif ($env:TRAINING_REGION) {
$value = $env:TRAINING_REGION; $source = 'environment'
} else {
$value = 'eu-central'; $source = 'default'
}
[pscustomobject]@{ Region = $value; Source = $source }
'@ | Set-Content -LiteralPath $script -Encoding utf8
Remove-Item Env:TRAINING_REGION -ErrorAction SilentlyContinue
& $script
$env:TRAINING_REGION = 'us-east'
& $script
& $script -Region ap-south
Remove-Item Env:TRAINING_REGION -ErrorAction SilentlyContinue
if (Get-Command pwsh -ErrorAction SilentlyContinue) {
& pwsh -NoProfile -File $script -Region eu-west
}
Remove-Item -LiteralPath $root -Recurse -Force- No override →
Source = default. - Process environment variable set →
Source = environment. - Explicit parameter supplied →
Source = parameter. - The lab removes the temporary process environment variable.
- The script does not need any profile customization.
10. Profile and CLI-design mistakes that create environment-dependent automation
| Mistake | Why it hurts production | Safer pattern |
|---|---|---|
| Put required functions only in personal profile | CI/other users cannot run the script | Package reusable code in repository/module |
| Assume one hard-coded profile path | Hosts/platforms use different profile paths | Discover paths through $PROFILE properties |
| Let environment variables silently override explicit parameters | Caller intent becomes unclear | Document a precedence order; parameters usually win |
Depend on $PSDefaultParameterValues for correctness | Behavior changes with user session preferences | Specify correctness-critical parameters explicitly |
| Mix human presentation with machine contract | Automation has to parse text | Emit structured objects and use appropriate streams |
11. Knowledge check
Question 1. What is a PowerShell profile?
Question 2. Why should production scripts run correctly with -NoProfile?
Question 3. In the lesson’s recommended precedence, which source wins: explicit parameter or environment variable?
Question 4. What is a major risk of relying on $PSDefaultParameterValues for correctness?
Question 5. Why should a CLI-style script emit structured objects?
12. Summary
Profiles customize interactive sessions; they must not become hidden dependencies of production tools. Discover profile locations through $PROFILE, test important scripts with pwsh -NoProfile, and define a configuration precedence policy such as explicit parameter → environment → config file → built-in default. Treat $PSDefaultParameterValues as user preference, not correctness infrastructure. A reusable script should present a stable CLI contract with help, validation, structured output, intentional streams, and documented exit behavior.
13. Further reading
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.