14

I have written a small bash script to find if a directory named anaconda or miniconda in my user $HOME. But it does not find the miniconda2 directory in my home.

How could I fix this?

if [ -d "$HOME"/"(ana|mini)conda[0-9]?" ]; then
    echo "miniconda directory is found in your $HOME"
else
    echo "anaconda/miniconda is not found in your $HOME"
fi

P.S: If I have [ -d "$HOME"/miniconda2 ]; then, then it finds the miniconda2 directory so I think error lies in the part "(ana|mini)conda[0-9]?"

I want the script to be general. For me, it's miniconda2 but for some other user it might be anaconda2, miniconda3 and so on.

  • Another user might use anaconda_2 or -2 or -may2019. So wouldn't xxxconda* be better? – WinEunuuchs2Unix May 31 at 19:44
  • 2
    Bash filename expansion uses glob expressions, not regexes. – Peter Cordes Jun 1 at 11:31
13

This is a surprisingly tricky thing to do nicely.

Fundamentally, -d will only test a single argument - even if you could match filenames using a regular expression.

One way would be to flip the problem around, and test directories for a regex match instead of testing the regex match for directories. In other words, loop over all the directories in $HOME using a simple shell glob, and test each against your regex, breaking on a match, finally testing whether the BASH_REMATCH array is non-empty:

#!/bin/bash

for d in "$HOME"/*/; do
  if [[ $d =~ (ana|mini)conda[0-9]? ]]; then
    break;
  fi
done

if ((${#BASH_REMATCH[@]} > 0)); then
    echo "anaconda/miniconda directory is found in your $HOME"
  else
    echo "anaconda/miniconda is not found in your $HOME"
fi

An alternate way would be to use an extended shell glob in place of the regex, and capture any glob matches in an array. Then test if the array is non-empty:

#!/bin/bash

shopt -s extglob nullglob

dirs=( "$HOME"/@(ana|mini)conda?([0-9])/ )

if (( ${#dirs[@]} > 0 )); then
  echo "anaconda/miniconda directory is found in your $HOME"
else
  echo "anaconda/miniconda is not found in your $HOME"
fi

The trailing / ensures that only directories are matched; the nullglob prevents the shell from returning the unmatched string in the case of zero matches.


To make either recursive, set the globstar shell option (shopt -s globstar) and then respectively:-

  • (regex version): for d in "$HOME"/**/; do

  • (extended glob version): dirs=( "$HOME"/**/@(ana|mini)conda?([0-9])/ )

  • 1
    I'd go the array route. You can use ?([0-9]) in place of @(|[0-9]) -- ?(...) matches zero or one, same as the regex ? quantifier. – glenn jackman May 31 at 19:20
  • 2
    You don't even need extglob is you use brace expansion (this generates all possible matching names): ~/{ana,mini}conda{0..9}*/ – xenoid May 31 at 19:43
  • Is there anyway to edit either of these solutions so that it will hold even if mini or anaconda is installed in $HOME/sub-directories? For example $HOME/sub-dir1/sub-dir2/miniconda2 – Jenny May 31 at 19:45
  • 1
    @Jenny please see my edit concerning globstar – steeldriver May 31 at 21:09
  • 1
    @terdon yeah I didn't really want to go down the rabbit hole of what's the "right" thing to match - I just took the OP's regex as-is for the purpose of illustrating a general approach – steeldriver Jun 1 at 13:01
9

Indeed, as already mentioned, this is tricky. My approach is the following:

  • use find and its regex capabilities to find the directories in question.
  • let find print an x for each found directory
  • store the xes in a string
  • if the string is non-empty, then one of the directories was found.

Thus:

xString=$(find $HOME -maxdepth 1 \
                     -type d \
                     -regextype egrep \
                     -regex "$HOME/(ana|mini)conda[0-9]?" \
                     -printf 'x');
if [ -n "$xString" ]; then
    echo "found one of the directories";
else
    echo "no match.";
fi

Explanation:

  • find $HOME -maxdepth 1 finds everything below $HOME but restricts the search to one level (that is: it doesn't recurse into subdirectories).
  • -type d restricts the search to only directories
  • -regextype egrep tells find what type of regular expression we deal with. This is needed because things like [0-9]? and (…|…) are somewhat special and find doesn't recognize them by default.
  • -regex "$HOME/(ana|mini)conda[0-9]?" is the actual regular expression we want to lookout for
  • -printf 'x' just prints an x for every thing that satisfies the previous conditions.
  • When there is a match. -bash: -regex: command not found found one of the directories – Jenny May 31 at 19:32
  • Hi PerlDuck: Thanks. A nice answer too. But I get an error for printf For example when I run the script, it runs ok but it doesn't find the printf command when there is no match but I think it's because there is nothing to print may be?. -bash: -printf: command not found no match. – Jenny May 31 at 19:37
  • 3
    @Jenny You may have made a typo when copying it, since it works fine for me. -printf is not a command but an argument to find. That's what the backslash at the end of the previous line does. – wjandrea May 31 at 20:18
  • 1
    I'd suggest -quit after printing the found path, unless you want to keep going to detect ambiguity. – Peter Cordes Jun 1 at 11:33
  • And why not print the actual path? You have it already, so it seems a shame to discard it and use x instead: foundDir=$(find $HOME -maxdepth 1 -type d -regextype egrep -regex "$HOME/(ana|mini)conda[0-9]?" -print -quit); echo "found $foundDir" – terdon Jun 1 at 12:34
2

You can loop over a list of directory names you want to test and act on it if one of them exists:

a=0
for i in {ana,mini}conda{,2}; do
  if [ -d "$i" ]; then
    unset a
    break
  fi
done
echo "anaconda/miniconda directory is ${a+not }found in your $HOME"

This solution obviously doesn’t allow for the full regex power, but shell globbing and brace expansion is equal at least in the case you showed. The loop exits as soon as one directory exists and unsets the previously set variable a. In the subsequent echo line, the parameter expansion ${a+not } expands to nothing if a is set (= no dir found) and “not ” else.

1

Possible work around is searching miniconda and anaconda separately as shown below

if [ -d "$HOME"/miniconda* ] || [ -d "$HOME"/anaconda* ]; then
    echo "miniconda directory is found in your $HOME"
else
    echo "anaconda/miniconda is not found in your $HOME"
fi

But if someone has suggestions, I'd like to know why we cannot pass a regex when searching for directories.

  • 2
    I upvoted this - but then realized it will break if the user has more than one matching directory (e.g. miniconda AND miniconda2) – steeldriver May 31 at 17:16
  • @steeldriver: "it will break if the user has more than one matching directory" Yes, thats's indeed true. Do you have any suggestions how to fix it? – Jenny May 31 at 17:34
  • @Jenny Use an array, like in steeldriver's answer. shopt -s nullglob; dirs=( "$HOME"/miniconda* "$HOME"/anaconda* ); if (( ${#dirs[@]} > 0 )); then ... – wjandrea May 31 at 20:25
  • If you replace ] || [ with -o it at least shouldn’t break if both directories are found as both directory globs are looked for in the same test. – Phoenix Jun 1 at 5:09
  • @steeldriver and Jenny: you might want it to break on ambiguity instead of just picking one. Make the user specify their directory instead of maybe picking the wrong one. (e.g. edit the script to set the dir name instead of running the auto-detection code.) – Peter Cordes Jun 1 at 11:37

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.