Jak przechwycić dane wyjściowe do zmiennej z procesu zewnętrznego w programie PowerShell?

158

Chciałbym uruchomić proces zewnętrzny i przechwycić jego dane wyjściowe polecenia do zmiennej w programie PowerShell. Obecnie używam tego:

$params = "/verify $pc /domain:hosp.uhhg.org"
start-process "netdom.exe" $params -WindowStyle Hidden -Wait

Potwierdziłem, że polecenie jest wykonywane, ale muszę przechwycić dane wyjściowe do zmiennej. Oznacza to, że nie mogę użyć -RedirectOutput, ponieważ to tylko przekierowuje do pliku.

Adam Bertram
źródło
3
Przede wszystkim: Nie używaj Start-Processdo synchronicznego wykonywania (z definicji zewnętrznych) aplikacji konsolowych - po prostu wywołaj je bezpośrednio , jak w każdej powłoce; a mianowicie: netdom /verify $pc /domain:hosp.uhhg.org. Dzięki temu aplikacja będzie połączona ze standardowymi strumieniami konsoli wywołującej, umożliwiając przechwytywanie jej danych wyjściowych przez proste przypisanie $output = netdom .... Większość odpowiedzi udzielonych poniżej domyślnie rezygnuje Start-Processz bezpośredniego wykonania.
mklement0
@ mklement0 z wyjątkiem być może, jeśli ktoś chce użyć -Credentialparametru
CJBS
@CJBS Tak, aby uruchomić z inną tożsamością użytkownika , użycie Start-Processjest koniecznością - ale tylko wtedy (i jeśli chcesz uruchomić polecenie w osobnym oknie). I należy być świadomym nieuniknionych ograniczeń w tym przypadku: Brak możliwości przechwytywania danych wyjściowych, z wyjątkiem - bez przeplotu - tekstu w plikach , za pośrednictwem -RedirectStandardOutputi -RedirectStandardError.
mklement0

Odpowiedzi:

161

Czy próbowałeś:

$OutputVariable = (Shell command) | Out-String

JNK
źródło
Próbowałem przypisać go do zmiennej za pomocą "=", ale nie próbowałem najpierw przesłać wyjścia do Out-String. Spróbuję.
Adam Bertram
10
Nie rozumiem, co się tutaj dzieje i nie mogę sprawić, żeby to zadziałało. Czy „Shell” jest słowem kluczowym w programie PowerShell? Więc w rzeczywistości nie używamy polecenia cmdlet Start-Process? Czy możesz podać konkretny przykład (tj. Zamień „Shell” i / lub „command” na prawdziwy przykład).
deadlydog
@deadlydog Zastąp Shell Commandczymkolwiek, co chcesz uruchomić. To takie proste.
JNK,
1
@stej, masz rację. Wyjaśniłem głównie, że kod w Twoim komentarzu ma inną funkcjonalność niż kod w odpowiedzi. Początkujący, tacy jak ja, mogą zrzucić z siebie subtelne różnice w zachowaniu!
Sam
1
@Atique Natknąłem się na ten sam problem. Okazuje się, że ffmpeg czasami zapisuje na stderr zamiast na stdout, jeśli na przykład użyjesz -iopcji bez określania pliku wyjściowego. 2>&1Rozwiązaniem jest przekierowanie danych wyjściowych przy użyciu sposobu opisanego w niektórych innych odpowiedziach.
jmbpiano
159

Uwaga: Polecenie w pytaniu używa Start-Process, co zapobiega bezpośredniemu przechwytywaniu wyników programu docelowego. Generalnie nie używaj Start-Processdo synchronicznego wykonywania aplikacji konsolowych - po prostu wywołuj je bezpośrednio , jak w każdej powłoce. Dzięki temu aplikacja będzie połączona ze standardowymi strumieniami konsoli wywołującej, umożliwiając przechwytywanie jej danych wyjściowych przez proste przypisanie $output = netdom ..., jak opisano szczegółowo poniżej.

Zasadniczo przechwytywanie danych wyjściowych z zewnętrznych narzędzi działa tak samo, jak w przypadku poleceń natywnych programu PowerShell (możesz chcieć odświeżyć informacje o wykonywaniu narzędzi zewnętrznych ):

$cmdOutput = <command>   # captures the command's success stream / stdout

Zauważ, że $cmdOutputotrzymuje tablicę obiektów, jeśli <command>tworzy więcej niż 1 obiekt wyjściowy , co w przypadku programu zewnętrznego oznacza tablicę ciągów zawierającą wiersze wyjściowe programu .
Jeśli chcesz $cmdOutputzawsze otrzymywać pojedynczy - potencjalnie wieloliniowy - ciąg , użyj
$cmdOutput = <command> | Out-String


Aby przechwycić wynik w zmiennej i wydrukować na ekranie :

<command> | Tee-Object -Variable cmdOutput # Note how the var name is NOT $-prefixed

Lub, jeśli <command>jest to polecenie cmdlet lub funkcja zaawansowana , możesz użyć wspólnego parametru
-OutVariable/-ov
:

<command> -OutVariable cmdOutput   # cmdlets and advanced functions only

Zauważ, że z -OutVariable, w przeciwieństwie do innych scenariuszy, $cmdOutputjest zawsze kolekcja , nawet jeśli tylko jeden obiekt jest wyjście. W szczególności [System.Collections.ArrayList]zwracane jest wystąpienie typu tablicowego .
Zobacz ten problem w serwisie GitHub, aby omówić tę rozbieżność.


Aby uchwycić dane wyjściowe z wielu poleceń , użyj subexpression ( $(...)) lub wywołaj blok skryptu ( { ... }) z &lub .:

$cmdOutput = $(<command>; ...)  # subexpression

$cmdOutput = & {<command>; ...} # script block with & - creates child scope for vars.

$cmdOutput = . {<command>; ...} # script block with . - no child scope

Należy zauważyć, że ogólna potrzeba prefiksu z &(operatora połączeń) pojedynczego polecenia, którego nazwa / ścieżka jest cytowany - na przykład $cmdOutput = & 'netdom.exe' ...- nie jest związane z programami zewnętrznymi per se (to równie dotyczy skryptów PowerShell), ale jest składnia wymóg : PowerShell analizuje instrukcję, która domyślnie zaczyna się od ciągu znaków w cudzysłowie w trybie wyrażenia , podczas gdy tryb argumentów jest potrzebny do wywoływania poleceń (poleceń cmdlet, programów zewnętrznych, funkcji, aliasów), co zapewnia.&

Kluczowa różnica między $(...)i & { ... }/ . { ... }polega na tym, że pierwszy z nich zbiera wszystkie dane wejściowe w pamięci przed zwróceniem go w całości, podczas gdy drugi przesyła dane wyjściowe, nadając się do przetwarzania potokowego jeden po drugim.


Przekierowania również działają zasadniczo tak samo (ale zobacz poniższe zastrzeżenia):

$cmdOutput = <command> 2>&1 # redirect error stream (2) to success stream (1)

Jednak w przypadku poleceń zewnętrznych bardziej prawdopodobne jest, że będą działać zgodnie z oczekiwaniami:

$cmdOutput = cmd /c <command> '2>&1' # Let cmd.exe handle redirection - see below.

Uwagi szczególne dotyczące programów zewnętrznych :

  • Programy zewnętrzne , ponieważ działają poza systemem typów programu PowerShell, zwracają ciągi tylko za pośrednictwem ich strumienia sukcesu (stdout).

  • Jeśli dane wyjściowe zawierają więcej niż 1 wiersz , program PowerShell domyślnie dzieli je na tablicę ciągów . Dokładniej, wiersze wyjściowe są przechowywane w tablicy typu, [System.Object[]]której elementami są stringi ( [System.String]).

  • Jeśli chcesz, aby wyjście było pojedynczym , potencjalnie wieloliniowym ciągiem , potokuj doOut-String :
    $cmdOutput = <command> | Out-String

  • Przekierowanie stderr na stdout za pomocą2>&1 , aby również uchwycić go jako część strumienia sukcesu, wiąże się z pewnymi zastrzeżeniami :

    • Aby dokonać 2>&1scalenia stdout i stderr u źródła , niech cmd.exeobsłużyć przekierowanie , stosując następujące idiomy:
      $cmdOutput = cmd /c <command> '2>&1' # *array* of strings (typically)
      $cmdOutput = cmd /c <command> '2>&1' | Out-String # single string

      • cmd /cwywołuje cmd.exepolecenie <command>i kończy działanie po <command>zakończeniu.
      • Zwróć uwagę na pojedyncze cudzysłowy 2>&1, które zapewniają, że przekierowanie jest przekazywane, cmd.exea nie interpretowane przez program PowerShell.
      • Zauważ, że angażowanie cmd.exeoznacza, że jego reguły ucieczki znaków i rozszerzania zmiennych środowiskowych wchodzą w grę, domyślnie oprócz własnych wymagań PowerShell; w PS v3 + można użyć specjalnego parametru --%(tak zwanego symbolu stop-parsing ), aby wyłączyć interpretację pozostałych parametrów przez PowerShell, z wyjątkiem cmd.exeodwołań do zmiennych środowiskowych w stylu, takich jak %PATH%.

      • Zauważ, że ponieważ łączysz stdout i stderr w źródle za pomocą tego podejścia, nie będziesz w stanie rozróżnić linii pochodzących ze stdout i stderr w PowerShell; jeśli potrzebujesz tego rozróżnienia, użyj własnego 2>&1przekierowania PowerShell - patrz poniżej.

    • Użyj przekierowania PowerShell, 2>&1 aby dowiedzieć się, które linie pochodzą z jakiego strumienia :

      • Stderr wyjście jest ujęte jako zapisy o błędach ( [System.Management.Automation.ErrorRecord]) nie strun, więc tablica wyjściowy może zawierać mieszankę z ciągów (ciąg reprezentujący każdą linię wyjścia) i zapisy o błędach (każdy rekord reprezentująca linię stderr) . Należy zauważyć, że zgodnie z żądaniem 2>&1zarówno ciągi, jak i rekordy błędów są odbierane przez strumień wyjściowy sukcesu programu PowerShell ).

      • W konsoli rekordy błędów są drukowane na czerwono , a pierwszy z nich domyślnie wyświetla wiele wierszy w tym samym formacie, w jakim byłby wyświetlany niekończący błąd polecenia cmdlet; Kolejne rekordy błąd drukowania w kolorze czerwonym, a także, ale tylko wydrukować swój błąd wiadomość , na jednej linii .

      • Podczas wysyłania do konsoli ciągi zwykle znajdują się na pierwszym miejscu w tablicy wyjściowej, a po nich następują rekordy błędów (przynajmniej wśród partii linii stdout / stderr wyświetlanych „w tym samym czasie”), ale na szczęście podczas przechwytywania danych wyjściowych , jest odpowiednio przepleciony , używając tej samej kolejności wyjściowej, jaką można uzyskać bez 2>&1; innymi słowy: podczas wysyłania do konsoli przechwycone dane wyjściowe NIE odzwierciedlają kolejności, w jakiej linie stdout i stderr zostały wygenerowane przez polecenie zewnętrzne.

      • Jeśli przechwycisz całe dane wyjściowe w jednym ciągu za pomocąOut-String , PowerShell doda dodatkowe wiersze , ponieważ reprezentacja ciągu rekordu błędu zawiera dodatkowe informacje, takie jak location ( At line:...) i category ( + CategoryInfo ...); co ciekawe, dotyczy to tylko pierwszego rekordu błędu.

        • Aby obejść ten problem, należy zastosować .ToString()metodę dla każdego obiektu wyjściowego zamiast rurociągów do Out-String:
          $cmdOutput = <command> 2>&1 | % { $_.ToString() };
          w PS v3 + możesz uprościć, aby:
          $cmdOutput = <command> 2>&1 | % ToString
          (Jako bonus, jeśli wyjście nie jest przechwytywane, daje to poprawnie przepleciony wynik nawet podczas drukowania na konsolę).
      • Alternatywnie, filtrować rekordy błędach out i wysyłać je do strumienia błędów PowerShell jest zWrite-Error (jako bonus, jeśli wyjście nie jest zrobione, to produkuje wyjście prawidłowo przeplotem nawet podczas drukowania do konsoli):

$cmdOutput = <command> 2>&1 | ForEach-Object {
  if ($_ -is [System.Management.Automation.ErrorRecord]) {
    Write-Error $_
  } else {
    $_
  }
}
mklement0
źródło
W końcu zadziałało to dla mnie po tym, jak wybrałem ścieżkę wykonywalną ORAZ moje argumenty, wrzuciłem je do łańcucha i potraktowałem to jako moje <polecenie>.
Dan,
2
@Dan: Gdy sam PowerShell interpretuje <command>, nie wolno łączyć pliku wykonywalnego i jego argumentów w jeden ciąg; z przywołaniem przez cmd /cciebie możesz to zrobić i zależy to od sytuacji, czy ma to sens, czy nie. Do którego scenariusza się odnosisz i czy możesz podać minimalny przykład?
mklement0
Działa: $ command = "c: \ mycommand.exe" + $ Args ..... $ output = cmd / c $ command '2> & 1'
Dan,
1
@Dan: Tak, to działa, chociaż nie potrzebujesz zmiennej pośredniej i jawnej konstrukcji łańcucha z +operatorem; działa również to, co następuje: cmd /c c:\mycommand.exe $Args '2>&1'- PowerShell dba o przekazanie elementów $Argsjako ciągu oddzielonego spacjami w tym przypadku, funkcja zwana splatting .
mklement0
Wreszcie poprawna odpowiedź, która działa w PS6.1 +. Sekretem sosu jest rzeczywiście '2>&1'część, a nie zamknięcie się w ()tak wielu skryptach.
not2qubit
24

Jeśli chcesz również przekierować wyjście błędu, musisz zrobić:

$cmdOutput = command 2>&1

Lub, jeśli nazwa programu zawiera spacje:

$cmdOutput = & "command with spaces" 2>&1
manojlds
źródło
4
Co oznacza 2> i 1? „uruchom polecenie o nazwie 2 i umieść jego wynik w poleceniu uruchomienia o nazwie 1”?
Richard
7
Oznacza to „przekieruj standardowe wyjście błędów (deskryptor pliku 2) do tego samego miejsca, do którego zmierza standardowe wyjście (deskryptor pliku 1)”. Zasadniczo przekierowuje komunikaty normalne i komunikaty o błędach w to samo miejsce (w tym przypadku konsola, jeśli stdout nie jest przekierowywana gdzie indziej - jak plik).
Giovanni Tirloni
11

Albo spróbuj tego. Przechwytuje dane wyjściowe do zmiennej $ scriptOutput:

& "netdom.exe" $params | Tee-Object -Variable scriptOutput | Out-Null

$scriptOutput
Finzzownt
źródło
7
-1, niepotrzebnie skomplikowane. $scriptOutput = & "netdom.exe" $params
CharlesB
8
Usunięcie out-null i jest to świetne do jednoczesnego podłączania rur do powłoki i zmiennej.
ferventcoder
10

Inny przykład z życia wzięty:

$result = & "$env:cust_tls_store\Tools\WDK\x64\devcon.exe" enable $strHwid 2>&1 | Out-String

Zwróć uwagę, że ten przykład zawiera ścieżkę (która zaczyna się od zmiennej środowiskowej). Zauważ, że cudzysłowy muszą otaczać ścieżkę i plik EXE, ale nie parametry!

Uwaga: nie zapomnij &znaku przed poleceniem, ale poza cudzysłowami.

Zbierane są również informacje o błędach.

Zajęło mi trochę czasu, zanim ta kombinacja zadziałała, więc pomyślałem, że podzielę się nią.

Davidiam
źródło
8

Wypróbowałem odpowiedzi, ale w moim przypadku nie otrzymałem surowego wyniku. Zamiast tego został przekonwertowany na wyjątek programu PowerShell.

Surowy wynik, który otrzymałem:

$rawOutput = (cmd /c <command> 2`>`&1)
Sevenforce
źródło
2

Do pracy dostałem:

$Command1="C:\\ProgramData\Amazon\Tools\ebsnvme-id.exe"
$result = & invoke-Expression $Command1 | Out-String

$ wynik daje ci to, co potrzebne

Faye Smelter
źródło
1

Ta rzecz zadziałała dla mnie:

$scriptOutput = (cmd /s /c $FilePath $ArgumentList)
Alex R.
źródło
0

Jeśli wszystko, co próbujesz zrobić, to przechwycić dane wyjściowe z polecenia, to zadziała dobrze.

Używam go do zmiany czasu systemowego, ponieważ [timezoneinfo]::localzawsze produkuje te same informacje, nawet po wprowadzeniu zmian w systemie. Tylko w ten sposób mogę zweryfikować i zarejestrować zmianę strefy czasowej:

$NewTime = (powershell.exe -command [timezoneinfo]::local)
$NewTime | Tee-Object -FilePath $strLFpath\$strLFName -Append

Oznacza to, że muszę otworzyć nową sesję PowerShell, aby ponownie załadować zmienne systemowe.

Kelly Davis
źródło