7

Issue

  • I have been struggling to craft a Bash command that is able to recursively search a directory and then return the paths of every sub-directory (up to a certain max-depth) that contains EXCLUSIVELY hidden files and/or hidden directories.

Visual Explanation

  • Consider the following File System excerpt:
+--- Root_Dir
|   +--- Dir_A
|   |   +--- abc.txt
|   |   +--- 123.txt
|   |   +--- .hiddenfile
|   |   +--- .hidden_dir
|   |   |   +--- normal_sub_file_1.txt
|   |   |   +--- .hidden_sub_file_1.txt
|   |     
|   +--- Dir_B
|   |   +--- abc.txt
|   |   +--- .hidden_dir
|   |   |   +--- normal_sub_file_2.txt
|   |   |   +--- .hidden_sub_file_2.txt
|   |    
|   +--- Dir_C
|   |   +--- 123.txt
|   |   +--- program.c
|   |   +--- a.out
|   |   +--- .hiddenfile
|   |   
|   +--- Dir_D
|   |   +--- .hiddenfile
|   |   +--- .another_hiddenfile
|   |     
|   +--- Dir_E
|   |   +--- .hiddenfile
|   |   +--- .hidden_dir
|   |   |   +--- normal_sub_file_3.txt   # This is OK because its within a hidden directory, aka won't be checked
|   |   |   +--- .hidden_sub_file_3.txt
|   | 
|   +--- Dir_F
|   |   +--- .hidden_dir
|   |   |   +--- normal_sub_file_4.txt
|   |   |   +--- .hidden_sub_file_4.txt

Desired Output

  • The command I am looking for would output
    ./Root_Dir/Dir_D
    ./Root_Dir/Dir_E
    ./Root_Dir/Dir_F
    
    • Dir_D because it only contains hidden files.
    • Dir_E because it only contains a hidden file and a hidden directory at the level I am searching.
    • Dir_F because it only contains a hidden directory at the level I am searching.

Attempts

  • I have attempted to use the find command to get the results I am looking for but I can't seem to figure out what other command I need to pipe the output to or what other options I should be using.
  • I think the command that will work would look something like this:
    bob@linux:/$ find ./Root_Dir -mindepth 2 -maxdepth 2 -type d -name "*." -type -f -name "*." | command to see if these are the only files in that directory  
    
5
  • You will have to break this into two parts: Directories with some .files; Of these, which don't have non .files?
    – waltinator
    Jun 9, 2021 at 18:58
  • @waltinator Correct, my question is how to do that exactly...
    – BitWrecker
    Jun 9, 2021 at 19:13
  • Is a hidden directory that contains hidden directories considered to be a match? Jun 9, 2021 at 21:46
  • 1
    @JeremyBoden Yes. The search should only be concerned with a files/directories immediately within the Dir_N directory... if there was a hidden directory within Dir_N and nothing else, that would be a match. It would not matter what is contained within that hidden directory.
    – BitWrecker
    Jun 9, 2021 at 22:05
  • You could get a flat file with tree -aif -L 2 root_dir then perhaps some cunning piece of AWK could do the job??? Jun 9, 2021 at 23:35

3 Answers 3

3

I have a less than beautiful solution for this.

In script form:

#!/bin/bash

# Echo whatever is passed to fail, and then exit with a status of 1
fail() {
  echo >&2 "$@"
  exit 1
}

# If the number of arguments are less or more than what
# we expect, throw help syntax and exit.
if [ $# -gt 2 ] || [ $# -lt 2 ]
then
  fail "
$0 check for directories that only contain hidden listings
Usage: $0 <Directory to search from> <Depth to check>
  "
fi

# Assign parameters
root_dir=$1
depth=$2

# Find a list of directories that contain files OR subdirectories
# that are hidden
readarray -t dirs < <(export LC_ALL=C
  find "$root_dir" -mindepth "$depth" -maxdepth "$depth" \
    -name ".*" \( -type d -o -type f \) -execdir pwd \; | sort -u
)

# Of the dirs we found, do any of them return any listings with a
# default ls execution? If so, that directory cannot only contain
# hidden listings.
final=()
for dir in "${dirs[@]}"
do
  notExclusive="$(ls -- "$dir")"
  if [ "$notExclusive" = "" ]
  then
     final+=("$dir")
  fi
done

# The array final contains the directories who only contain hidden
# files/subdirectories.
printf '%s\n' "${final[@]}"

Basically we just find directories that contain hidden listings (at a depth of 2 as specified in question), load them into an array, and if ls with no flags returns nothing, we can conclude that only hidden listings are contained in the directory, which meets our criteria.

To explain why you only need one invocation of find per the comments on the OP. The command find has operators for your basic logic, -a the default behavior when you join two expressions logical AND, ! as in a logical NOT, and -o as in logical OR.

This approach assumes directory paths don't contain newline characters, if they do readarray will incorrectly separate each directory path.

Ugly 'one-liner':

readarray -t dirs < <(export LC_ALL=C; find ./Root_Dir -mindepth 2 -maxdepth 2 -name '.*' \( -type d -o -type f \)  -execdir pwd \; | sort -u); final=(); for dir in "${dirs[@]}"; do notExclusive="$(ls -- "$dir")"; if [ "$notExclusive" = "" ]; then final+=("$dir"); fi; done; printf '%s\n' "${final[@]}"
4
  • I think that this works, however, in practice I'm trying to run it on a path that contains a space, and it throws and error... for example "/Root Dir/Dir A/" instead of /"Root_Dir/Dir_A/" .... If you knew of a way to make it handle paths with spaces it would be perfect. Aside from that it is what I am looking for. Thank you.
    – BitWrecker
    Jun 10, 2021 at 2:26
  • @hf it works fine with spaces. Either use the edited answer (note that your edit would have failed on spaces because you were not quoting the variables, Michael corrected it) or use "Root Dir/Dir A/" or Root\ Dir/Dir\ A/.
    – terdon
    Jun 10, 2021 at 8:24
  • @hf I editted the answer to behave more like a generic command since it seems like you want it to function that way. Thanks to everyone who helped edit my sloppy syntax from yesterday!
    – ReedGhost
    Jun 10, 2021 at 18:16
  • Thank you! Setting mindepth to 0 allows to find all such folders on a drive. The output can then be piped to | xargs -I{} mv "{}" tmpdir to visually inspect and remove the folders. You may need to repeat this a few times if there are folder name clashes. You can also use xargs -I{} trash-put "{}" if you have trash CLI tools installed. Oct 29, 2021 at 12:34
3

Using find and bash, the following command finds all directories. It then uses bash to test whether any of the found directories contain any non-hidden names. It does this by expanding the * glob pattern in each directory and counting the number of names this results in. If the number is zero, the pathname of the directory is outputted. The * glob pattern does not by default expand to hidden names.

find . -type d -exec bash -O nullglob -c '
    for dirpath do
        set -- "$dirpath"/*
        [[ $# -eq 0 ]] && printf "%s\n" "$dirpath"
    done' bash {} +

I'm setting the nullglob shell option in the in-line bash -c script to have the pattern itself removed if there are no matches.

With /bin/sh, you don't have access to the nullglob shell option, so you will have to test whether the single name that the pattern expands to actually exists. If it doesn't, the directory contains only hidden names.

find . -type d -exec sh -c '
    for dirpath do
        set -- "$dirpath"/*
        [ "$#" -eq 1 ] && [ ! -e "$1" ] && printf "%s\n" "$dirpath"
    done' sh {} +

If you want to avoid finding empty directories, add ! -empty before the -exec primary of find (assuming your implementation of find supports this non-standard by common test).

1
  • I like the -exec sh -c pattern that allows for a complex "payload" to -exec inline. Previously, I'd have created a function or another script to do the complicated shell action and then invoked it with a simple -exec expression. A useful pattern to recall.
    – cycollins
    Jul 5 at 8:46
3

With zsh:

print -rC1 -- **/*(NDF^e['()(($#)) $REPLY/*(NY1)'])

To exclude directories past a certain depth, you can exclude them with the ~ exclusion operator:

set -o extendedglob
print -rC1 -- **/*~*/*/*(NDF^e['()(($#)) $REPLY/*(NY1)'])

though note that it won't stop zsh from descending into them first.

If you only want the ones at depth 2, you can just do:

print -rC1 -- */*(NDF^e['()(($#)) $REPLY/*(NY1)'])

(though beware it follows symlinks at depth 1)

  • print -rC1 print raw on 1 Column
  • **/* recursive globbing
  • (...) glob qualifiers
  • N: Nullglob: don't complain if there's no match
  • D: Dotglob: don't ignore hidden files
  • F: only Full (with at least one entry other than . and ..) directories.
  • ^: negate following qualifier
  • e['code']: evaluate code to determine whether to select the file.
  • ()(($#)) args: returns true if passed at least one arg
  • $REPLY, within the code, holds the file currently being considered.
  • $REPLY/* the non-hidden files in $REPLY
  • Y1: stop after the first found file.

You must log in to answer this question.

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