rc shell instead of bash or fish
2024-05-29 (last update on 2024-08-17)
Why rc?
About five years ago, I switched from bash to fish because it has many little improvements compared to bash when used as interactive shell and also the language syntax makes more sense to me.
In the end, I wrote only a few scripts in fish because we use bash at work and having to memorize all the details of fish for just a few personal scripts isn't worth it. I rather try to write POSIX shell scripts which are both fast and portable.
Regarding the interactive shell it's different: I can use any shell I like both at home and at work (though I don't bother to install fish in Docker dev containers). And I use the shell a lot. This means that investing some time in getting used to a new shell might be reasonable.
On the one hand, fish comes with many nice features. On the other hand, I have developed a preference for simple programming languages like C, Hare, and Janet and simple software like the dynamic window manager (dwm), the simple terminal (st), pass, aerc, nsxiv.
Among the (Linux) shells, bash, zsh, and fish are the heavyweights (a.k.a. bloat). See the code line count for different shells (which always is only a rough indicator for software complexity):
rc in this diagram is Byron Rakitzis' implementation and this is the shell I'm writing about in this article.
To be fair: You can build rc with different line editing libraries. If you pick readline (as I do), you should add 29k lines of code to the 9k lines in the diagram. Then the total line count isn't that far from fish's. But maybe it's possible to get bestline (3k lines of code) integrated into rc. This combination would clearly win in terms of simplicity. UPDATE: I have brought bestline support to rc.
Syntax
While the differences in language syntax don't matter much for interactive shell usage, I want to mention the biggest advantage in rc's syntax: You don't need to quote a variable to preserve spaces in its value:
file = 'Artist - Song.mp3'
cp $file $home/Music
On the other hand, in bash, you can simply quote the result of a command substitution:
file="$(head -n 1 files.txt)" # first line: Artist - Song.mp3
rc will consider $file
an array of three elements: Artist
, -
, and Song.mp3
.
file = `{head -n 1 files.txt}
cp $file $home/Music # fails
To prevent this, you can temporarily override $ifs
:
file = `` $nl {head -n 1 files.txt}
In this case the bash syntax seems simpler to me.
But you get used to it quickly.
Read Rc — The Plan 9 Shell to learn more about rc's syntax.
What you might miss in rc (with readline)
In general, you can't expect everything being better in simpler software. Usually, the question is: Do I really need the features offered by the more complex software?
Here is a list of things I noticed to be different in rc:
~ for $HOME
After using bash and fish for so long, I'm used to type ~
for $HOME
. In rc
,
~
doesn't mean $HOME
. You write p.e. $home/.vimrc
instead. But luckily,
readline understands ~
. When the cursor is on a path starting with ~
, you
can enter Alt-~
or Alt-&
to expand it. Both are not easy to type and having
to do this manually is annoying.
My solution to this is to auto-expand ~
every time I hit tab or enter:
.inputrc
:
$if rc
# expand tilde on tab and enter
"\C-\xfe": menu-complete
"\C-i": "\e~\C-\xfe"
"\C-\xff": accept-line
"\C-m": "\e~\C-\xff"
$endif
C-i
is Tab
and C-m
is Enter
. C-\xfe
and C-\xff
are just dummy
keybindings because readline doesn't allow binding two commands (p.e.
tilde-expand
plus menu-complete
) directly to one key.
Copy current command
Sometimes, I want to copy the current command (buffer) to the clipboard and send it to a
colleague or paste it into some document. In fish, this is mapped to Ctrl-x
.
In bash, you can write a function like this:
copy_line_to_x_clipboard () {
printf %s "$READLINE_LINE" | xclip -selection CLIPBOARD
}
and map it to whatever you want.
In rc, you can also write such a function, but I couldn't find a way to bind it to a key.
Tab-completion of arguments
In fish, you can write ssh <Tab>
to get a list of available hosts. In bash,
you get the same after installing the bash-completion
package. In rc, this is
not possible, as far as I know.
Multi-line commands
If you want to repeat a three-liner in your interactive rc session, you have to search and repeat every single line one by one.
This means it's better to write one-liners in the first place. Or maybe even a small script (file).
Edit command in editor
Whenever I copy a multi-line command from a README or a webpage which needs some
adjustment, I start (Neo)vim via Alt-v
. (The default keybinding in bash is
Ctrl-x Ctrl-e
, in fish it's Alt-e
or Alt-v
.)
This is not possible in rc as far as I know. As a workaround, I have defined
this helper function in my .rcrc
:
# edit command in nvim and source it afterwards
fn editcmd {
tmp_file = `{mktemp /tmp/cmd-XXXXXX.rc}
nvim $tmp_file && . $tmp_file
}
fn e editcmd
This way, I can start Neovim with e<Enter>
, paste and edit a command which
will be executed after saving and quitting.
The only disadvantage is that I can't start typing a command and then decide that I want to continue editing it in Neovim.
Syntax highlighting (fish)
Fish's syntax highlighting is nice but not really necessary. Most entries in my shell history are just a command plus a few arguments. For writing/editing non trivial commands, I usually use vim which gives me syntax highlighting for free.
Abbreviations (fish)
fish's abbreviations are like aliases except that they are expanded
automatically when you press Space
or Enter
. This way you see what you get
which is nice when you are not sure if an abbreviation means what you think it
means.
In bash, you can add this feature via this script: momo-lab/bash-abbrev-alias.
Regarding rc, I don't see a way how to achieve this (except of patching the rc source code).
fzf helpers
You probably know fzf. It's an interactive filter for lists. fzf ships integrations with bash, fish, and zsh which allow you to fuzzy find a path to cd to, a file to pass as argument to a command, or to pick a command from your shell history.
There isn't such an integration for rc. This is my minimal solution for cd
:
.rcrc
:
FZF_ALT_C_COMMAND='fd --type d --hidden --follow --exclude .git'
fn cdfzf {
FZF_DEFAULT_COMMAND=$FZF_ALT_C_COMMAND dir = `` $nl {fzf --walker=dir}
if (!~ $dir ()) cd $dir
}
.inputrc
:
$if rc
# cd into subdirectory selected with fzf
"\ec": "\C-e\C-ucdfzf\C-m\C-y"
$endif
This even restores your current command line in case you start typing a command and notice then that you are in the wrong directory.
z
rupa/z keeps track of your most used directories, based on frecency (=the both frequently used and recently used directories). It works in bash and zsh. But there is also jethrokuan/z for fish.
It's used like this: When I type z start
, it changes the current working
directory to ~/.vim/pack/plugins/start
(because I have visited this directory
more often/recently than other directories containing start
in their name).
Unfortunately, these tools don't work in rc because they rely on sourcing code (which usually is incompatible with rc).
Good news: I have migrated z to rc: https://sr.ht/~maxgyver83/z.rc/
It works like the other implementations and even reuses your z history. But it has no argument completion because rc doesn't support this at all. (Please send me an e-mail if I'm wrong about this!)
bass (fish)
When you want to execute an incompatible line of bash code in fish, you can just
start bash and paste the line. But sometimes this doesn't help because this
line also sets variables. Or you want to source an initialization script, p.e
for ROS: source install/setup.bash
.
Sometimes, you can run this as a workaround:
exec bash -c "source install/setup.bash; exec fish"
But then you lose your non-exported variables.
For such cases, you can use edc/bass: Make Bash utilities usable in Fish shell.
I couldn't find something similar for rc.