Wszystko, co chciałeś wiedzieć o aplikacji ShouldProcess

Funkcje programu PowerShell mają kilka funkcji, które znacznie usprawniają sposób interakcji użytkowników z nimi. Jedną z ważnych funkcji, która jest często pomijana, jest -WhatIf obsługa i -Confirm łatwo jest dodać ją do funkcji. W tym artykule szczegółowo dowiesz się, jak zaimplementować tę funkcję.

Uwaga

Oryginalna wersja tego artykułu pojawiła się na blogu napisanym przez @KevinMarquette. Zespół programu PowerShell dziękuje Kevinowi za udostępnienie tej zawartości nam. Zapoznaj się ze swoim blogiem na PowerShellExplained.com.

Jest to prosta funkcja, którą można włączyć w funkcjach w celu zapewnienia sieci bezpieczeństwa dla użytkowników, którzy jej potrzebują. Nie ma nic przerażającego niż uruchomienie polecenia, które wiesz, że może być niebezpieczne po raz pierwszy. Opcja jej uruchamiania -WhatIf może mieć dużą różnicę.

Typowe parametry

Zanim przyjrzymy się implementacji tych typowych parametrów, chcę szybko przyjrzeć się sposobom ich użycia.

Korzystanie z parametru -WhatIf

Gdy polecenie obsługuje -WhatIf parametr , pozwala zobaczyć, co zrobiłoby to polecenie zamiast wprowadzać zmiany. Jest to dobry sposób na przetestowanie wpływu polecenia, zwłaszcza przed wykonaniem czegoś destrukcyjnego.

PS C:\temp> Get-ChildItem
    Directory: C:\temp
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         4/19/2021   8:59 AM              0 importantfile.txt
-a----         4/19/2021   8:58 AM              0 myfile1.txt
-a----         4/19/2021   8:59 AM              0 myfile2.txt

PS C:\temp> Remove-Item -Path .\myfile1.txt -WhatIf
What if: Performing the operation "Remove File" on target "C:\Temp\myfile1.txt".

Jeśli polecenie poprawnie implementuje ShouldProcesspolecenie , powinno zostać wyświetlone wszystkie wprowadzone zmiany. Oto przykład użycia symbolu wieloznakowego do usunięcia wielu plików.

PS C:\temp> Remove-Item -Path * -WhatIf
What if: Performing the operation "Remove File" on target "C:\Temp\myfile1.txt".
What if: Performing the operation "Remove File" on target "C:\Temp\myfile2.txt".
What if: Performing the operation "Remove File" on target "C:\Temp\importantfile.txt".

Korzystanie z opcji -Confirm

Polecenia, które obsługują -WhatIf również obsługę -Confirmpolecenia . Daje to prawdopodobieństwo potwierdzenia akcji przed wykonaniem.

PS C:\temp> Remove-Item .\myfile1.txt -Confirm

Confirm
Are you sure you want to perform this action?
Performing the operation "Remove File" on target "C:\Temp\myfile1.txt".
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"):

W takim przypadku masz wiele opcji, które umożliwiają kontynuowanie, pomijanie zmiany lub zatrzymywanie skryptu. W wierszu pomocy opisano każdą z tych opcji w następujący sposób.

Y - Continue with only the next step of the operation.
A - Continue with all the steps of the operation.
N - Skip this operation and proceed with the next operation.
L - Skip this operation and all subsequent operations.
S - Pause the current pipeline and return to the command prompt. Type "exit" to resume the pipeline.
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"):

Lokalizacja

Ten monit jest zlokalizowany w programie PowerShell, więc język zmienia się na podstawie języka systemu operacyjnego. Jest to jeszcze jedna rzecz, którą zarządza program PowerShell.

Parametry przełącznika

Poświęćmy chwilę, aby przyjrzeć się sposobom przekazywania wartości do parametru switch. Głównym powodem, dla którego to nazywam, jest to, że często chcesz przekazać wartości parametrów do funkcji, które wywołujesz.

Pierwsze podejście to określona składnia parametrów, która może być używana dla wszystkich parametrów, ale najczęściej jest używana do parametrów przełącznika. Należy określić dwukropek, aby dołączyć wartość do parametru.

Remove-Item -Path:* -WhatIf:$true

Możesz to zrobić za pomocą zmiennej.

$DoWhatIf = $true
Remove-Item -Path * -WhatIf:$DoWhatIf

Drugim podejściem jest użycie tabeli skrótu w celu splatowania wartości.

$RemoveSplat = @{
    Path = '*'
    WhatIf = $true
}
Remove-Item @RemoveSplat

Jeśli dopiero zaczynasz korzystać z tabel skrótów lub przeplatania, mam inny artykuł, który obejmuje wszystko, co chciałeś wiedzieć o tabelach skrótów.

SupportsShouldProcess

Pierwszym krokiem do włączenia -WhatIf i -Confirm obsługi jest określenie SupportsShouldProcess w CmdletBinding funkcji .

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess)]
    param()
    Remove-Item .\myfile1.txt
}

Określając SupportsShouldProcess w ten sposób, możemy teraz wywołać naszą funkcję za pomocą -WhatIf funkcji (lub -Confirm).

PS> Test-ShouldProcess -WhatIf
What if: Performing the operation "Remove File" on target "C:\Temp\myfile1.txt".

Zwróć uwagę, że nie utworzono parametru o nazwie -WhatIf. Określenie SupportsShouldProcess automatycznie powoduje utworzenie go dla nas. Po określeniu parametru w elemecie -WhatIfTest-ShouldProcessniektóre wywoływane elementy również wykonują -WhatIf przetwarzanie.

Ufaj, ale weryfikuj

Istnieje pewne niebezpieczeństwo, że wszystko, co wywołujesz, dziedziczy -WhatIf wartości. W pozostałych przykładach zakładam, że nie działa i jest bardzo wyraźny podczas wykonywania wywołań do innych poleceń. Polecam, że robisz to samo.

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess)]
    param()
    Remove-Item .\myfile1.txt -WhatIf:$WhatIfPreference
}

Będę ponownie niuanse znacznie później, gdy masz lepsze zrozumienie wszystkich utworów w grze.

$PSCmdlet.ShouldProcess

Metoda, która umożliwia zaimplementowanie SupportsShouldProcess , to $PSCmdlet.ShouldProcess. Wywołaj metodę $PSCmdlet.ShouldProcess(...) , aby sprawdzić, czy należy przetworzyć logikę, a program PowerShell zajmie się resztą. Zacznijmy od przykładu:

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess)]
    param()

    $file = Get-ChildItem './myfile1.txt'
    if($PSCmdlet.ShouldProcess($file.Name)){
        $file.Delete()
    }
}

Wywołanie funkcji $PSCmdlet.ShouldProcess($file.name) sprawdzania parametru -WhatIf (i -Confirm ) następnie obsługuje je odpowiednio. ShouldProcess Przyczyny -WhatIf wyprowadzania opisu zmiany i zwracania $false:

PS> Test-ShouldProcess -WhatIf
What if: Performing the operation "Test-ShouldProcess" on target "myfile1.txt".

Wywołanie przy użyciu wstrzymuje -Confirm skrypt i wyświetla użytkownikowi monit o kontynuowanie. Zwraca wartość $true , jeśli użytkownik wybrał pozycję Y.

PS> Test-ShouldProcess -Confirm
Confirm
Are you sure you want to perform this action?
Performing the operation "Test-ShouldProcess" on target "myfile1.txt".
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"):

Niesamowitą $PSCmdlet.ShouldProcess cechą jest to, że podwaja się jako pełne dane wyjściowe. Często polegam na tym podczas implementowania ShouldProcess.

PS> Test-ShouldProcess -Verbose
VERBOSE: Performing the operation "Test-ShouldProcess" on target "myfile1.txt".

Przeciążenia

Istnieje kilka różnych przeciążeń funkcji $PSCmdlet.ShouldProcess z różnymi parametrami dostosowywania komunikatów. W powyższym przykładzie widzieliśmy już pierwszy. Przyjrzyjmy się temu bliżej.

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess)]
    param()

    if($PSCmdlet.ShouldProcess('TARGET')){
        # ...
    }
}

Spowoduje to wygenerowanie danych wyjściowych, które zawierają zarówno nazwę funkcji, jak i docelową (wartość parametru).

What if: Performing the operation "Test-ShouldProcess" on target "TARGET".

Określenie drugiego parametru jako operacji używa wartości operacji zamiast nazwy funkcji w komunikacie.

## $PSCmdlet.ShouldProcess('TARGET','OPERATION')
What if: Performing the operation "OPERATION" on target "TARGET".

Następną opcją jest określenie trzech parametrów w celu pełnego dostosowania komunikatu. Gdy są używane trzy parametry, pierwszy z nich to cały komunikat. Dwa drugie parametry są nadal używane w danych wyjściowych komunikatu -Confirm .

## $PSCmdlet.ShouldProcess('MESSAGE','TARGET','OPERATION')
What if: MESSAGE

Dokumentacja szybkiego parametru

Na wypadek, gdyby przyszedł tutaj tylko dowiedzieć się, jakich parametrów należy użyć, oto krótki przewodnik pokazujący, jak parametry zmieniają komunikat w różnych -WhatIf scenariuszach.

## $PSCmdlet.ShouldProcess('TARGET')
What if: Performing the operation "FUNCTION_NAME" on target "TARGET".

## $PSCmdlet.ShouldProcess('TARGET','OPERATION')
What if: Performing the operation "OPERATION" on target "TARGET".

## $PSCmdlet.ShouldProcess('MESSAGE','TARGET','OPERATION')
What if: MESSAGE

Zwykle używam tego z dwoma parametrami.

ShouldProcessReason

Mamy czwarte przeciążenie, które jest bardziej zaawansowane niż inne. Pozwala to uzyskać przyczynę ShouldProcess wykonania. Dodawaję to tylko tutaj, aby uzyskać kompletność, ponieważ możemy tylko sprawdzić, czy $WhatIfPreference zamiast tego jest $true .

$reason = ''
if($PSCmdlet.ShouldProcess('MESSAGE','TARGET','OPERATION',[ref]$reason)){
    Write-Output "Some Action"
}
$reason

Musimy przekazać zmienną $reason do czwartego parametru jako zmienną referencyjną z parametrem [ref]. ShouldProcess wypełnia $reason wartość None lub WhatIf. Nie powiedziałem, że to było przydatne i nie miałem powodu, aby kiedykolwiek go używać.

Gdzie go umieścić

Użyj polecenia ShouldProcess , aby zwiększyć bezpieczeństwo skryptów. Dlatego używasz go podczas wprowadzania zmian przez skrypty. Lubię umieścić $PSCmdlet.ShouldProcess połączenie tak blisko zmiany, jak to możliwe.

## general logic and variable work
if ($PSCmdlet.ShouldProcess('TARGET','OPERATION')){
    # Change goes here
}

Jeśli przetwarzam kolekcję elementów, wywołaję ją dla każdego elementu. Dlatego wywołanie zostaje umieszczone wewnątrz pętli foreach.

foreach ($node in $collection){
    # general logic and variable work
    if ($PSCmdlet.ShouldProcess($node,'OPERATION')){
        # Change goes here
    }
}

Powodem, dla którego umieszczam ShouldProcess ściśle wokół zmiany, jest to, że chcę wykonać jak najwięcej kodu, jak to możliwe, gdy -WhatIf jest określony. Chcę, aby instalacja i walidacja były uruchamiane, jeśli to możliwe, aby użytkownik mógł zobaczyć te błędy.

Lubię również używać tego w testach Pester, które weryfikują moje projekty. Jeśli mam kawałek logiki, który jest trudny do wyśmiewać w pester, często mogę owinąć go ShouldProcess i nazwać go -WhatIf w moich testach. Lepiej przetestować część kodu niż żaden z nich.

$WhatIfPreference

Pierwszą zmienną preferencji, która mamy, jest $WhatIfPreference. $false Jest to domyślnie. Jeśli ustawisz ją na $true , funkcja jest wykonywana tak, jakby została określona -WhatIf. Jeśli ustawisz to w sesji, wszystkie polecenia wykonują -WhatIf wykonywanie.

Po wywołaniu funkcji za pomocą -WhatIffunkcji wartość $WhatIfPreference zostanie ustawiona na $true wewnątrz zakresu funkcji.

ConfirmImpact

Większość moich przykładów dotyczy -WhatIf , ale do tej pory wszystko działa również z monitem -Confirm użytkownika. Możesz ustawić ConfirmImpact funkcję na wysoką i wyświetlić monit o wywołanie elementu za pomocą polecenia -Confirm.

function Test-ShouldProcess {
    [CmdletBinding(
        SupportsShouldProcess,
        ConfirmImpact = 'High'
    )]
    param()

    if ($PSCmdlet.ShouldProcess('TARGET')){
        Write-Output "Some Action"
    }
}

To wywołanie Test-ShouldProcess powoduje wykonanie -Confirm akcji ze względu na High wpływ.

PS> Test-ShouldProcess

Confirm
Are you sure you want to perform this action?
Performing the operation "Test-ShouldProcess" on target "TARGET".
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"): y
Some Action

Oczywistym problemem jest to, że teraz trudniej jest użyć w innych skryptach bez monitowania użytkownika. W takim przypadku możemy przekazać wartość , $false aby -Confirm pominąć monit.

PS> Test-ShouldProcess -Confirm:$false
Some Action

Omówię sposób dodawania -Force obsługi w późniejszej sekcji.

$ConfirmPreference

$ConfirmPreference to zmienna automatyczna, która steruje monitem ConfirmImpact o potwierdzenie wykonania. Poniżej przedstawiono możliwe wartości dla parametrów $ConfirmPreference i ConfirmImpact.

  • High
  • Medium
  • Low
  • None

Za pomocą tych wartości można określić różne poziomy wpływu dla każdej funkcji. Jeśli ustawiono $ConfirmPreference wartość wyższą niż ConfirmImpact, nie zostanie wyświetlony monit o potwierdzenie wykonania.

Domyślnie $ConfirmPreference jest ustawiona wartość High i ConfirmImpact ma wartość Medium. Jeśli chcesz, aby funkcja automatycznie monitować użytkownika, ustaw wartość ConfirmImpactHigh. W przeciwnym razie ustaw go na Medium wartość , jeśli jego destrukcyjne i użyj Low , jeśli polecenie jest zawsze bezpieczne w środowisku produkcyjnym. Jeśli ustawisz go na nonewartość , nie wyświetla monitu, nawet jeśli -Confirm został określony (ale nadal zapewnia -WhatIf pomoc techniczną).

Podczas wywoływania funkcji za pomocą -Confirmparametru $ConfirmPreference wartość zostaje ustawiona na Low wewnątrz zakresu funkcji.

Pomijanie zagnieżdżonych monitów o potwierdzenie

Element $ConfirmPreference może być odbierany przez wywoływane funkcje. Może to tworzyć scenariusze, w których dodajesz monit o potwierdzenie, a wywołana funkcja również monituje użytkownika.

Mam tendencję do określania -Confirm:$false poleceń, które wywołujem, gdy już obsłużyłem monit.

function Test-ShouldProcess {
    [CmdletBinding(SupportsShouldProcess)]
    param()

    $file = Get-ChildItem './myfile1.txt'
    if($PSCmdlet.ShouldProcess($file.Name)){
        Remove-Item -Path $file.FullName -Confirm:$false
    }
}

Powoduje to powrót do wcześniejszego ostrzeżenia: istnieją niuanse, gdy -WhatIf nie jest przekazywany do funkcji i kiedy -Confirm przekazuje do funkcji. Obiecuję, że wrócę do tego później.

$PSCmdlet.ShouldContinue

Jeśli potrzebujesz większej kontroli niż ShouldProcess zapewnia, możesz wyzwolić monit bezpośrednio za pomocą polecenia ShouldContinue. ShouldContinueIgnoruje $ConfirmPreferencewartości , , ConfirmImpact-Confirm, $WhatIfPreferencei -WhatIf , ponieważ wyświetla monit za każdym razem, gdy jest wykonywany.

Na szybki rzut oka łatwo się mylą ShouldProcess i ShouldContinue. Zwykle pamiętam, aby użyć ShouldProcess , ponieważ parametr jest wywoływany SupportsShouldProcess w pliku CmdletBinding. Należy używać ShouldProcess w niemal każdym scenariuszu. Dlatego najpierw omówiłem tę metodę.

Przyjrzyjmy ShouldContinue się w działaniu.

function Test-ShouldContinue {
    [CmdletBinding()]
    param()

    if($PSCmdlet.ShouldContinue('TARGET','OPERATION')){
        Write-Output "Some Action"
    }
}

Zapewnia to prostszy monit z mniejszą liczbą opcji.

Test-ShouldContinue

Second
TARGET
[Y] Yes  [N] No  [S] Suspend  [?] Help (default is "Y"):

Największym problemem jest ShouldContinue to, że wymaga, aby użytkownik uruchamiał go interaktywnie, ponieważ zawsze monituje użytkownika. Zawsze należy tworzyć narzędzia, które mogą być używane przez inne skrypty. W ten sposób należy zaimplementować element -Force. Wrócę do tego pomysłu później.

Tak dla wszystkich

Jest to obsługiwane automatycznie, ShouldProcess ale musimy wykonać nieco więcej pracy dla programu ShouldContinue. Istnieje drugie przeciążenie metody, w której musimy przekazać kilka wartości, odwołując się do kontroli logiki.

function Test-ShouldContinue {
    [CmdletBinding()]
    param()

    $collection = 1..5
    $yesToAll = $false
    $noToAll = $false

    foreach($target in $collection) {

        $continue = $PSCmdlet.ShouldContinue(
                "TARGET_$target",
                'OPERATION',
                [ref]$yesToAll,
                [ref]$noToAll
            )

        if ($continue){
            Write-Output "Some Action [$target]"
        }
    }
}

Dodano pętlę foreach i kolekcję, aby pokazać ją w akcji. Wyciągnąłem ShouldContinue wezwanie z if oświadczenia, aby ułatwić czytanie. Wywołanie metody z czterema parametrami zaczyna się trochę brzydkie, ale starałem się, aby wyglądało to tak czyste, jak mogłem.

Implementowanie -Force

ShouldProcess i ShouldContinue muszą implementować -Force na różne sposoby. Sztuczką dla tych implementacji jest to, że ShouldProcess zawsze powinny być wykonywane, ale ShouldContinue nie powinny być wykonywane, jeśli -Force jest określony.

ShouldProcess - Force

Jeśli ustawisz ConfirmImpacthighwartość na , pierwszą rzeczą, którą użytkownicy spróbują, jest pominięcie go za pomocą polecenia -Force. To pierwsza rzecz, którą mimo to robię.

Test-ShouldProcess -Force
Error: Test-ShouldProcess: A parameter cannot be found that matches parameter name 'force'.

Jeśli pamiętasz z ConfirmImpact sekcji, faktycznie muszą wywołać ją w następujący sposób:

Test-ShouldProcess -Confirm:$false

Nie każdy zdaje sobie sprawę, że musi to zrobić i -Force nie pomija ShouldContinue. Dlatego powinniśmy zaimplementować -Force zasady rozsądku naszych użytkowników. Zapoznaj się z tym pełnym przykładem tutaj:

function Test-ShouldProcess {
    [CmdletBinding(
        SupportsShouldProcess,
        ConfirmImpact = 'High'
    )]
    param(
        [Switch]$Force
    )

    if ($Force -and -not $Confirm){
        $ConfirmPreference = 'None'
    }

    if ($PSCmdlet.ShouldProcess('TARGET')){
        Write-Output "Some Action"
    }
}

Dodamy własny -Force przełącznik jako parametr. Parametr -Confirm jest automatycznie dodawany podczas używania SupportsShouldProcess w elem.CmdletBinding

[CmdletBinding(
    SupportsShouldProcess,
    ConfirmImpact = 'High'
)]
param(
    [Switch]$Force
)

Tutaj skupiamy się na logice -Force :

if ($Force -and -not $Confirm){
    $ConfirmPreference = 'None'
}

Jeśli użytkownik określi wartość , chcemy -Forcepominąć monit o potwierdzenie, chyba że określi również wartość -Confirm. Dzięki temu użytkownik może wymusić zmianę, ale nadal potwierdzić zmianę. Następnie ustawiliśmy $ConfirmPreference zakres lokalny. Teraz, używając parametru -Force tymczasowo ustawia $ConfirmPreference wartość na brak, wyłączając monit o potwierdzenie.

if ($PSCmdlet.ShouldProcess('TARGET')){
        Write-Output "Some Action"
    }

Jeśli ktoś określi wartości i -Force-WhatIf, -WhatIf musi mieć priorytet. Takie podejście zachowuje -WhatIf przetwarzanie, ponieważ ShouldProcess zawsze jest wykonywane.

Nie należy dodawać sprawdzania $Force wartości wewnątrz instrukcji if za pomocą .ShouldProcess Jest to antywzór dla tego konkretnego scenariusza, mimo że to, co pokazujem w następnej sekcji dla .ShouldContinue

ShouldContinue - Force

Jest to prawidłowy sposób implementacji -Force za pomocą ShouldContinuepolecenia .

function Test-ShouldContinue {
    [CmdletBinding()]
    param(
        [Switch]$Force
    )

    if($Force -or $PSCmdlet.ShouldContinue('TARGET','OPERATION')){
        Write-Output "Some Action"
    }
}

Umieszczając element z $Force lewej -or strony operatora, zostaje on oceniony jako pierwszy. Napisanie go w ten sposób powoduje zwarcie wykonania instrukcji if . Jeśli $force parametr ma $truewartość , ShouldContinue parametr nie jest wykonywany.

PS> Test-ShouldContinue -Force
Some Action

Nie musimy się martwić -Confirm ani -WhatIf w tym scenariuszu, ponieważ nie są one obsługiwane przez ShouldContinueusługę . Dlatego musi być obsługiwana inaczej niż ShouldProcess.

Problemy z zakresem

Używanie elementów -WhatIf i -Confirm mają mieć zastosowanie do wszystkiego wewnątrz funkcji i wszystkiego, co wywołuje. W tym celu należy ustawić $true$WhatIfPreference wartość lub ustawić Low wartość $ConfirmPreference w lokalnym zakresie funkcji. Wywołanie innej funkcji w celu ShouldProcess użycia tych wartości.

To działa poprawnie przez większość czasu. Za każdym razem, gdy wywołujesz wbudowane polecenie cmdlet lub funkcję w tym samym zakresie, działa. Działa również podczas wywoływania skryptu lub funkcji w module skryptu z konsoli programu .

Jednym konkretnym miejscem, w którym nie działa, jest to, gdy skrypt lub moduł skryptu wywołuje funkcję w innym module skryptu. Może to nie wydawać się dużym problemem, ale większość modułów utworzonych lub ściągniętych z programu PSGallery to moduły skryptów.

Podstawowym problemem jest to, że moduły skryptów nie dziedziczą wartości dla $WhatIfPreference lub $ConfirmPreference (i kilku innych), gdy są wywoływane z funkcji w innych modułach skryptu.

Najlepszym sposobem, aby podsumować to jako ogólną regułę jest to, że działa to poprawnie w przypadku modułów binarnych i nigdy nie ufaj, że działa w przypadku modułów skryptów. Jeśli nie masz pewności, przetestuj go lub po prostu załóżmy, że nie działa poprawnie.

Osobiście czuję, że jest to bardzo niebezpieczne, ponieważ tworzy scenariusze, w których dodajesz -WhatIf obsługę wielu modułów, które działają poprawnie w izolacji, ale nie działają poprawnie, gdy się dzwonią.

Pracujemy nad rozwiązaniem tego problemu w usłudze GitHub RFC. Aby uzyskać więcej szczegółów, zobacz Propagacja preferencji wykonywania poza zakresem modułu skryptu.

Zamykanie

Muszę wyszukać, jak używać ShouldProcess za każdym razem, gdy muszę go używać. To zajęło mi dużo czasu, aby odróżnić ShouldProcess się od ShouldContinue. Prawie zawsze muszę wyszukać, jakich parametrów użyć. Więc nie martw się, jeśli nadal masz zdezorientowany od czasu do czasu. Ten artykuł będzie tutaj, gdy będzie potrzebny. Jestem pewien, że będę się do niego często odwoływać.

Jeśli podoba Ci się ten post, podziel się swoimi przemyśleniami ze mną na Twitterze, korzystając z poniższego linku. Zawsze lubię słyszeć od ludzi, którzy otrzymują wartość z mojej treści.