Powershell: Difference between revisions

From miki
Jump to navigation Jump to search
Line 655: Line 655:
try {
try {
Push-Location $fromPath; Get-Item $toPath | Resolve-Path -Relative
Push-Location $fromPath; Get-Item $toPath | Resolve-Path -Relative
}
finally {
Pop-Location
}
}
</source>

From current directory:
<source lang="powershell">
function Get-Relative-Path($toPath)
{
try {
Push-Location "."; Get-Item $toPath | Resolve-Path -Relative
}
}
finally {
finally {

Revision as of 12:11, 3 June 2022

Links

Reference

Files

Powershell scripts have a .ps1 extension.

Environment

Powershell uses preference variables [1]:

  • $ErrorActionPreference: value can be 'SilentlyContinue', 'Stop', 'Continue' (default), 'Inquire', 'Ignore', 'Suspend', or 'Break'.

To get powershell version:

Get-Host
Get-Host | select version

# or
$PSVersionTable

Syntax

 ✐  See learn Powershell in Y minutes for more!
# Documentation, information
Update-Help              # Update help system - to run as Administrator
help Test-Path           # Get help on a command, on an alias...
help Test-Path -full     # ... and on all options
help Test-Path -examples # ... see examples. VERY USEFUL. Eg. help save-help examples

Get-Command about_*      # DOES NOT WORK
Get-Command -Verb Add

Get-Alias ps                      # 'Get-Process'
Get-Alias -Definition Get-Process # 'ps'
gal | findstr Get-Process         # Unix style

ps | gm                  # Get-Member, useful to discover API
gi file.txt | gm

# Options can be abbreviated
Get-Alias -Definition Get-Process
gal -def Get-Process

# Single line comments start with a number symbol.

<#
  Multi-line comments
  like so
#>
# Frequent aliases (sorted by definition)
#   %       -> ForEach-Object
#   foreach -> ForEach-Object
#   alias   -> Get-Alias
#   gal     -> Get-Alias
#   dir     -> Get-ChildItem
#   ls      -> Get-ChildItem
#   gci     -> Get-ChildItem
#   gcm     -> Get-Command
#   cat     -> Get-Content
#   type    -> Get-Content
#   gi      -> Get-Item
#   help    -> Get-Help
#   gm      -> Get-Member
#   ps      -> Get-Process
#   man     -> help
#   ni      -> New-Item
#   md      -> New-Item
#   mkdir   -> New-Item
#   del     -> Remove-Item
#   erase   -> Remove-Item
#   rd      -> Remove-Item
#   ri      -> Remove-Item
#   rm      -> Remove-Item
#   rmdir   -> Remove-Item
#   select  -> Select-Object
#   where   -> Where-Object
#   ?       -> Where-Object
#   echo    -> Write-Output
#   write   -> Write-Output

Scopes

# See doc for more information

$foo = 0           # Script scope
$script:bar = 0    # Same

function Do-Stuff()
{
    $other = 0      # Local scope
    $script:foo++   # Refer to variable above
    $global:bar++   # Global scope, will persist in user env
}

Primitive types / operators

# Numbers
3
1 + 1                  # 2
2 - 1                  # 1
10 * 2                 # 20
10 / 2                 # 5
10 / 3                 # 3.3333333
[int](10 / 3)          # 3
[int]1.5 -eq [int]2.5  # True !
10 % 3                 # 1
[Math]::Pow(2,3)       # 8
# Null is empty reference - assigning to it is ignored
$null = "foo"
$null                  # None
# Boolean
$True                  # True
$False                 # False
!$True                 # False
!(Test-Path "c:\foo")  # Use (...) for expr
-not $True             # False
$True  -and $True
$False -or  $False
0 -eq $False           # True
2 -ne $True            # True
[bool](0)              # False
# Bitwise
0 -band 2              # 0
-5 -bor 0              # -5
# Comparison
1 -eq 1
1 -ne 0
0 -lt 1
1 -le 1
1 -gt 0
1 -ge 1

# With strings
"foo" -eq "foo"        # True
"foo" -eq "FOO"        # True, insensitive!
"foo" -ieq "FOO"       # True (insensitive)
"foo" -ceq "FOO"       # False (sensitive)
"a" -clt "A"           # True ! (strange)

# With objects
(1,2,3,1,1) -eq 1      # 1,1,1
@{a=1} -eq @{a=1}      # False !
# Type, Typecast, -is, GetType()
[int]2.5               # 2
[bool]5                # True
$b = (3,4)
(1,2) -is $b.GetType() # True

Strings / arrays / hashtables / ...

# Strings
$hello = "Hello"
$world = "World"
'Hello, World!'                        # No interpolation
"Hello, $world!"                       # 'Hello, World!'
"Hello, World!".Length                 # Length
"{0}, {1}!" -f $hello, $world          # f-string
"$world is $($world.length) char long"
'Hello, World!'[0]                     # 'H'
'Hello, World!'[0..5]                  # 'H', 'e', 'l', 'l', 'o'
'Hello, World!'[0,2,4]                 # 'H', 'l', 'o'
'Hello, World!'.Substring(0,5)         # 'Hello'
'Hello, ' + 'World!'                   # 'Hello, World!'
'First line`nSecond line'              # Escape with backtick
"foo" | gm                             # Get all methods / properties
"foo".ToLower()                        # ... Convert to lowercase
"foo".ToUpper()                        # ... Convert to uppercase

# Arrays
$fixed=1,2,3
$fixed.Add(4)        # ! Exception
$fixed.Item(1)       # 2
$fixed[1]            # 2

# Use .Net for variable (and faster!) arrays
[System.Collections.ArrayList]$array = @()
[System.Collections.ArrayList]$array = @(1,2,3)
$array.Add(4)          # 3 -- index of last element
$array.Add(5) > $null  # ... to absorb output

# Hash table
$emptyHash = @{}       # Empty hash
$filledHash = @{"one"= 1 
                "two"= 2 
                "three"= 3}
$filledHash["one"]          # => 1
$filledHash.Values          # => [1, 2, 3]
"one" -in $filledHash.Keys  # => True
1 -in $filledHash.Values    # => False
$filledHash["four"]         # $null
$filledHash.Add("five",5)   # $filledHash["five"] is set to 5
$filledHash.Add("five",6)   # exception "Item with key "five" has already been added"
$filledHash["four"] = 4     # $filledHash["four"] is set to 4, running again does nothing
$filledHash.Remove("one")   # Removes the key "one" from filled dict

Control flow

# if-then-else
if ($someVar -gt 10) {                      # Testing variable
   Write-Output "..."
}
elseif (-not $(Test-Path "somefile.txt")) { # Testing cmd result
   Write-Output "..."
}
else {
   Write-Output "..."
}
# foreach
foreach ($animal in ("dog", "cat", "mouse")) {
    # You can use -f to interpolate formatted strings
    "{0} is a mammal" -f $animal
}
# for
$letters = ('a','b','c','d','e','f','g','h')
for($i=0; $i -le $letters.Count-1; $i++){
    Write-Host $i, $letters[$i]
}
# while
$x = 0
while ($x -lt 4) {
    Write-Output $x
    $x += 1  # Shorthand for x = x + 1
}
# switch
$val = "20"
switch($val) {
  { $_ -eq 42 }           { "The answer equals 42"; break }
  '20'                    { "Exactly 20"; break }
  { $_ -like 's*' }       { "Case insensitive"; break }
  { $_ -clike 's*'}       { "clike, ceq, cne for case sensitive"; break }
  { $_ -notmatch '^.*$'}  { "Regex matching. cnotmatch, cnotlike, ..."; break }
  default                 { "Others" }
}
# try-catch-finally
try {
    throw "This is an error"   # Use "throw" to raise an error
}
catch {
    Write-Output $Error.ExceptionMessage
}
finally {
    Write-Output "We can clean up resources here"
}

IO / Files

# read / write
Write-Output "Hello, World!"            # alias: echo, write
"Hello, World!"                         # ... same!
Write-Host "Hello, World!"              # Direct to console!
$foo = $(Write-Host "Hello, World!")    # !!! LIKELY BAD !!!
$foo = $(Write-Output "Hello, World!")  # Good
$foo = Write-Output "Hello, World!"     # Better
$foo = "Hello, World!"                  # ... same ;-)
$foo = Read-Host "Enter foo"
$bar = Read-Host ("Enter {0}" -f $foo)
# Write to file
$contents = "Hello"
$contents | Out-File "$env:HOMEDRIVE\file.txt"

# Read from a file
$contents = Get-Content "file.txt"
New-Item c:\foo           # Create directory
New-Item -Path c:\foo -ItemType Directory # Same
Remove-Item c:\foo        # Remove directory
Test-Path c:\foo          # 'True'
Copy-Item c:\foo c:\bar   # Copy
New-Item c:\foo\sym -Value c:\baz -ItemType SymbolicLink -Force
                          # Symbolic link 
(gi c:\foo\sym).delete()  # Remove symlink (on posh <7.1)
ren old.txt new.txt
# Path manipulation
$path = "c:\foo\bar\baz.txt"
$parent = Split-Path -parent $path    # c:\foo\bar
$parent = Split-Path $path            # c:\foo\bar
$child = Join-Path $parent "foo.txt"  # c:\foo\bar\foo.txt
cd c:\foo\bar
$parent = Split-Path -resolve .     # c:\foo
# Directory
mkdir foo             # Fail if repeated
mkdir -f foo          # ... can be repeated
mkdir foo/bar         # Ok if foo doesn't exist
rmdir foo -ErrorAction ignore # To avoid fail if foo doesn't exist


# Grep-like
gal | findstr Get-Process
gal | where {$_.Definition -eq 'Get-Help'}
dir Alias: | where {$_.Definition -eq 'Get-Help'}

# Piping and Iterating
ls $Path | foreach {$_.Name}  
ls $Path | % {$_.Name}          # same
# ... size filter
ls $Path | where { $_.Length -ge 2*1024 } | % {$_.Name} 
# ... same, using gi and shorter notations (note extra parens)
gi $Path\* | ? Length -ge (2*1024) | % {$_.Name} 
# Selecting several attributes
gi $Path\* | ? Length -ge (2*1024) | select Name,Length
# Piping a single object
$old = gi old.txt
$old | ren -NameName new.txt
ls $Path | Measure-Object # wc-like
ls $Path | select -First 5 | head-like
ls $Path | select -Last 5 | tail-like

Get-Item $Path            # Get a file object
(Get-Item $Path).Parent   # Get parent directory
(Get-Item $Path).Parent.FullName   # ... get path only
(Get-Item $Path).Directory.Parent  # ... if $Path is a file
# ... These assumes $Path exists. If not, use Split-Path

Functions

# Functions: keep 'Verb-Noun' convention!

function Add-Numbers {   # No explicit args
 $args[0] + $args[1]
}

Add-Numbers 1 2          # 3

function Add-Numbers($first,$second) { # Explicit args
 $first + $second
}
function Add-Numbers {   # Explicit args with type
 param( [int]$first, [int]$second )
 $first + $second
}

Add-Numbers 1 2                # 3, still works
Add-Numbers -first 1 -second 2 # 3
Add-Numbers 1 -second 2        # 3
$params=@{first=1; second=2}
Add-Numbers @params            # 3, can pass hashtable as params

$res = $(Add-Numbers 1 2)
$res = Add-Numbers 1 2         # same

# Use Switch to build toggle flag
function Inc-Number($value,[Switch]$Verbose)
{
  if ($Verbose) { Write-Output $value }
  $value + 1
}
Inc-Number 1 -Verbose

Exec

dotnet.exe                             # Start an exe
&"c:\program files\dotnet\dotnet.exe"  # With space, use call operator '&' ...
."c:\program files\dotnet\dotnet.exe"  # ... or '.'
&"c:\program files\dotnet\dotnet.exe" --help # option outside the quotes

# We can run scriptblocks with '&' (in own scope)
$i = 2
$scriptBlock = { $i=5; Write-Output $i }
& $scriptBlock                         # 5
$i                                     # 2

# ... but '.' run in current scope
& $scriptBlock                         # 5
$i                                     # 5

# Testing command success with $? or checking for $null
$res = ...  # Some command exec
if ($? -and $res -ne $null) {
    Write-Host "Command succeeded"
}
else {
    Write-Host "Command failed"
}

Miscellaneous

# Script environment
$MyInvocation.MyCommand
# CommandType     Name      Version  Source
# -----------     ----      -------  ------
# ExternalScript  test.ps1           C:\tmp\test.ps1
$MyInvocation.MyCommand.Definition    # C:\tmp\test.ps1

# Namespaces
ls "HKLM:\Software\Microsoft\PowerShell\1\ShellIds"  # HKLM for registry
"$env:PATH"                                          # Env variables

Tips

Measure execution time of a command

Measure-Command { dir }
Measure-Command { dir | Out-default}   # To get output
Measure-Command { choco list }

Update help on offline computers

From MS devblogs:

# On online computer
New-Item c:\tmp\help
Save-Help -DestinationPath c:\tmp\help -Module * -Force

# On offline computer
# ... transfer files to c:\tmp\help
# ... start powershell in admin (Win-X-A)
Update-Help -SourcePath c:\tmp\help -Module * -Force

Find all links recursively

From SO:

dir 'd:\Temp' -recurse -force | ?{$_.LinkType} | select FullName,LinkType,Target

# In case of multi targets for hardlinks, this list them separated by TAB
dir 'd:\Temp' -recurse -force | ?{$_.LinkType} | select FullName,LinkType,@{ Name = "Targets"; Expression={$_.Target -join "`t"} }

Test if a path is a file or a folder

# https://devblogs.microsoft.com/scripting/powertip-using-powershell-to-determine-if-path-is-to-file-or-folder/

# Using object type
(Get-Item C:\fso) -is [System.IO.DirectoryInfo]           # True
(Get-Item C:\fso\csidl.txt) -is [System.IO.DirectoryInfo] # False

# Using PSIsContainer attribute
(Get-Item -Path C:\fso).PSIsContainer                     # True
(Get-Item -Path C:\fso\csidl.txt).PSIsContainer           # False

Read / Write / Modify file line by line

  • Reading a file is done through Get-Content.
  • Writing to a file is done through Set-Content.
  • DO NOT USE Out-File, because it does SLICE the long lines, hence modifying output, and also output non-matching (ie. empty) lines.
# This is slow on big file
Set-Content .\newfile.txt -Value (Get-content .\file.txt | Select-String -Pattern 'H\|159' -NotMatch)

# Another way using pipe and 
Get-Content .\file.txt | Where-Object { -not $_.Contains('H|159') } | Set-Content .\newfile.txt
  • Use foreach to process the file line by line, or ForEach-Object with a pipeline (see this blog).
  • foreach is a statement like if. It will always load the ENTIRE COLLECTION in an object before starting processing.
  • ForEach-Object is however pipeline aware, and have a lower memory usage (but is less performant).
# however foreach loads the ENTIRE file in memory first.
foreach($line in Get-Content .\file.txt) {
    if($line -match $regex){
        # Work here
    }
}

# Pipe-aware - Less memory, slower.
Get-Content .\file.txt | ForEach-Object {
    if($_ -match $regex){
        # Work here
    }
}

# Or use pipe and Where-Object
Get-Content .\file.txt | Where-Object {$_ -match $regex} | ForEach-Object {
    # Work here
}
# ... short syntax ...
gc 'file.txt' | ?{ $_ -match $regex } | %{ # Work here }
  • Use swith -regex -file to filter a file with regular expression.
'one
two
three' > file

$regex = '^t'

switch -regex -file file { 
  $regex { "line is $_" } 
}
  • With .NET library, use [System.IO.File] for better performance:
# Low memory and good performance
[System.IO.File]::ReadLines("C:\path\to\file.txt") | ForEach-Object {
       $_
}

# This is faster, but still foreach will load the whole file in memory first
foreach($line in [System.IO.File]::ReadLines("C:\yourpathhere\file.txt"))
{
    Write-Output $line
}

# ...
[System.IO.StreamReader]$sr = [System.IO.File]::Open($file, [System.IO.FileMode]::Open)
while (-not $sr.EndOfStream){
    $line = $sr.ReadLine()
}
$sr.Close()

Get-Content

Get-Content is aliased:

  • gc
  • cat
Get-Content .\File.txt
gc .\File.txt                # Shorter
cat .\File.txt               # Shorter... unix like
gc .\File.txt -TotalCount 5  # Limit output
# Process 
1..100 | ForEach-Object { Add-Content -Path .\LineNumbers.txt -Value "This is line $_." }
Get-Content -Path .\LineNumbers.txt

Regular expressions

Syntax is mostly the same as POSIX.

'book' -match 'oo'    # True
'book' -match 'o*'    # True
' - ' -match '\s- '   # True - Match whitespace

# Group and capture
'Hello, World!' -match '(Hello).*(World)' # True
$Matches
# Name         Value
# ----         -----
2              World
1              Hello
0              Hello, World
$Matches.1         # Hello
$Matches.Item(1)   # Hello
$Matches[1]        # Hello

# Replace
'John D. Smith' -replace '(\w+) (\w+)\. (\w+)', '$1.$2.$3@contoso.com'

Set-Location in a local scope

try {
    Push-Location .\Some\Path
    # stuff
}
finally {
    Pop-Location
}

Get relative path to another directory

Inspired from SO:

function Get-Relative-Path($fromPath,$toPath)
{
    try {
        Push-Location $fromPath; Get-Item $toPath | Resolve-Path -Relative
    }
    finally {
        Pop-Location
    }
}

From current directory:

function Get-Relative-Path($toPath)
{
    try {
        Push-Location "."; Get-Item $toPath | Resolve-Path -Relative
    }
    finally {
        Pop-Location
    }
}

Use Get-FileHash to compute sha1sum linux-like

References

# Powershell equivalent to sha1sum
#
# Usage:
#         sha1sum.ps1 path/to/file

$file=$args[0]
$sha1sum = (Get-FileHash -Algorithm SHA1 $file).Hash.ToLower()
"{0}  {1}" -f $sha1sum, $file.Replace("\","/")

Convert CRLF to LF like dos2unix

Reference:

# Convert CRLFs to LFs only.
#
# Usage:
#       dos2unix.ps1 [FILE...]
#
# Source:
#  * https://stackoverflow.com/questions/19127741/replace-crlf-using-powershell/19128003#19128003
# Note:
#  * (...) around Get-Content ensures that $file is read *in full*
#    up front, so that it is possible to write back the transformed content
#    to the same file.
#  * + "`n" ensures that the file has a *trailing LF*, which Unix platforms
#     expect.
#  * Use '-Encoding' to preserve input file encoding!

Get-Item $args | % { 
    Write-Output "dos2unix.ps1: converting file $_ to Unix format..."
    ((Get-Content $_) -join "`n") + "`n" | Set-Content -NoNewline $_
}

Alternative:

  • Convert-TextFile cmdlet with a -LineEnding in future PS version.

Issues

Resolve-Path / Directory.Parent returns wrong path

# This fails in PS 5.1, but is fixed in PS 7.2 at least
mkdir -f foo > $null
mkdir -f backup\foo\bin
echo "bar" > backup\foo\bin\bar.txt
$bar = $(gi backup\foo\bin\bar.txt)
$bar.Directory.Parent | Resolve-Path -Relative             # foo  -- BAD
gi $bar.Directory.Parent.FullName | Resolve-Path -Relative # backup\foo  -- GOOD