find command - syntax with -exec handle - linux

Background and problem:
I am using a find command to gzip all files that match my criteria:
sudo -u userAccount -i find /apps/server/userAccount/logs/. -maxdepth 1 -type f -exec gzip {} \;
This will go ahead and compress all files against my criteria but I don't want to be prompted to overwrite -- I want to be able to automagically skip and file that already exists as I'm dealing with sensitive data and cannot risk losing anything.
So far, I have tried:
statement as above -exec yes n | gzip {} \;
statement as above -exec [an if statement to determine if a file already exists]
None of these work due to find statement syntax errors.
Question:
Does anybody know how I can use either method above with my find statement?
Update:
I attempted to use:
sudo -u userAccount -i find /apps/server/userAccount/logs/. -maxdepth 1 -type f -exec bash -c "yes n | gzip {}" \;
But I just get scrolling 'y' characters returned to the console until I CTRL-C. Any suggestions?

The command after -exec is not run by a shell, it's executed directly by find, so it can't use shell metacharacters or builtins. If you need to use the shell, you have to invoke it explicitly:
-exec bash -c "yes n | gzip {}" \;

If you don't want to use pipes to the gzip command, you can use a case to test your file type.
find $PATH_TO_LOGS -maxdepth 1 -mtime +$SOME_NUMBER_OF_DAYS -exec sh -c "case {} in *.gz) ;; *) gzip '{}' ;; esac;" \;
Of course, this depends upon your file names ending in the traditional .gz extension. Benefits of this is that you can also process other file types by extension in this form.
Also, if you really don't want to lose anything, and you can't depend upon a backup copy, it might be a good idea to instead timestamp the files and simply incur the storage space hit.
find $PATH_TO_LOGS -maxdepth 1 -mtime +$SOME_NUMBER_OF_DAYS -exec gzip -N {} \;

Related

Better way of using the redirection symbol in conjunction with find -exec

My goal is to empty every file in a directory. I DON'T want to actually delete the file, I just want to delete it's contents.
If you want to do this with a single file you can do > file.txt
If I want to run this operation on every file in a directory I can do this:
find . -exec /bin/bash -c '> {}' \;
Notice how the above command has to call /bin/bash. This is because simply running the command like this, find . -exec > {} \; says find: invalid argument ;' to -exec' I suspect this is because the redirection symbol is confusing the command.
I would like to run this command without needing to run /bin/bash within -exec
How can this be done?
One easy way to do this is by using truncate:
find -type f -exec truncate -s0 {} +
If you want to only use bash, you could use a while loop:
find -type f -print0 |
while IFS= read -r -d '' file; do
> "$file"
done
Finally, if you didn't mind using bash -c, it would be better to do it as follows to avoid calling bash so many times:
find -type f -exec bash -c 'for file; do > "$file"; done' - {} +
although I don't like that solution.

Embedding a bash command inside the mv command

I have a directory that contains a list of files having the following format:
240-timestamp1.ts
240-timestamp2.ts
...
360-timestamp1.ts
360-timestamp2.ts
Now, I want to implement a bash command which matches the files that start with '240' and renames them so that instead of '240-timestampX.ts' the files look like '240-human-readable-timestampX.ts'.
I have tried the following:
find . -maxdepth 1 -mmin +5 -type f -name "240*"
-exec mv $0 {$0/240-***and here I want to insert
either stat -c %y filename or date -d #timestampX***} '{}' \;
I stuck here because I don't know if I can embed a bash command inside the mv command. I know the task may look a bit confusing and over-complicated, but I would like to know if it is possible to do so. Of course I can create a bash script that would go through all the files in the directory and while loop them with changing their respective names, but somehow I think that a single command would be more efficient (even if less readable).
The OS is Linux Ubuntu 12.04.5
The shell is bash
Thank you both Kenavoz and Kurt Stutsman for the proposed solutions. Both your answers perform the task; however, I marked Kenavoz's answer as the accepted one because of the degree of similarity between my question and Kenavoz's answer. Even if it is indeed possible to do it in a cleaner way with omitting the find command, it is necessary in my case to use the respective command because I need to find files older than X units of time. So thank you both once again!
In case you want to keep your mmin option, your can use find and process found files with a bash command using xargs :
find . -maxdepth 1 -mmin +5 -type f -name "240*.ts" | xargs -L 1 bash -c 'mv "${1}" "240-$(stat -c %y ${1}).ts"' \;
In bash if all your files are in a single directory, you don't need to use find at all. You can do a for loop:
for file in 240-*; do
hr_timestamp=$(date -d $(echo "$file" | sed 's/.*-\([0-9]*\)\.ts/\1/'))
mv "$file" "240-$hr_timestamp.ts"
done

how to cp files with spaces in the filename when files are provided by find

I would like to ensure that all files found by find with a given criteria are properly copied to the required location.
$from = '/some/path/to/the/files'
$ext = 'custom_file_extension'
$dest = '/new/destination/for/the/files/with/given/extension'
cp 'find $from -name "*.$ext"' $dest
The problem here is that, when a file found with the proper extension and it is containing space cp cannot copy it properly.
You don't do that. You can't splat filenames with spaces that way.
You either get to use something from http://mywiki.wooledge.org/BashFAQ/001 to read the output from find line-by-line or into an array or you use find -exec to do the copy work.
Something like this:
from='/some/path/to/the/files'
ext='custom_file_extension'
dest='/new/destination/for/the/files/with/given/extension'
find "$from" -name "*.$ext" -exec cp -t "$dest" {} +
Using -exec command + here means that find will only execute as many cp commands as it needs based on command length limits. Using -exec command ; here would run one cp-per-file-found (but is more portable to older systems).
See comment from gniourf_gniourf about the use of -t in that cp command to make -exec command + work correctly.
Use -exec:
find "$from" -name "*.$ext" -exec cp {} "$dest" \;
you need to copy file one by one:
for file in "$from"/*."$ext"; do
cp "$file" "$dest"
done
I just use glob here, and it's enough and complete. I think find may introduce problem if the file name contains funny character.
The solution for this sort of problem is xargs -0 and the -print0 flag for find.
-print0 instructs find to print the results with a NUL character termination, instead of a newline, while -0 for xargs tells it expect input in that format.
Finally, the -J option for xargs allows one to put the arguments in the right place for a copy.
find "$from" -name "*.$ext" -print0 | xargs -0 -J % cp % "$dest"
It's better to use -exec argument of find command to do this:
find . -type f -name "*.ext" -exec cp {} ./destination_dir \;
I've checked this case with files containing spaces and it's work for me. Also don't forger to point out '-type f' if you want to find only files, not directories.

How to use find to make copies of file with prefix in CSH?

I am trying to make copies of certain file and let them have a prefix.
in order to do it I thought of using find. for our use, let's call them kuku files and I want them to have a "foo" prefix:
find . -maxdepth 1 -name "kuku*" -exec cp '{}' foo_'{}' \;
but it doesn't work because the find always starts the results with ./ so i get a lot of error messages saying "cp: cannot create regular file `foo_./kuku...`: No such file or directory".
the problem is solvable by using foreach f (`ls`) and than using grep and the status var, but it is cumbersome and I want to learn a better solution (and improve my knowledge of the find command along the way...).
update foreach solution (which I don't like and want your help in finding a replacement):
foreach f (`ls`)
echo $f | grep -lq kuku
if (! $status) then
cp $f foo_$f
endif
end
but this is UGLY! (end of update)
as the header says, I'm using csh - not because I love it, just because that's what we use at work...
update
trying to use basename as a solution, because find -exec basename '{}' \; removes the ./ prefix, but i failed using the basename inside the find with backticks (`), meaning that
find -name "kuku*" -exec cp '{}' foo_`basename '{}` \;
simply doesn't work.
Here you go.. I have tested in my linux box
find . -name "kuku*" -exec sh -c 'cp {} foo_`basename {}`' \;

Why does find -exec mv {} ./target/ + not work?

I want to know exactly what {} \; and {} \+ and | xargs ... do. Please clarify these with explanations.
Below 3 commands run and output same result but the first command takes a little time and the format is also little different.
find . -type f -exec file {} \;
find . -type f -exec file {} \+
find . -type f | xargs file
It's because 1st one runs the file command for every file coming from the find command. So, basically it runs as:
file file1.txt
file file2.txt
But latter 2 find with -exec commands run file command once for all files like below:
file file1.txt file2.txt
Then I run the following commands on which first one runs without problem but second one gives error message.
find . -type f -iname '*.cpp' -exec mv {} ./test/ \;
find . -type f -iname '*.cpp' -exec mv {} ./test/ \+ #gives error:find: missing argument to `-exec'
For command with {} \+, it gives me the error message
find: missing argument to `-exec'
why is that? can anyone please explain what am I doing wrong?
The manual page (or the online GNU manual) pretty much explains everything.
find -exec command {} \;
For each result, command {} is executed. All occurences of {} are replaced by the filename. ; is prefixed with a slash to prevent the shell from interpreting it.
find -exec command {} +
Each result is appended to command and executed afterwards. Taking the command length limitations into account, I guess that this command may be executed more times, with the manual page supporting me:
the total number of invocations of the command will be much less than the number of matched files.
Note this quote from the manual page:
The command line is built in much the same way that xargs builds its command lines
That's why no characters are allowed between {} and + except for whitespace. + makes find detect that the arguments should be appended to the command just like xargs.
The solution
Luckily, the GNU implementation of mv can accept the target directory as an argument, with either -t or the longer parameter --target. It's usage will be:
mv -t target file1 file2 ...
Your find command becomes:
find . -type f -iname '*.cpp' -exec mv -t ./test/ {} \+
From the manual page:
-exec command ;
Execute command; true if 0 status is returned. All following arguments to find are taken to be arguments to the command until an argument consisting of `;' is encountered. The string `{}' is replaced by the current file name being processed everywhere it occurs in the arguments to the command, not just in arguments where it is alone, as in some versions of find. Both of these constructions might need to be escaped (with a `\') or quoted to protect them from expansion by the shell. See the EXAMPLES section for examples of the use of the -exec option. The specified command is run once for each matched file. The command is executed in the starting directory. There are unavoidable security problems surrounding use of the -exec action; you should use the -execdir option instead.
-exec command {} +
This variant of the -exec action runs the specified command on the selected files, but the command line is built by appending each selected file name at the end; the total number of invocations of the command will be much less than the number of matched files. The command line is built in much the same way that xargs builds its command lines. Only one instance of `{}' is allowed within the command. The command is executed in the starting directory.
I encountered the same issue on Mac OSX, using a ZSH shell: in this case there is no -t option for mv, so I had to find another solution.
However the following command succeeded:
find .* * -maxdepth 0 -not -path '.git' -not -path '.backup' -exec mv '{}' .backup \;
The secret was to quote the braces. No need for the braces to be at the end of the exec command.
I tested under Ubuntu 14.04 (with BASH and ZSH shells), it works the same.
However, when using the + sign, it seems indeed that it has to be at the end of the exec command.
The standard equivalent of find -iname ... -exec mv -t dest {} + for find implementations that don't support -iname or mv implementations that don't support -t is to use a shell to re-order the arguments:
find . -name '*.[cC][pP][pP]' -type f -exec sh -c '
exec mv "$#" /dest/dir/' sh {} +
By using -name '*.[cC][pP][pP]', we also avoid the reliance on the current locale to decide what's the uppercase version of c or p.
Note that +, contrary to ; is not special in any shell so doesn't need to be quoted (though quoting won't harm, except of course with shells like rc that don't support \ as a quoting operator).
The trailing / in /dest/dir/ is so that mv fails with an error instead of renaming foo.cpp to /dest/dir in the case where only one cpp file was found and /dest/dir didn't exist or wasn't a directory (or symlink to directory).
find . -name "*.mp3" -exec mv --target-directory=/home/d0k/Музика/ {} \+
no, the difference between + and \; should be reversed. + appends the files to the end of the exec command then runs the exec command and \; runs the command for each file.
The problem is find . -type f -iname '*.cpp' -exec mv {} ./test/ \+ should be find . -type f -iname '*.cpp' -exec mv {} ./test/ + no need to escape it or terminate the +
xargs I haven't used in a long time but I think works like +.

Resources