I recently moved many scripts from cmd (.bat or .cmd) to PowerShell (.ps1). In this post, I'll show all the differences I've seen during the migration. This is list is not exhaustive as it only contains the cases I had encountered in the migrated scripts.
#Why using PowerShell instead of cmd?
PowerShell has many advantages compared to cmd:
- Discoverability: PowerShell makes it easy to discover its features thanks to the auto-complete, the command
Get-Command and Get-Help. - Consistency: The consistency of PowerShell is one of its primary assets. For example, if you learn how to use the
Sort-Object cmdlet, you can use that knowledge to sort the output of any cmdlet. You don't have to learn the different sorting routines of each cmdlet. - Interactive and scripting environments: PowerShell combines an interactive shell and a scripting environment. PowerShell can access command-line tools, COM objects, and .NET class libraries.
- Object orientation: PowerShell is based on object, not text. The output of a command is an object. You can send the output object, through the pipeline, to another command as its input.
- Cross-platform: PowerShell runs on Windows, Linux, and Mac.
- Testable using Pester
If you still have a doubt about using PowerShell instead of cmd, Rich Turner explains clearly that PowerShell is the future:
Cmd is in maintenance mode. It's job is to preserve back compat with ancient & immutable scripts. That's it. Period. It should not be used for interactive shell work.
PowerShell is the future.
Rich Turner, Sr. Program Manager, Windows Console & Command-Line
#There are 2 flavors of PowerShell
There are 2 flavors of PowerShell:
- PowerShell (
powershell.exe) is a component of Windows. This version works only on Windows and won't receive any new features. - PowerShell Core (
pwsh.exe) is available on GitHub. This flavor works on Windows, Linux, and Mac. This is the version Microsoft works on.
If you have the choice, go with the latest version of pwsh. You can find it on GitHub: Download PowerShell.
#cmd/PowerShell differences
##Expand environment variables
PowerShell
echo %test%
echo $env:test
If the variable name has complex characters you need to use curly brackets (${env:name})
PowerShell
echo %ProgramFiles(x86)%
echo ${env:ProgramFiles(x86)}
The behavior is different when you use the environment variable as an argument. cmd expand the environment variables and then execute the command line. So, you need to escape arguments. PowerShell consider it as a single argument and escape it correctly without any additional work.
PowerShell
$test="a b"
sample.exe $test # 1 argument
$test=@("a", "b") # Declare an array
sample.exe $test # 2 arguments
##Set environment variable
PowerShell
set test=abc
$env:test="abc"
Set-Item -Path Env:test -Value "abc"
PowerShell
set /p test=<MyFilename.txt
$env:test=Get-Content MyFilename.txt -First 1 # Read only the first line to match the cmd behavior
You can set the environment variables for the current user or machine using C#:
PowerShell
# Get the value
$path = [Environment]::GetEnvironmentVariable('PSModulePath', 'Machine') # User or Machine
# Set the value
[Environment]::SetEnvironmentVariable("PSModulePath", $newpath, 'Machine')
Note that if the variable is used only in the script, you can use local variables instead of environment variables
PowerShell
$test="abc" # the variable is only valid in the current script
echo $test
##List environment variables
PowerShell
set
Get-ChildItem env:
##Get current directory
PowerShell
echo %cd%
echo $pwd
##Get script directory
PowerShell
echo %~dp0
echo $PSScriptRoot
##Get exit code
PowerShell
ping 127.0.0.1
echo %errorlevel%
echo $lastexitcode
If the previous command is a cmdlet, you can use $? to know if the command succeeds. $? returns a boolean value.
PowerShell
PS> Get-ChildItem
PS> echo $?
True
Documentation:
##Execute executable in the current directory
To execute a file that is in the current directory, you need to prefix the name by .\
PowerShell
myexe.exe
.\myexe.exe
##echo has a different behavior
In cmd, echo prints the argument as is. In PowerShell, echo is an alias for Write-Output which prints each argument on a line.
$> echo a b c
a b c
PS> echo a b c
a
b
c
PS> echo "a b c"
a b c
PS> echo a b c | Join-String -Separator " "
a b c
Documentation:
##Redirecting output to a file
The syntax is the same, you can use command > filename except there must be a space before the >
PowerShell
ping 127.0.0.1> output.txt
ping 127.0.0.1 > output.txt # There must be a space before ">"
ping 127.0.0.1 >> output.txt
ping 127.0.0.1 >> output.txt # There must be a space before ">>"
You can also use the Out-File cmdlet which provides more options such as file encoding:
PowerShell
ping 127.0.0.1 > output.txt
ping 127.0.0.1 | Out-File output.txt
ping 127.0.0.1 >> output.txt
ping 127.0.0.1 | Out-File output.txt -Append
Documentation:
##Escaping characters
In cmd, you can prefix a character by ^ to escape it. For instance, you can insert a new line by finishing a line by ^. In PowerShell, you can prefix any character by ` to escape it. Escaping character allows to:
Remove the special meaning of the character
PowerShell
PS> $def="variable"
PS> Write-Host "Value of `$def: $def"
Value of $def: variable
PS> Write-Host abc `| def
abc | def
PS> Write-Host "abc`"def"
abc"def
PS> Write-Host "abc``def"
abc`def
To represent special character
`0: null`a: Alert`b: Backspace`e: Escape`f: Form feed`n: New line`r: Carriage return`t: Horizontal tab`v: Vertical tab`u{x}: Unicode escape sequence (`u{2195} represent the up down arrow (↕) symbol)
Documentation:
##Quoting arguments
The quoting rules are not the same in cmd and PowerShell. Mainly because PowerShell has the notion of expression and a special behavior for arguments starting with a dash -. In argument mode, each value is treated as an expandable string unless it begins with one of the following special characters: dollar sign ($), at sign (@), single quotation mark ('), double quotation mark ("), or an opening parenthesis ((). In these cases, the value is treated as an expression and is evaluated.
PowerShell
PS> echo 2+2
2+2
PS> echo (2+2)
4
PS> echo "(2+2)"
(2+2)
PS> $a = 2+2
PS> echo $a
4
Arguments starting with a dash have special parsing rules, so you sometimes need to escape them:
PowerShell
PS> echo arg=abc.txt # 1 argument
arg=abc.txt
PS> echo -arg=abc.txt # 2 arguments
-arg=abc
.txt
PS> echo "-arg=abc.txt"
-arg=abc.txt
The parameter -- is a special parameter that indicates to not bind parameters after this.
PS> Write-Output -InputObject
Write-Output: Missing an argument for parameter 'InputObject'. Specify a parameter of type 'System.Management.Automation.PSObject' and try again.
PS> Write-Output -- -InputObject
-InputObject
The stop-parsing symbol (--%) directs PowerShell to refrain from interpreting input as PowerShell commands or expressions. When it encounters a stop-parsing symbol, PowerShell treats the remaining characters in the line as a literal. The only interpretation it performs is to substitute values for environment variables that use standard Windows notation, such as %USERPROFILE%. The stop-parsing symbol is effective only until the next newline or pipeline character. For example, the 2 following commands are equivalents:
PowerShell
PS> icacls X:\VMS /grant Dom\HVAdmin:`(CI`)`(OI`)F
PS> icacls X:\VMS --% /grant Dom\HVAdmin:(CI)(OI)F
Documentation:
##call command
The call operator lets you run commands that are stored in variables and represented by strings or script blocks.
PowerShell
call %exe%
& $env:exe
It is also necessary if the command must be quoted:
PowerShell
PS> & ".\script name with spaces.ps1"
Documentation:
##call a bat script
You can call bat script using cmd.exe:
PowerShell
myscript.bat
cmd.exe /c myscript.bat
PowerShell
folder/myscript.bat
cmd.exe /c folder\myscript.bat # must use backslash on Windows
Note that the environment variables set in the bat script won't be available in the PowerShell host after the script is executed.
##if block
PowerShell
set a=0
if %a% == 0 (
echo true
) else (
echo false
)
$a=0
if ($a -eq 0)
{
echo true
}
else
{
echo false
}
You can replace if defined VARIABLE with the Test-Path cmdlet:
PowerShell
if defined TEST (action) else (action)
if (Test-Path env:TEST) { action } else { action }
if not defined TEST (action)
if (-not (Test-Path env:TEST)) { action }
Documentation:
##Combining commands using && and ||
These 2 operators allow combining commands. They are supported starting with PowerShell 7. If you use an older version, you need to replace them with if statements.
PowerShell
# Test-Command '1' || Test-Command '2'
Test-Command '1'; if (-not $?) { Test-Command '2' }
# Test-Command '1' && Test-Command '2'
Test-Command '1'; if ($?) { Test-Command '2' }
Documentation:
##String manipulation
You can use any .NET functions to manipulate strings. So, you can substring, replace, etc. using System.String.* methods.
PowerShell
echo %var:~0,3%
echo $var.Substring(0, 3)
PowerShell
echo %var:~-3%
echo $var.Substring($var.Length - 3)
PowerShell
set test=123meziantou
echo %test:123=Hello %
$test="123meziantou"
echo $test.Replace("123", "Hello ")
Documentation:
##Wait for a few seconds
In cmd, it is common to use ping 127.0.0.1 to wait for a few seconds. In PowerShell, you can use the Sleep cmdlet:
PowerShell
Start-Sleep -Seconds 5
##Test if a machine is accessible
You can validate that a remote machine is accessible before doing an action using the Test-Connection cmdlet:
PowerShell
if (Test-Connection -TargetName Server01 -Quiet)
{
New-PSSession -ComputerName Server01
}
##Editing registry keys
You can use the Get-ItemProperty and Set-ItemProperty cmdlets to read and write registry keys:
PowerShell
# List values
Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows NT\"
# List values recursively
Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows NT\" -Recurse
# Read value
Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ProductName
# Create registry key
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft" -Name "Edge" -Force
# Create registry value
New-ItemProperty -Path HKLM:\SOFTWARE\Policies\Microsoft\Edge -Name "BuiltInDnsClientEnabled" -Value "Yes" -Force
#Replace exe by cmdlet
You should replace Windows-specific executables with cross-platform and more readable cmdlets when possible.
rmdir /s /q C:\MyFolder → Remove-Item C:\MyFolder -Recurse -Forcedel /s /q file.txt → Remove-Item file.txt -Recurse -Forcesc → Get-Service / Start-Service / Stop-Service / New-Service / Remove-Service / etc.taskkill → Stop-Processxcopy → Copy-Item
If you are curious enough, the following cmdlets are the only thing you need to learn PowerShell😉
Get-HelpGet-CommandGet-Member
Do you have a question or a suggestion about this post? Contact me!