31

In a shell script how would I find a file by a particular name and then navigate to that directory to do further operations on it?

From here I am going to copy the file across to another directory (but I can do that already just adding it in for context.)

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
Candyfloss
  • 3,848
  • 4
  • 31
  • 32

12 Answers12

33

You can use something like:

cd -- "$(dirname "$(find / -type f -name ls | head -1)")"

This will locate the first ls regular file then change to that directory.

In terms of what each bit does:

  • The find will start at / and search down, listing out all regular files (-type f) called ls (-name ls). There are other things you can add to find to further restrict the files you get.
  • The | head -1 will filter out all but the first line.
  • $() is a way to take the output of a command and put it on the command line for another command.
  • dirname can take a full file specification and give you the path bit.
  • cd just changes to that directory, the -- is used to prevent treating a directory name beginning with a hyphen from being treated as an option to cd.

If you execute each bit in sequence, you can see what happens:

pax[/home/pax]> find / -type f -name ls
/usr/bin/ls

pax[/home/pax]> find / -type f -name ls | head -1
/usr/bin/ls

pax[/home/pax]> dirname "$(find / -type f -name ls | head -1)"
/usr/bin

pax[/home/pax]> cd -- "$(dirname "$(find / -type f -name ls | head -1)")"

pax[/usr/bin]> _
paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
  • 1
    I think you need a space between `-type f` and `-name ls`. – Dennis Williamson Aug 11 '10 at 14:29
  • @Ross: note that there should be double quotes around each `$()` (otherwise your script will fail with paths containing whitespace and other special characters): `cd "$(dirname "$(find / -type f-name ls | head -1)")"` – Gilles 'SO- stop being evil' Aug 12 '10 at 12:19
  • That's a good point, Gilles, updated to fix. I rarely put spaces in my file specifications since I think they're abominable, but I realise some people like them and you're right: code should handle them. – paxdiablo Aug 12 '10 at 12:33
  • It would still break on directory names containing newlines. – Philipp Aug 12 '10 at 12:46
  • 1
    Yes, it would, and people using newlines and backspaces and tabs and other funny characters in their file names should be beaten to death, separated into small bits and have those bits launched to the furthest reaches of the universe to burn in the hearts of various stars :-) – paxdiablo Aug 12 '10 at 13:37
  • @paxdiablo: You can't always decide what characters are going to end up in a file name. Sometimes your scripts encounter other people's files. Sometimes you're on Windows and need to treat `c:\Program Files` correctly. – Gilles 'SO- stop being evil' Aug 12 '10 at 15:30
  • Any program that pretends to work with file names but doesn't accept all valid file names is flawed. – Philipp Aug 12 '10 at 20:24
  • Pure genius! Thanks for parsing that out. Others point out that it's not bullet-proof, but I really like this simple solution. It's exactly what I need 99.9% of the time, and it's easy to wrap this in a function in .bashrc for quick navigating. This made my day! – Matthew Kraus Feb 04 '15 at 21:48
14

The following should be more safe:

cd -- "$(find / -name ls -type f -printf '%h' -quit)"

Advantages:

  • The double dash prevents the interpretation of a directory name starting with a hyphen as an option (find doesn't produce such file names, but it's not harmful and might be required for similar constructs)
  • -name check before -type check because the latter sometimes requires a stat
  • No dirname required because the %h specifier already prints the directory name
  • -quit to stop the search after the first file found, thus no head required which would cause the script to fail on directory names containing newlines
Philipp
  • 48,066
  • 12
  • 84
  • 109
  • 1
    To me, this is the best answer. Can be even shortened for most cases and one could follow the style: 1) make find command finding what you need, e.g. `$ find . -name *.xsd` 2) add the printf and -quit `$ find . -name *.xsd -printf "%h" -quit` 3) surround by backtics and feed into cd (and ommit -- if not needed): `$ cd \`find . -name *.xsd -printf "%h" -quit\`` – Jan Vlcinsky Apr 11 '13 at 20:43
  • the `-printf '%h'` part makes this command actually return nothing, even if there are files... not quite sure why tho – phil294 Mar 05 '16 at 00:52
  • @JanVlcinsky Using backticks instead of quotes doesn't work for me. It fails and says `path/to/folder: Is a directory`. – mbomb007 Nov 13 '20 at 18:10
5

no one suggesting locate (which is much quicker for huge trees) ?

zsh:

cd $(locate zoo.txt|head -1)(:h)
cd ${$(locate zoo.txt)[1]:h}
cd ${$(locate -r "/zoo.txt$")[1]:h}   

or could be slow

cd **/zoo.txt(:h)

bash:

cd $(dirname $(locate -l1 -r "/zoo.txt$"))
zzapper
  • 4,743
  • 5
  • 48
  • 45
4

Expanding on answers already given, if you'd like to navigate iteratively to every file that find locates and perform operations in each directory:

for i in $(find /path/to/search/root -name filename -type f)
do (
  cd $(dirname $(realpath $i));
  your_commands;
)
done
cydonian
  • 1,686
  • 14
  • 22
  • shellcheck suggests that loops over `find` are fragile: https://github.com/koalaman/shellcheck/wiki/SC2044 – MakisH Mar 25 '21 at 15:13
4

Based on this answer to a similar question, other useful choice could be having 2 commands, 1st to find the file and 2nd to navigate to its directory:

find ./ -name "champions.txt"
cd "$(dirname "$(!!)")"

Where !! is history expansion meaning 'the previous command'.

Community
  • 1
  • 1
Ricardo
  • 3,696
  • 5
  • 36
  • 50
1
function fReturnFilepathOfContainingDirectory {
    #fReturnFilepathOfContainingDirectory_2012.0709.18:19
    #$1=File

    local vlFl
    local vlGwkdvlFl
    local vlItrtn
    local vlPrdct

    vlFl=$1
    vlGwkdvlFl=`echo $vlFl | gawk -F/ '{ $NF="" ; print $0 }'`
    for vlItrtn in `echo $vlGwkdvlFl` ;do
        vlPrdct=`echo $vlPrdct'/'$vlItrtn`
    done
    echo $vlPrdct

}
bkmfs
  • 11
  • 1
1

Simply this way, isn't this elegant?

cdf yourfile.py

Of course you need to set it up first, but you need to do this only once:

Add following line into your .bashrc or .zshrc, whatever you use as your shell initialization script.

source ~/bin/cdf.sh 

And add this code into ~/bin/cdf.sh file that you need to create from scratch.

#!/bin/bash

function cdf() {
    THEFILE=$1
    echo "cd into directory of ${THEFILE}"
    # For Mac, replace find with mdfind to get it a lot faster. And it does not need args ". -name" part.
    THEDIR=$(find . -name ${THEFILE} |head -1 |grep -Eo "/[ /._A-Za-z0-9\-]+/")
    cd ${THEDIR}
}
Ville Laitila
  • 1,187
  • 11
  • 18
1

if you are just finding the file and then moving it elsewhere, just use find and -exec

find /path -type f -iname "mytext.txt" -exec mv "{}" /destination +;
ghostdog74
  • 327,991
  • 56
  • 259
  • 343
  • 2
    I just had a similar situation where I wanted to rename copies of files with the same name in different directories. find's `-execdir` option did the trick for me: `find . -name foo.txt -execdir mv "{}" bar.txt ";"` renames foo.txt to bar.txt in each directory it's found in. – Andrew Hershberger Sep 13 '11 at 23:19
0

If your file is only in one location you could try the following:

cd "$(find ~/ -name [filename] -exec dirname {} \;)" && ...

You can use -exec to invoke dirname with the path that find returns (which goes where the {} placeholder is). That will change directories. You can also add double ampersands ( && ) to execute the next command after the shell has changed directory.

For example:
cd "$(find ~/ -name need_to_find_this.rb -exec dirname {} \;)" && ruby need_to_find_this.rb

It will look for that ruby file, change to the directory, then run it from within that folder. This example assumes the filename is unique and that for some reason the ruby script has to run from within its directory. If the filename is not unique you'll get many locations passed to cd, it will return an error then it won't change directories.

Gunga
  • 84
  • 2
  • 1
0

If it's a program in your PATH, you can do:

cd "$(dirname "$(which ls)")"

or in Bash:

cd "$(dirname "$(type -P ls)")"

which uses one less external executable.

This uses no externals:

dest=$(type -P ls); cd "${dest%/*}"
Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
0

try this. i created this for my own use.

cd ~
touch mycd
sudo chmod +x mycd
nano mycd
cd $( ./mycd search_directory target_directory )"

if [ $1 == '--help' ]
 then
    echo -e "usage: cd \$( ./mycd \$1 \$2 )"
    echo -e "usage: cd \$( ./mycd search_directory target_directory )"
else
  find "$1"/ -name "$2" -type d -exec echo {} \; -quit
  fi
Simas Joneliunas
  • 2,890
  • 20
  • 28
  • 35
0

cd -- "$(sudo find / -type d -iname "dir name goes here" 2>/dev/null)"

keep all quotes (all this does is just send you to the directory you want, after that you can just put commands after that)

  • 1
    why are you using `sudo`? I don't think the nested quotes will work as you intend – moo Dec 31 '22 at 07:27