5

I'm trying to write a shell script that lists certain types of files in a Directory (and sub-directories). I'm struggling with the recursive part.

Here's what I have:

#!/bin/sh

#download dir
DOWNLOADING_DIR=/Users/richard/Downloads

echo "Starting Script..."

for FILE in $DOWNLOADING_DIR/*
do
    if [ -d "$FILE" ]
    then
        echo "...Checking Directory "$FILE
        for DFILE in $FILE/*
        do
            echo "Found file ... $DFILE"
        done
    else
        echo "Found file ... $FILE"
        echo ""
    fi
done

The problem is when it finds the directory, it doesn't find any names of files in the directory. It just lists the sub-directory name and not the files in the sub. It works for the files in the first directory.

I need this script to search out .txt or .doc files and move them to another directory.

1
  • I would suggest that you can probably do this with "find" and "dirname" and maybe "cut" depending on what exactly your goal is.
    – Perkins
    Jan 13, 2019 at 4:23

1 Answer 1

19

Your script is not recursive, as it does not call itself.

Here is a variation that implements something like what you have recursively:

#!/bin/bash

walk_dir () {
    shopt -s nullglob dotglob

    for pathname in "$1"/*; do
        if [ -d "$pathname" ]; then
            walk_dir "$pathname"
        else
            printf '%s\n' "$pathname"
        fi
    done
}

DOWNLOADING_DIR=/Users/richard/Downloads

walk_dir "$DOWNLOADING_DIR"

The function walk_dir takes a directory pathname as its only argument and iterates over its content. If a directory is found, it calls itself recursively to traverse that sub-directory.

Modifying this to find the files whose filename suffix is either .txt or .doc:

#!/bin/bash

walk_dir () {    
    shopt -s nullglob dotglob

    for pathname in "$1"/*; do
        if [ -d "$pathname" ]; then
            walk_dir "$pathname"
        else
            case "$pathname" in
                *.txt|*.doc)
                    printf '%s\n' "$pathname"
            esac
        fi
    done
}

DOWNLOADING_DIR=/Users/richard/Downloads

walk_dir "$DOWNLOADING_DIR"

Note that by "file" above we really mean anything that is not a directory or a symbolic link to a directory, which may not be the same as a regular file. By setting the dotglob and nullglob shell options in bash, we are able to find hidden pathnames and will not have to test specially for possibly empty directories.

A variation for /bin/sh that does not care about hidden names:

#!/bin/sh

walk_dir () {
    for pathname in "$1"/*; do
        if [ -d "$pathname" ]; then
            walk_dir "$pathname"
        elif [ -e "$pathname" ]; then
            case "$pathname" in
                *.txt|*.doc)
                    printf '%s\n' "$pathname"
            esac
        fi
    done
}

DOWNLOADING_DIR=/Users/richard/Downloads

walk_dir "$DOWNLOADING_DIR"

With the globstar and exglob shell options in bash, one could even do the following (with no recursion) to move the files:

shopt -s globstar extglob

mv "$DOWNLOADING_DIR"/**/*.@(txt|doc) "$destdir"

... unless the resulting file list turned out to be too long. The ** matches across slashes in pathnames (enabled by globstar) and *.@(txt|doc) matches any filename that ends with either .txt or .doc (enabled by extglob).


A far more efficient and portable way to find regular files with a filename suffix of either .txt or .doc in or under some top-level directory $topdir, and to move them to some other directory $destdir:

find "$topdir" -type f \( -name '*.txt' -o -name '*.doc' \) \
    -exec mv {} "$destdir" \;

With GNU mv you can make it a bit more efficient,

find "$topdir" -type f \( -name '*.txt' -o -name '*.doc' \) \
    -exec mv -t "$destdir" {} +

This variation would move batches of files instead of one file at a time. Use mv with -v to see what gets moved, or add -print before -exec to get a listing of the pathnames that mv is called with.

6
  • I think I'll go toward the efficient route as you said.. and look into find instead of the loops.
    – Borg357
    Jan 12, 2019 at 22:51
  • 'find "$DOWNLOADING_DIR" -type f ( -name '.tex' -o -name '.doc' -o -name '*.txt' ) \ -exec mv -v {} "$COMPLETED_DIR" \;' Is what I went with. It works well. @Kusalananda, But another question, if I may. How does one skip moving a file.. Like if the file contains with file name "sample", is it possible to skip moving that file to the new directory?
    – Borg357
    Jan 12, 2019 at 23:25
  • @Borg357 Just before -exec, or just after -type f, add ! -name '*sample*'. That will skip files whose filename contains the string sample.
    – Kusalananda
    Jan 12, 2019 at 23:37
  • 1
    @Borg357 Also note that the parentheses need to be escaped (they weren't in your comment).
    – Kusalananda
    Jan 12, 2019 at 23:42
  • find "$DOWNLOADING_DIR" -type f \( -name '*.tct' -o -name '*.txt' -o -name '*.doc' \) \ ! -name '*sample*' -exec mv -v {} "$COMPLETED_DIR" \; Thanks
    – Borg357
    Jan 12, 2019 at 23:50

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .