Skip to content

Commit 5733078

Browse files
committed
Add Fish and $pipestatus post
1 parent a7c59dc commit 5733078

File tree

1 file changed

+215
-0
lines changed

1 file changed

+215
-0
lines changed

Diff for: content/posts/2024-07-30-fish-and-pipestatus.md

+215
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
+++
2+
title = 'Fish and pipestatus'
3+
date = 2024-07-19T17:00:00-04:00
4+
categories = 'tutorial'
5+
draft = true
6+
tags = ['shell', 'Fish']
7+
+++
8+
9+
# Fish and `$pipestatus`
10+
11+
_Note: This morning I answered [a Reddit
12+
post](https://www.reddit.com/r/fishshell/comments/1efpruv/about_checking_pipestatus/)
13+
about `$pipestatus`, and after learning that there's not a lot of good information out
14+
there, I thought making a blog post might be helpful for other future Fish users wanting
15+
to know more about using Fish's `$pipestatus`._
16+
17+
## What's an exit status?
18+
19+
In Fish, like other shells, you can tell whether a command failed via its return value,
20+
sometimes called an exit code or an exit status. An exit status is a number between 0
21+
and 255 (well, really 0-254, with 255 signaling any out-of-range exit code above 254).
22+
Canonically, an exit status of 0 means success, while any non-zero value indicates an
23+
error.
24+
25+
_Note: Don't make the mistake of thinking 0 means FALSE and 1 means TRUE - that's not
26+
how exit statuses work, and is in fact the exact opposite of what they mean in this
27+
case._
28+
29+
According to the [Linux Documentation
30+
Project](https://tldp.org/LDP/abs/html/exitcodes.html), certain exit codes are reserved
31+
to have certain meanings:
32+
33+
| Exit Code | Meaning |
34+
| ---------------- | ------------------------------ |
35+
| 0 | Success |
36+
| 1 | Catchall for general errors |
37+
| 2 | Misuse of shell built-ins |
38+
| 126 | Command invoked cannot execute |
39+
| 127 | Command not found |
40+
| 128 | Invalid argument to exit |
41+
| 128+n | Fatal error signal "n" |
42+
| 130 | Script terminated by Control-C |
43+
| 255 | Exit status out of range |
44+
45+
In Bash or Zsh, the special variable `$?` holds the exit status. In Fish, you can see
46+
the exit status from a command if you examine the contents of the special `$status`
47+
variable.
48+
49+
Now, `$status`, like `$?` in POSIX shells, is very volatile. Every Fish command you run
50+
changes the value of `$status`. It is common to see scripts where the exit status is
51+
stored in a variable so that it does't get blown away by subsequent commands. So, in
52+
Fish, that might look something like this:
53+
54+
```fish
55+
somecmd --arg1 foo bar
56+
set --local last_status $status # store off the status
57+
if test $last_status -ne 0
58+
# Write to stderr and return the status
59+
echo >&2 "Your command errored with status: $last_status..."
60+
return $last_status
61+
end
62+
```
63+
64+
If you tried to use `$status` here without saving it in a variable, every new command
65+
you run in the script would have modified its value with their own exit status. So in
66+
this case, `somecmd`, `test` and `echo` are commands, and each will change `$status`.
67+
68+
Do to its volatility, you need to use or store `$status` immediately after a command
69+
completes, otherwise it will change when the next command runs and you will lose its
70+
prior value.
71+
72+
## Piping commands
73+
74+
When shell scripting, it is common to pipe commands. What we mean by piping commands is
75+
running a command and piping its output as the input to another command, and so on.
76+
Let's take a quick example in Fish where we print the URL to this blog post and pipe
77+
that to `string match` to pull out the slug name:
78+
79+
```fish
80+
> echo "https://mattmc3.github.io/posts/2024/07/fish-and-pipestatus/" |
81+
> string match -rg '/([^/]+)/$'
82+
fish-and-pipestatus
83+
```
84+
85+
Here we just piped 2 commands, but you could pipe many more:
86+
87+
```fish
88+
# How many blog posts did I write this year?
89+
ls | grep '2024' | wc -l | string trim
90+
```
91+
92+
Remember how we just said `$status` is volatile? Well, now we have 4 commands we just
93+
ran together, and each would change the value of `$status` as it went along, leaving
94+
`$status` to only reflect the result of running `string trim`.
95+
96+
Enter `$pipestatus`.
97+
98+
## Getting the exit status from piped commands
99+
100+
Bash and Zsh have a way of handling command piping - setting the `$PIPESTATUS` or
101+
`$pipestatus` variables respectively. These variables are arrays which contain the exit
102+
status of each individual command. Let's demonstrate how that works in Bash:
103+
104+
```bash
105+
$ true | false | true | true # Fake 4 piped commands
106+
$ echo ${PIPESTATUS[@]} # Show the exit status of those 4 cmds
107+
0 1 0 0
108+
```
109+
110+
For many years, Fish did not have an equivalent. But in 2019, [ticket
111+
#2039](https://github.com/fish-shell/fish-shell/issues/2039) was closed and Fish gained
112+
its own `$pipestatus` variable.
113+
114+
Now, let's demonstrate that same Bash script in Fish:
115+
116+
```fish
117+
> true | false | true | true
118+
> echo $pipestatus
119+
0 1 0 0
120+
```
121+
122+
Behold! The magic of `$pipestatus`!
123+
124+
## Using `$pipestatus`
125+
126+
Now that we know about `$pipestatus`, it might be tempting to test for errors like so:
127+
128+
```fish
129+
# Oops... this looks like it should work, but it won't!
130+
true | false
131+
if test $pipestatus[1] -ne 0 || test $pipestatus[2] -ne 0
132+
echo >&2 "An error was found: $pipestatus" # <-- Unreachable code!
133+
end
134+
```
135+
136+
This fails spectacularly! What happened? Remember how we talked about how volatile
137+
`$status` is? Well, `$pipestatus` is the same - it changes after every new command. In
138+
the above script, when the first `test` was called, it reset `$status` and
139+
`$pipestatus` to reflect its own exit status, and then when `test $pipestatus[2]` tries
140+
to run, `$pipestatus` no longer has 2 elements.
141+
142+
So what's the fix? Store the results of `$pipestatus` **IMMEDIATELY** in a variable, and
143+
then use that variable to do your error handling.
144+
145+
```fish
146+
# This works since we save the volatile $pipestatus contents
147+
true | false
148+
set --local last_pipestatus $pipestatus # Save ourselves pain!
149+
if test $last_pipestatus[1] -ne 0 || test $last_pipestatus[2] -ne 0
150+
echo >&2 "An error was found: $last_pipestatus"
151+
end
152+
```
153+
154+
Remember - `$status` and `$pipestatus` are weird Schrödinger's variables. They change
155+
every time you peek in the box. Every. Single. Command.
156+
157+
That's not unique to Fish. Bash has the same behavior:
158+
159+
```bash
160+
$ true | false | true | true # simulate piping in Bash
161+
$ echo ${PIPESTATUS[@]} # echo is about to change $PIPESTATUS
162+
0 1 0 0
163+
$ echo ${PIPESTATUS[@]} # AND... it did
164+
0
165+
```
166+
167+
## Better ways to test $pipestatus
168+
169+
Let's say you have 6 piped commands, do you really have to test each result like this?
170+
171+
```fish
172+
# Don't do this
173+
true | false | true | true | false | true
174+
set --local last_pipestatus $pipestatus
175+
if test $last_pipestatus[1] -ne 0
176+
or test $last_pipestatus[2] -ne 0
177+
or test $last_pipestatus[3] -ne 0
178+
or test $last_pipestatus[4] -ne 0
179+
or test $last_pipestatus[5] -ne 0
180+
or test $last_pipestatus[6] -ne 0
181+
182+
echo >&2 "error"
183+
end
184+
```
185+
186+
The answer is no. This gets really messy to read and offers no benefit. You could
187+
choose to treat your array like a string:
188+
189+
```fish
190+
# Better, but don't do this either
191+
if test "$last_pipestatus" != "0 0 0 0 0 0"
192+
echo >&2 "error"
193+
end
194+
```
195+
196+
This is certainly more readable, but what if you miscounted your pipes and got the
197+
number wrong? This can lead to subtle bugs. Instead, my preferred test for `$pipestatus`
198+
is `string match -qr '[^0]' $last_pipestatus`. What this does is quietly (`-q`) tests
199+
every element in our saved `$last_pipestatus` array with a regex (`-r`) pattern looking
200+
for any non-zero value (`[^0]`).
201+
202+
Here's the full example:
203+
204+
```fish
205+
# This test works no matter how many things we have piped
206+
true | false | true | true
207+
set --local last_pipestatus $pipestatus
208+
if string match -qr '[^0]' $last_pipestatus
209+
echo >&2 "Errors were seen: $last_pipestatus"
210+
end
211+
```
212+
213+
## Conclusion
214+
215+
Hopefully this was a helpful introduction to handling exit statuses in Fish.

0 commit comments

Comments
 (0)