לבנות כלי ניתוח ל-PowerShell בתוך PowerShell — לקרוא סקריפטים עם AST, לא עם ביטויים רגולריים
· עודכן בתאריך: · Go Komura · PowerShell, AST, ניתוח סטטי, כלי פיתוח
נניח שאתם רוצים לעבור על מקבץ של סקריפטים של PowerShell ולהציג ברשימה כל מקום שקורא ל-Write-Host. חיפוש של המילה עצמה הוא עניין פשוט, אבל לכל שלוש השורות הבאות הוא יתפוס.
$source = @'
# Write-Host מציג טקסט על המסך
$message = 'Write-Host'
Write-Host 'שלום'
'@
מה שאנחנו מחפשים הוא שורה 3 בלבד. שורה 1 היא קומנט ושורה 2 היא מחרוזת שמושמת למשתנה, ולכן אף אחת מהן לא קוראת ל-Write-Host.
PowerShell עצמו מבדיל בין השניים בזמן שהוא מריץ את הקוד. אפשר גם לשלוף החוצה את התוצאה של ההבחנה הזו. במאמר הזה נתחיל מניתוח התחביר של הקוד ובדיקה של האובייקטים שנוצרים ממנו. לא נשתמש במודולים נוספים.1
ה-@' ... '@ שלמעלה הוא here-string במרכאות בודדות. הוא מכניס את שלוש השורות לתוך $source כמחרוזת שלא רצה כרגע ושבה $message אינו מתרחב.2
1. לראות מה ה-Parser מחזיר
קודם כל נעביר את $source ל-ParseInput.
$tokens = $null
$parseErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseInput(
$source, [ref] $tokens, [ref] $parseErrors)
if ($parseErrors.Count -gt 0) {
throw $parseErrors[0].Message
}
$ast.GetType().Name
ScriptBlockAst
מה שנכנס ל-$ast אינו מחרוזת ואינו תוצאת ההרצה של הסקריפט, אלא אובייקט מטיפוס ScriptBlockAst. הוא מייצג את כל הקוד שהזנתם. ParseInput מחזיר את האובייקט הזה בתור ערך החזרה, ובנוסף מחזיר דרך המשתנים שהועברו עם [ref] את הטוקנים ואת שגיאות התחביר.1
AST הוא קיצור של abstract syntax tree, עץ תחביר מופשט. השם אולי מעלה דמיון של מבנה נתונים מיוחד, אבל מנקודת המבט של PowerShell זה קודם כל אובייקט עם מאפיינים ומתודות. אם עוקבים אחרי המאפיינים מגיעים לאובייקטים אחרים שמייצגים הצהרות השמה וקריאות לפקודות.3
2. לאיזה אובייקטים הפכו שלוש שורות הקוד
בקוד כמו זה שלפנינו, שבו לא מצוינים במפורש begin, process ו-end, ההצהרות הרגילות נמצאות ב-EndBlock.Statements. נציג זה לצד זה את הסוג ואת הקוד המקורי.4
$statements = $ast.EndBlock.Statements
$statements | ForEach-Object {
[pscustomobject]@{
Type = $_.GetType().Name
Text = $_.Extent.Text
}
}
Type Text
---- ----
AssignmentStatementAst $message = 'Write-Host'
PipelineAst Write-Host 'שלום'
שני פריטים. שורת הקומנט אינה נכנסת לרשימת ההצהרות הזו. אם אתם צריכים את הקומנטים עצמם, אפשר לקבל אותם מה-$tokens שקיבלנו קודם.3
שורה 2 הפכה ל-AssignmentStatementAst, שמייצג השמה. שורה 3 היא PipelineAst: גם בלי לכתוב |, היא מיוצגת כ-pipeline עם פקודה אחת בלבד. Extent.Text הוא הקוד המקורי שמתאים לאובייקט הזה. כששם הטיפוס לבדו לא מספיק כדי להבין, אפשר להסתכל בזה כדי לדעת על איזה חלק בקוד מדובר.56
flowchart TB
accTitle: ההתאמה בין הקוד המקורי לאובייקטי התחביר
accDescr: ה-EndBlock של הסקריפט כולו מכיל את ההשמה משורה 2 ואת ה-pipeline משורה 3, ובאחרון נמצאת קריאת הפקודה.
R["ScriptBlockAst"] --> E["EndBlock"]
E --> A["הצהרת השמה: שורה 2"]
E --> P["pipeline: שורה 3"]
P --> C["CommandAst"]
איור 1: הקריאה משורה 3 נמצאת בתוך ה-pipeline.
האינדקסים במערך מתחילים מ-0, ולכן $statements[1] הוא ה-pipeline משורה 3. משם ניקח את האיבר הראשון.
$pipeline = $statements[1]
$call = $pipeline.PipelineElements[0]
$call.GetType().Name
$call.Extent.Text
$call.GetCommandName()
CommandAst
Write-Host 'שלום'
Write-Host
מצאנו CommandAst. גם Extent.Text, שמחזיר את הקוד המקורי, וגם GetCommandName(), שמחזיר את שם הקריאה, מופעלים על אותו אובייקט.7
גם הפירוט שכולל את הארגומנטים נמצא ב-CommandElements.
$call.CommandElements | ForEach-Object {
[pscustomobject]@{
Type = $_.GetType().Name
Text = $_.Extent.Text
}
}
Type Text
---- ----
StringConstantExpressionAst Write-Host
StringConstantExpressionAst 'שלום'
גם השם וגם הארגומנט הם צמתים שמייצגים מחרוזת. ובכל זאת GetCommandName() מחזיר דווקא את Write-Host. הוא לא בוחר לפי האיות של המחרוזת בלבד, אלא מסתכל על האיבר שמשמש כשם בתוך קריאה לפקודה. ה-'Write-Host' משורה 2 נמצא בצד ימין של הצהרת השמה, ומלכתחילה אינו שייך לקריאה הזו.78
זה ההבדל בין חיפוש מחרוזות לבין ניתוח תחביר. גם באותה מילה, אם בודקים את המבנה שבו היא נמצאת, אפשר להבדיל בין השימושים.
3. לחפש CommandAst במקום לעבור לפי אינדקסים
את התוכן אישרנו. אבל בסקריפט אמיתי אי אפשר להניח מראש שזה תמיד האיבר הראשון של ההצהרה השנייה. קריאות נמצאות גם בתוך פונקציות, גם בתוך if, וגם באמצע pipeline.
מתודת החיפוש לשם כך היא FindAll.
$commands = @($ast.FindAll({
param($node)
$node -is [System.Management.Automation.Language.CommandAst]
}, $true))
$commands.Count
$commands[0].GetType().Name
$commands[0].Extent.Text
1
CommandAst
Write-Host 'שלום'
FindAll עובר על עץ התחביר ומעביר כל צומת לבדיקה שבתוך { ... }. בודקים את הטיפוס של ה-$node שהתקבל עם -is, ואם הוא CommandAst מחזירים $true. הצומת הזה נשאר בתוצאות. ה-$true שבסוף הוא הציווי לחפש גם בתוך פונקציות ו-script blocks מקוננים.9
למשל ב-function Show-Message { Write-Host 'hello' } הוא יורד לתוך הפונקציה ומאתר את הקריאה. אין צורך להריץ את הפונקציה.
flowchart TB
accTitle: חיפוש לפי טיפוס שיורד לתוך פונקציה
accDescr: החיפוש יורד לגוף של פונקציה שמוכלת בסקריפט ומכניס לתוצאות את הקריאה שמתאימה ל-CommandAst.
R["סקריפט"] --> F["הגדרת פונקציה"]
F --> B["גוף הפונקציה"]
B --> C["CommandAst"]
C --> M["הטיפוס מתאים: נשאר בתוצאות"]
איור 2: אפשר לקבל את הצמתים שמתאימים לתנאי החיפוש בלי לספור בעצמכם את עומק הקינון.
בשלוש השורות מהפתיחה נמצאה קריאה אחת — אותה קריאה שהגענו אליה לפי אינדקס. אם נוציא את השם ואת המיקום, יש לנו משהו שאפשר להשתמש בו כתוצאת חיפוש.
$commands | ForEach-Object {
[pscustomobject]@{
Name = $_.GetCommandName()
Line = $_.Extent.StartLineNumber
Column = $_.Extent.StartColumnNumber
}
}
Name Line Column
---- ---- ------
Write-Host 3 1
שורות ועמודות מתחילות מ-1. מכיוון שאותו צומת מחזיק גם את השם וגם את המיקום, אין צורך לחזור ולחפש את השורה בחיפוש מחרוזות נפרד.6
4. איך נראית קריאה דרך משתנה
כאן נשנה מעט את מה שאנחנו מנתחים. בנוסף למקרה שבו השם נכתב ישירות, נוסיף גם מקרים שמעבירים מחרוזת או משתנה לאופרטור הקריאה &.
$source = @'
Write-Host 'direct'
& 'Write-Host' 'quoted'
$command = 'Write-Host'
& $command 'variable'
'@
$ast = [System.Management.Automation.Language.Parser]::ParseInput(
$source, [ref] $tokens, [ref] $parseErrors)
if ($parseErrors.Count -gt 0) { throw $parseErrors[0].Message }
$commands = @($ast.FindAll({
param($node)
$node -is [System.Management.Automation.Language.CommandAst]
}, $true))
$commands | ForEach-Object {
[pscustomobject]@{
Line = $_.Extent.StartLineNumber
Name = $_.GetCommandName()
Text = $_.Extent.Text
}
}
Line Name Text
---- ---- ----
1 Write-Host Write-Host 'direct'
2 Write-Host & 'Write-Host' 'quoted'
4 & $command 'variable'
גם שורה 4 נמצאה כ-CommandAst. אבל ערך ההחזרה של GetCommandName() הוא $null.
אדם שקורא את ארבע השורות האלה יכול להסיק את הערך של $command מתוך ההשמה שמיד לפניה. אבל המתודה הזו לא חוזרת אחורה על ההשמות למשתנה ולא מחשבת את הערך. בשונה מהמקרה שבו השם כתוב ישירות בקוד, ה-API הזה לבדו לא מצליח להוציא את השם.7
flowchart TB
accTitle: אותו צומת קריאה, תוצאות שונות בהשגת השם
accDescr: בקריאה ששמה הוא מחרוזת אפשר לקבל Write-Host, ובקריאה דרך משתנה השם הוא null, אבל בשתיהן יש את המיקום של הקריאה.
C["CommandAst"] --> L["האיבר של השם הוא מחרוזת"]
C --> V["האיבר של השם הוא משתנה"]
L --> N["שם: Write-Host"]
V --> U["שם: null"]
איור 3: גם כשהשם ריק, אפשר עדיין לדעת שבשורה 4 כתובה קריאה.
בכלי שעובר על קבצים נשמור את ההבדל הזה בעמודה NameKind. שורות שמהן הצלחנו להשיג את מחרוזת השם הן Static, ושורות שלא הצלחנו הן Unresolved. אם משאירים גם את השורות בלי שם, לא נאבד את המקומות שאדם צריך לבדוק.
5. להוציא מקובץ את המיקומים של Write-Host
כשקוראים .ps1 ולא מחרוזת, משתמשים ב-ParseFile במקום ב-ParseInput. הדרך לקרוא את ה-AST לא משתנה.10
את Get-ScriptCommand, שמאגד את כל מה שעשינו עד כאן, הכנו בקוד המלא בסוף המאמר ובדוגמה להורדה. שמרו את הקוד המלא כקובץ Get-ScriptCommand.ps1, ואת ארבע השורות שבתוך ה-here-string מסעיף 4 שמרו כ-demo.ps1 באותה תיקייה. החבילה להורדה מכילה את שני הקבצים האלה.
מריצים את זה באותה תיקייה.
. .\Get-ScriptCommand.ps1
$calls = @(Get-ScriptCommand -LiteralPath .\demo.ps1)
$calls |
Where-Object { $_.NameKind -eq 'Static' -and $_.Name -eq 'Write-Host' } |
Format-Table Line, Column, NameKind, Name -AutoSize
Line Column NameKind Name
---- ------ -------- ----
1 1 Static Write-Host
2 1 Static Write-Host
כך קיבלנו את המיקומים של הקריאות ששמן Write-Host. ההשמה משורה 3 לא נכנסת. כך נמנעת הבעיה שהטרידה אותנו בפתיחה — שקומנטים ומחרוזות רגילות מתערבבים לתוך תוצאות החיפוש.
מנגד, שורה 4 נופלת מחוץ לסינון הזה. לא מפני שאין שם קריאה, אלא מפני שהשם שלה לא הוכרע. את השורות שלא הוכרעו בודקים בנפרד.
$calls |
Where-Object NameKind -eq 'Unresolved' |
Format-Table Line, Column, NameKind, Name -AutoSize
Line Column NameKind Name
---- ------ -------- ----
4 1 Unresolved
Static הוא סימן לכך שהצלחנו להשיג את מחרוזת השם, והוא אינו ערובה שהפקודה קיימת או שיודעים איזה מימוש ייקרא. למשל echo חוזר בתור echo, ו-Microsoft.PowerShell.Utility\Write-Host חוזר כשם עם קידומת, ולכן אף אחד מהם לא נכנס לחיפוש ההתאמה המדויקת שלמעלה. אם גם aliases וקריאות עם שם מלא נמצאים במסגרת הבדיקה, צריך להתאים את תנאי החיפוש בהתאם.11
להבהרה: מה שעבר dot-source הוא כלי הניתוח. demo.ps1 רק נקרא עם ParseFile ולא הופעל.
6. תוצאות החיפוש אינן היסטוריית הרצה
החיפוש הזה בודק איך הדברים כתובים בקוד. גם בפונקציות שאינן בשימוש וגם בתוך if ($false) { Write-Host ... } כתובות קריאות, ולכן הן נכנסות לרשימה. סדר ההרצה או מספר הפעמים אינו ידוע ממנה.
גם במחרוזות היחס שונה: 'Write-Host' ו-"Today: $(Get-Date)" אינן אותו דבר. בשנייה מוטבע ביטוי, ולכן ה-Get-Date שבתוכה נמצא. לעומת זאת, קוד שנמצא בתוך מחרוזת רגילה אינו מנותח מחדש כסקריפט נפרד.2
קריאה למתודה של .NET כמו [Console]::WriteLine(...) היא סוג אחר של צומת, לא CommandAst. הרשימה הזו גם אינה מכסה כל פעולה וגם אינה מוכיחה דבר לגבי בטיחות. זה כלי לבדיקה של סקריפטים שאתם מנהלים בעצמכם. כדי לאמת aliases בזמן ריצה או פונקציות באותו שם, צריך גם את הסביבה שבה הסקריפט רץ.1211
במרכז הדברים שהשתמשנו בהם כאן נמצאים האובייקטים שמאשרים עם GetType() ו-Extent.Text. גם כשתרצו לבדוק תחביר אחר, אפשר להתחיל באותו אופן: להעביר קטע קוד קצר ל-ParseInput ולהציג את השניים זה לצד זה. אם המטרה היא בדיקת איכות מול כללים קיימים, עדיף להשתמש ב-PSScriptAnalyzer.
הקוד המלא לחיפוש בקבצים
להלן כל הפונקציה Get-ScriptCommand שהופיעה בסעיף 5. החלק שקורא את התחביר זהה לזה שבסעיף 3, ומסביבו נוספו הבאת הקובץ, הטיפול בשגיאות תחביר וקלט של כמה קבצים.
function Get-ScriptCommand {
[CmdletBinding()]
[OutputType([pscustomobject])]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[Alias('FullName')]
[ValidateNotNullOrEmpty()]
[string[]] $LiteralPath
)
process {
foreach ($path in $LiteralPath) {
$file = Get-Item -LiteralPath $path -Force -ErrorAction Stop
if ($file -isnot [System.IO.FileInfo]) {
throw "A file is required: $path"
}
$tokens = $null
$parseErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
$file.FullName, [ref] $tokens, [ref] $parseErrors)
if ($parseErrors.Count -gt 0) {
$first = $parseErrors[0]
throw ('Parse error: {0}:{1}:{2} ({3})' -f $file.FullName,
$first.Extent.StartLineNumber,
$first.Extent.StartColumnNumber, $first.ErrorId)
}
$commands = $ast.FindAll({
param($node)
$node -is [System.Management.Automation.Language.CommandAst]
}, $true)
foreach ($command in ($commands | Sort-Object { $_.Extent.StartOffset })) {
$name = $command.GetCommandName()
$kind = if ($null -eq $name) { 'Unresolved' } else { 'Static' }
[pscustomobject]@{
Path = $file.FullName
Line = $command.Extent.StartLineNumber
Column = $command.Extent.StartColumnNumber
NameKind = $kind
Name = $name
}
}
}
}
}
הוא משיג את הקובץ בפועל עם Get-Item -LiteralPath, ואז מעביר את FullName ל-ParseFile. שם כמו draft[1].ps1 אינו נחשב לתו כללי. תיקיות וקבצים שאינם קיימים מפיקים שגיאה.
אם יש שגיאת תחביר, הוא עוצר לפני שהוא מפיק תוצאות עבור אותו קובץ. החזרה חלקית של AST אינה נחשבת לניתוח מוצלח. המטרה היא להפריד בין נקרא כראוי ונמצאו אפס תוצאות לבין לא ניתן היה לקרוא.
ערך ההחזרה אינו מעוצב לטבלה, אלא הוא אובייקט עם Path, Line, Column, NameKind ו-Name. מלבד סינון כמו בסעיף 5, אפשר לשמור ל-CSV את התוצאות של כמה קבצים. מכיוון שהוא מקבל קלט גם דרך מאפיין בשם FullName, אפשר להעביר ישירות את אובייקטי ה-FileInfo ש-Get-ChildItem מחזיר.
Get-ChildItem -LiteralPath .\scripts -Filter *.ps1 -File -Recurse |
Get-ScriptCommand |
Export-Csv -LiteralPath .\commands.csv -NoTypeInformation -Encoding UTF8 -NoClobber
-NoClobber הוא ציווי שמונע דריסה של קובץ CSV קיים. אם קובץ באמצע נכשל, ייתכן שהתוצאות שלפניו כבר נכתבו ל-CSV. אל תסיקו שהכל הצליח רק מכך שהקובץ נוצר; בדקו גם את השגיאות.
הדקדוק שמשמש לניתוח נקבע לפי גרסת ה-PowerShell שמריצה את הכלי. הצלחה בניתוח תחביר תחת 7.x אינה מבטיחה שהסקריפט יעבוד ב-5.1. אם אתם קוראים תחת 5.1 גם קבצים שמכילים יפנית, התחשבו גם בקידוד התווים, למשל UTF-8 עם BOM.13
דוגמאות ואימות
כלי הניתוח, הדוגמאות והבדיקות (ZIP) מכיל את הפונקציה המוגמרת, דוגמאות לניתוח ובדיקות Pester. גוף הפונקציה זהה לקוד שמופיע כאן, ובגרסה שמופצת נוספים לו קומנטים של עזרה. אם אין אפשרות להשיג את קובץ ה-ZIP, אפשר לשמור ולהשתמש בקוד המלא שלמעלה.
הפונקציה המוגמרת ו-26 מקרי הבדיקה הקיימים של Pester לא שונו מהגרסה הקודמת. בעדכון הזה הורצו 12 בלוקי הקוד של PowerShell שחולצו מהמאמר תחת Windows PowerShell 5.1 ותחת PowerShell 7.x, ובדקנו זה מול זה את שמות הטיפוסים, את הקוד המקורי, את מיקומי הקריאות ואת תוצאות הסינון. הגרסאות המדויקות והיקף האימות מתועדים ב-README של החבילה להורדה.
ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 5, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle
מאמרים קשורים
- להגן על איכות סקריפטי PowerShell עם PSScriptAnalyzer
- תחזוקת בדיקות של PowerShell עם Pester
- תכנון ארגומנטים ומודולריזציה ב-PowerShell
מקורות
-
Microsoft Learn, Parser.ParseInput Method. על ה-API שמחזיר AST מתוך מחרוזת ומחזיר דרך ארגומנטי פלט טוקנים ושגיאות תחביר. ↩ ↩2
-
Microsoft Learn, about_Quoting_Rules. על here-strings במרכאות בודדות ועל תת-ביטויים במחרוזות שמתרחבות. ↩ ↩2
-
Microsoft PowerShell Team, Using abstract syntax trees (ASTs) with ISE to make scripting more productive. על הגישה לעץ התחביר מ-PowerShell ועל חיפוש צמתים כמו הגדרות פונקציה. ↩ ↩2
-
Microsoft Learn, NamedBlockAst Class. על בלוקים שהשם שלהם אינו מצוין במפורש ועל Statements, שמחזיק את ההצהרות. ↩
-
Microsoft Learn, PipelineAst.PipelineElements Property. על האיברים שמרכיבים pipeline. ↩
-
Microsoft Learn, IScriptExtent Interface. על הטווח בקוד המקור, על מיקום ההתחלה ועל כך ששורות ועמודות מתחילות מ-1. ↩ ↩2
-
Microsoft Learn, CommandAst.GetCommandName Method. על כך שמוחזר null בקריאות שאין להן שם שאפשר להשיג באופן סטטי. ↩ ↩2 ↩3
-
Microsoft Learn, CommandAst.CommandElements Property. על מרכיבי תחביר כמו שם הקריאה והארגומנטים. ↩
-
Microsoft Learn, Ast.FindAll Method. על מעבר על הצמתים שמתאימים לתנאי ועל הציווי לחפש בתוך פונקציות ו-script blocks מקוננים. ↩
-
Microsoft Learn, Parser.ParseFile Method. על ה-API שמנתח קובץ ומחזיר AST, טוקנים ושגיאות תחביר. ↩
-
Microsoft Learn, about_Command_Precedence. על סדר העדיפויות בזמן ריצה בין פקודות באותו שם, aliases, פונקציות וכדומה. ↩ ↩2
-
Microsoft Learn, InvokeMemberExpressionAst Constructors. על הצמתים שמייצגים קריאות למתודות של מופע ושל סטטי. ↩
-
Microsoft Learn, about_Character_Encoding. על אופן הקריאה של סקריפטים ב-Windows PowerShell ועל הטיפול ב-UTF-8 BOM. ↩
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
הרשת עובדת אבל Windows מציג "אין אינטרנט" — לבודד NCSI, DNS, Proxy ו-VPN ב-Windows
למה Windows מציג "אין אינטרנט" בזמן שהרשת עובדת — נקודת המוצא היא קביעת הקישוריות של NCSI. כאן מפרידים בין DNS, proxy, VPN ו-captive port...
דיסק ב-100%: מה באמת צריך לעצור? — להפריד בין SysMain, Windows Search ו-Defender
מבודדים שימוש של 100% בדיסק ב-Windows לפי throughput, זמן תגובה וקבצים. עוצרים את SysMain בבטחה, מצמצמים את Windows Search, ומנתחים את De...
סדר name resolution ב-Windows — hosts, DNS cache, LLMNR/mDNS ו-DoH
האם hosts, ה-DNS cache, שרת DNS או LLMNR/mDNS ענו קובע למה חלק מהמחשבים נכשלים. לומדים את סדר name resolution ב-Windows, מה DoH משנה, ואי...
מה Fast Startup באמת עושה — למה "Shut down" ב-Windows אינו אותו דבר כמו Restart
Shutdown ב-Windows הוא hybrid shutdown כברירת מחדל, ושומר את ה-kernel ואת ה-drivers ל-hiberfil.sys. למה רק restart מאפס אותם, ומתי לכבות ...
ניהול תצורה הצהרתי של Windows עם DSC — מתחילים IaC עם dsc.exe
יוצאים מסקריפטי runbook שנשברים בהרצה שנייה. DSC v3 (dsc.exe) מצהיר על תצורת Windows ב-YAML ומחיל אותה באופן אידמפוטנטי. המאמר מסדר את אר...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מה זה ה-AST של PowerShell?
- זהו abstract syntax tree שמייצג קוד כצמתים לפי כל מבנה תחבירי, למשל השמה, קריאה לפקודה או הגדרת פונקציה. הוא מאפשר להבדיל בין אותם תווים שנכתבים בתוך קומנט או מחרוזת לבין אותם תווים שנכתבים כשם של קריאה.
- צריך להריץ את קובץ ה-ps1 שאני מנתח?
- הכלי במאמר הזה קורא אותם עם Parser.ParseFile, והוא לא מפעיל את היעד ולא עושה לו dot-source. עם זאת זה אינו sandbox שמבטיח בטיחות, אלא כלי למיפוי מלאי של סקריפטים שאתם מנהלים בעצמכם. גם אי-מציאה של קריאה אינה הוכחה לבטיחות.
- אפשר לקבל את השם של פקודה שנקראת דרך משתנה?
- כאשר GetCommandName לא מצליח להשיג את השם באופן סטטי, הוא מחזיר null. הכלי במאמר הזה לא זורק את השורה הזו, אלא משאיר אותה עם NameKind של Unresolved. הוא לא עוקב אחרי השמות של משתנים ולא מריץ ביטויים כדי לנחש את השם.
- זה עובד גם ב-Windows PowerShell 5.1?
- הכלי שמופיע כאן מיועד ל-5.1 ולסדרת 7.x. אבל הדקדוק שמשמש לניתוח הוא זה של ה-PowerShell שמריץ את הכלי. הצלחה בניתוח תחת 7.x אינה הוכחה לתאימות עם 5.1. אם אתם קוראים תחת 5.1 גם קבצים שמכילים יפנית, התחשבו גם בקידוד התווים, למשל UTF-8 עם BOM.
- איך זה מתחלק מול PSScriptAnalyzer?
- הכלי במאמר הזה נועד להציג ברשימה את השמות ואת המיקומים של הקריאות. כשרוצים לבדוק איכות מול כללים קיימים ולנהל את האזהרות, משתמשים ב-PSScriptAnalyzer. הדרך לקרוא את ה-AST שמוצגת כאן היא נקודת כניסה להבנה של תוצאות ניתוח סטטי ושל הדרך שבה עובדים כללים מותאמים.