PDF comparator: Thanks for all the FISH =======================================
Published: 2025-08-26
Table of contents:
I take a step back in the project, and port the original script to the Friendly Interactive SHell, with the endeavor turning into a quick review.
This is part of a series of posts, the project intro article can be found here.
Motivation
I have talked in the past about my quickly deteriorating relationship with Bash, in fact it was in the previous post of this very project. When you are catching yourself staying away from some specific piece of tool, it is important to find the right answer to these questions: Can you avoid the tasks this tool helps you perform? Can you work your way around somehow and use an entirely different type of tool? If yes, great, move along now; and in fact that is what I wanted to do in this series, trying to turn a sad little script to a decent Rust application.
As we have seen, my first jab at the problem was kind of discouraging in that one should not expect the porting process to be quick, even when using command-line subprocesses instead of proper crates or libraries. Maybe this is just something characteristic of Rust, I am hearing similar stories about decrease in productivity. Although I believe that this sacrifice is not pointless, we are not here today to discuss Rust.
Shell scripts have their place. There are many small tasks that is just easier to tackle in the shell, and scripting is a useful skill that one can use to make life maybe just a little more comfortable. Especially in system administration, using scripts can also be self-documenting in a way: if I wanted to save a sequence of commands and write them down in a text note, it is not much more of an effort to create a `.sh` instead. Then it is a list for future reference *and* an executable at the same time. I can just view and quickly modify the file without much additional mental load of the context switching, that it would take if I used any standalone programming environment instead.
What I wanted to say really is that the shell is too important a piece of tool, for us to alienate ourselves from it. But this is true of course for any sort of software environment that we are spending so much time in, and I think it is essential to realize when there is considerable friction. If we should not fix when something is not broken, maybe the opposite is to be considered as well: if something feels broken, it is possible that the problem is not in you even if almost nobody else seems to notice the issue.
To be fair, of course many have pointed out the problems present with Bash and other earlier shells. In spite of this, Bash seem to be the default all around, certainly in distributions I have encountered so far. So much so that for a long time, I never really thought about switching to something else. Then this last couple of years I had to deal with shell scripts increasingly more, so I have been pushed over the edge.
And just now, finally, I have made the leap. Or maybe I should say splash...
Friendly interactions
Why did I choose Fish? It is hard to explain, I wanted something that is different, where the developers were bold enough to make conscious choices breaking compatibility for the sake of clarity. Additionally, the project seemed to be quite mature and stable. Then I heard that they had just finished rewriting it in Rust, which also got my attention (no surprise). Finally, I know it is off-topic, but I also just happen to like fish very much in general. Both as pets, as well as to eat...
I encourage everyone to just give the thing a try. That is what I did, installed on my desktop machine and took it for a spin. This was around two months ago I think. It looked promising right away so I set it as my default shell on X (by setting the `SHELL` envvar in `.xsessionrc`). I am naturally the cautious type, leaving Bash in its place outside of X. Now I think it would be quite safe to switch altogether on my physical boxes, although I am still a bit hesitant when it comes to headless remote installations I have or am responsible for. You see, trust is earned...
First thing that have struck me using Fish is just how nice it is. They were not kidding when they set out to make it friendly and interactive. I am not just talking about the fancy auto-complete system, which is sure good to have but not that essential. For me, the most important aspect of these values is the fact, that I do not feel the need to have a book or StackOverflow tab or any such outside resources open even for learning how to use the program, because all the internal help pages are very easy to access. There are two distinct layers of information one can reach for: there are the individual manuals of each command, and there is the main documentation.
In order to clean up the syntax, the devs have converted many aspect of the usual language constructs to be plain commands themselves. Think about conditional and loop keywords like `if` and `for`, or variable manipulations such as `set` and `test`, to name a few: these all work like command-line executables now, and have their own `--help` and `man` pages. Yes, this makes the language a bit more explicit and verbose, but that is fine with me: it is much more important that I never have to feel lost again out in the field without internet connection, I can just know that I can look anything up in a moment.
The main documentation can be viewed in two or three different ways, and is more suitable in situations when we do not know how to do something, or what is the exact name of a built-in command. In a graphical environment, the most comfortable option is probably just launching `help`, which opens the index of the offline HTML doc site in the default browser. In a text-only setting, if something like `lynx` is installed, I guess it would be opened in it by default, but I did not try this. In any case, one can consult `man help` to learn about using the `fish_help_browser` variable, which can be used to set the desired browser explicitly. Apart from these, there is also `man fish-doc`, for those more used to this kind of thing. Personally, I find these long manpages a bit disorienting and harder to search, but nice to know about as an option.
I would like to point out, that there is a whole chapter in the documentation with the title "Fish for bash users", where they go over most of the differences. By reading through this and another section called "Tutorial", it is real easy to get up to speed and be able to just start using Fish. After this, make sure to also search for ways to customize the experience, by setting up the prompts and color schemes for example.
Implementation
With that lengthy introduction, I would like to share a sample script written in Fish, that I had just put together as an experiment. I you remember the text note and script fragment from the last post in this project, the purpose of this is to `convert` two PDF files to one image per page, and then calling `compare` on these to produce visual diffs (both commands are part of imagemagick).
I am sorry that I did not put this on GitHub. I just wanted to have a go on this problem, to follow-up on my earlier tries. Also keep in mind that this is literally only the third longer script I have written in Fish. I did a couple when trying out initially, and wanted to keep on building the experience slowly so that I can reach for it next time when needed at work, instead of Bash.
``` #!/usr/bin/fish set usage "Usage: ./portable-document-comparator.sh <left_file> <right_rile> [out_dir]" # Parameters set density 150 set page_digits 3 set fuzz 1000 # TODO: input page ranges # Functions function confirm_yes_no while read res switch $res case "" "y" "Y" "yes" "Yes" "YES" return 0 case "n" "N" "no" "No" "NO" return 1 case "*" echo "[Y/n]? " end end end function try if test -z "$argv"; or ! $argv echo "Exiting due to error while executing '$argv'" exit 1 end end function clear_dir for dir in $argv if ! test -d $dir echo "Error in clear_dir, argument is not a directory: '$dir'" exit 2 end set files "$dir"/* test (count $files) -gt 0; and try rm $files end end # Main set arg_cnt (count $argv) switch $arg_cnt case 2 set left_file $argv[1] set right_file $argv[2] set out_dir "." case 3 set left_file $argv[1] set right_file $argv[2] set out_dir $argv[3] case "*" echo "Error: Invalid number of arguments supplied ($arg_cnt)" echo "$usage" exit 3 end if ! test -f $left_file echo "Error: left_file is not a regular file." echo "$usage" exit 4 end if ! test -f $right_file echo "Error: right_file is not a regular file." echo "$usage" exit 5 end if ! test -d $out_dir echo "Error: out_dir is not a directory." echo "$usage" exit 6 end echo -n "Creating output directories if necessary..." mkdir -p "$out_dir"/{left,right,diff} set left_dir "$out_dir/left" set right_dir "$out_dir/right" echo "Done." if test -n "$(ls -Aq1 $left_dir)"; or test -n "$(ls -Aq1 $right_dir)"; or test - echo -e "Some output directories are not empty.\nWe will clear those if we echo "Do you agree? [Y/n]" if confirm_yes_no echo -n "Clearing output directories..." clear_dir "$out_dir"/{left,right,diff} echo "Done." else echo "Aborting." exit 7 end end echo "Executing left-side conversion..." try convert -density "$density" -alpha remove -alpha off $left_file "$left_dir/% set left_cnt (count (ls -Aq1 "$left_dir")) if test $left_cnt -eq 0 echo "Error: empty result." exit 8 end echo "...Done" echo "Executing right-side conversion..." try convert -density "$density" -alpha remove -alpha off $right_file "$right_dir set right_cnt (count (ls -Aq1 "$right_dir")) if test $right_cnt -eq 0 echo "Error: empty result." exit 9 end echo "...Done" if test $left_cnt -ne $right_cnt echo "Left and right output file count does not match." echo "Do you want to continue? [Y/n]" if ! confirm_yes_no echo "Aborting." exit 10 end end echo "Starting comparison..." set n 0 for file in $left_dir/* set name (basename $file) set right_file "$right_dir/$name" if test -e "$right_file" compare -fuzz $fuzz "$file" "$right_file" "$out_dir/diff/$name" set n (math $n + 1) end end echo "...Done, number of pairs processed: $n" ```
Conclusion
I have worked on this for about 4 hours altogether, which included going over the earlier versions again. To be honest I have used the one in Rust much more as reference, so this maybe ended up more of a port of that than the original. As I wanted to use arguments for the file paths, I brought basically all the checks over, and manually testing every line took more time that I have anticipated. Eventually, I had to draw the line somewhere, so although the script seems to work, it still lacks some essential features that would make it really useful (but those are missing on the earlier version as well).
Naturally, I still had to use the offline manual extensively, so with more practice I will probably be able to do something like this in about 3 hours. I have only used the internet maybe 2 or 3 times to look something up, so I can be quite happy with the help system. One of the issues I keep falling into is the habit of using `set -e` in Bash, which I am aware being a bad practice to begin with (but it looks *so* useful at the first glance), and is missing in Fish for a good reason. So far I have found the solution of defining a `try` function works well, and although it requires much more conscious thought, this is definitely a good thing here.
Putting together a script like this quickly is a skill worth having, and there are situations when it is not acceptable to fiddle with the task for a whole day. Even if it means it would then be written in a proper language like Rust, ready to take the project to the next level, sometimes we just want to get it over with. The projected ratio of development time versus the time later spent running has to be considered, and the choice made accordingly, but this is assuming that we have a comfortable environment ready at both ends.
I have found Fish to be a good alternative to Bash. As always, it takes a little getting used to, but the experience is quite pleasant. I believe that the scripts written for Fish look much less chaotic, which means they are easier to comprehend and maintain. This little adventure have finally given me back my faith in the shell, so my advice to you is that you do not have to live with the pain, a little experimentation can lead to great improvements in the quality of life. Also keep in mind that fish are very healthy and delicious, every penguin could agree on that.