functions/getModule.ps1

function getModule {
    [CmdletBinding()]
    Param
    (
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Version,

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

        [Parameter(Mandatory)]
        [ValidateSet('Install', 'Update')]
        [string]
        $CommandType,

        [Parameter()]
        [PSCredential] $Credential,

        [Parameter()]
        [securestring] $Token
    )

    Convert-Path -Path $Path -ErrorAction Stop > $null  #throw exception when the path not exist

    $paramHash = @{}
    if ($PSBoundParameters.ContainsKey('Name')) {
        $paramHash.Name = $Name
    }
    if ($PSBoundParameters.ContainsKey('Version')) {
        $paramHash.Version = $Version
    }
    $moduleType = parseModuleType @paramHash

    switch ($moduleType.Type) {
        'GitHub' {
            $local:paramHash = $moduleType
            $paramHash.Remove('Type')

            if ($Credential) {$paramHash.Credential = $Credential}
            elseif ($Token) {$paramHash.Token = $Token}

            getModuleFromGitHub @paramHash -Path $Path
        }

        'PSGallery' {
            $local:paramHash = $moduleType
            $paramHash.Remove('Type')

            if ($CommandType -eq 'Update') {
                $paramHash.Force = $true
            }

            getModuleFromPSGallery @paramHash -Path $Path
        }
    }
}

function getModuleVersion {
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory)]
        [string]
        $Name
    )

    $moduleType = parseModuleType -Name $Name

    switch ($moduleType.Type) {
        'GitHub' {
            # getModuleVersionFromGitHub @paramHash -Path $Path
        }

        'PSGallery' {
            getModuleVersionFromPSGallery -Name $moduleType.Name
        }
    }
}


function getModuleVersionFromPSGallery {
    [CmdletBinding()]
    param
    (
        # The name of module
        [Parameter(Mandatory)]
        [string]
        $Name
    )

    $foundModules = @()

    if ((Get-Command Find-Module).Parameters.AllowPrerelease) {
        # Only PowerShellGet 1.6.0+ has AllowPrerelease param
        try {
            Find-Module -Name $Name -AllVersions -AllowPrerelease | ForEach-Object {$foundModules += $_}
        }
        catch {
            #Ignore Statement-terminating errors
        }
    }
    else {
        try {
            Find-Module -Name $Name -AllVersions | ForEach-Object {$foundModules += $_}
        }
        catch {
            #Ignore Statement-terminating errors
        }
    }

    if (($foundModules | Measure-Object).count -le 0) {
        Write-Error ('{0}: No match found for the specified search criteria and module name' -f $Name)
        return $null
    }
    else {
        $foundModules
    }
}


function getModuleFromGitHub {
    [CmdletBinding()]
    Param
    (
        # Parameter help description
        [Parameter(Mandatory)]
        [string]
        $Name,

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

        [Parameter()]
        [string]
        $Branch,

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

        [Parameter()]
        [PSCredential] $Credential,

        [Parameter()]
        [securestring] $Token
    )

    $PlatformTemp =
    if ($env:TEMP) {$env:TEMP}
    elseif ($env:TMPDIR) {$env:TMPDIR}
    elseif (Test-Path '/tmp' -PathType Container) {'/tmp'}
    else {Write-Error 'Could not find standard temp folder'; return}

    $TempDir = New-Item (Join-Path $PlatformTemp '/pspm') -Force -ItemType Directory -ErrorAction Stop
    $TempName = [System.Guid]::NewGuid().toString() + '.zip'
    $TargetDir = (Join-Path $Path $Name)

    # Get commit hash
    $paramHash = @{Owner = $Account; Repository = $Name}
    if ($Branch) {$paramHash.Ref = $Branch}
    if ($Credential) {$paramHash.Credential = $Credential}
    elseif ($Token) {$paramHash.Token = $Token}

    try {
        $CommitHash = Get-CommitHash @paramHash -ErrorAction Stop
    }
    catch {
        Write-Error $_.Exception
        return
    }

    if (-not $CommitHash) {
        Write-Error 'Could not get repository info'
        return
    }

    # Test whether the specified module already exists
    if (Test-Path $TargetDir) {
        $private:moduleInfo = Get-ModuleInfo $TargetDir
        if ($private:moduleInfo.Name -eq $Name) {
            if (Test-Path (Join-Path $TargetDir '.pspminfo')) {
                $private:hash = Get-Content -Path (Join-Path $TargetDir '.pspminfo')
                if ($private:hash -eq $CommitHash) {
                    Write-Host ('{0}@{1}: Module already exists in Modules directory. Skip download.' -f $Name, $private:moduleInfo.ModuleVersion)
                    $private:moduleInfo
                    return
                }
            }
        }
    }

    try {
        #Download zip from GitHub
        Write-Host ('{0}: Downloading module from GitHub.' -f $Name)
        $paramHash = @{Owner = $Account; Repository = $Name; Ref = $CommitHash}
        if ($Credential) {$paramHash.Credential = $Credential}
        elseif ($Token) {$paramHash.Token = $Token}
        Get-Zipball @paramHash -OutFile (Join-Path $TempDir $TempName) -ErrorAction Stop

        if (Test-Path (Join-Path $TempDir $TempName)) {
            Expand-Archive -Path (Join-Path $TempDir $TempName) -DestinationPath $TempDir
            $downloadedModule = Get-ChildItem -Path $TempDir -Filter ('{0}-{1}*' -f $Account, $Name) -Directory

            $moduleInfo = $downloadedModule.PsPath | Get-ModuleInfo

            #Copy to /Modules folder
            if (Test-Path (Join-Path $Path $moduleInfo.Name)) {
                Remove-Item -Path (Join-Path $Path $moduleInfo.Name) -Recurse -Force
            }
            $downloadedModule | Copy-Item -Destination (Join-Path $Path $moduleInfo.Name) -Recurse -Force -ErrorAction Stop

            #Save commit hash info
            $CommitHash | Out-File -FilePath (Join-Path (Join-Path $Path $moduleInfo.Name) '.pspminfo') -Force

            #Return module info
            $moduleInfo
        }
        else {
            Write-Error 'Download failed!'
        }
    }
    catch {
        Write-Error $_.Exception
    }
    finally {
        if (Test-Path $TempDir) {
            #Cleanup temp folder
            Remove-Item $TempDir -Recurse -Force -ErrorAction SilentlyContinue
        }
    }
}


function getModuleFromPSGallery {
    [CmdletBinding()]
    param
    (
        # The name of module
        [Parameter(Mandatory)]
        [string]
        $Name,

        # Desired version (semver range expression)
        [Parameter()]
        [string]
        $Version = '*',

        # The path for download
        [Parameter(Mandatory)]
        [string]
        $Path,

        # Get module from PSGallery even if the module already exists
        [Parameter()]
        [switch]
        $Force
    )

    $Latest = ($Version -eq 'Latest')   #"Latest" is special term

    if (-not $Latest) {
        try {
            $SemVerRange = [pspm.SemVerRange]::new($Version) #throw exception on parse error
        }
        catch {
            throw
            return
        }
    }

    if ((-not $Latest) -and (-not $Force)) {
        if (Test-Path (Join-path $Path $Name)) {
            $local:moduleInfo = Get-ModuleInfo -Path (Join-path $Path $Name) -ErrorAction SilentlyContinue
            if ($local:moduleInfo -and $SemVerRange.IsSatisfied($local:moduleInfo.ModuleVersion)) {
                # Already exist
                Write-Host ('{0}@{1}: Module already exists in Modules directory. Skip download.' -f $Name, $local:moduleInfo.ModuleVersion)
                $moduleInfo
                return
            }
        }
    }

    $foundModules = getModuleVersionFromPSGallery -Name $Name -ErrorAction SilentlyContinue

    if ($Latest) {
        $targetModule = $foundModules | Sort-Object -Property {[pspm.SemVer]$_.Version} -Descending | Select-Object -First 1
    }
    else {
        $targetModule = $foundModules | Where-Object {($null -ne $_.Version) -and $SemVerRange.IsSatisfied($_.Version)} | Sort-Object -Property {[pspm.SemVer]$_.Version} -Descending | Select-Object -First 1
    }

    if (($targetModule | Measure-Object).count -le 0) {
        Write-Error ('{0}: No match found for the specified search criteria and module name' -f $Name)
        return
    }

    if (-not $Force) {
        if (Test-Path (Join-path $Path $Name)) {
            $local:moduleInfo = Get-ModuleInfo -Path (Join-path $Path $Name) -ErrorAction SilentlyContinue
            if (([Version]$targetModule.Version) -eq $moduleInfo.ModuleVersion) {
                # Already downloaded
                Write-Host ('{0}@{1}: Module already exists in Modules directory. Skip download.' -f $Name, $targetModule.Version)
                $moduleInfo
                return
            }
        }
    }

    if (Test-Path (Join-path $Path $Name)) {
        Remove-Item -Path (Join-path $Path $Name) -Recurse -Force
    }

    Write-Host ('{0}@{1}: Downloading module.' -f $Name, $targetModule.Version)
    $targetModule | Save-Module -Path $Path -Force -ErrorAction Stop

    if (Test-Path (Join-path $Path $Name)) {
        $moduleInfo = Get-ModuleInfo -Path (Join-path $Path $Name) -ErrorAction SilentlyContinue
        $moduleInfo
    }
}


function parseModuleType {
    param
    (
        [Parameter()]
        [string]
        $Name,

        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]
        $Version
    )

    $Result = @{}

    if ($PSBoundParameters.ContainsKey('Name')) {
        #dependencies
        switch -regex ($Version) {
            #Git Urls
            '^git\+?.*:' {
                #Not implemented
                Write-Warning ('Sorry! This version specification format is not supported.')
                break
            }

            #Local path & Urls
            '^<http|https|file>://' {
                #Not implemented
                Write-Warning ('Sorry! This version specification format is not supported.')
                break
            }

            # GitHub Urls
            '^[^/]+/[^/]+' {
                $local:userAccount = $_.Split("/")[0]
                $local:repoName = $_.Split("/")[1].Split("#")[0]
                $local:branch = $_.Split("/")[1].Split("#")[1]

                $Result = @{
                    Type    = 'GitHub'
                    Name    = $repoName
                    Account = $userAccount
                    Branch  = $branch
                }

                break
            }

            # <version> (PSGallery)
            Default {
                $Result = @{
                    Type    = 'PSGallery'
                    Name    = $Name
                    Version = $_
                }
            }
        }
    }
    else {
        #parameter
        switch -regex ($Version) {
            #Git Urls
            '^git\+?.*:' {
                #Not implemented
                Write-Warning ('Sorry! This version specification format is not supported.')
                break
            }

            #Local path & Urls
            '^<http|https|file>://' {
                #Not implemented
                Write-Warning ('Sorry! This version specification format is not supported.')
                break
            }

            # GitHub Urls
            '^[^/]+/[^/]+' {
                $local:userAccount = $_.Split("/")[0]
                $local:repoName = $_.Split("/")[1].Split("#")[0]
                $local:branch = $_.Split("/")[1].Split("#")[1]

                $Result = @{
                    Type    = 'GitHub'
                    Name    = $repoName
                    Account = $userAccount
                    Branch  = $branch
                }

                break
            }

            # <name>@<version> (PSGallery)
            '^.+@.+' {
                $local:moduleName = $_.Split("@")[0]
                $local:version = $_.Split("@")[1]

                $Result = @{
                    Type    = 'PSGallery'
                    Name    = $moduleName
                    Version = $version
                }

                break
            }

            # <name> (PSGallery)
            Default {
                $local:moduleName = $_.Split("@")[0]

                $Result = @{
                    Type    = 'PSGallery'
                    Name    = $moduleName
                    Version = '*'
                }
            }
        }
    }

    $Result
}