PSToolLauncher.psm1

#Requires -Version 5.1

using namespace System.IO;
using namespace System.Management.Automation;

# +-----------------------------------------------------------------------------------------------------------
# | Public functions
# |
<#
#>

function Open-Tool
{
    [CmdletBinding(PositionalBinding = $false)]
    param(
        [Parameter(Position = 0, Mandatory)]
        [string] $ToolKey,

        [Parameter(ValueFromRemainingArguments)]
        $Arguments
    )

    process
    {
        Write-Verbose 'BEGIN: Open-Tool';
        try {
            $toolInfo = Get-ToolInstallation $ToolKey;
            if($toolInfo.IsInstalled -ne $true) {
                throw [InvalidOperationException] "'$($toolInfo.Definition.$KeyToolName)' is not installed.";
            }

            Write-Verbose "Executing '$($toolInfo.Definition.$KeyToolName) version '$($toolInfo.DefaultVersion.Version)'.";
            & $toolInfo.DefaultVersion.Command $Arguments;

            Write-Verbose 'OK: Open-Tool';
        }
        catch {
            Write-Verbose 'ERROR: Open-Tool';
            throw;
        }
        finally {
            Write-Verbose 'END: Open-Tool';
        }
    }
}

function Open-NotepadPlusPlus
{
    [CmdletBinding(PositionalBinding = $false)]
    param(
        [Parameter(ValueFromRemainingArguments)]
        $Arguments
    )

    process
    {
        Write-Verbose 'EXECUTING Open-NotepadPlusPlus.';
        Open-Tool -ToolKey $KeyNotepadPlusPlus -Arguments $Arguments;
    }
}

function Open-SqlAdmin
{
    [CmdletBinding(PositionalBinding = $false)]
    param(
        [Parameter(ValueFromRemainingArguments)]
        $Arguments
    )

    process
    {
        Write-Verbose 'EXECUTING Open-SqlAdmin.';
        Open-Tool -ToolKey $KeySqlAdmin -Arguments $Arguments;
    }
}

# +-----------------------------------------------------------------------------------------------------------
# | Private functions
# |
function Find-ToolFile
{
    [CmdletBinding(PositionalBinding = $false)]
    [OutputType([FileInfo])]
    param(
        [Parameter(Position = 0, Mandatory, ValueFromPipeline)]
        [DirectoryInfo[]] $ProductDirectories,

        [Parameter(Position = 1, Mandatory)]
        [string[]] $ExecutableNames,

        [Parameter()]
        [switch] $FindOnPath
    )

    process
    {
        Write-Verbose 'BEGIN: Find-ToolFile';
        try {
            $exeNameList = $ExecutableNames -join ', ';
            $allSearchDirectories = @();
            $allSearchDirectories += $ProductDirectories;

            if($FindOnPath) {
                $allSearchDirectories += $Env:Path -split ';' `
                                       | Select-Object -Unique `
                                       | Get-Item -ErrorAction SilentlyContinue;
            }

            $allSearchDirectories `
            | Select-Object -Unique `
            | ForEach-Object {
                Write-Verbose "Searching product directory '$_' for executables '$exeNameList'.";

                # return
                Get-ChildItem -Path $_ -File `
                | Where-Object {
                    $ExecutableNames -contains $_.Name;
                }
            }

            Write-Verbose 'OK: Find-ToolFile';
        }
        catch {
            Write-Verbose 'ERROR: Find-ToolFile';
            throw;
        }
        finally {
            Write-Verbose 'END: Find-ToolFile';
        }
    }
}

function Get-ProgramFilesDirectory
{
    [CmdletBinding(PositionalBinding = $false)]
    [OutputType([DirectoryInfo])]
    param()

    process
    {
        Write-Verbose 'BEGIN: Get-ProgramFilesDirectory';
        try {
            $toolDirs = Get-ChildItem -Path Env:ProgramFiles* `
                      | Select-Object -ExpandProperty Value;

            # return
            $toolDirs | Select-Object -Unique `
                      | Get-Item -ErrorAction SilentlyContinue;

            Write-Verbose 'OK: Get-ProgramFilesDirectory';
        }
        catch {
            Write-Verbose 'ERROR: Get-ProgramFilesDirectory';
            throw;
        }
        finally {
            Write-Verbose 'END: Get-ProgramFilesDirectory';
        }
    }
}

function Get-ToolInstallation
{
    [CmdletBinding(PositionalBinding = $false)]
    [OutputType('PSToolLauncher.Installation')]
    param(
        [Parameter(Position = 0, Mandatory)]
        [string] $ToolKey,

        [Parameter()]
        [switch] $Force
    )

    process
    {
        Write-Verbose 'BEGIN: Get-ToolInstallation';
        try {
            Write-Verbose "Retrieving tool installation information for '$ToolKey'.";

            if($Force -or $ToolInstallations.ContainsKey($ToolKey) -eq $false) {
                Write-Verbose "Acquiring installation information for '$ToolKey'.";

                $toolDef = $ToolDefinitions[$ToolKey];
                if($null -eq $toolDef) {
                    throw [InvalidOperation] "Tool '$ToolKey' is not a known common tool, available common tools are: (list of common tools).";
                }

                $productDirectories = & $toolDef.$KeyGetProductDirectory;

                $findOnPath = $toolDef.$KeyIgnorePath -ne $true;
                $toolVersions = Find-ToolFile $productDirectories $toolDef.$KeyExecutableNames -FindOnPath:$findOnPath `
                              | Get-ToolVersion;

                $ToolInstallations[$ToolKey] = New-ToolInstallation -ToolDefinition $toolDef -ToolVersions $toolVersions;
            }

            # return
            $ToolInstallations[$ToolKey];

            Write-Verbose 'OK: Get-ToolInstallation';
        }
        catch {
            Write-Verbose 'ERROR: Get-ToolInstallation';
            throw;
        }
        finally {
            Write-Verbose 'END: Get-ToolInstallation';
        }
    }
}

function Get-ToolVersion
{
    [CmdletBinding(PositionalBinding = $false)]
    [OutputType('PSToolLauncher.ToolVersion')]
    param(
        [Parameter(Position = 0, Mandatory, ValueFromPipeline)]
        [FileInfo[]] $ToolFiles
    )

    process
    {
        Write-Verbose 'BEGIN: Get-ToolVersion';
        try {
            $sortableVersionFormat = '{0}{1:D6}.{2:D6}.{3:D6}.{4:D6}.{5:D6}';

            $sequenceNumber = $ToolFiles.Length;
            $ToolFiles | ForEach-Object {
                Write-Verbose "Get tool version information for executable '$_'.";

                $versionInfo = $_.VersionInfo;
                if($null -eq $versionInfo) {
                    $exeDescription         = $_.BaseName;
                    $printableVersionNumber = 'Unknown';
                    $sortableProductVersion = $sortableVersionFormat -f 'X', 0, 0, 0, 0, $sequenceNumber
                    $sortableFileVersion    = $sortableVersionFormat -f 'X', 0, 0, 0, 0, $sequenceNumber
                }
                else {
                    $exeDescription         = $versionInfo.FileDescription;
                    $printableVersionNumber = '{0:D}.{1:D}.{2:D}.{3:D}' -f $versionInfo.ProductMajorPart, `
                                                                           $versionInfo.ProductMinorPart, `
                                                                           $versionInfo.ProductBuildPart, `
                                                                           $versionInfo.ProductPrivatePart;
                    $sortableProductVersion = $sortableVersionFormat -f 'V', $versionInfo.ProductMajorPart, `
                                                                             $versionInfo.ProductMinorPart, `
                                                                             $versionInfo.ProductBuildPart, `
                                                                             $versionInfo.ProductPrivatePart, `
                                                                             $sequenceNumber;
                    $sortableFileVersion    = $sortableVersionFormat -f 'V', $versionInfo.FileMajorPart, `
                                                                             $versionInfo.FileMinorPart, `
                                                                             $versionInfo.FileBuildPart, `
                                                                             $versionInfo.FilePrivatePart, `
                                                                             $sequenceNumber;
                }

                Write-Verbose "Tool '$exeDescription' is version '$printableVersionNumber'.";

                # return
                New-Object PSObject -Property @{
                    Command               = (Get-Command $_);
                    Version               = $printableVersionNumber;
                    OrderedProductVersion = $sortableProductVersion;
                    OrderedFileVersion    = $sortableFileVersion;
                } `
                | ForEach-Object {
                    $_.PSTypeNames.Insert(0, 'PSToolLauncher.ToolVersion');
                    $_;
                };

                $sequenceNumber--;
            }

            Write-Verbose 'OK: Get-ToolVersion';
        }
        catch {
            Write-Verbose 'ERROR: Get-ToolVersion';
            throw;
        }
        finally {
            Write-Verbose 'END: Get-ToolVersion';
        }
    }
}

function New-ToolDefinition
{
    [CmdletBinding(SupportsShouldProcess, PositionalBinding = $false)]
    [OutputType('PSToolLauncher.Definition')]
    param(
        [Parameter(Position = 0, Mandatory)]
        [string] $ToolName,

        [Parameter(Mandatory)]
        [string] $NounPrefix,

        [Parameter(Mandatory)]
        [string[]] $ExecutableNames,

        [Parameter()]
        [CommandInfo] $GetProductDirectory,

        [Parameter()]
        [switch] $IgnorePath,

        [Parameter()]
        [string[]] $Aliases
    )

    process
    {
        Write-Verbose 'BEGIN: New-ToolDefinition';
        try {
            Write-Verbose "Creating ToolDefinition for '$ToolName'.";

            if($PSCmdlet.ShouldProcess($ToolDefinition.$KeyToolName)) {
                if($null -eq $GetProductDirectory) {
                    $GetProductDirectory = Get-Command -Name "Get-$($NounPrefix)ProductDirectory";
                }

                $toolDef = New-Object PSObject -Property @{
                    $KeyToolName            = $ToolName;
                    $KeyNounPrefix          = $NounPrefix;
                    $KeyGetProductDirectory = $GetProductDirectory;
                    $KeyExecutableNames     = $ExecutableNames;
                    $KeyIgnorePath          = $IgnorePath -eq $true;
                    $KeyAliases             = $Aliases;
                };

                $toolDef.PSTypeNames.Insert(0, 'PSToolLauncher.Definition');

                # return
                $toolDef;
            }

            Write-Verbose 'OK: New-ToolDefinition';
        }
        catch {
            Write-Verbose 'ERROR: New-ToolDefinition';
            throw;
        }
        finally {
            Write-Verbose 'END: New-ToolDefinition';
        }
    }
}

function New-ToolInstallation
{
    [CmdletBinding(SupportsShouldProcess, PositionalBinding = $false)]
    [OutputType('PSToolLauncher.Installation')]
    param(
        [Parameter(Position = 0, Mandatory)]
        [PSTypeName('PSToolLauncher.Definition')]
        [object] $ToolDefinition,

        [Parameter(Mandatory)]
        [PSTypeName('PSToolLauncher.ToolVersion')]
        [object[]] $ToolVersions
    )

    process
    {
        Write-Verbose 'BEGIN: New-ToolInstallation';
        try {
            Write-Verbose "Creating tool installation information for '$($ToolDefinition.$KeyToolName)'.";

            if($PSCmdlet.ShouldProcess($ToolDefinition.$KeyToolName)) {
                $isInstalled = $ToolVersions.Count -gt 0;
                $defaultVersion = $ToolVersions `
                                | Sort-Object -Property OrderedFileVersion, OrderedPropertVersion `
                                | Select-Object -First 1;

                # return
                New-Object PSObject -Property @{
                    Definition     = $ToolDefinition;
                    Versions       = $ToolVersions;
                    IsInstalled    = $isInstalled;
                    DefaultVersion = $defaultVersion;
                } `
                | ForEach-Object {
                    $_.PSTypeNames.Insert(0, 'PSToolLauncher.Installation');
                    $_;
                };
            }

            Write-Verbose 'OK: New-ToolInstallation';
        }
        catch {
            Write-Verbose 'ERROR: New-ToolInstallation';
            throw;
        }
        finally {
            Write-Verbose 'END: New-ToolInstallation';
        }
    }
}

function Get-NotepadPlusPlusProductDirectory
{
    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([DirectoryInfo])]
    param()

    process
    {
        Write-Verbose 'BEGIN: Get-NotepadPlusPlusProductDirectory';
        try {
            Get-ProgramFilesDirectory `
            | ForEach-Object {
                $productDir = Join-Path $_ 'Notepad++';

                # return
                Get-Item -Path $productDir -ErrorAction SilentlyContinue;
            }

            Write-Verbose 'OK: Get-NotepadPlusPlusProductDirectory';
        }
        catch {
            Write-Verbose 'ERROR: Get-NotepadPlusPlusProductDirectory';
            throw;
        }
        finally {
            Write-Verbose 'END: Get-NotepadPlusPlusProductDirectory';
        }
    }
}

function Get-SqlAdminProductDirectory
{
    [CmdletBinding(PositionalBinding = $False)]
    [OutputType([DirectoryInfo])]
    param()

    process
    {
        Write-Verbose 'BEGIN: Get-SqlAdminProductDirectory';
        try {
            Get-ProgramFilesDirectory `
            | ForEach-Object {
                $programFilesDir = $_;

                # return
                Get-ChildItem -Path $programFilesDir -Filter 'Microsoft SQL Server Management Studio*' `
                | Get-ChildItem -Directory -Recurse;

                # return
                Get-ChildItem -Path $programFilesDir -Filter 'Microsoft SQL Server' `
                | Get-ChildItem -Directory `
                | Where-Object { $_.Name -match '^[1-9][0-9]*$' } `
                | Get-ChildItem -Directory -Recurse;
            }

            Write-Verbose 'OK: Get-SqlAdminProductDirectory';
        }
        catch {
            Write-Verbose 'ERROR: Get-SqlAdminProductDirectory';
            throw;
        }
        finally {
            Write-Verbose 'END: Get-SqlAdminProductDirectory.';
        }
    }
}

# ##############################################
#
# This section contains the function and alias
# that are actually exported from this module
#
New-Variable -Name KeyToolName -Option Constant `
             -Value 'ToolName';
New-Variable -Name KeyNounPrefix -Option Constant `
             -Value 'NounPrefix';
New-Variable -Name KeyGetProductDirectory -Option Constant `
             -Value 'Get-ProductDirectory';
New-Variable -Name KeyExecutableNames -Option Constant `
             -Value 'ExeNames';
New-Variable -Name KeyIgnorePath -Option Constant `
             -Value 'IgnorePath';
New-Variable -Name KeyAliases -Option Constant `
             -Value 'Aliases';

New-Variable -Name KeyNotepadPlusPlus -Option Constant `
             -Value 'notepad-plusplus';
New-Variable -Name ToolDefNotepadPlusPlus -Option Constant `
             -Value (New-ToolDefinition -ToolName 'Notepad++' `
                                        -NounPrefix 'NotepadPlusPlus' `
                                        -ExecutableNames 'notepad++.exe' `
                                        -Aliases 'notepad++');

New-Variable -Name KeySqlAdmin -Option Constant `
             -Value 'sqladmin-ide';
New-Variable -Name ToolDefSqlAdmin -Option Constant `
             -Value (New-ToolDefinition -ToolName 'SQL Server Management Studio' `
                                        -NounPrefix 'SqlAdmin' `
                                        -ExecutableNames 'ssms.exe', 'sqlwb.exe' `
                                        -Aliases 'sqladmin');

New-Variable -Name ToolDefinitions -Option ReadOnly `
             -Value @{
                 $KeyNotepadPlusPlus = $ToolDefNotepadPlusPlus;
                 $KeySqlAdmin        = $ToolDefSqlAdmin;
             };
New-Variable -Name ToolInstallations `
             -Value @{};

$PublicAliases = @();

$ToolDefinitions.Keys `
| ForEach-Object {
    $toolDef = $ToolDefinitions[$_];

    foreach($alias in $toolDef.$KeyAliases) {
        Set-Alias -Name $alias -Value "Open-$($toolDef.$KeyNounPrefix)" -Force;
        $PublicAliases += $alias;
    }
};

$PublicFunctions = @(
    'Open-NotepadPlusPlus',
    'Open-SqlAdmin',
    'Open-Tool'
);

if($DebugPreference -ne 'SilentlyContinue') {
    Write-Debug 'Adding all cmdlets for debug purposes.';

    $PublicFunctions += @(
        'Get-NotepadPlusPlusProductDirectory',
        'Get-ProgramFilesDirectory',
        'Get-SqlAdminProductDirectory',
        'Get-ToolInstallation',
        'Get-ToolVersion',
        'Find-ToolFile',
        'New-ToolDefinition',
        'New-ToolInstallation'
    );
}

Export-ModuleMember -Function $PublicFunctions `
                    -Alias $PublicAliases;