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

4.8k views Asked by At

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.

2

There are 2 answers

0
Donal Fellows On BEST ANSWER

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.)

0
Brad Lanam On

Since you are on a old version, you have to make sure that what eval sees will be converted to a properly quoted Tcl string.

Single quotes do nothing. They are not used by exec, nor are they passed on. exec utilizes the underlying exec(3) system call, and no argument interpretation will take place unless you purposely use something like: /bin/sh -c "some-cmd some-arg" where the shell is invoked and will reinterpret the command line.

What you have to do is construct a string that eval will interpret as a quoted Tcl string. You can use "{part1 part2}" or "\"part1 part2\"" for these constructs.

First, a little test script to verify that the arguments are being passed correctly:

#!/bin/bash
for i in "$@"; do
  echo $i
done

Then the Tcl script:

#!/usr/bin/tclsh
exec ./t.sh -u andrew -fx "some_binary -c some_config.cfg" >@ stdout
eval exec ./t.sh -u andrew -fx "{some_binary -c some_config.cfg}" \
     >@ stdout
eval exec ./t.sh -u andrew -fx "\"some_binary -c some_config.cfg\"" \
     >@ stdout
# the list will be converted to a string that is already properly
# quoted for interpretation by eval.
set cmd [list ./t.sh -u andrew -fx "some_binary -c some_config.cfg"]
eval exec $cmd >@ stdout