5

What is the best way to sort the results of $ find . -name scripts -type d by the occurrences of '/' and then choose the first result?

I want to create a function that would cd down to a common folder in a project. I wanted it to be flexible based on your relative directory.

So if I have 10 projects all with similar folder structure:

~/project-a/project/folder/structure
~/project-b/project/folder/structure
~/project-c/project/folder/structure

I could:

$ cd ~/project-a
$ cdd structure

And be dropped down into ~/project-a/project/folder/structure

Update

I'm unable to sort results in any predictable way, example:

$ find . -type d -name "themes"

./wp-content/plugins/contact-form-7/includes/js/jquery-ui/themes
./wp-content/plugins/jetpack/modules/infinite-scroll/themes
./wp-content/plugins/smart-youtube/themes
./wp-content/plugins/wptouch-pro/themes
./wp-content/themes
./wp-includes/js/tinymce/themes

I'd like the cdd function to drop down to the closest result. In this example it'd be ./wp-content/themes.

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255

4 Answers4

3

With zsh, you could do:

cdd() cd -- **/$1(/Od[1])

cdd themes

That cdd function finds all the files of type directory (/) with the name given as argument ($1), Orders them by depth and selects the first one ([1]).

Where it's not very efficient is that it crawls the whole directory tree (skipping hidden directories, add the D glob qualifier to change that) even when there's a matching directory in the current directory.

To traverse the directory tree one level of depth at a time, you could do instead:

cdd() {
  local dirs matches
  dirs=(.)
  while (($#dirs)) {
    matches=($^dirs/$1(N/[1]))
    if (($#matches)) {
      cd $matches[1]
      return
    }
    dirs=($^dirs/*(/N))
  }
  print >&2 Not found
  return 1
}
2

You can't use and alias for this problem, because an alias can't take arguments in the middle, only at the end. You need a function for that.

The following function does what you are asking for:

cdd() { 
   cd `find . -type d -name "$1"|head -n1`
}

You will have to add these lines to your .profile (or .bashrc file), so it is executed (once) when you log in.

For testing you can put it in a file e.g. called func.sh and then source it (i.e. execute it using the "." operator) so it becomes part of your environment:

. ./func.sh

Note: If you make changes to file func.sh you have to source it again.

You can then use it as:

cdd <dirname>

The function, as it is now, will search for a directory with the name <dirname> in the current path and below and will cd to the first occurrence it finds (head -n1).

You can fine-tune the find operation so it comes up with your desired result first. See man find.

To find the closest match you can sort the output of find to give you the shortest line:

find . -type d -name "$1"| awk '{ print length, $0 }' | sort -n -s | cut -d" " -f2- | head -n1

(The awk command was stolen from here.)

This will return the shortest line in the output of find. It will however return the wrong directory if you for instance are looking for directory proj and you have a structure like this:

./d1/d2/d3/proj
./longdirname/proj

Alternatively, you could count the number of / characters in the result to get around that.

NZD
  • 1,422
  • Thanks for the correction on using a function instead of alias. I actually ran into that testing. However, fine tuning the find command is where I was asking for help. How would I sort results by depth. – drrobotnik Jul 28 '15 at 17:17
  • Find doesn't have a sort option. You can however sort the output of find based on length. I've amended my answer to include that. – NZD Jul 28 '15 at 20:45
1

Maybe with bash's recursive wildcard feature:

shopt -s globstar  # in ~/.bashrc, or just in cdd() {}
cd **/structure/

I think ** expansion will happen in more or less the same order as find, and thus have most of the same problems. But it's so easy and useful that you may be able to overlook this!

More recent versions of bash reject cd with multiple args so this only works if there's exactly one match. Otherwise you get cd: too many arguments.

So you'll need a shell function. Maybe array=( **/"$1"/ ) and use the first element if non-empty, but if you're writing a function you might want find -depth -type d | tail -1 or something to take the last result (one of the shallowest). Or incrementally use -maxdepth if your tree might be very deep.

Peter Cordes
  • 6,466
1

This bash function does a brute-force find through all the layers of directories underneath the current directory, looking for the named directory.

cdd() {
  if [ "$#" -ne 1 ]
  then
    printf '%s: Error: one directory name expected\n' "${FUNCNAME[0]}" >&2
    return
  fi

  maxdepth=$(find . -type d -exec sh -c 'printf %s "$1" | LC_ALL=C tr -dc / | wc -c' findshell {} \; | sort -n | tail -1)
  for((i=1; i <= maxdepth; i++))
  do
    dir=$(find . -mindepth "$i" -maxdepth "$i" -type d -name "$1")
    if [ -n "$dir" ]
    then
        command cd -- "$dir"
        return
    fi
  done
  printf '%s: %s: no such directory found\n' "${FUNCNAME[0]}" "$1" >&2
}

The first find command execute a shell snippet for each directory under the current one; the shell snippet prints the directory name to a tr & wc pipeline that deletes every character that's not a forward-slash, then counts the number of forward-slashes. I picked up that trick from ilkkachu and Stéphane Chazelas here. After outputting all of the directory depths, we sort | tail to find the largest number. That number is the maximum directory depth that we'll need to search.

After that, it's a loop over find, limited to exactly each directory depth in turn, starting from the current directory down to the deepest directory. If a match is found, we cd there and exit the function. If no match is found, we fall off the end of the loop and print a message to stderr.

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255