Zakres
robiąc poprzedni wpis stwierdziłem, że największym problemem było pobranie nazwy skryptu uruchamiającego i spodziewałem się, że już jest to ogarnięte. chwilę potem znalazłem dodatkowe scenariusze udowadniające, że sprawa jest jeszcze trudniejsza i bardziej pogmatwana.
temat wydaje się prosty: uruchamiamy skrypt 'skrypt.ps1′ i chcemy założyć plik logu '_skrypt.log’. przy normalnym wywołaniu nie ma najmniejszego problemu. do wyboru mamy kilka zmiennych automatycznych. główną taką zmienną jest $PSScript oraz linqjące do poszczególnych gałęzi skróty:
- $PSCmdlet
- $MyInvocation ($PSCmdlet.MyInvocation)
- $PSScriptRoot
- $PSCommandPath ($MyInvocation.MyCommand.Path)
hint: aby $PSCmdlet zainicjowało się prawidłowo, należy włączyć advanced functions np. przy pomocy cmdletbinding().
w czym zatem problem? otóż $PScmdlet jest inicjowany w momencie ładowania skryptu, i jest zmienną lokalną. mamy więc wiele scenariuszy, w których zamienna przyjmie różne wartości:
- uruchomione bezpośrednio z linii poleceń (nie inicjuje zmiennych automatycznych, ale to co uruchamiamy zwróci wartość)
- uruchomione z funkcji, zdefiniowanej w ramach tego samego pliq
- uruchomione z funkcji, która ładuję bibliotekę
- uruchomione z funkcji która uruchamia funkcję z innej biblioteki
aby to unaocznić przygotowałem scenariusz testowy – dostępny na GH, jeśli ktoś chciałby się pobawić samemu. jest tu dużo ciekawych niuansów i zabawy (np. niedostępność wartości $MyInvocation podczas debugowania).
hint: jeśli odpalisz plik test-context.ps1 z tego samego katalogu gdzie są moduły testowe, nie musisz ich instalować.
czy zatem nie da się wyciągnąć informacji o faktycznym źródle wywołania?
get-PSCallStack
da się – przy pomocy funkcji get-PSCallStack. Funkcja ta zwraca … właśnie 'call stack’ czyli stos wywołania. przypatrzmy się, co konkretnie zwraca funkcja, zależnie od kontextu uruchomienia
- bezpośrednio z linii poleceń:
C:\_ScriptZ :))o- Get-PSCallStack Command Arguments Location ------- --------- -------- <ScriptBlock> {} <No file>
- z linii poleceń, wywołanie funkcji z modułu
C:\_ScriptZ :))o- level1-callstack Command Arguments Location ------- --------- -------- level1-callstack {} level1module.psm1: line 18 <ScriptBlock> {} <No file>
- ze skryptu:
C:\...PSCmdletTest :))o- .\test-contexts.ps1 WARNING: The names of some imported commands from the module 'level1module' include unapproved verbs that might make them less discoverable. To find the commands with unapproved verbs, run the Import-Module command again with the Verbose parameter. For a list of approved verbs, type Get-Verb. PSCallStack in different contexts directly from here Command Arguments Location ------- --------- -------- test-contexts.ps1 {} test-contexts.ps1: line 8 <ScriptBlock> {} <No file> from the function loaded by module Command Arguments Location ------- --------- -------- level1-callstack {} level1module.psm1: line 18 test-contexts.ps1 {} test-contexts.ps1: line 11 <ScriptBlock> {} <No file> from the function in module L2 calling function in module L1 Command Arguments Location ------- --------- -------- level1-callstack {} level1module.psm1: line 18 level2-callstack {} level2module.psm1: line 11 test-contexts.ps1 {} test-contexts.ps1: line 13 <ScriptBlock> {} <No file> MtInvocation test from different contexts first in-script PScommandPath (from myinvocation) local myinvocation -> C:\_scriptz\PUBLIC-GITHUB\PSCmdletTest\test-contexts.ps1 now PScommandpath but run from the function, loaded in a module L1 ($myinvocation.command.path is empty for functions) f1 PScommandPath -> C:\_scriptz\PUBLIC-GITHUB\PSCmdletTest\level1module.psm1 similar, but function inside module L1 calls function in the same module f1 via f2 PScommandPath -> C:\_scriptz\PUBLIC-GITHUB\PSCmdletTest\level1module.psm1 more complex - call function in module L2, which is calling function in module L1 f1 via diferent module -> C:\_scriptz\PUBLIC-GITHUB\PSCmdletTest\level1module.psm1
to co się jako pierwsze rzuca w oczy, równocześnie spójne z definicją 'stosu wykonania’, to że wielkość stosu jest równa zagnieżdżeniu wywołania. z linii poleceń – jest tylko jeden element, bezpośrednio ze skryptu: 2, z funkcji w skrypcie: 3, a z funkcji wołającej funkcję: 4.
drugim elementem rzucającym się w oczy jest kolumna 'location’ która pokazuje ścieżkę do pliq w którym nastąpiło wywołanie, wraz z linią. jest to stos, ale żeby nie było za łatwo – odwrócony. kolejność elementów jest zawsze taka sama ale przez odwrócenie, trzeba zrobić trochę matematyki:
- zero (na dnie): ostatnie wywołanie
- ostatnie-1 : właściwy skrypt wywołujący
- ostatnie: wywołanie bezpośrednie, linia hosta
i te dwie informacje dają nam już możliwość weryfikacji jak dana funkcja jest wywoływana (bezpośrednio z linii poleceń czy gdzieś ze skryptu) oraz dokładne sprawdzenie czy był to moduł, który, a może po prostu jakaś funkcja.
podsumowanie
funkcja get-pscallstack nie jest specjalnie popularna więc nie łatwo było na nią trafić. jednak przy pisaniu modułów lub jakiejś logiki wymagającej wiedzy o środowisq uruchomienia – okazuje się być dużo bardziej przydatna, niż niekontrolowalne $myInvocation, które zmieniają kontext i wartości.
eN.