Munkánk során valószínű jó néhány hasznos függvényt készítünk, amelyeket rendszeresen használni szeretnénk. Ezekhez úgy férünk hozzá legegyszerűbben, ha ezeket a függvényeket szkriptfájlokban elmentjük egy könyvtárba, majd a sok kis szkriptfájlunkat egy „beemelő”, „include” jellegű központi szkripttel lefuttatjuk (vagy modult készítünk belőlük, lásd 2.2 Modulok fejezetet.
Ennek modellezésére készítettem egy „scripts” könyvtárat, amelyben három szkriptem három függvényt definiál. Ezen kívül van egy include.ps1 szkriptem, ami csak annyit csinál, hogy a saját könyvtárában levő másik három szkriptet meghívja „dotsourcing” jelleggel, azaz úgy, hogy a szkriptek által definiált függvények bárhonnan elérhetők, meghívhatók legyenek.
[17] PS C:\powershell2\egyik> Get-ChildItem C:\powershell2\scripts
Directory: Microsoft.PowerShell.Core\FileSystem::C:\powershell2\scripts
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2008.04.19. 12:31 42 fv1.ps1
-a--- 2008.04.19. 12:31 45 fv2.ps1
-a--- 2008.04.19. 12:31 45 fv3.ps1
-a--- 2008.04.19. 12:32 40 include.ps1
[18] PS C:\powershell2\egyik> get-content C:\powershell2\scripts\fv1.ps1
function fv1
{
"Első függvény"
}
[19] PS C:\powershell2\egyik> get-content C:\powershell2\scripts\include.ps1
. .\fv1.ps1
. .\fv2.ps1
. .\fv3.ps1
Ez egyes függvények nagyon egyszerűek, csak annyit írnak ki, hogy hányadik függvényről van szó. Ez így külön-külön nagyon szépnek és logikusnak tűnik, próbáljuk meg futtatni az include.ps1 szkriptünket az „egyik” nevű könyvtárból:
[22] PS C:\powershell2\egyik> C:\powershell2\scripts\include.ps1
The term
'.\fv1.ps1' is not recognized as the name of a cmdlet, function, scri
pt file, or
operable program. Check the spelling of the name, or if a path was
included, verify that the path is correct and
try again.
At
C:\_munka\powershell2\scripts\include.ps1:3 char:2
+ .
<<<< .\fv1.ps1
+ CategoryInfo : ObjectNotFound: (.\fv1.ps1:String)
[], CommandN
otFoundException
+ FullyQualifiedErrorId :
CommandNotFoundException
The term
'.\fv2.ps1' is not recognized as the name of a cmdlet, function, scri
pt file, or
operable program. Check the spelling of the name, or if a path was
included, verify that the path is correct and
try again.
At
C:\_munka\powershell2\scripts\include.ps1:4 char:2
+ .
<<<< .\fv2.ps1
+ CategoryInfo : ObjectNotFound: (.\fv2.ps1:String)
[], CommandN
otFoundException
+ FullyQualifiedErrorId :
CommandNotFoundException
The term
'.\fv3.ps1' is not recognized as the name of a cmdlet, function, scri
pt file, or
operable program. Check the spelling of the name, or if a path was
included, verify that the path is correct and
try again.
At
C:\_munka\powershell2\scripts\include.ps1:5 char:2
+ .
<<<< .\fv3.ps1
+ CategoryInfo : ObjectNotFound: (.\fv3.ps1:String)
[], CommandN
otFoundException
+ FullyQualifiedErrorId :
CommandNotFoundException
Valami nem jó! Nyomozzunk utána, cseréljük le az include.ps1 belsejét egy olyan vizsgálatra, amely megmutatja, hogy mit érez a szkript aktuális könyvtárnak. Amíg nem találom meg a hibát, a függvényeket definiáló szkriptek hívását kikommenteztem:
#. .\fv1.ps1
#. .\fv2.ps1
#. .\fv3.ps1
Get-Location
Ezt futtatva a következőket kapjuk:
[23] PS C:\powershell2\egyik> C:\powershell2\scripts\include.ps1
Path
----
C:\powershell2\egyik
Kiderült a hiba oka, annak ellenére, hogy az include.ps1 a scripts könyvtárban fut, számára is az aktuális könyvtár az „egyik”. Hogyan lehetne azt megoldani, hogy az include.ps1 számára a saját könyvtára legyen az aktuális?
Szerencsére van egy $MyInvocation nevű automatikus változó, amely a szkriptek számára a futtatásukkal kapcsolatos információkat árulja el. Nézzük is ezt meg, módosítottam az include.ps1 szkriptemet:
#. .\fv1.ps1
#. .\fv2.ps1
#. .\fv3.ps1
$MyInvocation
Ezt futtatva kapjuk a következőket:
[30] PS C:\powershell2\egyik> C:\powershell2\scripts\include.ps1
MyCommand : include.ps1
ScriptLineNumber : 1
OffsetInLine : -2147483648
ScriptName :
Line : C:\powershell2\scripts\include.ps1
PositionMessage :
At line:1 char:34
+ C:\powershell2\scripts\include.ps1 <<<<
InvocationName : C:\powershell2\scripts\include.ps1
PipelineLength : 1
PipelinePosition : 1
Ha átlépünk a szülőkönyvtárba, és onnan hívjuk meg a szkriptet a következőképpen alakul a $MyInvocation értéke:
[31] PS C:\powershell2\egyik> cd ..
C:\powershell2
[32] PS C:\powershell2> .\scripts\include.ps1
MyCommand : include.ps1
ScriptLineNumber : 1
OffsetInLine : -2147483648
ScriptName :
Line : .\scripts\include.ps1
PositionMessage :
At line:1 char:21
+ .\scripts\include.ps1 <<<<
InvocationName : .\scripts\include.ps1
PipelineLength : 1
PipelinePosition : 1
Ebből az látszik, hogy se a Line, se az InvocationName tulajdonság nem a szkript tényleges elérési útját tartalmazza, hanem azt, ahonnan meghívtuk. Így ezekkel a tulajdonságokkal nem tudunk dolgozni, mert ezek sem adnak iránymutatást arra vonatkozólag, hogy hol van ténylegesen az include.ps1, és vele együtt a függvényeimet tartalmazó szkriptek.
Nézzük, hogy vajon a MyCommand -nak vannak-e tulajdonságai:
#. .\fv1.ps1
#. .\fv2.ps1
#. .\fv3.ps1
$MyInvocation.MyCommand
A futtatás eredménye:
[34] PS C:\powershell2> .\scripts\include.ps1 | fl
Path : C:\powershell2\scripts\include.ps1
Definition : C:\powershell2\scripts\include.ps1
Name : include.ps1
CommandType : ExternalScript
Itt már van egy sokat sejtető Path tulajdonság, ami egy teljes elérési út, így remény van rá, hogy ebből kiindulva már jól el fogjuk tudni érni a függvényeket tartalmazó szkripteket.
Létezik egy split-path cmdlet, amellyel le tudjuk választani egy elérési útról a könyvtár-hierarchia részt, ennek segítségével már el tudjuk készíteni az univerzális, bárhonnan működő include.ps1 szkriptünket:
Push-Location
Set-Location (split-path $MyInvocation.MyCommand.Path)
. .\fv1.ps1
. .\fv2.ps1
. .\fv3.ps1
Pop-Location
És a futtatásának eredménye:
[41] PS C:\powershell2> .\scripts\include.ps1
[42] PS C:\powershell2> fv1
The term
'fv1' is not recognized as the name of a cmdlet, function, script fil
e, or
operable program. Check the spelling of the name, or if a path was inclu
ded, verify
that the path is correct and try again.
At line:1
char:4
+ fv1
<<<<
+ CategoryInfo : ObjectNotFound: (fv1:String) [],
CommandNotFoun
dException
+ FullyQualifiedErrorId :
CommandNotFoundException
Na, most mi a hiba? Hát az, hogy bár jól lefutott az include.ps1, de a betöltött függvényszkriptek csak az ő szintjére lettek „dotsource”-olva. Ahhoz, hogy a globális scope-ból is elérhessük ezeket a függvényeket, magát az include.ps1-et is dotsource-szal kell meghívni ([43]-as sorban a plusz pont és szóköz a prompt után):
[43] PS C:\powershell2> . .\scripts\include.ps1
[44] PS C:\powershell2> fv1
Első függvény
[45] PS C:\powershell2> fv2
Második függvény
Így már tökéletesen működik a szkriptkönyvtárunk.
A fenti példában a $myinvocation.mycommand.definition tulajdonságot is használhattuk volna, az is pontosan megadja a szkripünk tényleges helyét.
PowerShell 3.0 óta azonban egy még egyszerűbb módszer is rendelkezésünkre áll, ez pedig a $PSScriptRoot változó, azaz már három lehetőségünk van, hogy a szkript saját helyének meghatározására:
$PSScriptRoot
Split-Path $MyInvocation.MyCommand.Definition
Split-Path $MyInvocation.MyCommand.Path
Ha ezt futtatjuk, akkor mindhárom módszer eredményét látjuk:
PS C:\> C:\PSKönyv\psscriptroot.ps1
C:\PSKönyv
C:\PSKönyv
C:\PSKönyv
Ha magára a szkriptünk teljes elérési útjára vagyunk kíváncsiak, azt PowerShell 3.0 utántól a $PSCommandPath változón keresztül is elérhetjük:
function CheckVars {
$PSCommandPath
}
CheckVars
Ha ezt futtatjuk:
PS C:\> C:\PowerShell\CheckNewVars.ps1
C:\PowerShell\CheckNewVars.ps1
Azaz nem kell a $myinvonvocation-el bajlódni, bár ha visszafelé kompatibilis szkripteket szeretnénk írni, kerülnünk kell az újdonságok használatát.
Nézzük kicsit alaposabban meg ezt a $myinvocation változót. Ha interaktívan szeretnénk megnézni, akkor ezt kapjuk:
[1] PS C:\> "kakukk"; $myinvocation
kakukk
MyCommand : "kakukk"; $myinvocation
ScriptLineNumber : 0
OffsetInLine : 0
ScriptName :
Line :
PositionMessage :
InvocationName :
PipelineLength : 2
PipelinePosition : 1
Látszik, hogy a MyCommand property tartalmazza, hogy mi is az éppen futtatott parancs. Mivel ezt ritkán használjuk interaktívan, nézzük meg, hogy egy szkriptből futtatva mit ad. Maga a szkript nagyon egyszerű:
$myinvocation | fl
És a kimenet:
[7] PS C:\> C:\powershell2\scripts\get-mycommand.ps1
MyCommand : get-mycommand.ps1
ScriptLineNumber : 1
OffsetInLine : -2147483648
ScriptName :
Line : C:\powershell2\scripts\get-mycommand.ps1
PositionMessage :
At line:1 char:40
+ C:\powershell2\scripts\get-mycommand.ps1 <<<<
InvocationName : C:\powershell2\scripts\get-mycommand.ps1
PipelineLength : 1
PipelinePosition : 1
Nézzük meg, hogy egy függvényben hogyan alakul ez a változó:
[8] PS C:\> function get-myinvocation {$myinvocation | fl *}
[9] PS C:\> "kakukk" | get-myinvocation
MyCommand : get-myinvocation
ScriptLineNumber : 1
OffsetInLine : -2147483648
ScriptName :
Line : "kakukk" | get-myinvocation
PositionMessage :
At line:1 char:27
+ "kakukk" | get-myinvocation <<<<
InvocationName : get-myinvocation
PipelineLength : 1
PipelinePosition : 1
A fenti példákból látszik, hogy elsősorban a MyCommand és a Line tulajdonság hasznos. Ha csak a szűken vett futtatási környezetre vagyunk kíváncsi, akkor a MyCommand kell nekünk, ha a teljes parancssor, akkor Line.
Ezt felhasználva készítsünk egy olyan függvényt, ami megismétli valahányszor az előtte formailag csővezetékként megadott kifejezést ilyen formában:
kifejezés | repeat-commandline 5
Az ezt megvalósító függvény:
[1] PS C:\> function repeat-commandline ([int] $x = 2)
>> {
>> $s = $myinvocation.line
>> $last = $s.LastIndexOf("|")
>> if ($last -lt 0) {throw "Nincs mit ismételni!"}
>> $cropped = $s.substring(0,$last)
>> for ($r = 0; $r -lt $x; $r++)
>> {
>> invoke-expression $cropped
>> }
>> }
>>
[2] PS C:\> "többször" | repeat-commandline 5
többször
többször
többször
többször
többször
A függvény lényegi része a $myinvocation automatikus változó Line tulajdonságának felhasználása. Nekünk itt a teljes parancssor kell, így a Line tulajdonságot használom. Megkeresem az utolsó csőjelet, (ha nincs ilyen benne, akkor a LastIndexOf metódus eredménye -1 lesz, ilyenkor hibát jelzek) és csonkolom odáig a kifejezést, majd egy ciklussal végrehajtom ezt annyiszor, amennyi a függvénynek átadott paraméter.