2

I'm in a directory that contains many folders like these:

At_5.2000_displacement
At_-6.4000_displacement
At_2nd_-4.3000_displacement
At_2nd_2.2000_displacement

I am writing a bash script that is cd-ing to each of the folders of the type:

At_X.XXXX_displacement

i.e., I would like to

cd At_5.2000_displacement and cd At_-6.4000_displacement folders. This leaves out the folders of the type:

At_2nd_X.XXXX_displacement

My attempts (Edited):

I found the reg-ex that yields the name of the target folders, which is the following:

At_-\?[0-9][.].*displacement

which yields:

At_5.2000_displacement
At_-6.4000_displacement

Now, in the bash script, every time I do:

cd /path_to_the_folders/At_-\?[0-9][.].*displacement

There is no way of accessing each folder, since the error received is:

trial_cding.sh: line 11: cd: /path_to_the_folders/At_-?[0-9][.].*displacement: No such file or directory

How can I make the line cd /path_to_the_folders/At_-\?[0-9][.].*displacement to work ?


The implementation of @dawg's answer via the if statement (not using the shortcut), as far as I understood, would be something like:

for pn in *; do

    if [[ ! -d "$pn" && $pn =~ At_[0-9.-]*_displacement ]]
    then
        echo No desired directory found 
    else
         continue

    cd $pn
    pwd
    cd -
    fi
done

However, this returns no results. What am I getting wrong here?

Also, the I am not quite sure how to use parenthesis in cd $pn in order to avoid going back


Regarding the [0-9.-] match...

In the end we are looking for positive or negative decimal numbers. In other words, we are looking for any number from (-)0 to (-)9, and followed by a .

This makes me think that [-0-9.] is the most intuitive instruction.

Surprisingly, it happens that the following three also do match:

[0-9.-]
[0-9-.]   
[-.0-9]

Please check https://regex101.com/r/G3fXo5/1, where I show the matches. This makes me think that the matching criteria inside [ ] is quite broad.

So, I f I try [.-0-9] I get no matching results. Why is this happening? What is the rule behind the matching inside [ ] ?

DavidC.
  • 669
  • 8
  • 26
  • you probably want to `cd $i` after using grep or awk to do your conversion – Gem Taylor Jan 04 '18 at 17:12
  • What's wrong with `cd $i`? –  Jan 04 '18 at 17:35
  • @Arkadiy Thanks for your comment. Sorry, I didn't explain it correctly - see edited post. – DavidC. Jan 04 '18 at 17:48
  • The regex`[]` is any **single** character from the set of characters inside the braces called a *character class*. Be careful with the `-` however since in a character class brace it defines a range. `[0-9]` is any single ascii integer in the range of `0123456789` `[09-]` is different; that is any single character of `0`, `9` or a literal `-` Once you define the set of characters, then you consider the repetition of any characters in any order from the class. `[0-9]+` matches 1 or more digits in any order. – dawg Jan 05 '18 at 04:03
  • The regex of `[.-0-9]` is equivalent to `[./0\-9]` since you can only have one range defining `-` in a character class and the first `-` is used to define `./0` from the ascii table. The second is taken as a literal dash. Play with [regex101](https://regex101.com/r/2LEuiG/1/) and note the explanation at the side. – dawg Jan 05 '18 at 04:05

3 Answers3

4

Assuming X can be only a number, you can use extended globbing:

shopt -s extglob nullglob
for folder in At_?(-)[0-9].[0-9][0-9][0-9][0-9]_displacement/; do
    #do stuff here
    echo "$folder"
done
  • shopt -s nullglob is to ensure you won't loop if there are no files matching your pattern
  • shopt -s extglob enables extended globs
  • ?(-) matches zero or one occurrence of -. In extended globs, the modifier comes before (pattern). See: Pattern matching

Advantage of this approach is looping only over the folders you really want. No additional checks needed.

PesaThe
  • 7,259
  • 1
  • 19
  • 43
  • *No additional checks needed.* Your glob would loop over file types other than folders (files, links, named pipes, sockets, etc.) so you do still need to test that an extended glob is actually a directory. – dawg Jan 04 '18 at 21:07
  • @dawg that's what the `/` is for. – PesaThe Jan 04 '18 at 21:12
1

Given:

$ ls -l
total 0
drwxr-xr-x  2 dawg  wheel  68 Jan  4 10:42 At_-6.4000_displacement
drwxr-xr-x  2 dawg  wheel  68 Jan  4 10:42 At_2nd_-4.3000_displacement
drwxr-xr-x  2 dawg  wheel  68 Jan  4 10:42 At_2nd_2.2000_displacement
drwxr-xr-x  2 dawg  wheel  68 Jan  4 10:42 At_5.2000_displacement
-rw-r--r--  1 dawg  wheel   0 Jan  4 10:51 file

You can use a regular Bash glob *, test to see each glob is a directory -d (vs some other OS object) and then test the string with a Bash regex:

for pn in *; do   # You could use At_*_displacement glob to narrow if desired...
    [[ -d "$pn" && $pn =~ At_[0-9.-]*_displacement ]]  || continue
       # ^^                           a directory?
       #        ^^                    and
       #                ^     ^^      match this regex
       #        OR                                     ^   
       # continue (skip) to the next glob pattern in loop     ^^
    # do you Bash thing on this directory...
    # you can cd "$pn" or operate on the directory directly
    # ( use parenthesis for a sub shell and you don't need to cd back )
    echo "$pn"
done    

Prints:

At_-6.4000_displacement
At_5.2000_displacement

You can also use find with appropriate depth qualifiers and a regex:

$ find . -type d -maxdepth 1 -regex '\./At_[0-9.-]*_displacement'
./At_-6.4000_displacement
./At_5.2000_displacement

And then either use exec {} or xargs or feed that output to a Bash while loop.


For your last edit, something like this:

for pn in *; do
    if [[ -d "$pn" && $pn =~ At_[0-9.-]*_displacement ]]
    then
        ( # the ( creates a subshell so no need to cd back...
        echo "Found \"$pn\"! Touching it!"
        cd "$pn"   # USE QUOTES!
        # you are now in that sub directory
        touch "dawg was here!"   # create a file in the directory...
        )
        # exit sub shell -- back to the original directory
    else
         echo "\"$pn\" is not what we are looking for..."
    fi
done

Be sure to use "$quotes" around expansions in Bash.

dawg
  • 98,345
  • 23
  • 131
  • 206
  • You might need to tweet the regex to get *exactly* what you want, but you get the idea. – dawg Jan 04 '18 at 19:14
  • 1
    Thanks for your answer. I am trying to understand the following line in your code: `[[ -d "$pn" && $pn =~ At_[0-9.-]*_displacement ]] || continue`. Does this mean: if the variables `pn` I am looping over are directory type (`[[ -d "$pn"`) and (`&&`) the variable `pn` is (`=~`) blah blah, then (`||`) continue the script ? – DavidC. Jan 04 '18 at 20:32
  • 1
    The `||` operator only tests the right hand if the left hand is false. So `[[ test ]] || continue` will execute the `continue` on the RH if `[[ test ]]` is false. So if `$pn` is not a directory or the regex does not match, `continue` is called. It is a shortcut to `if [[ ! test ]] [ do this part ] else continue_the_loop` – dawg Jan 04 '18 at 20:38
  • Thanks a lot for the clarification. I am trying to implement the `if` statement instead of the shortcut, but for some reason I am not sure what I am getting wrong, (and the issue with the parenthesis). Please see updated post. Thanks again – DavidC. Jan 04 '18 at 21:21
  • Thank you so much for the explanation. I also totally agree with the use of quotes to avoid confusion. I am trying to understand the `[0-9.-]` match and how flexible it is. Please see updated post. Thank you so much – DavidC. Jan 04 '18 at 22:03
  • Quotes are beyond confusion to Bash however. For example `rm f1 f2 f3` is very different than `rm "f1 f2 f3"` The first is three files and the second is one file with spaces. You did not have quotes so spaces in file names will break the logic. – dawg Jan 05 '18 at 04:09
0

cd cannot change to more than one directory at a time. You need a for loop.

How about this:

for i in `ls | grep "^At_-\?[0-9][.].*displacement$"`; do
    cd $i
    # do what needs to be done
done
  • 1
    [Why you shouldn't parse the output of ls](https://mywiki.wooledge.org/ParsingLs). – PesaThe Jan 04 '18 at 18:06
  • @PesaThe In theory, in the most general case, you are correct. In practice, in most cases, this works. Especially for the OP who already used grep to test the regexp on his directory. –  Jan 04 '18 at 18:09
  • Your code will match `At_5.2000_displacement_ouch`. You should consider using `-x`. However, that will still fail for `` in the filename: `At_5.2000_displacementouch`. It works in most cases is not the right spirit :) – PesaThe Jan 04 '18 at 18:13
  • @PesaThe I just prefer to avoid extended functionality (including setting of options) w/o a good reason. –  Jan 04 '18 at 18:17
  • I think it's better to use extended functionality (why not use `extglob`) instead of a code that works in *most cases*. But still, you should probably use the `-x` option :) – PesaThe Jan 04 '18 at 18:19
  • The reason not to use the extended functionality in this case: you need to remember to turn it off, or any other glob like `*.txt` in the script will break. `-x` option or `^...$` in regexp sounds right, agreed. –  Jan 04 '18 at 18:22
  • @Arkadiy extglob will not break or change the meaning of standard glob expressions like `*.txt`. extglob only adds new subexpressions, all of which include parentheses, which would cause syntax errors in most contexts without extglob. See "[Why would I not leave extglob enabled in bash?](https://stackoverflow.com/questions/17191622/why-would-i-not-leave-extglob-enabled-in-bash)". – Gordon Davisson Jan 04 '18 at 18:47
  • So grepping `ls` output introduces problems with some rarely found data, while keeping `extglob` on introduces problems with some rarely found patterns. :) In any case, thanks for teaching me about `extglob` - I'll keep it in my toolkit. –  Jan 04 '18 at 18:54