Out-Shebang.ps1

function Out-Shebang
{
    <#
    .Synopsis
        Outputs a shebang
    .Description
        Outputs a shebang file and marks it as executable.

        Shebang files can have no extension and will be run with the interpreter declared on the first line.
    .Link
        https://en.wikipedia.org/wiki/Shebang_(Unix)
    .Example
        { "hello world" } | Out-Shebang
    .Example
        { "hello world $args" } | Out-Shebang -OutputPath ./HelloShebang
    .Example
        Out-Shebang -Script "mount -a" -OutputPath /etc/network/if-up.d/AutoMountNetworkDrives
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
    # The scripcontents of the Shebang.
    [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName,Position=1)]
    [Alias('ScriptContents','ScriptBlock','Fullname','Definition')]
    [string]
    $Script,

    # The output path. If not provided, the contents of the shebang will be outputted.
    [Parameter(ValueFromPipelineByPropertyName)]    
    [string]
    $OutputPath,

    # The interpreter.
    # If this is not provided, and a ScriptBlock or ExternalScript is piped in (or the alias -ScriptBlock is used)
    [string]
    $Interpreter = '/bin/sh'
    )

    process {
        # Determine if the $script was a path or not by checking if the file exists.
        $scriptPathExists = 
            try {
                Test-Path $Script -ErrorAction SilentlyContinue
            }
            catch { $null }
        
        # If the file exists and no interpreter was passed, attempt to auto-detect it.
        if ($scriptPathExists -and -not $PSBoundParameters['Interpreter']) {
            # |Extensions|Interpreter|
            # |-|-|
            switch ($scriptPathExists.Extension) { 
                .ps1 { $Interpreter = '/bin/pwsh' }            # |.ps1 | /bin/pwsh |
                .js  { $Interpreter = '/usr/bin/env node' }    # |.js | /usr/bin/env node |
                .py  { $Interpreter = '/usr/bin/env python3' } # |.py | /usr/bin/env python3 |
                .sh  { $Interpreter = '/bin/sh' }              # |.sh | /bin/sh python3 |
            }
        }
        
        #region Generate the Shebang Content
        $in, $myInv = $_, $MyInvocation # Assign the raw input object to $in.
        if (-not $PSBoundParameters["Interpreter"]) { # If no -Interpreter was passed, see if it looks like a script
            if ($in -is [ScriptBlock] -or # If $in was a scriptblock
                $in -is [Management.Automation.ExternalScriptInfo] -or # or an external script
                $myInv.Line -match ' -ScriptBlock' # or it looks like the -ScriptBlock alias was used
            ) {
                $Interpreter = '/bin/pwsh' # assume PowerShell.
            } else {                
                $Interpreter = '/bin/sh'   # Otherwise, assume bash.
            }
        }

        # If we're making a Shebang for a function
        if ($in -is [Management.Automation.FunctionInfo] -or 
            $in -is [Management.Automation.FilterInfo] -or
            $in -is [Management.Automation.CmdletInfo]
        ) { 
            if (-not $PSBoundParameters['Interpreter']) {   # set the interpreter to PowerShell if we haven't already.
                $Interpreter = '/bin/pwsh'
            }
            if ($in.ModuleName -ne 'Microsoft.PowerShell.Core' -and $in.Module.Path) { # If the function came from a module.
                # recursively determine the import paths
                filter importPaths {
                    $module = $_
                    foreach ($req in $module.RequiredModules) { $req | importPaths }
                    if ($module.Path) {
                        $reqParentPath = $module.Path | Split-Path
                        if ($reqParentPath -match '\d.\d$') {
                            $reqParentPath | Split-Path
                        } elseif ($($reqParentPath | Split-Path -Leaf) -like "*$($module.Name)"){
                            $reqParentPath
                        } else {
                            $module.Path
                        }
                    }
                }

                # then import the module the function comes from and call it with the arguments
                $script = "
Import-Module '$(@($in.Module | importPaths) -join "','")'
$($in.Name) @args
"

            } elseif (
                $in -is [Management.Automation.FunctionInfo] -or
                $in -is [Management.Automation.FilterInfo]
            ) {
                # If the function did not come from a module, wrap the command
                $script = "function $($in.Name) {
$Script
}
$($in.Name) @args
"

            } elseif ($in -is [Management.Automation.CommandInfo]) {
                $script = "$($in.Name) @args"
            }
        }

        # Shebangs are actually pretty simple.
        # The first line tells which interpreter should be used.
        $Interpreter = $Interpreter -replace '^\#\!'
        $newShebang = "#!$Interpreter" + [Environment]::NewLine + $Script # After the, the script is placed inline.
        #region Generate the Shebang Content
        #region Create the Shebang
        if ($WhatIfPreference) { # If -WhatIf was passed
            return $newShebang   # return the Shebang script.
        }

        if (-not $outputPath) { return $newShebang }
        
        Set-Content -Path $OutputPath -Value $newShebang # Otherwise, Set-Content
        
        # Last but not least, we need to make this file executable.
        # Assuming we're on linux, we'll have the command chmod to do this for us.
        $chmod = $ExecutionContext.SessionState.InvokeCommand.GetCommand('chmod','Application')
        if (-not $chmod) { return } # If we couldn't find chmod, return.
        & $chmod +x $OutputPath     # If we could, set the file to be executable with +x.
        #endregion Create the Shebang
    }
}