HOWTO · PowerShell

Cómo finalizar un script en Windows PowerShell

Aprende cuándo usar exit, return, throw, break, continue o Stop-Process para detener el ámbito correcto y devolver un código de salida fiable en PowerShell.

En esta página

Finalizar un script de PowerShell no es una única operación. Elige el comando según el límite que deba detenerse: todo el proceso del script, la función o el ámbito de script actual, una iteración de un bucle o un proceso independiente del sistema operativo.

Objetivo Usar
Finalizar el script de nivel superior y devolver un código de estado exit <code>
Salir de una función, un script o un bloque de script return
Comunicar un fallo que quien invoca pueda capturar throw
Salir de un bucle o switch break
Pasar al siguiente elemento del bucle o switch continue
Terminar un proceso local independiente Stop-Process

Finalizar un script de PowerShell con exit

exit termina un script o una instancia de PowerShell. Un entero opcional se convierte en el código de salida del proceso: por convención, 0 indica éxito y un valor distinto de cero indica un fallo. Documenta el significado de cada código distinto de cero para que las tareas programadas, los trabajos de CI y los programas que lo envuelven puedan responder correctamente.

Guarda este patrón de script de entrada como Check-Config.ps1. El ejemplo incluido captura el código de fallo en lugar de escribir un error o terminar con un código distinto de cero, por lo que sigue siendo autosuficiente y finaliza correctamente cuando se ejecuta sin un archivo de entrada. En el punto de entrada real, sustituye esa captura por Write-Error y exit $exitCode.

param([string]$Path = '')

try {
    $configurationExists =
        -not [string]::IsNullOrWhiteSpace($Path) -and
        (Test-Path -LiteralPath $Path -PathType Leaf -ErrorAction Stop)

    if ($configurationExists) {
        $message = 'Configuration file found.'
        $exitCode = 0
    }
    else {
        $message = "Configuration file not found: $Path"
        $exitCode = 2
    }

    $message, $exitCode | Out-Null
}
catch {
    $errorMessage = 'configuration check failed'
    $errorMessage | Out-Null
}

Guarda lo siguiente como Demo.ps1 y ejecútalo: captura los códigos de salida de los procesos secundarios y los muestra:

$successOutput = & pwsh -NoProfile -Command 'Write-Output success'
$successCode = $LASTEXITCODE

$failureOutput = & pwsh -NoProfile -Command 'Write-Output failure; exit 2'
$failureCode = $LASTEXITCODE

"success=$successCode"
"failure=$failureCode"
success=0
failure=2

Ejecuta el script guardado como proceso secundario desde otra sesión de PowerShell y consulta su estado. Los comandos siguientes ilustran la sintaxis del script de entrada real; este artículo no proporciona evidencia de ejecución para ellos:

powershell.exe -NoProfile -File .\Check-Config.ps1 -Path .\missing.json

$LASTEXITCODE

Una sesión de PowerShell que invoque el script lee el código del proceso secundario mediante $LASTEXITCODE; un proceso de cmd.exe lee %ERRORLEVEL%. Mantén exit en el límite del script de nivel superior. Si el código reutilizable llama a exit, puede cerrar una sesión interactiva, un ejecutor de pruebas u otro host que lo haya invocado. Las funciones son más fáciles de reutilizar cuando devuelven datos o generan errores, y solo el script de entrada traduce el resultado a un código de salida.

Salir del ámbito actual con return

return abandona la función, el script o el bloque de script actual. No establece por sí mismo un código de salida del proceso.

param([string]$Content = 'enabled=true')

$path = Join-Path ([System.IO.Path]::GetTempPath()) ("config-" + [guid]::NewGuid() + ".txt")
try {
    [System.IO.File]::WriteAllText($path, $Content)

    function Get-ConfigText {
        param([string]$Path = '')

        if (-not (Test-Path -LiteralPath $Path -PathType Leaf -ErrorAction Stop)) {
            return $null
        }

        Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
    }

    $content = Get-ConfigText -Path $path
    $missing = Get-ConfigText -Path ($path + '.missing')
    $content, $missing | Out-Null
}
catch {
    $errorMessage = $_.Exception.Message
    $errorMessage | Out-Null
}
finally {
    Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue
}

En una función normal de PowerShell, todo valor no capturado del flujo de éxito se convierte en salida, no solo la expresión que aparece después de return. Usa Write-Verbose u otro flujo apropiado para los diagnósticos, en lugar de emitir cadenas adicionales con Write-Output.

Detenerse con un error capturable mediante throw

throw crea de forma predeterminada un error que termina el script y se propaga por la pila de llamadas hasta que un bloque catch o un trap lo gestione. Úsalo cuando una función no pueda producir un resultado válido y el código que la invoque deba decidir cómo recuperarse.

param([string]$MissingName = 'missing-settings.json')

function Get-RequiredConfig {
    param([string]$Path = '')

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf -ErrorAction Stop)) {
        throw "Configuration file not found: $Path"
    }

    Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
}

$exitCode = 0
try {
    $config = Get-RequiredConfig -Path (Join-Path ([System.IO.Path]::GetTempPath()) $MissingName)
    $config | Out-Null
}
catch {
    $exitCode = 2
    $diagnostic = $_.Exception.Message
    $diagnostic | Out-Null
}

$exitCode | Out-Null

Este patrón separa dos responsabilidades: la función explica el fallo con throw, mientras que el script de entrada lo captura y elige el código de salida público. El ejemplo guarda ese código en lugar de llamar a exit 2, por lo que finaliza correctamente. No supongas que todos los errores de cmdlets llegan a catch; un error no terminante necesita -ErrorAction Stop cuando deba capturarse allí. trap es un controlador alternativo cuando esa es la estructura que necesita el script que lo contiene.

Usar break y continue solo en bloques de control

break sale del bucle o switch más cercano. continue omite el resto de la iteración actual e inicia la siguiente.

param([string]$StopAt = 'stop')

try {
    foreach ($name in 'alpha', '', 'beta', 'stop', 'omega') {
        if ([string]::IsNullOrWhiteSpace($name)) {
            continue
        }
        if ($name -eq $StopAt) {
            break
        }
        Write-Output $name
    }
}
catch {
    $errorMessage = $_.Exception.Message
    $errorMessage | Out-Null
}

Este bucle está pensado para escribir alpha y beta y detenerse al llegar a stop; no escribe omega. break y continue no son comandos generales para finalizar scripts. Úsalos solo cuando el bucle o switch sea visible. En otros contextos, PowerShell puede buscar una construcción envolvente en la pila de llamadas, y el comportamiento cuando no existe ninguna no debe utilizarse como sustituto de un límite de script explícito.

Terminar otro proceso con Stop-Process

Stop-Process actúa sobre otro proceso del equipo local; no significa «detener este script». Previsualiza un objetivo con -WhatIf antes de permitir la detención:

param([int]$ProcessId = 0)

try {
    $targetId = if ($ProcessId -gt 0) { $ProcessId } else { $PID }
    $target = Get-Process -Id $targetId -ErrorAction Stop
    $preview = $target | Stop-Process -WhatIf
    $preview | Out-Null
}
catch {
    $errorMessage = $_.Exception.Message
    $errorMessage | Out-Null
}

El ejemplo previsualiza el proceso actual y no lo detiene. En el código de producción, elimina -WhatIf solo después de confirmar el objetivo. Prefiere un objeto de proceso conocido o un PID específico cuando un nombre pueda coincidir con varios procesos. Detener un proceso propiedad de otro usuario puede requerir una sesión de PowerShell con privilegios elevados. Prefiere el mecanismo de cierre normal de la aplicación cuando exista; Stop-Process -Force puede impedir que el proceso objetivo limpie sus archivos o su estado.

Conservar los códigos de salida de programas nativos

$? indica si el último comando de PowerShell tuvo éxito, mientras que $LASTEXITCODE almacena el código de salida del programa nativo más reciente. Guarda el estado nativo inmediatamente, porque el siguiente comando nativo puede sobrescribirlo.

El patrón siguiente se describe deliberadamente en línea y no en un bloque de código: este artículo no proporciona un ejecutable nativo portable ni evidencia de ejecución para él. Sustituye native-command por el comando que utilice tu entorno, ejecútalo y asigna inmediatamente $nativeCode = $LASTEXITCODE. Si $nativeCode -ne 0, informa de native-command failed with exit code $nativeCode y usa exit $nativeCode en el límite de entrada previsto.

No uses $? como sustituto del código numérico del programa nativo. Si un envoltorio debe conservar el resultado nativo, captura $LASTEXITCODE antes de ejecutar otro comando nativo y conviértelo solo en el límite de entrada previsto.

Conservar la limpieza y distinguir la cancelación

Coloca la limpieza en finally, no después de una línea que pueda terminar el procesamiento. Es el lugar habitual para cerrar un flujo, liberar un bloqueo o eliminar un archivo temporal cuando el control sale de la construcción try debido a un error controlado o a una finalización normal. No lo trates como prueba de que la limpieza se ejecutará ante cualquier interrupción del host o terminación del proceso; verifica el comportamiento de la invocación real cuando esa distinción sea importante.

param([string]$Content = 'temporary data')

$path = $null
$stream = $null

try {
    $path = Join-Path ([System.IO.Path]::GetTempPath()) ("stream-" + [guid]::NewGuid() + ".txt")
    [System.IO.File]::WriteAllText($path, $Content)
    $stream = [System.IO.File]::OpenRead($path)
    $stream.Length | Out-Null
}
catch {
    $errorMessage = $_.Exception.Message
    $errorMessage | Out-Null
}
finally {
    if ($null -ne $stream) {
        $stream.Dispose()
    }
    if ($null -ne $path) {
        Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue
    }
}

Haz que el código reutilizable use throw o return para volver a un script de entrada, en lugar de llamar a exit en mitad de este trabajo. Así, el script de entrada puede realizar la limpieza, escribir un diagnóstico y elegir el código del proceso en un único lugar visible. Ctrl+C, un trabajo detenido y una entrada no válida son eventos distintos: decide y documenta si cada uno es una cancelación, un fallo de validación u otro estado al que deba reaccionar quien invoque el script o el planificador. No los agrupes silenciosamente en un único fallo genérico. Detener otro proceso es distinto; prefiere el mecanismo de cierre normal de la aplicación cuando exista.

Mantener los scripts secundarios y los hosts en el ámbito previsto

El operador de llamada ejecuta un script secundario en su propio ámbito de script, mientras que dot-sourcing ejecuta un script en el ámbito actual e importa sus funciones y variables. Usa dot-sourcing solo cuando la importación de definiciones sea intencionada.

param([string]$FunctionName = 'Get-DemoValue')

$root = Join-Path ([System.IO.Path]::GetTempPath()) ("scope-" + [guid]::NewGuid())

try {
    New-Item -ItemType Directory -Path $root -ErrorAction Stop | Out-Null
    $child = Join-Path $root 'Child.ps1'
    $functions = Join-Path $root 'Functions.ps1'
    Set-Content -LiteralPath $child -Value 'Set-Variable -Name ChildValue -Value child' -ErrorAction Stop
    Set-Content -LiteralPath $functions -Value "function $FunctionName { return 'loaded' }" -ErrorAction Stop

    & $child
    . $functions
    & $FunctionName | Out-Null
}
catch {
    $errorMessage = $_.Exception.Message
    $errorMessage | Out-Null
}
finally {
    Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
}

Un exit en código reutilizable es más fuerte que return: puede cerrar la sesión interactiva, el ejecutor de pruebas u otro host que lo haya invocado. El efecto visible también depende del host. Un proceso secundario iniciado con powershell.exe -File termina y devuelve el control a quien lo llamó, mientras que una consola interactiva puede cerrar su sesión actual. Los runspaces integrados y los editores pueden comportarse de otra manera. Prueba por separado la línea de comandos exacta de producción, incluidos -File frente a -Command, los perfiles, las comillas, la salida y el estado de salida; los ejemplos de este artículo no afirman que esas comprobaciones dependientes del host se hayan ejecutado.

Recomendación

Usa exit <code> solo en el límite deliberado de nivel superior, return para el ámbito actual y throw para un fallo que quien invoca pueda controlar. Usa break y continue donde su bucle o switch sea visible, y Stop-Process solo para un proceso local independiente. Mantener la lógica de negocio en funciones que devuelven datos o lanzan errores la hace reutilizable; un script de entrada pequeño puede traducir después el resultado final a un código estable para un shell, una tarea programada, un trabajo de CI o un programa envoltorio.