UNIX: find a file in directories above $PWD - bash

I want to find a file with a certain name, but search in direcotories above the current one, instead of below.
I'd like something similar to: (except functional)
$ cd /some/long/path/to/my/dir/
$ find -maxdepth -1 -name 'foo'
/some/long/path/to/foo
/some/foo
Shell scripts or one-liners preferred.
In response to the several questions, the difference between the above example and the real find is that the search is proceeding upward from the current directory (and -maxdepth doesn't take a negative argument).

Interesting question, so I try to give a interesting answer :)
find `( CP=${PWD%/*}; while [ -n "$CP" ] ; do echo $CP; CP=${CP%/*}; done; echo / ) ` -mindepth 1 -maxdepth 1 -type f -name 'foo'
A bit of elaborate, the 'while' loop will try in generate list of path which is parent to current directory. The while loop won't generate /, so I add additional 'echo /' to cover that.
Finally, the enclosing "find" command is fairly basic usage.

You could use Parameter Expansion:
path="/some/long/path/to/my/dir"
while [ -n "$var" ]
do
find $path -maxdepth 1 -name 'foo'
path="${var%/*}"
done

This works, but it's not as simple as I hoped.
FILE=foo
DIR=$PWD
while [[ $DIR != '/' ]]; do
if [[ -e $DIR/$FILE ]]; then
echo $DIR/$FILE
else
DIR=`dirname $DIR`
fi
done

If you mean exclude the current dir:
find / -name 'foo' ! -iwholename "$PWD*"
If you mean: direct matches in any dir in the trail, this would work, but my bash-fu is not enough to easily get the list of dirs:
find /some/ /some/long /some/long/path/ /some/long/path/to/ /some/long/path/to/my -maxdepth=1 -name='foo'
So all we need is a method to alter /some/long/path/to/my/dir to
/some/ /some/long /some/long/path/ /some/long/path/to/ /some/long/path/to/my

Related

How to use find with variable and wildcards

I'm writing a simple program that downloads multiple images from multiple pages of a website. When trying to implement folder creation that has a naming structure similar to how the website is layed out, I ran into issues. Below is a bare bones example of what I used to replicate the behavior of my other program.
#!/bin/bash
# Sample inputs:
# http://testurl.com/post/1234
# http://testurl.com/post/5678
folder=""
if [[ $1 == *"post"* ]]; then
folder=${1##*/}
folder=${folder//[$'\t\r\n ']}
fi
if [[ $(find "$HOME" -name "*$folder*" -print -quit) ]]; then
echo 'Hi'
else
echo 'Bye'
fi
# Sample directories:
# /home/user/1234
# /home/user/0001
Everywhere I've looked tells me this should run perfectly. However, this does not run as it should and I've been at it for hours. Can anyone help me?
Bash version: GNU bash, version 5.0.3(1)-release (x86_64-pc-linux-gnu)
this simplifies the test whether find found something, using standard grep rather than bashisms:
if find "$HOME" -type d -name "$folder" -print -quit | grep .; then
echo "Hi"
else
echo "Bye"
fi
i also changed two constraints for find:
only search for directories: -type d
so you don't get ordinary files
only search for paths where the basename (the last componenent of the full path) matches ${folder} exactly
so you don't get matches for the /home/user/12345 or /home/user/.emacs.d/auto-save-list/.saves-12350-localhost~
for practical reasons (once the script is known to work), i would discard the output of grep, by redirecting it to /dev/null)
if the directories are all directly in "${HOME}", you could also add -maxdepth 1 as the first argument to find (to not recurse into subdirectories).
so you end up with something like:
if find "$HOME" -maxdepth 1 -type d -name "$folder" -print -quit | grep . >/dev/null
then
echo "Hi"
else
echo "Bye"
fi
or simply use:
if [ -d "${HOME}/${folder}" ]; then
echo "Hi"
else
echo "Bye"
fi

Bash: Use directory as variable

I'm writing a script to check if there actually is a directory that has content and a normal size, and see if there is a directory older then 36 hours, if not it should alert me.
However I'm having trouble using the directories as variable.
When I execute the script it returns: ./test.sh: line 5: 1: No such file or directory.
I tried ALLDIR=$(ls /home/customers/*/ as well but returned the same error.
What am I doing wrong? Below is the script.
Thanks a lot in advance!!
#!/bin/bash
ALLDIR=$(find * /home/customers/*/ -maxdepth 2 -mindepth 2)
for DIR in ${ALLDIR}
do
if [[ $(find "$DIR" -maxdepth 1 -type d -name '*' ! -mtime -36 | wc -l = <1 ) ]]; then
mail -s "No back-ups found today at $DIR! Please check the issue!" test#example.com
exit 1
fi
done
for DIR in ${ALLDIR}
do
if [[ $(find "$DIR" -mindepth 1 -maxdepth 1 -type d -exec du -ks {} + | awk '$1 <= 50' | cut -f 2- ) ]]; then
mail -s "Backup directory size is too small for $DIR, please check the issue!" test#example.com
exit 1
fi
done
For a start, to loop through all directories a fixed level deep, use this:
for dir in /home/customers/*/*/*/
A pattern ending in a slash / will only match directories.
Note that $dir is a lowercase variable name, don't use uppercase ones as they may clash with shell internal/environment variables.
Next, your conditions are a bit broken - you don't need to use a [[ test here:
if ! find "$dir" -maxdepth 1 -type d ! -mtime -36 | grep -q .
If anything is found, find will print it and grep will quietly match anything, so the pipeline will exit successfully. The ! at the start negates the condition, so the if branch will only be taken when this doesn't happen, i.e. when nothing is found. -name '*' is redundant.
You can do something similar with the second if, removing the [[ and $() and using grep -q . to test for any output. I guess the cut part is redundant too.

Script fails with spaces in directory names

I have a really easy question, I have found a bunch of similar questions answered but none that solved this for me.
I have a shell script that goes through a directory and prints out the number of files and directories in a sub directory, followed by the directory name.
However it fails with directories with spaces, it attempts to use each word as a new argument. I have tried putting $dir in quotations but that doesn't help. Perhaps because its already in the echo quotations.
for dir in `find . -mindepth 1 -maxdepth 1 -type d`
do
echo -e "`ls -1 $dir | wc -l`\t$dir"
done
Thanks in advance for your help :)
Warning: Two of the three code samples below use bashisms. Please take care to use the correct one if you need POSIX sh rather than bash.
Don't do any of those things. If your real problem does involve using find, you can use it like so:
shopt -s nullglob
while IFS='' read -r -d '' dir; do
files=( "$dir"/* )
printf '%s\t%s\n' "${#files[#]}" "$dir"
done < <(find . -mindepth 1 -maxdepth 1 -type d -print0)
However, for iterating over only immediate subdirectories, you don't need find at all:
shopt -s nullglob
for dir in */; do
files=( "$dir"/* )
printf '%s\t%s\n' "${#files[#]}" "$dir"
done
If you're trying to do this in a way compatible with POSIX sh, you can try the following:
for dir in */; do
[ "$dir" = "*/" ] && continue
set -- "$dir"/*
[ "$#" -eq 1 ] && [ "$1" = "$dir/*" ] && continue
printf '%s\t%s\n' "$#" "$dir"
done
You shouldn't ever use ls in scripts: http://mywiki.wooledge.org/ParsingLs
You shouldn't ever use for to read lines: http://mywiki.wooledge.org/DontReadLinesWithFor
Use arrays and globs when counting files to do this safely, robustly, and without external commands: http://mywiki.wooledge.org/BashFAQ/004
Always NUL-terminate file lists coming out of find -- otherwise, filenames containing newlines (yes, they're legal in UNIX!) can cause a single name to be read as multiple files, or (in some find versions and usages) your "filename" to not match the real file's name. http://mywiki.wooledge.org/UsingFind

List directories not containing certain files?

I used this command to find all the directories containing .mp3 in the current directory, and filtered out only the directory names:
find . -iname "*.mp3" | sed -e 's!/[^/]*$!!' -e 's!^\./!!' | sort -u
I now want the opposite, but I found it a little harder. I can't just add a '!' to the find command since it'll only exclude .mp3 when printing them not find directories that do not contain .mp3.
I googled this and searched on stackoverflow and unix.stackexchange.com.
I have tried this script so far and it returns this error:
#!/bin/bash
find . -type d | while read dir
do
if [[! -f $dir/*.mp3 ]]
then
echo $dir
fi
done
/home/user/bin/try.sh: line 5: [[!: command not found
#!/bin/bash
find . -type d | while read dir
do
if [! -f $dir/*.mp3 ]
then
echo $dir
fi
done
/home/user/bin/try.sh: line 5: [!: command not found
#!/bin/bash
find . -type d | while read dir
do
if [[! -f "$dir/*.mp3" ]]
then
echo $dir
fi
done
/home/user/bin/try.sh: line 5: [!: command not found
I'm thinking it has to do with multiple arguments for the test command.
Since I'm testing all the directories the variable is going to change, and I use a wildcard for the filenames.
Any help is much appreciated. Thank You.
[ "$(echo $dir/*.mp3)" = "$dir/*.mp3" ]
should work.
Or simply add a space between '[' and '!'
A method that is probably significantly faster is
if find "$dir" -name '*.mp3' -quit ; then
: # there are mp3-files in there.
else
; # no mp3:s
fi
Okay, I solved my own answer by using a counter.
I don't know how efficient it is, but it works. I know it can be made better. Please feel free to critique.
find . -type d | while read dir
do
count=`ls -1 "$dir"/*.mp3 2>/dev/null | wc -l`
if [ $count = 0 ]
then
echo $dir
fi
done
This prints all directories not containing MP3s It also shows sub-directories thanks to the find command printing directories recursively.
I ran a script to automatically download cover art for my mp3 collection. It put a file called "cover.jpg" in the directory for each album for which it could retrieve the art. I needed to check for which albums the script had failed - i.e. which CDs (directories) did not contain a file called cover.jpg. This was my effort:
find . -maxdepth 1 -mindepth 1 -type d | while read dir; do [[ ! -f $dir/cover.jpg ]] && echo "$dir has no cover art"; done
The maxdepth 1 stops the find command from descending into a hidden directory which my WD My Cloud NAS server had created for each album and placed a default generic disc image. (This got cleared during the next scan.)
Edit: cd to the MP3 directory and run it from there, or change the . in the command above to the path to point to it.

How to check the extension of a filename in a bash script?

I am writing a nightly build script in bash.
Everything is fine and dandy except for one little snag:
#!/bin/bash
for file in "$PATH_TO_SOMEWHERE"; do
if [ -d $file ]
then
# do something directory-ish
else
if [ "$file" == "*.txt" ] # this is the snag
then
# do something txt-ish
fi
fi
done;
My problem is determining the file extension and then acting accordingly. I know the issue is in the if-statement, testing for a txt file.
How can I determine if a file has a .txt suffix?
Make
if [ "$file" == "*.txt" ]
like this:
if [[ $file == *.txt ]]
That is, double brackets and no quotes.
The right side of == is a shell pattern.
If you need a regular expression, use =~ then.
I think you want to say "Are the last four characters of $file equal to .txt?" If so, you can use the following:
if [ "${file: -4}" == ".txt" ]
Note that the space between file: and -4 is required, as the ':-' modifier means something different.
You could also do:
if [ "${FILE##*.}" = "txt" ]; then
# operation for txt files here
fi
You just can't be sure on a Unix system, that a .txt file truly is a text file. Your best bet is to use "file". Maybe try using:
file -ib "$file"
Then you can use a list of MIME types to match against or parse the first part of the MIME where you get stuff like "text", "application", etc.
You can use the "file" command if you actually want to find out information about the file rather than rely on the extensions.
If you feel comfortable with using the extension you can use grep to see if it matches.
The correct answer on how to take the extension available in a filename in linux is:
${filename##*\.}
Example of printing all file extensions in a directory
for fname in $(find . -maxdepth 1 -type f) # only regular file in the current dir
do echo ${fname##*\.} #print extensions
done
Similar to 'file', use the slightly simpler 'mimetype -b' which will work no matter the file extension.
if [ $(mimetype -b "$MyFile") == "text/plain" ]
then
echo "this is a text file"
fi
Edit: you may need to install libfile-mimeinfo-perl on your system if mimetype is not available
I wrote a bash script that looks at the type of a file then copies it to a location, I use it to look through the videos I've watched online from my firefox cache:
#!/bin/bash
# flvcache script
CACHE=~/.mozilla/firefox/xxxxxxxx.default/Cache
OUTPUTDIR=~/Videos/flvs
MINFILESIZE=2M
for f in `find $CACHE -size +$MINFILESIZE`
do
a=$(file $f | cut -f2 -d ' ')
o=$(basename $f)
if [ "$a" = "Macromedia" ]
then
cp "$f" "$OUTPUTDIR/$o"
fi
done
nautilus "$OUTPUTDIR"&
It uses similar ideas to those presented here, hope this is helpful to someone.
I guess that '$PATH_TO_SOMEWHERE'is something like '<directory>/*'.
In this case, I would change the code to:
find <directory> -maxdepth 1 -type d -exec ... \;
find <directory> -maxdepth 1 -type f -name "*.txt" -exec ... \;
If you want to do something more complicated with the directory and text file names, you could:
find <directory> -maxdepth 1 -type d | while read dir; do echo $dir; ...; done
find <directory> -maxdepth 1 -type f -name "*.txt" | while read txtfile; do echo $txtfile; ...; done
If you have spaces in your file names, you could:
find <directory> -maxdepth 1 -type d | xargs ...
find <directory> -maxdepth 1 -type f -name "*.txt" | xargs ...
Credit to #Jox for the majority of this answer, though I found (js) was matching .json files so I added an eol character to more fully match the extension.
$file does not need to be quoted because [[ ]] won't expand and so spaces aren't an issue (credit: Hontvári Levente)
if [[ $file =~ .*\.(js$|json$) ]]; then
echo "The extension of '$file' matches .js|.json";
fi
Another important detail, you don't use else with another if inside:
else
if [ "$file" == "*.txt" ]
# this is the snag
then
# do something txt-ish
fi
instead:
elif [ "$file" == "*.txt" ]
# this is the snag
then
# do something txt-ish
fi
else is used when there's nothing else left > do > thatcommand
just because you can do something that doesn't necessarily means you should always do it

Resources