Linux Fu: Failing Pipelines [Hackaday]
Bash is great for automating little tasks, but sometimes a little script you think will take a minute to write turns into a half hour or more. This is the story of one of those half-hour scripts.
I have too many 3D printers. In particular, I have three that are almost — but not exactly — the same, so each one has a slightly different build process when I want to update their firmware. In all fairness, one of those printers is heading out the door soon, but I’ll probably still wind up building firmware images for it.
My initial process was painful. I have a special directory with the four files needed to configure Marlin for each machine. I copy all four files and ask PlatformIO to perform the build. Usually, it succeeds and gives me a file that looks like firmware-yyyyddmmhhmm.bin
or something like that.
The problem is that the build process doesn’t know which of the three machines is the target: Sulu, Checkov, or Fraiser. (Long story.) So, I manually look at the file name, copy it, and rename it. Of course, this is an error-prone process, and I’m basically lazy, so I decided to write a script to do it. I figured it would take just a minute to bundle up all the steps. I was wrong.
First Attempt
Copying the files to the right place was a piece of cake. I did check to make sure they existed. The problem came from launching PlatformIO, seeing the result on the screen, and being able to parse the filename out of the stream.
I thought it would be easy:
FN=$(pio run | grep '^Renamed to' | cut -d ' ' -f 3 )
That should do the build and leave $FN
with the name of the file I need to rename and process. It does, but there are two problems. You can’t see what’s happening, and you can’t tell when the build fails.
Easy Problem First
The pipeline consumes the build’s output. Of course, a tee
command can manage that, right? Well, sort of. The problem is that the tee
command sends things to a file and standard out, but the standard out, in this case, is the pipe. Sure, I could tee
the output to a temporary file and then process that file later, but that’s messy.
So, I resorted to a Bash-specific feature:
FN=$(pio run | tee /dev/fd/2 | grep ...
This puts the output on my screen but still sends it down the pipe, too. Sure, there are cases when this isn’t a good idea, and it isn’t very portable, but for my own use, it works just fine, and I’m OK with that. There are other ways to do this, like using /dev/tty if you know you are only using the script from a terminal.
Harder Problem
The bigger problem is that if the build fails — and it might — there isn’t a good way to fail the whole pipeline. By default, the pipe’s return value is the last return value, and cut
is happy to report success as long as it runs.
There are a number of possible answers. Again, I could have resorted to a temporary file. However, I decided to set a bash option to cause any failing item in a pipe to fail the whole pipe immediately:
set -o pipefail
So now, in part, my script looks like this:
set -o pipefail FN=$(pio run | tee /dev/fd/2 | grep '^Renamed to' | cut -d ' ' -f 3 ) if [ $? -eq 0 ] then echo Success... cp ".pio/build/STM32F103RC_creality/$FN" "configurations/$1" echo Result: "configurations/$1/$FN" else echo Build failed exit 3 fi
Final Analysis
Is it brain surgery? Nope. But it is one of those bumps in the road in what should have been a five-minute exercise. Maybe next time you run into it, you’ll save yourself at least 25 minutes. This gets the job done, but it isn’t a stellar example of bash programming. I would hate to run it through a lint-like checker.