4

How do I name one random file in the deepest level of a directory tree using basic Bash commands and script?

I was searching for a way to do it as an one-liner and without functions. Assuming also that I'm starting in the current directory and then work towards the depth.

find, grep, sed and awk are acceptable commands.

My current attempt looks like this and does not work:

find -type d | declare COUNT=-1; declare P=""; while read LINE ; do echo $LINE; declare C; C=$(echo $LINE | cut -c3- | sed "s/\//\n\//g" | grep '/' -c); echo $C; if [ $COUNT -gt $C ]; then let COUNT=$C; let P=$LINE; echo "Done"; fi; done

This would only find the directory.

How could this be solved in the most simple way?

migrated from stackoverflow.com Dec 17 '11 at 3:33

This question came from our site for professional and enthusiast programmers.

  • 4
    You call that a one-liner?? – Stéphane Gimenez Dec 17 '11 at 17:58
  • 1
    Do not cross-post. This is a duplicate of How do I get this Bash-script to work? – Stéphane Gimenez Dec 17 '11 at 20:01
  • Why do so many people try to do so much on one line? I don't get it! – Johnsyweb Dec 17 '11 at 20:07
  • Why should they be one-liners? As you've seen , when a one-liner goes wrong, it's very hard to debug! Open an editor and write a bash script! – Johnsyweb Dec 17 '11 at 20:09
  • 1
    Please don't ask the same question multiple times. – Paul Tomblin Dec 17 '11 at 23:36
10

That's an odd request!

I'd use find + awk to grab a file in the deepest directory:

bash-3.2$ deepest=$(find / -type f | awk -F'/' 'NF > depth {
>     depth = NF;
>     deepest = $0;
> }
>
> END {
>     print deepest;
> }')

Using ${deepest} in your mv command is left as an exercise but the following five lines may help you further:

bash-3.2$ echo "${deepest}"
/Developer/SDKs/MacOSX10.6.sdk/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/gems/1.8/gems/activesupport-2.3.5/lib/active_support/vendor/tzinfo-0.3.12/tzinfo/definitions/America/Argentina/Buenos_Aires.rb

bash-3.2$ echo "${deepest%.*}"
/Developer/SDKs/MacOSX10.6.sdk/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/gems/1.8/gems/activesupport-2.3.5/lib/active_support/vendor/tzinfo-0.3.12/tzinfo/definitions/America/Argentina/Buenos_Aires

bash-3.2$ echo "${deepest%/*}"
/Developer/SDKs/MacOSX10.6.sdk/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/gems/1.8/gems/activesupport-2.3.5/lib/active_support/vendor/tzinfo-0.3.12/tzinfo/definitions/America/Argentina

bash-3.2$ echo "${deepest##*/}"
Buenos_Aires.rb

bash-3.2$ echo "${deepest##*.}"
rb

Following update to question:

find -type d [...] "This would only find the directory. [...] How could this be solved in the most simple way?".

By supplying -type f to find to find all files (f), not all directories (d).

  • I've updated my question and added the command-line script I wrote to attempt to do this. It's not working. Some feedback from others will be welcomed. – Robert Sundström Dec 17 '11 at 14:22
  • 1
    Your one-liner is incomprehensible to me. I'm sorry I cannot debug that for you. Does my example not do what you asked for? – Johnsyweb Dec 18 '11 at 3:14
  • Sorry. Yes, it does. I was just seeking for different ways of tackling the problem as I'm quite new to Bash and the paradigm. Your solution does it!! – Robert Sundström Dec 18 '11 at 3:18
  • I see. There are many ways to solve this. I recommend my own solution since it involves only one |, keeping subprocesses to a minimum. – Johnsyweb Dec 18 '11 at 4:24
  • 1
    N.B: If you just want to print the filename, you can omit deepest=$( [...] ) and start from find. It's not really clear what you're trying to achieve. – Johnsyweb Dec 18 '11 at 4:31
4

You are missing some curly-bracketing, you inverted the comparison, and you need to print the result:

find -type d | {
  declare COUNT=-1
  declare P=""
  while read LINE
    do echo $LINE
    declare C=$(echo $LINE | cut -c3- | sed "s/\//\n\//g" | grep '/' -c)
    echo $C
    if [ $C -gt $COUNT ]; then let COUNT=$C; let P=$LINE; echo "Done"; fi
  done
  echo deepest: "$P"
}

Slightly improved version, with debugging stuff thrown away:

find -type d -links 2 | (
  declare COUNT=-1
  P=""
  while IFS= read -r LINE; do
    declare C=$(echo $LINE | tr -cd / | wc -c)
    if [ $C -gt $COUNT ]; then let COUNT=$C; P=$LINE; fi
  done
  echo deepest: "$P"
)
  • @Stéphane, could you specify the shell you used? The question is tagged bash, but I suppose you used something else, as bash` gives this: “bash: local: can only be used in a function”. – manatwork Dec 18 '11 at 12:34
  • 1
    @manatwork: Indeed I forgot to test again with bash. I used zsh and it was accepted. The best is maybe to use a subshell to deal with those little scope issues. – Stéphane Gimenez Dec 18 '11 at 12:44
  • A faster way to obtain the value of C is C=${LINE//[^\/]/}; C=${#C}. Also, you don't need the declare keyword if you're using a subshell to make variables local. – Gilles Dec 18 '11 at 23:39
  • Clever trick. Oh, and I think the point of declare was to make those variables integer variables, so I did not changed it. – Stéphane Gimenez Dec 20 '11 at 11:18
3
find -type f | awk -F/ 'NF > maxdepth { maxdepth = NF; file = $0 }; END {print file}'

And it seems this is essentially the same as this answer on the other question you posted, what's wrong with that/this one?

1

I would go with these, although they will fail on file names containing leading spaces. The first outputs just the file name, the second includes the path to that file too:

find -type f | sed 's:[^/]*/: :g' | LC_ALL=C sort | head -1 | sed 's/^ *//'

find -type f | sed 'h;s:[^/]*/: :g;G;s/\n/\t/' | LC_ALL=C sort | head -1 | sed 's/.*\t//'
1

With zsh:

f=(**/*(D.Oe:'REPLY=${REPLY//[^\/]}':[1])) && mv -- $f $f:h/new-name

We use zsh recursive globbing (**/*) and glob qualifiers (...), with D to include dot-files, . to only select regular files and Oe:'code' to define a sort order (here reversed because of the upper case O) based on the value of $REPLY after execution of the code.

In that code, the current file is passed as $REPLY. We remove everything but / characters in that $REPLY, and that's what is used for the sort order. So the file with the most / sorts last, first once reversed and we select that first one with [1].

The bash with (recent) GNU tools equivalent could be something like:

(
  export LC_ALL=C
  find . -type f -print0 |
    sed -z 'h;s:[^/]::g;G;s:[^/]::' |
    sort -rz |
    sed -z 's:/*::;q' |
    tr '\0' '\n'
)
  • I don't know zsh particularly, but I'm betting this is the only one of the many answers given so far that actually correctly handles any and all special characters in filenames, including newlines. (You should mention that in the answer if so; this deserves more attention.) – Wildcard Nov 13 '16 at 10:02
0
function step () {
    d=$1
    d=$((d+1))
    res=$(find -mindepth $d -type d ! -empty)
    test -n "$res" && step $d || echo $((d-1))
}

find -mindepth $(step 0) -type d ! -empty | head -n 1 

Recursively step deeper and deeper into directories, as long as they contain a file (! -empty). From the last step, we have to subtract 1, and we can then use the result in a second find command.

0

The following script prints out the longest paths in the file system and/or below the current directory, but instead of using find, it takes advantage of the existing locate database of (potentially very long) paths without searching/traversing (potentially very deep) directory structures. Note it does count directories to determine depth, not just the length of the path (but it could easily be modified/simplified to do either).

$ cat find-longest-path.sh
#!/bin/bash
pattern=$1
locate -r "^${PWD}/${pattern}" \
   | while read f; do printf "$(tr -cd / <<< "$f" | wc -c):$f\n"; done \
   | sort -nr -t: -k1 \
   | head -5

This prints out the deepest 5 paths (head -5) of a file name pattern (regexp) starting from pwd. Output is prefixed by the depth (i.e., number of /'s in the path). To search for just a specific filename and/or pattern, anywhere in the file system, remove $PWD and just search for locate -r /some_file$ (...etc, etc).

For example,

$ ./find-longest-path.sh 'foo.*log$'
5:/path/to/deep/path/foo-12345.log
3:/path/to/shallow/foobar.log
0

I am surprised no one mentioned using %d.

You can list all nested subfolders, and sort them by depth, using:

find . -type d -printf '%d:%p\n' | sort -t: -k1 -g -r > /tmp/foo.txt

where

  • . can be replaced with the root folder;
  • -type d finds nested subfolders (replace with -type f for files instead);
  • %d:%p prints the depth, followed by the path;
  • -t: means the separator is :;
  • -k1 means we want to sort by the first key/column;
  • -g means that we want to sort numbers;
  • -r means that we want the deepest folders to show first.

The result of this command are streamed into file /tmp/foo.txt (replace as desired, you can also use mktemp if needed).

The deepest level can then be determined automatically with:

D=$(head -n 1 /tmp/foo.txt | awk -F ":" '{print $1}')

If you want to filter only those folders or files at depth D, you can use:

awk -F ":" '{ if ($1 == D) print $2 }' /tmp/foo.txt 

Finally, to select a line randomly amongst the results, pipe with | shuf -n 1.


Putting this into a script random_deep_file <Folder> <Depth>:

Folder=$1

# Sort files by depth into temporary file
Temp=$(mktemp)
find "$Folder" -type f -printf '%d:%p\n' | sort -t: -k1 -g -r >| "$Temp"

# Determine deepest level if not specified
[ $# -lt 2 ] && { D=$(head -n 1 "$Temp" | awk -F ":" '{print $1}'); } || D=$2

# Select a random file at that depth
awk -F ":" "{ if (\$1 == $D) print \$2 }" "$Temp" | shuf -n 1

Note: If you are on OSX, you need to install (with Homebrew) and use gfind and gshuf instead.

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.