Parameter Design and Modularization for PowerShell Scripts — From a Script That Works to a Script You Can Hand Over
· Go Komura · PowerShell, Windows, Script, Automation, Operational Improvement, Business Efficiency, Legacy Asset Reuse, Command Line
“A PowerShell script one person wrote is running in production, but only they can touch it.” “Server names and paths are hard-coded all over the code, so we rewrite the script itself every time the environment changes.” “We passed the wrong argument and it just ran anyway — we only noticed later.” In consulting engagements about operational automation, it is very common for the problem to lie not in the script itself, but in how the script is handed over and grown.
If you only ever run it on your own machine, a “script that works” with hard-coded variables is fine. But the moment you put it into Task Scheduler, hand it to a colleague, or reuse it across several servers, the design of its parameters and the organization of its shared logic determine its quality. Fortunately, PowerShell has provided the tooling for this journey to “a script you can hand over” as language features from the start: the param block, validation attributes, comment-based help, and modules.
This article takes the “script that works” that IT and operations staff at small and medium-sized businesses already have as its starting point, and lays out a practical procedure for raising its quality in stages: parameter design → input validation → help and -WhatIf support → .psm1 modularization → internal distribution. It uses PowerShell 7.x as the baseline, with notes along the way for workplaces where only Windows PowerShell 5.1 is available.
1. The Bottom Line First
- The starting point is to declare parameters in a param block and add [CmdletBinding()] to make it an advanced function. Common parameters (-Verbose, -ErrorAction, and so on) are added automatically, and passing an undefined parameter produces a binding error, so you avoid the accident of a typo being silently ignored.1
- Declare required arguments with [Parameter(Mandatory)], and always give them a type. A call that forgets a required argument stops before execution, and a value of the wrong type is rejected before execution too.2
- Push format checks into validation attributes (ValidateSet/ValidateRange/ValidateScript/ValidateNotNullOrEmpty), not if statements. When validation fails the function is never called, which structurally eliminates “runs halfway, then breaks.” The principle is to raise errors early, at the entrance.2
- Declare on/off arguments as [switch]. Home-grown flag parameters that receive $true or $false as strings are a source of caller-side accidents.2
- Write comment-based help (.SYNOPSIS/.EXAMPLE) and Get-Help works on your own commands too. Graduating from “read the code to see how to use it” is the minimum condition for handing something over.3
- Declare SupportsShouldProcess on change-oriented functions so they support -WhatIf/-Confirm. Letting the caller check the blast radius before executing has the highest cost-effectiveness of any safety device in an operational script.4
- Extract functions reused across several scripts into a .psm1 module and put it under $env:PSModulePath. Get the location right and it is loaded automatically without Import-Module. A psd1 manifest can wait until the distribution stage.5678
- Version-control modules and scripts in Git, and mind the relationship with the execution policy when distributing via a shared folder. Scripts on a UNC path can be refused execution under RemoteSigned.9
2. The param Block and [CmdletBinding()] — the Entrance to an Advanced Function
First, here is the typical “script that works” you see everywhere.
# A common example: hard-coded variables. You end up rewriting the script itself whenever the environment changes
$logDir = "D:\Logs\AppA"
$days = 90
Get-ChildItem $logDir -Filter *.log |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$days) }
Let us rewrite it with a param block and [CmdletBinding()].
[CmdletBinding()]
param(
# Required. A call that forgets to specify it stops before execution
# (Note: [string] states intent, it does not reject. Numbers and the like are automatically converted
# to strings and passed through, so write conditions you strictly want rejected with the validation attributes below)
[Parameter(Mandatory)]
[string]$LogDir,
# Give optional arguments a default value that is safe from a business point of view
[int]$Days = 90
)
Get-ChildItem -LiteralPath $LogDir -Filter *.log -File |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$Days) }
[CmdletBinding()] is a declaration that “this function (script) should behave the way a compiled cmdlet does,” and a function carrying it becomes an advanced function. It has three effects.1
- Common parameters are added automatically. Callers can use -Verbose, -Debug, -ErrorAction, -ErrorVariable and others without you implementing them. You get the standard behaviour where Write-Verbose inside the script only appears when -Verbose is specified.
- Argument mistakes stop before execution. In an advanced function, passing an undefined parameter name, or an extra argument with no corresponding positional parameter, causes parameter binding to fail. The accident where a typo like
-Dyas 30is silently ignored and the default value is used disappears.1 - $PSCmdlet becomes available. It is the entrance to cmdlet-oriented features such as ShouldProcess, discussed later.10
If an argument marked Mandatory is omitted, PowerShell prompts for input before execution. It is the first safety device against “it ran with the default because I forgot to pass a required argument.”2 The one caution is that this “prompt for input” behaviour sits badly with unattended execution. If a required argument is missing when Task Scheduler launches the script, the job can sit indefinitely at a prompt nobody can answer. For unattended execution, the standard approach is to launch with -NonInteractive so it fails immediately with an error instead of prompting.
Incidentally, when you name a function and publish it, use the Verb-Noun form and pick the verb from the approved verbs you can list with Get-Verb. Unapproved verbs still work, but they produce a warning when the module is imported.11
3. Validate Input at the Entrance — Raising Errors Early With Validate Attributes
Writing argument format checks as if statements in the function body tends to introduce bugs — missed checks, or side effects that run before the check does. In PowerShell, validation attributes let validation live alongside the parameter declaration. Validation happens before the function is called, and if it fails not a single line of the body runs.2
[CmdletBinding()]
param(
# Accept only folders that exist. $_ is the value being validated
[Parameter(Mandatory)]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string]$LogDir,
# Restrict the retention period to 1-3650 days. Prevents the accident where 0 or a negative number means "all files"
[ValidateRange(1, 3650)]
[int]$Days = 90,
# Fix the set of choices. Tab completion starts working too
[ValidateSet('Zip', 'Move', 'ReportOnly')]
[string]$Mode = 'ReportOnly',
# Reject empty strings and $null. Standard equipment for string parameters
[ValidateNotNullOrEmpty()]
[string]$ReportName = 'log-report',
# On/off is a switch. Specified means $true, omitted means $false
[switch]$IncludeSubfolders
)
Here is a rule of thumb for choosing between them.
| Attribute | Purpose | Typical example from the field |
|---|---|---|
| ValidateSet | Restrict a value to a fixed set of choices and enable tab completion2 | Operating mode, environment name (Dev/Test/Prod) |
| ValidateRange | Range limits on numbers and dates2 | Retention days, retry count, port number |
| ValidateScript | Validate with an arbitrary script. Fails on $false or an exception2 | Checking a path exists, checking date ordering |
| ValidateNotNullOrEmpty | Reject $null, empty strings, and empty collections2 | Practically every string parameter |
| ValidatePattern | Format check with a regular expression2 | Document numbers, host naming conventions |
Two additional notes. First, the declaration order of validation attributes matters. If you write a validation attribute after the type, the value can be validated before type conversion, which can lead to unexpected failures — so the best practice recommended by the official documentation is the order attribute → type → variable name.2 Second, ValidateScript’s ErrorMessage argument (a custom error message) is a PowerShell 6-and-later feature and is not available in Windows PowerShell 5.1.2 In mixed environments including 5.1, the safe options are to throw inside the validation script to produce your own message, or to leave the default message as-is.
Once you start writing elaborate validation in ValidateScript, that is also a sign it is time to write tests. Putting the behaviour verification of the validation logic itself onto the patterns described in “Testing PowerShell with Pester” makes it harder to break.
4. The Basics of Pipeline Input — ValueFromPipeline and the process Block
Your own functions become combinable in the same way as PowerShell’s built-in commands once they can be used in a pipeline, as in Get-Content servers.txt | Test-AppServer. All you need are two things: a ValueFromPipeline declaration and a process block.2
function Test-AppServer {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$ComputerName
)
begin { $results = @() } # Runs once before pipeline processing
process {
# Runs for each element flowing in from the pipeline
foreach ($name in $ComputerName) {
$results += [pscustomobject]@{
ComputerName = $name
# -ComputerName works on both 5.1 and 7 (-TargetName, added in 7, does not exist on 5.1)
Reachable = Test-Connection -ComputerName $name -Count 1 -Quiet
}
}
}
end { $results } # Runs once at the end
}
There is only one point to nail down. If you accept pipeline input, put the work in the process block. Without a process block, sending multiple values down the pipeline results in the classic bug where only the last item is processed.10 begin and end are optional, so if you are unsure, remembering “body in process; begin/end if you need aggregation” is enough for practical work.
5. Making Get-Help and -WhatIf Work — the Minimum Conditions for Handing Something Over
5.1. Comment-Based Help
PowerShell users, when they meet a command they do not know, reach for Get-Help first. Whether your own function can participate in that culture comes down to whether you have written comment-based help. Simply writing comments with special keywords is enough for Get-Help to display help in the same format as a standard cmdlet.3
5.2. SupportsShouldProcess and -WhatIf
For functions that delete, move, or change settings, declare [CmdletBinding(SupportsShouldProcess)]. That alone adds the -WhatIf and -Confirm parameters automatically, and in the body you branch on the return value of $PSCmdlet.ShouldProcess() to decide whether to actually make the change.4
Here is the finished form of a function of hand-over quality, incorporating both.
function Remove-OldAppLog {
<#
.SYNOPSIS
Deletes log files past their retention period from the specified folder.
.DESCRIPTION
Deletes *.log files whose LastWriteTime is older than the retention period.
Use -WhatIf to only check which files would be deleted.
.EXAMPLE
Remove-OldAppLog -LogDir 'D:\Logs\AppA' -Days 90 -WhatIf
Only displays the deletion targets; nothing is actually deleted.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string]$LogDir,
[ValidateRange(1, 3650)]
[int]$Days = 90
)
process {
$limit = (Get-Date).AddDays(-$Days)
# -File excludes folders (so a folder whose name ends in ".log" is not deleted by mistake)
Get-ChildItem -LiteralPath $LogDir -Filter *.log -File |
Where-Object { $_.LastWriteTime -lt $limit } |
ForEach-Object {
# ShouldProcess returns $false when -WhatIf is used and when -Confirm is declined
if ($PSCmdlet.ShouldProcess($_.FullName, "Delete")) {
Remove-Item -LiteralPath $_.FullName
}
}
}
}
Type Remove-OldAppLog -LogDir D:\Logs\AppA -WhatIf and you get a list of “What if: …” lines and nothing is deleted. When you hand a change-oriented script to someone, hand over the -WhatIf rehearsal procedure with it — this is the operational pattern we recommend repeatedly on this site. A worked example of building this into a real log-tidying script is described in detail in “Applied PowerShell Scripting — Safely Automating Log Investigation, Archiving, and Reporting.”
One more caution from the official deep-dive article: do not place too much faith in -WhatIf always propagating to the commands you call. To be certain, pass -WhatIf:$WhatIfPreference explicitly to inner commands such as Remove-Item.4
6. Turning Shared Logic Into a .psm1 Module
6.1. .psm1 and Export-ModuleMember
As functions grow, you start wanting to use the same function from several scripts. Multiplying them by copy-paste means fixes no longer reach every copy, so extract shared functions into a script module (.psm1). Creating one is almost anticlimactically simple: just save the file containing your functions with a .psm1 extension.5
# AppOpsTools.psm1 — shared module for internal operations tooling
function Remove-OldAppLog { <# the function from the previous section #> }
function Get-AppLogSummary { <# aggregation function #> }
# Internal helper function. Not exposed to the outside
function ConvertTo-InternalPath { <# ... #> }
# State the functions to publish explicitly. Without this, every function is published
Export-ModuleMember -Function Remove-OldAppLog, Get-AppLogSummary
If you do not write Export-ModuleMember, all functions and aliases in the module are exported (variables are not). It is optional, but making the public surface explicit is considered a best practice.12 Hiding internal helpers leaves you room to refactor freely later.
6.2. Where to Put It — $env:PSModulePath and Automatic Loading
Modules go underneath one of the folders listed in $env:PSModulePath, inside a folder with the same name as the module (AppOpsTools\AppOpsTools.psm1). If the folder name and the file’s base name do not match, it is not recognized as a module.65 The default locations are as follows, and the fact that the paths differ between Windows PowerShell 5.1 and PowerShell 7 is a common stumbling block in the field.6
| Scope | PowerShell 7 | Windows PowerShell 5.1 |
|---|---|---|
| Your own use (CurrentUser) | $HOME\Documents\PowerShell\Modules |
$HOME\Documents\WindowsPowerShell\Modules |
| All users (AllUsers) | $env:ProgramFiles\PowerShell\Modules |
$env:ProgramFiles\WindowsPowerShell\Modules |
Put it in the right place and PowerShell imports it automatically the first time one of the module’s commands is run, without you writing Import-Module (module auto-loading).7 In other words, users can treat it as though “the command was there all along.” The practical rhythm is to Import-Module by full path while you are still experimenting, and to move it under PSModulePath once it settles down.
There are two environment-dependent cautions. The Documents folder may have been relocated by OneDrive folder redirection, in which case user-scope modules end up under OneDrive too.6 Also, placing modules in the all-users scope requires administrator rights. If you are putting it on a server, use the AllUsers scope so that Task Scheduler’s run-as account can see it too — that avoids “it works on my machine but not on the server.”
6.3. The psd1 Manifest Comes at the Distribution Stage
A module manifest (.psd1) is a hash table file describing metadata such as the module’s version and dependencies, and it is not mandatory. The only key a manifest requires is ModuleVersion.8 While you are using it within your own team, a standalone .psm1 is enough; once you reach the stage of distributing to other departments or managing versions rigorously, generate one with New-ModuleManifest.8
New-ModuleManifest -Path .\AppOpsTools\AppOpsTools.psd1 `
-RootModule 'AppOpsTools.psm1' `
-ModuleVersion '1.0.0' `
-FunctionsToExport 'Remove-OldAppLog', 'Get-AppLogSummary' `
-PowerShellVersion '5.1'
The generated psd1 is a commented template, so you can grow just the keys you need.8
6.4. The Essentials of Internal Sharing and Version Control
- Keep the master copy in Git. Scripts and modules are text, so they suit Git well, and being able to trace “when, by whom, and why it was changed” is itself the reliability of an operational script. Aligning ModuleVersion updates with commits makes it easy to identify which version is installed on a server.
- The basic distribution shape is “copy from a shared folder into each machine’s PSModulePath.” Copying the entire module folder gives you a manual install.7 Adding a path on a shared folder directly to PSModulePath is not something we recommend for everyday use, given the premise that shares can be “slow, disconnected, or simply not there” (details in “Pitfalls of Network Drives and UNC Paths”).
- Mind the relationship with the execution policy. The default RemoteSigned allows locally created unsigned scripts, but on systems that do not distinguish UNC paths from internet paths, scripts on a UNC path can be refused execution. Files carrying a download mark are also blocked, and need to be released with Unblock-File or signed.9 If you are going to scale up internal distribution, check the combination with code signing in “PowerShell Execution Policy and Script Signing.”
7. Standard Practice in the Field (Decision Table)
| Question | Options | Rule of thumb |
|---|---|---|
| Receiving arguments | Hard-coded variables / param block | If it is used more than once, or by someone else, param is the only choice. Bias defaults toward the safe side2 |
| [CmdletBinding()] | Leave it off / Add it | Always add it to anything you hand over or put into production. Typos stop before execution1 |
| Input checks | if statements in the body / Validation attributes | Format checks on a single parameter go in attributes. Only cross-parameter validation goes in the body2 |
| Safety device for changes | A custom -TestMode argument / SupportsShouldProcess | Do not invent your own flag. Ride on the standard -WhatIf/-Confirm4 |
| How to hold shared logic | Copy-paste / Dot-sourcing / .psm1 module | Modularize as soon as it is shared by a second script. State public functions with Export-ModuleMember512 |
| Where to put the module | Any folder + Import-Module / Under PSModulePath | Put things you use regularly in the prescribed location and ride on auto-loading. Use the AllUsers scope on servers67 |
| psd1 manifest | Create it from the start / Create it at the distribution stage | Use New-ModuleManifest once you are going outside your team or managing versions rigorously8 |
8. Summary
- Making a function advanced with a param block and [CmdletBinding()] is the starting point for “a script you can hand over.” Common parameters are added, and argument mistakes stop before execution.
- Type declarations, [Parameter(Mandatory)], safe-side defaults, and validation attributes raise errors early, at the entrance. ValidateSet also improves usability with tab completion.
- Accept pipeline input with the pair of ValueFromPipeline and a process block. Forget process and only the last item is processed.
- Make Get-Help work with comment-based help, and make change-oriented functions support -WhatIf via SupportsShouldProcess. Being able to rehearse is an operational safety device.
- Extract shared functions into a .psm1, state the public surface with Export-ModuleMember, and place it under PSModulePath. Note that the paths differ between 5.1 and 7.
- The psd1 manifest comes at the distribution stage. Manage the master copy in Git, and when distributing via a shared folder, check the execution policy (the relationship between UNC paths and RemoteSigned).
Related Articles
- PowerShell Command Basics — The Operations to Learn First and How to Use Them Safely
- Applied PowerShell Scripting — Safely Automating Log Investigation, Archiving, and Reporting
- Testing PowerShell with Pester — A Practical Approach to Making Operations Scripts Harder to Break
- PowerShell Execution Policy and Script Signing
- PowerShell Error Handling and Retry Design
- Handling Credentials Safely in PowerShell — Banishing Plain-Text Passwords From Your Scripts
Related Consulting Areas
KomuraSoft LLC handles tidying up and modularizing PowerShell scripts that have become one person’s private domain, design reviews of operational automation scripts, and building mechanisms for internal distribution and version control. We are happy to start from taking inventory of script assets that “work, but nobody dares touch.”
- Technical Consulting & Design Review
- Windows App Development
- Legacy Asset Reuse & Migration Support
- Contact
References
-
Microsoft Learn, about_Functions_CmdletBindingAttribute. On the CmdletBinding attribute making a function behave like a compiled cmdlet, common parameters being added automatically, $PSCmdlet becoming available, binding failing on unknown parameters or unmatched positional arguments, and SupportsShouldProcess adding the Confirm/WhatIf parameters. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, about_Functions_Advanced_Parameters. On the Parameter attribute and Mandatory, ValueFromPipeline, switch parameters, the specification of validation attributes such as ValidateSet/ValidateRange/ValidateScript/ValidateNotNullOrEmpty/ValidatePattern, the function not being called when validation fails, declaring attributes before the type being a best practice, and ValidateScript’s ErrorMessage argument being PowerShell 6 and later. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15
-
Microsoft Learn, about_Comment_Based_Help. On writing comment-based help with keywords such as .SYNOPSIS/.DESCRIPTION/.PARAMETER/.EXAMPLE causing Get-Help to display it in the same format as XML help, and the placement rules for scripts and functions respectively. ↩ ↩2
-
Microsoft Learn, Everything you wanted to know about ShouldProcess. On -WhatIf/-Confirm being created automatically merely by specifying SupportsShouldProcess, how to branch on $PSCmdlet.ShouldProcess(), and the recommendation not to over-trust -WhatIf propagation but to pass it explicitly to inner commands. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, How to Write a PowerShell Script Module. On simply saving with a .psm1 extension making it a script module, saving it in a folder with the same name as the script, all functions being published by default while variables are not, and the recommendation to state the published functions explicitly with Export-ModuleMember. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, about_PSModulePath. On $env:PSModulePath being the list of module search folders, the default CurrentUser/AllUsers scope paths differing between PowerShell 7 and Windows PowerShell 5.1, and the location of Documents potentially changing through OneDrive or folder redirection. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, about_Modules. On modules under PSModulePath being imported automatically the first time one of their commands runs (module auto-loading), the manual installation method of copying the whole module folder, and the default module locations. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, New-ModuleManifest. On a module manifest (.psd1) being a hash table describing a module’s contents, attributes, and prerequisites, and not being mandatory; the only required key being ModuleVersion; and New-ModuleManifest generating a skeleton usable as a template. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, about_Execution_Policies. On RemoteSigned allowing locally created unsigned scripts while requiring a signature for scripts originating from the internet, scripts on a UNC path sometimes not being allowed to run under RemoteSigned on systems that do not distinguish UNC paths from internet paths, and unblocking with Unblock-File. ↩ ↩2
-
Microsoft Learn, about_Functions_Advanced_Methods. On the begin/process/end input processing methods available in advanced functions, and the ShouldProcess method being called from within the process block and requiring declaration via the CmdletBinding attribute. ↩ ↩2
-
Microsoft Learn, Approved Verbs for PowerShell Commands. On command names being expected in Verb-Noun form, the list of approved verbs and how to check them with Get-Verb, and a warning being displayed when importing a module containing unapproved verbs. ↩
-
Microsoft Learn, Export-ModuleMember. On Export-ModuleMember being the cmdlet that specifies which members a script module exports, functions and aliases being exported while variables are not when it is unspecified, and it being optional but a best practice for indicating the author’s intent. ↩ ↩2
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Calling COM and .NET from PowerShell — Widening What Your Scripts Can Reach
A practical guide to calling .NET classes from PowerShell, embedding C# and Win32 APIs with Add-Type, driving COM, cleaning up leftover E...
The Differences Between Windows PowerShell 5.1 and PowerShell 7 — A Practical Guide to Migrating In-House Scripts
The relationship between Windows PowerShell 5.1 and PowerShell 7 (side-by-side coexistence and pwsh.exe), Microsoft's official position t...
Automating Excel and CSV Work with PowerShell — Practical Recipes for Aggregation, Reconciliation, and Report Output
Practical recipes for automating CSV aggregation, reconciliation, and Excel report output with PowerShell. Covers the default encodings o...
Practical PowerShell Command Recipes — Growing the Small Tools You Use Every Day
A practical roundup of PowerShell commands for everyday work, covering where to use Measure-Object, Group-Object, Select-String, Compare-...
Automating PC Provisioning With winget + PowerShell — Making the Runbook Executable
How to make new-hire PC setup reproducible. Covers installing applications with winget and export/import, declarative configuration with ...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Frequently Asked Questions
Common questions about the topic of this article.
- What changes when I add [CmdletBinding()] to a PowerShell param block?
- The function or script is treated as an advanced function, gaining the same behaviour as a compiled cmdlet. Specifically, common parameters such as -Verbose and -ErrorAction are added automatically, the $PSCmdlet variable becomes available, and passing an undefined parameter or an extra positional argument produces a binding error. Because a mistyped argument is no longer silently ignored, adding it is the baseline for operational scripts.
- Should I write argument checks with Validate attributes or with if statements?
- The standard practice is to push format checks for parameters into validation attributes such as ValidateSet, ValidateRange, and ValidateScript. Validation happens before the function body runs, so an invalid value produces an error without a single line executing, which prevents the accident of breaking halfway through. ValidateSet also has the practical benefit of enabling tab completion. Checks that span several parameters, or that depend on runtime state, belong in if statements in the body.
- Where should I put my own PowerShell module (.psm1)?
- Create a folder with the same name as the module underneath one of the folders listed in $env:PSModulePath. For your own use only, the default is $HOME\Documents\PowerShell\Modules on PowerShell 7; for all users it is $env:ProgramFiles\PowerShell\Modules. Note that on Windows PowerShell 5.1 the paths differ, using WindowsPowerShell\Modules instead. Put it in one of these places and it is loaded automatically the first time one of its commands runs, without you having to write Import-Module.
- Do I always need to create a module manifest (psd1)?
- No. A standalone .psm1 with no manifest works fine as a module. A manifest becomes necessary at the distribution stage, when you want metadata such as a version number, the required PowerShell version, dependent modules, and an explicit list of exported commands; you generate one with New-ModuleManifest. The only key a manifest requires is ModuleVersion, so it is perfectly reasonable to start minimal and grow it as needed.
- Why is a script placed on an internal shared folder blocked by the execution policy?
- The default RemoteSigned policy allows locally created scripts to run unsigned, but requires a signature for scripts marked as originating from the internet. The official documentation states explicitly that on systems configured not to distinguish UNC paths from internet paths, scripts on a shared folder can be refused execution under RemoteSigned. If you are going to scale up internal distribution, consider combining code signing with AllSigned, or configuring the intranet zone.
Author Profile
Profile page for the article author.
Go Komura
Representative of KomuraSoft LLC
Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.
Public links