Las funciones en PowerShell son el paso que separa un script suelto de una herramienta que se usa cada semana. Un bloque de órdenes que alguien copia y pega con otra ruta o con otro equipo acaba en diez versiones distintas; una función avanzada, con nombre, parámetros validados y ayuda, se escribe una vez, se prueba y se llama siempre igual, desde la consola, desde una tarea programada o desde otra función.
Esta guía de consulta explica las funciones en PowerShell desde la sintaxis básica hasta las funciones avanzadas en PowerShell: el bloque param, los tipos, los parámetros obligatorios y opcionales, las validaciones, los conjuntos de parámetros, la canalización con begin, process y end, el valor de retorno, -WhatIf, la ayuda basada en comentarios y el splatting. Cada comportamiento que se describe se midió el 13 de noviembre de 2026 en un Windows 11 en español con Windows PowerShell 5.1 y con PowerShell 7.7.0, la versión estable publicada el 10 de noviembre (la versión de soporte largo sigue siendo la 7.6). Donde las dos versiones se comportan distinto, se dice.
Si es la primera vez que abre la consola, empiece por la guía de PowerShell y vuelva aquí cuando ya ejecute órdenes con soltura.
Qué son las funciones en PowerShell y cuándo conviene escribir una
Una función es un bloque de código con nombre que PowerShell guarda en memoria y que se ejecuta escribiendo ese nombre, igual que un cmdlet. Las funciones en PowerShell pueden recibir datos por parámetros o por la canalización, y lo que producen sale por la canalización como cualquier otro comando.
Conviene escribir una función cuando la misma tarea se repite con datos distintos: revisar el espacio libre de varios equipos, limpiar archivos antiguos de una carpeta, crear cuentas con una convención de nombres o consultar el estado de un servicio. En una mesa de ayuda esas tareas aparecen a diario, y una función bien escrita evita que cada técnico las resuelva a su manera.
Hay dos clases de funciones y, sin duda, la diferencia importa más de lo que parece:
| Función simple | Función avanzada |
|---|---|
Solo function Nombre { } con param() opcional | Lleva [CmdletBinding()] o algún atributo [Parameter()] |
Un parámetro mal escrito va a $args sin ningún error | Un parámetro mal escrito detiene la llamada con un error |
| No tiene parámetros comunes | Recibe -Verbose, -ErrorAction y los demás parámetros comunes |
No admite -WhatIf ni -Confirm | Los admite si se declara SupportsShouldProcess |
| Útil para una ayuda rápida en la consola | La forma correcta para scripts que otros van a usar |
La documentación de Microsoft sobre funciones avanzadas las define como cmdlets escritos en PowerShell en lugar de compilados en C#: se comportan como Get-Service o Copy-Item, con los mismos parámetros comunes y la misma forma de recibir datos de la canalización.
Sintaxis básica de una función en PowerShell
La forma mínima de una función en PowerShell es la palabra function, un nombre y un bloque entre llaves. Lo que el bloque deja sin capturar sale como resultado:
function Get-EspacioLibre {
param($Unidad = 'C')
$disco = Get-PSDrive -Name $Unidad
[math]::Round($disco.Free / 1GB, 1)
}
Get-EspacioLibre
Get-EspacioLibre -Unidad C
El nombre sigue la forma verbo-sustantivo de los cmdlets (Get-EspacioLibre, Remove-ArchivoTemporal) y el bloque param declara los parámetros con un valor por defecto opcional. Una vez definida, la función existe en la sesión hasta que esta se cierra.
Cómo llamar a una función: sin paréntesis ni comas
Quien viene de otros lenguajes escribe Sumar(2, 3); sin embargo, en PowerShell eso no hace lo que parece. Los argumentos se separan con espacios; la coma construye un arreglo y los paréntesis lo agrupan, así que todo llega al primer parámetro:
function Sumar($a, $b) { $a + $b }
Sumar 2 3 # 5
Sumar -a 2 -b 3 # 5
Sumar(2, 3) # 2 y 3 en dos líneas: el arreglo entero fue a $a y $b quedó vacío
Medido en las dos versiones: con Sumar(2, 3), $a recibe el arreglo 2 3 y $b queda vacío, sin ningún aviso. La costumbre sana es llamar siempre con el nombre del parámetro (-a 2 -b 3), que además documenta la llamada.
Dónde vive una función
Una función definida en la consola desaparece al cerrarla. Por lo tanto, para usarla siempre hay cuatro sitios habituales:
| Dónde | Cómo se carga | Cuándo usarlo |
|---|---|---|
| La sesión actual | Se escribe o se pega en la consola | Pruebas rápidas |
Un script .ps1 con varias funciones | Con un punto delante: . .\Funciones.ps1 | Un grupo de funciones que se carga a mano |
El perfil ($PROFILE) | Se carga solo al abrir la consola | Atajos personales del técnico |
Un módulo (.psm1) | Import-Module o carga automática | Funciones compartidas por el equipo |
Por eso el punto delante importa. Medido: ejecutar .\Funciones.ps1 sin punto define las funciones dentro del script y las pierde al terminar; con . .\Funciones.ps1 (*dot sourcing*) quedan en la sesión.
# Cargar en la sesión las funciones guardadas en un archivo
. C:\Scripts\Funciones-MesaDeAyuda.ps1
Get-Command -CommandType Function -Name Get-Espacio*
Dentro de un script, la función tiene que estar definida antes de la línea que la llama. Llamarla antes da el error de comando no reconocido («El término no se reconoce como nombre de un cmdlet, función…»), en 5.1 y en 7. Para compartir funciones entre varios técnicos, el sitio correcto es un módulo: la guía de módulos de PowerShell explica cómo instalarlos y actualizarlos.
Funciones avanzadas en PowerShell: lo que cambia con CmdletBinding
Basta con añadir [CmdletBinding()] encima de param para que una función simple pase a ser avanzada. El cambio es pequeño en el código y grande en el comportamiento:
function Get-EspacioLibre {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidatePattern('^[A-Z]$')]
[string]$Unidad
)
Write-Verbose "Consultando la unidad $Unidad"
$disco = Get-PSDrive -Name $Unidad -ErrorAction Stop
[math]::Round($disco.Free / 1GB, 1)
}
Get-EspacioLibre -Unidad C -Verbose
Lo que se midió al comparar la misma función con y sin el atributo:
| Prueba | Función simple | Función con [CmdletBinding()] |
|---|---|---|
Parámetro mal escrito (-Nmbre Ana) | Sin error: -Nmbre y Ana acaban en $args y el parámetro real queda vacío | Error: no se encuentra ningún parámetro que coincida con el nombre Nmbre |
| Un argumento de más | Sin error: el sobrante va a $args | Error: no se encuentra ningún parámetro de posición que acepte el argumento |
| Parámetros comunes | Ninguno | 11 en 5.1 y 12 en 7.7 (la 7.4 añadió -ProgressAction) |
Write-Verbose | No hay forma de activarlo desde la llamada | Sale con -Verbose, y también en las funciones avanzadas que llama |
-WhatIf sin SupportsShouldProcess | No existe | Tampoco: no se encuentra ningún parámetro que coincida con WhatIf |
El primer caso es el más peligroso. Por ejemplo, en una función simple que borra archivos, un -Rta mal escrito deja $Ruta vacío y el script sigue adelante con el valor por defecto, sin avisar. [CmdletBinding()] convierte ese descuido en un error que detiene la llamada.
Los parámetros comunes que añade el atributo son Verbose, Debug, ErrorAction, WarningAction, InformationAction, ErrorVariable, WarningVariable, InformationVariable, OutVariable, OutBuffer y PipelineVariable, más ProgressAction en PowerShell 7.4 y posteriores. Con -ErrorAction Stop la función entra en un bloque catch, como se explica en la guía de try catch en PowerShell.
Parámetros en PowerShell: el bloque param
Los parámetros en PowerShell se declaran dentro de param(), separados por comas. Cada uno puede llevar un tipo, atributos y un valor por defecto. Un ejemplo con las piezas más habituales:
function Get-ArchivoAntiguo {
[CmdletBinding()]
param(
[Parameter(Mandatory, HelpMessage = 'Carpeta que se revisa, por ejemplo C:\Temp')]
[Alias('Path')]
[string]$Ruta,
[ValidateRange(1, 3650)]
[int]$Dias = 30,
[datetime]$Desde = (Get-Date).AddDays(-$Dias),
[switch]$Recursivo
)
Get-ChildItem -Path $Ruta -File -Recurse:$Recursivo -ErrorAction SilentlyContinue |
Where-Object LastWriteTime -lt $Desde
}
Get-ArchivoAntiguo -Ruta $env:TEMP -Dias 90 -Recursivo | Measure-Object
Tipos de parámetro y conversión
Primero, el tipo entre corchetes ([string], [int], [datetime], [string[]]) no es decorativo: PowerShell convierte el valor recibido al tipo declarado y, si no puede, rechaza la llamada. La conversión tiene reglas que sorprenden:
| Parámetro | Valor recibido | Resultado medido |
|---|---|---|
[int] | '10' (texto) | 10 |
[int] | 2.5 | 2: redondeo al par más cercano |
[int] | 3.5 | 4 |
[int] | 'diez' | Error de transformación del argumento |
[datetime] | '03/11/2026' | 11 de marzo, no 3 de noviembre |
[datetime] | '25/11/2026' | Error: no se reconoce como fecha válida |
[datetime] | '2026-11-03' | 3 de noviembre, en cualquier equipo |
[bool] | 'false' (texto) | Error: solo acepta $true, $false, 1 o 0 |
[string] | 123 | '123' |
[string] | 'a', 'b' (arreglo) | Error: no se puede convertir el valor al tipo String |
[string] | $null | Cadena vacía, no $null |
La fecha, en cambio, merece atención especial. Microsoft documenta que las funciones avanzadas convierten los parámetros con la cultura invariable, la de Estados Unidos, mientras que los cmdlets compilados usan la del equipo. Medido en un equipo configurado en español: la función recibe '03/11/2026' como 11 de marzo y Get-Date -Date '03/11/2026' lo lee como 3 de noviembre. Para que una función no dependa del idioma de quien la llama, pida fechas en formato año-mes-día (2026-11-03) o reciba un objeto [datetime] ya construido.
Valores por defecto
El valor por defecto se escribe con = después del nombre y se calcula en cada llamada, no al definir la función. Por eso [datetime]$Desde = (Get-Date).AddDays(-$Dias) funciona: toma la fecha del momento y el valor de $Dias, que se declaró antes. Un valor por defecto puede usar los parámetros declarados más arriba, nunca los de más abajo.
La variable automática $PSBoundParameters contiene solo los parámetros que el usuario escribió en la llamada, no los que tomaron su valor por defecto. Medido: con param($A = 'por defecto', $B) y la llamada -B 1, $PSBoundParameters tiene únicamente la clave B. Es la forma de saber si alguien pasó un valor o si se está usando el de fábrica.
Parámetros obligatorios
[Parameter(Mandatory)] obliga a dar un valor. Si falta y la consola es interactiva, PowerShell lo pide en pantalla; si la sesión no es interactiva, como en una tarea programada lanzada con -NonInteractive, la llamada falla con el mensaje «No se puede procesar el comando porque faltan uno o varios parámetros obligatorios».
Además, un parámetro obligatorio de tipo [string] tampoco acepta una cadena vacía, ni $null, que el tipo convierte en cadena vacía: «No se puede enlazar el argumento con el parámetro porque es una cadena vacía». Si la cadena vacía es un valor válido, se añade [AllowEmptyString()]. Y HelpMessage añade un texto de ayuda para quien rellena el valor a mano.
Parámetros switch
Un [switch] es un interruptor: vale $true si aparece en la llamada y $false si no. No se le asigna valor (-Recursivo, no -Recursivo $true), pero se puede forzar con dos puntos: -Recursivo:$false. Esa forma es la que permite pasar el interruptor de una función a un cmdlet, como hace -Recurse:$Recursivo en el ejemplo. Por eso, un switch nunca debe llevar valor por defecto $true: obligaría a escribir -Recursivo:$false para desactivarlo.
Alias y nombres abreviados
[Alias('Path')] permite llamar al parámetro con otro nombre, útil para conservar compatibilidad o para aceptar el nombre que traen los objetos de otros comandos (ComputerName, Name). Además, PowerShell admite cualquier prefijo que no sea ambiguo: -Rut basta para -Ruta. Medido: con dos parámetros Equipo y Etiqueta, -E da el error «el nombre de parámetro E es ambiguo» y enumera las coincidencias, que incluyen -ErrorAction y -ErrorVariable. En la consola el prefijo ahorra teclas; en cambio, en un script, el nombre completo evita que una función deje de funcionar el día que alguien añade un parámetro que empieza igual.
Validar parámetros en PowerShell
Los atributos de validación comprueban el valor antes de que se ejecute la primera línea de la función y, además, rechazan la llamada con un mensaje que dice qué falló. Así el código de la función no se llena de comprobaciones. La lista completa está en la referencia de parámetros de funciones avanzadas de Microsoft:
function New-SolicitudSoporte {
[CmdletBinding()]
param(
[ValidateSet('Bogota', 'Medellin', 'Cali')]
[string]$Sede = 'Bogota',
[ValidatePattern('^PC-\d{3}$')]
[string]$Equipo = 'PC-001',
[ValidateRange(1, 4)]
[int]$Prioridad = 3,
[ValidateScript({ Test-Path $_ })]
[string]$Adjunto = $env:TEMP,
[ValidateNotNullOrEmpty()]
[string[]]$Servicio = @('Spooler')
)
"Sede $Sede, equipo $Equipo, prioridad $Prioridad, servicios: $($Servicio -join ', ')"
}
New-SolicitudSoporte -Sede Cali -Equipo PC-042 -Prioridad 2
| Atributo | Qué comprueba | Mensaje medido al fallar (resumido) |
|---|---|---|
[ValidateSet()] | Que el valor esté en una lista | El argumento no pertenece al conjunto especificado por el atributo ValidateSet |
[ValidateRange()] | Que un número esté entre dos límites | El argumento 45 es mayor que el intervalo máximo permitido de 30 |
[ValidatePattern()] | Que el texto cumpla una expresión regular | El argumento no coincide con el patrón |
[ValidateScript()] | Que un bloque de código devuelva $true | El script de validación no devolvió el resultado True |
[ValidateLength()] | La longitud mínima y máxima de un texto | La longitud de caracteres (2) del argumento es insuficiente |
[ValidateCount()] | Cuántos elementos tiene un arreglo | El número de argumentos proporcionados (3) supera el número máximo de argumentos permitidos (2) |
[ValidateNotNullOrEmpty()] | Que no sea nulo, vacío ni un arreglo vacío | El argumento es NULL, está vacío o un elemento de la colección contiene un valor NULL |
[ValidateNotNullOrWhiteSpace()] | Lo mismo, y tampoco solo espacios | Solo existe en PowerShell 7.4 y posteriores |
Detalles medidos de las validaciones
Los mensajes de la tabla son los de Windows PowerShell 5.1. Los de PowerShell 7 dicen lo mismo con otras palabras, y alguno sale mal traducido: el de ValidateCount es, literalmente, «El parámetro requiere al menos 1 valores y no más de 2 valores: 3 se proporcionaron valores».
Tres detalles medidos que cambian la forma de usarlas:
- ValidateSet no distingue mayúsculas y conserva lo que escribió el usuario: con el conjunto
Bogota, Medellin, Cali, la llamada-Sede bogotase acepta y$Sedevalebogota, en minúsculas. Si el resto del script compara con-ceqo usa el valor como nombre de carpeta en Linux, conviene normalizarlo. ConIgnoreCase = $falseel conjunto pasa a distinguir mayúsculas. A cambio, ValidateSet completa los valores con la tecla Tab, que ahorra errores de escritura. - El mensaje de ValidateScript repite el código del bloque, poco útil para quien no lo escribió. PowerShell 7 admite un mensaje propio con
ErrorMessage; en Windows PowerShell 5.1 esa propiedad no existe y la función no llega a definirse («No se encuentra la propiedad ErrorMessage»). - Las validaciones nuevas no existen en 5.1.
[ValidateRange('Positive')]da en 5.1 un error de constructor y[ValidateNotNullOrWhiteSpace()]uno de tipo no encontrado. Si el script debe funcionar en los dos, use las clásicas.
El mensaje propio de PowerShell 7 se escribe así:
# Solo PowerShell 7: mensaje propio para la validación
param(
[ValidateScript({ Test-Path $_ }, ErrorMessage = 'La carpeta {0} no existe.')]
[string]$Ruta = $env:TEMP
)
"Carpeta revisada: $Ruta"
Conjuntos de parámetros
Un conjunto de parámetros agrupa los que se usan juntos y separa los que se excluyen. Get-Service es el ejemplo de siempre: se busca por nombre o por nombre visible, nunca por los dos a la vez. En una función se declara con ParameterSetName:
function Get-Equipo {
[CmdletBinding(DefaultParameterSetName = 'PorNombre')]
param(
[Parameter(Mandatory, ParameterSetName = 'PorNombre')]
[string]$Nombre,
[Parameter(Mandatory, ParameterSetName = 'PorSede')]
[string]$Sede
)
"Búsqueda por $($PSCmdlet.ParameterSetName)"
}
Get-Equipo -Sede Cali
Get-Command Get-Equipo -Syntax
Dentro de la función, $PSCmdlet.ParameterSetName dice qué conjunto se usó. Asimismo, Get-Command -Syntax muestra una línea por conjunto, que es la mejor comprobación de que quedaron bien definidos. Lo medido:
- Pasar parámetros de dos conjuntos a la vez falla con «No se puede resolver el conjunto de parámetros usando los parámetros con nombre especificados».
- Una función con conjuntos y sin
DefaultParameterSetName, llamada sin parámetros, da ese mismo error. Declare siempre el conjunto por defecto. - En cambio, un parámetro sin
ParameterSetNamepertenece a todos los conjuntos.
Funciones en PowerShell con canalización: begin, process y end
Una función avanzada puede recibir objetos por la canalización, igual que Get-Service | Stop-Service. Se declara con ValueFromPipeline (recibe el objeto entero) o con ValueFromPipelineByPropertyName (toma el valor de una propiedad con el mismo nombre o alias que el parámetro), y el trabajo con cada objeto va en el bloque process:
function Test-Equipo {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('ComputerName', 'CN')]
[string[]]$Equipo
)
begin { $total = 0 }
process {
foreach ($e in $Equipo) {
$total++
[pscustomobject]@{
Equipo = $e
Responde = Test-Connection -ComputerName $e -Count 1 -Quiet
}
}
}
end { Write-Verbose "Equipos revisados: $total" }
}
'localhost', '127.0.0.1' | Test-Equipo
Test-Equipo -Equipo localhost -Verbose
En concreto, begin se ejecuta una vez antes del primer objeto, process una vez por objeto y end una vez al final. Lo medido en las dos versiones:
| Situación | Resultado |
|---|---|
ValueFromPipeline sin bloque process y tres equipos por la canalización | Solo se procesa el último (PC-003): todo el cuerpo se trata como end |
El mismo caso con process | Se procesan los tres |
Parámetro [string] y un arreglo pasado con -Equipo PC-001, PC-002 | Error: no se puede convertir el valor al tipo String |
Parámetro [string[]] y foreach dentro de process | Funciona igual por parámetro y por canalización |
Parámetro [string[]] sin foreach | Por parámetro, un solo objeto con el texto PC-001 PC-002 |
Objeto con la propiedad ComputerName y un alias ComputerName en el parámetro | El valor llega solo, sin escribir nada |
return dentro de process | Salta al siguiente objeto, no termina la función: end se ejecuta igual |
Por eso, la plantilla de arriba combina [string[]] y foreach: la función acepta un equipo, una lista o la salida de otro comando. El alias ComputerName permite encadenarla con comandos que devuelven objetos con esa propiedad, como los de Active Directory con PowerShell (que traen Name y DNSHostName).
El bloque clean y el coste de la canalización
PowerShell 7.3 añadió un cuarto bloque, clean, que se ejecuta siempre al final, también si la canalización se corta con un error, con Select-Object -First o con Ctrl+C, y sirve para cerrar conexiones o borrar archivos temporales. Lo que escribe no sale como resultado: medido, una función con end y clean solo devolvió lo de end. En Windows PowerShell 5.1 no existe: la función ni siquiera se puede definir.
Rendimiento. La canalización tiene un coste por objeto. Medido con 100.000 números sumados por la misma función: 2.230 ms por la canalización frente a 122 ms pasados como arreglo en 5.1, y 481 ms frente a 35 ms en 7.7, con el mismo resultado (5.000.050.000). Para decenas de equipos la diferencia no se nota; para cientos de miles de registros, conviene pasar la colección por parámetro.
Valor de retorno en PowerShell: lo que sale de una función
Aquí está la confusión más frecuente con las funciones en PowerShell. Una función no devuelve solo lo que va detrás de return: devuelve todo lo que no se captura. return solo termina la función; lo que salió antes también forma parte del resultado. Medido: una función que escribe un texto de depuración y después return 42 devuelve un arreglo de dos elementos, el texto y el 42.
El caso clásico es ArrayList.Add(), que devuelve el índice del elemento añadido:
function Get-Lista {
$lista = New-Object System.Collections.ArrayList
$lista.Add('PC-001') # Add devuelve el índice: 0
$lista.Add('PC-002') # y aquí, 1
return $lista
}
(Get-Lista).Count # 4, no 2
function Get-ListaBien {
$lista = [System.Collections.Generic.List[string]]::new()
$lista.Add('PC-001') # List[string].Add no devuelve nada
$lista.Add('PC-002')
$lista
}
(Get-ListaBien).Count # 2
Medido en 5.1 y 7.7: Get-Lista devuelve cuatro elementos, 0, 1, PC-001, PC-002. Por lo tanto, para descartar una salida que no se quiere se usa $null = $lista.Add('PC-001') o [void]$lista.Add('PC-001'). Las demás reglas del valor de retorno en PowerShell que conviene tener presentes:
| Situación | Resultado medido |
|---|---|
| La función devuelve un arreglo de un elemento | Llega un escalar (un String), no un arreglo |
| La función devuelve un arreglo vacío | Llega $null |
.Count sobre ese escalar con Set-StrictMode -Version Latest | Error: no se encuentra la propiedad Count |
La función devuelve , @('PC-001') (coma delante) | Llega un arreglo de un elemento |
Write-Host dentro de la función | Se ve en pantalla pero no forma parte del resultado |
[OutputType([int])] y la función devuelve texto | Llega texto: el atributo documenta, no convierte |
function Get-Uno { @('PC-001') }
(Get-Uno).GetType().Name # String: el arreglo se desenrolla
function Get-UnoArreglo { , @('PC-001') }
(Get-UnoArreglo).GetType().Name # Object[]
$equipos = @(Get-Uno) # @() en quien llama: siempre un arreglo
$equipos.Count # 1
En resumen, la regla práctica es esta: dentro de la función, devuelva objetos y deje que la canalización los desenrolle; en quien llama, envuelva el resultado en @() cuando necesite contar o recorrer. Para devolver varios datos a la vez, construya un [pscustomobject] con propiedades en lugar de devolver varias cosas sueltas. Y nunca use Write-Host para devolver datos: lo que se ve en pantalla no se puede guardar en una variable ni exportar a un CSV.
WhatIf y Confirm en funciones avanzadas en PowerShell
Por seguridad, una función que borra, detiene o cambia algo debe admitir -WhatIf (mostrar lo que haría sin hacerlo) y -Confirm (pedir confirmación). Se activa con SupportsShouldProcess y se usa con $PSCmdlet.ShouldProcess() antes de cada cambio:
function Remove-ArchivoTemporal {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[string]$Ruta,
[ValidateRange(1, 365)]
[int]$Dias = 7
)
$limite = (Get-Date).AddDays(-$Dias)
Get-ChildItem -Path $Ruta -File | Where-Object LastWriteTime -lt $limite | ForEach-Object {
if ($PSCmdlet.ShouldProcess($_.FullName, 'Eliminar archivo')) {
Remove-Item -LiteralPath $_.FullName
}
}
}
Remove-ArchivoTemporal -Ruta $env:TEMP -Dias 30 -WhatIf
Lo que se midió con ShouldProcess
Con -WhatIf, cada archivo muestra una línea («Se está realizando la operación Eliminar archivo en el destino…» en 5.1 y «Realizando la operación… en el objetivo…» en 7) y no se borra nada. Lo medido alrededor de ShouldProcess, que es donde están las sorpresas:
-WhatIfllega a los cmdlets que llama la función: unRemove-ItemsinShouldProcesspropio dentro de una función con-WhatIftampoco borró el archivo.- 🔴
-WhatIfno llega a una función de otro módulo de script. Medido: la misma función llamando a una función de un módulo, con-WhatIf, borró el archivo de verdad, en 5.1 y en 7.7, y lo borró también cuando la función del módulo declarabaSupportsShouldProcess. La guía de Microsoft sobre ShouldProcess lo reconoce: los módulos de script no heredan$WhatIfPreferenceni$ConfirmPreference, y recomienda no confiar nunca en que funcione. La solución medida es pasarlo de forma explícita en la llamada: conRemove-DesdeModulo -Ruta $Ruta -WhatIf:$WhatIfPreference, la vista previa funciona y, sin-WhatIf, el archivo se borra con normalidad. - 🔴
ConfirmImpact = 'High'rompe la función en una sesión no interactiva. Con ese valor, PowerShell pide confirmación siempre (la preferencia$ConfirmPreferencevaleHighde fábrica). En una tarea programada o en una consola lanzada con-NonInteractive, la llamada falla con «Windows PowerShell se encuentra en modo no interactivo. Las funciones de lectura y confirmación no están disponibles». Esas llamadas necesitan-Confirm:$false. La guía de tareas programadas con PowerShell explica cómo lanzar scripts desatendidos. - En cambio, con el
ConfirmImpactpor defecto (Medium), la función no pide confirmación salvo que se escriba-Confirm.
Ayuda basada en comentarios
Get-Help muestra la ayuda de una función igual que la de un cmdlet si se escribe un bloque de comentarios con palabras clave. Las más usadas son .SYNOPSIS (una línea), .DESCRIPTION, .PARAMETER (una por parámetro), .EXAMPLE (uno o varios) y .NOTES. El bloque va dentro de la función, al principio, o justo antes de la palabra function:
function Get-InventarioEquipo {
<#
.SYNOPSIS
Devuelve el fabricante, el modelo y la memoria del equipo local.
.PARAMETER Detallado
Añade el número de serie de la BIOS.
.EXAMPLE
Get-InventarioEquipo -Detallado
#>
[CmdletBinding()]
param([switch]$Detallado)
$cs = Get-CimInstance -ClassName Win32_ComputerSystem
$datos = [ordered]@{
Equipo = $cs.Name
Fabricante = $cs.Manufacturer
Modelo = $cs.Model
MemoriaGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1)
}
if ($Detallado) { $datos.Serie = (Get-CimInstance -ClassName Win32_BIOS).SerialNumber }
[pscustomobject]$datos
}
Get-Help Get-InventarioEquipo -Examples
Si el bloque va antes de function, la documentación de la ayuda basada en comentarios admite como máximo una línea en blanco entre los dos. Medido: con cero o una línea en blanco, Get-Help muestra la sinopsis; con dos, la ayuda desaparece sin aviso y Get-Help solo muestra el nombre de la función. Ponerla dentro del cuerpo evita el problema.
Nombres de funciones: verbos aprobados
PowerShell recomienda nombres verbo-sustantivo con un verbo de la lista aprobada, que Get-Verb muestra: 98 verbos en Windows PowerShell 5.1 y 100 en PowerShell 7.7, medido. Usar un verbo aprobado hace que la función se encuentre con Get-Command -Verb Get y que su propósito se entienda sin leerla: Remove siempre borra, Get nunca cambia nada.
Un verbo no aprobado funciona igual en una función suelta. Sin embargo, al importar un módulo que lo exporta PowerShell avisa: «Algunos nombres de comando importados del módulo incluyen verbos no aprobados que podrían dificultar su reconocimiento». Los equivalentes que más se necesitan en español:
| Si piensa en | Use el verbo aprobado |
|---|---|
| Borrar, eliminar | Remove |
| Crear, agregar un objeto nuevo | New |
| Añadir a algo que existe | Add |
| Cambiar, configurar | Set |
| Consultar, listar, mostrar | Get |
| Comprobar, verificar | Test |
| Arrancar o detener un servicio | Start y Stop |
| Reiniciar | Restart |
| Exportar a un archivo | Export |
| Limpiar el contenido | Clear |
Por otro lado, el sustantivo va en singular (Get-Equipo, no Get-Equipos) y, si la función es de la empresa, un prefijo propio evita choques con cmdlets de otros módulos (Get-KhEquipo).
Splatting y $PSBoundParameters
Cuando una llamada lleva muchos parámetros, el *splatting* los agrupa en una tabla hash y los pasa con @ en lugar de $. La línea queda corta, cada valor se puede comentar y los parámetros opcionales se añaden con un if:
$parametros = @{
Ruta = $env:TEMP
Dias = 90
Recursivo = $true
}
Get-ArchivoAntiguo @parametros | Measure-Object
function Get-TemporalAntiguo {
[CmdletBinding()]
param([int]$Dias = 30)
Get-ArchivoAntiguo -Ruta $env:TEMP @PSBoundParameters
}
Get-TemporalAntiguo -Dias 60 | Measure-Object
Además, como se midió, una clave mal escrita en la tabla (Diaz en lugar de Dias) falla igual que en una llamada normal: «No se encuentra ningún parámetro que coincida con el nombre del parámetro Diaz». El segundo ejemplo reenvía los parámetros recibidos con @PSBoundParameters, la forma de escribir una función que envuelve a otra sin repetir cada parámetro.
Por otra parte, para cambiar el valor por defecto de un parámetro sin tocar la función está $PSDefaultParameterValues: con $PSDefaultParameterValues['Get-ArchivoAntiguo:Dias'] = 90, todas las llamadas de la sesión usan 90 días salvo que se indique otro valor. Puesto en el perfil, aplica en cada consola.
Ámbito de las variables dentro de una función
Una función de PowerShell tiene su propio ámbito. Las variables que crea desaparecen al terminar y, si tienen el mismo nombre que una de fuera, no la modifican:
$contador = 1
function Add-Contador { $contador = $contador + 1; "dentro: $contador" }
Add-Contador # dentro: 2
"fuera: $contador" # fuera: 1
Es decir, la función lee las variables de fuera, pero al asignar crea una copia local. Para modificar la de fuera existe $script:contador, aunque es mejor costumbre que la función reciba lo que necesita por parámetro y devuelva el resultado: así se puede probar sola y no depende de lo que haya en la sesión.
Por último, otro efecto del ámbito: una función con el nombre de un cmdlet lo tapa. Medido: tras definir function Get-Date { 'falsa' }, Get-Date devuelve falsa y Get-Command Get-Date dice que es una función. El cmdlet original sigue disponible con su nombre completo, Microsoft.PowerShell.Utility\Get-Date. Por eso conviene el prefijo propio en el sustantivo.
Comandos más usados con funciones en PowerShell
| Comando o variable | Para qué sirve |
|---|---|
Get-Command Nombre -Syntax | Ver los parámetros y conjuntos de una función |
Get-Help Nombre -Full | Ver la ayuda basada en comentarios |
Get-Command -CommandType Function | Listar las funciones cargadas en la sesión |
Get-ChildItem Function: | Lo mismo, como si fueran archivos de una unidad |
Remove-Item Function:\Nombre | Quitar una función de la sesión |
(Get-Command Nombre).Definition | Ver el código de una función cargada |
Get-Verb | La lista de verbos aprobados |
$PSBoundParameters | Los parámetros que se escribieron en la llamada |
$PSCmdlet.ParameterSetName | El conjunto de parámetros usado |
$PSCmdlet.ShouldProcess() | Preguntar antes de cambiar algo (-WhatIf, -Confirm) |
Write-Verbose | Mensajes de detalle que solo salen con -Verbose |
Export-ModuleMember -Function | Elegir qué funciones publica un módulo |
$PSDefaultParameterValues | Valores por defecto para cualquier comando |
Escenario: inventario de espacio en disco de varios equipos
Este ejemplo de funciones en PowerShell reúne casi todo lo anterior en una función de uso diario en el soporte técnico para empresas: recibe uno o varios equipos por parámetro o por la canalización, valida el umbral, avisa de los que no responden sin detenerse y devuelve un objeto por disco, listo para filtrar o exportar a un CSV.
function Get-EspacioDisco {
<#
.SYNOPSIS
Muestra el espacio libre de los discos locales de uno o varios equipos.
.DESCRIPTION
Consulta Win32_LogicalDisk por CIM y devuelve un objeto por disco, con el porcentaje libre
y una alerta cuando baja del umbral.
.PARAMETER Equipo
Nombre de los equipos. Acepta la canalización y las propiedades ComputerName y Name.
.PARAMETER UmbralPorcentaje
Por debajo de este porcentaje libre, la columna Alerta vale True.
.EXAMPLE
Get-EspacioDisco -Equipo PC-001, PC-002 -UmbralPorcentaje 15
.EXAMPLE
Get-ADComputer -Filter * | Get-EspacioDisco | Where-Object Alerta
#>
[CmdletBinding()]
[OutputType([pscustomobject])]
param(
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('ComputerName', 'CN', 'Name')]
[string[]]$Equipo = $env:COMPUTERNAME,
[ValidateRange(1, 99)]
[int]$UmbralPorcentaje = 10
)
process {
foreach ($e in $Equipo) {
$cim = @{ ClassName = 'Win32_LogicalDisk'; Filter = 'DriveType = 3'; ErrorAction = 'Stop' }
if ($e -notin $env:COMPUTERNAME, 'localhost', '.') { $cim.ComputerName = $e }
try {
$discos = Get-CimInstance @cim
} catch {
Write-Warning "$e no responde: $($_.Exception.Message)"
continue
}
foreach ($d in $discos) {
$libre = [math]::Round($d.FreeSpace / $d.Size * 100, 1)
[pscustomobject]@{
Equipo = $e
Unidad = $d.DeviceID
TamanoGB = [math]::Round($d.Size / 1GB, 1)
LibreGB = [math]::Round($d.FreeSpace / 1GB, 1)
LibrePorc = $libre
Alerta = $libre -lt $UmbralPorcentaje
}
}
}
}
}
Get-EspacioDisco -UmbralPorcentaje 15 | Format-Table -AutoSize
Además, un detalle medido explica la línea del if: Get-CimInstance con -ComputerName usa siempre WS-Management, también contra el propio equipo. Con el servicio WinRM detenido, que es lo habitual en un portátil, -ComputerName localhost falla con «El cliente no puede conectarse con el destino especificado en la solicitud», y sin -ComputerName funciona. Por eso la función solo pasa ese parámetro para equipos remotos, armado con splatting. Para los remotos hace falta WinRM habilitado, como se explica en la guía de PowerShell remoto; para descartar antes un problema de red sirve Test-NetConnection.
Por último, el continue va dentro de un foreach, no de ForEach-Object: en un ForEach-Object, continue termina el script entero, como se midió en la guía de try catch.
Escenario: limpieza de temporales con vista previa
La función Remove-ArchivoTemporal de la sección sobre -WhatIf es el patrón para cualquier tarea que borra: primero se ejecuta con -WhatIf y se revisa la lista, después sin él. En una tarea programada se llama con el nombre completo de cada parámetro, se registra la salida con -Verbose en un archivo y, si la función tiene ConfirmImpact = 'High', se añade -Confirm:$false. Combinada con la plantilla de manejo de errores de la guía de try catch, el script termina con un código de salida que la tarea programada sabe interpretar.
Diferencias entre Windows PowerShell 5.1 y PowerShell 7 en funciones
| Característica | Windows PowerShell 5.1 | PowerShell 7.7 |
|---|---|---|
| Parámetros comunes | 11 | 12 (-ProgressAction, desde la 7.4) |
Bloque clean | No existe: la función no se puede definir | Sí, desde la 7.3 |
ErrorMessage en ValidateScript y ValidatePattern | No existe | Sí |
[ValidateRange('Positive')] y similares | No existe | Sí |
[ValidateNotNullOrWhiteSpace()] | No existe | Sí, desde la 7.4 |
Verbos aprobados (Get-Verb) | 98 | 100 |
| 100.000 objetos por la canalización / por parámetro | 2.230 ms / 122 ms | 481 ms / 35 ms |
| Mensajes de error de .NET dentro de un parámetro | En español | En inglés, por ejemplo The input string 'diez' was not in a correct format. |
Fechas en parámetros [datetime] | Cultura invariable (mes/día) | Igual |
En general, una función escrita para 5.1 funciona en 7 casi siempre; al revés, no. Si los scripts tienen que correr en los equipos de usuario, que traen solo Windows PowerShell 5.1, evite las validaciones nuevas y el bloque clean, y pruebe con powershell.exe además de pwsh.exe. Para escribir y depurar funciones largas, Visual Studio Code con la extensión de PowerShell avisa de verbos no aprobados y de parámetros sin usar; la comparación con el ISE está en la guía de PowerShell ISE vs Visual Studio Code.
Errores frecuentes y recomendaciones
Estos son los errores más comunes al escribir funciones de PowerShell, todos comprobados en las dos versiones:
- Llamar con paréntesis y comas. Por ejemplo,
Sumar(2, 3)pasa un arreglo al primer parámetro. Llame con espacios y con el nombre de cada parámetro. - Funciones simples en scripts compartidos. Sin
[CmdletBinding()], un parámetro mal escrito se ignora en silencio. Toda función que vaya a usar otra persona debe ser avanzada. - Salida que se cuela. Cualquier valor sin capturar forma parte del resultado. Descarte con
$null =lo que no quiera devolver. - Contar el resultado sin
@(). Un solo resultado llega como escalar y$nullcuando no hay ninguno. ValueFromPipelinesinprocess. Solo se procesa el último objeto.- Fechas como texto.
03/11/2026es 11 de marzo para una función. Pida año-mes-día o un[datetime]. - Confiar en
-WhatIfa través de módulos. Pruebe cada función con-WhatIfen una carpeta de prueba antes de usarla en producción. ConfirmImpact = 'High'en tareas desatendidas. Añada-Confirm:$falsea la llamada o la tarea fallará.- Ayuda separada por dos líneas en blanco. Póngala dentro de la función.
- Validar dentro del código. Use los atributos de validación: el error sale antes de que la función haga nada.
- Devolver texto formateado. Devuelva objetos (
[pscustomobject]) y dejeFormat-Tablepara quien llama; así el resultado se puede filtrar, ordenar y exportar. - Una función que hace de todo. Una función, una tarea. Varias funciones pequeñas que se encadenan por la canalización se prueban y se reutilizan mejor.
Funciones en PowerShell para la mesa de ayuda: por dónde seguir
Las funciones en PowerShell son la base de todo lo demás: un módulo es un archivo de funciones, una tarea programada llama a una función y un informe de cien equipos es una función con canalización. Con [CmdletBinding()], parámetros con tipo y validación, process para la canalización, ShouldProcess para lo que cambia algo y ayuda basada en comentarios, un script deja de depender de quien lo escribió.
En KHARONTE, la mesa de ayuda TI usa este tipo de herramientas para atender, documentar y repetir las tareas de soporte de nuestros clientes con el mismo criterio cada vez. Si su empresa necesita soporte técnico para empresas o quiere conocer nuestros servicios administrados de TI, revise los servicios de TI para empresas o escríbanos.




