Take the 2-minute tour ×
Ask Ubuntu is a question and answer site for Ubuntu users and developers. It's 100% free, no registration required.

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!

#!/bin/bash
if [ $# == 0 ]; then
    echo ""
    echo "==== Give me a directory to begin with! ===="
    echo ""
    exit
fi

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 ??!!
    fi
done
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
add comment

3 Answers 3

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 (./my)
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
./my
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)
ITERATING (./my
bad.exe)
ITERATING ()

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 ./yourscript.sh {} \;

Then yourscript.sh 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 mywiki.wooledge.org/BashPitfalls for an explanation of why, and mywiki.wooledge.org/BashFAQ/020 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
show 2 more comments

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'"
done

Here's a full example:

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

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
add comment

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

#!/bin/bash

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

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

See http://mywiki.wooledge.org/BashFAQ/020 and http://mywiki.wooledge.org/BashFAQ/100 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
1  
@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
add comment

Your Answer

 
discard

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.