Wsl.psm1

# Copyright (c) Sven Groot. See LICENSE for details.

# Represents the state of a distribution.
enum WslDistributionState {
    Stopped
    Running
    Installing
    Uninstalling
    Converting
}

# Represents a WSL distribution.
class WslDistribution
{
    WslDistribution()
    {
        $this | Add-Member -Name FileSystemPath -Type ScriptProperty -Value {
            return "\\wsl$\$($this.Name)"
        }

        $defaultDisplaySet = "Name","State","Version","Default"

        #Create the default property display set
        $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet("DefaultDisplayPropertySet",[string[]]$defaultDisplaySet)
        $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)
        $this | Add-Member MemberSet PSStandardMembers $PSStandardMembers
    }

    [string] ToString()
    {
        return $this.Name
    }

    [string]$Name
    [WslDistributionState]$State
    [int]$Version
    [bool]$Default
    [Guid]$Guid
    [string]$BasePath
}

# Ensure IsWindows is set for Windows PowerShell to make future checks easier.
if ($PSVersionTable.PSVersion.Major -lt 6) {
    $IsWindows = $true
}

if ($IsWindows) {
    $wslPath = "$env:windir\system32\wsl.exe"
    if (-not [System.Environment]::Is64BitProcess) {
        # Allow launching WSL from 32 bit powershell
        $wslPath = "$env:windir\sysnative\wsl.exe"
    }

} else {
    # If running inside WSL, rely on wsl.exe being in the path.
    $wslPath = "wsl.exe"
}

# Helper that will launch wsl.exe, correctly parsing its output encoding, and throwing an error
# if it fails.
function Invoke-Wsl
{
    $hasError = $false
    if ($PSVersionTable.PSVersion.Major -lt 6) {
        try {
            $oldOutputEncoding = [System.Console]::OutputEncoding
            [System.Console]::OutputEncoding = [System.Text.Encoding]::Unicode
            $output = &$wslPath $args
            if ($LASTEXITCODE -ne 0) {
                throw "Wsl.exe failed: $output"
                $hasError = $true
            }

        } finally {
            [System.Console]::OutputEncoding = $oldOutputEncoding
        }

    } else {
        # Using Console.OutputEncoding is currently broken on PowerShell Core, so use an alternative
        # method of starting wsl.exe.
        # See: https://github.com/PowerShell/PowerShell/issues/10789
        $startInfo = New-Object System.Diagnostics.ProcessStartInfo $wslPath
        $args | ForEach-Object { $startInfo.ArgumentList.Add($_) }
        $startInfo.RedirectStandardOutput = $true
        $startInfo.StandardOutputEncoding = [System.Text.Encoding]::Unicode
        $process = [System.Diagnostics.Process]::Start($startInfo)
        $output = @()
        while ($null -ne ($line = $process.StandardOutput.ReadLine())) {
            if ($line.Length -gt 0) {
                $output += $line
            }
        }

        $process.WaitForExit()
        if ($process.ExitCode -ne 0) {
            throw "Wsl.exe failed: $output"
            $hasError = $true
        }
    }

    # $hasError is used so there's no output in case error action is silently continue.
    if (-not $hasError) {
        return $output
    }
}

# Helper to parse the output of wsl.exe --list
function Get-WslDistributionHelper()
{
    # Use --verbose if it's available.
    # N.B. If running inside WSL, it's assumed verbose is available since it's harder to determine
    # the Windows version number.
    if ([System.Environment]::OSVersion.Version.Build -ge 18917 -or -not $IsWindows) {
        Invoke-Wsl --list --verbose | Select-Object -Skip 1 | ForEach-Object { 
            $fields = $_.Split(@(" "), [System.StringSplitOptions]::RemoveEmptyEntries) 
            $defaultDistro = $false
            if ($fields.Count -eq 4) {
                $defaultDistro = $true
                $fields = $fields | Select-Object -Skip 1
            }

            [WslDistribution]@{
                "Name" = $fields[0]
                "State" = $fields[1]
                "Version" = [int]$fields[2]
                "Default" = $defaultDistro
            }
        }

    } else {
        # Fall back to the old command line (version will always be 1 in this case).
        # N.B. This is intended for Windows 10 version 1903; this script won't work on older
        # versions that use wslconfig.exe
        try { 
            $running = Invoke-Wsl --list --running 

        } catch {
            # Wsl.exe returns a non-zero error code if there are no running distros, so ignore the
            # error.
            $running = @()
        }

        Invoke-Wsl --list | Select-Object -Skip 1 | ForEach-Object {
            # A line-ending translation bug in wsl.exe causes some of the output of wsl.exe --list
            # to look like blank lines when redirected.
            if ($_.Length -gt 0) {
                $name = $_
                $defaultDistro = $false
                $distroState = [WslDistributionState]::Stopped

                # "Default" is localized to just match on the (), which is illegal in a distribution name.
                $index = $name.IndexOf("(")
                if ($index -ge 0) {
                    $defaultDistro = $true
                    $name = $name.Substring(0, $index).Trim()
                }

                # Check if it's running.
                # N.B. Other states such as installing can't be distinguished with older versions.
                if ($running.Contains($_)) {
                    $distroState = [WslDistributionState]::Running
                }

                [WslDistribution]@{
                    "Name" = $name
                    "State" = $distroState
                    "Version" = 1
                    "Default" = $defaultDistro
                }
            }
        }
    }
}

# Helper to get additional distribution properties from the registry.
function Get-WslDistributionProperties([WslDistribution]$Distribution)
{
    $key = Get-ChildItem "hkcu:\SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss" | Get-ItemProperty | Where-Object { $_.DistributionName -eq $Distribution.Name }
    if ($key) {
        $Distribution.Guid = $key.PSChildName
        $Distribution.BasePath = $key.BasePath
        if ($Distribution.BasePath.StartsWith("\\?\")) {
            $Distribution.BasePath = $Distribution.BasePath.Substring(4)
        }
    }
}

<#
.SYNOPSIS
Gets the WSL distributions installed on the computer.
 
.DESCRIPTION
The Get-WslDistribution cmdlet gets objects that represent the WSL distributions on the computer.
 
This cmdlet wraps the functionality of "wsl.exe --list --verbose".
 
.PARAMETER Name
Specifies the distribution names of distributions to be retrieved. Wildcards are permitted. By
default, this cmdlet gets all of the distributions on the computer.
 
.PARAMETER Default
Indicates that this cmdlet gets only the default distribution. If this is combined with other
parameters such as Name, nothing will be returned unless the default distribution matches all the
conditions. By default, this cmdlet gets all of the distributions on the computer.
 
.PARAMETER State
Indicates that this cmdlet gets only distributions in the specified state (e.g. Running). By
default, this cmdlet gets all of the distributions on the computer.
 
.PARAMETER Version
Indicates that this cmdlet gets only distributions that are the specified version. By default,
this cmdlet gets all of the distributions on the computer.
 
.INPUTS
System.String
 
You can pipe a distribution name to this cmdlet.
 
.OUTPUTS
WslDistribution
 
The cmdlet returns objects that represent the distributions on the computer.
 
.EXAMPLE
Get-WslDistribution
Name State Version Default
---- ----- ------- -------
Ubuntu Stopped 2 True
Ubuntu-18.04 Running 1 False
Alpine Running 2 False
Debian Stopped 1 False
 
Get all WSL distributions.
 
.EXAMPLE
Get-WslDistribution -Default
Name State Version Default
---- ----- ------- -------
Ubuntu Stopped 2 True
 
Get the default distribution.
 
.EXAMPLE
Get-WslDistribution -Version 2 -State Running
Name State Version Default
---- ----- ------- -------
Alpine Running 2 False
 
Get running WSL2 distributions.
 
.EXAMPLE
Get-WslDistribution Ubuntu* | Stop-WslDistribution
 
Terminate all distributions that start with Ubuntu
 
.EXAMPLE
Get-Content distributions.txt | Get-WslDistribution
Name State Version Default
---- ----- ------- -------
Ubuntu Stopped 2 True
Debian Stopped 1 False
 
Use the pipeline as input.
#>

function Get-WslDistribution
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$false, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [string[]]$Name,
        [Parameter(Mandatory=$false)]
        [Switch]$Default,
        [Parameter(Mandatory=$false)]
        [WslDistributionState]$State,
        [Parameter(Mandatory=$false)]
        [int]$Version
    )

    process {
        $distributions = Get-WslDistributionHelper
        if ($Default) {
            $distributions = $distributions | Where-Object {
                $_.Default
            }
        }

        if ($PSBoundParameters.ContainsKey("State")) {
            $distributions = $distributions | Where-Object {
                $_.State -eq $State
            }
        }

        if ($PSBoundParameters.ContainsKey("Version")) {
            $distributions = $distributions | Where-Object {
                $_.Version -eq $Version
            }
        }

        if ($Name.Length -gt 0) {
            $distributions = $distributions | Where-Object {
                foreach ($pattern in $Name) {
                    if ($_.Name -ilike $pattern) {
                        return $true
                    }
                }
                
                return $false
            }
        }

        # The additional registry properties aren't available if running inside WSL.
        if ($IsWindows) {
            $distributions | ForEach-Object {
                Get-WslDistributionProperties $_
            }
        }

        return $distributions
    }
}

<#
.SYNOPSIS
Stops one or more running WSL distributions.
 
.DESCRIPTION
The Stop-WslDistribution cmdlet terminates each of the specified WSL distributions. You can specify
distributions by their names, or use the Distribution parameter to pass an object returned by
Get-WslDistribution.
 
This cmdlet wraps the functionality of "wsl.exe --terminate".
 
.PARAMETER Name
Specifies the distribution names of distributions to be terminated. Wildcards are permitted.
 
.PARAMETER Distribution
Specifies WslDistribution objects that represent the distributions to be terminated.
 
.PARAMETER Passthru
Returns an object that represents the distribution. By default, this cmdlet does not generate any
output.
 
.INPUTS
WslDistribution, System.String
 
You can pipe a WslDistribution object retrieved by Get-WslDistribution, or a string that contains
the distribution name to this cmdlet.
 
.OUTPUTS
WslDistribution
 
The cmdlet returns an object that represent the distribution, if you use the Passthru parameter.
Otherwise, this cmdlet does not generate any output.
 
.EXAMPLE
Stop-WslDistribution Ubuntu
 
Stops the distribution named "Ubuntu".
 
.EXAMPLE
Stop-WslDistribution Ubuntu*
 
Terminate all distributions whose names start with Ubuntu
 
.EXAMPLE
Get-WslDistribution -Version 2 | Stop-WslDistribution -Passthru
Name State Version Default
---- ----- ------- -------
Ubuntu Stopped 2 True
Alpine Stopped 2 False
 
Stops all WSL2 distributions.
#>

function Stop-WslDistribution
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "DistributionName", Position = 0)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildCards()]
        [string[]]$Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Distribution")]
        [WslDistribution[]]$Distribution,
        [Parameter(Mandatory = $false)]
        [Switch]$Passthru
    )

    process
    {
        if ($PSCmdlet.ParameterSetName -eq "DistributionName") {
            $Distribution = Get-WslDistribution $Name
        }

        $Distribution | ForEach-Object {
            if ($Distribution.State -ne [WslDistributionState]::Running) {
                Write-Warning "Distribution $($_.Name) is not running."

            } elseif ($PSCmdlet.ShouldProcess($_.Name, "Terminate")) {
                Invoke-Wsl --terminate $_.Name
            }
        }
    }
}

<#
.SYNOPSIS
Configures one or more WSL distributions
 
.DESCRIPTION
The Set-WslDistribution cmdlet changes the properties of a WSL distribution. You can specify
distributions by their names, or use the Distribution parameter to pass an object returned by
Get-WslDistribution.
 
This cmdlet wraps the functionality of "wsl.exe --set-default" and "wsl.exe --set-version".
 
.PARAMETER Name
Specifies the distribution names of distributions to be configured. Wildcards are permitted.
 
.PARAMETER Distribution
Specifies WslDistribution objects that represent the distributions to be configured.
 
.PARAMETER Version
When specified, converts the distribution to the specified version. This may take several minutes.
 
.PARAMETER Default
When specified, sets the distribution as the default distribution. If multiple distributions are
specified as input, the last one processed will be the default after the command finishes.
 
.PARAMETER Passthru
Returns an object that represents the distribution. By default, this cmdlet does not generate any
output.
 
.INPUTS
WslDistribution, System.String
 
You can pipe a WslDistribution object retrieved by Get-WslDistribution, or a string that contains
the distribution name to this cmdlet.
 
.OUTPUTS
WslDistribution
 
The cmdlet returns an object that represent the distribution, if you use the Passthru parameter.
Otherwise, this cmdlet does not generate any output.
 
.EXAMPLE
Set-WslDistribution Ubuntu -Default
 
Makes the distribution named "Ubuntu" the default.
 
.EXAMPLE
Get-WslDistribution -Version 1 | Set-WslDistribution -Version 2 -Passthru
Name State Version Default
---- ----- ------- -------
Ubuntu-18.04 Running 2 False
Debian Stopped 2 False
 
Converts all version 1 distributions to version 2.
#>

function Set-WslDistribution
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "DistributionName", Position = 0)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildCards()]
        [string[]]$Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Distribution")]
        [WslDistribution[]]$Distribution,
        [Parameter(Mandatory = $false)]
        [int]$Version = 0,
        [Parameter(Mandatory = $false)]
        [Switch]$Default,
        [Parameter(Mandatory = $false)]
        [Switch]$Passthru
    )

    process
    {
        if ($PSCmdlet.ParameterSetName -eq "DistributionName") {
            $Distribution = Get-WslDistribution $Name
        }

        $Distribution | ForEach-Object {
            if ($Version -ne 0) {
                if ($Distribution.Version -eq $Version) {
                    Write-Warning "The distribution '$($Distribution.Name)' is already the requested version."

                } elseif ($PSCmdlet.ShouldProcess($Distribution.Name, "Set Version")) {
                    # Suppress output since it messes with passthru
                    Invoke-Wsl --set-version $Distribution.Name $Version | Out-Null
                }
            }

            if ($Default) {
                if ($Distribution.Default) {
                    Write-Warning "The distribution '$($Distribution.Name)' is already the default."

                } if ($PSCmdlet.ShouldProcess($Distribution.Name, "Set Default")) {
                    Invoke-Wsl --set-default $Distribution.Name | Out-Null
                }
            }

            # Get updated info for pass-through.
            if ($Passthru) {
                Get-WslDistribution $Distribution.Name
            }
        }
    }
}

<#
.SYNOPSIS
Removes one or more WSL distributions from the computer.
 
.DESCRIPTION
The Remove-WslDistribution cmdlet unregisters each of the specified WSL distributions. You can specify
distributions by their names, or use the Distribution parameter to pass an object returned by
Get-WslDistribution.
 
This cmdlet wraps the functionality of "wsl.exe --unregister".
 
.PARAMETER Name
Specifies the distribution names of distributions to be removed. Wildcards are permitted.
 
.PARAMETER Distribution
Specifies WslDistribution objects that represent the distributions to be removed.
 
.INPUTS
WslDistribution, System.String
 
You can pipe a WslDistribution object retrieved by Get-WslDistribution, or a string that contains
the distribution name to this cmdlet.
 
.OUTPUTS
None. This cmdlet does not generate any output.
 
.EXAMPLE
Remove-WslDistribution Ubuntu
 
Unregisters the distribution named "Ubuntu".
 
.EXAMPLE
Remove-WslDistribution Ubuntu*
 
Unregisters all distributions whose names start with Ubuntu
 
.EXAMPLE
Get-WslDistribution -Version 1 | Remove-WslDistribution
 
Unregisters all WSL1 distributions.
 
.EXAMPLE
Get-WslDistribution | Where-Object { $_.Name -ine "Ubuntu" } | Remove-WslDistribution
 
Unregisters all distributions except the one named "Ubuntu".
#>

function Remove-WslDistribution
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "DistributionName", Position = 0)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildCards()]
        [string[]]$Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Distribution")]
        [WslDistribution[]]$Distribution
    )

    process
    {
        if ($PSCmdlet.ParameterSetName -eq "DistributionName") {
            $Distribution = Get-WslDistribution $Name
        }

        $Distribution | ForEach-Object {
            if ($PSCmdlet.ShouldProcess($_.Name, "Unregister")) {
                Invoke-Wsl --unregister $_.Name | Out-Null
            }
        }
    }
}

<#
.SYNOPSIS
Exports one or more WSL distributions to a .tar.gz file.
 
.DESCRIPTION
The Export-WslDistribution cmdlet exports each of the specified WSL distributions to a gzipped
tarball. You can specify distributions by their names, or use the Distribution parameter to pass
an object returned by Get-WslDistribution.
 
You can export multiple distributions by specifying a directory as the Destination. In this case,
this cmdlet will automatically create files using the distribution name with the extension .tar.gz.
 
This cmdlet wraps the functionality of "wsl.exe --export".
 
.PARAMETER Name
Specifies the distribution names of distributions to be exported. Wildcards are permitted.
 
.PARAMETER Distribution
Specifies WslDistribution objects that represent the distributions to be exported.
 
.PARAMETER Destination
Specifies the destination directory or file name where the exported distribution will be stored.
 
If you specify an existing directory as the destination, this cmdlet will append a file name based
on the distribution name. If you specify a non-existing file name, that name will be used verbatim.
 
.PARAMETER Passthru
Returns an object that represents the distribution. By default, this cmdlet does not generate any
output.
 
.INPUTS
WslDistribution, System.String
 
You can pipe a WslDistribution object retrieved by Get-WslDistribution, or a string that contains
the distribution name to this cmdlet.
 
.OUTPUTS
WslDistribution
 
The cmdlet returns an object that represent the distribution, if you use the Passthru parameter.
Otherwise, this cmdlet does not generate any output.
 
.EXAMPLE
Export-WslDistribution Ubuntu D:\backup.tar.gz
 
Exports the distribution named "Ubuntu" to a file named D:\backup.tar.gz.
 
.EXAMPLE
Export-WslDistribution Ubuntu* D:\backup
 
Exports all distributions whose names start with Ubuntu to files in a directory named D:\backup.
 
.EXAMPLE
Get-WslDistribution -Version 2 | Export-WslDistribution -Destination D:\backup -Passthru
Name State Version Default
---- ----- ------- -------
Ubuntu Stopped 2 True
Alpine Stopped 2 False
 
Exports all WSL2 distributions to a directory named D:\backup.
#>

function Export-WslDistribution
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "DistributionName", Position = 0)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildCards()]
        [string[]]$Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Distribution")]
        [WslDistribution[]]$Distribution,
        [Parameter(Mandatory = $true, Position = 1)]
        [string]$Destination,
        [Parameter(Mandatory = $false)]
        [Switch]$Passthru
    )

    process
    {
        if ($PSCmdlet.ParameterSetName -eq "DistributionName") {
            $Distribution = Get-WslDistribution $Name
        }

        $Distribution | ForEach-Object {
            $fullPath = $Destination
            if (Test-Path $Destination -PathType Container) {
                $fullPath = Join-Path $Destination "$($_.Name).tar.gz"
            }

            if (Test-Path $fullPath) {
                throw "The path '$fullPath' already exists."
            }

            if ($PSCmdlet.ShouldProcess("Name: $($_.Name), Path: $fullPath", "Export")) {
                Invoke-Wsl --export $_.Name $fullPath
            }

            if ($Passthru) {
                $Distribution
            }
        }
    }
}

<#
.SYNOPSIS
Imports one or more WSL distributions from a .tar.gz file.
 
.DESCRIPTION
The Import-WslDistribution cmdlet imports each of the specified gzipped tarball files to a WSL
distribution. to a gzipped.
 
By default, this cmdlet derives the distribution name from the input file name, and appends that
name to the destination path. This allows you to import multiple distributions.
 
This cmdlet wraps the functionality of "wsl.exe --import".
 
.PARAMETER Path
Specifies the path to a .tar.gz file to import. Wildcards are permitted.
 
.PARAMETER LiteralPath
Specifies the path to a .tar.gz file to import. The value of LiteralPath is used exactly as it is
typed. No characters are interpreted as wildcards.
 
.PARAMETER Destination
Specifies the destination directory or file name where the imported distribution will be stored. The
distribution name will be appended to this path (e.g. if you specify "D:\wsl" and the distribution
is named "Ubuntu", the distribution will be stored in "D:\wsl\Ubuntu"), unless the RawDestination
parameter is specified.
 
.PARAMETER Name
Specifies the name of the imported WSL distribution.
 
By default, this cmdlet uses the base name of the file being imported (e.g. "Ubuntu" if the file
is "Ubuntu.tar.gz"). Note that distribution names can only contain letters, numbers, dashes and
underscores. If the file contains any other characters, you must specify a distribution name.
 
If you specify a distribution name, you cannot import multiple distributions with one command.
 
.PARAMETER Version
Specifies the WSL version to use for the imported distribution. By default, this cmdlet uses the
version set with "wsl.exe --set-default-version".
 
.PARAMETER RawDestination
Indicates that the destination path should be used as is, without appending the distribution name
to it. By default, the distribution name is appended to the path.
 
If RawDestination is specified, you cannot import multiple distributions with one command.
 
.PARAMETER Passthru
Returns an object that represents the distribution. By default, this cmdlet does not generate any
output.
 
.INPUTS
System.String
 
You can pipe a string that contains a path to this cmdlet.
 
.OUTPUTS
WslDistribution
 
The cmdlet returns an object that represent the distribution, if you use the Passthru parameter.
Otherwise, this cmdlet does not generate any output.
 
.EXAMPLE
Import-WslDistribution D:\backup.tar.gz D:\wsl Ubuntu
 
Imports the file named D:\backup.tar.gz as a distribution named "Ubuntu" stored in D:\wsl\Ubuntu.
 
.EXAMPLE
Import-WslDistribution D:\backup.tar.gz D:\wsl\mydistro Ubuntu -RawDestination
 
Imports the file named D:\backup.tar.gz as a distribution named "Ubuntu" stored in D:\wsl\mydistro.
 
.EXAMPLE
Import-WslDistribution D:\backup\*.tar.gz D:\wsl
 
Imports all .tar.gz files from D:\backup to distributions with names based on the file names, stored
in subdirectories of D:\wsl.
 
.EXAMPLE
Get-Item D:\backup\*.tar.gz -Exclude Ubuntu* | Import-WslDistribution -Destination D:\wsl -Version 2 -Passthru
Name State Version Default
---- ----- ------- -------
Alpine Stopped 2 False
Debian Stopped 2 False
 
Imports all .tar.gz files, except those whose names start with Ubuntu, as WSL2 distributions stored
in subdirectories of D:\wsl.
#>

function Import-WslDistribution
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Path", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [string[]] $Path,
        [Parameter(Mandatory = $true, ParameterSetName = "LiteralPath", ValueFromPipelineByPropertyName = $true)]
        [Alias("PSPath")]
        [ValidateNotNullOrEmpty()]
        [string[]] $LiteralPath,
        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string]$Destination,
        [Parameter(Mandatory = $false)]
        [string]$Name,
        [Parameter(Mandatory = $false)]
        [int]$Version = 0,
        [Parameter(Mandatory = $false)]
        [Switch]$RawDestination,
        [Parameter(Mandatory = $false)]
        [Switch]$Passthru
    )

    process {
        if ($PSCmdlet.ParameterSetName -eq "Path") {
            $files = Get-Item $Path
        } else {
            $files = Get-Item -LiteralPath $LiteralPath
        }    

        $files | ForEach-Object {
            $DistributionName = $Name
            if ($DistributionName -eq "") {
                $DistributionName = $_.BaseName
                # If the file name is .tar.gz, the base name isn't what we want.
                if ($DistributionName.EndsWith(".tar", "OrdinalIgnoreCase")) {
                    $DistributionName = $DistributionName.Substring(0, $DistributionName.Length - 4)
                }
            }

            $DistributionDestination = $Destination
            if (-not $RawDestination) {
                $DistributionDestination = Join-Path $DistributionDestination $DistributionName
            }

            if ($PSCmdlet.ShouldProcess("Path: $($_.FullName), Destination: $DistributionDestination, Name: $DistributionName", "Import")) {
                $args = @("--import", $DistributionName, $DistributionDestination, $_.FullName)
                if ($Version -ne 0) {
                    $args += @("--version", $Version)
                }

                Invoke-Wsl @args
            }

            if ($Passthru) {
                Get-WslDistribution $DistributionName
            }
        }
    }
}

<#
.SYNOPSIS
Runs a command in one or more WSL distributions.
 
.DESCRIPTION
The Invoke-WslCommand cmdlet executes the specified command on the specified distributions, and
then exits.
 
This cmdlet will raise an error if executing wsl.exe failed (e.g. there is no distribution with
the specified name) or if the command itself failed.
 
This cmdlet wraps the functionality of "wsl.exe <command>".
 
.PARAMETER Command
Specifies the command to run.
 
.PARAMETER DistributionName
Specifies the distribution names of distributions to run the command in. Wildcards are permitted.
By default, the command is executed in the default distribution.
 
.PARAMETER Distribution
Specifies WslDistribution objects that represent the distributions to run the command in.
By default, the command is executed in the default distribution.
 
.PARAMETER User
Specifies the name of a user in the distribution to run the command as. By default, the
distribution's default user is used.
 
.INPUTS
WslDistribution, System.String
 
You can pipe a WslDistribution object retrieved by Get-WslDistribution, or a string that contains
the distribution name to this cmdlet.
 
.OUTPUTS
System.String
 
This command outputs the result of the command you executed, as text.
 
.EXAMPLE
Invoke-WslCommand 'ls /etc'
 
Runs a command in the default distribution.
 
.EXAMPLE
Invoke-WslCommand 'whoami' -DistributionName Ubuntu* -User root
 
Runs a command in all distributions whose names start with Ubuntu, as the "root" user.
 
.EXAMPLE
Get-WslDistribution -Version 2 | Invoke-WslCommand 'echo $(whoami) in $WSL_DISTRO_NAME'
 
Runs a command in all WSL2 distributions.
#>

function Invoke-WslCommand
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string]$Command,
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "DistributionName", Position = 1)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildCards()]
        [string[]]$DistributionName,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Distribution")]
        [WslDistribution[]]$Distribution,
        [Parameter(Mandatory = $false, Position = 2)]
        [ValidateNotNullOrEmpty()]
        [string]$User
    )

    process {
        if ($PSCmdlet.ParameterSetName -eq "DistributionName") {
            if ($DistributionName) {
                $Distribution = Get-WslDistribution $DistributionName
            } else {
                $Distribution = Get-WslDistribution -Default
            }
        }

        $Distribution | ForEach-Object {
            $args = @("--distribution", $_.Name)
            if ($User) {
                $args += @("--user", $User)
            }

            # Invoke /bin/bash so the whole command can be passed as a single argument.
            $args += @("/bin/bash", "-c", $Command)

            if ($PSCmdlet.ShouldProcess($_.Name, "Invoke Command")) {
                &$wslPath $args
                if ($LASTEXITCODE -ne 0) {
                    # Note: this could be the exit code of wsl.exe, or of the launched command.
                    throw "Wsl.exe returned exit code $LASTEXITCODE"
                }    
            }
        }
    }
}

<#
.SYNOPSIS
Enters a session in a WSL distribution.
 
.DESCRIPTION
The Enter-WslDistribution cmdlet starts an interactive shell in the specified distribution.
 
This cmdlet will raise an error if executing wsl.exe failed (e.g. there is no distribution with
the specified name) or if the session exited with an error code.
 
This cmdlet wraps the functionality of "wsl.exe" with no arguments other than possibly
"--distribution" or "--user".
 
.PARAMETER Name
Specifies the name of the distribution to enter. Wildcards are NOT permitted.
By default, the command enters the default distribution.
 
.PARAMETER Distribution
Specifies a WslDistribution object that represent the distributions to enter.
By default, the command is executed in the default distribution.
 
.PARAMETER User
Specifies the name of a user in the distribution to enter as. By default, the
distribution's default user is used.
 
.INPUTS
WslDistribution, System.String
 
You can pipe a WslDistribution object retrieved by Get-WslDistribution, or a string that contains
the distribution name to this cmdlet.
 
.OUTPUTS
None. This cmdlet does not return any output.
 
.EXAMPLE
Enter-WslDistribution
 
Start a shell in the default distribution.
 
.EXAMPLE
Enter-WslDistribution Ubuntu root
 
Starts a shell in the distribution named "Ubuntu" using the "root" user.
 
.EXAMPLE
Import-WslDistribution D:\backup\Alpine.tar.gz D:\wsl -Passthru | Enter-WslDistribution
 
Imports a WSL distribution and immediately opens a shell in that distribution.
#>

function Enter-WslDistribution
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ParameterSetName = "DistributionName", Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = "Distribution")]
        [WslDistribution]$Distribution,
        [Parameter(Mandatory = $false, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string]$User
    )

    process {
        if ($PSCmdlet.ParameterSetName -eq "Distribution") {
            $Name = $Distribution.Name
        }

        $args = @()
        if ($Name) {
            $args = @("--distribution", $Name)
        }

        if ($User) {
            $args = @("--user", $User)
        }

        if ($PSCmdlet.ShouldProcess($Name, "Enter WSL")) {
            &$wslPath $args
            if ($LASTEXITCODE -ne 0) {
                # Note: this could be the exit code of wsl.exe, or of the shell.
                throw "Wsl.exe returned exit code $LASTEXITCODE"
            }    
        }
    }
}

<#
.SYNOPSIS
Stops all WSL distributions.
 
.DESCRIPTION
The Stop-Wsl cmdlet terminates all WSL distributions, and for WSL2 also shuts down the lightweight
utility VM.
 
This cmdlet wraps the functionality of "wsl.exe --shutdown".
 
.INPUTS
None. This cmdlet does not take any input.
 
.OUTPUTS
None. This cmdlet does not generate any output.
 
.EXAMPLE
Stop-Wsl
 
Shuts down WSL.
#>

function Stop-Wsl
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param()

    if ($PSCmdlet.ShouldProcess("Wsl", "Shutdown")) {
        Invoke-Wsl --shutdown
    }
}

$tabCompletionScript = {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    (Get-WslDistributionHelper).Name | Where-Object { $_ -ilike "$wordToComplete*" } | Sort-Object
}

Register-ArgumentCompleter -CommandName Get-WslDistribution,Stop-WslDistribution,Set-WslDistribution,Remove-WslDistribution,Export-WslDistribution,Enter-WslDistribution -ParameterName Name -ScriptBlock $tabCompletionScript
Register-ArgumentCompleter -CommandName Invoke-WslCommand -ParameterName DistributionName -ScriptBlock $tabCompletionScript

Export-ModuleMember Get-WslDistribution
Export-ModuleMember Stop-WslDistribution
Export-ModuleMember Set-WslDistribution
Export-ModuleMember Remove-WslDistribution
Export-ModuleMember Export-WslDistribution
Export-ModuleMember Import-WslDistribution
Export-ModuleMember Invoke-WslCommand
Export-ModuleMember Enter-WslDistribution
Export-ModuleMember Stop-Wsl