Triki z PowerShellem #7 – Zliczanie wierszyGdy zawzięcie i wytrwale tworzymy jakiś koderski projekt, chciałoby się od czasu do czasu zmierzyć, ile to już pracy zdołaliśmy w nim wykonać. Pewnym sposobem na to jest policzenie ilości wszystkich linii kodu, które udało nam się już napisać.
Istnieją do tego nawet oddzielne aplikacje, niekiedy z rozbudowanym interfejsem graficznym. Jednak takie proste zadanie można by przecież zrealizować przy pomocy równie prostego narzędzia. Jakiego? Oczywiście - niewielkiego skryptu PowerShella :) Chociażby takiego jak ten:
# Parametr: ścieżka do katalogu z plikami
param ([string]$path = ".")
# Stałe
$EXTENSIONS = @("cpp", "h", "hpp") # Rozszerzenia interesujących plików
# Zmienne
$filesCount = 0
$totalLinesCount = 0
$emptyLinesCount = 0
# Przeszukujemy podany katalog rekurencyjnie (wszerz)
$dirQueue = New-Object Collections.Queue
$dirQueue.Enqueue($path)
while ($dirQueue.Count -gt 0)
{
# Dodajemy podkatalogi do przeszukania
$dir = [string]$dirQueue.Dequeue()
foreach ($subDir in [IO.Directory]::GetDirectories($dir))
{ $dirQueue.Enqueue($subDir) }
# Pobieramy wszystkie pliki w aktualnym katalogu pasujące do filtra
$files = New-Object Collections.ArrayList
foreach ($ext in $EXTENSIONS)
{ $files.AddRange([IO.Directory]::GetFiles($dir, "*." + $ext)) }
# Liczymy w nich wiersze
foreach ($file in $files)
{
++$filesCount
foreach ($line in [IO.File]::ReadAllLines($file))
{
++$totalLinesCount
if ($line.Trim().Length -eq 0) { ++$emptyLinesCount }
}
}
}
# Wyświetlamy statystyki
"Files: " + $filesCount | Out-Host
"Total lines: " + $totalLinesCount | Out-Host
"Non-empty lines: " + ($totalLinesCount - $emptyLinesCount) | Out-Host
Podajemy mu tylko ścieżkę do katalogu z naszym kodem, a on przeglądnie wszystkie pliki z ustalonymi rozszerzeniami i wyświetli statystykę z liczbą linii: wszystkich oraz niepustych. Cała ta czynność ta nie zajmuje w kodzie specjalnie dużo miejsca, gdyż najważniejsze jej elementy: pobranie podkatalogów, plików w katalogu i wierszy tekstu w pliku dają się zrobić pojedynczymi wywołaniami funkcji (odpowiednio: GetDirectories i GetFiles z System.IO.Directory oraz System.IO.File.ReadAllLines). To aczkolwiek zasługa nie samego PowerShella, tylko niezawodnej platformy .NET ;)
Skrypt ten można rzecz jasna znacznie usprawnić, pozwalając chociażby na definiowanie filtra nazw plików z kodem w jego wywołaniu czy też umożliwiając wykluczanie niektórych katalogów (np. bin, Debug, Release) w celu przyspieszenia zliczania. Jeśli ktoś miałby na to ochotę, może się dodawaniem takich feature'ów pobawić :)
Kolorowanie semantyczneW dzisiejszych czasach niemal każdy edytor plików tekstowych nieco bardziej zaawansowany od windowsowego Notatnika oferuje funkcję kolorowania składni (syntax highlighting) przynajmniej kilku najpopularniejszych języków programowania. W przypadku całych IDE jest to oczywiście funkcjonalność absolutnie oczywista i niezbędna, jednak coraz więcej środowisk oferuje też coś więcej. Coś, co dla odróżnienia nazywam kolorowaniem semantycznym; nie jestem bowiem pewien, czy ten mechanizm ma jakąś ogólnie przyjętą nazwę.

Kodowanie semantyczne w Visual Studio z pluginem AssistX
O co w nim chodzi? Standardowe kolorowanie potrafi między innymi odróżnić słowa kluczowe od identyfikatorów, wyróżniając zwykle te pierwsze innym stylem czcionki, zaś te drugie pozostawiając bez zmian. Wszystkie identyfikatory: nazwy zmiennych, funkcji, typów, stałych, szablonów, itp., są więc formatowane tak samo i wyglądają identycznie.
Trik kolorowania semantycznego polega właśnie na rozróżnieniu tych wszystkich kategorii identyfikatorów. Oczywiście jest to możliwe tylko wtedy, gdy IDE dysponuje bazą danych o całej strukturze projektu. Ale taka baza jest przecież coraz częściej tworzona, tyle że w innym celu: zapewnienia automatycznego uzupełniania poprzez mechanizmy w rodzaju IntelliSense w Visual Studio. Na szczęście ktoś kreatywny wpadł na pomysł, że można ją wykorzystać także w inny sposób - i chwała mu za to :)
Kolorowanie semantyczne ma przynajmniej dwie zalety. Ułatwia ono zorientowanie się w przeglądanym kodzie, zwłaszcza takim który widzimy po raz pierwszy. Ponadto zaś pozwala ono na wczesne wykrycie prostych acz uciążliwych błędów w rodzaju literówek. Sprawiają one bowiem, że dany identyfikator - jako nieznany - jest kolorowany inaczej, co pozwala na łatwiejsze dostrzeżenie pomyłki.
A wady? Jak każdy mechanizm działający w tle, kolorowanie semantyczne zjada trochę zasobów systemowych - m.in. dla niego opłaca się mieć w procesorze więcej niż jeden rdzeń. A poza tym można zwyczajnie nie lubić pstrokatego kodu i preferować bardziej jednolitą kolorystykę... O gustach się przecież nie dyskutuje, a teoretycznie kodować można i w Notatniku :)
Triki z PowerShellem #6 – Mail z załącznikamiKiedy mamy komuś przesłać plik, możemy niekiedy użyć do tego zwykłej poczty e-mail. Nie jest to oczywiście możliwe zawsze: plik nie powinien być zbyt duży i zwykle nie może też należeć do żadnego z "niebezpiecznych" typów (np. aplikacji EXE), by serwery pocztowe mogły go przepchnąć bez narażania lub zatykania sieci.
Jeśli w naszym przypadku tak jest, to możemy wykonać całą operację na przykład przy pomocy poniższego skryptu PowerShella:
# Stałe
$SERVER = "moj.server.pl"
$LOGIN = "loginSmtp"
$PASSWORD = "hasłoSmtp"
$FROM_ADDRESS = "ja@server.pl"
# Odczytanie adresu docelowego
$recipentAddress = Read-Host -Prompt "Recipent e-mail address"
$recipent = New-Object Net.Mail.MailAddress @($recipentAddress)
# Złożenie maila
$mail = New-Object Net.Mail.MailMessage
$mail.From = New-Object Net.Mail.MailAddress @($FROM_ADDRESS)
$mail.To.Add($recipent)
$mail.Subject = (New-Object IO.FileInfo @($file)).Name
# Dodanie załącznika
$attachment = New-Object Net.Mail.Attachment @($file,
[Net.Mime.MediaTypeNames+Application]::Octet)
$cd = $attachment.ContentDisposition
$cd.CreationDate = [IO.File]::GetCreationTime($file)
$cd.ModificationDate = [IO.File]::GetLastWriteTime($file)
$cd.ReadDate = [IO.File]::GetLastAccessTime($file)
$mail.Attachments.Add($attachment)
# Łączenie z serwerem SMTP i wysłanie maila
$smtp = New-Object Net.Mail.SmtpClient @($SERVER)
$smtp.Credentials = New-Object Net.NetworkCredential @($LOGIN, $PASSWORD)
# $smtp.EnableSsl = $true # Odkomentowujemy, jeśli serwer wymaga SSL
$smtp.Send($mail)
Jak widać, nie jest to nic skomplikowanego. A jak wykorzystać go w praktyce?... Podobnie jak prezentowany wcześniej skrypt do uploadu na serwer FTP, można go wywołać komendą w rodzaju:
którą podpinamy do menu kontekstowego plików lub podmenu Wyślij do.
W ten prosty sposób możemy oszczędzić sobie uruchamiania całego klienta poczty, względnie przeglądarki z webmailem. Naturalnie sporo rzeczy - jak chociażby temat wysyłanej wiadomości czy jej treść - możemy w zaprezentowanym kawałku kodu zmienić lub uzupełnić.
Triki z PowerShellem #5 – SkrótyPisząc skrypty w PowerShellu mamy dostęp do niemal całej platformy .NET, zawierającej grubo ponad dziesięć tysięcy klas, dlatego nieczęsto będziemy potrzebowali czegoś spoza tego wielkiego bogactwa. Ale takie przypadki też istnieją. Na szczęście PSh umożliwia też dostęp do innych narzędzi niż .NET - jak choćby do starych (nie)dobrych obiektów COM.
Przy ich pomocy można zrobić całkiem sporo, zwłaszcza jeśli chodzi o sterowanie aplikacjami, które udostępniają na zewnątrz odpowiednie interfejsy (a robi tak wiele większych programów, nie tylko Microsoftu). Ciekawsze jest chyba jednak łączenie technologii: "starego" COM-a i nowego .NET-a, aby razem zrobić coś użytecznego. Oto prosty przykład:
# Parametr: ścieżka do przeszukiwanego katalogu
param([string]$path = ".")
# Szukamy plików skrótów
$allLinks = [IO.Directory]::GetFiles($path, "*.lnk", [IO.SearchOption]::AllDirectories)
# Przeglądamy listę i szukamy nieistniejących
$wsh = New-Object -ComObject WScript.Shell
$invalidLinks = New-Object Collections.ArrayList
foreach ($linkFile in $allLinks)
{
$lnk = $wsh.CreateShortcut($linkFile)
if (-not ([IO.File]::Exists($lnk.TargetPath) -or [IO.Directory]::Exists($lnk.TargetPath)))
{ $invalidLinks.Add($linkFile) | Out-Null }
}
return [string[]]$invalidLinks.ToArray()
Dziwnym trafem najprostsze przykłady użycia COM są zwykle związane z tworzeniem skrótów (plików .lnk) przy pomocy metody CreateShortcut obiektu COM o identyfikatorze WScript.Shell. Obiekt ten był dawniej używany w skryptach hosta systemu Windows (Windows Scripting Host) i, jak widać, nadal zdarza mu się być użytecznym ;-)
Co jednak robi powyższy skrypt? Otóż nie tworzy żadnych nowych skrótów, a wręcz przeciwnie: wskazuje kandydatów do usunięcia, czyli skróty pokazujące na nieistniejące pliki lub katalogi. Na wyjściu dostajemy ich zgrabną listę, którą możemy przekierować na przykład do polecenia Remove-Item.
A tak możemy sobie wyczyścić Menu Start:
I pomyśleć, że kiedyś napisałem do tego cały osobny program... :)
Debugowanie preprocesoraPreprocesor w C++ to może i przestarzała, ale i całkiem fajna zabawka. Przynajmniej czasami - mimo dość ubogiej logiki - pozwala ona załatać choć niektóre braki języka. A niekiedy pozwala też na pewne sztuczki, jakie ciężko byłoby osiągnąć inaczej. W końcu, można go też używać do tego, w czym jest najlepszy: do automatycznego wklejania powtarzających się fragmentów kodu w wielu miejscach.
Ponieważ jednak preprocesor działa na kodzie jak na tekście, to rezultaty mogą być czasami zadziwiające - zwłaszcza dla kompilatora. Może on nam wówczas pokazać błąd w linijce, która jest całkowicie poprawna... Ale tylko z dokładnością do zawartego w niej makra, które po rozwinięciu generuje błąd składniowy lub semantyczny. Weźmy chociażby chyba najprostszy przypadek tego typu:
W nim łatwo o wykrycie pomyłki, lecz w rzeczywistych sytuacjach może to być zadanie nie lada. Zwłaszcza wtedy, gdy użyte makro jest skomplikowane, a my przecież nie możemy przełączyć preprocesor w tryb pracy krokowej i śledzić, jak jest ono rozwijane.
Możemy jednak przynajmniej podejrzeć ostateczne rezultaty. Wprawdzie domyślne każdy plik źródłowy po przetworzeniu przez preprocesor trafia od razu do kompilatora, jednak zachowanie to można łatwo zmienić. Możliwe jest na przykład zrzucenie wstępnie przetworzonego źródła do pliku, który to możemy potem przejrzeć i zobaczyć, w co ostatecznie nasze makra się zmieniły.
W Visual C++ takie zachowanie ustawia się we właściwościach projektu, na zakładce Configuration Properties > C/C++ > Preprocessor. Tam możemy wybrać, czy chcemy, by rezultaty działania preprocesora na plikach .cpp były zapisywane do osobnych plików (z rozszerzeniem .i). Wachlarz opcji umożliwia też określenie, czy mają być do nich dołączane numery linii lub komentarze.

Włączenie tych opcji bywa przydatne, jeśli dostajemy na linijkach kodu z użyciem makr dostajemy komunikaty o błędach kompilatora, które są zupełnie "z innej bajki". Ponadto w ten sposób można też zobaczyć, jakie fragmenty kodu są kierowane do kompilacji za pomocą dyrektyw typu #ifdef, co też bywa przydatne.
Pamiętajmy jednak, że wygenerowane w ten sposób pliki .i są prawie zawsze bardzo, bardzo duże - rzędu kilku megabajtów - gdyż zawierają treść wszystkich dołączonych nagłówków. Dlatego, ze względu na szybkość budowania projektu, nie powinniśmy ich generować poza przypadkami debugowania preprocesora.
Lis w operze
Parę dni temu odbyła się premiera nowej wersji przeglądarki Opera, oznaczonej numerkiem 9.5. Z kolei jutro z wielką pompą opublikowana zostanie trzecia edycja Firefoksa. Przy tej zresztą okazji Mozilla organizuje Dzień Pobierania, z zamiarem ustanowienia rekordu ilości ściągnięć oprogramowania w ciągu jednej doby. (Mam aczkolwiek przeczucie, że prawdopodobne jest ustanowienie innego rekordu: najdłużej niedziałającego serwera pobierania ;]).
Zbieżność tych dwóch wydarzeń zapewne nie jest przypadkowa. Tym bardziej dziwi mnie to, że przy tej okazji zaprzysięgli fani któregoś z tych dwóch programów zajmują się głównie wykazywaniem w każdym możliwym miejscu, iż to właśnie ich ulubiona przeglądarka jest lepsza od tej drugiej. Naturalnie, argumentów jest przy tym mnóstwo: a to gesty myszki, a to współdzielenie zakładek i Ulubionych między komputerami, nie wspominając o tysiącach dostępnych rozszerzeń i wtyczek, coraz lepszych wynikach testów typu Acid3, wbudowanych klientach innych usług niż WWW, kilogramach skórek i pewnie wielu jeszcze innych zaletach. Część z nich może i jest ważna i interesująca. Rzecz w tym, że - statystycznie rzecz biorąc - nie obchodzą one psa z kulawą nogą.
Smutna prawda jest taka, że nadal większość użytkowników Internetu korzysta z "jedynie słusznej" przeglądarki pewnej znanej skądinąd firmy, a o dywagacjach na temat wyższości innych nad jeszcze innymi nigdy tak naprawdę nie słyszeli i raczej nie mają zamiaru usłyszeć. A to z wielu powodów niepokojące, chociażby ze względu na bezpieczeństwo czy powszechność stosowania standardów sieciowych. Wzajemne zwalczanie się zwolenników przeglądarek "niezależnych" (że je tak umownie nazwę...) na pewno nie zaradzi temu, iż połowa internautów wciąż używa do przeglądania Sieci programów przestarzałych i dziurawych jak ser szwajcarski.
A bliskość premier nowych edycji dwóch bezpiecznych, wygodnych i nowoczesnych przeglądarek to przecież całkiem dobra okazja, żeby coś na to poradzić. Zamiast więc bezowocnie spierać się, czy Safari jest lepsze od Opery, a ta od Firefoksa czy Camino, można by wspólnie zwiększyć wysiłki na rzecz uświadamiania użytkownikom niesłusznie najpopularniejszych przeglądarek, że istnieją dla nich o wiele lepsze alternatywy. Cóż bowiem z tego, że oto "wszyscy" przerzucą się czy na to Firefoksa, Operę czy inny tego typu program, jeśli ci 'wszyscy' to tak naprawdę ledwie 1/3 internautów?...
Triki z PowerShellem #4 – DirectXJakiś czas temu pokazałem, że w PowerShellu można zrobić rzeczy, które są - jak na konsolkę tekstową - co najmniej niespotykane. Przedstawiłem na przykład sposób na wyświetlenie zwykłego windowsowego okienka. Nie było ono aczkolwiek zbyt interesujące, jako że w środku było zupełnie puste - chociaż, oczywiście, Aero robił co mógł, by wyglądało ono na atrakcyjniejsze niż było w rzeczywistości ;)
Jako że zajmowania się rzeczami zupełnie nieprzydatnymi nigdy dość, postanowiłem więc pewnego razu spróbować wnieść w stworzoną formę nieco więcej życia. Efekty okazały się raczej ciekawe, przedstawiając się następująco:
Tak, to jest trójkąt. Tak, on się obraca. Tak, na pasku tytułowym okna jest napisane Direct3D... Wszystkie te trzy fakty nie są żadnym zbiegiem okoliczności :) Za wszystko odpowiedzialny jest ten ono skrypt:
# Ładujemy assemblies
[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
[Reflection.Assembly]::LoadWithPartialName("System.Drawing") | Out-Null
[Reflection.Assembly]::LoadWithPartialName("Microsoft.DirectX") | Out-Null
[Reflection.Assembly]::LoadWithPartialName("Microsoft.DirectX.Direct3D") | Out-Null
# Tworzymy okienko
$form = New-Object Windows.Forms.Form
$form.ControlBox = $false # Bo zamykanie niespecjalnie działa :P
$form.Text = "Direct3D"
# Tworzymy urządzenie
$pp = New-Object Microsoft.DirectX.Direct3D.PresentParameters
$pp.BackBufferCount = 1
$pp.Windowed = $true
$pp.SwapEffect = [Microsoft.DirectX.Direct3D.SwapEffect]::Discard
$pp.DeviceWindow = $form
$pp.PresentationInterval = [Microsoft.DirectX.Direct3D.PresentInterval]::Immediate
$dev = New-Object Microsoft.DirectX.Direct3D.Device @(0,
[Microsoft.DirectX.Direct3D.DeviceType]::Hardware, $form,
[Microsoft.DirectX.Direct3D.CreateFlags]::HardwareVertexProcessing, $pp)
# Przygotowujemy się do rysowania
$dev.RenderState.Lighting = $false # Wyłączamy oświetlenie
$tri = New-Object Microsoft.DirectX.Direct3D.CustomVertex+PositionColored[] @(3)
$tri[0] = New-Object Microsoft.DirectX.Direct3D.CustomVertex+PositionColored @(
0.0, 0.0, 0.5, [Drawing.Color]::Blue.ToArgb())
$tri[1] = New-Object Microsoft.DirectX.Direct3D.CustomVertex+PositionColored @(
0.5, -0.5, 0.5, [Drawing.Color]::Red.ToArgb())
$tri[2] = New-Object Microsoft.DirectX.Direct3D.CustomVertex+PositionColored @(
-0.5, -0.5, 0.5, [Drawing.Color]::Green.ToArgb())
# Wyświetlamy
[Windows.Forms.Application]::EnableVisualStyles()
$form.Show()
$time = 0
$lastTickCount = [Environment]::TickCount
# Pętlimy się
while ($time -lt 5000) {
# Przetwarzanie komunikatów
[Windows.Forms.Application]::DoEvents()
$time += [Environment]::TickCount - $lastTickCount
$lastTickCount = [Environment]::TickCount
# Rysujemy
$dev.Clear([Microsoft.DirectX.Direct3D.ClearFlags]::Target,
[Drawing.Color]::Lime, [float]1.0, 0)
$dev.BeginScene()
# Ustawiamy macierz obrotu
$angle = (10 * $time / 1000.0) / (2 * [Math]::PI)
$dev.Transform.World = [Microsoft.DirectX.Matrix]::RotationZ($angle)
# Rysujemy trójkąt
$dev.VertexFormat = [Microsoft.DirectX.Direct3D.CustomVertex+PositionColored]::Format
$dev.DrawUserPrimitives([Microsoft.DirectX.Direct3D.PrimitiveType]::TriangleList,
1, $tri)
$dev.EndScene()
$dev.Present()
}
Przyznam, że byłem mocno zaskoczony, iż taki trik jest w ogóle możliwy przy pomocy czegoś, co w założeniu jest tylko powłoką tekstową dla poleceń administracyjnych. A tu proszę: DirectX w całej (aczkolwiek zarządzanej) okazałości!
Nietrudno rzecz jasna zauważyć, że powyższy skrypt jest w dużym stopniu tłumaczeniem na język PowerShella odpowiedniego kodu, który mógłby zostać napisany w dowolnym języku programowania z platformy .NET. Trzeba było jedynie poradzić sobie z pewnymi niedogodnościami, jak na przykład koniecznością ręcznego załadowania assemblies czy jakimś obejściem braku sensownej możliwości reakcji na zdarzenia - tutaj "program" po prostu kończy się po 5 sekundach.
Właśnie ta niemożność sprawia, że tandem PowerShell + DirectX zapewne nie ma przed sobą świetlanej przyszłości w grach :) Przez chwilę zastanawiałem się, czy wobec tego nie da się go użyć do tworzenia dem... Niestety, już powyższy skrypt zajmuje ponad 2 kilobajty, nie potrafiąc przy tym zbyt wiele, więc ta możliwość też prawdopodobnie odpada.
Pozostaje zatem pokazywanie podobnych trików linuksowcom i wytykanie im, że ich bash czegoś takiego nie potrafi ;D