2

Let's say a have a following command

search /home/user proc .h .c .txt ...

I am building a script with find command to get all files that start with given name and end with one of given extensions.

I've managed to build this using a loop:

directory=$1
fileName=$2
fileExtensions=""

for arg in "$@"
do
    #skipping first and second argument
     if [ $arg = $1 -o $arg = $2 ]; then 
        continue;
    fi
    fileExtensions+="${arg//.}\|"
done
#removing the last '|', otherwise regex parser error occurs
fileExtensions=${fileExtensions::-1}

find $directory -name "$fileName*" -regex ".*\.\($fileExtensions)"

Is there more elegant way of achieving this using regex?

Thank you for your help!

2 Answers 2

2

The script could be reduced to:

directory=$1
fileName=$2
shift 2

a="$*"    b="${*#.}"
(( ${#a} - ${#b} - $# )) && echo "some extension(s) is(are) missing a leading dot." >&2

fileExtensions="$(IFS=\|; echo "${*#.}")"

find "$directory" -name "$fileName*" -regextype posix-egrep -regex ".*\.($fileExtensions)$"

By default, find accepts regex of type emacs, to use that type several backslashes are necessary, like \|. That could be avoided by using a different type of regex (as above).

Where ${*#.} removes the leading dot (if it exists) and join all the remaining "Positional Parameters" with the value of the first character of IFS, which was set to be | for the execution of the subshell.

Just one variable assignment with one "Parameter Expansion", that's all that's needed.


EDIT
The (( ${#a} - ${#b} - $# )) is used to check whether all the extensions provided in the argument list start with a dot.

The character count (${#a}) of all arguments concatenated (a=$*)
should be equal to
the character count (${#b}) of all arguments concatenated (one leading dot removed) (b=${*#.})
plus
the number of arguments ($#).

${#a} == ${#b} + $#

If and only if all arguments have one leading dot.

As an arithmetic test:

((  ${#a} - (${#b} + $#)  ))

Or also:

((  ${#a} - ${#b} - $#  )) && echo "missing leading dot(s)."

Edit II

The list of extensions given in the command line arguments is processed here:

fileExtensions="$(IFS=\|; echo "${*#.}")"

Here is how it works, inside out:

$*   # This generates a string of all positional arguments using IFS.

From LESS=+'/Special Parameters' man bash:

That is, "$*" is equivalent to "$1c$2c...", where c is the first character of the value of the IFS variable.

Then we use "parameter expansion" to cut the front of each positional argument:

${parameter#word}     # used as ${*# } above.

From LESS=+/'parameter#word' man bash:

If parameter is @ or *, the pattern removal operation is applied to each positional parameter in turn, and the expansion is the resultant list.

The word in the previous expansion is set to a dot ., thus removing the dots in the front of all positional parameters.

As the IFS at that point starts with an | character, that is used to build a string with | as the separator for the parameter list that has been striped of a dot in the front.

That string is provided to the command echo to make it printed.
But before the echo command is executed, the variable $IFS is set to a |.

That is wrapped inside a command execution $(…) (to create a sub shell that will forget the change to IFS when it ends).

Then we assign the string to a variable:

fileExtensions="$(IFS=\|; echo "${*#.}")"

In short: that transforms .c .h .txt into c|h|txt.

3
  • Sorry for bringing this back, but I try to understand how "$(IFS=\|; echo "${*#.}")" works. I know what it does, but how does it do it? I know we launch this in a subshell and we replace the dafault internal field separator with |, but why then we call echo "${*#.}")" and what each character in ${*#.} mean/do?
    – Dandry
    Nov 17, 2016 at 21:17
  • 1
    @Dandry Read my Edit II. Does it clear your questions?
    – user232326
    Nov 17, 2016 at 23:49
  • I wish I could've upvoted you more. Thank you so so much!
    – Dandry
    Nov 18, 2016 at 10:58
2

That seems the regex should work, and the only other option I can come up with is something like -name "proc*" \( -name "*.c" -o -name "*.h" ... \) which probably isn't that much simpler.

Some small things that could be done:

1) You can use shift to get rid of $1 and $2, instead of the conditional inside the loop.

2) You could combine the -name and -regex, so that in the end, you'd have -regex ".*/proc.*\.(c|h|txt)"

3) If you want to make the code shorter, you can join the positional parameters with IFS and $*:

$ set .c .h
$ IFS='|'
$ echo "(${*/./})"
(c|h)

(I'm not saying anything about readability or elegance.)

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .