Ubuntu – Write a shell script which will move all files in the current directory containing the word “hello” into a separate folder called “hello-world”

bashcommand linefilesscripts

Write a shell script which will move all files in the current directory containing the word hello into a separate folder called hello-world.

The current directory contains files a b c d e and f out of which files a, c and f contain the word hello.

How can I search for the word hello in files of the current directory and move the files satisfying the condition into another directory hello-world?

Best Answer

Let's break this down into smaller tasks. We need to

  • create a directory hello-world, if it doesn't exist
  • reliably identify files that contain the text hello
  • reliably move the files to the new directory.

Making a directory is done with mkdir. It will always refuse to create a directory that exists (thereby refusing to overwrite such directories) but it also has a flag -p which, as mentioned in man mkdir

-p, --parents
       no error if existing, make parent directories as needed

doesn't bother to tell you if the directory you are trying to create already exists.

So the first command of our script could be (check you are in the right directory first).

mkdir -p hello-world

The second step is identifying the files we want. The most obvious command to search for text in files is grep. If we wanted only the filenames of the files that contain hello, we can use the -l flag, which, according to man grep

-l, --files-with-matches
       Suppress  normal output; instead print the name of each input file
       from which output would normally have been printed.  The scanning
       will stop on the first match.

suppresses the normal output. But, although we can get away with it for your example, we really don't want the filenames as output, because the output of a command is just text, and filenames might contain all sorts of characters (from the humble space to the exotic newline) that will cause the shell to see something different from the actual name of the file you want to do something to, and behave in a way you didn't want. For example, having set up a test directory for your script, if I add a file that has a space in its name, see what happens:

$ echo hello > with\ space
$ ls
a  b  c  d  e  f  with space
$ for i in $(grep -l hello *); do echo "$i"; done
a
c
f
with
space

The file with space is treated as two separate files.

To identify files in order to have the shell do something with them, we should avoid parsing filenames. We could test each file, see whether it contains the text, and if it does, move it. To loop over files, as shown above, we can use for which has syntax like this:

for var in things; do stuff $var; done

Inside a for loop (and anywhere else too) we can use the test command, which sometimes looks like [ and is also like [[ to make conditional statements, and if you only wanted to check whether the file was empty or not, or was of a particular type, I would advise you to use the test command.

But you want to find some particular text, so we ought to call grep, but we only really want to know if looking for hello in the file succeeded, so we know that we then have to do something with the file.

To make a conditional statement based on the success of a command, we can use if. if is often used with test/[/[[, but you don't need those, because grep has another useful flag:

-q, --quiet, --silent
       Quiet; do not write anything to standard output. Exit immediately
       with zero status if any match is found, even if an error  was detected.

Zero in Bash means success. So to do what we want, we could write something like

if grep -q hello file; then mv file hello-world; fi

(the fi is part of the if command's syntax. It tells the shell you've finished your if)

I usually write shell scripts in an interactive shell and only scriptify them into a file later if at all, because I'm lazy and I mostly only write very trivial scripts. Anyway, you can write a short script as a one-line command by separating commands with ;. If something goes wrong, hit the up arrow key to edit the last command...

We don't want to run that command once for each file. That would defeat the object of writing a script to do the job and save us typing, so to loop over the files we can use for. To avoid getting an error from grep about the hello-world directory, we can add another flag to exclude it.

Here's my test command to do this job and the output I get from it in my test environment:

$ mkdir -p hello-world; for file in *; do if grep -q --exclude-dir=hello-world -- hello "$file"; then echo mv -v -- "$file" hello-world; fi; done
mv -v -- a hello-world
mv -v -- c hello-world
mv -v -- f hello-world
mv -v -- with space hello-world

This script doesn't move any files, because of echo before mv which shows what command will be run with each iteration of the loop. Remove echo after testing if the commands look right (note that you can't always reliably use echo for testing, and you might have to introduce some temporary quoting to do so, but it works fine here).

* is all non-hidden files in the current directory. It is safer to use ./* which means the same thing, but ensures all paths start with ./ (which is the address of the current working directory .), preventing filenames starting with - being interpreted as options. We have dealt with that possibility by adding -- to grep and to the mv command to indicate the end of options. -v is mv's verbose flag.

We should quote variables to suppress shell expansions. If I remove the quotes around "$file", my output is

mv -v -- a hello-world
mv -v -- c hello-world
mv -v -- f hello-world
grep: with: No such file or directory
grep: space: No such file or directory

Here's the script as a script:

#!/bin/bash

mkdir -p hello-world
for file in *; do
    if grep -q --exclude-dir=hello-world -- hello "$file"; then
        echo mv -v -- "$file" hello-world
    fi
done

Don't forget to remove echo when you're ready to move the files for real.

PS, there might well be more efficient ways to do this. My way is just an example.

Related Question