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.
katalog stricte sieciowy
w jaki sposób 'dostarczyć’ jakiś katalog na kilka serwerów/VDI? łatwym i popularnym sposobem jest wykorzystanie Azure Files i po prostu podmapować dysk sieciowy.
takie rozwiązanie ma (potencjalnie) kilka wad. potencjalnie – ponieważ wszystko zależy od założeń po co i dla kogo to robimy:
chodziło o dostarczenie wspólnego repozytorium, dostępnego na wszystkich VDI. można taki efekt osiągnąć mapując udział sieciowy jako katalog.
potrzebny do tego jest Storage Account wspierający Azure Files, dodany do domeny. jest to konieczne ze względu na możliwość uwierzytelnienia. a kiedy mamy już założony Azure Files, wystarczy utworzyć w systemie katalog, który będzie na niego wskazywał:
as simple as that. i mamy katalog, który zachowuje się i wygląda (niemal) jak normalny katalog. nie trzeba już polis GPO ponieważ Windows już nie traktuje tego jako dysq sieciowego a zwykły katalog lokalny.
dodam do tego kilka uwag i ciekawostek:
\\wfilesrepo.file.core.windows.net\repo\wlasciwykatalog – i wyłączyć dziedziczenie na poziomie 'wlasiciwykatalog’
albo
c:\root\[repo] – gdzie [repo] to symlink – i wyłączyć dziedziczenie na poziomie 'root’…
to problemu nie będzie. tylko jest wtedy bzdurny dodatkowy katalog /:
mimo pewnych dziwnych zachowań sprawdza się fajnie a dla użytkowników wygląda i zachowuje się jak folder lokalny.
eN.