Search code examples
shelltclexecevalquoting

How do I get Tcl's exec to run a command whose arguments have quoted strings with spaces?


I want to use pgrep to find the pid of a process from its command-line. In the shell, this is done as so:

pgrep -u andrew -fx 'some_binary -c some_config.cfg'

But when I try this from Tcl, like this:

exec pgrep -u $user -fx $cmdLine

I get:

pgrep: invalid option -- 'c'

Which makes sense, because it's seeing this:

pgrep -u andrew -fx some_binary -c some_config.cfg

But it's the same when I add single quotes:

exec pgrep -u $user -fx '$cmdLine'

And that also makes sense, because single quotes aren't special to Tcl. I think it's consider 'some_binary an argument, then the -c, then the some_config.cfg'.

I've also tried:

exec pgrep -u $user -fx {$cmdLine}

and

set cmd "pgrep -u $user -fx '$cmdLine'"
eval exec $cmd

to no avail.

From my reading it seems the {*} feature in Tcl 8.5+ might help, but my company infrastructure runs Tcl 8.0.5.


Solution

  • The problem is partially that ' means nothing at all to Tcl, and partially that you're losing control of where the word boundaries are.


    Firstly, double check that this actually works:

    exec pgrep -u $user -fx "some_binary -c some_config.cfg"
    

    or perhaps this (Tcl uses {} like Unix shells use single quotes but with the added benefit of being nestable; that's what braces really do in Tcl):

    exec pgrep -u $user -fx {some_binary -c some_config.cfg}
    

    What ought to work is this:

    set cmdLine "some_binary -c some_config.cfg"
    exec pgrep -u $user -fx $cmdLine
    

    where you have set cmdLine to exactly the characters that you want to have in it (check by printing out if you're unsure; what matters is the value in the variable, not the quoted version that you write in your script). I'll use the set cmdLine "…" form below, but really use whatever you need for things to work.

    Now, if you are going to be passing this past eval, then you should use list to add in the extra quotes that you need to make things safe:

    set cmdLine "some_binary -c some_config.cfg"
    set cmd [list pgrep -u $user -fx $cmdLine]
    eval exec $cmd
    

    The list command produces lists, but it uses a canonical form that is also a script fragment that is guaranteed to lack “surprise” substitutions or word boundaries.


    If you were on a more recent version of Tcl (specifically 8.5 or later), you'd be able to use expansion. That's designed to specifically work very well with list, and gets rid of the need to use eval in about 99% of all cases. That'd change the:

    eval exec $cmd
    

    into:

    exec {*}$cmd
    

    The semantics are a bit different except when cmd is holding a canonical list, when they actually run the same operation. (The differences come when you deal with non-canonical lists, where eval would do all sorts of things — imagine the havoc with set cmd {ab [exit] cd}, which is a valid but non-canonical list — whereas expansion just forces things to be a list and uses the words in the list without further interpretation.)