Why does this conditional return "No such file or directory" - bash

My conditional works properly when the dirs exist, but if they don't, it seems to execute both then and else statements (is that the correct term?).
script.sh
#!/bin/bash
if [[ $(find path/to/dir/*[^thisdir] -type d -maxdepth 0) ]]
then
find path/to/dir/*[^thisdir] -type d -maxdepth 0 -exec mv {} new/location \;
echo "Huzzah!"
else
echo "hey hey hey"
fi
prompt
For the first call, the dirs are there; in the second, they've been moved from the first call.
$ sh script.sh
Huzzah!
$ sh script.sh
find: path/to/dir/*[^thisdir]: No such file or directory
hey hey hey
How can I fix this?
tried suggestion(s)
if [[ -d $(path/to/dir/*[^thisdir]) ]]
then
find path/to/dir/*[^thisdir] -type d -maxdepth 0 -exec mv {} statamic-1.3-personal/admin/themes \;
echo "Huzzah!"
else
echo "hey hey hey"
fi
result
$ sh script.sh
script.sh: line 1: path/to/dir/one_of_the_dirs_to_be_moved: is a directory
hey hey hey

There seem to be some errors:
First, the pattern path/to/dir/*[^thisdir] is interpreted in bash in the same manner than path/to/dir/*[^dihstr] mean *all filename ending by d, i, h, s, t or r.
Than if you are searching for something in this dir (path/to/dir) but not on path/to/dir/thisdir, and not on a nth subdir, you could bannish find and write:
Edit: There was an error on my sample too: [ -e $var ] was wrong.
declare -a files=( path/to/dir/!(thisdir) )
if [ -e $files ] ;then
mv -t newlocation "${files[#]}"
echo "Huzzah!"
else
echo "hey hey hey"
fi
If you need find for searching in subirs, please give us samples and/or more descriptions.

Your error is probably occurring at if [[ $(find path/to/dir/*[^thisdir] -type d -maxdepth 0) ]] and then it goes to else because find errors out.
find wants its directory parameter to exist. Based on what you are trying to do you should probably consider
$(find path/to/dir/ -name "appropriate name pattern" -type d -maxdepth 1)
Also, I'd consider using actual logical function in if. See this for file conditionals.

Try adding a #!/bin/bash on the first line to ensure that it is bash that is executing your script, as recommended by this post:
Why is both the if and else executed?

The OP wishes to move all files excluding thisdir to a new location.
A solution using find would be to exclude thisdir using find's functionality, rather than by using bash's shell expansion:
#!/bin/bash
if [[ $(find path/to/directory/* -maxdepth 0 -type d -not -name 'thisdir') ]]
then
find path/to/directory/* -maxdepth 0 -type d -not -name 'thisdir' -exec mv {} new/location \;
echo "Huzzah!"
else
echo "hey hey hey"
fi
This has been tested, and works under bash version 4.2.39, and GNU findutils v4.5.10.

Related

Test if the find command found files or not

I have a simple script to check a file of today's date. In the below example, the else condition is ignored if the file is not found. Why so?
if filedir=$(find . -type f -name "*.sql.gz" -mtime -1 -printf "%f\n"); then
echo $filedir
else
echo "oops"
fi
find returns an exit-code of 0 if all arguments are processed successfully, and has only a non-zero exitcode if there was an error. The exit code for find does not indicate whether or not files are found.
This is unlike, for example grep. You can see the difference in behaviour when you use
if filedir=$(find . -type f -name "*.sql.gz" -mtime -1 -printf "%f\n" | grep '.'); then
As Ljm Dullaart explained earlier, the find command does not return a specific code, when no file match patterns or rules.
Though, You may test it found a match, by checking the variable filedir is not empty:
[ -n "${filedir}" ]
if filedir="$(find . -type f -name '*.sql.gz' -mtime -1)" && [ -n "${filedir}" ]; then
printf 'Found: %s\n' "${filedir}"
else
printf 'Oops! Found no file matching *.sql.gz!\n' >&2
fi

How to stop Bash expansion of '*.h" in a function?

In trying to run the following function—Bash is expanding my variable in an unexpected way—thus preventing me from getting my expected result.
It comes down to the way bash deals with a "*.h" which I am passing in to the function.
Here is the function I call:
link_files_of_type_from_directory "*.h" ./..
And where I would expect this variable to stay this way all the way through at some point, by the time it hits the echo $command_to_run; part of my Bash script...this variable has expanded to...
MyHeader1.h MyHeader2.h MyHeader3.h
and so on.
What I want is for Bash to not expand my files so that my code runs the following:
find ./.. -type f -name '*.h'
Instead of
find ./.. -type f -name MyHeader1.h MyHeader2.h MyHeader3.h
This is the code:
function link_files_of_type_from_directory {
local file_type=$1;
local directory_to_link=$2;
echo "File type $file_type";
echo "Directory to link $directory_to_link";
command="find $directory_to_link -type f -name $file_type";
echo $command;
#for i in $(find $directory_to_link -type f -name $file_type);
for i in $command;
do
echo $i;
if test -e $(basename $i); then
echo $i exists;
else
echo Linking: $i;
ln -s $i;
fi
done;
}
How can I prevent the expansion so that Bash does search for files that end in *.h in my the directory I want to pass in?
UPDATE 1:
So I've updated the call to be
link_files_of_type_from_directory "'*.h'" ..
And the function now assembles the string of the command to be evaluated like so:
mmd="find $directory_to_link -type f -name $file_type";
When I echo it out—it's correct :)
find .. -type f -name '*.h'
But I can't seem to get the find command to actually run. Here are the errors / mistakes I'm getting while trying to correctly assemble the for loop:
# for i in $mmd; # LOOPS THROUGH STRINGS IN COMMAND
# for i in '$(mdd)'; # RUNS MMD LITERALLY
# for i in ${!mmd}; # Errors out with: INVALID VARIABLE NAME — find .. -type f -name '*.h':
Would love help on this part—even though it is a different question :)
With quoting of your variables, removed semicolons and your loop wrapped into an -exec action to prevent problems with spaces, tabs and newlines in filenames, your function looks like this:
function link_files_of_type_from_directory {
local file_type=$1
local directory_to_link=$2
echo "File type $file_type"
echo "Directory to link $directory_to_link"
find "$directory_to_link" -type f -name "$file_type" -exec sh -c '
for i do
echo "$i"
if test -e "$(basename "$i")"; then
echo "$i exists"
else
echo "Linking: $i"
ln -s "$i"
fi
done
' sh {} +
}

find statement in cygwin bash script

for i in `find . -type f -name "VF-Outlet*.edi" -exec basename \{} \;` ; do
if [ -n "${i}" ];
then
echo file "VF-Outlet found";
sed -e 's/\*UK\*00/\*UP\*/g;s/XQ.*$/XQ\*H\*20150104\*20150110/g' $i > ${i}_fix
else
echo file "VF-Outlet" not found;
fi
done
The above code works if the file is found. The 'echo' statement prints file found.
If the file is not found however, nothing prints. I tried all the various tests for empty string, and unset variables, nothing works.
Also if I try:
i=`find . -type f -name "VF-Outlet*.edi" -exec basename \{} \;`;
Then do the test:
if [ -n "${i}" ];
then
echo file ${i} found;
else
echo file "VF-Outlet" not found;
fi
done
It works correctly if the file is found or not.
Need help in figuring this out. I need the for loop to test multiple files.
The reason it is not working is due to the fact that "for" does not take null value as input for the variable "i"
For ex:
for i in echo > /dev/null; do echo hi; done
The above command wont give any result, because no value has been assigned to value $i for running the loop.
In the case mentioned here if we check the script in debug mode, we can see that the script dies at initial variable assignment.
# sh -x script.sh
+ find . -type f -name VF-Outlet*.edi -exec basename {} ;
here, script.sh file contains the script you have provided.
If there is a file present in the directory, the script will successfully execute.
# sh -x tet
+ find . -type f -name VF-Outlet*.edi -exec basename {} ;
+ [ -n VF-Outlet1.edi ]
+ echo file VF-Outlet found
file VF-Outlet found
As #shellter mentioned, this not how I would have done. You can use -f instead of -n to check if a file exists.
Hope this helps!

Variable from conditional statement

I have a few scripts I am taking ownership of that use Bash shell, there is a find statement inside a conditional statement.
Something like this:
if [ -z $(find / -type f -perm -002) ] ; then echo "no world writable found"
where as an else I would like to display what was found instead of world write perms found.
I can do:
echo $(find / -type f -perm -002) has world write permissions
or set variable to $(find / -type f -perm -002).
But was wondering if there was a a better way to do this. Is there another way to retrieve the contents of the find statement as a variable?
You just take the output and store it in a variable. If it is not empty you can print its contents. This way you only need to run the command once.
RESULT=$(find / -type f -perm -002)
if [ -z "$RESULT" ]
then
echo "no world writable found"
else
echo "$RESULT has world write permissions"
fi
You can use use sed to insert a headline, if you like.
REPORT=$(find /tmp -type f -perm -002 | sed '1s/^/Found world write permissions:\n/')
echo ${REPORT:-No world writable found.}
Notice: your example seems to be broken, because find can return more than one line.
And awk can do both at once:
find /tmp -type f -perm -002 |
awk -- '1{print "Found world write permissions:";print};END{if(NR==0)print "No world writable found."}'
If you don't mind not having the message no world writable found, you can use a single find statement, and that's all:
find / -type f -perm -002 -printf '%p has world write permissions\n'
If you need to store the returned files for future use, store them in an array (assuming Bash):
#!/bin/bash
files=()
while IFS= read -r -d '' f; do
files+=( "$f" )
# You may also print the message:
printf '%s has world write permissions\n' "$f"
done < <(find / -type f -perm -002 -print0)
# At this point, you have all the found files
# You may print a message if no files were found:
if ((${#files[#]}==0)); then
printf 'No world writable files found\n'
exit 0
fi
# Here you can do some processing with the files found...

Perform an action in every sub-directory using Bash

I am working on a script that needs to perform an action in every sub-directory of a specific folder.
What is the most efficient way to write that?
A version that avoids creating a sub-process:
for D in *; do
if [ -d "${D}" ]; then
echo "${D}" # your processing here
fi
done
Or, if your action is a single command, this is more concise:
for D in *; do [ -d "${D}" ] && my_command; done
Or an even more concise version (thanks #enzotib). Note that in this version each value of D will have a trailing slash:
for D in */; do my_command; done
for D in `find . -type d`
do
//Do whatever you need with D
done
The simplest non recursive way is:
for d in */; do
echo "$d"
done
The / at the end tells, use directories only.
There is no need for
find
awk
...
Use find command.
In GNU find, you can use -execdir parameter:
find . -type d -execdir realpath "{}" ';'
or by using -exec parameter:
find . -type d -exec sh -c 'cd -P "$0" && pwd -P' {} \;
or with xargs command:
find . -type d -print0 | xargs -0 -L1 sh -c 'cd "$0" && pwd && echo Do stuff'
Or using for loop:
for d in */; { echo "$d"; }
For recursivity try extended globbing (**/) instead (enable by: shopt -s extglob).
For more examples, see: How to go to each directory and execute a command? at SO
Handy one-liners
for D in *; do echo "$D"; done
for D in *; do find "$D" -type d; done ### Option A
find * -type d ### Option B
Option A is correct for folders with spaces in between. Also, generally faster since it doesn't print each word in a folder name as a separate entity.
# Option A
$ time for D in ./big_dir/*; do find "$D" -type d > /dev/null; done
real 0m0.327s
user 0m0.084s
sys 0m0.236s
# Option B
$ time for D in `find ./big_dir/* -type d`; do echo "$D" > /dev/null; done
real 0m0.787s
user 0m0.484s
sys 0m0.308s
find . -type d -print0 | xargs -0 -n 1 my_command
This will create a subshell (which means that variable values will be lost when the while loop exits):
find . -type d | while read -r dir
do
something
done
This won't:
while read -r dir
do
something
done < <(find . -type d)
Either one will work if there are spaces in directory names.
You could try:
#!/bin/bash
### $1 == the first args to this script
### usage: script.sh /path/to/dir/
for f in `find . -maxdepth 1 -mindepth 1 -type d`; do
cd "$f"
<your job here>
done
or similar...
Explanation:
find . -maxdepth 1 -mindepth 1 -type d :
Only find directories with a maximum recursive depth of 1 (only the subdirectories of $1) and minimum depth of 1 (excludes current folder .)
the accepted answer will break on white spaces if the directory names have them, and the preferred syntax is $() for bash/ksh. Use GNU find -exec option with +; eg
find .... -exec mycommand +; #this is same as passing to xargs
or use a while loop
find .... | while read -r D
do
# use variable `D` or whatever variable name you defined instead here
done
if you want to perform an action INSIDE the folder and not ON folder.
Explanation: You have many pdfs and you would like to concetrate them inside a single folder.
my folders
AV 001/
AV 002/
for D in *; do cd "$D"; # VERY
DANGEROUS COMMAND - DONT USE
#-- missing "", it will list files too. It can go up too.
for d in */; do cd "$d"; echo $d; cd ..; done; # works
succesfully
for D in "$(ls -d */)"; do cd "$D"; done; #
bash: cd: $'Athens Voice 001/\nAthens Voice 002/' - there is no such
folder
for D in "$(*/)"; do cd "$D"; done; # bash: Athens
Voice 001/: is folder
for D in "$(`find . -type d`)"; do cd $D; done; # bash: ./Athens: there is no such folder or file
for D in *; do if [ -d "${D}" ] then cd ${D}; done; # many
arguments

Resources