Terminal : Difference between quoted argument and non quoted in find function - shell

What is the difference between these two commands in the command line in the terminal on ubuntu?
find . -name "*.txt"
find . -name *.txt

It depends on the shell you are using and the contents of your directory.
If you're unfortunate enough to use csh (or some of its heritage, like zsh) for some reason, it will refuse to execute this when there is no file in your directory that matches the pattern (unless you've turned that behaviour off).
If there are files that match the pattern, the command will be expanded by the shell:
find . -name foo.txt bar.txt baz.txt
and find will report a syntax error. Unless there is a single file that matches, then you get:
find . -name foo.txt
and this will then only find files named "foo.txt" in subdirectories.
Only if you're using a sane shell and there are no files matching in the current directory will the two commands be identical.
Moral: don't do it. Always use quotes.

Related

How to delete all files in a dir except ones with a certain pattern in their name?

I have a lot of kernel .deb files from my custom kerenls. I would like to write a bash that would delete all the old files except the ones associated with the currently installed kernel version. My script:
#!/bin/bash
version='uname -r'
$version
dir=~/Installed-kernels
ls | grep -v '$version*' | xargs rm
Unfortunately, this deletes all files in the dir.
How can I get the currently installed kernel version and set said version as a perimeter with? Each .deb I want to keep contains the kernel version (5.18.8) but have other strings in their name (linux-headers-5.18.8_5.18.8_amd64.deb).
Edit: I am only deleting .deb files inside the noted directory. The current list of file names in the tree are
linux-headers-5.18.8-lz-xan1_5.18.8-lz-1_amd64.deb
linux-libc-dev_5.18.8-lz-1_amd64.deb
linux-image-5.18.8-lz-xan1_5.18.8-lz-1_amd64.deb
This can be done as a one-liner, though I've preserved your variables:
#!/bin/bash
version="$(uname -r)"
dir="$HOME/Installed-kernels"
find "$dir" -maxdepth 1 -type f -not -name "*$version*" -print0 |xargs -0 rm
To set a variable to the output of a command, you need either $(…) or `…`, ideally wrapped in double-quotes to preserve spacing. A tilde isn't always interpreted correctly when passed through variables, so I expanded that out to $HOME.
The find command is much safer to parse than the output of ls, plus it lets you better filter things. In this case, -maxdepth 1 will look at just that directory (no recursion), -type f seeks only files, and -not -name "*$version*" removes paths or filenames that match the kernel version (which is a glob, not a regex—you'd otherwise have to escape the dots). Also note those quotes; we want find to see the asterisks, and without the quotes, the shell will expand the glob prematurely. The -print0 and corresponding -0 ensure that you preserve spacing by delimiting entries with null characters.
You can remove the prompts regarding read-only files with rm -f.
If you also want to delete directories, remove the -type f part and add -r to the end of that final line.

What is good way to move a directory and then run a command to the file inside it using a bash shell one-liner

I would like to find txt files with find command and move the directory of the found file, and then apply a command to the file using a bash shell one-liner
For example, this command works, but the acmd is executed in the current directory.
$ find . -name "*.txt" | xargs acmd
I would like to run acmd in the txt file's direcotry.
Does anyone have good idea?
From the find man page:--
-execdir command ;
-execdir command {} +
Like -exec, but the specified command is run from the subdirec‐
tory containing the matched file, which is not normally the
directory in which you started find. This a much more secure
method for invoking commands, as it avoids race conditions dur‐
ing resolution of the paths to the matched files. As with the
-exec action, the `+' form of -execdir will build a command line
to process more than one matched file, but any given invocation
of command will only list files that exist in the same subdirec‐
tory. If you use this option, you must ensure that your $PATH
environment variable does not reference `.'; otherwise, an
attacker can run any commands they like by leaving an appropri‐
ately-named file in a directory in which you will run -execdir.
The same applies to having entries in $PATH which are empty or
which are not absolute directory names. If find encounters an
error, this can sometimes cause an immediate exit, so some pend‐
ing commands may not be run at all. The result of the action
depends on whether the + or the ; variant is being used;
-execdir command {} + always returns true, while -execdir com‐
mand {} ; returns true only if command returns 0.
Just for completeness, the other option would be to do:
$ find . -name \*.txt | xargs -i sh -c 'echo "for file $(basename {}), the directory is $(dirname '{}')"'
for file schedutil.txt, the directory is ./Documentation/scheduler
for file devices.txt, the directory is ./Documentation/admin-guide
for file kernel-parameters.txt, the directory is ./Documentation/admin-guide
for file gdbmacros.txt, the directory is ./Documentation/admin-guide/kdump
...
i.e. have xargs "defer to a shell". In usecases where -execdir suffices, go for it.

How to 'cd' to the output of the 'find' command in terminal

Pretty much I want to cd to the output of the find command:
find ~ -name work_project_linux
cd the_output
In general the best way to execute an arbitrary command on the results of find is with find -exec. Curly braces {} are placeholders for the file names it finds, and the entire command ends with + or \;. For example, this will run ls -l on all of the files found:
find ~ -name work_project_linux -exec ls -l {} +
It doesn't work with some special commands like cd, though. -exec runs binaries, such as those found in /usr/bin, and cd isn't a binary. It's a shell builtin, a special type of command that the shell executes directly instead of calling out to some executable on disk. For shell builtins you can use command substitution:
cd "$(find ~ -name work_project_linux)"
This wouldn't work if find finds multiple files. It's only good for a single file name. Command substitution also won't handle some unusual file names correctly, such as those with embedded newlines—unusual, but legal.

Bash find: changing matched name for use in -exec

I'm writing a deploy script, and I need to run a less compiler against all .less files in a directory. This is easy to do with the following find command:
find -name "*.less" -exec plessc {} {}.css \;
After running this command on a folder with a file named main.less, I'm left with a file named main.less.css, but I want it to be main.css.
I know I can easily strip the .less portion of the resulting files with this command: rename 's/\.less//' *.css but I'm hoping to learn something new about using -exec.
Is it possible to modify the name of the file that matches while using it in the -exec parameter?
Thanks!
Your find command is using a couple of non standard GNU extensions:
You do not state where to find, this is an error in POSIX but GNU find select the current directory in that case
You use a non isolated {}, POSIX find doesn't expand it in that case.
Here is a one liner that should work with most find implementations and fix your double extension issue:
find . -name "*.less" -exec sh -c "plessc \$0 \$(dirname \$0)/\$(basename \$0 less)css" {} \;
On Solaris 10 and older, sh -c should be replaced by ksh -c if the PATH isn't POSIX compliant.
No, it is not possible to do it directly. You can only use {} to directly insert the full filename. However, in exec, you COULD put in other things like awk. Or you can redirect output to another program via pipes.
From the find man 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.

find command in bash shell and the -name option doubts

What is the difference between the two below:
find . -type f -name \*.bmp
find . -type f -name *.bmp
I have tested,they both return the same result,so is there anything different _deep inside_?
Added from the removed answer:
So it is to avoid the shell expansion for the special ***** character,solely pass * as a argument to the find command and let it process it.
But on my machine,they are all good, both return the bmp files in and below the current directory,to name a few,the result is like below,some are omitted for brevity
./images/building_color.bmp
./images/building_gray.bmp
./images/car_gray.bmp
./images/temple_color.bmp
./images/boat_gray.bmp
./images/tools_gray.bmp
./images/temple_gray.bmp
./images/tools_color.bmp
./images/car_color.bmp
./images/boat_color.bmp
system info:
GNU bash, version 4.1.5(1)-release (i486-pc-linux-gnu)
Linux sysabod-laptop 2.6.32-30-generic #59-Ubuntu SMP Tue Mar 1 21:30:21 UTC 2011 i686 GNU/Linux
Here's how they're different: the first one always works, and the second one doesn't.
As for why: in bash, shell globs (wildcard patterns including * or ?) are expanded by the shell into all files matching the glob. However, if no such files exist, the pattern is left alone.
So, if you're in a directory with no bmp files, the commands work the same way, because the first is escaped and bash fails to find any files matching in the second case.
If you ran it from a directory containing only one such file, say foo.bmp, the first would find all bmp files in the subtree, while the second would find all files named foo.bmp only. If run in a directory with multiple bmp files, I believe you'll get an error because find doesn't know what to do with all the filenames.
When you escape the asterisk (\*) the asterisk itself is passed as argument to the find command and will be evaluated by find. If you don't escape the asterisk (*) already the shell evaluates it and expands it to the file names matching the pattern.
Fore example consider following directory structure:
./a.txt
./b.bmp
./c.bmp
./dir/d.doc
./dir/e.bmp
When you execute
find . -type f -name *.bmp
the shell expands *.bmp to b.bmp c.bmp. I.e. the command that is actually executed will be:
find . -type f -name b.bmp c.bmp
which will find b.bmp and c.bmp but not dir/e.bmp.
When you execute
find . -type f -name \*.bmp
*.bmp is passed directly as it is to find. find will recurse through the current directory (.) and all its subdirectories (in the example only dir) and will find all files in those directories matching the pattern. The result will be: b.bmp, c.bmp and also dir/e.bmp.
The first command:
find . -type f -name \*.bmp
passes an asterisk to the find command, and that tells it to find all the files in and below the current directory ending with .bmp.
The second command:
find . -type f -name *.bmp
may be resolved by the shell to, for example:
find . -type f -name image1.bmp image2.bmp image3.bmp
(that would be the bmp files in the current directory only)
and find would only list them, not the bmp files in other directories below the current one.

Resources