Valor de retorno de la función en PowerShell

188

Desarrollé una función de PowerShell que realiza una serie de acciones que implican el aprovisionamiento de sitios de SharePoint Team. En última instancia, quiero que la función devuelva la URL del sitio aprovisionado como una Cadena, así que al final de mi función tengo el siguiente código:

$rs = $url.ToString();
return $rs;

El código que llama a esta función se ve así:

$returnURL = MyFunction -param 1 ...

Así que espero una cadena, sin embargo, no lo es. En cambio, es un objeto de tipo System.Management.Automation.PSMethod. ¿Por qué devuelve ese tipo en lugar de un tipo de cadena?

ChiliYago
fuente
2
¿Podemos ver la declaración de función?
zdan
1
No, no la invocación de la función, muéstrenos cómo / dónde declaró "MyFunction". Qué sucede cuando lo hace: Función Get-Item: \ MyFunction
zdan
1
Sugirió una forma alternativa de abordar esto: stackoverflow.com/a/42842865/992301
Feru
Creo que esta es una de las preguntas más importantes de PowerShell sobre el desbordamiento de la pila en relación con las funciones / métodos. Si está planeando aprender secuencias de comandos de PowerShell, debe leer toda esta página y comprenderla a fondo. Te odiarás más tarde si no lo haces.
Birdman

Respuestas:

271

PowerShell tiene una semántica de retorno realmente extraña, al menos cuando se ve desde una perspectiva de programación más tradicional. Hay dos ideas principales para entender:

  • Toda la salida se captura y se devuelve.
  • La palabra clave return realmente solo indica un punto de salida lógico

Por lo tanto, los siguientes dos bloques de script harán exactamente lo mismo:

$a = "Hello, World"
return $a

 

$a = "Hello, World"
$a
return

La variable $ a en el segundo ejemplo se deja como salida en la tubería y, como se mencionó, se devuelve toda la salida. De hecho, en el segundo ejemplo, podría omitir el retorno por completo y obtendría el mismo comportamiento (el retorno estaría implícito a medida que la función se completa y sale de forma natural).

Sin más de su definición de función, no puedo decir por qué está obteniendo un objeto PSMethod. Supongo que probablemente tengas algo en línea que no se está capturando y se está colocando en la tubería de salida.

También vale la pena señalar que probablemente no necesite esos puntos y comas, a menos que esté anidando varias expresiones en una sola línea.

Puede leer más sobre la semántica de retorno en la página about_Return en TechNet, o invocando el help returncomando desde PowerShell.

Goyuix
fuente
13
Entonces, ¿hay alguna forma de devolver un valor de escala de una función?
ChiliYago
10
Si su función devuelve una tabla hash, tratará el resultado como tal, pero si "repite" algo dentro del cuerpo de la función, incluida la salida de otros comandos, y de repente el resultado se mezcla.
Luke Puplett
55
Si desea enviar cosas a la pantalla desde una función sin devolver ese texto como parte del resultado de la función, use el comando write-debugdespués de configurar la variable en $DebugPreference = "Continue"lugar de"SilentlyContinue"
BeowulfNode42
77
Esto ayudó, pero este stackoverflow.com/questions/22663848/… ayudó aún más. Canalice resultados no deseados de comandos / llamadas a funciones en la función llamada a través de "... | Out-Null" y luego simplemente devuelva lo que desea.
Straff
1
¡@LukePuplett echofue el problema para mí! Ese pequeño punto secundario es muy muy importante. Estaba devolviendo una tabla hash, siguiendo todo lo demás, pero aún así el valor de retorno no era lo que esperaba. Eliminado echoy trabajado como un encanto.
Ayo I
54

Esta parte de PowerShell es probablemente el aspecto más estúpido. Cualquier salida extraña generada durante una función contaminará el resultado. A veces no hay salida, y luego, en algunas condiciones, hay otra salida no planificada, además de su valor de retorno planificado.

Entonces, elimino la asignación de la llamada a la función original, de modo que el resultado termina en la pantalla, y luego paso hasta que algo que no planeé aparece en la ventana del depurador (usando el ISE de PowerShell ).

Incluso cosas como reservar variables en ámbitos externos causan resultados, como lo [boolean]$isEnabledcual escupirá molestamente un False a menos que lo haga [boolean]$isEnabled = $false.

Otra buena es la $someCollection.Add("thing")que escupe el nuevo recuento de colecciones.

Luke Puplett
fuente
2
¿Por qué debería reservar un nombre en lugar de hacer la mejor práctica de inicializar correctamente la variable con un valor definido explícitamente?
BeowulfNode42
2
Supongo que quiere decir que tiene un bloque de declaraciones de variables al comienzo de cada ámbito para cada variable utilizada en ese ámbito. Aún debe, o puede que ya esté, inicializando todas las variables antes de usarlas en cualquier lenguaje, incluido C #. También es importante hacerlo [boolean]$aen powershell en realidad no declara la variable $ a. Puede confirmar esto siguiendo esa línea con la línea $a.gettype()y comparar esa salida con cuando declara e inicializa con la cadena $a = [boolean]$false.
BeowulfNode42
3
Probablemente tenga razón, preferiría un diseño más explícito para evitar escrituras accidentales.
Luke Puplett
44
Viniendo desde la perspectiva de un programador, aunque soy un PS-n00b, no estoy tan seguro de que este sea el peor de los muchos aspectos molestos de la sintaxis de PS. ¿Cómo demonios alguien pensó que era preferible escribir "x -gt y" en lugar de "x> y"?!? ¿Seguramente al menos los operadores integrados podrían haber usado una sintaxis más amigable para los humanos?
The Dag
55
@TheDag porque es un shell primero, segundo lenguaje de programación; >y <ya se utilizan para la redirección de flujo de E / S hacia / desde archivos. >=escribe stdout en un archivo llamado =.
TessellatingHeckler
38

Con PowerShell 5 ahora tenemos la capacidad de crear clases. Cambie su función a una clase, y return solo devolverá el objeto inmediatamente anterior. Aquí hay un ejemplo muy simple.

class test_class {
    [int]return_what() {
        Write-Output "Hello, World!"
        return 808979
    }
}
$tc = New-Object -TypeName test_class
$tc.return_what()

Si esta fuera una función, el resultado esperado sería

Hello World
808979

pero como clase, lo único que se devuelve es el entero 808979. Una clase es como una garantía de que solo devolverá el tipo declarado o nulo.

DaSmokeDog
fuente
66
Nota. También es compatible con los staticmétodos. Conduciendo a la sintaxis[test_class]::return_what()
Sancarn
1
"Si se tratara de una función, la salida esperada sería 'Hello World \ n808979". ¿No creo que sea correcto? El valor de salida siempre es solo el valor de la última declaración, en su caso return 808979, función o no.
amn
1
@amn ¡Gracias! Estás en lo correcto, el ejemplo solo devolvería eso si creara resultados dentro de la función. Que es lo que me molesta. A veces, su función puede generar salida y esa salida más cualquier valor que agregue en la declaración de devolución se devuelve. Las clases como saben solo devolverán el tipo declarado o nulo. Si prefiere utilizar funciones, un método sencillo es asignar la función a una variable y si se devuelve más que el valor de retorno, var es una matriz y el valor de retorno será el último elemento de la matriz.
DaSmokeDog
1
Bueno, ¿qué esperas de un comando llamado Write-Output? Aquí no se hace referencia a la salida de "consola", sino a la función / cmdlet. Si quieres volcar cosas en la consola hay Write-Verbose, Write-Hosty Write-Errorfunciones, entre otras. Básicamente, Write-Outputestá más cerca de la returnsemántica que de un procedimiento de salida de consola. No abuses de ella, Write-Verboseúsala con verbosidad, para eso es.
amn
1
@amn Soy muy consciente de que esta no es la consola. Fue una forma rápida de mostrar la forma en que un retorno trata con resultados inesperados dentro de una función frente a una clase. en una función, todos los resultados + el valor que devuelve se devuelven. SIN EMBARGO, solo se pasa el valor especificado en el retorno de una clase. Intenta ejecutarlos por ti mismo.
DaSmokeDog
31

Como solución, he estado devolviendo el último objeto en la matriz que obtienes de la función ... No es una gran solución, pero es mejor que nada:

someFunction {
   $a = "hello"
   "Function is running"
   return $a
}

$b = someFunction
$b = $b[($b.count - 1)]  # Or
$b = $b[-1]              # Simpler

Con todo, una forma más lineal de escribir lo mismo podría ser:

$b = (someFunction $someParameter $andAnotherOne)[-1]
Jonesy
fuente
77
$b = $b[-1]sería una forma más simple de obtener el último elemento o la matriz, pero aún más simple sería simplemente no generar valores que no desea.
Duncan
@Duncan ¿Y qué pasa si quiero generar las cosas en la función, mientras que al mismo tiempo también quiero un valor de retorno?
Utkan Gezer
@ThoAppelsin si quiere decir que quiere escribir cosas en el terminal, luego use write-hostpara enviarlo directamente.
Duncan
7

Paso un objeto Hashtable simple con un solo miembro de resultado para evitar la locura de retorno, ya que también quiero enviar a la consola. Actúa a través del pase por referencia.

function sample-loop($returnObj) {
  for($i = 0; $i -lt 10; $i++) {
    Write-Host "loop counter: $i"
    $returnObj.result++
  }
}

function main-sample() {
  $countObj = @{ result = 0 }
  sample-loop -returnObj $countObj
  Write-Host "_____________"
  Write-Host "Total = " ($countObj.result)
}

main-sample

Puedes ver ejemplos reales de uso en mi proyecto GitHub unpackTunes .

Lo que sería genial
fuente
4

Es difícil de decir sin mirar el código. Asegúrese de que su función no devuelva más de un objeto y que capture los resultados de otras llamadas. ¿Qué obtienes por:

@($returnURL).count

De todos modos, dos sugerencias:

Lanza el objeto a una cadena:

...
return [string]$rs

O simplemente enciérrelo entre comillas dobles, igual que arriba pero más corto para escribir:

...
return "$rs"
Shay Levy
fuente
1
O incluso "$rs"- el returnsólo se requiere al regresar temprano de la función. Dejar de lado el retorno es mejor modismo de PowerShell.
David Clarke
4

La descripción de Luke de los resultados de la función en estos escenarios parece ser correcta. Solo deseo comprender la causa raíz y el equipo del producto PowerShell haría algo al respecto. Parece ser bastante común y me ha costado demasiado tiempo de depuración.

Para solucionar este problema, he estado usando variables globales en lugar de devolver y usar el valor de la llamada a la función.

Aquí hay otra pregunta sobre el uso de variables globales: establecer una variable global de PowerShell desde una función donde el nombre de la variable global es una variable que se pasa a la función

Ted B.
fuente
3

Lo siguiente simplemente devuelve 4 como respuesta. Cuando reemplaza las expresiones de agregar para cadenas, devuelve la primera cadena.

Function StartingMain {
  $a = 1 + 3
  $b = 2 + 5
  $c = 3 + 7
  Return $a
}

Function StartingEnd($b) {
  Write-Host $b
}

StartingEnd(StartingMain)

Esto también se puede hacer para una matriz. El siguiente ejemplo devolverá "Texto 2"

Function StartingMain {
  $a = ,@("Text 1","Text 2","Text 3")
  Return $a
}

Function StartingEnd($b) {
  Write-Host $b[1]
}

StartingEnd(StartingMain)

Tenga en cuenta que debe llamar a la función debajo de la función misma. De lo contrario, la primera vez que se ejecute devolverá un error que no sabe qué es "StartingMain".

Edwin
fuente
2

Las respuestas existentes son correctas, pero a veces no estás devolviendo algo explícitamente con a Write-Outputo a return, sin embargo, hay un valor misterioso en los resultados de la función. Esta podría ser la salida de una función integrada comoNew-Item

PS C:\temp> function ContrivedFolderMakerFunction {
>>    $folderName = [DateTime]::Now.ToFileTime()
>>    $folderPath = Join-Path -Path . -ChildPath $folderName
>>    New-Item -Path $folderPath -ItemType Directory
>>    return $true
>> }
PS C:\temp> $result = ContrivedFolderMakerFunction
PS C:\temp> $result


    Directory: C:\temp


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----         2/9/2020   4:32 PM                132257575335253136
True

Todo ese ruido extra de la creación del directorio se está recopilando y emitiendo en la salida. La manera fácil de mitigar esto es agregar | Out-Nullal final de la New-Itemdeclaración, o puede asignar el resultado a una variable y simplemente no usar esa variable. Se vería así ...

PS C:\temp> function ContrivedFolderMakerFunction {
>>    $folderName = [DateTime]::Now.ToFileTime()
>>    $folderPath = Join-Path -Path . -ChildPath $folderName
>>    New-Item -Path $folderPath -ItemType Directory | Out-Null
>>    # -or-
>>    $throwaway = New-Item -Path $folderPath -ItemType Directory 
>>    return $true
>> }
PS C:\temp> $result = ContrivedFolderMakerFunction
PS C:\temp> $result
True

New-Itemes probablemente el más famoso de estos, pero otros incluyen todos los StringBuilder.Append*()métodos, así como el SqlDataAdapter.Fill()método.

StingyJack
fuente