Funciones en PowerShell: funciones avanzadas, parámetros y CmdletBinding

Funciones en PowerShell: un estuche abierto con llaves combinadas, una fila de dados ordenados de menor a mayor y puntas intercambiables, cada pieza en su hueco

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 simpleFunción avanzada
Solo function Nombre { } con param() opcionalLleva [CmdletBinding()] o algún atributo [Parameter()]
Un parámetro mal escrito va a $args sin ningún errorUn parámetro mal escrito detiene la llamada con un error
No tiene parámetros comunesRecibe -Verbose, -ErrorAction y los demás parámetros comunes
No admite -WhatIf ni -ConfirmLos admite si se declara SupportsShouldProcess
Útil para una ayuda rápida en la consolaLa 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óndeCómo se cargaCuándo usarlo
La sesión actualSe escribe o se pega en la consolaPruebas rápidas
Un script .ps1 con varias funcionesCon un punto delante: . .\Funciones.ps1Un grupo de funciones que se carga a mano
El perfil ($PROFILE)Se carga solo al abrir la consolaAtajos personales del técnico
Un módulo (.psm1)Import-Module o carga automáticaFunciones 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:

PruebaFunción simpleFunción con [CmdletBinding()]
Parámetro mal escrito (-Nmbre Ana)Sin error: -Nmbre y Ana acaban en $args y el parámetro real queda vacíoError: no se encuentra ningún parámetro que coincida con el nombre Nmbre
Un argumento de másSin error: el sobrante va a $argsError: no se encuentra ningún parámetro de posición que acepte el argumento
Parámetros comunesNinguno11 en 5.1 y 12 en 7.7 (la 7.4 añadió -ProgressAction)
Write-VerboseNo hay forma de activarlo desde la llamadaSale con -Verbose, y también en las funciones avanzadas que llama
-WhatIf sin SupportsShouldProcessNo existeTampoco: 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ámetroValor recibidoResultado medido
[int]'10' (texto)10
[int]2.52: redondeo al par más cercano
[int]3.54
[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]$nullCadena 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
AtributoQué compruebaMensaje medido al fallar (resumido)
[ValidateSet()]Que el valor esté en una listaEl argumento no pertenece al conjunto especificado por el atributo ValidateSet
[ValidateRange()]Que un número esté entre dos límitesEl argumento 45 es mayor que el intervalo máximo permitido de 30
[ValidatePattern()]Que el texto cumpla una expresión regularEl argumento no coincide con el patrón
[ValidateScript()]Que un bloque de código devuelva $trueEl script de validación no devolvió el resultado True
[ValidateLength()]La longitud mínima y máxima de un textoLa longitud de caracteres (2) del argumento es insuficiente
[ValidateCount()]Cuántos elementos tiene un arregloEl 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íoEl argumento es NULL, está vacío o un elemento de la colección contiene un valor NULL
[ValidateNotNullOrWhiteSpace()]Lo mismo, y tampoco solo espaciosSolo 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 bogota se acepta y $Sede vale bogota, en minúsculas. Si el resto del script compara con -ceq o usa el valor como nombre de carpeta en Linux, conviene normalizarlo. Con IgnoreCase = $false el 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 ParameterSetName pertenece 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ónResultado
ValueFromPipeline sin bloque process y tres equipos por la canalizaciónSolo se procesa el último (PC-003): todo el cuerpo se trata como end
El mismo caso con processSe procesan los tres
Parámetro [string] y un arreglo pasado con -Equipo PC-001, PC-002Error: no se puede convertir el valor al tipo String
Parámetro [string[]] y foreach dentro de processFunciona igual por parámetro y por canalización
Parámetro [string[]] sin foreachPor parámetro, un solo objeto con el texto PC-001 PC-002
Objeto con la propiedad ComputerName y un alias ComputerName en el parámetroEl valor llega solo, sin escribir nada
return dentro de processSalta 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ónResultado medido
La función devuelve un arreglo de un elementoLlega un escalar (un String), no un arreglo
La función devuelve un arreglo vacíoLlega $null
.Count sobre ese escalar con Set-StrictMode -Version LatestError: 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ónSe ve en pantalla pero no forma parte del resultado
[OutputType([int])] y la función devuelve textoLlega 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:

  • -WhatIf llega a los cmdlets que llama la función: un Remove-Item sin ShouldProcess propio dentro de una función con -WhatIf tampoco borró el archivo.
  • 🔴 -WhatIf no 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 declaraba SupportsShouldProcess. La guía de Microsoft sobre ShouldProcess lo reconoce: los módulos de script no heredan $WhatIfPreference ni $ConfirmPreference, y recomienda no confiar nunca en que funcione. La solución medida es pasarlo de forma explícita en la llamada: con Remove-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 $ConfirmPreference vale High de 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 ConfirmImpact por 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 enUse el verbo aprobado
Borrar, eliminarRemove
Crear, agregar un objeto nuevoNew
Añadir a algo que existeAdd
Cambiar, configurarSet
Consultar, listar, mostrarGet
Comprobar, verificarTest
Arrancar o detener un servicioStart y Stop
ReiniciarRestart
Exportar a un archivoExport
Limpiar el contenidoClear

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 variablePara qué sirve
Get-Command Nombre -SyntaxVer los parámetros y conjuntos de una función
Get-Help Nombre -FullVer la ayuda basada en comentarios
Get-Command -CommandType FunctionListar las funciones cargadas en la sesión
Get-ChildItem Function:Lo mismo, como si fueran archivos de una unidad
Remove-Item Function:\NombreQuitar una función de la sesión
(Get-Command Nombre).DefinitionVer el código de una función cargada
Get-VerbLa lista de verbos aprobados
$PSBoundParametersLos parámetros que se escribieron en la llamada
$PSCmdlet.ParameterSetNameEl conjunto de parámetros usado
$PSCmdlet.ShouldProcess()Preguntar antes de cambiar algo (-WhatIf, -Confirm)
Write-VerboseMensajes de detalle que solo salen con -Verbose
Export-ModuleMember -FunctionElegir qué funciones publica un módulo
$PSDefaultParameterValuesValores 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ísticaWindows PowerShell 5.1PowerShell 7.7
Parámetros comunes1112 (-ProgressAction, desde la 7.4)
Bloque cleanNo existe: la función no se puede definirSí, desde la 7.3
ErrorMessage en ValidateScript y ValidatePatternNo existeSí
[ValidateRange('Positive')] y similaresNo existeSí
[ValidateNotNullOrWhiteSpace()]No existeSí, desde la 7.4
Verbos aprobados (Get-Verb)98100
100.000 objetos por la canalización / por parámetro2.230 ms / 122 ms481 ms / 35 ms
Mensajes de error de .NET dentro de un parámetroEn españolEn 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:

  1. 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.
  2. 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.
  3. Salida que se cuela. Cualquier valor sin capturar forma parte del resultado. Descarte con $null = lo que no quiera devolver.
  4. Contar el resultado sin @(). Un solo resultado llega como escalar y $null cuando no hay ninguno.
  5. ValueFromPipeline sin process. Solo se procesa el último objeto.
  6. Fechas como texto. 03/11/2026 es 11 de marzo para una función. Pida año-mes-día o un [datetime].
  7. Confiar en -WhatIf a través de módulos. Pruebe cada función con -WhatIf en una carpeta de prueba antes de usarla en producción.
  8. ConfirmImpact = 'High' en tareas desatendidas. Añada -Confirm:$false a la llamada o la tarea fallará.
  9. Ayuda separada por dos líneas en blanco. Póngala dentro de la función.
  10. Validar dentro del código. Use los atributos de validación: el error sale antes de que la función haga nada.
  11. Devolver texto formateado. Devuelva objetos ([pscustomobject]) y deje Format-Table para quien llama; así el resultado se puede filtrar, ordenar y exportar.
  12. 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.

Hablemos de la TI de su empresa

Cuéntenos qué necesita su operación y le enviamos una propuesta por escrito. Cada línea del portafolio se contrata por separado.

Compartir este artículo

Últimas entradas

Escríbanos ahora