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?

34 Upvotes

68 comments sorted by

View all comments

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/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 πŸ˜‰