A few PowerShell tips and functions snippets
Defining a variable for the entire script; initialize it once and use it everywhere.
The key is to use the $global:
prefix like below:
begin {
# Folder where the running Powershell script is stored
$global:scriptDir = ""
function initialize() {
$global:scriptDir = Split-Path $script:MyInvocation.MyCommand.Path
}
function someFunction() {
Write-Host $global:scriptDir
}
}
Including an external script (f.i. my_helper.ps1
) can be done using the dot notation: . my_helper.ps1
.
# Define the debug mode constant
set-variable -name DEBUG -value ([boolean]$FALSE) -option Constant
function include_helpers() {
# List of helpers to load
$helpers = @("files", "images", "markdown")
if ($DEBUG -eq $TRUE) {
# If debug mode is enabled, debug.ps1 will also be loaded
$helpers = $helpers + @("debug")
}
foreach ($helper in $helpers) {
# Suppose that files are in the helpers sub folder
$filename = ".\helpers\$helper.ps1"
try {
if ([System.IO.File]::Exists($filename)) {
# The file exists; load it
. $filename
}
}
catch {
Write-Error "Error while loading helper $filename"
}
}
return;
}
Note: functions should be declare with the global:
prefix.
Don't write things like:
Write-Error "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab ...")
but split the string using the $( ...)
syntax:
Write-Error $("Sed ut perspiciatis unde omnis iste natus error " +
"sit voluptatem accusantium doloremque laudantium, totam rem "
"aperiam, eaque ipsa quae ab ...")
For commands too, don't write things like:
$target = Get-Item -Path $filename | Select-Object -ExpandProperty Target | Select-Object -First 1
but split the command like this:
$target = Get-Item -Path $filename `
| Select-Object -ExpandProperty Target `
| Select-Object -First 1
Variable | Description |
---|---|
[string](Get-Location) |
Get the current folder. |
$MyInvocation.MyCommand.Name |
Return the full name of the running script (return f.i. c:\temp\a.ps1 ). |
$PSScriptRoot |
Return the folder of the running script (c:\temp ) |
<#
.SYNOPSIS
Check if a file exists on disk
.PARAMETER Filename
Name of the file to check
.OUTPUTS
True if the file exists,
False othwerise
#>
function fileExists([string] $filename) {
return [Boolean](Test-Path $filename -PathType Leaf)
}
Alternative solution: [System.IO.File]::Exists($filename)
(no support for wildcard characters)
The function below will retrieve the list of all files below the current running folder and will returns a flat list (i.e. only files name).
The function support exclusions like skipping specific folders or files.
<#
.DESCRIPTION
Retrieve the list of all files (based on the mentioned pattern).
The result will be a flat list (i.e. only files name).
Support exclusions like skipping folders or files.
.PARAMETER Pattern
Pattern for files like c:\temp\*.* or just *.*
.PARAMETER Exclude
It's a regex that contains patterns to exclude
For instance, exclude some folders
".*\\\.config\\|.*\\\.git\\|.*\\backup\\"
And exclude files based on their extensions
".bmp$|.gif$|.ico$|.jpe?g$|.png$"
.OUTPUTS
Array
#>
function getListOfFiles([string] $pattern = "*.*", [string] $exclude = "") {
# Get the list of all files, retrieve a flatlist and
# don't report errors when f.i. a folder is a symlink
$files = Get-ChildItem . -Filter $pattern -Recurse `
| Where-Object { $_.Fullname -notmatch $exclude } `
| Group-Object "FullName" `
| Select-Object "Name"
return $files
}
# Sample
# Skip .git and backups folders
$exclude = ".*\\\.git\\|.*\\backups\\"
# and skip some extensions
$exclude += "|.bmp$|.gif$|.ico$|.jpe?g$|.png$"
$files = getListOfFiles '*.*' $exclude
foreach ($file in $files) {
Write-Host "Process" $file.Name
}
This will return something like below:
C:\Christophe\demo\readme.md
C:\Christophe\demo\03_Annex\index.md
C:\Christophe\demo\03_Annex\01_Annex1\index.md
C:\Christophe\demo\07_Date\index.md
The following snippet calculate a FolderDepth column based on the number of \
separator in the filename:
<!-- concat-md::include "./files/getListOfFilesWithDepth.ps1" -- >
This will return something like below:
<!-- concat-md::include "./files/getListOfFilesWithDepth.txt" -- >
Get the list of files, with or without a filter like a file's extension and define the maximum deep allowed so it's possible to get only the root folder (MaxDepth=0
, the root and the first children, ...).
<!-- concat-md::include "./files/getListOfFilesRecursiveWithMaxDepth.ps1" -- >
<#
.SYNOPSIS
Return the parent folder of a file
.PARAMETER Filename
Filename for which the parent folder should be returned
.OUTPUTS
String
#>
function getParentFolderName([string] $filename) {
return (Split-Path -Path $filename)
}
When a file is a symbolic or hard link, the following function will return the original filename.
For instance if c:\temp\a.ps1
is symlink to c:\christophe\repositories\tools\utils.ps1
, the following function will return the full name of the original filename so will return c:\christophe\repositories\tools\utils.ps1
.
<#
.DESCRIPTION
When a file is a symlink, return the target path i.e. the original path of the file
Note: when the same file is symlinked multiple times, the ExpandProperty
will return all files so get only the first item which is the original file
.PARAMETER Filename
That file should be a symlink (hard or symbolic)
.OUTPUTS
String
#>
function getSymLinkTargetPath([string] $filename) {
$target = Get-Item -Path $filename `
| Select-Object -ExpandProperty Target `
| Select-Object -First 1
return [string]$target
}
$objJSON = Get-Content -Path ".\test.json"
$objJSON | ConvertFrom-Json | ConvertTo-Json | Out-File -FilePath ".\test-Pretty.json"
Get-Content ".\test-Pretty.json"
Consume a service returning XML and display the response
[int] $Amount = 5
[string] $Type = "pargraphs"
[string] $Start = "yes"
[xml]$Temp = Invoke-WebRequest -UseBasicParsing -Uri "https://www.lipsum.com/feed/xml?amount=$Amount&what=$Type&start=$Start"
$Temp.feed.lipsum
Retrieve the content between two HTML tags:
# Search for the pattern like:
# <!-- start -->
# CONTENT
# <!-- end -->
#
# Can be on the same line or on multiple lines
$content = "<!-- start -->Ipso Lorem<!-- end -->"
$pattern = [regex]$(
"\n?\<\!\-\- start \-\-\>" +
"([\s\S]*?)" +
"\<\!\-\- end \-\-\>\n?"
)
# Write the CONTENT
Write-Host $([string]($pattern).Match($content).groups[1].value)
The default mode is case sensitive, to change this, the mode to use is (?i)
at the very start of the [regex]
expression:
$content = "Ipso lorem`n`n@TOdo Christophe: do this.`n`nIpso lorem"
$pattern = [regex]"(?msi)^(\@todo ([^\n\r]*))"
if (($pattern.Match($content)).success) {
Write-Warning "@TODO found at the beginning of the string"
Write-Host "There is a TODO for $($pattern.Match($content).groups[2].value)"
}
Process every occurrences:
$content = "# Heading 1`n`n## Heading 1.1`n`n## Heading 1.2`n`n## Heading 1.3"
# Skip heading 1 so start at 2
# (?ms) ==> Multi-lines regex
# ^ ==> The start of the line
# (\#{2,}) ==> We need to find at least two consecutive #
# ([^\n\r]*) ==> Capture the end of the line (exclude CRLF)
# $ ==> End of the line
$pattern = [regex]"(?ms)^(\#{2,}) ([^\n\r]*)$"
$headings = $pattern.Matches($content);
if ($headings.Count -gt 0) {
$match = $pattern.Match($content)
while ($match.Success) {
# Isolate the # (one or more) and the title
$pattern = [regex]"(#{2,})* (.*)"
# Display "Heading 1.1", "Heading 1.2", ...
Write-Host $pattern.Match($match).Groups[2].Value
$match = $match.NextMatch()
}
}
To make a search on more than one line:
# Match a YAML block
#
# The block start with --- on his own line
# Then there is a content (one or more line)
# The block ends with --- on his own line
$content = "---`nTitle: My great title`n---`n"
$pattern = [regex]"(?ms)(^\-{3}([\s\S]*\s)^\-{3})"
# Write the YAML content
Write-Host $([string]($pattern).Match($content).groups[2].value)