Problems with complex scripting in fish

During the development of redfish, I encountered several issues with the design of fish 3.4 as a scripting language. Based on my experience, I do not recommend writing complex scripts in fish, except in service of its interactive use. I still very much like fish as an interactive shell.

My main reason is the error handling. The lack of set -e means scripts require adding an or return after every command that can fail in order to propagate the error. This resembles error-handling in C or Go without compiler warnings for ignored errors. It is easy to miss a place where you are supposed to add an error check.

Simultaneously, fish doesn’t have the equivalent of set -o pipefail. Some years ago, the developers discussed this feature but had concerns about backward compatibility and having it controlled by global state like in Bash. During this discussion, a contributor proposed pipestatus as an alternative, which was later implemented. The list variable pipestatus is an unambiguous improvement. The use of pipestatus to check for errors is nevertheless manual and left up to the script writer.

Not having pipefail causes a problem with loops in functions. When a loop’s output is piped to another command, and the loop body tries to return on error with or return, the pipeline’s exit status overwrites the return status, masking the error. The best solution I have found for this problem is to save the exit status in a variable, break out of the loop, then return the saved status. There is an example in example.fish for redfish:

function count_items
    # ...

    set --local exit_status 0
    for key in $keys
        if not set --local count (redfish get $key)
            # We can't use `or return` here
            # because the pipeline will eat the non-zero status
            # when we return from the function.
            # We also can't examine the contents of `$pipestatus`
            # after an `or return`.
            # Therefore, we'll track the status manually.
            set exit_status 1
            break
        end

        echo $count (string replace $prefix '' $key)
    end | sort -n -r

    # ...

    return $exit_status
end

I also had some issues with command substitution, albeit smaller. Command substitution splits output into multiple arguments on newlines unless you use double quotes. This is convenient in interactive use, but I quickly began to see it as a negative for scripting. When testing my scripts against somewhat tricky input like filenames with line feeds, I discovered that a robust script must quote all command substitution that produced a single filename similar to POSIX shell and Bash.

This points to tension between ease of interactive use and correctness. If fish required explicit splitting of command output, quoting would not be necessary for handling filenames. However, this would be more verbose and potentially make interactive use harder.

List variables become multiple arguments as well. A list variable is substituted as multiple arguments by default and as a single argument when quoted. The list variable behavior was not a significant issue. It requires defensive quoting when you are unsure whether a variable can be a list, which seems rare in normal code. I would prefer explicit syntax to indicate when a substitution yields multiple arguments.

There is currently no ShellCheck for fish. One would be very helpful, because it could detect probable bugs like or return from inside a piped loop. I have not found a linter for fish code besides fish -n, which only validates syntax.

While I genuinely like and prefer fish as an interactive shell, I think GNU Bash is preferable to fish for shell scripting. I would trust myself and other programmers to write less buggy shell scripts in Bash with ShellCheck than in fish.

That said, a comprehensive linter for fish that caught issues like missing error checks and or return statements inside piped loops could make fish significantly more suited for scripting. Similar to ShellCheck, it could address practical problems with scripting without changes to the language design. If you are aware of a project like this, please let me know.