długi wstęp o obiektach
kolejna zabawka edukacyjna (po antiidle) tym razem może bardziej praktyczna – wielopoziomowe menu textowe. chociaż ostatnio sporo czasu poświęciłem na pisanie GUI, nadal uważam, że skrypty powinny zostać w linii poleceń.
scenariusz prosty – zrobić coś, co pozwoli uży-adminowi w łatwy sposób wybrać konkretną opcję *z drzewa wyborów*. jednopoziomowych menu można znaleźć od groma, niektóre nawet całkiem sprytne, ale wielopoziomowe musiałem napisać po swojemu. a przy okazji odświeżyłem sobie lekcje ze Ś.P. Janem Bieleckim – ponieważ to na zajęciach z C++ miałem wykłady o klasach.
w początkowych wersjach PowerShell pojęcia klasy nie było – ponieważ PS jest w pełni obiektowy, tworzenie własnych obiektów, rozszerzanie ich i wykorzystanie jest 'natywnie proste’ – czyli jest tak bardzo integralną częścią PS, że używanie obiektów jest hiperproste. ot najprostsze utworzenie obiektu:
C:\ :))o- $person = [pscustomobject]@{gn='John';sn='Smith';age='23'}
C:\ :))o- $person
gn sn age
-- -- ---
John Smith 23
obiekty można oczywiście bez problemu zagnieżdżać. ot mikrospołeczność z dwóch osób:
C:\ :))o- $community = [pscustomobject]@{ citizens=@($person) }
C:\ :))o- $community.citizens += [pscustomobject]@{gn='Joana';sn='DArc';age='19'}
C:\ :))o- $community.citizens
gn sn age
-- -- ---
John Smith 23
Joana DArc 19
C:\ :))o- $community|gm
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
citizens NoteProperty Object[] citizens=System.Object[]
być może to z tego powodu klas długo nie było? w dużym uproszczeniu klasa jest definicją własnego obiektu. aż tak nie będę się rozpisywał, bo teorii o klasach jest wszędzie pełno – a przypadq PS polecam MS docs. pojawiły się dopiero w wersji PS 5.o. skoro można tak łatwo operować obiektami, i od zawsze (?) były Enumy, to po co dodatkowo klasy??
lubię o obiektach myśleć jak o bakteriach. mogą być malutkie i statyczne, ale są również całkiem duże i złożone, z dużą ilością różnych funkcji – np. mogą się poruszać, coś przetworzyć… są 'żywymi obiektami’… no dobra… w natywny PSowy sposób również można ożywić obiekt:
C:\ :))o- $person | add-member -MemberType ScriptMethod -Name 'tellYourName' `
-Value { "my name is $($this.gn) $($this.sn) and I'm $($this.age) old" }
C:\ :))o- $person.tellYourName()
my name is John Smith and I'm 23 old
…ale klasy nadal mają pewne przewagi. taka wersja 'pro’ dla obiektów.
- po pierwsze są i mają nazwę. wszystkie obiekty pscustomobject są… obiektami typu pscustomobject – bez względu na to, co opisują i co robią. to już samo w sobie czasem stanowi ograniczenie. część logiki można oprzeć właśnie na tym, jaki obiekt przetwarzamy.
- wynikająca z tego faktu powtarzalność – gwarancja, że dane obiekty, mają konkretny typ, gwarantujący te same atrybuty i metody i przewidywalność zachowania. bez klasy – obiekt za każdym razem trzeba tworzyć od nowa.
- mają konstruktory. konstruktory dodają dodatkowe funkcje życia, już w okresie prenatalnym obiektu (; dodatkowo uzupełniają 'przewidywalność’, potrafiąc w odpowiedni sposób odpowiednia zainicjować zmienne oraz nałożyć na nie restrykcje [np. żeby imię było obligatoryjnie podane i było stringiem, a wiek, żeby nie był ujemny]
back to class
najlepiej pokazać sqteczność klasy na przykładzie. skoro to ma być menu 'z drzewem wyborów’, to trzeba zaimplementować drzewo. drzewo ma korzeń, gałęzie i liście. te są ze sobą powiązane i trzeba móc przemieszczać się z gałęzi na gałąź/liść i z powrotem. czyli trzeba przechowywać wskazanie na poprzedni obiekt i kolejny. przy czym korzeń i liść różnią się od gałęzi tym, że korzeń nie ma poprzedniego [jest pierwszy] a liść kolejnego [terminujący] obiektu.
tutaj 'gałęzią’ jest lista menu danego poziomu typu:
moja lista
wybór 1
wybór 2
dodatkowo widać, że menu powinno mieć również tytuł. ponieważ to jest pojedynczy poziom, brakuje opcji 'wróć’, która również będzie potrzebna do nawigowania po 'ekranach menu’. i tak powstała klasa:
class MenuLevel {
[string]$menuPrompt
[string]$name
[MenuLevel]$previousLevel
[MenuLevel[]]$nextLevel
MenuLevel() {
}
MenuLevel(
[string]$title
) {
$this.menuPrompt = $title
$this.nextLevel += [MenuLevel]@{ name = 'exit' }
}
#add additional menu level with subitems
[MenuLevel[]] addMenuLevel([string]$name,[string]$prompt) {
$nLevel = [MenuLevel]::new()
$nLevel.name = $name
$nLevel.menuPrompt = $prompt
$nLevel.previousLevel += $this
$this.nextLevel += $nLevel
$back = [MenuLevel]::new()
$back.name = 'back'
$nLevel.nextLevel += $back
return $nLevel
}
#add leaf - for actual choice and execution
[void] addLeafItem([string]$name) {
$leaf = [MenuLevel]::new()
$leaf.name = $name
$leaf.previousLevel += $this
$this.nextLevel += $leaf
}
#print menu items from current level
[string[]] getMenuItems(){
if($this.nextLevel) {
return $this.nextLevel.name
} else {
return $null
}
}
}
dzięki temu, że klasa definiuje nowy typ obiektu, nowe menu (korzeń) można zainicjować w prosty sposób:
$mainMenu = [MenuLevel]::new('select option')
obiekt ma dwa konstruktory. jest konstruktor prosty, który po prostu inicjuje obiekt oraz konstruktor przyjmujący parametr – ten wykorzystany powyżej. co się dzieje w takim przypadq? to właśnie utworzenie 'korzenia’:
MenuLevel(
[string]$title
) {
$this.menuPrompt = $title
$this.nextLevel += [MenuLevel]@{ name = 'exit' }
}
text jest ustawiany jako tytuł menu, i dodawana jest opcja 'exit’. czyli najprostsze menu, tuż po inicjalizacji, składa się z tytułu i opcji 'exit’.
jeśli chcemy dodać gałąź, wykorzystamy metodę 'addMenuLevel’, która tworzy nową 'gałąź’ – de facto nowy obiekt typu 'menuLevel’. dzieją się tu 3 rzeczy:
- utworzenie nowego obiektu menuLevel – czyli płaskiej listy opcji z tytułem
- tworzony jest pusty obiekt menuLevel 'back’ – posłuży do zareagowania i wyświetlenia poprzedniej gałęzi
- atrybut 'previousLevel’ ustawiony jest na obiekt wywołujący tą funkcję (relacja korzeń-gałąź) – będzie wybrany w przypadq opcji 'back’
przykładowe menu:
$mainMenu = [MenuLevel]::new('select option')
$mainMenu.addLeafItem('terminating option') #terminating option
$l2_1 = $mainMenu.addMenuLevel('submenu options 1', 'SUBMENU L2') #adding 2nd level menu
$l3_1 = $l2_1.addMenuLevel('submenu option 2', 'SUBMENU L3') #adding next submenu - 3rd level
$l2_1.addLeafItem('terminating option 2_1') #add terminating option to L2 menu
$l2_1.addLeafItem('terminating option 2_2') #add terminating option to L2 menu
$l3_1.addLeafItem('terminate op 3_1')
$l3_1.addLeafItem('terminate op 3_2')
$choice = Get-MenuSelection $mainMenu
switch($choice) {
'terminating option' { write-host "some logic there for option from main menu" }
'terminating option 2_1' { write-host "some logic there for option 1 from L2 menu" }
'terminating option 2_2' { write-host "some logic there for option 2 from L2 menu"}
'terminate op 3_1' { write-host "some logic there for option 1 from L3 menu" }
'terminate op 3_2' { write-host "some logic there for option 2 from L3 menu" }
default { write-host -ForegroundColor red "UNKNOWN OPTION" }
}
dalej nie będę się rozpisywał – pełny kod wraz z 'silnikiem’ wyświetlającym menu,
można zassać z GH. i choć dałoby się to napisać bez klasy – tak jest bardziej sexy (=
eN.