Convert cmd scripts to PowerShell

 
 
  • Gérald Barré

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:\MyFolderRemove-Item C:\MyFolder -Recurse -Force
  • del /s /q file.txtRemove-Item file.txt -Recurse -Force
  • scGet-Service / Start-Service / Stop-Service / New-Service / Remove-Service / etc.
  • taskkillStop-Process
  • xcopyCopy-Item

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub