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.
grep
command is able to do.grep "hello" *
gives you not only the filename but also the line containing "hello". Therefore you need to use an option forgrep
: seeman grep
for output control options.-t
option in themv
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 thegrep
command in a variable and then use this variable to move the files.