PowerShell vs. Bash for Linux Users

PowerShell vs. Bash for Linux Users

If you prefer read the Spanish version, it is available here.

Current Windows systems allow the use of the Bash shell thanks to the Windows Subsystem for Linux (WSL). If we are regular Linux users and, for some reason, we are forced to use Windows, WSL offers us an easy way to use a command interface and tools that we know well and with which, surely, we feel much more comfortable than with the native ones from Windows.

However, when managing Windows systems, resorting to Linux ecosystem tools is usually not the best option because they present some limitations when used outside their original environment.

In this article, we will discuss the basic differences between PowerShell and Bash, and how these differences are conditioned by the characteristics of the ecosystem for which they were designed.

Before PowerShell

Until very recently, the default command line interface for Windows was the Command Prompt.

This was the original command line interface of Windows and has its roots in the old MS-DOS operating system. MS-DOS was a text-based operating system, developed for early PCs and popular in the 80s and early 90s, before graphical user interfaces became common.

MS-DOS 6 inteface executing the dir command.
MS-DOS 6 inteface executing the dir command.

One of the components of MS-DOS (and other versions of DOS) was the COMMAND.COM program, which ran at the end of system boot and implemented the command line interface. This interface and its scripting language (known as (.BAT) batch files) were very limited, compared to Unix/Linux shells, like Bash, mainly because of the limitations of the system itself and the hardware on which it was to run.

When Microsoft introduced Windows, it initially ran as a graphical user interface over MS-DOS. Over time, Windows evolved to become an independent operating system and introduced a new command line interface identified as the Command Prompt. This interface was implemented by the cmd.exe program, but its features were very similar to the old MS-DOS interface.

Command Prompt icon in Windows NT 3.1 with the MS-DOS logo.
Command Prompt icon in Windows NT 3.1 with the MS-DOS logo.

In addition, cmd.exe is closely linked to the architecture of the Windows operating system and cannot be easily ported to other operating systems. This means that scripts and commands that work on cmd.exe often don't work on other operating systems without significant modifications.

Command Prompt of Windows 10.
Command Prompt of Windows 10.

PowerShell was introduced by Microsoft in 2006 to address many of these limitations. PowerShell is a much more powerful shell and scripting language than cmd.exe, with support for object-based scripting and a set of features that is comparable to Unix/Linux shells. PowerShell has also been ported to other operating systems, including Linux and macOS.

Although cmd.exe is still available in modern versions of Windows (and it is still possible to run .BAT files) PowerShell has become the default shell of the system and is usually the preferred one for many system administration-related tasks.

Why PowerShell?

Since the early versions of Windows, Microsoft has always had a predilection for developing Graphical User Interfaces (GUIs). This is not only evident in the evolution of Windows, but also in the development of many associated applications and technologies, such as most of the wizards and system configuration and management tools.

However, over time, they realized that GUIs, despite their accessibility and ease of use for most users, have significant limitations when it comes to managing systems and automating tasks, especially on a large scale. PowerShell was the answer to this need.

The original intention was to include in Windows a set of tools similar to those of Unix1. However, this did not work as well as expected. The reason is that the Unix ecosystem is built around text files. In Unix/Linux, system core, services, and application configuration and information are available in the form of text files, so the multiple text processing tools that accompany Bash are effectively management tools.

However, Windows is an ecosystem built around APIs and binary objects (such as COM, WMI, Active Directory, the System Registry, etc.) so the traditional Unix text processing tools can't help too much in system management. It is for this reason they developed PowerShell, aiming to get a tool comparable to Unix/Linux shells but in the Windows ecosystem.

Character Strings vs. Objects

In Bash and other Unix/Linux shells, most operations are performed on character strings. The output of a command, through its standard output, is a string of characters, which can be redirected to a file or another command using the '>' and '|' operators, respectively. Similarly, commands can accept a string of characters through their standard input, which can come from a file or the output of another command using the '<' and '|' operators, respectively. And the variables used in scripts store and substitute as character strings.

For example, the following command can be used to list and filter processes:

ps aux | grep python

The ps aux command generates a detailed listing of all running processes, with a line of information for each process. The output of the command returns this information as a string of characters. The grep python command filters this text to display only the lines that contain the word "python". If this word appears in the command name, it is likely a Python language interpreter execution.

The grep python command does not search for the word in a specific property of the process but in the entire line containing its information. If, for example, we are only interested in listing the processes of the user “python” or the processes of the “python” command, we would need to find another solution.

In contrast, PowerShell handles the connection between commands differently. Instead of passing character strings, PowerShell can pass complete objects (in fact, they are .NET objects) where each one is an entity with properties and methods. This allows commands that are chained in a PowerShell pipeline using the '|' operator to be more aware of the structure and content of the information they receive.

For example, consider the PowerShell equivalent command of the previously mentioned Bash command:

Get-Process | Where-Object { $_.ProcessName -like "*python*" }

The Get-Process command generates a list of process objects, each of which has properties such as ProcessName, Id, etc. The Where-Object command iterates over the objects evaluating the indicated condition, to filter them based on their properties; instead of searching for the text "python" in an unstructured string of characters, like grep does. Specifically, in the previous example, Where-Object extracts the value of the ProcessName property of the current object and checks if it contains the substring "python". That is, it is easy to check conditions in any of the properties of each process listed returned by Get-Process.

Although it may seem so, the result of Where-Object is not text, but a list of objects that meet the indicated condition. Since the output of Where-Object will not be stored in a variable nor used with another command, the PowerShell interpreter converts it into text to display it to the user in the console.

Obviously, PowerShell can process texts like Bash does. It has its own tools, which accept and return character strings, in addition to being able to invoke the typical tools of Unix systems, if they are available on the system.

In both Unix/Linux and Windows, any process can have a standard input and output, being the mechanism by which shells (including Bash and PowerShell) can connect the text output of a command with the input of another. The difference is that, in PowerShell, additionally, functions and cmdlets allow implementing commands that receive and return objects, instead of just character strings.

In PowerShell, both worlds can be easily combined. For example, suppose we have a list of books in a text file books.json in JSON format.

    "books": [
        {"title": "1984", "author": "George Orwell", "year": 1949},
        {"title": "Brave New World", "author": "Aldous Huxley", "year": 1932},
        {"title": "Fahrenheit 451", "author": "Ray Bradbury", "year": 1953}

Filtering books according to some criteria and selecting various properties from this listing, using only text processing tools, can be very complex. However, we can convert the content of the file into lists of objects, to easily iterate over them, filter, and select the information we want to show to the user.

(Get-Content books.json | ConvertFrom-Json).books |
    Where-Object { $_.year -lt 1950 } | ForEach-Object { $_.title }

Cmdlets in PowerShell

Cmdlets (pronounced as command-let) are a type of PowerShell command implemented through .NET classes (inheriting from the System.Management.Automation.Cmdlet class). They are usually written in a language like C# and compiled into .DLL files, which PowerShell loads to execute the command within the context of the current interpreter or script. Therefore, they are lighter than other types of commands because they do not create an additional process every time they need to be executed.

Cmdlets are easy to identify because they are named following the "Verb-Noun" convention. For example, the Get-ChildItem command uses the verb Get followed by the noun ChildItem. When executed, it lists the contents of one or more paths, similarly to how the ls command works in Bash.

Get-ChildItem -Path C:\Users

This convention can make the commands longer compared to their equivalents in Bash, which may not be very comfortable when working with PowerShell interactively. However, the aim is to make them easy to remember and make scripts more readable and self-explanatory, which can facilitate understanding and code maintenance.

Cmdlets are typically small, in the sense that they perform a single specific function or task, in line with the philosophy of Unix commands to "do one thing and do it well". As we have mentioned, each cmdlet can accept objects as input and produce them as output, allowing several cmdlets to be chained together to perform complex operations in a single command.

Since cmdlets are implemented in the .NET framework, they have very easy access to system APIs, as well as to services and installed applications. Therefore, it is easy to create cmdlets that act as command line interfaces for these APIs, in order to facilitate the creation of scripts to automate all kinds of tasks.

For example, the PowerShell AzureAD and MSOnline modules offer a set of cmdlets that can be used to automate tasks or manage both Azure Active Directory and Office 365 from the shell2.

Returning to the Get-ChildItem command, thanks to the use of various system APIs, it not only serves to list directory contents, like ls in Bash, but also allows listing the contents of the certificate store or the system registry (where Windows saves its configuration):

Get-ChildItem -Path HKLM:\SOFTWARE

As Scripting Languages

Unix systems emerged in an era before the dominance of graphical user interfaces. In this context, shells like Bash were designed to be the primary interface for users, so they need to provide a very general set of tools and commands to solve a wide range of different tasks. Likewise, efficiency in writing and executing commands is essential, so short names for commands and options are often used.

In contrast, on Windows, it is expected that most of the user interactions with the system will be performed through the graphical interface, rather than a command line. PowerShell was not designed to be an alternative interface to the graphical one, but rather as a tool to facilitate task automation and system administration for more advanced users. Therefore, PowerShell comes with a series of cmdlets that simplify the administration and maintenance of Windows systems and these follow a naming convention that facilitates script readability and maintenance. However, this results in a more verbose syntax, at least for those accustomed to using Bash and other similar shells.

Another significant difference between both shells appears in the way commands are processed.

Bash is a traditional command-line interpreter, which can be said to operate linearly. That is, when a command is entered, it performs multiple expansions on the line in a specific order (variable expansion, command substitution, word splitting, or filename expansion) which replace different parts of the command.

In this process, no context is taken into account. For example, if wildcards are used — as in the expression A*.txt — the expansion will occur whether it has been put in the command name or in any of the arguments. If it is the latter case, the shell does not take into account whether a filename or something else is expected in that argument. The expansion will always occur, as long as there is any file that fits the expression.

In the end, after all the expansions, what Bash gets is a list of words from which it extracts the command name and its arguments to execute it.

The result is that, when using Bash as a scripting language, it turns out to be somewhat different from other programming languages. This does not mean that Bash is not used as a language to develop scripts for system administration, because obviously, every Unix/Linux system has many Bash scripts. But when scripts gain some complexity, many administrators and developers prefer to use more conventional languages, such as Python.

PowerShell, on the other hand, takes an approach more similar to that of a traditional programming language than a Unix-style command-line shell. Instead of interpreting linearly, successively substituting different parts, the PowerShell language has a well-defined grammar. As in any modern language, the interpreter tokenizes, parses, constructs the abstract syntax tree (AST) of the command, and finally evaluates the AST to execute the command. It is during the evaluation of the command elements, when all the necessary expansions are made, such as variable expansion, command substitution, or filename expansion. To make these expansions, PowerShell takes the context into account.

For example, in Bash, the following command prints all files that start with A and end with '.txt' in the current directory:

echo A*.txt

The echo command is used to print a string to standard output, but Bash will do the filename expansion and then call echo with all those files as command arguments.

However, in PowerShell, that same command only prints A*txt because echo —an alias for Write-Output— expects a string of characters as an argument, not a filename. Therefore, there is no reason for PowerShell to apply filename expansion, transforming the string.

Both scripting languages offer common constructs in any programming language, such as loops or conditionals. However, PowerShell's object-based approach favors the use of pipes to solve problems by combining cmdlets, rather than using these constructs.

Commands and Equivalences

The following table contains a list of the most commonly used commands in Bash, along with their equivalents in PowerShell and their aliases in this shell.

Sure, here's the translated content:

In BashIn PowerShellAlias in PowerShellDescription
lsGet-ChildItemgci, dir, lsList the items in a directory.
cdSet-Locationcd, chdir, slChange the current directory.
pwdGet-Locationpwd, glShow the path of the current directory.
manGet-Helpman, helpShow the help of a command.
cpCopy-Itemcp, copy, cpiCopy an item from one place to another.
mvMove-Itemmv, move, miMove an item from one place to another.
rmRemove-Itemrm, rmdir, del, erase, riRemove one or more items.
catGet-Contentcat, type, gcShow the content of a file.
echoWrite-Outputecho, writeWrite the output to the console.
findGet-ChildItem -Recurse | Where-Objectgci -r | ?Search files in a directory.
grepSelect-StringslsFilter contents of a file according to a pattern.
psGet-Processgps, psShow information about running processes.
killStop-Processspps, killStop a running process.

As can be seen, many of the PowerShell commands in the list have an alias with the name of the equivalent command in Bash. However, it's important to note that, while these commands are equivalent in terms of functionality, they may behave slightly differently or require different parameters depending on the command and the operating system.

  1. "Is PowerShell ready to replace my Cygwin shell on Windows?". Stack Overflow. Accessed June 9, 2023. stackoverflow.com/questions/573623/is-power..

  2. "Manage Microsoft 365 with Microsoft 365 PowerShell". Microsoft Learn. Accessed June 9, 2023. learn.microsoft.com/en-us/microsoft-365/ent..