1

I'm having a problem here on a bash script I made. In a for loop, I iterate on all the arguments to construct a string variable that is later fed to an "eval" command:

    for arg in "$*"
    do
        if [ $arg != $lastArg ]; then
            findTarget+="-name $arg -o "
        else
            findTarget=$(echo $findTarget | sed 's/-o$//')
            break
        fi
    done

The problem stems from the "$*". For example when I enter "*.c" in the arguments, and the current folder contains files that match that pattern, the *.c argument is expanded into those files; I do not want that, I want findTarget to be concatenated with -name *.c -o, I have tried with and witout quotes, using eval, nothing seems to work. Any idea how to do this (simply if possible) ? Note: the total number of arguments can vary.

This is an example of how I run the script:

$ trouver.bash *.c *.f90 someString

At the end of my for loop, the variable findTarget should read -name *.c -o -name *.f90

This does not work if the *.c or *.f90 match files in the current folder...

  • if you paste your script in shellcheck.net, you'll see this: "$*" Since you double quoted this, it will not word split, and the loop will only run once – Sundeep Oct 5 '16 at 8:58
  • @Val, can you edit your post to show how you are running the script, and what the final command that you're running, is? Also check the formatting help in the edit window, esp. the code blocks (the {} symbol in the edit toolbar) – ilkkachu Oct 5 '16 at 9:01
  • "$*" is the concatenation of the positional parameters, see How to use arguments like $1 $2 … in a for loop? – Stéphane Chazelas Oct 5 '16 at 9:07
1

The syntax to loop over the positional parameters is for arg do alone. "$*" is the concatenation of the positional parameters with the first character of $IFS, so you would be looping over one element only.

Also, if you want to build a list of arguments for the find command, you need an array, not a string. And don't forget to quote your variables!

So:

findTarget=()
or=()
for arg
do
    [ "$arg" = "$lastArg" ] && break
    findTarget+=("${or[@]}" -name "$arg")
    or=(-o)
done

find . \( "${findTarget[@]}" \)

Note that when you invoke your script, you need to quote the *.c... patterns as otherwise they would be expanded by the shell before being passed to the script.

trouver.bash '*.c' '*.f90' someString

If your interactive shell is zsh, you can define an alias for your command where globbing is disabled with:

alias trouver.bash='noglob trouver.bash'

That way, you can do:

trouver.bash *.c *.f90 someString

without the shell expanding those *.c *.f90 globs.

  • Thank you very much, your elegant solution solved the problem. Single-quoting the arguments seems reasonable. My interactive shell is in bash too, so the noglobcommand is not available. I could do `alias trouver.bash='set -o noglob; trouver.bash' but then the noglob option would stay on after running the script... But that's another issue, you did solve my problem swiftly ! – Valentin B. Oct 5 '16 at 10:58
  • I found how to execute just the script in noglob mode without tempering with the options after running my script: alias trouver='set -f;func';func(){ command ~/trouver.bash "$@";set +f;}. After running my aliased command, set -o | grep noglob shows the option is still off ! – Valentin B. Oct 5 '16 at 12:57
  • @ValentinB., yes though that won't work properly if you do trouver *.txt | grep ... or trouver *.txt &, or xxx && trouver.sh whatever. (it also unconditionally restores globbing (even if it was turned off beforehand)). – Stéphane Chazelas Oct 5 '16 at 13:08
  • Yes I am well aware of that! But this should roll fine for me, as I don't usually turn noglob on, this script is meant to be used by me only and the output is not supposed to be piped (the script does an -exec grep "someString" .... already and outputs everything nicely with colors). But thank you again for pointing it out ! – Valentin B. Oct 5 '16 at 13:41
1

The use of "$*" means to join all arguments into a long one. Changing it to "$@" should give a list of arguments. But there is no real need to use for arg in "$@" as for arg is the correct idiom.

Then, to add a -o for each new argument we may use ${findTarget[@]+"-o "}.
It will be null if no value has been assigned to findTarget and will be the plain string -o if findTarget has any value assigned. In the first loop run it will collapse to nothing, in the next runs it will become -o.

(( $# < 1 )) && echo "please provide some argument(s)" && exit 1

unset  findTarget
for    arg
do     [[ "$arg" == "$lastArg" ]] && break
       findTarget+=("${findTarget[@]+"-o "}-name $arg")
done

find . \( "${findTarget[@]}" \)

You need to remember to run the script as this:

$ trouver.bash '*.c' '*.f90' someString

as you want that each argument gets not expanded by the shell (use single quotes. double quotes may work but do not express the intent to keep the arguments without expansion as clearly).

If you need to use the script without the "single quotes", you may remove the "Pathname expansion" (set -f) to avoid the expansion of *.

$ ( set -f; trouver.bash *.c *.f90 someString )

The () will avoid that the change to noglob affects the present running shell.

That works because the script will write the results to stdout, which also work printing from inside the sub-shell (…).

  • Thank you for you additional info on bash arrays. Your answer is pretty much identical to the one Stéphane gave, with an alternate way to append the -o to each argument ! If you look in the comments on Stéphane's post, I searched for and found a way of aliasing the script so that it runs with noglob on and restores it to off when done. Running it in a subshell is a good, cleaner idea though. – Valentin B. Oct 6 '16 at 9:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.