Friday, October 9, 2026

PowerShell help doesn't.

I want to print a file to the console. I wonder what my options are.

If I'm in Git Bash (no man), I use the documentation built into the individual utility:


$ cat --help
Usage: cat [OPTION]... [FILE]...
Concatenate FILE(s) to standard output.

With no FILE, or when FILE is -, read standard input.

  -A, --show-all           equivalent to -vET
  -b, --number-nonblank    number nonempty output lines, overrides -n
  -e                       equivalent to -vE
  -E, --show-ends          display $ at end of each line
  -n, --number             number all output lines
  -s, --squeeze-blank      suppress repeated empty output lines
  -t                       equivalent to -vT
  -T, --show-tabs          display TAB characters as ^I
  -u                       (ignored)
  -v, --show-nonprinting   use ^ and M- notation, except for LFD and TAB
      --help     display this help and exit
      --version  output version information and exit

Examples:
  cat f - g  Output f's contents, then standard input, then g's contents.
  cat        Copy standard input to standard output.

GNU coreutils online help: <https://www.gnu.org/software/coreutils/>
Full documentation <https://www.gnu.org/software/coreutils/cat>
or available locally via: info '(coreutils) cat invocation'

Oh, neat.

What if I'm in PowerShell?

PS C:\Users\me> Get-Help Get-Content

NAME
    Get-Content

SYNOPSIS
    Gets the content of the item at the specified location.


SYNTAX
    Get-Content [-AsByteStream <System.Management.Automation.SwitchParameter>] [-Credential
    <System.Management.Automation.PSCredential>] [-Delimiter <System.String>] [-Encoding <System.Text.Encoding>]
    [-Exclude <System.String[]>] [-Filter <System.String>] [-Force <System.Management.Automation.SwitchParameter>]
    [-Include <System.String[]>] [-Path] <System.String[]> [-Raw <System.Management.Automation.SwitchParameter>]
    [-ReadCount <System.Int64>] [-Stream <System.String>] [-Tail <System.Int32>] [-TotalCount <System.Int64>] [-Wait
    <System.Management.Automation.SwitchParameter>] [<CommonParameters>]

    Get-Content [-AsByteStream <System.Management.Automation.SwitchParameter>] [-Credential
    <System.Management.Automation.PSCredential>] [-Delimiter <System.String>] [-Encoding <System.Text.Encoding>]
    [-Exclude <System.String[]>] [-Filter <System.String>] [-Force <System.Management.Automation.SwitchParameter>]
    [-Include <System.String[]>] -LiteralPath <System.String[]> [-Raw <System.Management.Automation.SwitchParameter>]
    [-ReadCount <System.Int64>] [-Stream <System.String>] [-Tail <System.Int32>] [-TotalCount <System.Int64>] [-Wait
    <System.Management.Automation.SwitchParameter>] [<CommonParameters>]


DESCRIPTION
    The `Get-Content` cmdlet gets the content of the item at the location specified by the path, such as the text in a
    file or the content of a function. For files, the content is read one line at a time and returns a collection of
    objects, each representing a line of content.

    Beginning in PowerShell 3.0, `Get-Content` can also get a specified number of lines from the beginning or end of
    an item.


RELATED LINKS
    Online Version https://learn.microsoft.com/powershell/module/microsoft.powershell.management/get-content?view=power
    shell-7.5&WT.mc_id=ps-gethelp
    about_Automatic_Variables ../Microsoft.PowerShell.Core/About/about_Automatic_Variables.md
    about_Providers ../Microsoft.PowerShell.Core/About/about_Providers.md
    Add-Content Add-Content.md
    Clear-Content Clear-Content.md
    ForEach-Object ../Microsoft.PowerShell.Core/ForEach-Object.md
    Get-PSProvider Get-PSProvider.md
    Set-Content Set-Content.md

REMARKS
    To see the examples, type: "Get-Help Get-Content -Examples"
    For more information, type: "Get-Help Get-Content -Detailed"
    For technical information, type: "Get-Help Get-Content -Full"
    For online help, type: "Get-Help Get-Content -Online"

I can kind of skim the options if I squint. I can see their types, but not what they do. (Do I need to know the argument types? I hope not.)

I can probably run one of the commands from the "Remarks" section to see flag descriptions. What is the difference between "more information" and "technical information"? Is "technical information" about the implementation?

"Full" sounds like absolutely everything, so let's try "Detailed" for brevity. It won't be cluttered with examples because that's a separate command, "Examples".

PS C:\Users\me> Get-Help Get-Content -Detailed
NAME
    Get-Content

SYNOPSIS
    Gets the content of the item at the specified location.


SYNTAX
    Get-Content [-AsByteStream <System.Management.Automation.SwitchParameter>] [-Credential <System.Management.Automation.PSCredential>] [-Delimiter <System.String>] [-Encoding <System.Text.Encoding>]
    [-Exclude <System.String[]>] [-Filter <System.String>] [-Force <System.Management.Automation.SwitchParameter>] [-Include <System.String[]>] [-Path] <System.String[]> [-Raw
    <System.Management.Automation.SwitchParameter>] [-ReadCount <System.Int64>] [-Stream <System.String>] [-Tail <System.Int32>] [-TotalCount <System.Int64>] [-Wait
    <System.Management.Automation.SwitchParameter>] [<CommonParameters>]

    Get-Content [-AsByteStream <System.Management.Automation.SwitchParameter>] [-Credential <System.Management.Automation.PSCredential>] [-Delimiter <System.String>] [-Encoding <System.Text.Encoding>]
    [-Exclude <System.String[]>] [-Filter <System.String>] [-Force <System.Management.Automation.SwitchParameter>] [-Include <System.String[]>] -LiteralPath <System.String[]> [-Raw
    <System.Management.Automation.SwitchParameter>] [-ReadCount <System.Int64>] [-Stream <System.String>] [-Tail <System.Int32>] [-TotalCount <System.Int64>] [-Wait
    <System.Management.Automation.SwitchParameter>] [<CommonParameters>]


DESCRIPTION
    The `Get-Content` cmdlet gets the content of the item at the location specified by the path, such as the text in a file or the content of a function. For files, the content is read one line at a time and
    returns a collection of objects, each representing a line of content.

    Beginning in PowerShell 3.0, `Get-Content` can also get a specified number of lines from the beginning or end of an item.


PARAMETERS
    -AsByteStream [<System.Management.Automation.SwitchParameter>]
        Specifies that the content should be read as a stream of bytes. The **AsByteStream** parameter was
        introduced in Windows PowerShell 6.0.

        A warning occurs when you use the **AsByteStream** parameter with the **Encoding** parameter. The
        **AsByteStream** parameter ignores any encoding and the output is returned as a stream of bytes.

        When reading from and writing to binary files, use the **AsByteStream** parameter and a value of 0
        for the **ReadCount** parameter. A **ReadCount** value of 0 reads the entire file in a single read
        operation. The default **ReadCount** value, 1, reads one byte in each read operation and converts
        each byte into a separate object. Piping single-byte output to `Set-Content` causes errors unless
        you use the **AsByteStream** parameter with `Set-Content`.

    -Credential [<System.Management.Automation.PSCredential>]
        > [!NOTE]
        > This parameter isn't supported by any providers installed with PowerShell. To impersonate another
        > user, or elevate your credentials when running this cmdlet, use
        > [Invoke-Command](../Microsoft.PowerShell.Core/Invoke-Command.md).

    -Delimiter [<System.String>]
        Specifies the delimiter that `Get-Content` uses to divide the file into objects while it reads. The
        default is `\n`, the end-of-line character. When reading a text file, `Get-Content` returns a
        collection of string objects, each ending with an end-of-line character. When you enter a delimiter
        that doesn't exist in the file, `Get-Content` returns the entire file as a single, undelimited
        object.

        You can use this parameter to split a large file into smaller files by specifying a file separator,
        as the delimiter. The delimiter is preserved (not discarded) and becomes the last item in each file
        section.

        **Delimiter** is a dynamic parameter that the **FileSystem** provider adds to the `Get-Content`
        cmdlet. This parameter works only in file system drives.

        > [!NOTE]
        > Currently, when the value of the **Delimiter** parameter is an empty string, `Get-Content` does
        > not return anything. This is a known issue. To force `Get-Content` to return the entire file as
        > a single, undelimited string. Enter a value that doesn't exist in the file.

    -Encoding [<System.Text.Encoding>]
        Specifies the type of encoding for the target file. The default value is `utf8NoBOM`.

        The acceptable values for this parameter are as follows:

        - `ascii`: Uses the encoding for the ASCII (7-bit) character set.
        - `ansi`: Uses the encoding for the for the current culture's ANSI code page. This option was added
          in PowerShell 7.4.
        - `bigendianunicode`: Encodes in UTF-16 format using the big-endian byte order.
        - `bigendianutf32`: Encodes in UTF-32 format using the big-endian byte order.
        - `oem`: Uses the default encoding for MS-DOS and console programs.
        - `unicode`: Encodes in UTF-16 format using the little-endian byte order.
        - `utf7`: Encodes in UTF-7 format.
        - `utf8`: Encodes in UTF-8 format.
        - `utf8BOM`: Encodes in UTF-8 format with Byte Order Mark (BOM)
        - `utf8NoBOM`: Encodes in UTF-8 format without Byte Order Mark (BOM)
        - `utf32`: Encodes in UTF-32 format.

        Encoding is a dynamic parameter that the **FileSystem** provider adds to the `Get-Content` cmdlet.
        This parameter is available only in file system drives.

        Beginning with PowerShell 6.2, the **Encoding** parameter also allows numeric IDs of registered code
        pages (like `-Encoding 1251`) or string names of registered code pages (like
        `-Encoding "windows-1251"`). For more information, see the .NET documentation for
        [Encoding.CodePage](xref:System.Text.Encoding.CodePage%2A).

        Starting with PowerShell 7.4, you can use the `Ansi` value for the **Encoding** parameter to pass
        the numeric ID for the current culture's ANSI code page without having to specify it manually.

        > [!NOTE]
        > **UTF-7*** is no longer recommended to use. As of PowerShell 7.1, a warning is written if you
        > specify `utf7` for the **Encoding** parameter.

    -Exclude [<System.String[]>]
        Specifies, as a string array, an item or items that this cmdlet excludes in the operation.
        The value of this parameter qualifies the **Path** parameter.

        Enter a path element or pattern, such as `*.txt`. Wildcard characters are permitted.

        The **Exclude** parameter is effective only when the command includes the contents of an item,
        such as `C:\Windows\*`, where the wildcard character specifies the contents of the `C:\Windows`
        directory.

    -Filter [<System.String>]
        Specifies a filter to qualify the **Path** parameter. The
        [FileSystem](../Microsoft.PowerShell.Core/About/about_FileSystem_Provider.md) provider is the only
        installed PowerShell provider that supports the use of filters. You can find the syntax for the
        **FileSystem** filter language in
        [about_Wildcards](../Microsoft.PowerShell.Core/About/about_Wildcards.md). Filters are more efficient
        than other parameters, because the provider applies them when the cmdlet gets the objects rather
        than having PowerShell filter the objects after they're retrieved.

    -Force [<System.Management.Automation.SwitchParameter>]
        **Force** can override a read-only attribute or create directories to complete a file path. The
        **Force** parameter doesn't attempt to change file permissions or override security restrictions.

    -Include [<System.String[]>]
        Specifies, as a string array, an item or items that this cmdlet includes in the operation. The value
        of this parameter qualifies the **Path** parameter. Enter a path element or pattern, such as
        `"*.txt"`. Wildcard characters are permitted. The **Include** parameter is effective only when the
        command includes the contents of an item, such as `C:\Windows\*`, where the wildcard character
        specifies the contents of the `C:\Windows` directory.

    -LiteralPath <System.String[]>
        Specifies a path to one or more locations. The value of **LiteralPath** is used exactly as it's
        typed. No characters are interpreted as wildcards. If the path includes escape characters, enclose
        it in single quotation marks. Single quotation marks tell PowerShell not to interpret any characters
        as escape sequences.

        For more information, see
        [about_Quoting_Rules](../Microsoft.PowerShell.Core/About/about_Quoting_Rules.md).

    -Path <System.String[]>
        Specifies the path to an item where `Get-Content` gets the content. Wildcard characters are
        permitted. The paths must be paths to items, not to containers. For example, you must specify a path
        to one or more files, not a path to a directory.

    -Raw [<System.Management.Automation.SwitchParameter>]
        Ignores newline characters and returns the entire contents of a file in one string with the newlines
        preserved. By default, newline characters in a file are used as delimiters to separate the input
        into an array of strings. This parameter was introduced in PowerShell 3.0.

        **Raw** is a dynamic parameter that the **FileSystem** provider adds to the `Get-Content` cmdlet
        This parameter works only in file system drives.

    -ReadCount [<System.Int64>]
        Specifies how many lines of content are sent through the pipeline at a time. The default value is 1.
        A value of 0 (zero) or negative numbers sends all the content at one time.

        This parameter doesn't change the content displayed, but it does affect the time it takes to
        display the content. As the value of **ReadCount** increases, the time it takes to return the first
        line increases, but the total time for the operation decreases. This can make a perceptible
        difference in large items.

    -Stream [<System.String>]
        > [!NOTE]
        > This Parameter is only available on Windows.

        Gets the contents of the specified alternate NTFS file stream from the file. Enter the stream name.
        Wildcards aren't supported.

        **Stream** is a dynamic parameter that the **FileSystem** provider adds to the `Get-Content` cmdlet.
        This parameter works only in file system drives on Windows systems.

        This parameter was introduced in Windows PowerShell 3.0. In PowerShell 7.2, `Get-Content` can
        retrieve the content of alternative data streams from directories as well as files.

    -Tail [<System.Int32>]
        Specifies the number of lines from the end of a file or other item. You can use the **Tail**
        parameter name or its alias, **Last**. A value of `0` returns no lines. Negative values cause an
        error.

        This parameter was introduced in PowerShell 3.0.

    -TotalCount [<System.Int64>]
        Specifies the number of lines from the beginning of a file or other item. A value of `0` returns no
        lines. Negative values cause an error.

        You can use the **TotalCount** parameter name or its aliases, **First** or **Head**.

    -Wait [<System.Management.Automation.SwitchParameter>]
        Causes the cmdlet to wait indefinitely, keeping the file open, until interrupted. While waiting,
        `Get-Content` checks the file once per second and outputs new lines if present. When used with the
        **TotalCount** parameter, `Get-Content` waits until the specified number of lines are available in
        the specified file. For example, if you specify a **TotalCount** of 10 and the file already has 10
        or more lines, `Get-Content` returns the 10 lines and exits. If the file has fewer than 10 lines,
        `Get-Content` outputs each line as it arrives, but waits until the tenth line arrives before
        exiting.

        You can interrupt **Wait** by pressing <kbd>Ctrl</kbd>+<kbd>C</kbd>. Deleting the file causes a
        non-terminating error that also interrupts the waiting.

        **Wait** is a dynamic parameter that the FileSystem provider adds to the `Get-Content` cmdlet. This
        parameter works only in file system drives. **Wait** can't be combined with **Raw**.

    <CommonParameters>
        This cmdlet supports the common parameters: Verbose, Debug,
        ErrorAction, ErrorVariable, WarningAction, WarningVariable,
        OutBuffer, PipelineVariable, and OutVariable. For more information, see
        about_CommonParameters (https://go.microsoft.com/fwlink/?LinkID=113216).

    --------- Example 1: Get the content of a text file ---------

    This example gets the content of a file in the current directory. The `LineNumbers.txt` file
    has 100 lines in the format, **This is Line X** and is used in several examples.

    ```powershell
    1..100 | ForEach-Object {
        Add-Content -Path .\LineNumbers.txt -Value "This is line $_."
    }
    Get-Content -Path .\LineNumbers.txt
    ```

    ```Output
    This is line 1.
    This is line 2.
    ...
    This is line 99.
    This is line 100.
    ```

    The array values 1-100 are sent down the pipeline to the `ForEach-Object` cmdlet. `ForEach-Object`
    uses a scriptblock with the `Add-Content` cmdlet to create the `LineNumbers.txt` file. The variable
    `$_` represents the array values as each object is sent down the pipeline. The `Get-Content` cmdlet
    uses the **Path** parameter to specify the `LineNumbers.txt` file and displays the content in the
    PowerShell console.

     --------- Example 2: Limit the number of lines Get-Content returns ---------

    This command gets the first five lines of a file. The **TotalCount** parameter gets the first five
    lines of content. This example uses the `LineNumbers.txt` referenced in Example 1.

    ```powershell
    Get-Content -Path .\LineNumbers.txt -TotalCount 5
    ```

    ```Output
    This is line 1.
    This is line 2.
    This is line 3.
    This is line 4.
    This is line 5.
    ```

     --------- Example 3: Get a specific line of content from a text file ---------

    This command gets a specific number of lines from a file and then displays only the last line of
    that content. The **TotalCount** parameter gets the first 25 lines of content. This example uses the
    `LineNumbers.txt` file referenced in Example 1.

    ```powershell
    (Get-Content -Path .\LineNumbers.txt -TotalCount 25)[-1]
    ```

    ```Output
    This is line 25.
    ```

    The `Get-Content` command is wrapped in parentheses so that the command completes before going to
    the next step. `Get-Content`returns an array of lines, this allows you to add the index notation
    after the parenthesis to retrieve a specific line number. In this case, the `[-1]` index specifies
    the last index in the returned array of 25 retrieved lines.

     --------- Example 4: Get the last line of a text file ---------

    This command gets the last line of content from a file. This example uses the `LineNumbers.txt` file
    that was created in Example 1.

    ```powershell
    Get-Item -Path .\LineNumbers.txt | Get-Content -Tail 1
    ```

    ```Output
    This is line 100.
    ```

    This example uses the `Get-Item` cmdlet to demonstrate that you can pipe files to `Get-Content`. The
    **Tail** parameter gets the last line of the file. This method is faster than retrieving all the
    lines in a variable and using the `[-1]` index notation.

     --------- Example 5: Get the content of an alternate data stream ---------

    This example describes how to use the **Stream** parameter to get the content of an alternate data
    stream for files stored on a Windows NTFS volume. In this example, the `Set-Content` cmdlet is used
    to create sample content in a file named `Stream.txt`.

    ```powershell
    Set-Content -Path .\Stream.txt -Value 'This is the content of the Stream.txt file'
    # Specify a wildcard to the Stream parameter to display all streams of the recently
    # created file.
    Get-Item -Path .\Stream.txt -Stream *
    ```

    ```Output
    PSPath        : Microsoft.PowerShell.Core\FileSystem::C:\Test\Stream.txt::$DATA
    PSParentPath  : Microsoft.PowerShell.Core\FileSystem::C:\Test
    PSChildName   : Stream.txt::$DATA
    PSDrive       : C
    PSProvider    : Microsoft.PowerShell.Core\FileSystem
    PSIsContainer : False
    FileName      : C:\Test\Stream.txt
    Stream        : :$DATA
    Length        : 44
    ```

    ```powershell
    # Retrieve the content of the primary stream.
    # Note the single quotes to prevent variable substitution.
    Get-Content -Path .\Stream.txt -Stream ':$DATA'
    ```

    ```Output
    This is the content of the Stream.txt file
    ```

    ```powershell
    # Alternative way to get the same content.
    Get-Content -Path .\Stream.txt -Stream ""
    # The primary stream doesn't need to be specified to get the primary stream of the file.
    # This gets the same data as the prior two examples.
    Get-Content -Path .\Stream.txt
    ```

    ```Output
    This is the content of the Stream.txt file
    ```

    ```powershell
    # Use the Stream parameter of Add-Content to create a new Stream containing sample
    # content.
    $addContentSplat = @{
        Path = '.\Stream.txt'
        Stream = 'NewStream'
        Value = 'Added a stream named NewStream to Stream.txt'
    }
    Add-Content @addContentSplat

    # Use Get-Item to verify the stream was created.
    Get-Item -Path .\Stream.txt -Stream *
    ```

    ```Output
    PSPath        : Microsoft.PowerShell.Core\FileSystem::C:\Test\Stream.txt::$DATA
    PSParentPath  : Microsoft.PowerShell.Core\FileSystem::C:\Test
    PSChildName   : Stream.txt::$DATA
    PSDrive       : C
    PSProvider    : Microsoft.PowerShell.Core\FileSystem
    PSIsContainer : False
    FileName      : C:\Test\Stream.txt
    Stream        : :$DATA
    Length        : 44

    PSPath        : Microsoft.PowerShell.Core\FileSystem::C:\Test\Stream.txt:NewStream
    PSParentPath  : Microsoft.PowerShell.Core\FileSystem::C:\Test
    PSChildName   : Stream.txt:NewStream
    PSDrive       : C
    PSProvider    : Microsoft.PowerShell.Core\FileSystem
    PSIsContainer : False
    FileName      : C:\Test\Stream.txt
    Stream        : NewStream
    Length        : 46
    ```

    ```powershell
    # Retrieve the content of your newly created Stream.
    Get-Content -Path .\Stream.txt -Stream NewStream
    ```

    ```Output
    Added a stream named NewStream to Stream.txt
    ```

    The **Stream** parameter is a dynamic parameter of the
    [FileSystem provider](../Microsoft.PowerShell.Core/About/about_FileSystem_Provider.md#stream-string).
    By default `Get-Content` only retrieves data from the default, or `:$DATA` stream. **Streams** can
    be used to store hidden data such as attributes, security settings, or other data. They can also be
    stored on directories without being child items.

     --------- Example 6: Get raw content ---------

    The commands in this example get the contents of a file as one string, instead of an array of
    strings. By default, without the **Raw** dynamic parameter, content is returned as an array of
    newline-delimited strings. This example uses the `LineNumbers.txt` file referenced in Example 1.

    ```powershell
    $raw = Get-Content -Path .\LineNumbers.txt -Raw
    $lines = Get-Content -Path .\LineNumbers.txt
    Write-Host "Raw contains $($raw.Count) lines."
    Write-Host "Lines contains $($lines.Count) lines."
    ```

    ```Output
    Raw contains 1 lines.
    Lines contains 100 lines.
    ```

     --------- Example 7: Use Filters with Get-Content ---------

    You can specify a filter to the `Get-Content` cmdlet. When using filters to qualify the **Path**
    parameter, you need to include a trailing asterisk (`*`) to indicate the contents of the
    path.

    The following command gets the content of all `*.log` files in the `C:\Temp` directory.

    ```powershell
    Get-Content -Path C:\Temp\* -Filter *.log
    ```

     --------- Example 8: Get file contents as a byte array ---------

    This example demonstrates how to get the contents of a file as a `[byte[]]` as a single object.

    ```powershell
    $byteArray = Get-Content -Path C:\temp\test.txt -AsByteStream -Raw
    Get-Member -InputObject $byteArray
    ```

    ```Output
       TypeName: System.Byte[]

    Name           MemberType            Definition
    ----           ----------            ----------
    Count          AliasProperty         Count = Length
    Add            Method                int IList.Add(System.Object value)
    ```

    The first command uses the **AsByteStream** parameter to get the stream of bytes from the file. The
    **Raw** parameter ensures that the bytes are returned as a `[System.Byte[]]`. If the **Raw**
    parameter was absent, the return value is a stream of bytes, which is interpreted by PowerShell as
    `[System.Object[]]`.

 REMARKS
    To see the examples, type: "Get-Help Get-Content -Examples"
    For more information, type: "Get-Help Get-Content -Detailed"
    For technical information, type: "Get-Help Get-Content -Full"
    For online help, type: "Get-Help Get-Content -Online"

-_-

Thursday, June 11, 2026

My programming languages of choice: 06-11-2026

Shell

If the real work can be done with existing utilities.

Bash:
  • My default
  • Included with Git for Windows

PowerShell:
  • Included with Windows
  • JSON/XML support

PowerShell has delusions of being general-purpose and thus is horribly verbose. It requires typed parameter definitions and uses long function names (for example, "cd" is an alias for "Set-Location"). I can't imagine how anyone uses it interactively, where everything is one-time-use and speed is king. But you can call the entire .NET library from PowerShell, so it's very powerful for scripting, or so I've heard.

Your user is running Windows. Your user has some combination of settings that will manage to cause your script to fail, so he will need to read through your script and execute the commands interactively, copying and pasting with the mouse to mitigate syntax pain. Your user does not wish to install a new language for every utility you write. Therefore, you should write your programs in a language that is included with Windows and is interpreted rather than compiled. This narrows down your choices to the singleton set of PowerShell.

Though Windows, as mentioned, comes with PowerShell installed by default, it also blocks PowerShell scripts from running by default, so you may have to guide your user through that. Microsoft has obviously assumed that most people will wish to use PowerShell interactively, for which purpose it is a stomach cramp, or they could have just published it to winget and let you install it yourself. Also, the PowerShell included with Windows is ancient, arcane, and wrong; if you want a non-buggy experience, you'll need to install the latest cross-platform PowerShell (which is actually a separate program!) and use that. So PowerShell is installed by default with Windows, except that it isn't, and is primarily used interactively, for which it is no good, and is best for scripting, which is disallowed by default. And if you just run your scripts using the default installation to avoid installing anything, you'll still need Internet access to look up the command to allow scripting, because, instead of something simple like "chmod +x", it's "Set-ExecutionPolicy -Bypass", or possibly one of three or so other arguments instead of "Bypass", the difference between which escapes me at the moment. And you'll have to assure Windows that, yes, you know what you are about and would like to use their scripting language for Windows to run scripts on Windows.

The high practicality of running PowerShell, combined with the low practicality of writing it, is one of the best arguments I have heard for the use of AI.

Interpreted (non-shell)

Interpreted languages need installed to run, so I rarely ship them to others. I write my personal utilities either in Bash or in compiled languages. The result is that I almost never write programs in interpreted languages. I know Python and have programmed in Ruby, but I haven't found a non-educational use for either.

On the other hand, if someone else has written a program in an interpreted language, installation and continued updates are very easy for you. Just clone the Git repository somewhere useful and add "cd <source directory> && git pull" to your update script for each program.

Node.js:
  • Programmers probably have it installed already.
  • JSON support if jq is insufficient
  • Native regex support

Perl:
  • Native regex support
  • Easy for reading and writing files
  • Included with Git for Windows
  • Doesn't induce guilt like JavaScript since no one uses Perl anymore

awk: Line-by-line file transformations, I guess?

Compiled

I compile to native code and ship an executable. I don't do runtimes.

C99: Default choice

C#:
  • JSON/XML support
  • Needs an IDE by design
  • Verbose
  • Opinionated about all the wrong things

F#:
  • Can do (almost) anything C# can do
  • Can define functions without first making a class to put them in
  • Syntax almost as concise as Python, and sometimes much more so
  • Union types with exhaustive matching so that switches finally just work
  • Everything is like writing a pipeline of transforms in Bash. This is by design.

Of these three languages, everything today is written in C#. It has the tools. Who wants to go back to C? But when you try C after programming in C# for a long time, the first thing you notice (in your starting main.c, before you have started to worry about CMake, malloc, or header files) is the sudden calm. The shouting in your head has stopped. You suddenly know where everything is. When you say that you need a FooStruct, C allocates you a FooStruct, no constructor call needed. When you allocate some memory, you know exactly what goes into it. You don't have to remember which static manager class you put that function in because there are no qualifiers or namespaces of any sort. Or classes. Functions go where you put them. You can do funky things with enum case values without fear. You control everything. The world is yours.

It's addicting. I'm addicted.

F# is what you use to lose half the pain of C# while keeping the conveniences. Except that no one ever uses it because we're all used to programming in C# and switching syntaxes is hard. And, after a few years, C# will halfheartedly steal any idea it can with a new feature. You'll have to learn a new syntax, but you will suddenly have one fewer splinter in your foot, and all will be well with the world and with C#. The new feature won't work as well slapped onto C# as it did in F#, where it has existed from the early versions, but who will know? So we will all just continue to use C#, telling ourselves that it's just as good as F# now, which lie we will preserve so long as we do not spend an afternoon remembering how to use F# and recalling its true rhythm.

Tuesday, March 24, 2026

Programming languages and paradigms

Steps for creating a programming language:

  1. Think about who will want to use your new language and for what.
  2. Conjecture a unified mental model of how a programmer should think about your language. Make it fit how your users think and their expected use cases.
  3. Translate your mental model into machine code.
  4. Announce your new language to the world, making sure to report how users should think about your language.

Steps for creating a modern general-purpose programming language:

  1. Define your target audience as "everyone" and their expected use cases as "anything ever".
  2. Make a unified mental model of how everyone should approach doing anything ever. Model it after how everyone currently does everything.
  3. ???
  4. Be a large corporation. Make everyone use your language to interact with your products. Eventually, people will start using it for other tasks out of familiarity.

That was probably the fastest I have yet derailed a blog post. I want to talk about how to make a proper mental model.

Switch-statements

C# requires a control flow statement at the end of every case in a switch-statement. I'm sure they wished to add an implicit `break` after every (non-empty) case, but that would have shocked developers coming from C, where cases fall through by default. So they resigned themselves to making the user choose control-flow behavior explicitly, presumably reasoning that any default would surprise someone.

I know why C chose default fall-through and C# didn't.

In my oversimplified understanding, control flow constructs compile to jump-instructions--the equivalent of using `goto` everywhere. Switch-statements compile to jump tables, which figure out how far forward in the code to jump before you start executing again.

Notably, a jump table says nothing about where to jump after that. It doesn't know what a switch case is. So a `break` statement at the end of the switch case is another jump instruction--extra effort for the runtime. Fall-through is the default in C because it is the default for the implementation. C probably expects its programmers to know already basically what the assembler will do and to write programs based on that knowledge.

C# doesn't do that. C# would consider that an implementation detail. Why would you need to know assembly concepts to program in C#? In C#, fall-through requires the use of `goto`, the most damning marker of unidiomaticity that I can imagine. The creators of the C# language did not want people using switch-statements with fall-through. (Stephen Toub does, though, if I remember correctly.)

(Actually, switch-statements don't always compile to jump tables in C#. The probably do if you're switching on int (or char or bool), but they can also do pattern matching using the same syntax. In C#, the code you write may have a completely different implementation based on context.)

The point

Why does every modern language try to be opaque?

Richard Gabriel says that simplicity of implementation is at least as important as simplicity of interface. What sense does this make unless the user is supposed to understand the implementation? And Joel Spolsky argues that the user will need a correct mental model of the implementation anyway. These are big figures in software, worthy of our attention.

We all agree that programs should be simple. Is there any plausible measurement of software simplicity other than how easy it is for the computer to run? ("Quick for the programmer to write" is a separate question; a throwaway script may rely on brute force.)

To write good code, you must first have the right mental model. The right mental model is that which most correctly describes the problem. In programming, every problem is flipping some bits somewhere into the proper state. So should we not be thinking in those terms? Should we not use technologies that we are allowed to understand?

Friday, March 20, 2026

Managing vim plugins

I wish to draw attention to this brilliant Reddit comment:

vim-plug, minpac or just git clone to your ~/.vim/pack/my-plugins/start/ directory.

My update script now sports this crudely elegant addition:


while read -r dir; do
    cd "$dir"
    git pull
done < <(find ~/.vim/pack -maxdepth 3 -mindepth 3 -type d)

And I need not remember which plugin manager I installed. Everything is taken care of.

I love simple technologies.

Thursday, February 12, 2026

Review: The new Outlook for Windows

Outlook is asking for my feedback. I went to the bother to type it up, so I'm posting it here as well to get as much mileage out of my effort as possible. I don't trust Microsoft in general to read feedback, but I think that the Outlook team does.

Likelihood to recommend: 1/5

Why?


The feedback

I am a programmer, not a suit. Outlook's strength is in coordinating meetings. My job doesn't require many meetings.

I have to use Outlook because my company does. The features I would look for in an email client:
  • Plain-text email
  • Markdown support
  • HTML editing
  • Keyboard-based navigation
  • Scripting capabilities
I do appreciate that Outlook now allows me to check email headers.

I find Microsoft 365's Ctrl+Backspace behavior idiosyncratic and unhelpful--except that it works properly in this feedback box, which is nice.

I actively disable any sort of smart tooling, even spelling autocorrection, and definitely including Copilot. I don't use Copilot to draft emails because it would be simpler just to send the prompt. I don't use Copilot to summarize emails because I don't read emails that need summarized.

Outlook and Teams have all these buttons I will never use and care about, but I still get a reminder for a recurring meeting every day at the default time even though I changed the reminder time. The notification comes up fifteen minutes before the meeting. The little drop-down box says "10 minutes before", which is what I selected. It is most amusing.

The "Upload to OneDrive" button

  1. I go to attach a file to an email.
  2. The file is too big, so I drop it onto the button for "Upload to OneDrive and send link".
  3. I send the email.
  4. The recipient emails me back to tell me that he can't access the file at the link I sent.
What good is a link that the recipient can't access? If I am attempting to email a file, can we not assume that I wish the recipient to be able to read it? Why can't the "Upload to OneDrive" button make the file accessible by the recipient of the email? Or at least provide a quick way to do that?

I feel as though the assumption is that I will mostly send files to others within the company. But I do nearly all my internal communication through Teams. So if I send a file by email, then I am almost certainly sending it outside the organization.

(Unless I am sending a script that isn't in the Git repo. If I send new versions by Teams, then Teams will increment the file name of each version. This increment will include a space character, forcing the recipient either to rename the file after downloading it or to quote the file name when he types it into the terminal. So I might send the script by email to avoid having the file automatically renamed. But I digress.)

Thursday, February 5, 2026

Generate more, parse less

Be liberal in what you accept, and conservative in what you send.

--Postel's Prescription. (I think that this popular quote is relevant to this post, but I can't decide how.)

I wrote a code generator to take a C enum type and make a string parsing function for it. While writing this parser, I ran into parsing difficulties.

Code has to be generated from some other information. To get that information, I had to read it from another file. So I had to parse it myself before I could write my parser.

On my first try, I started by reading the header file containing the enum type. I immediately ran into apparently necessary feature creep. What if the user put other code before the enum in the file? What if the user defined values for the cases? These cases would require me to expend a lot more work to implement, but a user would be shocked to find that his code suddenly didn't work because he did these perfectly normal things. I implemented more features than I needed for my use case, but still felt that my approach was far too naive and constrained. Eventually I gave up.

The next time, I made a text file listing the cases I wanted. Then I iterated over that and generated both the enum and the parser function. Easy as pi.

For the understanding of non-programmers, which looks easier to deal with?

gardening-tools.h:


  #ifndef GARDENINGTOOLS_H
  #define GARDENINGTOOLS_H
  
  typedef enum GardeningTools {
    Trowel,
    Hose,
    Dynamite
  }
  
  #endif // GARDENINGTOOLS_H

Or gardening-tools.enum:


  Trowel
  Hose
  Dynamite

But they contain the same information! I can generate the second from the first!

If I have to read data, why would I encode it in a format that has more features than I need to use right now?

Hooray for flat, simple data layouts! My three lines of data doesn't need the power of C. Here is the entire spec for my layout: Put a line break after each case name. I get the enum type name from the input file name.

I can add more features if I wish. Since my format is simple, modifying it is easy. Values for the enum cases? Put a space between the case name and value. File name different from the enum name? One extra parameter. Documentation comments? Now lines starting with "//" pass through to the output unmodified. But I probably won't need any of that, so why bother now?

Code generation: Accomplished

Number of library references added: 0

Tuesday, January 20, 2026

Rethinking my past dislike of C#

In response to my previous post.


We don't use "goto", of course, because we want our iteration to be structured.

But "for" is only for collection iteration with indexing. For everything else, we use "while", even though "for" gives the reader the accumulator variable, means of accumulation, and end condition upfront. And then we scatter in a few "break"s to confuse the hurried reader further.

Still annoyed at the limited idiom of "for".


We use "for" instead of "foreach" for performance

On lists that could have been arrays. We always call "ToList", never "ToArray".

ToArray often builds a list first, then converts it to an array, which takes an extra copy operation. ToArray could be worth it if you're just using Select or other length-preserving transformations.

Also, most codebases don't use "for" instead of "foreach" for performance. That's not idiomatic.


We don't need discriminated unions. Classes and interfaces are enough for any situation.

And then we "switch" on them and have to repair bugs having to do with missing cases that were created later.

Now that I am older and wiser, I realize that my greatest mistake was when I thought that inheritance might have been worth using to get exhaustive matching. If you are programming in C#, learn to accept that you don't get exhaustive matching. If you use inheritance, you will end up with so many layers of indirection that you never quite know where to find the code that does the thing. Don't do polymorphism. Kill it on sight. You are allowed to use "if" and to switch on enums. That is all.

C# is supposedly getting DUs one of these days. I wonder whether that will change my opinion.


We don't use static fields and singleton classes. Instead, we use dependency injection frameworks to ensure that only one instance of each class is generated and that all other classes have access to that instance. Which is different.

I'm withholding judgment on DI until I have been bitten by global objects. To figure this one out, I'll need to use more global objects.


We don't have an order of compilation below the project level. Every class in a project, unless it is private to another class, is accessible from every other class. We deal with circular class dependencies by not worrying about them. Code goes wherever happens to feel best at the moment.

I haven't thought about this in a while. F#'s file ordering was quite nice.

I think that this problem probably mostly goes away if you understand what your program does. Of course, this requires writing a program that a human can understand.

The non-F# solution is to split the program into small units with narrow interfaces. Maybe that is why the early C programmers encouraged us to split functionality into separate executables and communicate between them with plain text. Your interface can't be very complex if you have to parse all input and output.


"IConvertsFooToBar" is supposed to be prettier than "Func<Foo, Bar>" for some reason.

Granted, "Func" is an ugly word. But I guess we had already used up all the punctuation elsewhere.

And we need to have separate "Func" and "Action" delegates since you can't have a value of type "void".

Same answer as for polymorphism: C# wasn't designed for passing functions. Don't do that.

Don't pass interfaces, either, if you can help it. Refer to the previous comment on avoiding polymorphism. Flat code is easy to read.


We use static typing so that we can be sure that all representable cases are valid.

Unless they're null. Anything could be null. (Except structs, but we don't use structs.)

A defined value representing "no value" is highly useful. This is a sub-problem of understanding your program's flow so that you know the range of possible values at any point. This, again, requires writing an understandable program.


"default". Without "default", you have to make two versions of every generic method, one for classes and one for structs. With "default", we still have to make two versions of every generic method since the two have completely different behaviors. For classes, it returns "null", which might blow up your program if you're not on guard. For structs, it returns an instance in which every field is set to its own "default", which is significantly worse since it's very difficult to detect.

Now that I have written a little C, "default" makes sense. Now it is the distinction between structs and classes that I don't understand. Why can I only allocate structs on the stack and classes on the heap? Isn't allocation strategy more related to data access and lifetime than to data type? I have yet to explore functional programming in C, but at least I can pass a reference to stack-allocated data.


Exceptions are unexceptional. We wrap deep calls in try/catch because something in there will probably throw an exception at some point, and just because there was an exception doesn't mean that we need to crash now.

Catching all exceptions from a function call is evil; you don't know exactly where the exception was thrown or how the program state has changed. You must at least take some effort to recover rather than continuing blithely on as though no error had occurred.

That's more a pitfall than a language problem, though F#'s native Either type provides a much nicer alternative to exceptions than C# does.


Everything is a "manager". "Manager" is pretty close to synonymous with "class" when you think about it.

This is an anti-pattern, but also C#'s fault for making us wrap all code in classes while telling us that each class should do one thing. How do you name a class and method that do one thing? WidgetFrobber.FrobWidget? WidgetFrobber.Frob? WidgetFrobber.Do? FrobWidget.Execute? I usually go with WidgetFrobation.Do, but it still feels wrong. Organizing code by verbs is often better than organizing it by nouns.


I don't mind C# as much as I did then; I have since spent much time learning to simplify code to avoid the features that make C# painful to use. C# has records now, which take most of the headache out of classes. If you avoid polymorphism of any sort--interfaces, inheritance, or passing delegates--and structure your code as a straightforward imperative program, then modern C# isn't so bad.

PowerShell help doesn't.

I want to print a file to the console. I wonder what my options are. If I'm in Git Bash (no man ), I use the documentation built into t...