Ask Ubuntu is a question and answer site for Ubuntu users and developers. Join them; it only takes a minute:

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

I want to write a simple script to detect a file created by Windows virus. Usually it creates an .exe file, with the same name, as the directory it drops.

Here is the script. It only works, if the path name doesn't contain \n. Can someone help me fix this script, please!

if [ $# == 0 ]; then
    echo ""
    echo "==== Give me a directory to begin with! ===="
    echo ""

for f in `find $1 -name '*.exe'` ; do
    filename=`basename "$f" .exe`
    dir_name=`dirname "$f"`
    current_dir_name=`basename "$dir_name"`

    if [ $filename == $current_dir_name ]; then
        rm -f "$f"  # It can't remove files 
                    # where the path contains spaces or \n ??!!
share|improve this question
You might get a better response for this in Ubuntu Forums. – hannaman Nov 1 '10 at 10:51
Could you please post an example directory tree? It's not clear whether you have C:\dir\subdir\subsubdir.exe or C:\dir\subdir\subdir.exe or something else. – Mikel Feb 3 '11 at 3:55
up vote 5 down vote accepted

Bash has a special variable called IFS.

it's used by bash in order to know how to split a string into a list of files. (Internal Field Separator)

By default it contains space, tab and newline.

for f in `find ... will split on the chars contained in the $IFS variable. If there is a newline or space in file names returned by the find command, it will be treated as a different file name and thus the loop will be executed for each part of the name.

$ ls -l
-rw-r--r-- 1 marko marko 0 2010-11-01 12:06 my?bad.exe
-rw-r--r-- 1 marko marko 0 2010-11-01 12:06 space bad.exe
$ for f in $(find -name "*.exe"); do echo "ITERATING ($f)"; done 
ITERATING (./space)
ITERATING (bad.exe)
ITERATING (bad.exe)

We could tweak the $IFS variable, but it's tricky. Let see why:

(Test this code in scripts so that the change of the IFS variable doesn't remain in your shell, you might get confused)

$ IFS=""; for f in $(find -name "*.exe"); do echo "ITERATING ($f)"; done
ITERATING (./space bad.exe

As you can see, now bash doesn't have any rule to split the string returned by the find command, and treats the whole output as a single value.

Clearly the problem here is that if we use for f in `find.... we have no way to distinguish a newline contained in the file from a newline generated by the find command.

Now for sake of completeness I will show you how to make your approach work, but please see an easier solution at the end of the answer.

We could use other delimiter characters and fix your current issue:

$ for f in $(find -name "*.exe" -exec echo -n {}: \;); do echo "ITERATING ($f)"; done;
ITERATING (./space bad.exe)

Here the find -name "*.exe" -exec echo -n {}: \; command returns the list of files separated by ":" (which are not legal in windows file names).

Now the next step would be to actually delete those files. Take in consideration that the last file is terminated by ":" and thus bash iterates one last time with an empty string.

IFS=":"; for f in $(find -name "*.exe" -exec echo -n {}: \;); do if [ ! -z "$f" ]; then rm "$f"; fi; done;

You should be able to perform also your basename/dirname test within this loop, as you did before.

However, this solution is ugly and works only because we have a character which we can rely on.

It turns out that the find command itself can execute commands for each file it finds.

find -name "*.exe" -exec ./ {} \;

Then wil lget the full name in $1 and you can delete with rm "$1" even if it contains newlines and spaces.

The find command can also be used to perform inline the basename/dirname test you do inside your script, but it's more advanced topic.

share|improve this answer
the last example is much cleaner, except that I need to create two separate script to do the trick, how about bash function? – c.sokun Nov 1 '10 at 13:28
no bash functions cannot be invoked from find -exec but you perhaps you can try using multiple exec clauses, one which performs a test with the 'test' command and the other connected with an <code>-a</code> 'and' boolean operator. – ithkuil Nov 1 '10 at 16:04
@ithkuil: In addition to emptying IFS, you'd need to use set -f to disable globbing (normally, Windows filenames don't contain globbing characters, but they're not supposed to contain newlines either, so you can't rely on this here). @c.sokun: The separate script is by far the easiest approach here; you could use sh -c '…', but that requires that you be very confident with shell quoting (and if you were you wouldn't need to ask). – Gilles Nov 1 '10 at 19:25
for f in $(find...); is bad practice. See pitfall 1 at for an explanation of why, and too see how to do it the proper way. – geirha Feb 3 '11 at 2:42
You're forgetting about bash arrays. exes=( *.exe ) will do the right thing, provided you expand it using "${exes[@]}". – Mikel Feb 3 '11 at 4:08

You could actually use ( *.exe ) to solve this problem in bash.

Putting parenthesis like this ( and ) around a pattern means that it will be expanded and stored in an array.

Then you can iterate over each item in the array using something like this:

exes=( *.exe )
for exe in "${exes[@]}"; do
    echo "'$exe'"

Here's a full example:

$ mkdir 'dir
> with
> spaces'
$ touch 'dir
> with
> spaces/dir
> with
> spaces'
$ touch 'dir
> with
> spaces/other
> file'
$ find .
$ cd 'dir
> with
> spaces'
$ files=( * )
$ for file in "${files[@]}"; do
> echo "'$file'"
> done

I can only provide a full solution after you clarify what your directory structure looks like, because I am not sure if current_dir_name is doing what you think it is.

share|improve this answer

This will handle filenames containing any character allowed in a filename, including newlines.


if (($# != 1)); then
     echo >&2 "Usage: $0 directory"
     exit 1

while IFS= read -r -d '' file; do 
    base=${file##*/} dir=${file%/*/*}
    if [[ $dir/${base%.exe}/$base -ef $file ]]; then
        echo rm "$file"
done < <(find "$1" -type f -name "*.exe" -print0)

See and for more elaboration.

share|improve this answer
I'm not sure that works. Your code suggests IFS= and -print0 will work together, but they don't seem to when I run find . -print0 | while IFS= read -r file; do echo "'$file'"; done. – Mikel Feb 3 '11 at 4:16
You are right, I failed at copy pasting it. The read was missing the -d '' to tell it that lines end with \0 instead of \n. Fixed in the answer now. – geirha Feb 3 '11 at 10:22
you should have -d $'\0' – glenn jackman Mar 26 '11 at 23:31
@glenn jackman $'\0' expands to the exact same thing as ''. Bash is written in C, and internally, the variables are stored as C-strings (char[]). C-strings are terminated by the NUL-byte (\0). read's -d option uses the first character of the following argument, which for an empty string will then be the NUL-byte. – geirha Mar 27 '11 at 0:54

Your Answer


By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.