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?

6
  • 4
    You call that a one-liner?? Dec 17, 2011 at 17:58
  • 1
    Do not cross-post. This is a duplicate of How do I get this Bash-script to work? Dec 17, 2011 at 20:01
  • Why do so many people try to do so much on one line? I don't get it!
    – johnsyweb
    Dec 17, 2011 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, 2011 at 20:09
  • 1
    Please don't ask the same question multiple times. Dec 17, 2011 at 23:36

8 Answers 8

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).

5
  • 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. Dec 17, 2011 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, 2011 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!! Dec 18, 2011 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, 2011 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, 2011 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"
)
4
  • @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, 2011 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. Dec 18, 2011 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. Dec 18, 2011 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. Dec 20, 2011 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?

0
2

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'
)
1
  • 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, 2016 at 10:02
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//'
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.

You must log in to answer this question.

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