1

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?

  • 5
    That looks like a school assignment. Consider what the grep command is able to do. – muclux Jan 30 '18 at 19:41
  • grep "hello" * gives you not only the filename but also the line containing "hello". Therefore you need to use an option for grep: see man grep for output control options. – muclux Jan 30 '18 at 20:24
  • Then you have to look at how xargs builds up the command: it appends what it gets from stdin to the command. So you have to use the -t option in the mv command to define the target before the files to be moved. Instead of trying to use a one-liner you could also consider using a little script: first collect the output of the grep command in a variable and then use this variable to move the files. – muclux Jan 30 '18 at 20:29
  • 1
    @muclux are school assignments off topic then? – Thufir Feb 17 '18 at 10:44
4

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.

  • That's a totally awesome answer to a mediocre question! However, if I were you (luckily for you I'm not), I wouldn't have included the script at the end though so the pupil would still have had to figure out some things for herself, but upvoted anyway! :-) :-) :-) – Fabby Feb 13 '18 at 20:08
0

This would be a solution following your idea with grep and xargs:

#!/bin/bash
mkdir hello-world
grep -l hello * | xargs mv -t hello-world
  • That should work for OP, but will fail if filenames have spaces... – Zanna Jan 31 '18 at 22:03
  • 1
    The -Z and -0 options may help with that. – muru Feb 1 '18 at 3:37
-1

From a bash script named test.sh

#!/bin/sh
IFS=$'\n'
for i in $( grep -l hello --exclude=*.sh --exclude-dir=hello-world * ); do mv $i hello-world/; done;

From command line:

IFS=$'\n' && for i in $( grep -l hello --exclude-dir=hello-world * ); do mv $i hello-world/; done;

Hopes it helps you...

Please consider to create first directory hello-world.

  • Thanks for your feedback, I've edited my answer... – Alvaro Niño Feb 1 '18 at 9:01
  • well, that's a little better but it will still break if filenames have spaces (quoting won't fix it) – Zanna Feb 1 '18 at 9:02
  • Thanks a lot @zanna, what about now? – Alvaro Niño Feb 1 '18 at 9:17
  • I already undid my downvote, but see unix.stackexchange.com/questions/321697/… for the general problems with this kind of approach – Zanna Feb 1 '18 at 9:26
  • 1
    Thanks a lot for your help, it is more important knowledge than votes. – Alvaro Niño Feb 1 '18 at 9:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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