Diseño de parámetros y modularización de scripts de PowerShell — de un «script que funciona» a un «script que se puede entregar»
· Actualizado el: · Go Komura · PowerShell, Windows, Scripts, Automatización, Mejora operativa, Eficiencia empresarial, Aprovechamiento de activos existentes, Línea de comandos
«El script de PowerShell que escribió el encargado funciona, pero solo esa persona puede tocarlo», «los nombres de servidor y las rutas están escritos directamente por todo el código, y cada vez que cambia el entorno hay que reescribir el cuerpo del script», «se pasó un argumento equivocado y el script funcionó igualmente sin avisar, y el problema se descubrió después» — en las consultas sobre automatización operativa, es muy frecuente que el problema no sea el script en sí, sino «cómo se entrega y cómo se hace crecer el script».
Si el script solo se ejecuta en la propia máquina, un «script que funciona» con variables escritas directamente no da ningún problema. Pero en el momento en que se sube al Programador de tareas, se entrega a un compañero o se reutiliza en varios servidores, la calidad queda determinada por el diseño de los argumentos y por cómo se organiza el procesamiento común. Afortunadamente, PowerShell trae desde el principio, como parte del propio lenguaje, las herramientas necesarias para llegar a ese «script que se puede entregar»: el bloque param, los atributos de validación, la ayuda basada en comentarios y los módulos.
En este artículo partimos del «script que funciona» que ya tienen los encargados de sistemas y operaciones de pymes, y ordenamos el procedimiento práctico para elevar su calidad paso a paso, en este orden: diseño de argumentos → validación de entrada → ayuda y compatibilidad con -WhatIf → modularización en .psm1 → distribución interna. Tomamos PowerShell 7.x como referencia, añadiendo en cada punto las advertencias necesarias para los entornos que solo disponen de Windows PowerShell 5.1.
1. Conclusión, primero
- El punto de partida es declarar los argumentos en un bloque param y añadir [CmdletBinding()] para convertir la función en una «función avanzada». Los parámetros comunes (-Verbose, -ErrorAction, etc.) se añaden automáticamente, y si se pasa un parámetro no definido se produce un error de enlace, lo que evita el problema de que un error de tecleo se ignore en silencio. 1
- Los argumentos obligatorios se declaran con [Parameter(Mandatory)] y siempre se les asigna un tipo. Una llamada en la que se olvide un argumento obligatorio se detiene antes de ejecutarse, y un valor de tipo incorrecto también se rechaza antes de la ejecución. 2
- La comprobación de formato se delega en atributos de validación (ValidateSet, ValidateRange, ValidateScript, ValidateNotNullOrEmpty) en lugar de en instrucciones if. Si la validación falla, la función ni siquiera se invoca, lo que elimina de forma estructural el problema de «ejecutarse a medias y romperse». El principio es sacar los errores lo antes posible, en la entrada. 2
- Los argumentos de activación/desactivación se declaran con [switch]. Los indicadores caseros que reciben $true o $false como cadena de texto son fuente de errores para quien llama a la función. 2
- Si escribe ayuda basada en comentarios (.SYNOPSIS, .EXAMPLE), Get-Help también funcionará con sus propios comandos. Abandonar el «lea el código para saber cómo se usa» es el requisito mínimo para entregar un script a otra persona. 3
- Las funciones que modifican algo deben declarar SupportsShouldProcess para admitir -WhatIf y -Confirm. Que quien llama a la función pueda comprobar el alcance del impacto antes de ejecutarla es, como mecanismo de seguridad para scripts operativos, lo que ofrece la mejor relación coste-beneficio. 4
- Las funciones que se reutilizan en varios scripts se extraen a un módulo .psm1 y se colocan dentro de $env:PSModulePath. Si la ubicación es correcta, se cargan automáticamente sin necesidad de Import-Module. Basta con añadir el manifiesto psd1 en la «etapa de distribución». 5678
- Los módulos y los scripts se gestionan con control de versiones en Git, y al distribuirlos por una carpeta compartida hay que prestar atención a la directiva de ejecución. Un script ubicado en una ruta UNC puede ver rechazada su ejecución bajo RemoteSigned. 9
2. El bloque param y [CmdletBinding()] — la puerta de entrada a la «función avanzada»
Para empezar, así es el ejemplo típico de «script que funciona» que se ve habitualmente sobre el terreno.
# Ejemplo habitual: variables escritas directamente. Hay que reescribir el cuerpo cada vez que cambia el entorno
$logDir = "D:\Logs\AppA"
$days = 90
Get-ChildItem $logDir -Filter *.log |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$days) }
Reescribamos esto con un bloque param y [CmdletBinding()].
[CmdletBinding()]
param(
# Obligatorio. Una llamada en la que se olvide indicarlo se detiene antes de ejecutarse
# (nota: [string] expresa la intención, no es un rechazo. Los valores numéricos, por ejemplo,
# se convierten automáticamente a cadena de texto, así que las condiciones que se quieran
# rechazar de forma estricta se escriben en los atributos de validación descritos más adelante)
[Parameter(Mandatory)]
[string]$LogDir,
# Los argumentos opcionales llevan un valor por defecto seguro para el negocio
[int]$Days = 90
)
Get-ChildItem -LiteralPath $LogDir -Filter *.log -File |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$Days) }
[CmdletBinding()] es una declaración que dice «ejecuta esta función (script) con el mismo comportamiento que un cmdlet compilado»; la función a la que se le añade se convierte en una función avanzada (advanced function). Tiene tres efectos. 1
- Se añaden automáticamente los parámetros comunes. Sin necesidad de implementar usted mismo -Verbose, -Debug, -ErrorAction, -ErrorVariable, etc., quien llama a la función puede usarlos. Obtiene directamente el comportamiento estándar por el que Write-Verbose dentro del script solo se muestra cuando se indica -Verbose.
- Los errores en los argumentos se detienen antes de la ejecución. En una función avanzada, si se pasa un nombre de parámetro no definido, o un argumento adicional que no tiene un parámetro posicional correspondiente, falla el enlace de parámetros. Desaparece el problema de que un error de tecleo como
-Dyas 30se ignore en silencio y el script funcione con el valor por defecto. 1 - Se puede usar $PSCmdlet. Es la puerta de entrada a funciones orientadas a cmdlets, como ShouldProcess, que se describe más adelante. 10
Si se omite un argumento marcado como Mandatory, PowerShell solicita su valor antes de ejecutar. Es el primer mecanismo de seguridad que evita que «un argumento obligatorio que se olvidó pasar acabe ejecutándose con un valor por defecto». 2 Una advertencia: este comportamiento de «solicitar el valor» convive mal con la ejecución desatendida. Si al iniciar desde el Programador de tareas falta un argumento obligatorio, el trabajo puede quedarse detenido en un mensaje que nadie va a responder. En la ejecución desatendida, la práctica habitual es iniciar con -NonInteractive para que, en lugar de esperar una respuesta, falle de inmediato con un error.
Como no siempre queda claro «dónde se escribe cada cosa», aquí tiene un ejemplo de cómo rellenar la pestaña «Acciones» del Programador de tareas (para ejecutarlo con PowerShell 7).
| Campo | Ejemplo de contenido |
|---|---|
| Programa o script | C:\Program Files\PowerShell\7\pwsh.exe |
| Agregar argumentos (opcional) | -NoProfile -NonInteractive -File "C:\Scripts\Remove-OldAppLog.ps1" -LogDir "D:\Logs\AppA" -Days 90 |
| Iniciar en (opcional) | C:\Scripts |
Si lo va a ejecutar con Windows PowerShell 5.1, el campo Programa es powershell.exe. Hay tres puntos clave: usar -NoProfile para eliminar las diferencias de entorno y la lentitud de inicio que provoca la carga del perfil; usar -NonInteractive para evitar la espera de un mensaje como la mencionada antes; y entrecomillar tanto la ruta del script como los valores de los argumentos siempre que puedan contener espacios. Todo lo que se escriba después de -File se pasa como argumento del script, así que los parámetros propios del script deben ir después de -File. Si deja «Iniciar en (opcional)» vacío, el directorio de trabajo será el predeterminado, así que indíquelo siempre en scripts que usen rutas relativas.
Además, cuando se asigna un nombre a una función para publicarla, se debe usar el formato Verbo-Sustantivo, eligiendo el verbo entre los verbos aprobados que se pueden consultar con Get-Verb. Un verbo no aprobado también funciona, pero genera una advertencia al importar el módulo. 11
3. La validación de entrada, en la puerta de entrada — «errores tempranos» con los atributos Validate
Si la comprobación de formato de los argumentos se escribe con instrucciones if en el cuerpo de la función, es fácil que se cuelen comprobaciones olvidadas o errores del tipo «se ejecuta un efecto secundario antes de la comprobación». En PowerShell, los atributos de validación permiten integrar la validación en la propia declaración del parámetro. La validación se realiza antes de que se llame a la función, y si falla, no se ejecuta ni una sola línea del cuerpo. 2
[CmdletBinding()]
param(
# Solo acepta carpetas que existan. $_ es el valor que se está validando
[Parameter(Mandatory)]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string]$LogDir,
# Limita los días de retención a un rango de 1 a 3650. Evita el problema de que un 0 o un número negativo hagan que "se apliquen todos los archivos"
[ValidateRange(1, 3650)]
[int]$Days = 90,
# Fija las opciones posibles. También habilita el autocompletado con tabulador
[ValidateSet('Zip', 'Move', 'ReportOnly')]
[string]$Mode = 'ReportOnly',
# Rechaza cadena vacía y $null. Equipamiento básico de cualquier argumento de tipo cadena
[ValidateNotNullOrEmpty()]
[string]$ReportName = 'log-report',
# Activación/desactivación con switch. $true si se indica, $false si se omite
[switch]$IncludeSubfolders
)
Resumimos el criterio para elegir entre ellos.
| Atributo | Uso | Ejemplo típico sobre el terreno |
|---|---|---|
| ValidateSet | Restringe el valor a un conjunto fijo de opciones y habilita el autocompletado con tabulador2 | Modo de funcionamiento, nombre de entorno (Dev/Test/Prod) |
| ValidateRange | Restricción de rango para números o fechas2 | Días de retención, número de reintentos, número de puerto |
| ValidateScript | Validación mediante un script arbitrario. Falla con $false o con una excepción2 | Comprobación de existencia de una ruta, relación de precedencia entre fechas |
| ValidateNotNullOrEmpty | Rechaza $null, cadena vacía y colección vacía2 | Prácticamente cualquier argumento de tipo cadena |
| ValidatePattern | Comprobación de formato mediante expresiones regulares2 | Número de comprobante, reglas de nomenclatura de nombres de host |
Dos aclaraciones. Primero, hay que prestar atención al orden de declaración de los atributos de validación. Si el atributo de validación se escribe después del tipo, se puede validar el valor antes de la conversión de tipo y provocar fallos inesperados, así que la práctica recomendada en la documentación oficial es escribir atributo → tipo → nombre de variable, en ese orden. 2 Segundo, el argumento ErrorMessage de ValidateScript (mensaje de error propio) es una función disponible desde PowerShell 6 en adelante y no se puede usar en Windows PowerShell 5.1. 2 En entornos mixtos con 5.1, lo más seguro es lanzar (throw) un mensaje propio dentro del script de validación, o dejar el mensaje predeterminado tal cual.
Si empieza a escribir validaciones elaboradas con ValidateScript, eso también es una señal de que conviene escribir pruebas. Verificar el funcionamiento de la propia lógica de validación se vuelve más resistente a roturas si se apoya en el patrón que se organiza en «Mantenimiento de pruebas de PowerShell con Pester».
4. Fundamentos de la entrada por pipeline — ValueFromPipeline y el bloque process
Si una función propia también se puede usar en una tubería como Get-Content servers.txt | Test-AppServer, se puede combinar con la misma sensación que los comandos estándar de PowerShell. Lo que se necesita son dos cosas: la declaración de ValueFromPipeline y el bloque process. 2
function Test-AppServer {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$ComputerName
)
begin { $results = @() } # Se ejecuta una sola vez, antes de procesar la tubería
process {
# Se ejecuta por cada elemento que llega desde la tubería
foreach ($name in $ComputerName) {
$results += [pscustomobject]@{
ComputerName = $name
# -ComputerName se puede usar tanto en 5.1 como en 7 (-TargetName, añadido en 7, no existe en 5.1)
Reachable = Test-Connection -ComputerName $name -Count 1 -Quiet
}
}
}
end { $results } # Se ejecuta una sola vez, al final
}
Solo hay un punto clave que retener: si la función recibe entrada por pipeline, el procesamiento debe escribirse en el bloque process. Sin el bloque process, aunque se envíen varios valores por la tubería, se produce el error típico de que solo se procesa el último. 10 begin y end se pueden omitir, así que si tiene dudas, con recordar «el cuerpo va en process, y begin/end solo si necesita agregación» es suficiente para el trabajo diario.
Como cuesta hacerse una idea real de esta trampa, aquí tiene un ejemplo incorrecto para comparar.
# Ejemplo incorrecto: no tiene bloque process. Aunque se envíen 3 valores por la tubería, solo se procesa el último
function Test-AppServerBad {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$ComputerName
)
# Si no se escribe ninguno de begin/process/end, todo el cuerpo se trata como si fuera el bloque end, y solo se ejecuta "una vez, al final".
# Los elementos de la tubería se enlazan al parámetro de uno en uno, así que en el momento en que se llega a end, lo único que queda es el último
foreach ($name in $ComputerName) {
[pscustomobject]@{ ComputerName = $name }
}
}
'SV01', 'SV02', 'SV03' | Test-AppServerBad # → Solo una línea de SV03. SV01 y SV02 se descartan en silencio
'SV01', 'SV02', 'SV03' | Test-AppServer # → Devuelve 3 líneas (la versión anterior, con bloque process)
Lo complicado es que no aparece ni un solo error. Además, si se pasan los valores como argumento, tal como en Test-AppServerBad -ComputerName 'SV01','SV02','SV03', los tres se procesan correctamente, así que si solo se verifica el funcionamiento llamando por argumento, no hay forma de darse cuenta. Si escribe una función que recibe entrada por pipeline, incluya siempre una prueba que envíe varios valores por la tubería.
5. Hacer que funcionen Get-Help y -WhatIf — el requisito mínimo para entregar un script
5.1. Ayuda basada en comentarios
Cuando un usuario de PowerShell se encuentra con un comando que no conoce, lo primero que hace es ejecutar Get-Help. Que una función propia pueda sumarse a esa cultura depende de si se ha escrito ayuda basada en comentarios. Basta con escribir un comentario con unas palabras clave especiales para que Get-Help muestre la ayuda en el mismo formato que los cmdlets estándar. 3
A continuación se listan las palabras clave más usadas. No hace falta escribirlas todas: basta con ir añadiéndolas en este orden: como mínimo, .SYNOPSIS y .EXAMPLE, y si el script se va a entregar a otra persona, hasta .DESCRIPTION y .PARAMETER. 3
| Palabra clave | Qué escribir |
|---|---|
| .SYNOPSIS | Resumen de una línea. Aparece al principio de Get-Help |
| .DESCRIPTION | Explicación detallada. Los requisitos previos y los efectos secundarios van aquí |
| .PARAMETER nombre_del_parámetro | Explicación de cada parámetro. El nombre del parámetro va justo después de la palabra clave |
| .EXAMPLE | Ejemplo de uso. En la primera línea, el comando a ejecutar; en las siguientes, su explicación. Se puede repetir varias veces |
| .INPUTS | Tipo de objeto que se puede recibir por la tubería |
| .OUTPUTS | Tipo de objeto que devuelve |
| .NOTES | Información adicional: autor, fecha de actualización, limitaciones conocidas, etc. |
| .LINK | Comandos relacionados o URL. La primera URL es el destino de Get-Help -Online |
5.2. SupportsShouldProcess y -WhatIf
En las funciones que eliminan, mueven o cambian configuraciones, se declara [CmdletBinding(SupportsShouldProcess)]. Con esto basta para que se añadan automáticamente los parámetros -WhatIf y -Confirm, y en el cuerpo de la función se decide si se ejecuta realmente el cambio según el valor devuelto por $PSCmdlet.ShouldProcess(). 4
Así queda, en su forma final, una función con calidad suficiente para entregar, que incorpora ambas cosas.
function Remove-OldAppLog {
<#
.SYNOPSIS
Elimina, de la carpeta indicada, los archivos de registro cuyo plazo de retención ya venció.
.DESCRIPTION
Elimina los archivos *.log cuya fecha LastWriteTime sea anterior a los días de retención indicados.
Con -WhatIf solo se puede comprobar cuáles serían los archivos a eliminar.
.EXAMPLE
Remove-OldAppLog -LogDir 'D:\Logs\AppA' -Days 90 -WhatIf
Solo muestra los archivos objetivo de eliminación; no elimina nada realmente.
#>
[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)
# Excluye las carpetas con -File (para no eliminar por error una carpeta cuyo nombre termine en ".log")
Get-ChildItem -LiteralPath $LogDir -Filter *.log -File |
Where-Object { $_.LastWriteTime -lt $limit } |
ForEach-Object {
# ShouldProcess devuelve $false cuando se usa -WhatIf o cuando -Confirm se rechaza
if ($PSCmdlet.ShouldProcess($_.FullName, "Eliminar")) {
Remove-Item -LiteralPath $_.FullName
}
}
}
}
Si escribe Remove-OldAppLog -LogDir D:\Logs\AppA -WhatIf, solo aparece una lista de «What if: …» y no se borra nada. Cuando se entrega a otra persona un script que modifica algo, hay que entregar también el procedimiento de ensayo con -WhatIf — este es el patrón operativo que este sitio recomienda repetidamente. Puede ver un ejemplo de integración en un script real de limpieza de registros en «Aplicación práctica de scripts de PowerShell — automatizar de forma segura la investigación, el archivado y la generación de informes de registros».
Además, el propio artículo explicativo oficial advierte que no conviene confiar ciegamente en que -WhatIf siempre se propague hasta los comandos que se llaman internamente. Para tener plena seguridad, hay que pasar explícitamente -WhatIf:$WhatIfPreference a comandos internos como Remove-Item. 4
6. Convertir el procesamiento común en un módulo .psm1
6.1. .psm1 y Export-ModuleMember
A medida que las funciones crecen, surge la necesidad de usar la misma función desde varios scripts. Si se multiplican mediante copiar y pegar, las correcciones dejan de propagarse a todas las copias, así que conviene mantener las funciones comunes agrupadas.
La opción intermedia, previa a esto, es el dot sourcing (fuente con punto). Si se ejecuta con un punto y un espacio antes de la ruta del script, ese script se ejecuta en el ámbito de quien lo invoca, y las funciones y variables definidas dentro quedan disponibles tal cual en el llamador. 12
# Carga Common.ps1, donde están escritas las funciones comunes (el punto y el espacio al principio son el dot sourcing)
. C:\Scripts\Common.ps1
# Se puede llamar directamente a las funciones definidas dentro de Common.ps1
Remove-OldAppLog -LogDir 'D:\Logs\AppA' -WhatIf
Es sencillo, pero quien lo llama necesita conocer la ruta física del archivo, y no se puede elegir el alcance de la visibilidad (funciones, variables y alias, todo entra tal cual). Es suficiente para dividir un único script, pero el criterio para pasar a la modularización es el momento en que se quiere usar la misma función desde un segundo script.
La forma de crear un módulo es sorprendentemente sencilla: basta con guardar con la extensión .psm1 el archivo donde están escritas las funciones. 5
# AppOpsTools.psm1 — módulo común de herramientas operativas internas
function Remove-OldAppLog { <# la función del capítulo anterior #> }
function Get-AppLogSummary { <# función de agregación #> }
# Función auxiliar interna. No se expone al exterior
function ConvertTo-InternalPath { <# ... #> }
# Indica explícitamente qué funciones se publican. Si no se escribe, se publican todas las funciones
Export-ModuleMember -Function Remove-OldAppLog, Get-AppLogSummary
Si no se escribe Export-ModuleMember, se exportan todas las funciones y alias del módulo (las variables no). Es opcional, pero se considera una buena práctica dejar explícito el alcance de la publicación. 13 Ocultar los ayudantes internos deja margen para refactorizar libremente más adelante.
6.2. Ubicación — $env:PSModulePath y la carga automática
El módulo se coloca dentro de una de las carpetas listadas en $env:PSModulePath, creando una carpeta con el mismo nombre que el módulo (AppOpsTools\AppOpsTools.psm1). Si el nombre de la carpeta no coincide con el nombre base del archivo, no se reconoce como módulo. 65 Las ubicaciones predeterminadas son las siguientes, y el hecho de que la ruta sea distinta entre Windows PowerShell 5.1 y PowerShell 7 es un punto donde se suele tropezar en el terreno. 6
| Ámbito | PowerShell 7 | Windows PowerShell 5.1 |
|---|---|---|
| Personal (CurrentUser) | $HOME\Documents\PowerShell\Modules |
$HOME\Documents\WindowsPowerShell\Modules |
| Todos los usuarios (AllUsers) | $env:ProgramFiles\PowerShell\Modules |
$env:ProgramFiles\WindowsPowerShell\Modules |
Más seguro que memorizar la tabla es comprobarlo en el entorno real. Se puede ver con una sola línea.
# Comprueba las rutas de búsqueda de su entorno, una por línea (en Windows el separador es el punto y coma)
$env:PSModulePath -split ';'
# Forma de escribirlo tomando el separador del propio sistema. Úsela si con PowerShell 7 también trabaja en macOS/Linux
$env:PSModulePath -split [System.IO.Path]::PathSeparator
Aunque el nombre $env:PSModulePath sea el mismo, el contenido es distinto entre 5.1 y 7. La causa de que «no se encuentre el módulo» suele ser que la ubicación donde se colocó no está dentro de la ruta de búsqueda de ese entorno, así que antes de sospechar de otra cosa, ejecute primero esta línea.
Si se coloca en el lugar correcto, aunque no se escriba Import-Module, PowerShell lo importa automáticamente la primera vez que se ejecuta un comando del módulo (carga automática de módulos). 7 Es decir, el usuario puede usarlo «como si el comando ya estuviera instalado desde el principio». El ritmo habitual en el trabajo diario es comprobar el funcionamiento de un módulo en fase de prueba y error importándolo con Import-Module y la ruta completa, y una vez que se estabiliza, colocarlo dentro de PSModulePath.
Hay dos advertencias que dependen del entorno. La ubicación real de la carpeta Documents puede haberse movido por la redirección de carpetas de OneDrive, y en ese caso los módulos de ámbito de usuario también quedan dentro de OneDrive. 6 Además, colocar un módulo en el ámbito de todos los usuarios requiere privilegios de administrador. Si lo coloca en un servidor, conviene usar el ámbito AllUsers y asegurarse de que también sea visible desde la cuenta con la que se ejecuta el Programador de tareas; así se evita el problema de «funciona en mi equipo pero no funciona en el servidor».
6.3. El manifiesto psd1, en la «etapa de distribución»
El manifiesto del módulo (.psd1) es un archivo con una tabla hash que describe metadatos como la versión del módulo y sus dependencias, y no es obligatorio. La única clave obligatoria del manifiesto es ModuleVersion. 8 Mientras se use solo dentro del propio equipo, basta con el .psm1 en solitario; cuando llegue la etapa de distribuirlo a otros departamentos o de llevar un control de versiones estricto, se genera con New-ModuleManifest. 8
New-ModuleManifest -Path .\AppOpsTools\AppOpsTools.psd1 `
-RootModule 'AppOpsTools.psm1' `
-ModuleVersion '1.0.0' `
-FunctionsToExport 'Remove-OldAppLog', 'Get-AppLogSummary' `
-PowerShellVersion '5.1'
El psd1 generado queda como una plantilla con comentarios, así que se pueden ir desarrollando solo las claves que se necesiten. 8
6.4. Claves de la distribución interna y el control de versiones
- El original se guarda en Git. Como los scripts y los módulos son texto, se llevan bien con Git, y poder rastrear «cuándo, quién y por qué lo cambió» es, en sí mismo, la fiabilidad de un script operativo. Si se hace corresponder la actualización de ModuleVersion con cada commit, resulta más fácil identificar qué versión está instalada en el servidor.
- La forma básica de distribución es «copiar desde una carpeta compartida hasta PSModulePath en cada máquina». Copiando la carpeta completa del módulo se puede hacer una instalación manual. 7 No se recomienda usar de forma habitual una configuración en la que se añada directamente a PSModulePath una ruta situada en una carpeta compartida, teniendo en cuenta la premisa de que un recurso compartido puede ser «lento, cortarse o no estar disponible» (más detalles en «Las trampas de las unidades de red y las rutas UNC»).
- Preste atención a la relación con la directiva de ejecución. La directiva predeterminada RemoteSigned permite scripts sin firmar creados localmente, pero en sistemas que no distinguen entre una ruta UNC y una ruta de Internet, un script situado en una ruta UNC puede ver rechazada su ejecución. Además, los archivos marcados como procedentes de una descarga quedan bloqueados y requieren desbloquearlos con Unblock-File o firmarlos. 9 Si va a formalizar la distribución interna, consulte la combinación con la firma de código en «Directiva de ejecución y firma de scripts de PowerShell».
7. Reglas prácticas habituales (tabla de decisión)
| Punto | Opciones | Criterio de decisión |
|---|---|---|
| Recepción de argumentos | Variables escritas directamente / bloque param | Si se usa dos o más veces, o si lo usa otra persona, param es la única opción. Los valores por defecto deben inclinarse hacia el lado «seguro»2 |
| [CmdletBinding()] | No añadirlo / añadirlo | Añádalo siempre en lo que se entrega a otra persona o se pone en producción. Los errores de tecleo se detienen antes de ejecutarse1 |
| Comprobación de entrada | Instrucciones if en el cuerpo / atributos de validación | La comprobación de formato de un único parámetro va a los atributos. Solo la validación combinada de varios parámetros va en el cuerpo2 |
| Mecanismo de seguridad para cambios | Argumento propio -TestMode / SupportsShouldProcess | No cree indicadores propios. Súmese al estándar -WhatIf/-Confirm4 |
| Forma de mantener el procesamiento común | Copiar y pegar / dot sourcing / módulo .psm1 | Modularice en el momento en que se comparte con un segundo script. Las funciones publicadas se indican explícitamente con Export-ModuleMember513 |
| Ubicación del módulo | Carpeta cualquiera + Import-Module / dentro de PSModulePath | Lo que se usa habitualmente se coloca en la ubicación estándar para aprovechar la carga automática. En servidores, ámbito AllUsers67 |
| Manifiesto psd1 | Crearlo desde el principio / crearlo en la etapa de distribución | New-ModuleManifest cuando llega la etapa de sacarlo fuera del equipo o de llevar un control de versiones estricto8 |
8. Resumen
- Convertir la función en una función avanzada con el bloque param y [CmdletBinding()] es el punto de partida de un «script que se puede entregar». Se añaden los parámetros comunes y los errores en los argumentos se detienen antes de la ejecución.
- Con el tipado, [Parameter(Mandatory)], valores por defecto del lado seguro y atributos de validación, los errores salen pronto en la entrada. ValidateSet también mejora la usabilidad gracias al autocompletado con tabulador.
- La entrada por pipeline se recibe con el par ValueFromPipeline y bloque process. Si se olvida process, solo se procesa el último elemento.
- La ayuda basada en comentarios hace que funcione Get-Help, y las funciones que modifican algo se adaptan a -WhatIf con SupportsShouldProcess. Poder ensayar antes de ejecutar es el mecanismo de seguridad de la operación.
- Las funciones comunes se extraen a .psm1, se indica explícitamente el alcance de la publicación con Export-ModuleMember y se colocan dentro de PSModulePath. Tenga en cuenta que la ruta es distinta entre 5.1 y 7.
- El manifiesto psd1, en la etapa de distribución. El original se gestiona con Git, y en la distribución por carpeta compartida hay que verificar la directiva de ejecución (la relación entre las rutas UNC y RemoteSigned).
Artículos relacionados
- Fundamentos de los comandos de PowerShell — las operaciones que hay que aprender primero y su uso seguro
- Aplicación práctica de scripts de PowerShell — automatizar de forma segura la investigación, el archivado y la generación de informes de registros
- Mantenimiento de pruebas de PowerShell con Pester — el patrón práctico para que los scripts operativos sean más difíciles de romper
- Directiva de ejecución y firma de scripts de PowerShell
- Manejo de errores y diseño de reintentos en PowerShell
- Manejo seguro de credenciales en PowerShell — desterrar las contraseñas en texto plano de los scripts
Áreas de consultoría relacionadas
En KomuraSoft LLC nos ocupamos de organizar y modularizar scripts de PowerShell que dependen de una sola persona, de revisar el diseño de scripts de automatización operativa y de construir mecanismos de distribución interna y control de versiones. Puede consultarnos incluso desde la etapa de hacer inventario de activos de scripts que «funcionan, pero que nadie puede tocar».
- Consultoría técnica y revisión de diseño
- Desarrollo de aplicaciones Windows
- Migración y aprovechamiento de activos heredados
- Contacto
Referencias
-
Microsoft Learn, about_Functions_CmdletBindingAttribute. Sobre que el atributo CmdletBinding hace que una función se comporte como un cmdlet compilado, que se añaden automáticamente los parámetros comunes, que se puede usar $PSCmdlet, que el enlace falla con parámetros desconocidos o argumentos posicionales adicionales sin correspondencia, y que SupportsShouldProcess añade los parámetros Confirm y WhatIf. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, about_Functions_Advanced_Parameters. Sobre las especificaciones del atributo Parameter y de Mandatory, ValueFromPipeline, el parámetro switch, los atributos de validación como ValidateSet, ValidateRange, ValidateScript, ValidateNotNullOrEmpty y ValidatePattern; sobre que la función no se invoca cuando falla la validación; sobre que declarar los atributos antes del tipo es una buena práctica; y sobre que el argumento ErrorMessage de ValidateScript está disponible desde PowerShell 6 en adelante. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15
-
Microsoft Learn, about_Comment_Based_Help. Sobre que, al escribir ayuda basada en comentarios con palabras clave como .SYNOPSIS, .DESCRIPTION, .PARAMETER y .EXAMPLE, Get-Help la muestra en el mismo formato que la ayuda XML, y sobre las reglas de colocación en scripts y en funciones respectivamente. ↩ ↩2 ↩3
-
Microsoft Learn, Everything you wanted to know about ShouldProcess. Sobre que basta con indicar SupportsShouldProcess para que se creen automáticamente -WhatIf y -Confirm, la forma de ramificar el código con $PSCmdlet.ShouldProcess(), y que se recomienda no confiar ciegamente en la propagación de -WhatIf sino pasarlo explícitamente a los comandos internos. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, How to Write a PowerShell Script Module. Sobre que basta con guardar con la extensión .psm1 para obtener un módulo de script, que hay que guardarlo en una carpeta con el mismo nombre que el script, que de forma predeterminada se publican todas las funciones y las variables no se publican, y que se recomienda indicar explícitamente con Export-ModuleMember las funciones que se publican. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, about_PSModulePath. Sobre que $env:PSModulePath es la lista de carpetas de búsqueda de módulos, que las rutas predeterminadas de los ámbitos CurrentUser y AllUsers son distintas entre PowerShell 7 y Windows PowerShell 5.1, y que la ubicación de Documents puede cambiar por OneDrive o por la redirección de carpetas. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, about_Modules. Sobre que los módulos dentro de PSModulePath se importan automáticamente al ejecutar por primera vez un comando (carga automática de módulos), el método de instalación manual copiando la carpeta completa del módulo, y las ubicaciones predeterminadas de colocación de módulos. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, New-ModuleManifest. Sobre que el manifiesto del módulo (.psd1) es una tabla hash que describe el contenido, los atributos y los requisitos previos del módulo, y que no es obligatorio; que la única clave obligatoria es ModuleVersion; y que New-ModuleManifest genera una plantilla que se puede usar como base. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, about_Execution_Policies. Sobre que RemoteSigned permite scripts sin firmar creados localmente y exige firma a los scripts procedentes de Internet, que en sistemas que no distinguen entre rutas UNC y rutas de Internet un script en una ruta UNC puede no obtener permiso de ejecución bajo RemoteSigned, y sobre el desbloqueo mediante Unblock-File. ↩ ↩2
-
Microsoft Learn, about_Functions_Advanced_Methods. Sobre los métodos de procesamiento de entrada begin/process/end disponibles en las funciones avanzadas, y sobre que el método ShouldProcess se invoca desde dentro del bloque process y requiere haberlo declarado con el atributo CmdletBinding. ↩ ↩2
-
Microsoft Learn, Approved Verbs for PowerShell Commands. Sobre que el nombre de un comando debe seguir el formato Verbo-Sustantivo, la lista de verbos aprobados y cómo consultarla con Get-Verb, y que se muestra una advertencia al importar un módulo que contiene un verbo no aprobado. ↩
-
Microsoft Learn, about_Scripts. Sobre que, de forma predeterminada, un script se ejecuta en su propio ámbito, y que las funciones, variables, alias y unidades creadas dentro solo existen en el ámbito del script; y sobre que, al usar el «dot sourcing», que consiste en ejecutar con un punto y un espacio antes de la ruta, el script se ejecuta en el ámbito actual y los elementos creados permanecen en la sesión después de la ejecución. ↩
-
Microsoft Learn, Export-ModuleMember. Sobre que Export-ModuleMember es el cmdlet que especifica los miembros que se exportan de un módulo de script, que si no se indica se exportan las funciones y los alias pero no las variables, y que aunque es opcional, se considera una buena práctica para mostrar la intención del autor. ↩ ↩2
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Diferencias entre Windows PowerShell 5.1 y PowerShell 7 ── Guía práctica de migración de scripts internos
Explicamos la relación entre Windows PowerShell 5.1 y PowerShell 7 (coexistencia y pwsh.exe), la política oficial de no añadir funciones ...
Automatizar el kitting de PC con winget + PowerShell ── Convertir el manual de procedimientos en algo ejecutable
Resumimos cómo hacer reproducible la configuración de PC para nuevos empleados: instalación de aplicaciones con winget, export/import, co...
Dónde mirar cuando un script de PowerShell es lento — claves de arrays, pipeline y cruces de datos
Analizamos las causas típicas de la lentitud en PowerShell: += en arrays, pipeline vs. foreach, cruces con tablas hash, E/S de archivos y...
Deje de usar Write-Host — Flujos de salida de PowerShell y diseño de registros
Explica los seis flujos de salida de PowerShell, los problemas de Write-Host y su uso correcto, por qué se contamina el valor de retorno ...
Procesamiento paralelo en PowerShell — Cuándo usar ForEach-Object -Parallel y cuándo usar jobs
Diferencias entre ForEach-Object -Parallel, Start-ThreadJob y Start-Job, uso de $using:, ThrottleLimit y cuándo el paralelismo resulta má...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Aplicaciones empresariales, integración de dispositivos y herramientas de comunicación, de los requisitos al desarrollo.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Qué cambia si se añade [CmdletBinding()] al bloque param de PowerShell?
- La función o el script pasa a tratarse como una «función avanzada» (advanced function) y adquiere el mismo comportamiento que un cmdlet compilado. En concreto, se añaden automáticamente parámetros comunes como -Verbose o -ErrorAction, se puede usar la variable $PSCmdlet, y pasar un parámetro no definido o un argumento posicional adicional produce un error de enlace. Como un argumento con un error de tecleo deja de ignorarse en silencio, en los scripts operativos lo habitual es añadirlo siempre.
- ¿La comprobación de argumentos de un script debe escribirse con atributos Validate o con instrucciones if?
- Lo habitual es delegar la comprobación de formato de los parámetros en atributos de validación como ValidateSet, ValidateRange o ValidateScript. La validación se realiza antes de que se ejecute el cuerpo de la función, así que si el valor es incorrecto, se produce un error sin que se ejecute ni una sola línea de procesamiento, lo que evita el problema de «ejecutarse a medias y romperse». ValidateSet tiene además la ventaja práctica de habilitar el autocompletado con tabulador. Por otro lado, la validación de combinaciones entre varios parámetros o las comprobaciones que dependen del estado en tiempo de ejecución se hacen con instrucciones if en el propio cuerpo de la función.
- ¿Dónde se debe colocar un módulo propio (.psm1) de PowerShell?
- Se coloca dentro de una de las carpetas incluidas en $env:PSModulePath, creando una «carpeta con el mismo nombre que el módulo». Para uso personal, en PowerShell 7 la ubicación predeterminada es $HOME\Documents\PowerShell\Modules; para todos los usuarios, $env:ProgramFiles\PowerShell\Modules. Tenga en cuenta que en Windows PowerShell 5.1 la ruta es distinta en ambos casos, con WindowsPowerShell\Modules. Si se coloca en esta ubicación, se carga automáticamente la primera vez que se ejecuta el comando, sin necesidad de escribir Import-Module.
- ¿Es imprescindible crear un manifiesto de módulo (psd1)?
- No es obligatorio. Un .psm1 en solitario, sin manifiesto, también funciona como módulo. El manifiesto se vuelve necesario en la «etapa de distribución», cuando se quiere incorporar metadatos como el número de versión, la versión de PowerShell necesaria, los módulos de los que depende o la indicación explícita de los comandos que se exportan; en ese momento se genera con New-ModuleManifest. Como la única clave obligatoria del manifiesto es ModuleVersion, basta con empezar con la configuración mínima e irla desarrollando según haga falta.
- ¿Por qué un script colocado en una carpeta compartida interna queda bloqueado por la directiva de ejecución?
- La directiva predeterminada RemoteSigned permite ejecutar sin firma los scripts creados localmente, pero exige firma a los scripts marcados como procedentes de Internet. La documentación oficial deja explícito que, en sistemas que no distinguen entre una ruta UNC y una ruta de Internet, la ejecución de un script situado en una carpeta compartida puede ser rechazada bajo RemoteSigned. Si va a formalizar la distribución interna, considere combinar la firma de código con AllSigned, o configurar la zona de intranet.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.