3

I would like to create a Windows Shortcut (.lnk) to the desktop and the startmenu in Golang.

I actually got the Desktop & Startmenu folders via the gowin module and I would like to create a shortcut to thoses locations.

I searched but I did not find any golang project for it. Should I create it ? Is there an other pretty method ?

Alexis Paques
  • 1,885
  • 15
  • 29
  • I don't know windows and don't have access to it to test. However, AFAIU lnk is the equivalent of a symlink on *nix. So I would try [os.Symlink](https://golang.org/pkg/os/#Symlink) as I would expect that to work cross platform unless otherwise documented. _(I may be conflating symlinks and shortcuts)_. – reedobrien Sep 07 '15 at 15:35

5 Answers5

9

Using https://github.com/go-ole/go-ole:

func makeLink(src, dst string) error {
    ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_SPEED_OVER_MEMORY)
    oleShellObject, err := oleutil.CreateObject("WScript.Shell")
    if err != nil {
        return err
    }
    defer oleShellObject.Release()
    wshell, err := oleShellObject.QueryInterface(ole.IID_IDispatch)
    if err != nil {
        return err
    }
    defer wshell.Release()
    cs, err := oleutil.CallMethod(wshell, "CreateShortcut", dst)
    if err != nil {
        return err
    }
    idispatch := cs.ToIDispatch()
    oleutil.PutProperty(idispatch, "TargetPath", src)
    oleutil.CallMethod(idispatch, "Save")
    return nil
}
tmm1
  • 2,025
  • 1
  • 20
  • 35
  • One year later, I finally have a true solution! Thanks :D – Alexis Paques Jan 27 '17 at 08:03
  • 2
    After running this in production for many months, I've noticed some rare failures with the message "CoInitialize has not been called.". To fix this you need to ensure that runtime.LockOSThread() is called, or use the comshim library as outlined in https://github.com/go-ole/go-ole/issues/124 – tmm1 Oct 24 '17 at 20:12
  • Adding here that `dst` and `src` should be of type `string`. This is automatically made into an appropriate UTF16 encoding under the hood as needed. – Zyl Oct 26 '20 at 13:39
2

Solution via external program from this subject:

Shortcut executable from NirSoft

shortcut "f:\winnt\system32\calc.exe" "~$folder.desktop$" "Windows Calculator" 
shortcut "f:\winnt\system32\calc.exe" "~$folder.programs$\Calculators" "Windows Calculator" 
shortcut "f:\Program Files\KaZaA\Kazaa.exe" "c:\temp\MyShortcuts" "Kazaa" 
shortcut "f:\Program Files" "c:\temp\MyShortcuts" "Program Files Folder" "" "f:\winnt\system32\shell32.dll" 45 
shortcut "f:\Program Files" "c:\temp\MyShortcuts" "Program Files Folder" "" "" "" "max"

Shortcut executable from Optimumx

Shortcut.exe /f:"%USERPROFILE%\Desktop\sc.lnk" /a:c  /t:%USERPROFILE%\Desktop\scrum.pdf

.vbs

Set oWS = WScript.CreateObject("WScript.Shell")
sLinkFile = "C:\MyShortcut.LNK"
Set oLink = oWS.CreateShortcut(sLinkFile)
    oLink.TargetPath = "C:\Program Files\MyApp\MyProgram.EXE"
 '  oLink.Arguments = ""
 '  oLink.Description = "MyProgram"   
 '  oLink.HotKey = "ALT+CTRL+F"
 '  oLink.IconLocation = "C:\Program Files\MyApp\MyProgram.EXE, 2"
 '  oLink.WindowStyle = "1"   
 '  oLink.WorkingDirectory = "C:\Program Files\MyApp"
oLink.Save

Powershell script

set TARGET='D:\Temp'
set SHORTCUT='C:\Temp\test.lnk'
set PWS=powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile

%PWS% -Command "$ws = New-Object -ComObject WScript.Shell; $s = $ws.CreateShortcut(%SHORTCUT%); $S.TargetPath = %TARGET%; $S.Save()"
Community
  • 1
  • 1
Alexis Paques
  • 1,885
  • 15
  • 29
2

The AWFUL Working golang solution using VBS;

package main

import(
    "bytes"
    "fmt"
    "io/ioutil"
    "os"
    "os/exec"
)

func createShortcut(linkName string, target string, arguments string, directory string, description string, destination string) {
    var scriptTxt bytes.Buffer
    scriptTxt.WriteString("option explicit\n\n")
    scriptTxt.WriteString("sub CreateShortCut()\n")
    scriptTxt.WriteString("dim objShell, strDesktopPath, objLink\n")
    scriptTxt.WriteString("set objShell = CreateObject(\"WScript.Shell\")\n")
    scriptTxt.WriteString("strDesktopPath = objShell.SpecialFolders(\"")
    scriptTxt.WriteString(destination)
    scriptTxt.WriteString("\")\n")
    scriptTxt.WriteString("set objLink = objShell.CreateShortcut(strDesktopPath & \"\\")
    scriptTxt.WriteString(linkName)
    scriptTxt.WriteString(".lnk\")\n")
    scriptTxt.WriteString("objLink.Arguments = \"")
    scriptTxt.WriteString(arguments)
    scriptTxt.WriteString("\"\n")
    scriptTxt.WriteString("objLink.Description = \"")
    scriptTxt.WriteString(description)
    scriptTxt.WriteString("\"\n")
    scriptTxt.WriteString("objLink.TargetPath = \"")
    scriptTxt.WriteString(target)
    scriptTxt.WriteString("\"\n")
    scriptTxt.WriteString("objLink.WindowStyle = 1\n")
    scriptTxt.WriteString("objLink.WorkingDirectory = \"")
    scriptTxt.WriteString(directory)
    scriptTxt.WriteString("\"\n")
    scriptTxt.WriteString("objLink.Save\nend sub\n\n")
    scriptTxt.WriteString("call CreateShortCut()")
    fmt.Print(scriptTxt.String())

    filename := fmt.Sprintf("lnkTo%s.vbs", destination)
    ioutil.WriteFile(filename, scriptTxt.Bytes(), 0777)
    cmd := exec.Command("wscript", filename)
    err := cmd.Run()
    if err != nil {
        fmt.Println(err)
    }
    cmd.Wait()
    os.Remove(filename)
    return
}
Alexis Paques
  • 1,885
  • 15
  • 29
  • 1
    You could use back-quoted Go string literals to avoid needing to escape quotes (\"). You can also use those to make readable multi-line string literals to decrease the number of `WriteString` calls you make. You could also use something like `text/template` with the template in a single string literal to make it much more readable (and still easily put the contents of your variables into the script). – Dave C Sep 09 '15 at 02:52
  • What about the performances ? I used the concatenation (+=) with a simple string, it was readable then I read this : [Stackoverflow](http://stackoverflow.com/a/23857998/3540247) and it was 4 thousand times faster with the buffer (copy is 3 times faster than the buffer but less readable I think) – Alexis Paques Sep 09 '15 at 09:38
  • After looking at text/template, it is exactly what I need ! :o – Alexis Paques Sep 09 '15 at 09:54
1

No, there isn't any pretty method for creating .lnk file, in golang.

Primary reason is that, .lnk files are windows specific.

In Windows, even a native program need to use OLE (Object linking and embedding) and COM (component object model) to create a shortcut file, as described in this answer.

In my opinion, One way to approach this problem in golang is to use gowin, and try to communicate with OLE COM.

OR

Write a native windows component that does actual work of creating .lnk file, and just spawn its process through your go program.

Parth Desai
  • 1,671
  • 13
  • 15
1

Here is an alternative, if for any reason you don't want to use an external go package. As mentioned by Alexis Paques you can use Powershell to create shortcuts under Windows. The advantage is, that it's already available in virtually all Windows environments. Here is an implementation for creating a shortcut in the shell:startup folder, which will auto start the linked program for the current user on startup:

package main

import (
    "bytes"
    "log"
    "os/exec"
    "strings"
)

type PowerShell struct {
    powerShell string
}

var WIN_CREATE_SHORTCUT = `$WshShell = New-Object -comObject WScript.Shell
$Shortcut = $WshShell.CreateShortcut("$HOME\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\MyAPP.lnk")
$Shortcut.TargetPath = "PLACEHOLDER"
$Shortcut.Save()`

// New create new session
func New() *PowerShell {
    ps, _ := exec.LookPath("powershell.exe")
    return &PowerShell{
        powerShell: ps,
    }
}

func (p *PowerShell) execute(args ...string) (stdOut string, stdErr string, err error) {
    args = append([]string{"-NoProfile", "-NonInteractive"}, args...)
    cmd := exec.Command(p.powerShell, args...)

    var stdout bytes.Buffer
    var stderr bytes.Buffer
    cmd.Stdout = &stdout
    cmd.Stderr = &stderr

    err = cmd.Run()
    stdOut, stdErr = stdout.String(), stderr.String()
    return
}

// enableAutostartWin creates a shortcut to MyAPP in the shell:startup folder
func enableAutostartWin() {
    ps := New()
    exec_path := "C:\\MyAPP.exe"
    WIN_CREATE_SHORTCUT = strings.Replace(WIN_CREATE_SHORTCUT, "PLACEHOLDER", exec_path, 1)
    stdOut, stdErr, err := ps.execute(WIN_CREATE_SHORTCUT)
    log.Printf("CreateShortcut:\nStdOut : '%s'\nStdErr: '%s'\nErr: %s",
        strings.TrimSpace(stdOut), stdErr, err)
}

This answer is based on this SO answer and this gist.

mc51
  • 1,883
  • 14
  • 28