Driving ExifTool from Tcl

From the Exiftool website[L1 ]:

ExifTool is a platform-independent Perl library plus a command-line application for reading, writing and editing meta information in a wide variety of files.

The command-line has a large variety of options to do various things to one file or many.

I (EMJ) use ExifTool a lot with my photo collection, adding metadata as well retrieving it for a variety of purposes. Doing the same thing with lots of files is easy and efficient, but doing the same sort of updates but with different values to a lot of files can be very slow, needing many invocations of the program. Howver, ExifTool provides for this (again from the website):

... the -execute option may be used to perform multiple independent operations with a single invocation of exiftool, and together with the -stay_open option provides a method for calling applications to avoid (the) startup overhead

Add this to

Read command-line arguments from the specified file. The file contains one argument per line (NOT one option per line -- some options require additional arguments, and all arguments must be placed on separate lines). Blank lines and lines beginning with # and are ignored. Normal shell processing of arguments is not performed, which among other things means that arguments should not be quoted and spaces are treated as any other character. ARGFILE may exist relative to either the current directory or the exiftool directory unless an absolute pathname is given.

(which doesn't mention -@ - for stdin) and you have something that can easily be driven from Tcl using open (as open |) and fileevent. After a fair bit of messing about, I have ended up with the following:


# start of "package"
namespace eval ::run_exiftool {
    variable et_vars

    set et_vars [dict create]
    # the caller can use this dict, we only care about the keys "chan" and
    # "func"

# called by the user to start using ExifTool
proc ::run_exiftool::start { func } {
    variable et_vars

    # remember the user-supplied function
    dict set et_vars func $func

    # start ExifTool in the right way and remember the channel for the pipe
    set et [open "|exiftool -stay_open true -@ -" r+]
    fconfigure $et -blocking false
    fileevent $et readable [list [namespace current]::isReadable]
    dict set et_vars chan $et

    # call the user-supplied function with the NAME of the dictionary and the
    # special string "INIT"
    $func [namespace current]::et_vars INIT
    # presumably the user-supplied code will feed arguments to ExifTool,
    # deal with its output and, eventually, stop it properly.

    vwait ::DONE

    close [dict get $et_vars chan]

# this is how to stop ExifTool properly
proc ::run_exiftool::stop {} {
    variable et_vars
    set et [dict get $et_vars chan]
    puts $et "-stay_open"
    puts $et "false"
    flush $et

# pretty much a standard fileevent handler
proc ::run_exiftool::isReadable {} {
    variable et_vars

    set et [dict get $et_vars chan]

    # The channel is readable; try to read it.
    set status [catch { gets $et line } result]
    if { $status != 0 } {
        # Error on the channel
        puts "error reading $et: $result"
        set ::DONE 2
    } elseif { $result >= 0 } {
        # Successfully read the channel, so tell the user (dict passed
        # by NAME)
        set usr_status [catch { [dict get $et_vars func] [namespace current]::et_vars $line } usr_result]
        if { $usr_status != 0 } {
            puts "error in user function: $usr_result"
            close $et
            set ::DONE 99
    } elseif { [eof $et] } {
       # End of file on the channel
       set ::DONE 1
    } elseif { [fblocked $et] } {
       # Read blocked.  Just return
    } else {
       # Something else
       puts "can't happen"
       set ::DONE 3

# end of "package"

Not actually a package yet, of course.

When everthing is ready, we will want to do

# ::run_exiftool::start do_one_line

so here is a suitable proc:

proc do_one_line { et_dict line } {
    # the dict is available for user data needed across calls, which is why
    # we get it by name, and pass it on by name as well
    upvar 1 $et_dict et_vars
    # the channel to ExifTool is the most important thing we get
    set et [dict get $et_vars chan]
    # process a line of ExifTool's output
    switch -regexp -- $line {
        {^INIT$} {
            # This is the special case to allow us to initialize - 
            # in this case we are driven by a file, so open it, save the
            # channel, and call our function to read from it and send something
            # to ExifTool
            dict set et_vars fp [open captionlist]
            dict set et_vars dates -
            sendnext et_vars
        {^date} {
            # this output will result from what we sent first, so remember it
            dict set et_vars dates [lindex [split $line] 1]
        {^{ready1}$} {
            # this is from -execute1, now we can use what we remembered to
            # build some updates
            updnext et_vars
        {^{ready2}$} {
            # this is from -execute2, our updates are done, so go get the data
            # for the next and send it
            sendnext et_vars
        default {
            # not from anything we did or from -execute, so just show it
            # to the user
            puts $line

To be continued ...