Bash while loop behaves differently dealing with multiple test conditions - bash

I wonder if someone could explain why a while loop treats multipe test conditions differently to an if loop. I have 2 tests that i verified came out as True and False:
Bash$ test ! -n "$(find . -maxdepth 1 -name '*.xml' -print -quit)"; echo $?
0
Bash$ test ! -e "unsentData.tmp"; echo $?
1
Bash$
When I ANDed these 2 tests into an if statement I got an aggregate of False as expected:
Bash$ if [ ! -n "$(find . -maxdepth 1 -name '*.xml' -print -quit)" ] && [ ! -e "unsentData.tmp" ]; then echo "True"; else echo "False"; fi
False
Bash$
Now when I put the 2 tests into a while loop I expected a sleep until both conditions were met but instead I got immediately a true result.
Bash$ while [ ! -n "$(find . -maxdepth 1 -name '*.xml' -print -quit)" ] && [ ! -e "unsentData.tmp" ]; do sleep 1; done; echo -e "All files Exist\n$(ls /opt/pcf/mfe/unsentXmlToTSM/xmlConnection0_TSM/)"
All files Exist
unsentData.tmp
Bash$
What am I missing here? I simply want to write something that waits until the 2 conditions are met before it breaks out of the loop
A

Your assumption is worng i think. While does execute the code between do and done while (as long as) the condition holds true. Your conditions combined evaluate to false, as seen in the output of your if-statement. Thus the body of the while loop never gets executed.
Try:
while ! ( [ ! -n "$(find . -maxdepth 1 -name '*.xml' -print -quit)" ] &&
[ ! -e "unsentData.tmp" ] )
do
sleep 1
done
echo -e "All files Exist\n$(ls /opt/pcf/mfe/unsentXmlToTSM/xmlConnection0_TSM/)"

A while loop executes as long as ("while") its condition is true; it sounds like you want to run the loop until its condition is true. bash has an until loop which does exactly this:
until [ ! -n "$(find . -maxdepth 1 -name '*.xml' -print -quit)" ] && [ ! -e "unsentData.tmp" ]; do
sleep 1
done
echo -e "All files Exist\n$(ls /opt/pcf/mfe/unsentXmlToTSM/xmlConnection0_TSM/)"
Or you can just negate the condition (i.e. use "while there are files left, do..." rather than "until all files are done, do..."). In this case that just means removing the negations of the individual conditions and switching the and to an or:
while [ -n "$(find . -maxdepth 1 -name '*.xml' -print -quit)" ] || [ -e "unsentData.tmp" ]; do
sleep 1
done
echo -e "All files Exist\n$(ls /opt/pcf/mfe/unsentXmlToTSM/xmlConnection0_TSM/)"

while [ ! -n "$(find . -maxdepth 1 -name '*.xml' -print -quit)" ] && [ ! -e "unsentData.tmp" ]
do sleep 1
done
==>
while false
do sleep 1
done
so the do sleep 1 didn't run at all.

Related

How can I check if file type exists in folder

how can I check if multiple files of a certain type exists in shell script?
I've tried this:
[ -e $HOME/somefolder/Images/*.jpeg ] && echo "ok"
[ -f $HOME/somefolder/Images/*.jpeg ] && echo "ok"
but in both cases I got:
[: too many arguments
I would use the below. find is an excellent utility.
test -n "$(find $HOME/somefolder/Images/ -maxdepth 1 -name '*.jpeg' -print -quit)" && echo "ok" || echo "not ok"
As an alternative, consider the pure-bash solution.
if [ "$(shopt -s nullglob ; echo /path/to/file*.jpeg)" ] ; then
...
fi

if then else twice and if not found then run a command

I have this code:
if [[ $(find /path/to/folder1 -type f -not -path "*configs*" -size -800k 2>/dev/null) ]]; then
echo "[[Warning]]: The files size is under 800 Kilobytes"
if [[ $(find /path/to/another/folder -type f \( ! -iname "123.file*" \) -not -path "*logs*" -size -40k 2>/dev/null) ]]; then
echo "[[Warning]]: The file size is under 40 Kilobytes"
fi
else
Run a command here
fi
The target is:
Get an echo if a file under size found in any of the above paths or get both echo if a file under size found on both paths and run the command at the end only if not files under size found at any of the above two paths.
Both checks are tested and working but it seems that I have the if or else statements in wrong order?
That should work what you have. An easier way would be to use OR (||) in structure below. If both of those statements are false, then you run command.
if statementA || statementB then...
else ...
Your else is already executed when the first if gets false. You could do a
if [[ $(find ....) ]] || [[ $(find ....) ]]; then
else
run
fi
or
if [[ -n $(find ....) || -n $(find ....) ]]; then
else
run
fi
but this would only take care about the running command; you want, however, also display an individual message if the find commands succeed.
There are several possibilities. The easiest would be IMO to use a control variable:
run_command=1
if [[ $(find ....) ]]; then
echo ....
run_command=0
fi
if [[ $(find ....) ]]; then
echo ....
run_command=0
fi
if ((run_command == 1)); then
run ...
fi

How to refactor a find | xargs one liner to a human readable code

I've written an OCR wrapper batch & service script for tesseract and abbyyocr11 found here: https://github.com/deajan/pmOCR
The main function is a find command that passes it's arguments to xargs with -print0 in order to deal with special filenmames.
The find command became more and more complex and ended up as a VERY long one liner that becomes difficult to maintain:
find "$DIRECTORY_TO_PROCESS" -type f -iregex ".*\.$FILES_TO_PROCES" ! -name "$find_excludes" -print0 | xargs -0 -I {} bash -c 'export file="{}"; function proceed { eval "\"'"$OCR_ENGINE_EXEC"'\" '"$OCR_ENGINE_INPUT_ARG"' \"$file\" '"$OCR_ENGINE_ARGS"' '"$OCR_ENGINE_OUTPUT_ARG"' \"${file%.*}'"$FILENAME_ADDITION""$FILENAME_SUFFIX$FILE_EXTENSION"'\" && if [ '"$_BATCH_RUN"' -eq 1 ] && [ '"$_SILENT"' -ne 1 ];then echo \"Processed $file\"; fi && echo -e \"$(date) - Processed $file\" >> '"$LOG_FILE"' && if [ '"$DELETE_ORIGINAL"' == \"yes\" ]; then rm -f \"$file\"; fi"; }; if [ "'$CHECK_PDF'" == "yes" ]; then if ! pdffonts "$file" 2>&1 | grep "yes" > /dev/null; then proceed; else echo "$(date) - Skipping file $file already containing text." >> '"$LOG_FILE"'; fi; else proceed; fi'
Is there a nicer way to pass the find results to a human readable function (without impacting too much speed) ?
Thanks.
Don't use bash -c. You are already committed to starting a new bash process for each file from the find command, so just save the code to a file and run that with
find "$DIRECTORY_TO_PROCESS" -type f -iregex ".*\.$FILES_TO_PROCES" \
! -name "$find_excludes" -print0 |
xargs -0 -I {} bash script.bash {}
You can replace find altogether. It's easier in bash 4 (which I'll show here), but doable in bash 3.
proceed () {
...
}
shopt -s globstar
extensions=(pdf tif tiff jpg jpeg bmp pcx dcx)
for ext in "${extensions[#]}"; do
for file in /some/path/**/*."$ext"; do
[[ ! -f $file || $file = *_ocr.pdf ]] && continue
# Rest of script here
done
done
Prior to bash 4, you can write your own recursive function to descend through a directory hierarchy.
descend () {
for fd in "$1"/*; do
if [[ -d $fd ]]; then
descend "$fd"
elif [[ ! -f $fd || $fd != *."$ext" || $fd = *_ocr.pdf ]]; then
continue
else
# Rest of script here
fi
done
}
for ext in "${extensions[#]}"; do
descend /some/path "$ext"
done
OK, create the script, then run find.
#!/bin/bash
trap cleanup EXIT
cleanup() { rm "$script"; }
script=$(mktemp)
cat <<'END' > "$script"
########################################################################
file="$1"
function proceed {
"$OCR_ENGINE_EXEC" "$OCR_ENGINE_INPUT_ARG" "$file" "$OCR_ENGINE_ARGS" "$OCR_ENGINE_OUTPUT_ARG" "${file%.*}$FILENAME_ADDITION$FILENAME_SUFFIX$FILE_EXTENSION"
if [ "$_BATCH_RUN" -eq 1 ] && [ "$_SILENT" -ne 1 ]; then
echo "Processed $file"
fi
echo -e "$(date) - Processed $file" >> "$LOG_FILE"
if [ "$DELETE_ORIGINAL" == "yes" ]; then
rm -f "$file"
fi
}
if [ "$CHECK_PDF" == "yes" ]; then
if ! pdffonts "$file" 2>&1 | grep "yes" > /dev/null; then
proceed
else
echo "$(date) - Skipping file $file already containing text." >> '"$LOG_FILE"';
fi
else
proceed
fi
########################################################################
END
find "$DIRECTORY_TO_PROCESS" -type f \
-iregex ".*\.$FILES_TO_PROCES" \
! -name "$find_excludes" \
-exec bash "$script" '{}' \;
The 'END' of the heredoc is quoted, so the variables are not expanded until the script is actually executed.
I finished using a while loop with a substituted find command, ie:
while IFS= read -r -d $'\0' file; do
if ! lsof -f -- "$file" > /dev/null 2>&1; then
if [ "$_BATCH_RUN" == true ]; then
Logger "Preparing to process [$file]." "NOTICE"
fi
OCR "$file" "$fileExtension" "$ocrEngineArgs" "$csvHack"
else
if [ "$_BATCH_RUN" == true ]; then
Logger "Cannot process file [$file] currently in use." "ALWAYS"
else
Logger "Deferring file [$file] currently being written to." "ALWAYS"
kill -USR1 $SCRIPT_PID
fi
fi
done < <(find "$directoryToProcess" -type f -iregex ".*\.$FILES_TO_PROCES" ! -name "$findExcludes" -and ! -wholename "$moveSuccessExclude" -and ! -wholename "$moveFailureExclude" -and ! -name "$failedFindExcludes" -print0)
The while loop reads every file from the find command in file variable.
Using -d $'\0' in while and -print0 in find command helps dealing with special filenames.

Nesting a for loop in an if else statement

if [ ! -f ./* ]; then
for files in $(find . -maxdepth 1 -type f); do
echo $files
else
echo Nothing here
fi
Returns
syntax error near unexpected token `else'
New to this. Can anyone point me to what I did wrong?
You forgot done!
if [ ! -f ./* ]; then
for files in $(find . -maxdepth 1 -type f); do
echo $files
done
else
echo Nothing here
fi
The reason you get a syntax error is because you are not ending the loop with the "done" statement. You should be using a while loop, instead of a for loop in this case, as the for loop will break if any of the filenames contain spaces or newlines.
Also, the test command you have issued will also give a syntax error if the glob expands to multiple files.
$ [ ! -f ./* ]
bash: [: too many arguments
Here is a better way to check if the directory contains any files:
files=(./*) # populate an array with file or directory names
hasfile=false
for file in "${files[#]}"; do
if [[ -f $file ]]; then
hasfile=true
break
fi
done
if $hasfile; then
while read -r file; do
echo "$file"
done < <(find . -maxdepth 1 -type f)
fi
Also, you could simply replace the while loop with find -print if you have GNU find:
if $hasfile; then
find . -maxdepth 1 -type f -print
fi
The syntax for "for" is
for: for NAME [in WORDS ... ;] do COMMANDS; done
You are missing the "done"
Try
if [ ! -f ./* ]; then
for files in $(find . -maxdepth 1 -type f); do
echo $files
done
else
echo Nothing here
fi
BTW, did you mean echo with lowercase rather than ECHO?

How to tell if the output of the "find" command is empty?

I want to return an exit status of 0 if the output is empty and 1 otherwise:
find /this/is/a/path/ -name core.*
When you say you want it to return a particular number, are you referring to the exit status? If so:
[[ -z `find /this/is/a/path/ -name core.*` ]]
And since you only care about a yes/no response, you may want to change your find to this:
[[ -z `find /this/is/a/path/ -name core.* -print -quit` ]]
which will stop after the first core file found. Without that, if the root directory is large, the find could take a while.
Here's my version. :)
[ -z "$(find /this/is/a/path/ -name 'core.*')" ] && true
Edited for brevity:
[ -z "$(find /this/is/a/path/ -name 'core.*')" ]
There are probably many variants, but this is one:
test $(find /this/is/a/path/ -name core.* | wc -c) -eq 0
Perhaps this
find /this/is/a/path/ -name 'core.*' | read
I am using this:
if test $(find $path -name value | wc -c) -eq 0
then
echo "has no value"
exit
else
echo "perfect !!!"
fi

Resources