{"article":{"slug":"i-cant-believe-powershell-is-actually-good","title":"I can't believe PowerShell is actually good","subtitle":null,"summary":"Corbin Davenport of The Spacebar admits he underestimated PowerShell after Bash scripting frustrations, showing how its native data types, easy web API parsing, and .NET objects made a real scripting job, migrating old Tumblr posts with their metadata, far easier.","content_type":"opinion","language":"en","canonical_url":"https://www.spacebar.news/powershell-is-actually-good/","author":{"name":"Corbin Davenport","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"The Spacebar","url":"https://www.spacebar.news/","listing_slug":null,"listing":null},"topics":[{"name":"Developer Tools","slug":"developer-tools","url":"https://listedarticles.com/topics/developer-tools"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Opinion","slug":"opinion","url":"https://listedarticles.com/topics/opinion"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":2286,"reading_minutes":10,"published_at":"2026-10-21T15:00:00.000Z","added_at":"2026-10-22T05:07:27.943Z","updated_at":"2026-10-22T05:07:27.943Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/i-cant-believe-powershell-is-actually-good","markdown_url":"https://listedarticles.com/articles/i-cant-believe-powershell-is-actually-good.md","example":false,"citation":"Corbin Davenport, The Spacebar. \"I can't believe PowerShell is actually good.\" 21 Oct 2026. https://www.spacebar.news/powershell-is-actually-good/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://www.spacebar.news/powershell-is-actually-good/"},"body_markdown":"Microsoft makes a *lot* of bad software. That includes many aspects of Windows, from [the primary email application being a web wrapper](https://www.howtogeek.com/new-outlook-still-just-as-bad-after-six-months/) to [the ever-present Copilot buttons](https://www.theregister.com/software/2026/03/23/microsoft-removing-some-copilots-will-improve-windows-11/5228713). As such, it's always a surprise to me when I find a Microsoft product that *improves* my life in some respect, especially when it's something that came from Windows.\n\nI never paid much attention to PowerShell, the command-line shell and scripting tool that [first arrived in 2006](https://devblogs.microsoft.com/powershell/windows-powershell-monad-has-arrived/), and became a bundled application with the release of Windows 7. For the time period I used Windows as my main desktop operating system, I mostly stuck to the Command Prompt and Batch scripts, or [Bash in WSL](https://learn.microsoft.com/en-us/windows/wsl/about) or Python for more complex tasks. The modern version of PowerShell is cross-platform, and can also be installed on Mac and Linux.\n\nEarlier this year, I needed to learn PowerShell scripting for one Windows-related project, and I have tried it out a few more times on Mac. It has quickly become one of my favorite tools for automation and scripting—taking on jobs that I would have otherwise tried to do with Node.js, Shortcuts, macOS Automator, or heaven forbid, Bash scripts.\n\nI get it now. PowerShell is great, even if you don't use it as your shell, and even if you're on Mac or Linux.\n\n## Bashing my head against the wall\n\nThe most common way to run a sequence of commands on Linux, macOS, and other Unix-based environments is with a [Bash script](https://www.geeksforgeeks.org/linux-unix/introduction-linux-shell-shell-scripting/). They are also called shell scripts, even though they're [not *technically* the same thing](https://www.geeksforgeeks.org/linux-unix/bash-script-difference-between-bash-script-and-shell-script/), because they are also used by other shells like [Z Shell](https://en.wikipedia.org/wiki/Z_shell) (zsh).\n\nHere's an example Bash script that asks you for a media URL, and then uses the [yt-dlp application](https://www.spacebar.news/yt-dlp-best-way-to-download-videos-audio/) to save it as an MP3 audio file to your Desktop folder:\n\n```\n#!/bin/bash\n\necho -e \"Enter a URL: \"\nread url\ncd ~/Desktop\nyt-dlp -x --audio-format mp3 \"$url\"\necho \"Done!\"\n```\n\nBash scripts are fine for just running a series of commands. You can use variables and loops, code you use more than once can go in a function, and there's even limited support for data types like arrays. They can also *sometimes* work on a wide variety of hardware and operating systems.\n\nFor anything slightly more complicated, Bash scripting is just [the worst](https://www.youtube.com/watch?v=ETDHptCqRgQ). There are almost no \"built-in\" functions—you are supposed to use [POSIX commands](https://en.wikipedia.org/wiki/List_of_POSIX_commands) for common tasks, like file operations or parsing text. Anything beyond that depends on external tools, like [cURL](https://en.wikipedia.org/wiki/CURL) or [Wget](https://en.wikipedia.org/wiki/Wget) for retrieving web content, [several](https://v5.chriskrycho.com/journal/read-the-manual/pbcopy-pbpaste/) [different](https://github.com/bugaevc/wl-clipboard) [applications](https://github.com/astrand/xclip) for accessing the clipboard, and [jq](https://shapeshed.com/jq-json/) for reading JSON data. You might also need [boilerplate code for parsing options and parameters](https://www.redhat.com/en/blog/arguments-options-bash-scripts).\n\nIt is also surprisingly difficult to make a Bash script that works reliably on *other people's computers*. Some people might try your script in a different shell that doesn't interpret scripts in the same way—that's why [the command for installing Homebrew](https://brew.sh/) starts with calling the `bash` shell specifically, and many other tools use the same trick. Someone might only have cURL installed for fetching web content, or only Wget, or both, or neither! You can't assume every system will have the same basic set of POSIX commands, or even the `sudo` command, as [I learned a few months ago](https://github.com/corbindavenport/just-the-browser/issues/32).\n\nThe limitations of Bash scripting usually pushed me towards actual programming languages, even for tasks that didn't strictly need them. Python might be the best alternative, but its lack of curly braces makes me sad. I write a lot of JavaScript code, so [Node.js](https://nodejs.org/en) is an option, but I have to put `await` operators everywhere for system operations and [hope that I'm not installing malware](https://www.microsoft.com/en-us/security/blog/2026/05/29/33-malicious-npm-packages-abuse-dependency-confusion-profile-developer-environments/) when more dependencies are needed.\n\n## Hello, PowerShell\n\nLike many other products from Microsoft (and Google), PowerShell is actually [two different things](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_editions?view=powershell-7.6) with similar names. The one you should use is PowerShell 7, also called PowerShell Core, which is the modern cross-platform version based on .NET Core. The other one is Windows PowerShell (v5.1 and below), which is based on .NET Framework and not actively developed. Windows 11 still [only includes Windows PowerShell](https://github.com/PowerShell/PowerShell/discussions/24340), and not PowerShell 7.\n\nPowerShell 7 is a [free download](https://learn.microsoft.com/en-us/powershell/scripting/install/install-powershell?view=powershell-7.6) on Windows, Mac, and Linux. On my Mac computers, I [used Homebrew to install it](https://formulae.brew.sh/formula/powershell), and then I can run it with `pwsh` in the Terminal.\n\n![A Terminal window running an echo command in ZSH and PowerShell](https://storage.ghost.io/c/91/6a/916a9f97-6ca5-4206-befd-00d97de05b3d/content/images/2026/10/image-20.png)\n\nThe basic premise of a PowerShell script isn't much different than a Bash script. It's a list of commands, either provided by the shell or the underlying system, with the ability to use additional functions and keywords (like `if`, `foreach`, or `try`/`catch`) as needed.\n\nPowerShell has many built-in commands, called \"cmdlets\" (pronounced \"command-lets\"), and you can [install more](https://learn.microsoft.com/en-us/powershell/module/powershellget/install-module) or make your own. The names for most of them are [verb-noun pairs](https://learn.microsoft.com/en-us/powershell/scripting/developer/cmdlet/approved-verbs-for-windows-powershell-commands), like `Set-Location` instead of POSIX's `cd`, `Clear-Host` instead of `cls`, and `Remove-Item` instead of `rm`.\n\nHere's that MP3 downloader again, but now as a PowerShell script:\n\n```\nParam(\n    [Parameter(Mandatory = $true)][uri]$url\n)\n\nSet-Location -Path ~/Desktop\n\nyt-dlp -x --audio-format mp3 \"$url\"\nWrite-Host \"Done!\"\n```\n\nThe cool part in that script is the `Param` block, which uses [PowerShell's built-in parameter support](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-7.6) to define \"url\" as a mandatory [URI input](https://learn.microsoft.com/en-us/dotnet/api/system.uri?view=net-10.0). PowerShell will ask for that URL when the script starts, similar to the Bash example, while also allowing a `-url` parameter on the script as an alternative input method.\n\nYou can even run PowerShell's built-in `Get-Help` command on the script, which shows the list of parameters with their data types, just like `Get-Help` would for a built-in command.\n\n![Terminal screenshot of different ways of running the script](https://storage.ghost.io/c/91/6a/916a9f97-6ca5-4206-befd-00d97de05b3d/content/images/2026/10/image-17.png)\n\nThis same functionality in Bash would need a lot of boilerplate code with `getparams` and various `if` statements. PowerShell handles all that for you.\n\nIn case you were wondering, that Unix-style `~/Desktop` path works in PowerShell on all platforms. Even though Windows uses paths with backslashes at the system level, PowerShell can convert them as needed—native system commands might not, though, unless you use the `Convert-Path` [cmdlet](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/convert-path?view=powershell-7.6) first. Just like in Bash, the tilde (~) character is an alias for the home directory, or you can use the `$HOME` variable that [PowerShell supports on all platforms](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-7.6#home).\n\nPowerShell has native support for integers, strings, arrays, and hash tables. That's especially useful when parsing content from web APIs, because PowerShell can convert [to](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertto-json?view=powershell-7.6) and [from](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertfrom-json?view=powershell-7.6) JSON. It's also fully object-oriented, and you can [create custom objects](https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-pscustomobject?view=powershell-7.6) or call .NET functions if needed.\n\nYou can use any text editor to write PowerShell scripts, just like with Bash. If you happen to use Visual Studio Code, it offers [syntax completions, definition tracking, and linting support](https://code.visualstudio.com/docs/languages/powershell) for PowerShell, among other features.\n\n## The trial run\n\nMy first real project with PowerShell was [Just the Browser](https://justthebrowser.com/), a project that aims to remove AI features, tracking, and other unwanted functionality from desktop web browsers. The installer for Mac and Linux is a Bash script, so it made sense to make [the Windows version](https://github.com/corbindavenport/just-the-browser/blob/main/main.ps1) as a PowerShell script.\n\nThe installer's job is to check which browsers are present on the computer, and then show an \"Update settings\" menu item for each browser. When that item is selected, the script downloads and installs a Windows registry file containing the browser settings. If the settings have been installed, the script shows an additional \"Remove settings\" option in the menu to reverse the process.\n\n![A browser selection menu in a PowerShell window](https://storage.ghost.io/c/91/6a/916a9f97-6ca5-4206-befd-00d97de05b3d/content/images/2026/10/image-18.png)\n\nThis works by creating a list object with a [.NET List<T> Class](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1?view=net-10.0), with each list item being an object that contains a menu label and update/remove function. To check if any registry entries have already been modified, I used the `Test-Path` and `Get-ItemProperty` cmdlets, like this:\n\n```\nif (Test-Path \"HKLM:\\SOFTWARE\\Policies\\Google\\Chrome\") {\n  $IfAiSettingExists = (Get-ItemProperty -Path \"HKLM:\\SOFTWARE\\Policies\\Google\\Chrome\").AIModeSettings\n}\n```\n\nThere are a few cool things in that snippet. You might have noticed that registry path looks more like a file path on a drive called \"HKLM\" than the usual Windows Registry hierarchy, which looks like this:\n\n```\nHKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Google\\Chrome\n```\n\nPowerShell creates a [virtual drive for the Windows Registry](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_registry_provider?view=powershell-7.6), allowing you to get, add, change, clear, and delete registry keys as if they were files in any file system. That same `Test-Path` [cmdlet](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/test-path?view=powershell-7.6) would also work for checking if a certain file was in my Downloads folder. You don't have to learn how to use files *and* how to use registry keys, because *they're the same thing* in PowerShell.\n\nThere are other virtual drives in PowerShell, like the one for [certificate storage in Windows](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/about/about_certificate_provider?view=powershell-7.6), and you can even [create your own](https://learn.microsoft.com/en-us/powershell/scripting/samples/managing-windows-powershell-drives?view=powershell-7.6). If you're tired of typing a long path in your shell or scripts, make a virtual drive that points to it! You can even call it `FART:\\` if you want.\n\nThat script also has to download the `.reg` files for configuring web browsers. Here's a simplified version of the process for Google Chrome:\n\n```\n$GoogleChromeInstallRegistry = \"https://raw.githubusercontent.com/corbindavenport/just-the-browser/main/chrome/install.reg\"\n\nInvoke-WebRequest $GoogleChromeInstallRegistry -OutFile \"$env:LocalAppData\\chrome.reg\"\n\n$ChromeInstall = Start-Process \"reg.exe\" -ArgumentList \"import `\"$env:LocalAppData\\chrome.reg`\"\" -WindowStyle Hidden -Wait -PassThru\n```\n\nThis uses the `Invoke-WebRequest` [cmdlet](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-webrequest?view=powershell-7.6) to download the registry file from the provided URL, and stores it temporarily in the user account's AppData folder. It then starts the `reg.exe` command in a hidden window, tells it to import the file that was just downloaded, and waits until the command is completed. PowerShell could modify the keys directly, but I wanted them to be in a separate file that can be imported manually, since not everyone wants to use the automated installer.\n\nThe `Invoke-WebRequest` cmdlet works like Wget or cURL on a Mac or Linux system, allowing you to download web content in your script. That's another feature built into PowerShell on all platforms that I frequently need in Bash scripts.\n\n🤦‍♂️\n\nEarlier versions of Windows PowerShell included an alias for the `curl` command that redirected to `Invoke-WebRequest`. It didn't support all the same features as cURL, which led to [people filing bug reports for the actual cURL project](https://daniel.haxx.se/blog/2016/08/19/removing-the-powershell-curl-alias/). That alias was removed in PowerShell 7, and Windows [now includes the real application](https://learn.microsoft.com/en-us/windows/curl/).\n\nI wanted the installer to work in older versions of Windows PowerShell, so it could run on older PCs without first upgrading the PowerShell version. That only required a few alterations, like this command that enables TLS v1.2 before using `Invoke-WebRequest`, because it [used to not be enabled by default](https://megamorf.gitlab.io/2020/05/09/force-powershell-to-use-newer-tls-versions/):\n\n```\n[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12\n```\n\nFor another project, I was experimenting with merging my old Tumblr blog into my main personal website, which uses the [Eleventy static site generator](https://www.11ty.dev/). There were no existing tools for that, so I created a [\"Tumblr to Eleventy\" PowerShell script](https://github.com/corbindavenport/tumblr-to-eleventy).\n\nThe script has two main inputs: Tumblr's archive export, which is a bunch of HTML and media attachments in a ZIP file, and the Tumblr API. It uses `Invoke-WebRequest` and `ConvertFrom-Json` to check the API and iterate through all posts on the blog, `Expand-Archive` to extract the ZIP file, and cmdlets like `Copy-Item` and `New-Item` to create a new file structure ready for an Eleventy site.\n\nThe most important part was keeping all the metadata from the original Tumblr post, including the title, permalink, publish date, and tags. The script does that by creating a custom object with all the data, then turns it into [Eleventy-friendly JSON format](https://www.11ty.dev/docs/data-template-dir/) with the `ConvertTo-Json` cmdlet:\n\n```\n$OutputJson = Join-Path -Path $OutputPath -ChildPath \"index.json\"\n\n$PostMetadata = [PSCustomObject]@{\n    title = $Post.title ? $Post.title : $Post.summary\n    permalink = ($Post.post_url -replace $Post.blog.url, \"/\") + \"/index.html\"\n    date = [DateTimeOffset]::FromUnixTimeSeconds($Post.timestamp).UtcDateTime.ToString(\"yyyy-MM-ddTHH:mm:ssZ\")\n    tags = $tag ? (@($tag) + $Post.tags) : $Post.tags\n    tumblr_id = $Post.id\n    tumblr_url = $Post.post_url\n    tumblr_short_url = $Post.short_url\n    tumblr_blog_name = $Post.blog_name\n}\n\n$PostMetadata | ConvertTo-Json -Depth 1 | Set-Content -Path $OutputJson -Encoding UTF8\n```\n\nThe `$Post` variable contains the response from the Tumblr API, so this is just mapping that data to the keys that Eleventy expects. For example, the API sometimes put my post titles as the `summary` instead of the `title`, so the script checks `.title` first and uses `.summary` as the fallback. Tumblr provided the publish date and time for each post in [Unix time](https://en.wikipedia.org/wiki/Unix_time), which PowerShell can convert to the [UTC format](https://en.wikipedia.org/wiki/Coordinated_Universal_Time) required by Eleventy.\n\n![The script converting my blog posts in a Terminal window](https://storage.ghost.io/c/91/6a/916a9f97-6ca5-4206-befd-00d97de05b3d/content/images/2026/10/image-19.png)\n\nI probably would have used Node.js for this if it wasn't PowerShell, since Node does have built-in [web requests](https://nodejs.org/learn/getting-started/fetch) and [file handling](https://nodejs.org/api/fs.html). However, it would have needed external dependencies for simple tasks like unzipping the archive (either calling the system's `unzip` or using a JS-native library), as well as `await` statements all over the place. I didn't end up moving my blog, but hopefully someone else will find that script useful.\n\n## Another tool in the toolbox\n\nI'm still getting the hang of PowerShell scripting, but I have come away more impressed with it after every project. It's cross-platform, more predictable and reusable than Bash scripts, and relatively easy to understand. I can't believe this came out of the same company as [Copilot](https://www.spacebar.news/excel-ai-twenty-percent-of-the-time-it-works-every-time/).\n\nOf course, none of this matters if you do scripting and automation the *cool way*, with Claude or another AI chatbot taking the wheel. The bot can do the thinking and testing for you. However, I like learning things, and I'm trying my best to prevent my brain from atrophying. A local Gemma 4 model in [LM Studio](https://lmstudio.ai/) did help me with troubleshooting when web searches failed, but the vast majority of my scripting thus far has been lovingly hand-crafted.\n\nIf you're interested in learning how to use PowerShell, there are plenty of online guides and video tutorials. I have been watching [\"PowerShell 7 for Beginners\" from JackedProgrammer](https://www.youtube.com/playlist?list=PLnK11SQMNnE4vcvuAahz4KhNOS7zOfmB3), and I might even get a reference book at some point. It can go right next to my 1996 paperback edition of O'Reilly's *HTML: The Definitive Guide*.\n","body_html":"<p>Microsoft makes a <em>lot</em> of bad software. That includes many aspects of Windows, from <a href=\"https://www.howtogeek.com/new-outlook-still-just-as-bad-after-six-months/\" rel=\"nofollow ugc noopener\">the primary email application being a web wrapper</a> to <a href=\"https://www.theregister.com/software/2026/03/23/microsoft-removing-some-copilots-will-improve-windows-11/5228713\" rel=\"nofollow ugc noopener\">the ever-present Copilot buttons</a>. As such, it&#39;s always a surprise to me when I find a Microsoft product that <em>improves</em> my life in some respect, especially when it&#39;s something that came from Windows.</p>\n<p>I never paid much attention to PowerShell, the command-line shell and scripting tool that <a href=\"https://devblogs.microsoft.com/powershell/windows-powershell-monad-has-arrived/\" rel=\"nofollow ugc noopener\">first arrived in 2006</a>, and became a bundled application with the release of Windows 7. For the time period I used Windows as my main desktop operating system, I mostly stuck to the Command Prompt and Batch scripts, or <a href=\"https://learn.microsoft.com/en-us/windows/wsl/about\" rel=\"nofollow ugc noopener\">Bash in WSL</a> or Python for more complex tasks. The modern version of PowerShell is cross-platform, and can also be installed on Mac and Linux.</p>\n<p>Earlier this year, I needed to learn PowerShell scripting for one Windows-related project, and I have tried it out a few more times on Mac. It has quickly become one of my favorite tools for automation and scripting—taking on jobs that I would have otherwise tried to do with Node.js, Shortcuts, macOS Automator, or heaven forbid, Bash scripts.</p>\n<p>I get it now. PowerShell is great, even if you don&#39;t use it as your shell, and even if you&#39;re on Mac or Linux.</p>\n<h2 id=\"bashing-my-head-against-the-wall\">Bashing my head against the wall</h2>\n<p>The most common way to run a sequence of commands on Linux, macOS, and other Unix-based environments is with a <a href=\"https://www.geeksforgeeks.org/linux-unix/introduction-linux-shell-shell-scripting/\" rel=\"nofollow ugc noopener\">Bash script</a>. They are also called shell scripts, even though they&#39;re <a href=\"https://www.geeksforgeeks.org/linux-unix/bash-script-difference-between-bash-script-and-shell-script/\" rel=\"nofollow ugc noopener\">not <em>technically</em> the same thing</a>, because they are also used by other shells like <a href=\"https://en.wikipedia.org/wiki/Z_shell\" rel=\"nofollow ugc noopener\">Z Shell</a> (zsh).</p>\n<p>Here&#39;s an example Bash script that asks you for a media URL, and then uses the <a href=\"https://www.spacebar.news/yt-dlp-best-way-to-download-videos-audio/\" rel=\"nofollow ugc noopener\">yt-dlp application</a> to save it as an MP3 audio file to your Desktop folder:</p>\n<pre><code>#!/bin/bash\n\necho -e &quot;Enter a URL: &quot;\nread url\ncd ~/Desktop\nyt-dlp -x --audio-format mp3 &quot;$url&quot;\necho &quot;Done!&quot;</code></pre>\n<p>Bash scripts are fine for just running a series of commands. You can use variables and loops, code you use more than once can go in a function, and there&#39;s even limited support for data types like arrays. They can also <em>sometimes</em> work on a wide variety of hardware and operating systems.</p>\n<p>For anything slightly more complicated, Bash scripting is just <a href=\"https://www.youtube.com/watch?v=ETDHptCqRgQ\" rel=\"nofollow ugc noopener\">the worst</a>. There are almost no &quot;built-in&quot; functions—you are supposed to use <a href=\"https://en.wikipedia.org/wiki/List_of_POSIX_commands\" rel=\"nofollow ugc noopener\">POSIX commands</a> for common tasks, like file operations or parsing text. Anything beyond that depends on external tools, like <a href=\"https://en.wikipedia.org/wiki/CURL\" rel=\"nofollow ugc noopener\">cURL</a> or <a href=\"https://en.wikipedia.org/wiki/Wget\" rel=\"nofollow ugc noopener\">Wget</a> for retrieving web content, <a href=\"https://v5.chriskrycho.com/journal/read-the-manual/pbcopy-pbpaste/\" rel=\"nofollow ugc noopener\">several</a> <a href=\"https://github.com/bugaevc/wl-clipboard\" rel=\"nofollow ugc noopener\">different</a> <a href=\"https://github.com/astrand/xclip\" rel=\"nofollow ugc noopener\">applications</a> for accessing the clipboard, and <a href=\"https://shapeshed.com/jq-json/\" rel=\"nofollow ugc noopener\">jq</a> for reading JSON data. You might also need <a href=\"https://www.redhat.com/en/blog/arguments-options-bash-scripts\" rel=\"nofollow ugc noopener\">boilerplate code for parsing options and parameters</a>.</p>\n<p>It is also surprisingly difficult to make a Bash script that works reliably on <em>other people&#39;s computers</em>. Some people might try your script in a different shell that doesn&#39;t interpret scripts in the same way—that&#39;s why <a href=\"https://brew.sh/\" rel=\"nofollow ugc noopener\">the command for installing Homebrew</a> starts with calling the <code>bash</code> shell specifically, and many other tools use the same trick. Someone might only have cURL installed for fetching web content, or only Wget, or both, or neither! You can&#39;t assume every system will have the same basic set of POSIX commands, or even the <code>sudo</code> command, as <a href=\"https://github.com/corbindavenport/just-the-browser/issues/32\" rel=\"nofollow ugc noopener\">I learned a few months ago</a>.</p>\n<p>The limitations of Bash scripting usually pushed me towards actual programming languages, even for tasks that didn&#39;t strictly need them. Python might be the best alternative, but its lack of curly braces makes me sad. I write a lot of JavaScript code, so <a href=\"https://nodejs.org/en\" rel=\"nofollow ugc noopener\">Node.js</a> is an option, but I have to put <code>await</code> operators everywhere for system operations and <a href=\"https://www.microsoft.com/en-us/security/blog/2026/05/29/33-malicious-npm-packages-abuse-dependency-confusion-profile-developer-environments/\" rel=\"nofollow ugc noopener\">hope that I&#39;m not installing malware</a> when more dependencies are needed.</p>\n<h2 id=\"hello-powershell\">Hello, PowerShell</h2>\n<p>Like many other products from Microsoft (and Google), PowerShell is actually <a href=\"https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_editions?view=powershell-7.6\" rel=\"nofollow ugc noopener\">two different things</a> with similar names. The one you should use is PowerShell 7, also called PowerShell Core, which is the modern cross-platform version based on .NET Core. The other one is Windows PowerShell (v5.1 and below), which is based on .NET Framework and not actively developed. Windows 11 still <a href=\"https://github.com/PowerShell/PowerShell/discussions/24340\" rel=\"nofollow ugc noopener\">only includes Windows PowerShell</a>, and not PowerShell 7.</p>\n<p>PowerShell 7 is a <a href=\"https://learn.microsoft.com/en-us/powershell/scripting/install/install-powershell?view=powershell-7.6\" rel=\"nofollow ugc noopener\">free download</a> on Windows, Mac, and Linux. On my Mac computers, I <a href=\"https://formulae.brew.sh/formula/powershell\" rel=\"nofollow ugc noopener\">used Homebrew to install it</a>, and then I can run it with <code>pwsh</code> in the Terminal.</p>\n<figure><img src=\"https://storage.ghost.io/c/91/6a/916a9f97-6ca5-4206-befd-00d97de05b3d/content/images/2026/10/image-20.png\" alt=\"A Terminal window running an echo command in ZSH and PowerShell\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>The basic premise of a PowerShell script isn&#39;t much different than a Bash script. It&#39;s a list of commands, either provided by the shell or the underlying system, with the ability to use additional functions and keywords (like <code>if</code>, <code>foreach</code>, or <code>try</code>/<code>catch</code>) as needed.</p>\n<p>PowerShell has many built-in commands, called &quot;cmdlets&quot; (pronounced &quot;command-lets&quot;), and you can <a href=\"https://learn.microsoft.com/en-us/powershell/module/powershellget/install-module\" rel=\"nofollow ugc noopener\">install more</a> or make your own. The names for most of them are <a href=\"https://learn.microsoft.com/en-us/powershell/scripting/developer/cmdlet/approved-verbs-for-windows-powershell-commands\" rel=\"nofollow ugc noopener\">verb-noun pairs</a>, like <code>Set-Location</code> instead of POSIX&#39;s <code>cd</code>, <code>Clear-Host</code> instead of <code>cls</code>, and <code>Remove-Item</code> instead of <code>rm</code>.</p>\n<p>Here&#39;s that MP3 downloader again, but now as a PowerShell script:</p>\n<pre><code>Param(\n    [Parameter(Mandatory = $true)][uri]$url\n)\n\nSet-Location -Path ~/Desktop\n\nyt-dlp -x --audio-format mp3 &quot;$url&quot;\nWrite-Host &quot;Done!&quot;</code></pre>\n<p>The cool part in that script is the <code>Param</code> block, which uses <a href=\"https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-7.6\" rel=\"nofollow ugc noopener\">PowerShell&#39;s built-in parameter support</a> to define &quot;url&quot; as a mandatory <a href=\"https://learn.microsoft.com/en-us/dotnet/api/system.uri?view=net-10.0\" rel=\"nofollow ugc noopener\">URI input</a>. PowerShell will ask for that URL when the script starts, similar to the Bash example, while also allowing a <code>-url</code> parameter on the script as an alternative input method.</p>\n<p>You can even run PowerShell&#39;s built-in <code>Get-Help</code> command on the script, which shows the list of parameters with their data types, just like <code>Get-Help</code> would for a built-in command.</p>\n<figure><img src=\"https://storage.ghost.io/c/91/6a/916a9f97-6ca5-4206-befd-00d97de05b3d/content/images/2026/10/image-17.png\" alt=\"Terminal screenshot of different ways of running the script\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>This same functionality in Bash would need a lot of boilerplate code with <code>getparams</code> and various <code>if</code> statements. PowerShell handles all that for you.</p>\n<p>In case you were wondering, that Unix-style <code>~/Desktop</code> path works in PowerShell on all platforms. Even though Windows uses paths with backslashes at the system level, PowerShell can convert them as needed—native system commands might not, though, unless you use the <code>Convert-Path</code> <a href=\"https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/convert-path?view=powershell-7.6\" rel=\"nofollow ugc noopener\">cmdlet</a> first. Just like in Bash, the tilde (~) character is an alias for the home directory, or you can use the <code>$HOME</code> variable that <a href=\"https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-7.6#home\" rel=\"nofollow ugc noopener\">PowerShell supports on all platforms</a>.</p>\n<p>PowerShell has native support for integers, strings, arrays, and hash tables. That&#39;s especially useful when parsing content from web APIs, because PowerShell can convert <a href=\"https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertto-json?view=powershell-7.6\" rel=\"nofollow ugc noopener\">to</a> and <a href=\"https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertfrom-json?view=powershell-7.6\" rel=\"nofollow ugc noopener\">from</a> JSON. It&#39;s also fully object-oriented, and you can <a href=\"https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-pscustomobject?view=powershell-7.6\" rel=\"nofollow ugc noopener\">create custom objects</a> or call .NET functions if needed.</p>\n<p>You can use any text editor to write PowerShell scripts, just like with Bash. If you happen to use Visual Studio Code, it offers <a href=\"https://code.visualstudio.com/docs/languages/powershell\" rel=\"nofollow ugc noopener\">syntax completions, definition tracking, and linting support</a> for PowerShell, among other features.</p>\n<h2 id=\"the-trial-run\">The trial run</h2>\n<p>My first real project with PowerShell was <a href=\"https://justthebrowser.com/\" rel=\"nofollow ugc noopener\">Just the Browser</a>, a project that aims to remove AI features, tracking, and other unwanted functionality from desktop web browsers. The installer for Mac and Linux is a Bash script, so it made sense to make <a href=\"https://github.com/corbindavenport/just-the-browser/blob/main/main.ps1\" rel=\"nofollow ugc noopener\">the Windows version</a> as a PowerShell script.</p>\n<p>The installer&#39;s job is to check which browsers are present on the computer, and then show an &quot;Update settings&quot; menu item for each browser. When that item is selected, the script downloads and installs a Windows registry file containing the browser settings. If the settings have been installed, the script shows an additional &quot;Remove settings&quot; option in the menu to reverse the process.</p>\n<figure><img src=\"https://storage.ghost.io/c/91/6a/916a9f97-6ca5-4206-befd-00d97de05b3d/content/images/2026/10/image-18.png\" alt=\"A browser selection menu in a PowerShell window\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>This works by creating a list object with a <a href=\"https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1?view=net-10.0\" rel=\"nofollow ugc noopener\">.NET List&lt;T&gt; Class</a>, with each list item being an object that contains a menu label and update/remove function. To check if any registry entries have already been modified, I used the <code>Test-Path</code> and <code>Get-ItemProperty</code> cmdlets, like this:</p>\n<pre><code>if (Test-Path &quot;HKLM:\\SOFTWARE\\Policies\\Google\\Chrome&quot;) {\n  $IfAiSettingExists = (Get-ItemProperty -Path &quot;HKLM:\\SOFTWARE\\Policies\\Google\\Chrome&quot;).AIModeSettings\n}</code></pre>\n<p>There are a few cool things in that snippet. You might have noticed that registry path looks more like a file path on a drive called &quot;HKLM&quot; than the usual Windows Registry hierarchy, which looks like this:</p>\n<pre><code>HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Google\\Chrome</code></pre>\n<p>PowerShell creates a <a href=\"https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_registry_provider?view=powershell-7.6\" rel=\"nofollow ugc noopener\">virtual drive for the Windows Registry</a>, allowing you to get, add, change, clear, and delete registry keys as if they were files in any file system. That same <code>Test-Path</code> <a href=\"https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/test-path?view=powershell-7.6\" rel=\"nofollow ugc noopener\">cmdlet</a> would also work for checking if a certain file was in my Downloads folder. You don&#39;t have to learn how to use files <em>and</em> how to use registry keys, because <em>they&#39;re the same thing</em> in PowerShell.</p>\n<p>There are other virtual drives in PowerShell, like the one for <a href=\"https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/about/about_certificate_provider?view=powershell-7.6\" rel=\"nofollow ugc noopener\">certificate storage in Windows</a>, and you can even <a href=\"https://learn.microsoft.com/en-us/powershell/scripting/samples/managing-windows-powershell-drives?view=powershell-7.6\" rel=\"nofollow ugc noopener\">create your own</a>. If you&#39;re tired of typing a long path in your shell or scripts, make a virtual drive that points to it! You can even call it <code>FART:\\</code> if you want.</p>\n<p>That script also has to download the <code>.reg</code> files for configuring web browsers. Here&#39;s a simplified version of the process for Google Chrome:</p>\n<pre><code>$GoogleChromeInstallRegistry = &quot;https://raw.githubusercontent.com/corbindavenport/just-the-browser/main/chrome/install.reg&quot;\n\nInvoke-WebRequest $GoogleChromeInstallRegistry -OutFile &quot;$env:LocalAppData\\chrome.reg&quot;\n\n$ChromeInstall = Start-Process &quot;reg.exe&quot; -ArgumentList &quot;import `&quot;$env:LocalAppData\\chrome.reg`&quot;&quot; -WindowStyle Hidden -Wait -PassThru</code></pre>\n<p>This uses the <code>Invoke-WebRequest</code> <a href=\"https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-webrequest?view=powershell-7.6\" rel=\"nofollow ugc noopener\">cmdlet</a> to download the registry file from the provided URL, and stores it temporarily in the user account&#39;s AppData folder. It then starts the <code>reg.exe</code> command in a hidden window, tells it to import the file that was just downloaded, and waits until the command is completed. PowerShell could modify the keys directly, but I wanted them to be in a separate file that can be imported manually, since not everyone wants to use the automated installer.</p>\n<p>The <code>Invoke-WebRequest</code> cmdlet works like Wget or cURL on a Mac or Linux system, allowing you to download web content in your script. That&#39;s another feature built into PowerShell on all platforms that I frequently need in Bash scripts.</p>\n<p>🤦‍♂️</p>\n<p>Earlier versions of Windows PowerShell included an alias for the <code>curl</code> command that redirected to <code>Invoke-WebRequest</code>. It didn&#39;t support all the same features as cURL, which led to <a href=\"https://daniel.haxx.se/blog/2016/08/19/removing-the-powershell-curl-alias/\" rel=\"nofollow ugc noopener\">people filing bug reports for the actual cURL project</a>. That alias was removed in PowerShell 7, and Windows <a href=\"https://learn.microsoft.com/en-us/windows/curl/\" rel=\"nofollow ugc noopener\">now includes the real application</a>.</p>\n<p>I wanted the installer to work in older versions of Windows PowerShell, so it could run on older PCs without first upgrading the PowerShell version. That only required a few alterations, like this command that enables TLS v1.2 before using <code>Invoke-WebRequest</code>, because it <a href=\"https://megamorf.gitlab.io/2020/05/09/force-powershell-to-use-newer-tls-versions/\" rel=\"nofollow ugc noopener\">used to not be enabled by default</a>:</p>\n<pre><code>[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12</code></pre>\n<p>For another project, I was experimenting with merging my old Tumblr blog into my main personal website, which uses the <a href=\"https://www.11ty.dev/\" rel=\"nofollow ugc noopener\">Eleventy static site generator</a>. There were no existing tools for that, so I created a <a href=\"https://github.com/corbindavenport/tumblr-to-eleventy\" rel=\"nofollow ugc noopener\">&quot;Tumblr to Eleventy&quot; PowerShell script</a>.</p>\n<p>The script has two main inputs: Tumblr&#39;s archive export, which is a bunch of HTML and media attachments in a ZIP file, and the Tumblr API. It uses <code>Invoke-WebRequest</code> and <code>ConvertFrom-Json</code> to check the API and iterate through all posts on the blog, <code>Expand-Archive</code> to extract the ZIP file, and cmdlets like <code>Copy-Item</code> and <code>New-Item</code> to create a new file structure ready for an Eleventy site.</p>\n<p>The most important part was keeping all the metadata from the original Tumblr post, including the title, permalink, publish date, and tags. The script does that by creating a custom object with all the data, then turns it into <a href=\"https://www.11ty.dev/docs/data-template-dir/\" rel=\"nofollow ugc noopener\">Eleventy-friendly JSON format</a> with the <code>ConvertTo-Json</code> cmdlet:</p>\n<pre><code>$OutputJson = Join-Path -Path $OutputPath -ChildPath &quot;index.json&quot;\n\n$PostMetadata = [PSCustomObject]@{\n    title = $Post.title ? $Post.title : $Post.summary\n    permalink = ($Post.post_url -replace $Post.blog.url, &quot;/&quot;) + &quot;/index.html&quot;\n    date = [DateTimeOffset]::FromUnixTimeSeconds($Post.timestamp).UtcDateTime.ToString(&quot;yyyy-MM-ddTHH:mm:ssZ&quot;)\n    tags = $tag ? (@($tag) + $Post.tags) : $Post.tags\n    tumblr_id = $Post.id\n    tumblr_url = $Post.post_url\n    tumblr_short_url = $Post.short_url\n    tumblr_blog_name = $Post.blog_name\n}\n\n$PostMetadata | ConvertTo-Json -Depth 1 | Set-Content -Path $OutputJson -Encoding UTF8</code></pre>\n<p>The <code>$Post</code> variable contains the response from the Tumblr API, so this is just mapping that data to the keys that Eleventy expects. For example, the API sometimes put my post titles as the <code>summary</code> instead of the <code>title</code>, so the script checks <code>.title</code> first and uses <code>.summary</code> as the fallback. Tumblr provided the publish date and time for each post in <a href=\"https://en.wikipedia.org/wiki/Unix_time\" rel=\"nofollow ugc noopener\">Unix time</a>, which PowerShell can convert to the <a href=\"https://en.wikipedia.org/wiki/Coordinated_Universal_Time\" rel=\"nofollow ugc noopener\">UTC format</a> required by Eleventy.</p>\n<figure><img src=\"https://storage.ghost.io/c/91/6a/916a9f97-6ca5-4206-befd-00d97de05b3d/content/images/2026/10/image-19.png\" alt=\"The script converting my blog posts in a Terminal window\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>I probably would have used Node.js for this if it wasn&#39;t PowerShell, since Node does have built-in <a href=\"https://nodejs.org/learn/getting-started/fetch\" rel=\"nofollow ugc noopener\">web requests</a> and <a href=\"https://nodejs.org/api/fs.html\" rel=\"nofollow ugc noopener\">file handling</a>. However, it would have needed external dependencies for simple tasks like unzipping the archive (either calling the system&#39;s <code>unzip</code> or using a JS-native library), as well as <code>await</code> statements all over the place. I didn&#39;t end up moving my blog, but hopefully someone else will find that script useful.</p>\n<h2 id=\"another-tool-in-the-toolbox\">Another tool in the toolbox</h2>\n<p>I&#39;m still getting the hang of PowerShell scripting, but I have come away more impressed with it after every project. It&#39;s cross-platform, more predictable and reusable than Bash scripts, and relatively easy to understand. I can&#39;t believe this came out of the same company as <a href=\"https://www.spacebar.news/excel-ai-twenty-percent-of-the-time-it-works-every-time/\" rel=\"nofollow ugc noopener\">Copilot</a>.</p>\n<p>Of course, none of this matters if you do scripting and automation the <em>cool way</em>, with Claude or another AI chatbot taking the wheel. The bot can do the thinking and testing for you. However, I like learning things, and I&#39;m trying my best to prevent my brain from atrophying. A local Gemma 4 model in <a href=\"https://lmstudio.ai/\" rel=\"nofollow ugc noopener\">LM Studio</a> did help me with troubleshooting when web searches failed, but the vast majority of my scripting thus far has been lovingly hand-crafted.</p>\n<p>If you&#39;re interested in learning how to use PowerShell, there are plenty of online guides and video tutorials. I have been watching <a href=\"https://www.youtube.com/playlist?list=PLnK11SQMNnE4vcvuAahz4KhNOS7zOfmB3\" rel=\"nofollow ugc noopener\">&quot;PowerShell 7 for Beginners&quot; from JackedProgrammer</a>, and I might even get a reference book at some point. It can go right next to my 1996 paperback edition of O&#39;Reilly&#39;s <em>HTML: The Definitive Guide</em>.</p>","headings":[{"level":2,"text":"Bashing my head against the wall","id":"bashing-my-head-against-the-wall"},{"level":2,"text":"Hello, PowerShell","id":"hello-powershell"},{"level":2,"text":"The trial run","id":"the-trial-run"},{"level":2,"text":"Another tool in the toolbox","id":"another-tool-in-the-toolbox"}]}}