How do I use parens '()' in a find command when building options from array? - bash

I have a function that looks like this. I have stripped error handling, and the commands outside the function are to make sure I have something to look for in the example.
#!/bin/bash
findfiles() {
local path=$1
local mtime=$2
local prunedirs=$3
local -a fopts
fopts+=("$path")
[[ -n $prunedirs ]] && {
fopts+=('-type' 'd')
fopts+=('(' '-path')
fopts+=("${prunedirs// / -o -path }")
fopts+=(')' '-prune' '-o')
}
fopts+=('-type' 'f')
fopts+=('-writable')
fopts+=('-mtime' "+$mtime")
[[ -n $prunedirs ]] && fopts+=('-print')
echo "find ${fopts[*]}"
find "${fopts[#]}"
}
mkdir -p dir1/{dir2,dir3}
touch dir1/5daysago.txt -mt "$(date -d 'now - 5 days' +%Y%m%d%H%M)"
touch dir1/dir2/6daysago.txt -mt "$(date -d 'now - 6 days' +%Y%m%d%H%M)"
touch dir1/dir3/10daysago.txt -mt "$(date -d 'now - 10 days' +%Y%m%d%H%M)"
echo '---------------------------------------------'
findfiles dir1 4
echo '---------------------------------------------'
findfiles dir1 4 'dir1/dir2'
echo '---------------------------------------------'
findfiles dir1 4 "dir1/dir2 dir1/dir3"
This outputs the following:
---------------------------------------------
find dir1 -type f -writable -mtime +4
dir1/dir2/6daysago.txt
dir1/dir3/10daysago.txt
dir1/5daysago.txt
---------------------------------------------
find dir1 -type d ( -path dir1/dir2 ) -prune -o -type f -writable -mtime +4 -print
dir1/dir3/10daysago.txt
dir1/5daysago.txt
---------------------------------------------
find dir1 -type d ( -path dir1/dir2 -o -path dir1/dir3 ) -prune -o -type f -writable -mtime +4 -print
dir1/dir2/6daysago.txt
dir1/dir3/10daysago.txt
dir1/5daysago.txt
Notice that the third attempt does not prune the directories. If I copy and paste the find (escaping the parens) it works correctly.
$ find dir1 -type d \( -path dir1/dir2 -o -path dir1/dir3 \) -prune -o -type f -writable -mtime +4 -print
dir1/5daysago.txt
What am I doing wrong?

You need to add -o and the -path primary as separate array elements. Each directory to prune should be passed as a separate argument, not a single space-separated string.
findfiles() {
local path=$1
local mtime=$2
shift 2
n=$# # Remember for later
local -a fopts
fopts+=("$path")
if (( $# > 0 )); then
fopts+=(-type d '(')
while (( $# > 1 )); do
fopts+=(-path "$1" -o)
shift
done
fopts+=(-path $1 ')' -prune -o)
fi
fopts+=('-type' 'f')
fopts+=('-writable')
fopts+=('-mtime' "+$mtime")
# Now it's later
((n > 0)) && fopts+=('-print')
echo "find ${fopts[*]}"
find "${fopts[#]}"
}
findfiles dir1 4 "dir1/dir2" "dir1/dir3"

Change echo "find ${fopts[*]}" to declare -p fopts to unambiguously print the options. Doing so will show that the -o -path part is being added as a single word:
$ declare -p fopts
declare -a fopts=(
[0]="dir1" [1]="-type" [2]="d" [3]="(" [4]="-path"
[5]="dir1/dir2 -o -path dir1/dir3" [6]=")" [7]="-prune" [8]="-o" [9]="-type" [10]="f"
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[11]="-writable" [12]="-mtime" [13]="+4" [14]="-print"
)
To fix it you'll want to add each directory to prune to the array individually, something like:
local prunedirs=("${#:3}")
...
fopts+=(-type d '(' -false)
for dir in "${prunedirs[#]}"; do
fopts+=(-o -path "$dir")
done
fopts+=(')' -prune -o)
I've switched prunedirs to an array so it can handle directory names with whitespace.
It starts with an initial -false check so there's no need to check if prunedirs is empty. If it's empty the whole thing is still added but since it just says -type d '(' -false ')' -prune -o it's a no-op.
Also, notice you don't have to quote every single argument. It's fine to write -type d and such unquoted, the same as you would if you typed them at the command line. Only '(' and ')' need single quotes.

Related

Passing a date/time value to touch command within bash function

I am trying to write a bash function (Mac OS X) which searches for a specific range of files between two input date/times. Most variations I have tried for evaluating the two inputs $1 and $2 fail. Hardcoding the times works fine (as per usage line below) i.e. the search syntax is fine. Grateful for pointers where I'm going wrong on passing the two inputs to the touch commands.
function ffiles_search1 () {
echo "usage start 201911270000 end 201912102359 "
touch -t $(eval echo "$1") /tmp/lower-date && touch -t $(eval echo "$2") /tmp/upper-date && find . -path "./Library" -prune -o -type f -a -newer /tmp/lower-date -a ! -newer /tmp/upper-date -a -size +32k -a ! -size +1024k -print0 | xargs -0 ls -ld | egrep -iv "|ppt|doc"
}
Corrected code, invoking as
ffiles_search 201911270000 201912102359
on the function
function ffiles_search () {
echo "usage start 201911270000 end 201912102359 "
touch -t "$1" /tmp/lower-date &&
touch -t "$2" /tmp/upper-date &&
find . -path "./Library" -prune -o -type f -a -newer /tmp/lower-date \
-a ! -newer /tmp/upper-date -a -size +32k -a -size -1024k -print0 |
xargs -0 ls -ld |
egrep -iv -f $HOME/Scripts/egrep_exclusions/time_search.txt
}

How should I search a group of files using linux `find` command?

I have a group of files in a certain directory. Now I want to search them in two different directories. I used below code:
(jumped to the directory that contains that group of files)
ls | while read name; do find ~/dir1 ~/dir2 -name {$name};done
But I guess it is too slow since for each file, dir1 and dir2 should be searched once. So the search will be do too many times.
Is my guess right? and if so, what should I write?
find supports -o for OR operation.
You can use this:
files=(); # Initialize an empty bash array
for i in *; do files+=(-o -name "$i"); done # Add names of all the files to the array
find dir1/ dir2/ -type f '(' "${files[#]:1}" ')' # Search for those files
e.g., consider this case:
$ touch a b c
$ ls
a b c
$ files=()
$ for i in *; do files+=(-o -name "$i"); done
$ printf '%s ' "${files[#]}"; echo
-o -name a -o -name b -o -name c
$ printf '%s ' "${files[#]:1}"; echo
-name a -o -name b -o -name c
$ printf '%s ' find dir1/ dir2/ -type f '(' "${files[#]:1}" ')'; echo
find dir1/ dir2/ -type f ( -name a -o -name b -o -name c ) # This is the command that actually runs.

dont list files in the mentioned directories

I have three directories with sub directories the parent directory are a, b, & c. a1&a2 are subdirectories of a, b1&b2 are subdirectories of b, c1&c2 are subdirectories of c. The files are arranged like this format a.txt(a), a1.txt(a1), a2.txt(a2), b.txt(b), b1.txt(b1), b2.txt(b2), c.txt(c), c1.txt(c1), c2.txt(c2). Now I am trying to list the files like i want to ignore the files in the entire directory a with its sub directories a1&a2 and want to igonre the files only in the directory b1 not directory b or b2 and want to ignore the files only in the directory c2 not the directories c or c1. Script which I have tried is below
#!/bin/sh
find * -type d | while IFS= read d
do
dirname=`basename $d`
if [ ${dirname} != "a" ] || [ ${dirname} != "b1" ] || [ ${dirname} != "c2" ]
then
cd $dirname
find * ! -name . -prune -type f | while read fname
do
fname=`basename $fname`
echo $fname
done
fi
done
The results are of the above script is
a.txt
a2.txt
b.sh: a1: does not exist
May i know what is the mistake i am doing in it.
To reproduce test
mkdir -p {a/a,b/b,c/c}{1,2}
touch {a/a,b/b,c/c}.txt
for d in {a/a,b/b,c/c}{1,2}; do touch $d/${d#*/}.txt; done
find . -type f
./a/a.txt
./a/a1/a1.txt
./a/a2/a2.txt
./b/b.txt
./b/b1/b1.txt
./b/b2/b2.txt
./c/c.txt
./c/c1/c1.txt
./c/c2/c2.txt
The find command
find . \( -path ./a -o -path ./b/b1 -o -path ./c/c2 \) -prune -o -print
Explanation
-prune over a directory will prevent find to descend into it, is applied over condition between escaped parenthesis -o equivalent to -or, however it returns true on matching files, after the next -o conditions can be added, (by default junction is -a, -and which can be ommited) and action (-printf -ls -exec etc.).
Other example :
find . \( -path ./a -o -path ./b/b1 -o -path ./c/c2 \) -prune -type f -o -type f
./b/b.txt
./b/b2/b2.txt
./c/c.txt
./c/c1/c1.txt
EDIT after comment:
find . -type d \( -name a -o -name b1 -o -name c2 \) -prune -type f -o -type f -printf '%f\n'
b.txt
b2.txt
c.txt
c1.txt
EDIT after last comment (SunOS find doesn't have -printf) :
find . -type d \( -name a -o -name b1 -o -name c2 \) -prune -o -type f -exec sh -c 'for f; do d=${f%/*}; echo "${d##*/}" "${f##*/}"; done' sh-echo {} +
b b.txt
b2 b2.txt
c c.txt
c1 c1.txt

Code in bashrc doesn't work

The code below doesn't work in bashrc but works in terminal with other arguments null.
search () {
find $1 -type f | egrep '(.$2|.$3|.$4|.$5|.$6|.$7|.$8|.$9|.$10)'
}
Write this:
search() {
find "$1" -type f \( -true \
-o -name "*$2*" \
-o -name "*$3*" \
-o -name "*$4*" \
-o -name "*$5*" \
-o -name "*$6*" \
-o -name "*$7*" \
-o -name "*$8*" \
-o -name "*$9*" \
-o -name "*$10*" \
\)
}
As #chepner points out, the single quotes prevent the parameters from expanding. Use double quotes.
The egrep will create a line-based match result, which is less precise than the above. It's also slower.
If the above statements are not exactly what you need, keep in mind GNU find has regular expression predicates in addition to -name's pattern matching. There's no need to pipe to grep. You can expand the above function to take an unlimited number of arguments by constructing the arguments to find, such as in this answer.
I didn't know that the egrep get the literal text $2 instead of argument. I solved with this code:
search-type () {
case "$#" in
1) echo "Missing arguments";;
2) find $1 -type f | egrep '(.'$2')';;
3) find $1 -type f | egrep '(.'$2'|.'$3')';;
4) find $1 -type f | egrep '(.'$2'|.'$3'|.'$4')';;
5) find $1 -type f | egrep '(.'$2'|.'$3'|.'$4'|.'$5')';;
6) find $1 -type f | egrep '(.'$2'|.'$3'|.'$4'|.'$5'|.'$6')';;
7) find $1 -type f | egrep '(.'$2'|.'$3'|.'$4'|.'$5'|.'$6'|.'$7')';;
8) find $1 -type f | egrep '(.'$2'|.'$3'|.'$4'|.'$5'|.'$6'|.'$7'|.'$8')';;
9) find $1 -type f | egrep '(.'$2'|.'$3'|.'$4'|.'$5'|.'$6'|.'$7'|.'$8'|.'$9')';;
10) find $1 -type f | egrep '(.'$2'|.'$3'|.'$4'|.'$5'|.'$6'|.'$7'|.'$8'|.'$9'|.'$10')';;
11) echo "Many arguments";;
esac;
}
The #kojiro code doesn't work.
Is it possible to simplify this code with regex?
Thank you guys!
I change the code for the something more simple and clear; and works with any quantity of parameters.
search-type() {
# Flags
flag=0
fld=1
for x in "$#"
do
# The first parameter is the directory; ignored!
if [ $fld = 1 ]; then
fld=0
else
# Verify if have more than one file
if [ $flag = 0 ]; then
cmd='-name '$x
flag=1
else
cmd+=' -o -name '$x
fi
fi
done
find $1 -type f $cmd;
}

HandBrakeCLI command break while loop?

In a bash script, result of find is
/path/to/file1.nrg
/path/to/file2.nrg
/path/to/file3.nrg
i have this while loop:
process preset
processpreset ()
{
x=$1
# Replace , by -o -iname for file types.
iname=" -o -iname \*."
# Find specified files. Eval allow var prst1_in with find.
eval "find "$fpath" -type f \( -iname \*."${prst_in[x]//,/$iname}" \) -size ${prst_lim_size[x]}" | sort | while read -r i
do
titles=$(HandBrakeCLI --input "$i" --scan |& grep -Po '(?<=DVD has )([0-9]+)')
if (( $titles > 1 )); then
echo "DVD has $titles title(s)"
fi
done
}
the script only echo 1 time File has 8 title(s) after it stop, when using titles="8" the loop echo for all files in folder. Can anyone point me my error please?
EDIT: what work for me, many thanks Anubhava
processpreset ()
{
x=$1
# Replace , by -o -iname for file types.
iname=" -o -iname \*."
# Find specified files. Eval allow var prst1_in with find.
eval "find "$fpath" -type f \( -iname \*."${prst_in[x]//,/$iname}" \) -size ${prst_lim_size[x]}" | sort | while read -r i
do
titles="$(echo ""|HandBrakeCLI --input "$i" --scan |& grep -Po '(?<=DVD has )([0-9]+)')"
if (( $titles > 1 )); then
echo "DVD has $titles title(s)"
fi
done
}
the echo ""| fix the problem.
ok try this script:
while read -r i
do
echo "i is: $i"
titles="$(echo ""|HandBrakeCLI --input "$i" --scan | grep -Po '(?<=DVD has )([0-9]+)')"
if (( titles > 1 )); then
echo "DVD has $titles title(s)"
fi
done < <(find "$imgpath" -type f \( -iname \*.iso -o -iname \*.nrg -o -iname \*.img \) | sort)

Resources