Fish 4.0: The Fish of Theseus
fishshell.com
fishshell.com
I wonder if this is the biggest project that has moved from C++ entirely to Rust (or maybe even C to Rust?) It probably has useful lessons for other projects.
If I'm reading this right, it looks like fish was not released as a hybrid C++ / Rust program, with the autocxx-generated bindings. There was a release during that time, but it says "fish 3.7 remains a C++ program" [1]
It sounds like they could have released if they wanted to, but there was a last stage of testing that didn't happen until the end.
Some people didn't quite get the motivation for adding C++ features to Rust [2], and vice versa, to enable inter-op. But perhaps this is a good case study.
It would be nice if you could just write new Rust code in a C++ codebase, without writing/generating bindings, and then throwing them away, which is mentioned in this post.
---
Also the #1 gripe with Rust seems to be that it supports version detection, not feature detection.
But feature detection is better for distros, web browsers, and compilers:
Feature Detection Is Better than Version Detection - https://github.com/oils-for-unix/oils/wiki/Feature-Detection...
Version/name detection is why Chrome and IE pretend to be Mozilla, and why Clang pretends to be GCC. Feature detection (e.g. ./configure and eval() ) doesn't cause this problem!
We never considered releasing anything with a hybrid codebase; aside from the philosophical purity of fully making the switch to rust, it would have been a complete distribution nightmare (we take package maintainer requirements very seriously). Moreover, the code itself was not in a very pretty state - the port was very much like trying to undo a knot: you had to make it much uglier in order to get it properly undone. There were proverbial tons of SLoC that were introduced only for transitional interop purposes that were later removed, this code was never held to the same quality standards (in terms of maintainability; it was still intended to be bug-free and required to pass all our unit and integration tests, however).
As mentioned in the article, we prefer to do feature detection when and where needed/possible. The old codebase was purely feature-detected via the CMake build system but we ended up writing our own feature detection crate for rust invoked via build.rs (maintained here [0]) though we just defer to libc on a lot (which doesn't do that yet). One side effect of the libc issue is that we're beholden to their minimum supported targets (though I'm not sure if that's strictly the case if we don't use the specific apis that cause that restriction?), which are higher than what we would have liked because we were fine with feature detecting and implementing using both older and newer apis where needed.
The problem with feature detection (normally referred to as configuration probing), at least the way it's done in ./configure and similar, is that it relies on compiling and potentially linking (and sometimes even running, which doesn't work when cross-compiling) of a test program and then assuming that if compilation/linking fails, then the feature is not available.
But the compilation/linking can fail for a myriad of other reasons: misconfigured toolchain, bug in test, etc. For example, there were a bunch of recent threads on this website where both GCC and Clang stopped accepting certain invalid C constructs which in turn broke a bunch of ./configure tests. And "broke" doesn't mean you get an error, it means your build now thinks the latest Fedora and Ubuntu all of a sudden don't have strlen().
One answer is the __has_feature tests mentioned in a sibling comment. Then you are using a supported API, not arbitrary code. Browsers should probably support something like that, if they don't already.
But the arbitrary code is still a useful fallback, for when the platform itself doesn't support config probing
I think you're saying that "writing good ./configure is hard", which is absolutely true. But it's still true that feature detection is better than version detection.
One thing about rust is that it has always treated cross-compiling as a first-class citizen. Cargo is very intentional about the difference between the HOST and TARGET triplets and you can't mix them up unless you are doing so intentionally.
The rsconf feature detection crate was similarly designed with cross-compilation in mind from the start and eschews running binaries in favor of some clever hacks to exfiltrate values during the cross-compilation process.
There is only one rsconf feature (retrieving compile-time constants) that is currently labeled caveat emptor as it does not support cross-compilation; perhaps I can nerdsnipe someone here into figuring out a workaround: https://github.com/mqudsi/rsconf/issues/3
I re-did Firefox's autoconf to do this back around 2010 (was contracting for Mozilla as a part-time job in college), after running into one too many features that were automatically disabled because of a missing library. There was at least one Firefox nightly that was missing an important feature because the build machine didn't have the required library.
Heartily recommend others give it a try as a daily driver for a couple of weeks. I liken it to Sublime Text: an excellent “out of the box” tool. Just the right amount of features, with the option to add more if you want. But you also don’t feel like your missing out if you keep it bare bones. A great tool in and of itself.
1. With zsh, I can copy-paste some bash snippet and in 99% of cases it will just work. Aside of copy-pasting from StackExchange, I also know a lot of bash syntax by heart by now, and can write some clever one-liners. With zsh, I didn't need to learn everything from scratch. (I guess this matters less now that you can ask AI to convert a bash one-liner into fish one-liner?)
2. For standalone scripts... well, I think it's best to reach for a proper programming language (e.g. Python) instead of any shell language, but if I had to use one, I would pick bash. Sure, it has many footguns, but I know them pretty well. And fish language is also not ideal - e.g. IIRC it doesn't have an equivalent of `set -e`, you have to add `; or return 1` to each line.
I'm sure you know this, but: no particular reason the interactive shell you use has to match the shell you use for scripts. All of my scripts are in bash, but I haven't used bash interactively in decades now, at least on purpose.
Clarification: /usr/bin/env should be used for pretty much every shebang since it looks up the binary on $PATH.
For me, for example: `/data/data/com.termux/files/usr/bin/bash`
In such cases, scripts containing the absolute path to bash in shebang do not run correctly.
If you don't want bash-specific features, you might as well use zsh or dash or whatever lives in /bin/sh. If you do want bash-specific features, you might as well take advantage of the latest and greatest.
On that note, on my Macs, the bash I want is usually /opt/pkg/bin/bash or /run/current-system/sw/bin/bash :)
Writing portable software is difficult, and doing it for shell scripts even more so. Blindly pursuing portability for its own sake is not worth it. Weigh the cost of portability against the odds that the software will ever run on different systems.
For me personally it is never worth it to write my personal programs portably. This would require that I test them on different systems that I do not even use. Pointless.
Zsh has a few nasty Bashism footgun incompatibilities. If I remember correctly the worst one is with how globbing / “*” works, which is why that is guarded with an option.
My main reason for sticking with Bash is that it’s everywhere, and the places where it isn’t try very hard to support the most-used featureset of Bash.
A stock Bash shell does feel a little naked without my dotfiles though :)
$ cat <(echo "Hello")
Hello
$ cat <<<"Test"
Test$ cat (echo Hello | psub)
In any case the discussion in that issue made a convincing (to me) argument that if you're doing the sort of scripting for which "set -e" makes sense, which is most of it, you should be using Bash. That doesn't mean you need to use Bash interactively though, as others have pointed out.
There's not much to imagine since that's pretty much every other language?
Sure you can recover with error handlers (sometimes[0]), but by default all of them will hard abort in case of exceptions.
In our modern language landscape shells are very much the odd ones, where errors are completely silent by default and the thing just carries on oblivious that the world around it might be crumbling completely.
[0]: https://doc.rust-lang.org/book/ch09-01-unrecoverable-errors-...
Laughs in client-side JS.
Client-side JS is event-driven. An unhandled exception stops processing for that event, but doesn’t block other events.
* Command auto suggestions as you type based on your history
* History search (using up arrow) based on a partial command
* Helpful completions and descriptions when you hit TAB
* Muti-line command editing
* Syntax highlighting
You can get all those same features in Zsh by using plugins, but those features work out-of-the-box with Fish with zero configuration. Zsh is a bit of a pain to configure, and pretty anemic without plugins. Fish makes configuration optional because it works how you'd hope your shell would out of the box. Even though Zsh has those features as plugins, they're kinda janky, not well maintained, and often conflict with other plugins.
Additionally, Fish also has:
* Excellent built in commands (string, math, argparse)
* Sane scripting (word parsing where you don't need to quote everything, etc)
* Great documentation
* A web-based configuration if you're into that sort of thing (it's a bit of a gimmick for beginners)
The main reason I use Zsh (or Bash) at all is for POSIX/portability, or for when I can't install something else. But for an interactive shell on a machine I control, it's hard to compete with Fish for speed, features, and ease of use.
docker stop <TAB>
it suggests the hashes of the containers. About Git, you can, for example, type:
git checkout <TAB>
and Fish will suggest the available commits and tags. If you start to type a string it will also suggest the hash of commits whose messages match the string.
I know that zsh may do the same using plugins. But Fish have all of that by default without being bloated. I use Fish since 2018, never installed a plugin and never thought that something was extra in it
Really, not needing to pull in other people’s janky scripts because the built-in features work well is huge. I still configure fish and use a few scripts, but it’s the lack of the massive cottage industry that is the primary draw for me.
Of course, many devs see that as a failing: “how could a shell do its job well without a thousand knobs to tweak?”
The main reason I switched is because zsh can (often) source bash scripts and can use bash completion scripts (usually), and I was tired of having to translate things from bash to fish. I also ran into a few things where something that was relatively easy to do in bash was impossible to do with fish. But that was years ago so maybe that is less of an issue now, and I don't remember exactly what it was.
Having used zsh, I think a big advantage it has over fish is the completions. There are completions available for more programs for zsh, and the zsh completions are sometimes higher quality in zsh.
But I do generally like the syntax, and good out of the box experience of fish. I wish it had a bash or even posix compatibility mode and more available completions.
Fish was so much better than either out of the box, and I still have done virtually no configuration other than setting it up to use my common starship prompt, which is supported in bash as well.
I don’t understand personally the argument about not having bash syntax. If I want it, I just run `bash`.
Three main reasons:
1. The Fish language is only useful only for somehow extend fish itself, so it is pointless to spend time learning and practicing it unless I'm writing something for me or other Fish users.
2. Sometimes we need to copy and paste something to our shell. When using Fish I must remember to set variables with set, get the status code with $status instead of $?, use () instead of $() and so on, which is a unnecessary overhead
3. Bash's syntax is a hell: sometimes we forget a space, an escape, use end or done when we need to use fi or esac and so on. I don't trust my Bash code, I type everything in the terminal to check if everything is ok. In Fish I just can't do that...
> If I want it, I just run `bash`
That's what I do for 2 and 3. But when I do that I don't have the nice features of Fish...
I still love Fish, though.
For example, here's how you write an autoloaded function "foo" in Fish: you make a file called "foo.fish" in its config directory. Inside that, you write "function foo ..." to implement it. There's no step 3. That's it.
Want to customize your shell prompt? Follow the process above to write a function called "fish_prompt" that uses normal scripting things like echo, pwd, git, or whatever to write your prompt to the screen. There's no step 2. That's it.
Fish was revelatory. Other shells of the same vintage feel hopelessly outdated to me now. For context, I was the maintainer of FreeBSD's "bash-completion" port for a few years way back when. It's not that I don't have experience with other shells. I have plenty. I just don't want to use any of the others now.
> what more could a shell?
Is quite good. It could almost be the tag line for fish shell.
I use zsh with plugins which pretty much makes it act like fish's convenience but one can use fish as their shell scripting while keeping the "bash" compatibility by keep using zsh or bash under terminal.
It's a Bash-like shell written in Python. It has significant overlap with the awesomeness of fish, and has the advantage of being able to write your shell scripts in a Python dialect. So if you know Python, the mental burden is much lower.
On top of that, it's cross platform, since Python is. No WSL needed.
I switched to it in 2018 and haven't looked back. Originally it was just because I wanted a better command prompt environment in Windows for work, but I liked it so much I switched to it in Linux as well.
(And yes, you can type any Python statement right in the command prompt).
[1] https://xon.sh/
> We’re also losing Cygwin as a supported platform for the time being, because there is no Rust target for Cygwin and so no way to build binaries targeting it. We hope that this situation changes in future, but we had also hoped it would improve during the almost two years of the port. For now, the only way to run fish on Windows is to use WSL.
I understand, but this is indeed incredibly sad. To this day I still use Cygwin, and in fact prefer it to WSL depending on what I'm doing. Cygwin is an incredible project that is borderline miraculous for what it accomplished and provides. Without Cygwin I may not have any sanity left. I can't exude enough love for the Cygwin team.
Hopefully rust will support cygwin as a build target in the future!
> We’ve experienced some pain with C++. In short:
> tools and compiler/platform differences
before conceding that, because of Rust, they 1) are actually dropping support for a platform they previously supported and 2) can only support (in theory) a small fraction of those platforms supported by g++, but that that's OK because those are the only platforms which really matter. I get that it's a trade-off, but it would have been more intellectually honest to just admit this is one area (portability, backwards compatibility, and ABI stability) where C++ mops the floor with Rust, instead of pretending it's a another paintpoint Rust avoids.
Embedded distros should still be supported, though you might need to cross-compile for a few depending on rust toolchain availability. Cygwin is supposed to be getting a working rust target at some point, but who knows?
Personally, what I want is inexpensive hardware (routers, but also storage devices) that don't use much power (e.g. ~5W) but are also viable targets for projects like fish, so we can all have nice things.
In this scenario C++ will provide "atomic" types that are just locking - they work but they don't have the behaviour you presumably wanted - and Rust will say OK, there's no 64-bit CAS so you can't have these features.
If all you actually needed was a type that has apparently atomic behaviour then I guess the C++ feature is perfect, but I'm assuming almost nobody asks for an atomic type if they would have accepted a locked type instead and so for them Rust saying "No, sorry" is actually a good heads up that they need to think again for this target.
What they got from this isn't that they can now support more platforms, but that they now don't have to spend as much effort on supporting dealing with differences between different platforms.
C++ is the clear winner in portability because of GCC and the wealth of platforms it supports. You can argue you don't care about supporting, e.g., OpenBSD on PPC, but trying to hand-wave away this advantage C++ has over Rust is disingenuous.
But as they outlined, in a lot of cases achieving it is substantially more effort per arch+OS+version target than Rust.
Getting a better ROI on your time is a valid reason to consider something better for your use cases.
> but in some ways rust makes it easier to support multiple platforms than it is in c++
The ease of installing Rust on Windows has helped build a culture of cross-platform libraries/crates, and so it's significantly easier to build applications that support more platforms than C++.
Take a library for coloring text on a console, and chances are, it'll work on cmd.exe too (despite that not even using ANSI escape codes).
Take a library for loading other libraries, and it will load .so, .dll, and .dylib too, with feature-gated methods to deal with each platform's quirks.
Rust's standard library helps a bit, e.g. it isolates platform-specific modules into things like std::os::unix and std::os::windows, so it's a bit more obvious when you're trying to use something that won't work on all platforms.
However, it's more just a cultural thing; many Rust things work cross-platform now, people see how nice that is, and so people try to maintain the status quo.
With C++, you often need MSYS or Cygwin, and those have their own limitations; you can certainly make something in C++ that won't need them, it's just harder in comparison.
I'm not trying to hand wave that away. That is an advantage c++ has. But if you only care about supporting platforms that rust supports, rust can make supporting all the platforms you do care about easier than if you used c++.
I refuse to work at large companies for this reason, but one company I worked for brought on a large American bank as a customer and their infosec terms for vendors essentially required their IT "standards" on us, which sucked as we were a Mac shop. It almost came to a head when all the developers were told they had to seek approval for upgrading their build tools.
https://github.com/rust-lang/rust/issues/5526
(this feature request has been open for 12 years)
> That’s because, while cargo is great at building things, it is very simplistic at installing them. Cargo wants everything in a few neat binaries, and that isn’t our use case. Fish has about 1200 .fish scripts (961 completions, 217 associated functions), as well as about 130 pages of documentation (as html and man pages), and the web-config tool and the man page generator (both written in python).
Our issue for this is https://github.com/rust-lang/cargo/issues/2729
Personally, I lean away from Cargo expanding into these use cases and prefer another tool being implemented on top. I've written more about this at https://epage.github.io/blog/2023/08/are-we-gui-build-yet/
I would definitely love to see Cargo have the ability to do this -- it means that `cargo install --locked` stays as a viable approach. It probably won't apply to fish, but I think being able to run a post-install command from the binary you just installed would suffice for my needs.
In my current project I just wrote the installer and config generation as part of the main method. Gets rid of a lot of complexity, with a simpler build, and is arguably easier to maintain. Single language, single binary.
People can complain as much as they want about the borrow checker, but you basically have to be as strict as Rust is in C++ if you want to really avoid use-after-free issues, ... I've been writing "Rusty C++" since before Rust was a thing, because that's the only sane approach to memory safety. I'd rather have a program check that I don't fumble up instead of running sanitizers when things go awry (often years later). The best bug is a bug that can't happen at all.
Static analyzers are sadly too limited compared to what a borrow checker can do in my experience. Some bad stuff will always slip in in C/C++.
Is this typical for such a translation. They also mention addition of new features contributing to more code, how much of the addition was new features vs pure translation?
Would be interesting to see the line count of the c++ version if it was run through a formater with similar configuration.
It may be just down to rustfmt. It really adds a lot of vertical sprawl. I personally can't stand how much rustfmt makes multi-line code explode.
The problem is that as soon as a whole statement doesn't fully fit on a single line, rustfmt switches from "horizontal" to "vertical" strategy. It then stops caring how much vertical space it takes, inserts line breaks as often as the style allows, and won't try to use the available line width.
You end up with long runs of finely chopped lines that contain only a single variable or a single call (often with each argument on a separate line too), which looks like having a 20-char line length limit.
It's either fully oneliner `foo().bar().baz()` or fully vertical
foo()
.bar()
.baz();
and you can't have anything in between. It will fight you if you put two calls on the same line.- There are only two strategies and the algorithm to choose between them is trivial for a human to compute. This makes for way better readability, you can reliably predict where the next call/argument is going to be positioned.
- Refactoring becomes easier - moving an argument is now a simple `Line up` editor action.
- Source control diffs become more stable across changes.
...but it's hard to see the benefits on trivially simple examples like the one you presented. Here's a reformatting I did [1] to illustrate this:
Original:
ReturnType<SomeOther<S, TypeConstructor<Nested<S, T, U>, T>>, U, HmmLetsAdd<V, W>>
Reformatted: ReturnType<
SomeOther<
S,
TypeConstructor<
Nested<S, T, U>,
T,
>,
>,
U,
HmmLetsAdd<V, W>,
>
Vertically verbose, yes, but that hardly matters. The reformatting gave the code visual structure that's made it easy to understand.You rename a method, now it's a few chars shorter/longer, and some 1-liner call sites can become completely rewritten to the vertical style or vice versa.
You delete a single line, now the expression can fit under a threshold, and the whole block is spaghettified (especially terrible when deleting a struct field).
You wrap code in an if{}, which could have been a clean whitespace-only change for the entire body, but the indentation affects rustfmt's char counts, and it will now force different formatting decisions all over the place.
If you're changing an infallible method to a fallible one, you could have added error handling/fallback on separate lines, keeping changes to the happy path minimal – but not with rustfmt.
For minimal diffs you'd want something with a slack like gofmt, but rustfmt completely overlooked what makes gofmt so great and widely acceptable, and is a misanthropic ruthless canonicalizer instead, which ruins lots of use-cases, including minimal diffs.
Rustfmt's heuristics aren't simple. They're based on char counting of AST nodes, with multiple different limits for different node types, and crossing any threshold can trigger an avalanche of reformatting. There are more complex rules for closures and comments.
These heuristics are a source of formatting inconsistencies, e.g. each match arm is evaluated independently. Even when all match arms contain the same code, they can be formatted wildly differently due to patterns/name lengths pushing different thresholds. Indentation and AST context can make the same expression look differently each time. Such problems largely don't exist in gofmt, so it's not a problem of code formatters, it's a problem of rustfmt specifically.
> The initial PR had a timeline of “handwaving, half a year”. It was clear to all of us that it might very well be entirely off, and we’re not disappointed that it was.
I'm amazed that you estimated it at so little time originally, and I'm amazed you shipped it in full in just 2 years. Congrats!
We technically removed the last C++ code from the core project in January 2024 (~a year ago), the last C++ code altogether (a test helper) in June 2024 (six months ago). We only decided to push out a release now because we've added enough new features (not counting the rewrite as a feature) to warrant a release.
But at the same time, someone could argue that the current codebase is still far from being fully idiomatic rust, there are various C++-isms ranging from the use of UTF-32 (historical from the nature of std::wchar/std::wstring under *nix) to still passing around file descriptors rather than rust `File` objects (that will take a lot of rearchitecting to make mut-safe).
Ultimately, a project is never "done" and we're not being paid at all let alone contingent upon completion of the port, so there's no real use in saying it took precisely this long or that long. We're releasing now because we want to, but I wouldn't tie the release cadence with the port timespan.
Unfortunately it's pervasive throughout the entire codebase (which does a lot of string shuffling).
So it's a historical mistake, but not something to be fixed in the same step as switching the implementation language.
bstr was mentioned as a direction and is a possibility.
Makes me reconsider fish :)
One weird thing I'd also like to see is more bash integration, as others pointed out that being their primary motivation against switching to fish full-time. My use case is mostly sourcing bashrc/bashevv, and theoretically it should be possible in fish if I understand correctly: you need to be able to import e.g. every new env variable that changed before and after sourcing a bash script via real bash.
How about updating the project tagline to: "Finally, a shell for the 00s!"
My experience with this is the other way around, especially if you have crates tied to that feature.
The cfg! is a marco that compiles to true/false, so whatever is inside of the if guard needs to compile regardless.
E.g.:
Cargo.toml
[features]
default = []
my_feature = ["deps:feature_dependency"]
[dependencies]
feature_dependency = "1.0.0"
And in code: if cfg!(feature = "my_feature") {
feature_dependency::something::Something::invoke();
}
This will fail if you compile without `my_feature`.Atuin for improved history search.
Starship for an improved shell prompt.
zoxide - better cd
ripgrep - better grep
just - a command runner. I put project specific commands/scripts in there so I don’t have to remember.
All of these are indispensable for me.
I want to say though, great work on this migration! Really fantastic work that others can learn from when they try a similar migration.
I run into so many issues trying to adapt the regex in my head (usually PCRE) into the older grep and egrep style.
Whenever I setup a new system now, I install chezmoi, clone my comfig repo and then initialize it and it uses the chezmoi scripts to automatically installs all my programs and copies in the needed config files.
I switched over from zsh about four years ago and my config went from several hundred lines to a handful with just one plugin (fzf.fish).
It just works how I expect it to and I can't imagine changing again any time soon.
(note to editor: find an actual award)
Here's the code if you were looking for it: https://github.com/fish-shell/fish-shell/tree/4.0b1
(Also thanks for putting so much work into maintaining fish - i have used it as a daily driver for years, and posts like TFA showing it's maintained so professionally impress me a lot!)
Generally code that has been running for years is unlikely to have too many bugs since they have been shaken out, "rewrite it in Rust" as a fad just ignores the decades of work already put into the codebase and for large codebases likely eont succeed.
As you mentioned, write new modules with rust. That means likely needing to export a C API for your libraries but there's a good chance you were already doing that. There was also a rust crate that tried to automate most of the c++ rust interop for you but not sure about how good it is in reality.
Does something like fish make the experience a bit smoother? is it pretty easy to get into?
It's absolutely worth a test drive to see if the features excite you. If the lack of bash familiarity is too much of a blocker, then zsh with plugins that provide the same features as fish might be worth a look too.
I've tried Fish a few times but hard to migrate over from bash/zsh. Does anyone have tips on how to port over a bunch of aliases/scripts/etc. easily?
Does the fish team have any plans for integrating AI models for other completion?
https://github.com/search?q=repo%3Afish-shell%2Ffish-shell%2...
That agency includes interpreting the search results alongside the reply to your original comment.
By asking for the docs? If you found that file already then it’s clear they misunderstood what you are looking for, and also that what you are looking for doesn’t exist.
It's honestly a non-issue in the current year to learn zsh or tcsh
Compatibility has also come a long way in recent releases; you should give it a try.
Try this (tested with zsh 5.9 on archlinux):
foo='PWD[$(echo hahaha >&2)0]+42'
[[ "$foo" -eq 5 ]]
In bash, this would also print "hahaha" with "a" (or any other possible variable name) instead of "PWD", that's why many think it doesn't work at all in zsh.I'm pretty sure zsh has an optimization where it skips the indexing if the variable doesn't exist, which happens to sometimes stop this. But since you can just reference variables like PWD, that you know exist, it's not really a security improvement.
Fish offers one behaviour out of the box with very little configurability, and that it's. If you like that behaviour then Fish is great. If you don't, then well, it's not so much.
zsh on the other hand is very flexible and can be shaped in to more or less anything you like. Some people really like that sort of flexibility and configurability, others don't.
Just install it and see if it's for you.
...
> A lot of the increase in line count can be explained by rustfmt’s formatting, as it likes to spread code out over multiple lines, like: ...
I wonder what the character count diff is?
That said, you asked so here's the result of the difference within the `src/` directories comparing 3.7.1 and 4.0 beta (so excluding docs, fish scripts, etc):
> cd fish3/src; find . -type f -exec cat {} + | tr -d '[:space:]' | wc -c
2172330
> cd fish-shell/src; find . -type f -exec cat {} + | tr -d '[:space:]' | wc -c
2207996
That's an increase of 35,666 bytes, or just 1.6%If you don't exclude whitespace, the difference is just under +250k bytes (an 8% increase).
I dont even need the OMZ prompt (i use starship for that), but the aliases from the kubectl and git plugins are just so great to have if you use kubectl and git often.
Other plugins (like colored-man-pages, fzf-tab and syntax-highlighting) are also nice.
Is there something like that for fish?
Oh-my-fish has some of those features, but it seems to be abandoned.
We don't recommend oh-my-fish for various reasons, but I guess what's really missing is just a gallery.
Care to elaborate a bit on those? Or is it the kind of thing that's impolite to discuss?
I wonder how a conversion of a C++ project that makes extensive usage on ranges would go.
It would say whether the file extension is executable (part of pathext env variable)