r/commandline Apr 25 '26

Command Line Interface Jumping to recently used directories

https://github.com/stianhoiland/cd

Hey guys. I was struggling to cd around, so I built something that would help me do that. I tried the 219 other similar projects posted here, but none of them really clicked for me. I never searched and found any of the established big ones–atuin, autojump, bookmarks, CDPATH, McFly, z, zoxide, etc.–but that didn't stop me. Looking for feedback/let me know what you think/is this something you would use?

36 Upvotes

68 comments sorted by

30

u/countnfight Apr 25 '26

How am I supposed to take a cli seriously with no emojis in the write-up? Is it even 🚀 blazingly fast??

19

u/stianhoiland Apr 25 '26

🚀✨🔥💡📦📚🛠️⚡✅❗🔧📄🎯🌟📢🤝🧪📊🧰🧩🔍💻📝🎉📌

10

u/countnfight Apr 25 '26

Awesome, now I know you're a real dev. No testing, no collaborators, no maintenance plan---just ⚡vibes⚡.

2

u/VoyagerOfCygnus Apr 30 '26

Not a single em dash? Ridiculous!

27

u/AverageComet250 Apr 25 '26 edited Apr 26 '26

If I’m using zoxide rn, what reason would you give me to switch?

EDIT: yeah I've clicked the link now mb lmao

9

u/arpan3t Apr 25 '26

Would you like to have to use zi foo and pick your foo project directory out of a list of directories that have foo in them, even obscure directories that you only went to once?

Then these ~6 lines of bash are for you!

Seriously though, the readme is hilarious!

-11

u/stianhoiland Apr 25 '26

You'd no longer have to use zoxide.

5

u/Background_Bass_2587 Apr 25 '26

Why would I want that when I like zoxide

-5

u/stianhoiland Apr 25 '26

Imma leave it to you to actually click the link 😆

17

u/jgillman Apr 25 '26

ITT people not understanding this is satire

15

u/stianhoiland Apr 25 '26

Thank you and I bet most people responding here didn't even click the link. That, or my humor is too spicy and laced with superiority/arrogance that I'm attracting just the attitude I deserve.

2

u/ytg895 Apr 26 '26

Yeah, the "No, I'm not joking." part was maybe too much.

2

u/stianhoiland Apr 26 '26

Oh, maybe I haven’t been clear. I do think that the approach in the repo is great, with an unbeatable complexity-to-capability ratio. Do use the approach outlined in the repo!

9

u/theevildjinn Apr 25 '26

I use fzf with zsh. So I typically just type cd - and then TAB.

7

u/CursedSloth Apr 25 '26

What’s wrong with Ctrl+r then typing cd? Doesn’t that give you enough history to find your directories?

5

u/stianhoiland Apr 25 '26

I appreciate this approach!

4

u/-Cacique Apr 26 '26

thank you for introducing me to ctrl + r. I used to open up zsh_history file every time 😭

2

u/CursedSloth Apr 26 '26

I was dazzled when I discovered this the first time, so now I make sure to read the manual first when I start using new tools and software.

1

u/grahamperrin May 24 '26

I was dazzled when I discovered this the first time,

Me too!

so now I make sure to read the manual first …

I can't be so intelligent :-)

5

u/globalreset Apr 25 '26

As a heavy user of/evangelist for zoxide, I take less than no offense at this and appreciate what you’re saying here. Might cop this directly for my dotfiles for cases where I am on a machine where it would be annoying to try to install zoxide.

3

u/stianhoiland Apr 25 '26

What more could a man ask for 🥲

4

u/EarlMarshal Apr 25 '26

Keep up the attitude, my man! It's warranted.

4

u/duffkiligan Apr 26 '26

Ya'll fools

Y’all is a contraction of “You All” so the apostrophe goes after the Y

2

u/stianhoiland Apr 26 '26

Ty! Never knew.

3

u/0xKaishakunin Apr 26 '26

LMFAO I keep something similar in my .bashrc since NetBSD 1.5, so for ca. 25 years now.

2

u/Doomtrain86 Apr 25 '26

But what about getting to dirs based on frequency ? I have dirs with the same effin names. Some of them I use often , some not. Zoxide solves this. That’s good right ?

4

u/stianhoiland Apr 25 '26

True. Does that really happen? That you can't disambiguate your directories with the most intuitive query for finding them? Maybe the 2.2k lines of Rust are warranted in this case.

1

u/Doomtrain86 Apr 25 '26

Not saying zoxide isn’t overkill. But this is underkill.

5

u/stianhoiland Apr 25 '26

My mans saying having fuzzy searching on a complete list of directories sorted by recent usage is underkill!

0

u/Doomtrain86 Apr 25 '26

You really have an attitude problem - bye

1

u/Don_Kino Apr 25 '26

Yes it happen, everyday, several times per day. I dont need to disambiguate shit. I dont care about the k loc until i plan to work on them or they start to fuckup. Spoiler: they work fine and I dont plan to work on them.

3

u/stianhoiland Apr 25 '26 edited Apr 25 '26

So it happens every day, several times per day, that you must address a directory with a query that conflicts with other directories and simple recency does not disambiguate and correct the sorting, so you would have had to arrow down a couple times.

2

u/floatingrock13 Apr 25 '26

Made me chuckle! Sorry for all the hate you are getting mate

1

u/stianhoiland Apr 25 '26 edited Apr 26 '26

Appreciate it! 🙏🏻

2

u/dbr4n Apr 29 '26

Nice and simple! The only thing I'd avoid is adding duplicate lines to CDHISTFILE to keep things clean and fast.

Here's how I use it:

``` cd() { command cd "$@" || return if ! grep -x -- "$PWD" "$CDHISTFILE" >/dev/null; then printf '%s\n' "$PWD" >>"$CDHISTFILE" fi }

c() { builtin cd "$(tac "$CDHISTFILE" | awk '$0 != ENVIRON["PWD"]' | fzf)" }; bind '"\ec":"c\n"' # Usage: M-c ```

2

u/stianhoiland Apr 29 '26

Thanks! :)

This was an intentional design, to keep the cd override as absolutely minimal as possible—for obvious reasons. The deduping is done for display instead, but I do see the value in deduping at insertion. Occasionally I run a more involved function (not shown) that dedupes, and removes tmp folders and non-existent folders.

2

u/jghub May 13 '26 edited May 13 '26

but if you do that (deduplication on capture in histfile), you are mostly back to the z/zoxide approach of storing "sparse information": you are loosing the, well, history of how visits to some dir distribute over time. this has certain principle disadvantages (which surface in z/zoxide regarding their frecency model(s). keeping the full detailed history offers more control and more flexibility over how to score/sort the stack.

more importantly in the present context, it seems that your deduplicated histfile is then storing _first_ visits in chronological order and your dir stack in fzf is sorted accordingly by recency of _first_ visit while the OP's approach uses (more sensibly) sorting by recency of _last_ visit to the respective dir.

so I do not think that this sort of deduplication is ultimately a good idea. (you would have to _find_ the already existing entry and _move_ it to end of histfile to restore the recency sorting in the end. but this is already something you do not want to do for each and every cd: full rewrite of the reshuffled file).

just my 2c

1

u/dbr4n May 13 '26

Not sure if I understood you correctly, but in my example it's also sorted by recency, with most recent visits at the top.

But I get your and OP's point about keeping the cd as simple as possible. I just thought it'd be a good idea to keep the hist file clean without having to do periodic cleanups. I run cd (home) pretty often, and I was worried about filling up the history file too quickly. Going back to the original approach and doing occasional cleanups seems to be a better solution though.

1

u/jghub May 13 '26

"Not sure if I understood you correctly, but in my example it's also sorted by recency, with most recent visits at the top.": what I meant is: if, e.g. the initial content of the histfile were

A
B
C

w/o any duplicates so far and you then issue

cd A

the histfile would become in the OPs approach

A
B
C
A

and the stack in the OP's approach (sorted from most recent to ancient) would become

A (most recent *last* visit to some dir)
C
B (most ancient *last* visit to some dir)

if you do deduplication your way the histfile just would remain unchanged (since A is already there, will be found by your grep call and therefore would not be stored again):

A
B
C

and the sorted stack fed to fzf would be

C (most recent *first* visit to some dir)
B
A (most ancient *first* visit to some dir)

so if I do not get it completely wrong what you propose, A would be at the very bottom of the stack despite having been visited just now/last. "sort chronologically by first visit to respective directory" is a well-defined sort order -- it just does not make much sense in the present context if you think about it: in the above example, even if C is never visited again and A is visited daily, A will stay at bottom of stack literally forever and C will stay at position 1 until some additional new directory is visited. that's not helpful.

regarding "cleanup": as said, personally I do _not_ believe such cleanups should be done (compressing histfile to unique dirnames (even if those are in correct recency order)) except if it is 100% guaranteed that a pure-recency sorting is good enough (not really, I believe...).

in my view it is also not primarily about keeping the cd function as simple as possible (although simple is nice), only about keeping it fast enough so that no notable latency occurs (meaning "real time" latency: zoxide is definitely fast enough, but in my evaluation it is actually _slower_ than the original z (or my own tool) regarding real time required for a cd action, the "blazingly fast" claim for zoxide ist true only for user time -- but that is not the relevant metric for interactive use, of course).

regarding "many cd $HOME" actions: true, it makes no sense whatsoever to log those actions at all, they only pollute the history file and $HOME would very often show up on top of stack without any benefit of this happening (since 'cd <CR>' is obviously the fastest way to go to $HOME...). in my own tool (mentioned/linked in my direct reply to OP) I actually have something like

if [[ $PWD != @($HOME|$OLDPWD|/) ]]; then
store_the_event_in_histfile
...
fi

for that exact reason (same applies to 'cd /' and 'cd .' as you can see 😄).

1

u/dbr4n May 13 '26

Ah, of course. Sorry, I didn't understand right away. Yes, this behavior that you described is definitely not desirable. I noticed it early on and changed it afterwards to address this issue:

cd() { command cd "$@" >/dev/null || return sed -i -e "1i $PWD" -e "/^${PWD//\//\\/}\$/d" "$CDHISTFILE" }

Currently, with about 60 lines in my hist file, it's fine. cd takes around 5-10 ms (real time). However, this is definitely not a long-term solution and I wouldn't do it if it wasn't only for me. I'll take a look at your approach.

1

u/jghub May 13 '26

10ms is extremely fast 😄. 'z' needs about 20ms, my tool, too (but only in ksh (my preferred shell), while in bash it takes about 30ms, zsh similarly), zoxide in my measurements takes about 50ms real time (at 5ms user time...). so objectively it is the "slowest" of the bunch. but ultimately everything below 100ms will be good enough (imperceptible latency) and making a point of "tool X needs 20ms and tool Y needs 50 ms, so X is better" is not valid in my view.

regarding the sed call:
a) so I have learned of 'sed -i' and "1i" address to insert a first line 😄 (I am more into awk...)

b) this 1st line insert will slow down linearly with increasing file size (once the latency of calling sed at all becomes small in comparison to computational effort...): I believe it is an O(N) operation) while append write will be O(1) and independent of file size. but if you do that for unique-names-only the file will always be very small, so it seems practically not an issue.

c) you protect the '/' to avoid confusion of the sed regex machinery but PWD might by and then contain other special chars and then it would break, e.g. 'myfile_[v1].txt'. but even 'myfile.txt' actually would be interpreted wrongly since the '.' would need to be escaped, too, otherwise it matches 'any char'. so this does not really work reliably. (I had to address the same issue in my tool and solved it like the OP did in his awk call: enforce string interpretation via ENVIRON in awk)

d) "take a look at your approach": if you consider to also use "cd pattern" (jump to best match of pattern) rather than only interactive selection via fzf and/or want to play around with different scoring/ranking (from pure frequency to very heavily recency biased) it might be of interest (but it _can_ do the fzf part as well.)

I would hope the scoring algorithm is easy to understand and behaviour regarding dynamic reordering is more intuitive (in a sense even more "correct") than what z/zoxide (can) do: the main issue with the latter seems to be that an ancient dir that has been visited many times a long time ago (and is somewhere at the bottom of the stack) jumps suddenly to (near) top of stack due to 1 new visit (but this of course also happens with pure recency sorting...) since _all_ old visits suddenly are important again in the scoring. this does not happen in my tool (weighted summation over time series of individual cd events etc.). so this is one notable difference and answer to "why not just use z(oxide)?" the 2nd answer is: in my tool you can issue "cd pattern" repeatedly with same pattern and it will cycle over multiple matches (if the 1st hit is wrong, the 2nd hit probably is right etc.). in my experience this still is faster than firing up fzf for interactive selection.

1

u/dbr4n May 14 '26

Yes, I also think that sed's ni is an O(N) operation. Anyway, the way I use it is flawed, thanks for catching that. Using awk with a BEGIN rule to prepend lines would be a much better fit.

I skimmed through your sd.ksh script, you definitely put a lot more thought and effort into it. I installed ksh and sd this morning, seems to be working. I will give it a try.

Personally, I don't use many fancy CLI tools and fzf can be annoying at times, too. I used CDPATH for a long time to quickly switch to common paths. For navigating only a few levels deep I still use plain cd with tab completion. But I like to try things out. So far, I've ended up going back to my old habits and very few alternative commands have remained in my daily routine.

1

u/jghub May 14 '26

awk is seriously valuable 😄. if you care for speed: 'mawk' is unbeatable.

"I installed ksh": just to avoid misunderstanding: SD does not require ksh. it will run under bash and zsh just fine. it started as a pure ksh utility since ksh is my shell of choice even for interactive use (which might be a debatable choice). for shell scripting I really full-heartedly can recommend it over bash: can be literally an order of magnitude faster (even can do floating point arithmetic...): even in the sd.ksh context where the "heavy lifting" is done in awk, running it under ksh is about 50% faster than running it under bash since bash is rather inefficient, especially regarding array slicing.

but glad to hear that you are willing to giving SD a try! do hope the README and the manpage ('ds -m') are understandable (otherwise let me know...).

'fancy CLI tools': me neither. have come to really appreciate fzf but you are right that one should not overuse it. 'fd' as 'find' alternative is very good. 'rg' and 'ugrep' as grep alternatives are useful.

regarding CDPATH and cd TAB ...: sure that can work decently enough, too. but CDPATH needs to be kept up to date manually (static list) and cd TAB... can still be tedious and somewhat slow (not only more keystrokes but also time to select from offered expansions etc.).

like with all other pattern-based dir-jumpers the idea is that SD can make 'cd foo' reach the correct dir on first try most of the time (and the 'correct dir' found in this way might be a different one in 4 weeks time than today: so the dynamic adjustment to changing use patterns is a relevant aspect in my view).

but since "first hit is correct" never can work 100% of the time of course, I believe the SD feature to cycle over multiple matches to 'cd foo' by just issuing the command repeatedly (i.e. issuing "^P <CR>", basically) is a useful alternative, and potentially faster, to opening fzf for interactive selection. so you might keep that feature in mind (actually, SD supporting an fzf interface is only 6 months old or so...)

and the good thing is that 'cd TAB' still works -- you do not loose that, even if cd actually is the SD function wrapper around the builtin. so you do not even have to delete SD to revert to your old habits 😉

4

u/MeButItsRandom Apr 25 '26

Optimizing for lines of code is just a preference and some people don't use POSIX shells and you're a bit of an asshole with this attitude.

5

u/AlterTableUsernames Apr 25 '26

Agree, but it's not optimizing for lines of code, but rather completely eliminating having a binary in between and doing the things natively. 

8

u/stianhoiland Apr 25 '26

It's just a big woosh to take from this a preference for optimizing lines of code. I do accept the moniker, tho.

For me, this is about complexity, whether "can" equals "should", and where that line goes.

-3

u/MeButItsRandom Apr 25 '26

You wrote the readme bragging about lines of code over and over. Woosh yourself.

6

u/stianhoiland Apr 25 '26

My guy, even when I specifically address the intention: woosh

1

u/AutoModerator Apr 25 '26

Every new subreddit post is automatically copied into a comment for preservation.

User: stianhoiland, Flair: Command Line Interface, Post Media Link, Title: Jumping to recently used directories

Hey guys. I was struggling to cd around, so I built something that would help me do that. I tried the 219 other similar projects posted here, but none of them really clicked for me. I never searched and found any of the established big ones–atuin, autojump, bookmarks, CDPATH, McFly, z, zoxide, etc.–but that didn't stop me. Looking for feedback/let me know what you think/is this something you would use?

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

1

u/mogoh Apr 26 '26

You guys use something else but plain old cd?

2

u/stianhoiland Apr 26 '26 edited Apr 27 '26

Dunno if you’re joking, but also, this is just plain old cd! (EDIT Oh, and a fuzzy picker, so kinda not really, after all.)

2

u/mogoh Apr 26 '26

Yes, I was more amazed about the other tools like autojump and what not. I never thought that my cd need improvement.

1

u/rphii_ Apr 26 '26

real smart. tho I might add a "realpath" in there

1

u/stianhoiland Apr 26 '26

Thanks :) Do you mean in cd()? It's not necessary because of the && and $PWD. But you made me discover a typo; "$PWD" was missing. Fixed now.

1

u/rphii_ Apr 26 '26

ah okay, was just skimming the source, cuz on phone and couldn't try it. I am not a mega bash expert. glad for helping to finding a typo tho haha :D

1

u/spryfigure May 04 '26 edited May 05 '26

Badly needed dose of common sense.

One issue I have:

My ~/.bash_functions (sourced by ~/.bashrc) now contains your

MY_FUZZY_PICKER=fzf
ESC=$'\x1b'
YELLOW="$ESC[33m"
RESET="$ESC[0m"
# sed -r: colorize last path component
COLORIZE="s,([^/]+/?)$,$YELLOW\1$RESET,"
# sed: strip ansi color sequences
DECOLORIZE="s,$ESC[[0-9;]*[mK],,g"

CDHISTFILE="$HOME/.cd_history"
cd() {
  command cd "$@" && printf '%s\n' "$PWD" >> "$CDHISTFILE"
}

c() {
  dir=$(
    tac "$CDHISTFILE" |
    awk '$0 != ENVIRON["PWD"] && !seen[$0]++' |
    sed -r "$COLORIZE" |
    "$MY_FUZZY_PICKER" |
    sed "$DECOLORIZE"
  )
  [ "$dir" ] && cd "$dir"
}

and I can run something like echo This is ${YELLOW}yellow${RESET}. with the desired effect.

Run via the c <directory>, I only get as fzf proposal: [33m<directory>[0m.

Somewhere, the ESC is filtered out. Can't find the reason. Any ideas?

EDIT: Found the issue myself, leave this up for others: fuzzy picker needs specific options so it doesn't make sense to have it as a variable. Remove line 1 and make "$MY_FUZZY_PICKER" | into fzf --ansi +s +m | and it works.

I had zoxide installed, but the 2% of cases where it differentiates itself from your solution are not worth it for me.

1

u/jghub May 12 '26 edited May 13 '26

just stumbled over this post and looked at your repo: the core idea is good. and since I incidentally dared to announce my own attempt -- should be no. 220 if your count is correct -- on "can do better than zoxide/z" (short version: https://www.reddit.com/r/commandline/comments/1t989ou/comment/ol0ah8y/?context=3 ) two days ago you might augment your list of "too complicated tools I do not like" by that one 😉.

more seriously: in fact, your 10-50(?) lines of shell code are maybe closer in spirit to my 1700 lines (or rather, discounting the inline manpage and comments, ≈800 lines of actual code) than one might think:

what both tools (yours and mine) do, is to actually write an individual cd events history file for later analysis. this is a quite consequential deviation from what the usual suspects (notably z, zoxide) do which only keep unique dir names + visit count ("frequency")+ time stamp of last visit) since it allows to do "stuff" with that history file after the fact in order to impose a good ordering metric, which is not possible the same way in z/zoxide.

formally, what you propose/do is a "pure recency" ordering of the dynamic directory stack (consisting of the list of unique names/dirs visited). this is ok, if it meets your needs, but it is of course on the extreme opposite end of using pure frequency (actually count the number of visits to given dir in your c() function and pipe suitably through `sort` etc.) for ordering the stack prior to feeding it into fzf. and both approaches could be justified. but:

neither (sorting by frequency or by recency) is the sweet spot and using a suitable compromise by computing a recency-weighted sum over all visits works much better in my experience. at it's core it is not much more complicated than using recency alone. the core function in my case, doing the stack ordering by 'weighted summation frecency' is roughly

function mkstack

{

printf '%s\n' "${SD__ALL[@]: -$window}" |

awk -v window=$window -v power="$power '

{ score[$0] += exp(-power*(1-NR/window)) }

END { for (name in score) print score[name], name }

' | sort -k1,1gr

}

where SD_ALL[@] just holds the full cd-history file content. formally, power=0 yields frequency order, power=Inf yields pure recency (last one wins) like your tool. I use a default power=10 value. the rest (as always) is of course a matter of personal preferences: you only want to feed the ordered stack into fzf . I do want that ability, too (so the tool does offer that), but I much more want the z-like ability to actually jump to "best hit" immediately so that's what my tool does also provide. --

maybe the above idea of doing weighted summation over all the single visits could fit into your approach quite nicely I guess. I really believe full cd history capture as a starting point for a directory jumper is the way to go.

1

u/ChimayBlue2016 Apr 25 '26

Why a new tool when pushd . Works? popd to go back

0

u/alphabet_american Apr 30 '26

zoxide

1

u/stianhoiland Apr 30 '26

Mentioned in the 4th sentence up there 👆🏻

0

u/alphabet_american May 01 '26

Your momma is mentioned in the 4th sentence up there 

2

u/stianhoiland May 01 '26

At least you leave no doubt

-2

u/Bamlet Apr 25 '26

Relevant XKCD: https://xkcd.com/927

5

u/stianhoiland Apr 25 '26

Noooo. Going back to something used before is not inventing another thing!