Convert cmd scripts to PowerShell
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 compare to cmd:
- Discoverability: PowerShell makes it easy to discover its features thanks to the auto-complete, the command
Get-Command
andGet-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.
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
echo %test%
echo $env:test
If the variable name has complex characters you need to use curly brackets (${env:name}
)
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.
$test="a b"
sample.exe $test # 1 argument
$test=@("a", "b") # Declare an array
sample.exe $test # 2 arguments
##Set environment variable
set test=abc
$env:test="abc"
Set-Item -Path Env:test -Value "abc"
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#:
# 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
$test="abc" # the variable is only valid in the current script
echo $test
##List environment variables
set
Get-ChildItem env:
##Get current directory
echo %cd%
echo $pwd
##Get script directory
echo %~dp0
echo $PSScriptRoot
##Get exit code
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.
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 .\
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 >
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:
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
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.
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:
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:
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.
call %exe%
& $env:exe
It is also necessary if the command must be quoted:
PS> & ".\script name with spaces.ps1"
Documentation:
##call a bat script
You can call bat script using cmd.exe
:
myscript.bat
cmd.exe /c myscript.bat
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
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:
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.
# 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.
echo %var:~0,3%
echo $var.Substring(0, 3)
echo %var:~-3%
echo $var.Substring($var.Length - 3)
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:
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:
if (Test-Connection -TargetName Server01 -Quiet)
{
New-PSSession -ComputerName Server01
}
#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 -Force
del /s /q file.txt
→Remove-Item file.txt -Recurse -Force
sc
→Get-Service
/Start-Service
/Stop-Service
/New-Service
/Remove-Service
/ etc.taskkill
→Stop-Process
xcopy
→Copy-Item
Do you have a question or a suggestion about this post? Contact me!