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 } } }
$mainMenu = [MenuLevel]::new('select option')
MenuLevel( [string]$title ) { $this.menuPrompt = $title $this.nextLevel += [MenuLevel]@{ name = 'exit' } }
- 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" } }