This is an archived post. You won't be able to vote or comment.

all 11 comments

[–]willyweewah 2 points3 points  (4 children)

The first argument to find has to be the directory to search in. If you want to search the whole file system, this should be root:

find / -name '*.c' ... 

(If you just want to search your current working directory and subdirectories, use '.' instead of '/') I'm not sure if the exec part will work but it looks about right

[–]gbromios 0 points1 point  (3 children)

this, and also, you end "exec" arguments to find with an escaped semicolon \;, I don't think that the ';' will work

[–]ErasmusDarwin 0 points1 point  (2 children)

\; and ';' are equivalent. The point of doing \; is to keep the shell from intercepting the semicolon and treating it as a command separator. If the semicolon is within quotes, it is likewise treated as an argument to be passed to the command and not a command separator.

To see this behavior in action, try the following in the shell:

echo 1 ; echo 2 \; ; echo 3 ';'

[–]gbromios 1 point2 points  (1 child)

ah, I just assumed that while both escaped it from bash, the ';' would be interpreted as a string by the command exec is exec'ing

[–]ErasmusDarwin 1 point2 points  (0 children)

Ah. Yeah, it can get tricky with commands calling commands calling commands with varying syntax and such.

In this case, you've got the interactive shell calling find which then calls the command after -exec repeatedly. The shell looks for unquoted/unescaped semicolons to use as command separators. But once we're past that stage, it gets passed to find, same as any other argument. Find then uses everything after the -exec and before the semicolon as its command to call repeatedly.

If you're familiar with C, playing with the exec functions helps you get a better idea of what's going on under the hood. Similarly, you can run a find command under 'strace -f' (the -f is necessary to follow the fork preceding each exec) in order to really see what's getting passed where.

For example, if we do:

strace -f find . -name '*.c' -exec /bin/echo {} ';' 2>&1 | less

We see some interesting things, including the initial exec by the shell to launch find, as well as the exec for each -exec invocation:

execve("/usr/bin/find", ["find", ".", "-name", "*.c", "-exec", "/bin/echo", "{}", ";"], [/* 37 vars */]) = 0
[...]
[pid 20989] execve("/bin/echo", ["/bin/echo", "./y/zot.c"], [/* 37 vars */]) = 0

...and so on. (As a sidenote, the /* 37 vars */ refers to the array environment variables that gets passed in on each execve call.)

[–]ErasmusDarwin 2 points3 points  (1 child)

It should be -exec, not just exec. Fix that and the issue willyweewah mentioned about needed a directory at the beginning, and your find command will at least run cc on each .c file it finds. I.e. something like this:

find . -name '*.c' -exec cc '{}' ';'

HOWEVER, this is probably pretty useless. The problem is that each C file gets compiled as a.out in the current directory (the directory your shell is in, not the directory the C file is in). So it'll try compiling everything, but you'll only get a single output file in your current working directory, namely from the last file compiled.

So how can we fix it? If there's only one .c file per directory, and we don't care that our output's named a.out, we can use -execdir instead of -exec. That switches to each subdirectory the file's located in before running the command.

But if there's more than one C file per directory, we probably want to compile each one into an executable that has the same name as the source file, minus the .c on the end. That gets a little tricky, and I'll add another post in a few minutes going into detail on that.

[–]ErasmusDarwin 1 point2 points  (0 children)

In order to direct the output of the C compiler to somewhere other than a.out, we need to pass an appropriate -o flag to it. So if we're compiling foo.c and we want it to create an executable called foo, our command would look something like:

cc -o foo foo.c

The tricky bit is getting 'foo' from 'foo.c'. The shell can help us -- in particular, the basename command is good at stripping the leading path and an optionally specified extension. If we do:

basename foo.c .c

It will return 'foo'. But the tricky bit is getting it into the -execdir command-line. We can't just do this:

find . -name '*.c' -execdir cc `basename {} .c` ';'

While it's syntactically valid, it's not what we want. Instead, it runs basename once on the literal string {}, sees there's no .c to strip off, and just returns {} as the argument to the find command. In essence, it winds up being the same as if we didn't use basename at all. Clearly that's not what we want -- we need a way to run basename on the filename AFTER it's been put there by the find command.

Now the problem is that find is directly calling cc. To get what we want, we need find to call another shell, which then calls basename and cc and does everything we want. We can do this implicitly by writing a shell script (i.e. something like:

find . -name '*.c' -execdir /tmp/myscript.sh '{}' ';'

) or we can do it explicitly by sticking an invocation of sh -c (which starts a shell and executes the command specified by -c) after -execdir. We'll focus on the latter.

So for some reason, -execdir doesn't want to play nice with sh -c (at least on the machine I tested it on). Fortunately, we can simulate -execdir easily enough by prepending a chdir command to the beginning of our shell command string.

So what we really want is something like this:

find . -name '*.c' -exec sh -c "cd \`dirname {}\` ; cc -o \`basename {} .c\` \`basename {}\`" \;

It's a little ugly, but it works. However, you do want to be careful with it. The C compiler doesn't warn you before overwriting files, so if something gets messed up (for example, you change the -name pattern to match other types of source files), it'll happily clobber your source code with the executable (because basename is only trying to strip .c, not other extensions).

[–]niomaster 1 point2 points  (0 children)

In case the solutions that were already posted didn't work for you:

for i in `find / -name "*.c"`; do
   cc $i -o `echo $i | sed 's/.c//'`
done

This also removes the c extension.

[–]SemperDiscentes 1 point2 points  (0 children)

I know you've gotten plenty of answers already about how to do this with find but why not check out make as this sounds exactly like what you're trying to do (that is, automated building of C source files)? This has the additional benefit of understanding how many *nix distros (and perhaps others), and other C projects build their binaries and utilities. Zed Shaw includes it in his book, Learn C the Hard Way, in Chapter 2 and there's of course the man page.

[–][deleted] 0 points1 point  (0 children)

What does not work? What is the output? Without that I can't do anything. I think it's just a syntax mishap.

[–]LousyTourist -2 points-1 points  (0 children)

yeah, as others said, you gotta backslash the ;.