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 compare 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.

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 feature.
  • 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 arguments 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 new line by finishing a line by ^. In PowerShell, you can prefix any character by ` to escape it. Escaping character allow 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}</code>: Unicode escape sequence (<code>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 has 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 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 to combine 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:

#Replace exe by cmdlet

You should replace Windows specific executables by 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