Powershell: Difference between revisions
Jump to navigation
Jump to search
(→Tips) |
|||
(37 intermediate revisions by the same user not shown) | |||
Line 3: | Line 3: | ||
* [https://docs.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-arrays?view=powershell-7.2 Everything you wanted to know about arrays - microsoft.com] |
* [https://docs.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-arrays?view=powershell-7.2 Everything you wanted to know about arrays - microsoft.com] |
||
* [https://docs.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-hashtable?view=powershell-7.2 Everything you wanted to know about hashtables - microsoft.com] |
* [https://docs.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-hashtable?view=powershell-7.2 Everything you wanted to know about hashtables - microsoft.com] |
||
* [https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-7.2 About preference variables - microsoft.com] |
|||
* [https://devblogs.microsoft.com/scripting/getting-to-know-foreach-and-foreach-object/ Getting to Know ForEach and ForEach-Object - microsoft.com] |
|||
* [https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-content?view=powershell-7.2 Get-Content - microsoft.com] |
|||
* [https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_scopes?view=powershell-7.2 about_Scopes - microsoft.com] |
|||
== Reference == |
== Reference == |
||
Line 8: | Line 12: | ||
=== Files === |
=== Files === |
||
Powershell scripts have a {{file|.ps1}} extension. |
Powershell scripts have a {{file|.ps1}} extension. |
||
=== Environment === |
|||
Powershell uses '''preference''' variables [https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-7.2]: |
|||
* <code>$ErrorActionPreference</code>: value can be <code>'SilentlyContinue'</code>, <code>'Stop'</code>, <code>'Continue'</code> (default), <code>'Inquire'</code>, <code>'Ignore'</code>, <code>'Suspend'</code>, or <code>'Break'</code>. |
|||
To get powershell version: |
|||
<source lang="powershell"> |
|||
Get-Host |
|||
Get-Host | select version |
|||
# or |
|||
$PSVersionTable |
|||
</source> |
|||
=== Syntax === |
=== Syntax === |
||
Line 73: | Line 90: | ||
# echo -> Write-Output |
# echo -> Write-Output |
||
# write -> Write-Output |
# write -> Write-Output |
||
</source> |
|||
</div> |
|||
==== Scopes ==== |
|||
<div style="column-width:35em;-webkit-column-width:35em;-moz-column-width:35em;"> |
|||
<source lang="powershell" style="overflow: hidden"> |
|||
# 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 |
|||
} |
|||
</source> |
</source> |
||
</div> |
</div> |
||
Line 90: | Line 124: | ||
10 % 3 # 1 |
10 % 3 # 1 |
||
[Math]::Pow(2,3) # 8 |
[Math]::Pow(2,3) # 8 |
||
</source> |
|||
<source lang="powershell" style="overflow: hidden"> |
|||
# Null is empty reference - assigning to it is ignored |
|||
$null = "foo" |
|||
$null # None |
|||
</source> |
</source> |
||
Line 97: | Line 137: | ||
$False # False |
$False # False |
||
!$True # False |
!$True # False |
||
!(Test-Path "c:\foo") # Use (...) for expr |
|||
-not $True # False |
-not $True # False |
||
$True -and $True |
$True -and $True |
||
Line 120: | Line 161: | ||
1 -ge 1 |
1 -ge 1 |
||
# With |
# With strings |
||
"foo" -eq "foo" # True |
"foo" -eq "foo" # True |
||
"foo" -eq "FOO" # True, insensitive! |
"foo" -eq "FOO" # True, insensitive! |
||
"foo" - |
"foo" -ieq "FOO" # True (insensitive) |
||
"foo" -ceq "FOO" # False (sensitive) |
|||
"a" -clt "A" # True ! (strange) |
"a" -clt "A" # True ! (strange) |
||
# With objects |
|||
(1,2,3,1,1) -eq 1 # 1,1,1 |
(1,2,3,1,1) -eq 1 # 1,1,1 |
||
@{ |
@{a=1} -eq @{a=1} # False ! |
||
</source> |
</source> |
||
Line 157: | Line 200: | ||
'First line`nSecond line' # Escape with backtick |
'First line`nSecond line' # Escape with backtick |
||
"foo" | gm # Get all methods / properties |
"foo" | gm # Get all methods / properties |
||
"foo".ToLower() # ... Convert to lowercase |
|||
"foo".ToUpper() # ... Convert to uppercase |
|||
# Arrays |
# Arrays |
||
$fixed=1,2,3 |
|||
# TBC |
|||
$fixed.Add(4) # ! Exception |
|||
$fixed.Item(1) # 2 |
|||
$fixed[1] # 2 |
|||
# Use .Net for variable (and faster!) arrays |
|||
# Dictionaries |
|||
[System.Collections.ArrayList]$array = @() |
|||
# TBC |
|||
[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 |
|||
</source> |
</source> |
||
</div> |
</div> |
||
Line 267: | Line 333: | ||
# Symbolic link |
# Symbolic link |
||
(gi c:\foo\sym).delete() # Remove symlink (on posh <7.1) |
(gi c:\foo\sym).delete() # Remove symlink (on posh <7.1) |
||
ren old.txt new.txt |
|||
</source> |
</source> |
||
<source lang="powershell" style="overflow: hidden"> |
|||
# 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 |
|||
</source> |
|||
<source lang="powershell" style="overflow: hidden"> |
|||
# 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 |
|||
</source> |
|||
<source lang="powershell" style="overflow: hidden"> |
<source lang="powershell" style="overflow: hidden"> |
||
Line 275: | Line 361: | ||
dir Alias: | where {$_.Definition -eq 'Get-Help'} |
dir Alias: | where {$_.Definition -eq 'Get-Help'} |
||
# Piping and Iterating |
|||
# Iterating on every files in $Path |
|||
ls $Path | foreach {$_.Name} |
ls $Path | foreach {$_.Name} |
||
ls $Path | % {$_.Name} # same |
ls $Path | % {$_.Name} # same |
||
Line 284: | Line 370: | ||
# Selecting several attributes |
# Selecting several attributes |
||
gi $Path\* | ? Length -ge (2*1024) | select Name,Length |
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 # Get a file object |
||
Line 295: | Line 387: | ||
==== Functions ==== |
==== Functions ==== |
||
<div style="column-width: |
<div style="column-width:50em;-webkit-column-width:50em;-moz-column-width:50em;"> |
||
<source lang="powershell |
<source lang="powershell"> |
||
# Functions: keep 'Verb-Noun' convention! |
# Functions: keep 'Verb-Noun' convention! |
||
Line 313: | Line 405: | ||
} |
} |
||
Add-Numbers 1 2 # 3 |
Add-Numbers 1 2 # 3, still works |
||
Add-Numbers -first 1 -second 2 # 3 |
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) |
$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 |
|||
</source> |
</source> |
||
Line 398: | Line 501: | ||
# In case of multi targets for hardlinks, this list them separated by TAB |
# 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"} } |
dir 'd:\Temp' -recurse -force | ?{$_.LinkType} | select FullName,LinkType,@{ Name = "Targets"; Expression={$_.Target -join "`t"} } |
||
</source> |
|||
=== Test if a path is a file or a folder === |
|||
<source lang="powershell"> |
|||
# 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 |
|||
</source> |
|||
=== Read / Write / Modify file line by line === |
|||
* Reading a file is done through <code>Get-Content</code>. |
|||
* Writing to a file is done through <code>Set-Content</code>. |
|||
:* DO NOT USE <code>Out-File</code>, because it does SLICE the long lines, hence modifying output, and also output non-matching (ie. empty) lines. |
|||
<source lang="powershell"> |
|||
# 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 |
|||
</source> |
|||
* Use <code>foreach</code> to process the file line by line, or <code>ForEach-Object</code> with a pipeline (see [https://devblogs.microsoft.com/scripting/getting-to-know-foreach-and-foreach-object/ this blog]). |
|||
:* <code>foreach</code> is a statement like <code>if</code>. It will always load the ENTIRE COLLECTION in an object before starting processing. |
|||
:* <code>ForEach-Object</code> is however pipeline aware, and have a lower memory usage (but is less performant). |
|||
<source lang="powershell"> |
|||
# 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 } |
|||
</source> |
|||
* Use <code>swith -regex -file</code> to filter a file with regular expression. |
|||
<source lang="powershell"> |
|||
'one |
|||
two |
|||
three' > file |
|||
$regex = '^t' |
|||
switch -regex -file file { |
|||
$regex { "line is $_" } |
|||
} |
|||
</source> |
|||
* With .NET library, use <code>[System.IO.File]</code> for better performance: |
|||
<source lang="powershel"> |
|||
# 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() |
|||
</source> |
|||
=== Get-Content === |
|||
* Source: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-content?view=powershell-7.2 |
|||
<code>Get-Content</code> is aliased: |
|||
* <code>gc</code> |
|||
* <code>cat</code> |
|||
<source lang="powershell"> |
|||
Get-Content .\File.txt |
|||
gc .\File.txt # Shorter |
|||
cat .\File.txt # Shorter... unix like |
|||
gc .\File.txt -TotalCount 5 # Limit output |
|||
</source> |
|||
<source lang="powershell"> |
|||
# Process |
|||
1..100 | ForEach-Object { Add-Content -Path .\LineNumbers.txt -Value "This is line $_." } |
|||
Get-Content -Path .\LineNumbers.txt |
|||
</source> |
|||
=== Regular expressions === |
|||
* Source: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_regular_expressions?view=powershell-7.2 |
|||
Syntax is mostly the same as POSIX. |
|||
<source lang="powershell"> |
|||
'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' |
|||
</source> |
|||
=== Set-Location in a local scope === |
|||
<source lang="powershell"> |
|||
try { |
|||
Push-Location .\Some\Path |
|||
# stuff |
|||
} |
|||
finally { |
|||
Pop-Location |
|||
} |
|||
</source> |
|||
=== Get relative path to another directory === |
|||
Inspired from [https://stackoverflow.com/questions/10972589/get-relative-path-of-files-in-sub-folders-from-the-current-directory SO]: |
|||
<source lang="powershell"> |
|||
function Get-Relative-Path($fromPath,$toPath) |
|||
{ |
|||
try { |
|||
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 { |
|||
Pop-Location |
|||
} |
|||
} |
|||
</source> |
|||
=== Use Get-FileHash to compute sha1sum linux-like === |
|||
References |
|||
* https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-filehash?view=powershell-7.2 |
|||
<source lang="powershell"> |
|||
# Powershell equivalent to sha1sum |
|||
# |
|||
# Usage: |
|||
# sha1sum.ps1 [FILE...] |
|||
function Get-Relative-Path($toPath) |
|||
{ |
|||
try { |
|||
Push-Location "."; Get-Item $toPath | Resolve-Path -Relative |
|||
} |
|||
finally { |
|||
Pop-Location |
|||
} |
|||
} |
|||
Get-Item $args | % { |
|||
$sha1sum = (Get-FileHash -Algorithm SHA1 $_).Hash.ToLower() |
|||
"{0} {1}" -f $sha1sum, (Get-Relative-Path "$_") |
|||
} |
|||
</source> |
|||
=== Convert CRLF to LF like dos2unix === |
|||
Reference: |
|||
* https://stackoverflow.com/questions/19127741/replace-crlf-using-powershell/19128003#19128003 |
|||
<source lang="powershell"> |
|||
# 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! |
|||
function Get-Relative-Path($toPath) |
|||
{ |
|||
try { |
|||
Push-Location "."; Get-Item $toPath | Resolve-Path -Relative |
|||
} |
|||
finally { |
|||
Pop-Location |
|||
} |
|||
} |
|||
Get-Item $args | % { |
|||
Write-Output ("dos2unix.ps1: converting file {0} to Unix format..." -f (Get-Relative-Path $_)) |
|||
((Get-Content $_) -join "`n") + "`n" | Set-Content -NoNewline $_ |
|||
} |
|||
</source> |
|||
Alternative: |
|||
* <code>Convert-TextFile</code> cmdlet with a <code>-LineEnding</code> in future PS version. |
|||
== Issues == |
|||
=== Resolve-Path / Directory.Parent returns wrong path === |
|||
<source lang="powershell"> |
|||
# 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 |
|||
</source> |
</source> |
Latest revision as of 12:16, 3 June 2022
Links
- Learn powershell in Y minutes — A MUST-READ to learn rapidly.
- Everything you wanted to know about arrays - microsoft.com
- Everything you wanted to know about hashtables - microsoft.com
- About preference variables - microsoft.com
- Getting to Know ForEach and ForEach-Object - microsoft.com
- Get-Content - microsoft.com
- about_Scopes - microsoft.com
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! |
Scopes
Primitive types / operators
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
IO / Files
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
Miscellaneous
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.
- DO NOT USE
# 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, orForEach-Object
with a pipeline (see this blog).
foreach
is a statement likeif
. 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 [FILE...]
function Get-Relative-Path($toPath)
{
try {
Push-Location "."; Get-Item $toPath | Resolve-Path -Relative
}
finally {
Pop-Location
}
}
Get-Item $args | % {
$sha1sum = (Get-FileHash -Algorithm SHA1 $_).Hash.ToLower()
"{0} {1}" -f $sha1sum, (Get-Relative-Path "$_")
}
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!
function Get-Relative-Path($toPath)
{
try {
Push-Location "."; Get-Item $toPath | Resolve-Path -Relative
}
finally {
Pop-Location
}
}
Get-Item $args | % {
Write-Output ("dos2unix.ps1: converting file {0} to Unix format..." -f (Get-Relative-Path $_))
((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