Pleasant94
Pleasant94

Reputation: 511

Copy directories that have more than 2 files in them

I have a lot of directories; a lot of them have just 1 file or 2 in them.

I want to copy the directories which have more than 2 files in them in another directory, how can I check it and move the directories?

I have come so far with the script but I'm not sure of it.

#!/bin/bash

mkdir "subset"
for dir in *; do
        #if the file is a directory
        if [ -d "$dir" ]; then
            #count number of files
            count=$(find "$dir" -type f | wc -l)
            #if count>=2 then move
            if [ "$count" -ge 2 ]; then
                #move dir
                #   ...
            fi
        fi
done

The command mv $dir .. moves the directory up of one, but is it possible to move up of one and down in subset without using complete path mv $dir complete_path/subset?

Upvotes: 1

Views: 97

Answers (3)

pjh
pjh

Reputation: 8174

There are many traps and pitfalls if you want to handle arbitrary directory names and contents. This Shellcheck-clean code tries to avoid all of them:

#! /bin/bash -p

shopt -s nullglob   # glob patterns that match nothing expand to nothing
shopt -s dotglob    # glob patterns expand names that start with '.'

destdir='subset'

[[ -d $destdir ]] || mkdir -- "$destdir"

for dir in * ; do
    [[ -L $dir ]] && continue               # Skip symbolic links
    [[ -d $dir ]] || continue               # Skip non-directories
    [[ $dir -ef $destdir ]] && continue     # Skip the destination dir.

    numfiles=$(find "./$dir//." -type f -print | grep -c //)
    (( numfiles > 2 )) && mv -v -- "$dir" "$destdir"
done
  • shopt -s nullglob means that the code will work if run in an empty directory. (Otherwise it will try to process a spurious directory entry called '*'.)
  • shopt -s dotglob enables the code to handle directories whose names begin with '.' (e.g. .mydir).
  • You could avoid the directory check later in the code by changing the loop guard to for dir in */ ..., but that would slightly complicate the check for symbolic links.
  • The code assumes that you don't want to move symlinks to directories that contain more than two files ([[ -L $dir ]] && continue). Remove the line if that assumption is not correct.
  • Counting the number of files under a directory is tricky because files can have newlines in their names, which means that find ... | wc -l might not work correctly. See How can I get a count of files in a directory using the command line?.
    The convoluted first argument to find ("./$dir//.") is designed to avoid several pitfalls. The quotes prevent special characters in directory names causing problems. The ./ prefix avoids the argument being treated as an option if the directory name starts with -. The //. suffix means that there will be exactly one '//' on a line for every file found by find, so grep -c // will accurately count the number of files.
  • The -- argument to the mkdir and mv commands is to ensure that they work correctly if $dir or $destdir begins with a - (which would cause them to be treated as options). See Bash Pitfalls #2 (cp $file $target).

Upvotes: 4

John Kugelman
John Kugelman

Reputation: 361889

Your general approach is fine. There's no clearly superior way to count how many files are in a directory, unfortunately.

You can get rid of the -d check by looping over */. The trailing / means only directories will match.

You could combine the assignment and test. You may like it or may find it weird looking.

You could also use ((...)) to do arithmetic. I find it a bit more readable, personally.

for dir in */; do
    if count=$(find "$dir" -type f | wc -l) && ((count >= 2)); then
        ...
    fi
done

Or you could elide the count variable entirely.

for dir in */; do
    if (($(find "$dir" -type f | wc -l) >= 2)); then
        ...
    fi
done

It's possible to do all of this in a single find invocationbut now we're definitely in arcane territory.

find . -type d \
    -exec bash -c '(($(compgen -G "$1/*" | wc -l) > 2))' bash {} \; \
    -exec mv {} subset/ \;

The command mv $dir .. moves the directory up of one, but is it possible to move up of one and down in subset without using complete path mv $dir complete_path/subset?

You haven't change directories so use whatever relative path you like. If I understand the question, it might be as simple as mv <dir> subset/.

Upvotes: 0

Pleasant94
Pleasant94

Reputation: 511

This is the best solution I've come so far.

I wanted to know if there is a more elegant solution:

#!/bin/bash

mkdir "OOOO3_LESS_THAN_TWO"

for dir in *; do
        #if the file is a directory
        if [ -d "$dir" ]; then
            #count number of files
            count=$(find "$dir" -type f | wc -l)
            #if count<=2 then move
            if [ "$count" -le 2 ]; then
                #move dir
                mv -v "$dir" /completepath/"OOOO3_LESS_THAN_TWO"
            fi
        fi
done

Upvotes: 0

Related Questions