Windows shell links

Can Tcl manage Windows "shell links"? Certainly; there are at least eight ways:

      dde execute progman progman "" "\[CreateGroup(Bogus)\]"
      dde execute progman progman "" \
            "\[AddItem(notepad.exe,BogusPadLink)\]"
      dde execute progman progman "" "\[ShowGroup(Bogus,0)\]"
      dde execute progman progman "" "\[ShowGroup(Bogus,1)\]"
      dde execute progman progman "" "\[DeleteItem(BogusPadLink)\]"
      dde execute progman progman "" "\[DeleteGroup(Bogus)\]"

When I do this, it actually shows a directory explorer window thing on the screen. I don't want that, but rather, to just add a program to the Start->Programs menu... Ideas?

  • Steve Cassidy wraps this DDE approach in a progman.tcl [2 ] package designed "package to allow creation of program groups and shortcuts in the start menu on Windows 9x/NT"
  • Bill Schongar's WISE hack [3 ]
  • freeWrap includes "shell link" functionality comparable to tlink32's.
  • NT-TCL [4 ] includes a shortcut.dll that although ancient works with recent version of Tcl thanks to the stubs interface. Can't seem to find the source though.
  • Use Tcom (see below)
  • TWAPI provides the read_shortcut [5 ] and write_shortcut [6 ] commands.

A ZIP file containing a PDF with detailed information on the link file format [7 ]

Anybody knows of an extension that lets you create unicode-aware shell links ? For example, for chinese version of windows (none of the above seems to work for me). APN I presume this comment was before the TWAPI item was added in the above list. twapi::write_shortcut should have not problems with Unicode strings.


RS tried reading such links, or "shortcuts", in a simpler, and pure-Tcl way. If you look at .lnk files in hexdump, you notice that they contain the string of what they link to, among other things. So I tried just to split on NUL bytes and see whether a snippet is a valid absolute path:


 proc readlnk {lnk} {
   set res ""
   set fp [open $lnk]
   foreach snip [split [read $fp] \x00] {
        if {[regexp {[A-Z]:\\} $snip] && [file exists $snip]} {
           lappend res $snip
        }
   }
   close $fp
   join $res
 }

This is highly experimental, but it worked on the few examples I tried - please correct me if you know better! Simple links to directories and files work. Links that contain an executable invocation with arguments and icon, are a funny mix of ASCII and Unicode, so splitting on 0 is not a good idea there, and the above code does not work.

See also Symbolic links in Windows/CE


NJG

 package require tcom
 set sh [::tcom::ref createobject "WScript.Shell"]
 set lnk [$sh CreateShortcut {D:\WORK\Acrobat.lnk}]
 $lnk TargetPath {"D:\Program Files\Adobe\Acrobat 4.0\Acrobat\Acrobat.exe"}
 $lnk WorkingDirectory {D:\WORK}
 $lnk Arguments Tutorial.pdf
 $lnk Save

EF

And to read the content of a link (code inspired by the VBScript example below):

 proc readlnk { lnk }  {
     package require tcom

     if { ![file exists $lnk] } {
         return -code error "'$lnk' is not an accessible file"
     } 
     set sh [::tcom::ref createobject "WScript.Shell"]
     set lnk [$sh CreateShortcut [file nativename $lnk]]
     return [$lnk TargetPath]
 }

BEO Using Tcom these procedures can create, modify, and get the contents of MS Shortcut files.

 package require tcom

 #
 # Create a new MS Windows shortcut file.
 #
 # Requires MS Windows 2000 or later (shell v5.00+)
 #
 # Args: file = Name of shortcut file to create.
 #      other args are:
 #      Arguments "args.."      = any cmd lines options
 #      Description "text"      = Description of Shortcut
 #      Hotkey "sequence"       = Hot key sequence. Must start with CTRL+ALT. Ex. CTRL+ALT+SHIFT+X
 #      IconLocation "filename,number" = Path to icon file and icon #
 #      TargetPath filename     = Destination of the shortcut
 #      WindowStyle style       = number (1=normal, 3=maximized, 7=minimized)
 #      WorkingDirectory dirname = pathname of working directory
 # Returns: Boolean result
 #
 # Example: create_shortcut wish.lnk Description "Tcl/Tk" WindowStyle 1 \
 #              TargetPath {C:\Program Files\TCL\bin\wish.exe}
 #
 proc create_shortcut {file args} {
    if {![string match ".lnk" [string tolower [file extension $file]]]} {
      append file ".lnk"
    }

    if {[string match "windows" $::tcl_platform(platform)]} {
    # Make sure filenames are in nativename format.
      array set opts $args
      foreach item [list IconLocation Path WorkingDirectory] {
        if {[info exists opts($item)]} {
          set opts($item) [file nativename $opts($item)]
        }
      }

      set oShell [tcom::ref createobject "WScript.Shell"]
      set oShellLink [$oShell CreateShortcut [file nativename $file]]
      foreach {opt val} [array get opts] {
        if {[catch {$oShellLink $opt $val} result]} {
          return -code error "Invalid shortcut option $opt or value $value: $result"
        }
      }
      $oShellLink Save
      return 1
    }
    return 0
 }

 #
 # Modify a MS Windows shortcut file.
 #
 # Requires MS Windows 2000 or later (shell v5.00+)
 #
 # Args: file = Shortcut filename.
 #      other args are:
 #      Arguments = any cmd lines options
 #      Description = Description of Shortcut
 #      Hotkey = Hot key sequence. Must start with CTRL+ALT. Ex. CTRL+ALT+SHIFT+X
 #      IconLocation = "pathname","icon #"
 #      Path = destination of the shortcut
 #      WindowStyle = number (1=normal, 3=maximized, 7=minimized)
 #      WorkingDirectory = pathname of working directory
 # Returns: Boolean result
 #
 # See: http://www.microsoft.com/technet/scriptcenter/resources/qanda/feb05/hey0209.mspx
 # http://www.microsoft.com/technet/scriptcenter/guide/sas_wsh_aytf.mspx
 # http://www.microsoft.com/technet/scriptcenter/resources/qanda/aug05/hey0812.mspx
 #
 proc modify_shortcut {file args} {
    set dir [file nativename [file dirname $file]]
    set tail [file nativename [file tail $file]]

    if {![string match ".lnk" [string tolower [file extension $file]]]} {
      return -code error "$file is not a valid shortcut name"
    }

    if {[string match "windows" $::tcl_platform(platform)]} {
      # Make sure filenames are in nativename format.
      array set opts $args
      foreach item [list IconLocation Path WorkingDirectory] {
        if {[info exists opts($item)]} {
          set opts($item) [file nativename $opts($item)]
        }
      }

      # Get Shortcut file as an object
      set oShell [tcom::ref createobject "Shell.Application"]
      set oFolder [$oShell NameSpace $dir]
      set oFolderItem [$oFolder ParseName $tail]
      # If its a shortcut, do modify
      if {[$oFolderItem IsLink]} {
        set oShellLink [$oFolderItem GetLink]
        foreach {opt val} [array get opts] {
          if {[catch {$oShellLink $opt $val} result]} {
            return -code error "Invalid shortcut option $opt or value $value: $rsult"
          }
        }
        $oShellLink Save
      }
      return 1
    }
    return 0
 }

 #
 # Get linked to file for MS Windows shortcut or file link.
 #
 # Requires MS Windows 2000 or later (shell v5.00+)
 #
 # Args: file = Shortcut filename.
 # Returns: filename shortcut links to.
 #
 proc get_shortcut_filename {file} {
    set dir [file nativename [file dirname $file]]
    set tail [file nativename [file tail $file]]

    if {![string match ".lnk" [string tolower [file extension $file]]]} {
      return -code error "$file is not a valid shortcut name"
    }

    if {[string match "windows" $::tcl_platform(platform)]} {
      # Get Shortcut file as an object
      set oShell [tcom::ref createobject "Shell.Application"]
      set oFolder [$oShell NameSpace $dir]
      set oFolderItem [$oFolder ParseName $tail]
      # If its a shortcut, do modify
      if {[$oFolderItem IsLink]} {
        set oShellLink [$oFolderItem GetLink]
        return [$oShellLink Path]
      } else {
        if {![catch {file readlink $file} new]} {
          set new
        } else {
          set file
        }
      }
    } else {
      if {![catch {file readlink $file} new]} {
        set new
      } else {
        set file
      }
    }
 }

 #
 # Get properties of a MS Windows shortcut file.
 #
 # Requires MS Windows 2000 or later (shell v5.00+)
 #
 # Args: file = Shortcut filename.
 # Returns: list of option and value pairs.
 #
 # Return Example: Path {C:\Program Files\TCL\bin\test.file} Hotkey 0 
 #      Description {Shortcut to test.file} WorkingDirectory {} 
 #      Arguments {} ShowCommand 1 Target ::tcom::handle0x00C2D938
 #
 proc get_shortcut {file} {
    set dir [file nativename [file dirname $file]]
    set tail [file nativename [file tail $file]]

    if {![string match ".lnk" [string tolower [file extension $file]]]} {
      return -code error "$file is not a valid shortcut name"
    }

    if {[string match "windows" $::tcl_platform(platform)]} {
      # Get Shortcut file as an object
      set oShell [tcom::ref createobject "Shell.Application"]
      set oFolder [$oShell NameSpace $dir]
      set oFolderItem [$oFolder ParseName $tail]
      # If its a shortcut, get linked to file
      if {[$oFolderItem IsLink]} {
        set oShellLink [$oFolderItem GetLink]
        set if [tcom::info interface $oShellLink]
        set list [list]
        foreach entry [$if properties] {
          foreach {ptr io type name} $entry break
          if {[catch {lappend list $name [$oShellLink $name]}]} {
            lappend list $name {}
          }
        }
        set list
      }
    }
 }

AF - here is a code snippet i wrote to parse .lnk files. I lost interest when i found out how complex the format was and that i would never be able to write them.

 array set sizeof [list a 1 A 1 b 1 B 1 h 1 H 1 c 1 s 2 S 2 i 4 I 4 w 8 W 8]

 proc getdword {fh} {
    binary scan [read $fh 4] i tmp
    return $tmp
 }

 proc getword {fh} {
    binary scan [read $fh 2] s tmp
    return $tmp
 }

 proc struct {name defs} {
    global struct_sizes struct_defs
    set offset 0
    set tmp {}
    foreach {type n} $defs {
        set type [string trim $type]
        set n [string trim $n]
        if {[string match string* $type]} {
            set len [lindex [split $type {[]}] 1]
            set type string
        } else {
            if {![info exists struct_sizes($type)]} { error "unknown type" }
            set len $struct_sizes($type)
        }
        lappend tmp [list $n $offset $type $len]
        incr offset $len
    }
    set struct_defs($name) [linsert $tmp 0 $offset]
 }

 proc _islnk {fh} {
    fconfigure $fh -encoding binary -translation binary -eofchar {}
    if {[getdword $fh] != "76"} { close $fh; return -code error "not a lnk file" }
    binary scan [read $fh 16] h32 tmp
    if {$tmp != "10412000000000000c00000000000064"} { close $fh; return -code error "unrecognized GUID" }
    return $fh
 }

 proc readlnk {lnk} {
    array set array {}
    set fh [_islnk [open $lnk r]]

    set array(flags) [getdword $fh]
    set attributes [read $fh 4]
    struct $fh * array(created) w array(modified) w array(acessed) w array(size) i array(icon) i array(showwnd) i array(hotkey) i res1 i res2 i

    if {$array(flags) & 1 > 0} {
        set len [getword $fh]
        set w1 [walkStart $fh s *]
        while {![walkDone $w1]} {
            puts [walkNext $w1]
        }
        walkEnd $w1
    }

    set offset [tell $fh]
    set structlen [getdword $fh]
    if {$array(flags) & 2 > 0 && $structlen > 0} {
        struct $fh * nextstruct i flflags i lvt i lvbp i nvt i path i
        set nextstruct [expr {[tell $fh] + $structlen - $nextstruct}]
        if {($flflags & 1) > 0} {
            seek $fh [expr {$offset + $lvt}] start
            struct $fh [expr {[getdword $fh] - 4}] type i serial i noff i name a*
            seek $fh [expr {$offset + $lvbp}] start
            set base [read $fh [expr {$offset + $structlen - [tell $fh]}]]
        }
        if {($flflags & 2) > 0} {
            seek $fh [expr {$offset + $nvt}] start
            struct $fh [expr {[getdword $fh] - 4}] a i b i c i d i base a*
        }
        seek $fh [expr {$offset + $path}] start
        set path [read $fh [expr {$nextstruct - [tell $fh]}]]
        seek $fh $nextstruct start
    }
    set array(base)  [string trimright $base \x00]
    set array(final) [string trimright $path \x00]

    set f 4
    foreach name "comment relativepath workingdir commandline icon" {
        if {($array(flags) & $f) > 0} {
            set len [getword $fh]
            set array($name) [read $fh [expr {$len * 2}]]
        }
        set f [expr {$f * 2}]
    }

    return [array get array]
 }

 array set blah [readlnk $argv]
 parray blah

 if {$blah(final) == ""} {
    puts "--> $blah(base)"
 } else {
    puts "--> $blah(base)\\$blah(final)"
 }

Francois Vogel, 25/12/06:

I found the following link [8 ] detailing the format of the Windows shortcut. Not straightforward, but the code above is a good starting point candidate for parsing it.


Another simple way, if you have Cygwin (RS):

 exec ln -s /path/to/this/file that

LV What does it take to create Start menu entries from a Tcl script? What about putting a specific icon on the Windows desktop that will then launch a specific program?

MG Nov 21 2005 - On Win XP, you can get all the info you need from the $env var:

  set base $env(USERPROFILE) ;# this user only
  set base $env(ALLUSERSPROFILE) ;# for all users
  # Create a folder in Start->Programs to put your shortcuts/files in..
  set start_menu [file mkdir [file join $base "Start Menu" "Programs" "MyApp"]]
  # Get the path of the Desktop to put your files in/on...
  set desktop [file join $base Desktop]

davidw - as far as I can tell the above will only work on English windows installations.


Peter Newman 24 November 2005: The easiest way to read/write *.LNK/*.URL files has to be to use Windows Script Host. As I understand it, this comes bundled with Windows 98 to XP. For other versions of Windows (or to upgrade to the latest version of WSH), you can download the latest version (5.6) (from Microsoft, for free).

Basically, WSH is two EXE files:- wscript.exe, which you call from GUI programs, and:- cscript.exe, which you run from the command line. Both run either VBScript or JScript script files. And both VBScript and JScript support a CreateShortcut method/function, which either creates a new, or opens an existing, *.LNK/*.URL file. You can then read or write the following properties:-

 Arguments            http://msdn.microsoft.com/library/en-us/script56/html/wsproarguments.asp
 Description          http://msdn.microsoft.com/library/en-us/script56/html/wsprodescription.asp
 FullName             http://msdn.microsoft.com/library/en-us/script56/html/wslrffullnamepropertywshshortcutobject.asp
 Hotkey               http://msdn.microsoft.com/library/en-us/script56/html/wsprohotkey.asp
 IconLocation         http://msdn.microsoft.com/library/en-us/script56/html/wsproiconlocation.asp
 RelativePath         http://msdn.microsoft.com/library/en-us/script56/html/wslrfrelativepathproperty.asp
 TargetPath           http://msdn.microsoft.com/library/en-us/script56/html/wsprotargetpath.asp
 WindowStyle          http://msdn.microsoft.com/library/en-us/script56/html/wsprowindowstyle.asp
 WorkingDirectory     http://msdn.microsoft.com/library/en-us/script56/html/wsproworkingdirectory.asp

The following is a VBScript that reads just the TargetPath property, storing it in the one-line text file whose filespec you pass to the script in an environment variable.

 ' ------------------------------------------------------------------------------
 ' GetShortcutTarget.VBS
 ' VBScript to extract info from a Windows *.LNK (= 'shortcut') file.
 ' ------------------------------------------------------------------------------

 ' ------------------------------------------------------------------------------
 ' OVERVIEW!
 ' ---------
 ' This routine:-
 '
 ' 1. Opens the Windows shortcut file specified by the:-
 '                GetShortcutTarget_LinkFileFilespec
 '    environment variable, and then;
 '
 ' 2. Writes the required shortcut details to the text file specified by the:-
 '                GetShortcutTarget_DataFileFilespec
 '           environment variable.
 '
 ' The Link file info. that can be obtained is:-
 '                Arguments                         Property
 '                Description                 Property
 '                FullName                         Property
 '                Hotkey                         Property
 '                IconLocation                 Property
 '                RelativePath                 Property
 '                TargetPath                         Property
 '                WindowStyle                  Property
 '                WorkingDirectory               Property
 ' ------------------------------------------------------------------------------

 Dim WSHShell
 Set WSHShell = WScript.CreateObject("WScript.Shell")
 Set WshSysEnv = WshShell.Environment("PROCESS")

 'WScript.Echo "LINK FILE: " & WshSysEnv("GetShortcutTarget_LinkFileFilespec")
 'WScript.Echo "DATA FILE: " & WshSysEnv("GetShortcutTarget_DataFileFilespec")

 Set myShortcut = WSHShell.CreateShortcut(WshSysEnv("GetShortcutTarget_LinkFileFilespec"))

 'MyShortcut.TargetPath = WSHShell.ExpandEnvironmentStrings("%windir%\notepad.exe")
 'MyShortcut.WorkingDirectory = WSHShell.ExpandEnvironmentStrings("%windir%")
 'MyShortcut.WindowStyle = 4
 'MyShortcut.IconLocation = WSHShell.ExpandEnvironmentStrings("%windir%\notepad.exe, 0")
 'MyShortcut.Save              (This is an example of CREATING an *.LNK file)

 Const ForReading = 1, ForWriting = 2

 Dim myFileSystemObject, myFileObject
 Set myFileSystemObject = CreateObject("Scripting.FileSystemObject")
 Set myFileObject = myFileSystemObject.OpenTextFile(WshSysEnv("GetShortcutTarget_DataFileFilespec"), ForWriting,  True)
 myFileObject.WriteLine myShortcut.TargetPath
 myFileObject.Close 

I run that script from VB .NET. But, from Tcl you'd go something like (I haven't tested it):-

 set env("GetShortcutTarget_LinkFileFilespec") yourLinkFile
 set env("GetShortcutTarget_DataFileFilespec") yourDataFile  # (will be created/overwritten)

 exec wscript.exe GetShortcutTarget.VBS                      # if you're using 'wish'
 # --OR--
 exec cscript.exe GetShortcutTarget.VBS                      # if you're using 'tclsh'

 # ...some Tcl to read the first line of the output DataFile (which gives you the required TargetPath)...

You can easily modify the above to get the other properties too. Note that you don't have to worry about the paths to wscript.exe and cscript.exe. Windows figures this out itself.

Creating the Link file is just as easy. See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/script56/html/wsmthcreateshortcut.asp (If that link dies, search for "CreateShortcut" on MSDN.)

See also http://msdn.microsoft.com/library/default.asp?url=/library/en-us/script56/html/vtoriMicrosoftWindowsScriptTechnologies.asp (which seems to be the MSDN home page for WSH/VBScript/JScript info).

Obviously, you can do the above from any language (not just Tcl). You can also; call COM objects, manipulate network drives and printers, do Office/Word/Excel automation, and quite a lot of other useful things from WSH/VBscript/JScript. And it's quite lightweight and fast too. I doubt you'd chuck Tcl for VBScript or JScript. But, for some Windows specific things (like those listed above), it's usually the easiest and most powerful solution. (And at least you're still scripting.)


ASH

The example above is very good. Based on this, if you want a program just to echo the target path of the lnk file, then you can do this:

Dim WSHShell
 Set WSHShell = WScript.CreateObject("WScript.Shell")
 Set WshSysEnv = WshShell.Environment("PROCESS")
 Set myShortcut = WSHShell.CreateShortcut(Wscript.Arguments.Item(0))
 WScript.Echo myShortcut.TargetPath

put this in a .vbs file, let's say lnk2path.vbs, and then run:

cscript //nologo lnk2path.vbs xxx.lnk

with xxx.lnk replaced by your actual link and it will output the target path.


Suppose you have to programmatically launch some application using it's shell link, exec obviously doesn't work. Here're the ways to do this:

 # Doesn't respect "Run" settings of the shortcut (window state - Normal/Maximized/Minnimized).
 exec rundll32 shell32.dll,ShellExec_RunDLL $link

 # Slightly complicated, requires tcom package, but respects "Run" settings.
 package require tcom
 set shell [ ::tcom::ref createobject WScript.Shell ]
 $shell Run $link

Related information appears in "Windows specific Tcl commands". Microsoft Windows and Tcl - Windows shortcut