Calling a bash function isn’t working as it should to change to recent directory


So I expect my function to change to a recently modified directory.

This is it:

function cdrc { echo 'cd "$(ls -t | HEAD -1)"'; }

When I want to change to a recent dir:

$~ cd Desktop/Folder
$~ cdrc

I get:

-bash: Desktop: Command not found

Best Answer

Why your function is not working

There are few reasons:

  • I guess echo is an artifact used for testing the syntax or so. It makes no sense if you want the function to do what you described.
  • Uppercase HEAD. In Linux it should be head. I'm not sure about other OS-es where you can run bash and head. HEAD may or may not work in some of them but head should work everywhere.
  • Parsing ls is not recommended. There is an article about it. The main point in your case would be that ls cannot reliably print names including special or unprintable characters.
  • There's no logic to test directories only, you may end up trying to cd to a file when there's no directory.
  • There's no logic to handle the situation when the current working directory is empty.

All these issues can be debugged except this parsing ls thing. It's a design flaw. If you think ls limitations won't bite you then you can go with a solution from this another answer.

To do some tests you may create a troublesome directory with mkdir "$(echo -ne "foo\nbar")"; ls-based solutions will probably fail if this is the directory cdrc should cd to. To remove the troublesome directory invoke rmdir "$(echo -ne "foo\nbar")".

I managed to create a safer function.


function cdrc { cd "$(find -maxdepth 1 -mindepth 1 -type d -exec stat --printf "%Y %n\0" {} + | sort -znr | head -zn 1 | cut -f 2- -d " ")" ;}


To explain my function I shall write it more clearly. Note a \ at the very end of a line tells bash the command continues in the next line; so my code below is treated as a one-liner, it can be pasted as a whole to interactive bash.

function cdrc { \
  cd "$( \
    find -maxdepth 1 -mindepth 1 -type d -exec \
      stat --printf "%Y %n\0" {} + |
    sort -znr |
    head -zn 1 |
    cut -f 2- -d " " \
  )" \

The procedure is as follows:

  • At first find is run. It doesn't descend to subdirectories (-maxdepth 1), it doesn't find the current directory either (-mindepth 1). It finds directories only (-type d). Then the stat command is run (thanks to -exec):
    • stat prints time of last data modification (%Y, mtime, seconds since Epoch), single space and name (%n). Due to --printf option it doesn't add the newline character but interprets \0 as a null character that should be added at the end of every line.
    • {} is a part of find -exec syntax. During find execution it is replaced by directory name so stat knows what its target is.
    • + is also a part of find -exec syntax. It causes find to pass multiple names to single stat (and stat can handle it). This way fewer stat processes are created, it's faster.

At this moment we have zero or more lines. They look similar to this:

1493488341 directory name
1497365306 troublesome?directory name

but they are null-terminated, so even if there are names with troublesome characters, they will be handled properly. In the first column there are mtimes without leading spaces (I checked stat behavior with numbers of various length to be sure), then the first space separates mtime from directory name.

  • This output is processed furhter:
    • sort sorts lines according to numerical value (-n), uses reverse order (-r) and works with null-terminated strings (-z). This way the directory we need is now in the first line.
    • Then head leaves the first line only (-n 1); it's also told to work with null-terminated strings (-z).
    • cut cuts the line, treating space as delimiter (-d " ") and leaving the second field and everything that follows (-f 2-), i.e. everything after the first space. It works with null-terminated strings (-z). The final output is the desired directory name.

Note the output will be empty if there's no directory in the current working directory.

  • $(…) is replaced by the output of everything that is inside. At this moment we have either cd "some directory name" or cd "". The former command does what you want; the latter (when there's no directory) does nothing.

The function will fail if the directory it's supposed to cd to is (re)moved/renamed after find finds it. Also stat may throw error(s) if any directory is (re)moved/renamed when the function works.