Get the filename from bash script recursively using find - bash

I am trying retrieve the filename from the find command recursively. This command prints all the filenames with full path
> for f in $(find -name '*.png'); do echo "$f";done
> ./x.png
> ./bg.png
> ./s/bg.png
But when i try to just get the name of the file using these commands, it prints
for f in $(find -name '*.png'); do echo "${f##*/}";done
bg.png
and
for f in $(find -name '*.png'); do echo $(basename $f);done
bg.png
It omits other 2 files. I am new to shell scripting. I couln't figure out whats wrong with this one.
EDIT:
THis is what i have actually wanted.
I want to loop through a directory recursively and find all png images
send it to pngnq for RGBA compression
It outputs the new file with orgfilename-nq8.png
send it to pngcrush and rename and generate a new file (org file will be overwritten )
remove new file
i have a code which works on a single directory
for f in *.png; do pngnq -f -n 256 "$f" && pngcrush "${f%.*}"-nq8.png "$f";rm "${f%.*}"-nq8.png; done
I want to do this recursively

Simply do :
find -name '*.png' -printf '%f\n'
If you want to run something for each files :
find -name '*.png' -printf '%f\n' |
while read file; do
# do something with "$file"
done
Or with xargs :
find -name '*.png' -printf '%f\n' | xargs -n1 command
Be sure you cannot use find directly like this :
find -name '*.png' -exec command {} +
or
find -name '*.png' -exec bash -c 'do_something with ${1##*/}' -- {} \;
Search -printf on http://unixhelp.ed.ac.uk/CGI/man-cgi?find or in
man find | less +/^' *-printf'

Related

How to write the wc's stdout into a file?

The below command will show how many characters contains in every file in current directory.
find -name '*.*' |xargs wc -c
I want to write the standout into a file.
find -name '*.*' |xargs wc -c > /tmp/record.txt
It encounter an issue:
wc: .: Is a directory
How to write all the standard output into a file?
Why -name '*.*'? That will not find every file and will find directories. You need to use -type f, and better than piping the result to xargs is using -exec:
find . -type f -maxdepth 1 -exec wc -c {} + > /tmp/record.txt
-maxdepth 1 guarantees that the search won't dive in subdirectories.
I think you maybe meant find |xargs wc -c?
find -name '.' just returns .
Filter only files, if you want only files.
find -type f

How do I use find command with pipe in bash?

The directory structure looks like
home
--dir1_foo
----subdirectory.....
--dir2_foo
--dir3_foo
--dir4_bar
--dir5_bar
I'm trying to use 'find' command to get directories containing specific strings first, (in this case 'foo'), then use 'find' command again to retrieve some directories matching conditions.
So, I first tried
#!/bin/bash
for dir in `find ./ -type d -name "*foo*" `;
do
for subdir in `find $dir -mindepth 2 -type d `;
do
[Do some jobs]
done
done
, and this script works fine.
Then I thought that using only one loop with pipe like below would also work, but this does not work
#!/bin/bash
for dir in `find ./ -type d -name "*foo*" | find -mindepth 2 -type d `;
do
[Do some jobs]
done
and actually this script works the same as
for dir in `find -mindepth 2 -type d`;
do
[Do some jobs]
done
, which means that the first find command is ignored..
What is the problem?
What your script is doing is not a good practice and has lot of potential pitfalls. See BashFAQ- Why you don't read lines with "for" to understand why.
You can use xargs with -0 to read null delimited files and use the another find command without needing to use the for-loop
find ./ -type d -name "*foo*" -print0 | xargs -0 -I{.} find {.} -mindepth 2 -type d
The string following -I in xargs acts like a placeholder for the input received from the previous pipeline and passes it to the next command. The -print0 option is GNU specific which is a safe option to hande filenames/directory names containing spaces or any other shell meta-characters.
So with the above command in-place, if you are interested in doing some action over the output from 2nd command, do a process-substitution syntax with the while command,
while IFS= read -r -d '' f; do
echo "$f"
# Your other actions can be done on "$f" here
done < <(find ./ -type d -name "*foo*" -print0 | xargs -0 -I{.} find {.} -mindepth 2 -type d -print0)
As far the reason why your pipelines using find won't work is that you are not reading the previous find command's output. You needed either xargs or -execdir while the latter is not an option I would recommend.

find option available to omit leading './' in result

I think this is probably a pretty n00ber question but I just gotsta ask it.
When I run:
$ find . -maxdepth 1 -type f \( -name "*.mp3" -o -name "*.ogg" \)
and get:
./01.Adagio - Allegro Vivace.mp3
./03.Allegro Vivace.mp3
./02.Adagio.mp3
./04.Allegro Ma Non Troppo.mp3
why does find prepend a ./ to the file name? I am using this in a script:
fList=()
while read -r -d $'\0'; do
fList+=("$REPLY")
done < <(find . -type f \( -name "*.mp3" -o -name "*.ogg" \) -print0)
fConv "$fList" "$dBaseN"
and I have to use a bit of a hacky-sed-fix at the beginning of a for loop in function 'fConv', accessing the array elements, to remove the leading ./. Is there a find option that would simply omit the leading ./ in the first place?
The ./ at the beginning of the file is the path. The "." means current directory.
You can use "sed" to remove it.
find . -maxdepth 1 -type f \( -name "*.mp3" -o -name "*.ogg" \) | sed 's|./||'
I do not recommend doing this though, since find can search through multiple directories, how would you know if the file found is located in the current directory?
If you ask it to search under /tmp, the results will be on the form /tmp/file:
$ find /tmp
/tmp
/tmp/.X0-lock
/tmp/.com.google.Chrome.cUkZfY
If you ask it to search under . (like you do), the results will be on the form ./file:
$ find .
.
./Documents
./.xmodmap
If you ask it to search through foo.mp3 and bar.ogg, the result will be on the form foo.mp3 and bar.ogg:
$ find *.mp3 *.ogg
click.ogg
slide.ogg
splat.ogg
However, this is just the default. With GNU and other modern finds, you can modify how to print the result. To always print just the last element:
find /foo -printf '%f\0'
If the result is /foo/bar/baz.mp3, this will result in baz.mp3.
To print the path relative to the argument under which it's found, you can use:
find /foo -printf '%P\0'
For /foo/bar/baz.mp3, this will show bar/baz.mp3.
However, you shouldn't be using find at all. This is a job for plain globs, as suggested by R Sahu.
shopt -s nullglob
files=(*.mp3 *.ogg)
echo "Converting ${files[*]}:"
fConv "${files[#]}"
find . -maxdepth 1 -type f \( -name "*.mp3" -o -name "*.ogg" \) -exec basename "{}" \;
Having said that, I think you can use a simpler approach:
for file in *.mp3 *.ogg
do
if [[ -f $file ]]; then
# Use the file
fi
done
If your -maxdepth is 1, you can simply use ls:
$ ls *.mp3 *.ogg
Of course, that will pick up any directory with a *.mp3 or *.ogg suffix, but you probably don't have such a directory anyway.
Another is to munge your results:
$ find . -maxdepth 1 -type f \( -name "*.mp3" -o -name "*.ogg" \) | sed 's#^\./##'
This will remove all ./ prefixes, but not touch other file names. Note the ^ anchor in the substitution command.

Using cp in bash to use piped in information about files like modification date

I am trying to copy files from one directory into another from certain modification date ranges. For example, copy all files created after May 10 from dir1 to dir2. I have tried a few things but have been unsuccessful so far.
This made sense to me but cp does not take the filenames piped to it, but just executes ./* and copies all files in the directory:
find . -type f -daystart -mtime 2 | cp ./* /dir/
This almost worked, but did not copy all of the matching files, I also tried xargs -s 50000, but did not work:
find . -type f -daystart -mtime 2 | xargs -I {} cp {} /dir/
find . -type f -daystart -mtime 2 | xargs cp -t /dir/
Found this online, does not work:
cp $(find . -type f -daystart -mtime 2) /dir/
Ideas? Thanks.
Given as your actual question is about using filenames from stdin rather than metadata from stdin, this is quite straightforward:
while IFS= read -r -d '' filename; do
cp "$filename" /wherever
done < <(find . -type f -daystart -mtime 2 -print0)
Note the use of IFS= read -r -d '' and -print0 -- as NUL and / are the only two characters which can't be used in UNIX filenames, using any other character, including the newline, to delimit them is unsafe. Think about what would happen if someone (or a software bug) created a file called $'./ \n/etc/passwd'; you want to be damned sure none of your scripts try to delete or overwrite /etc/passwd when they're trying to delete or overwrite that file.
That said, you don't actually need to use a pipe at all:
find . -type f -daystart -mtime -2 -exec cp '{}' /wherever ';'
...or, if you're only trying to support GNU cp, you can use this more efficient variant:
find . -type f -daystart -mtime -2 -exec cp -t /wherever '{}' +
You don't specify why the various attempts didn't work, so I can only assume that they are the result of whitespace in the filenames.
Try using find's useful -exec action instead of using xargs:
find . -type f -daystart -mtime 2 -exec cp {} /media/alex/Extra/Music/watchfolder/ \;
find . -type f -daystart -mtime 2 \
| cpio -pdv /media/alex/Extra/Music/watchfolder/

How can I list all unique file names without their extensions in bash?

I have a task where I need to move a bunch of files from one directory to another. I need move all files with the same file name (i.e. blah.pdf, blah.txt, blah.html, etc...) at the same time, and I can move a set of these every four minutes. I had a short bash script to just move a single file at a time at these intervals, but the new name requirement is throwing me off.
My old script is:
find ./ -maxdepth 1 -type f | while read line; do mv "$line" ~/target_dir/; echo "$line"; sleep 240; done
For the new script, I basically just need to replace find ./ -maxdepth 1 -type f
with a list of unique file names without their extensions. I can then just replace do mv "$line" ~/target_dir/; with do mv "$line*" ~/target_dir/;.
So, with all of that said. What's a good way to get a unique list of files without their file names with bash script? I was thinking about using a regex to grab file names and then throwing them in a hash to get uniqueness, but I'm hoping there's an easier/better/quicker way. Ideas?
A weird-named files tolerant one-liner could be:
find . -maxdepth 1 -type f -and -iname 'blah*' -print0 | xargs -0 -I {} mv {} ~/target/dir
If the files can start with multiple prefixes, you can use logic operators in find. For example, to move blah.* and foo.*, use:
find . -maxdepth 1 -type f -and \( -iname 'blah.*' -or -iname 'foo.*' \) -print0 | xargs -0 -I {} mv {} ~/target/dir
EDIT
Updated after comment.
Here's how I'd do it:
find ./ -type f -printf '%f\n' | sed 's/\..*//' | sort | uniq | ( while read filename ; do find . -type f -iname "$filename"'*' -exec mv {} /dest/dir \; ; sleep 240; done )
Perhaps it needs some explaination:
find ./ -type f -printf '%f\n': find all files and print just their name, followed by a newline. If you don't want to look in subdirectories, this can be substituted by a simple ls;
sed 's/\..*//': strip the file extension by removing everything after the first dot. Both foo.tar ad foo.tar.gz are transformed into foo;
sort | unique: sort the filenames just found and remove duplicates;
(: open a subshell:
while read filename: read a line and put it into the $filename variable;
find . -type f -iname "$filename"'*' -exec mv {} /dest/dir \;: find in the current directory (find .) all the files (-type f) whose name starts with the value in filename (-iname "$filename"'*', this works also for files containing whitespaces in their name) and execute the mv command on each one (-exec mv {} /dest/dir \;)
sleep 240: sleep
): end of subshell.
Add -maxdepth 1 as argument to find as you see fit for your requirements.
Nevermind, I'm dumb. there's a uniq command. Duh. New working script is: find ./ -maxdepth 1 -type f | sed -e 's/.[a-zA-Z]*$//' | uniq | while read line; do mv "$line*" ~/target_dir/; echo "$line"; sleep 240; done
EDIT: Forgot close tag on code and a backslash.

Resources