scripting in fish vs bash

I recently switched from the bash shell to fish. The reason is that both Kali Linux and Apple’s OS X are forcing a switch from bash to zsh. I used zsh for a while a few years ago. It’s probably an improvement on bash, but I didn’t really click with it. I also messed with fish a while back, and didn’t stick with it, probably because I didn’t see any reason to switch from the shell that’s the default everywhere.

However, if I’m going to be forced to change away from bash, I’d rather invest the time learning fish than zsh. In the weeks since the switch, I’m mostly very happy with the change. It is a marked improvemnent on bash, and all the annoyances I’ve had have been places where it differed from bash. Not because the bash way was better, of course. But because it wasn’t immediately familiar and I had to go learn something new. Not that I don’t like learning, but when I’m trying to get things done, I tend to take the path of least resistance and just do it the familiar way.

The largest difference, of course, is the language itself. You could go all day using bash or fish and never know the difference if you just did all the basics – ls, cd, cp, mv, rm, vim, ssh, and on and on. However, the moment you write a function or a shell script, or modify the config file (~/.config/fish/config.fish as opposed to the familiar ~/.bashrc), you will be forcibly stopped short.

The biggest differences are capturing output from a command no longer uses $:

# bash
date=$(date +"%Y-%m-%d")
echo $date
# fish
set date (date +"%Y-%m-%d")
echo $date

I use the bash version of that syntax a lot by default, and, evidently, I’m lazy and have a bunch of shell scripts that have no shebang lines. Many would work perfectly in fish but for the unneeded $.

Another common issue is that fish uses end to end a function, loop, or if block. This is (irrationally) annoying to me. Probably because the programming languages I’ve used which use this syntax have not been among my favorites. It just doesn’t make sense. How is end the appropriate closing tag for if or function?

On the other hand, what does bash use instead of end? Oh, it depends. An if block? Well fi, obviously. A for loop? Well, there’s no actual closer; you follow the loop with a do/done block. Functions are the best, where there are opening and closing curly-braces. So, I’d say that, objectively, fish does a much better job here because of consistency, if nothing else.

Then we have the issue with do. A for loop in bash looks like this:

for filename in *.log
do
    echo $filename
done

Why is do there? Why is done the closer for a do block? I don’t know. What I do know, however, is that I’ve been typing that do for so many years that I keep doing it in fish, and obviously it breaks things. This despite the fact that I regularly forget to use it in bash. This is clearly a personal problem.

Conclusion: Fish is better, if for no other reason than consistency – although I do believe it’s also better in other ways.

This brings me what I’ve actually spent the most time thinking about due to this switch: Scripts and functions. In both bash and fish, you can write functions in a config file, which will be available in any shell session. For that matter, I’ll throw aliases into the conversation as well, because they’re conceptually identical for the purpose of this conversation.

# fish
function twice
    echo $argv
    echo $argv
end
#bash
function twice {
    echo $*
    echo $*
}

Of course, you can always write these lines of code in a separate file, mark it executable, and run it using an absolute path, or by adding it to your $PATH. For that matter, you can write your code in any language, compile it (if necessary), and put it on your $PATH. In that case, when should you do one or the other – add a function to your shell’s config or write a script?

I can’t come up with a great answer for this, but I’m now leaning towards always creating scripts. Because, why not? It doesn’t clutter the config file, you can use any scripting (or other) language that’s appropriate to the task, and they’re simple to copy across computers. The only time I default to a function in the config file is when it’s something that is custom to the computing environment. For example, if I’m doing work for my employer, there are multiple series of commands I type in sequence multiple times per day. These could include specific paths, server names, and other things which, in a script, should be replaced with a variable. But given how I use these commands, that would be a waste of time. Those are great candidates for a function in the shell config. Everything else can be a shell or Python script, and occasionally a Go command-line tool.

If you’re unfamiliar with fish, but do spend a lot of time on the command line, I encourage you to check it out. It’s well worth the learning curve.