“Exit traps” can make your Bash scripts more robust and reliable (2013)
redsymbol.net
redsymbol.net
I asked on the mailing list if this was expected behavior, and it turns out that POSIX only requires EXIT to run on a clean shutdown; to catch interruptions, add more signals.
trap 'eval $(ssh-agent -k)' EXIT INT ABRT KILL TERMAnd SIGKILL can't be handled, so that is indeed pointless.
I'm guessing this is some relic from 80s Unix systems where SIGKILL behaved different, or perhaps just an inconsistency/oversight.
The debugger could then allow the signal to go through to the process as is, or to replace it with another signal, or have it be ignored.
I made a program that looked like sh in ps, but actually was a simple debugger that just ran a specific other processes of mine. When that process hit a signal the debugger poked the signal number into a fixed location in the process' memory, then changed the signal to something innocuous like SIGALRM, and let that be delivered.
The process' SIGALRM handler would get the original signal number that the debugger had poked in, and print some obnoxious message like "Stupid sysadmin...your wimpy SIGxxx cannot hurt me!" where xxx was whatever signal someone had tried to send it.
I then told my fellow admins I had a stuck process that I could not kill, and asked them to kill it.
It took quite a while before someone got suspicious enough to suspect that the sh that was the parent of the "stuck" process wasn't actually a normal shell and try killing it.
trap 'ssh-agent -k' EXIT INT TERM
I don't see any reason for the eval as "ssh-agent -k" doesn't return anything useful you want the shell to evaluate.The "ssh-agent -k" command will emit shell commands that the shell must then execute which will kill the agent daemon and unset the socket environment variable.
Does it really? I've executed it here and it just runs kill, doesn't emit any bash. Running just ssh-agent (without any args) does that though, which is what's probably causing the confusion.
$ eval $(ssh-agent)
Agent pid 56785
$ ssh-agent -k
unset SSH_AUTH_SOCK;
unset SSH_AGENT_PID;
echo Agent pid 56785 killed;
The correct processing of that output requires an eval.Did you have any other questions?
$(ssh-agent)
won’t substitute that with the stdout and run that?
While redirecting to /dev/null will certainly work, the agent is holding sensitive credentials (by design), and confirmation of shutdown has a tangible security benefit.
$ ssh-agent
SSH_AUTH_SOCK=/var/folders/8p/_pwq997168s7vdwwdg_qr1j40000gn/T//ssh-DE0IoJfU5rrM/agent.15015; export SSH_AUTH_SOCK;
SSH_AGENT_PID=15016; export SSH_AGENT_PID;
echo Agent pid 15016;
$ SSH_AGENT_PID=15016; export SSH_AGENT_PID;
$ ssh-agent -k
unset SSH_AUTH_SOCK;
unset SSH_AGENT_PID;
echo Agent pid 15016 killed;
That said, it doesn't hurt to eval it, so I overstated my case in my original comment.In basic terms for my purposes these respectively account for a clean exit, the terminal emulator being closed, ctrl-c, the kill command (edit: the default SIGTERM kill -15, not the SIGKILL kill -9)
https://www.spinics.net/lists/dash/msg02208.html
"Signal terminations are not caught by EXIT. It only catches normal exits. Unfortunately, the EXIT condition is not well-defined by POSIX, so it's left to interpretation."
...
POSIX is making it unspecified what happens here:
https://austingroupbugs.net/view.php?id=621
The EXIT condition shall occur when the shell terminates normally (exits), and may occur when the shell terminates abnormally as a result of delivery of a signal (other than SIGKILL) whose trap action is the default.
It's a very common mistake with trap.
Also, zsh has a much nicer mechanism for the common case:
{
echo lol
} always {
# Ensure *all* temporary files are cleaned up.
nohup rm -rf / &
}Did you mean "it's hard" instead of "it's not hard"?
It took a few seconds before I thought... "Why does it take that long for only a handful of files?"
I never did that again.
Had my DOS filesystem mounted under Linux too (yes that long ago), and I spent a few days guessing the first letter of each deleted file with norton disk doctor or undeleter or something. That was fun (FAT16 filesystems overwrote the first letter of each filename to delete it)
At least it wasn't a mistake I made at work on some production thing. Though there is a reason I make all the desktops on windows production servers bright red. One time I was tired and shut down "my laptop" forgetting I was still logged into a remote server 200km away..... :/ Of course the iLO wasn't hooked up but I was extremely happy to find that HP servers listen to wake on LAN even when they're off. Another one for the never again books :P
with UNIX/POSIX systems it pays to say "it depends" often.
There's this idea that macOS is "based on BSD", but that's not really the case in any meaningful way; it took some components, but the system overall isn't really "based on it" as such.
That said, it makes sense that at least some distros would leave a job in place to do so but initially disabled so that the user can decide based on the use-case
how about you?
You can examine $? on entry to the trap function. On signals, it will be 128 + signal. i.e. on TERM (15) it will be 143. On INT (2) it will be 130.
#!/bin/bash
skip_exit=
on_exit() {
code=$?
if test $code == 130; then
skip_exit=1
fi
if test -n "$skip_exit"; then
return
fi
echo "Exiting with: $code"
return $code
}
trap on_exit INT EXIT
sleep 2
false
With ctrl-c: $ ./foo.sh
^C
After 2 seconds: $ ./foo.sh
Exiting with: 1
You can also setup separate handlers for each signal and use a sentinel: $ cat foo.sh
#!/bin/bash
skip_exit=
on_int() {
echo int
skip_exit=1
}
on_exit() {
test -n "$skip_exit" && return
echo exit
}
trap on_int INT
trap on_exit EXIT
sleep 2
With ctrl-c: $ ./foo.sh
^Cint
After 2 seconds:
$ ./foo.sh
exitThis changes things again. Often a process started by the shell will get the signal, too (depends of course on how it is sent) and exit with a non-zero return value (depends on the process of course). I believe (not at the computer right now) the handler for EXIT is called in that case.
Was it so that bash can also trap ERR, but dash cannot?
It's not perfectly easy to handle all possible cases, and certainly impossible in a fully portable way.
Highly debatable.
We use an exit handler that reports
"$0 has exited prematurely"
in cases the script did not reach expected exit points.Of course in special cases where you do explicit error handling you can disable it. But for the big mass of commands where nobody checks whether it worked.
> The behavior of set -e is quite unpredictable. If you choose to use it, you will have to be hyper-aware of all the false positives that can cause it to trigger, and work around them by "marking" every line that's allowed to fail with something like ||true.
Start there, then go back to the beginning for the extensive exposition against using set -e.
FWIW, I (a random person on the internet) use set -e for most scripts I write, and despite the caveats, find set -e is generally more beneficial than troublesome. I don't think Mr. Wooledge is wrong, it's just the groove I've settled in. I do sometimes consciously avoid using set -e, when I explicitly handle errors for every single element of the script.
I do not doubt that may bash scripters shoot themselves in the foot. You need at least one reviewer that understands well how the language works.
That said I prefer dash for scripting except when I really need arrays, which is rare. I have no scientific evidence, but KISS is good and bash just seems to have too many bells and whistles. And as the article mentions, bash seems to change in surprising ways between versions. I have also been bitten by that. Unfortunately dash has no set -o pipefail.
Regarding the expected behaviour: I believe this is largely a matter of preference/philosophy. I have previously used trap handlers in bash scripts explicitly so that the cleanup is run in every case and /tmp is not polluted - even of failures. Reasoning: For automated tasks or people not familiar with Linux/my script, the disk should not be filled up with old garbage data. And for debugging, I usually add a '-d/--debug' option to have more verbose output and disable the cleanup on Ctrl+C (but still cleanup on normal exit).
But as I said, I don't believe there is the one true way. So except for pointing out that I had to ensure external cleanup in case the script was run by some auomated tasks in a permanent environment (read: native), I wouldn't seriously complain if I ever used one of your scripts :)
That's perfectly fine – even desirable – behaviour for a whole bunch of use cases. The thing is you can opt-in to that trivially with "trap EXIT INT TERM ABRT", whereas the reverse is harder and much less obvious, so it seems like a better design to me (although a "trap ALWAYS" shortcut would be even better so you don't need to list signals).
At this point it's difficult to change due to compatibility as people's bash scripts would break in subtle ways, but it's one of my many annoyances with bash (my view is that people really ought to ditch bash in favour of a better shell – zsh being the most obvious incremental improvement, though far from the only option – but this always proves to be a controversial opinion).
Glossing over some complexity, but roughly:
add_cleanup(){
event on cleanup "$@"
}
trap "event emit 'cleanup'" HUP EXIT
start_postgres(){
add_cleanup stop_postgres
# actually start pg
}
start_apache(){
add_cleanup stop_apache
# actually start apache
}
I wrote a little about some other places where I've used it in https://www.t-ravis.com/post/shell/neighborly_shell_with_bas... and https://t-ravis.com/post/nix/avoid_trap_clobbering_in_nix-sh... (though I make the best use of it in my private bootstrap and backup scripts...)I must admit I find the code somewhat painfully terse and hard to read.
Still, interesting idea. I wonder if using a temporary SQLite/Berkeley DB/etc for queue might generalize the idea to a "Unix" event system - allowing other programs and scripts to use it for coordinating? (Like logger(1) does for logging)?
So keep the api, but support cross process queues, more or less.
For me it is "read only".
It is too arcane, even for me.
I use Perl now. I tried to reform last year as I was building a system from lots of executable pieces, the perfect job for Bash
After much pain and suffering I re-wrote it in Perl. What a (relative) breeze.
Just. Don't. Do. Bash.
Works for me!
I agree, shell programming is ugly and very unsafe.
> Anything more complex than that? Python, Perl, or any other proper programming language is there for you.
I disagree.
First, there are certain things, specifically when you want to process very large datasets that are easiest - by a very large margin - to build using shell scripts: a combination a ripgrep, sed, awk, grep, cut, tr, paste, jq, head, tail, etc ... is way easier and faster to put together in bash than anything else.
Second, to get maximum flexibility you'd like to be able to switch from one (python, perl) to the other (shell script) transparently and both ways
Do certain things that can be expressed cleanly in python, and then transparently switch to bash calling a horde of specialized shell commands when the task at hand is easier to express that way.
Shell can transparently go down to Python or Perl very, very easily.
Unfortunately, the converse is absolutely not true: while Perl can - to a certain extent - be used to construct complex pipelines of data processing external commands, it is nowhere near Shell in ease of use.
And it is a giant PITA in Python which gives you fuck-all above exec/fork level subprocess manipulation: writing large external pipelines in Python is about as easy as it is in C.
> > Bash is riddled with absurd footguns
> I agree, shell programming is ugly and very unsafe.
Sharp knives are "unsafe". Kids shouldn't use them. Professionals prefer them.I find most 'footguns' to be normal expected behavior, and the user shot themselves in the foot because they're careless or ignorant.
Both can be unsafe, but one is a professional tool while the other is an abomination for our current standards.
If Bash has taught me anything, it's that many advanced features should seldom be used. Always resist the temptation to be fancy.
The second is different events can trigger an exit trap, and those may have different implications on what's going on.
The third is there's parts of standards left out about what happens during/after a trap or when they get called, what data you have available, and different implementations can behave differently.
Fourth is that sometimes people will use an exit trap to, say, report on a failure, but they may have lost context of what block they were in when it exited, and now the error reporting doesn't tell you everything you wanted to know.
I can't remember more specifics atm because I stopped using them like a decade ago. I'll still use them to clean up temp files, but I also have to add the cleanup logic to the start of the script in case it didn't run.
If your cleanup logic is no more complicated than "perform some cleanup whenever the script exits for any reason, without concern for what state the things to clean up are in", I think trapping everything and calling the cleanup function is fine.
If you have to do anything more complicated, it's probably a better idea to stick all that logic into a non-bash program. You can do it in Bash if you know what you're doing, but it's going to be ugly, hacky, error-prone, and tedious.
Not using traps, it's clearer what happens at specific points in the execution of the rest of the code, so simpler to reason about how to deal with those cases as/where they happen.
You can stick all kinds of logic into the cleanup function, but again, it's ugly. The second is different events can trigger an exit trap, and those may have different implications on what's going on.
The third is there's parts of standards left out about what happens during/after a trap or when they get called, what data you have available, and different implementations can behave differently.
Which is why I think it's preferable not to do this in bash if you have any concern for why the program is exiting Fourth is that sometimes people will use an exit trap to, say, report on a failure, but they may have lost context of what block they were in when it exited, and now the error reporting doesn't tell you everything you wanted to know.
If you need any tracing, the only way this makes sense in bash is when running with -evx (errexit, verbose, trace) so you know exactly where you exited. This isn't always a bad idea, though -vx probably is most of the time.If you think you can do any complex logic in the trap function then you have to consider whether that logic might fail at any point, and depending on the signal there's a good chance you're on a clock as well.
# usage : utility NEW_DIRECTORY -- user : I think not
#
trap 'cleanup' INT HUP TERM
cleanup() { rm -rf "${mydir}" ;}
# stuff
mydir="${HOME}/$1" # but $1 is empty
if test -z "$1"; then
# handle; but while stuff is happening,
# user presses Ctrl-C
fiI start questioning whether I should be writing bash as soon as I hit about ten lines. I won't even consider writing functions or loops.
If I'm manipulating the system, I'm probably using configuration management, and for most other tasks, I'm using a full-blown programming language with a nice set of standard libraries.
E.g., Python and Ansible.
The concepts are simple, the implementation is a pain and (honestly) if you need something truly resilient you're probably better off leaning on a system that can provide that guarantee for you (like using a database for state storage).
"Secret Sauce", why is this secret at all.
Nothing against the author who's helping the ecosystem here, but is there an authoritative guide on Bash that anyone can recommend?
Hopefully something that's portable between Mac & Linux.
The web is full of contradictory guides and shellcheck seems to be the last line of defense.
I think this might be a bit easier to appreciate if you've ever worked at a young company and made choices because you have to (oh, we'll use MySQL, that sounds better than Postgres) and then give yourself an extra three months work in two years when you finally come up against a shortcoming in the tech. We have to make an awful lot of decisions and generally don't have the budget in time or money to fully grok the options we're deciding between.
More seriously, I think that we have been trained to rely on just in time searches (or ChatGPT sessions) when we encounter the next thing we need to learn. RTFM is just so time consuming and I personally don't recall everything I have read, leading me to rely on search/AI to re-learn the next thing just in time anyways.
In some ways this is a vast improvement, which is why it's the default behavior now. Why cream your brain with information you might never use?
But it DEFINITELY has a weakness in that you don't know what you don't know. I never knew about this 'trap' trick, for example... and I didn't know I didn't know it, despite it being something I see as quite useful.
Side note: I think RTFM has historically meant "try to find the answer first before asking for it", leading to me designating LMGTFY (Let Me Google That For You) as the modern equivalent in this just in time searches reality we live in. I wonder how long it will be before we start saying LMAAIFY (Let Me Ask AI For You)...
You can spend twice as much time over twice as many years reading random blog posts and googling, and you'll have no cohesive, comprehensive picture of the full tool and all its features. In this case, if you look at `man bash` and find the trap builtin function you'll learn about the DEBUG and ERR traps, for example, which I didn't see mentioned in the discussion here. These things might be useful to just file away; in case you ever need it someday, then you'll know it's there and exactly where to find it, not some half-remembered blog post you can't find again.
Over ten or twenty years, the difference between these two habits is night and day. The people who read the documentation first, and only then ask for help, and the people who ask for help first and get it and so never read the docs, end up in a totally different place with respect to overall confidence and comfort with the tools. Reading the man page means you don't get your answer right away, it's slower, less enjoyable, and less fun than googling. It's competing with content that was literally filtered by an engagement selection process. Of course it's less immediate gratification. The only reason people will do it is if they internalize the habit long enough to appreciate the benefits.
"RTFM" was a bit of social shaming, to tell people "don't be lazy, the answer you seek is literally in the documentation, please just read it". Shaming strangers on the internet turns out to not work well at scale, so generally we don't do this now, and the message just doesn't get passed on at all.
I've been shocked at the attitude even in some companies that reading docs is some kind of unnecessary or obsolete practice. As you say, it's become the default option to seek answers online. A culture of reading documentation still exists, but now it has to be maintained inside organizations that care about it, because it's no longer understood as the basic professional attitude.
> Over ten or twenty years, the difference between these two habits is night and day. The people who read the documentation first, and only then ask for help, and the people who ask for help first and get it and so never read the docs, end up in a totally different place with respect to overall confidence and comfort with the tools.
Ooof! This one's hit home.
There's incomplete documentation. There's API documentation without examples (ie specification but no example/tutorial). There's outdated documentation!
I can also say that I started studying SQL by reading the docs for mysql, and after an hour I was still stuck inside INSERT or SELECT. Reading about all use cases in detail was not useful to learn a first approach to the queries!
So I'd say that this "truism" isn't always true. Sometimes the docs suck or don't provide the info you need at that time.
Not that I regularly read them all myself, but it's nice to be rewarded with a new dark art (like alias-based ~metaprogramming) every once in a while.
Besides, it is slightly ignorant, and if I may say so, it can be a bit neurodivergent.
You see, manual pages are more often than not the wrong type of document to point people to.
`man` pages are information-oriented. You can (or should, if they are well written, which is not always the case, but that's another matter) find all the information about a piece of software there - they are references. As you say, it's something that needs to be digested before you can use it.
There's a certain kind of person, very commonly seen around computers, which can't help but digest a manual before they use a tool. Often they enjoy doing that. And that's fine. But that is not how everyone else does things.
People often have a particular problem they want to solve. They want to know how to zip a whole folder, or generate a ssh key with a particular algorithm. Whatever. Something concrete. They want to solve that, they don't want to "digest a document in order to solve that". That is not something those people enjoy, or are good at.
What those people need is a goal-oriented doc. Something that has a list of possible problems, and then gives solutions. Something that they can search for "whole directory" and find what they are looking for quickly. Something like a FAQ. This does not exist for all command lines (although the `tldr` app fits that well often enough for me).
Blogposts are often (yet another) type of documentation, they are learning-oriented. Like a tutorial.
The thing about blogposts is that they are indexed by Google, Bing and others. So in effect the combination of Google+Blogposts works like a big FAQ document.
Please understand that I am not trying to be offensive here.
You don't have to be born loving knowing how things work. But if you want to be a programmer (and most people don't) you should accept that the people who understand how things work are eventually going to run rings about people who don't. So you can either cultivate curiosity in how things work, or you can cultivate the habits and fake it till you make it. You put in the work if you want to develop a skill. There's no magic in it.
If you're not interested in being a programmer, then you don't need to waste your time reading man pages, obviously.
Programming is a vast ocean and very few people are going to know every single detail of ever single nook and cranny. If your day to day involves bash scripts, sure, learn and digest all of them. If you are a python programmer and you just want to compress a file you don't need to know all the flags that `tar` supports.
Easy! It is a reference manual. It documents every feature, even those that you should not use, does not discuss pitfalls, and does not discuss the best practice. Even worse, there is sometimes a disagreement whether using a particular feature is a good practice. Often there is a factor of adjusting your code style to something that is not too advanced for other people to review.
There seems to be a common sentiment that cargo culting random opinions from the internet is a "best practice" and that no code reviewer should ever have to learn anything new during the process. Both of these opinions are in my experience a fast track to a culture of mediocrity.
This guide is a great introduction and I refer back to it from time to time even after using Bash for ~15 years:
My opinion is that LLM pair programming is most or maybe only beneficial to already skilled programmers. ChatGPT can open the door for you, but it can’t show you where the door is. I needed the experience to ask it for a Bash script that handles exit codes gracefully, which is not a question all junior programmers would be able to ask.
```
failure() {
local lineno="$1"
local msg="$2"
echo "Failed at ${lineno}: ${msg}"
}trap 'failure "$LINENO" "BASH_COMMAND"' ERR
```
I'll complete with patterns I'm using for exit traps:
- for temporary files I have a global array that lists files to remove (and for my use case umount them beforehand)
- in the EC2 example, I add a line with just "bash", so I have an env with the container still running to debug what happened and I just need to close that shell to clear the allocated resources
while true; do
echo hello
test -f down && exit
doneNow to stop early you execute "touch down".
https://github.com/kidd/scripting-field-guide/blob/master/bo...
A nice bonus is that it also keeps the return value of the last non-callback function, so your script behaves better when called from other scripts.
Otherwise a good article. I use the following code to enable passing the signal name to the trap handler, so that I can kill the Bash process with the correct signal name, which is best practice for Unix signal handling (EXIT would have to be handled specially in `sig_rekill`):
# Set trap for several signals and pass signal name to trap function.
# https://stackoverflow.com/a/2183063/207384
trap_with_arg() {
func="$1" ; shift
for sig ; do
trap "$func $sig" "$sig"
done
}
sig_rekill() {
# Kill whole process group.
trap "$1"; kill -"$1" -$$
}
# Catch signal and kill whole process group.
trap_with_arg sig_rekill HUP INT QUIT PIPE TERM # All normal and error exits
trap 'e=$?; trap - EXIT; your cleanup here; exit $e' EXIT
# Error only trap
trap 'e=$?; trap - ERR; your error only cleanup here; exit $e' ERR
Save the previous exit condition to preserve it, otherwise it will be destroyed.Untrapping is necessary to prevent multiple calls, especially if it can call exit or fail within the trap handler.
You don't need an exhaustive list of signals, which is almost never correct in oft touted cargo culted examples.
You can report the error code with $? at the start of your trap, IIRC.
While it's certainly true that leaving around files with sensitive data is a security problem, you probably don't want to put sensitive data in /tmp to begin with.
umask 077
...before creating temp files then they won't be world-readable. Still lots of pitfalls. (What user are you running as, and who else is running as that user? What's the mount point file system, and does it have POSIX permissions? Why are you persisting secrets to disk in the first place? Etc.)In general, this can usually be mitigated to some extent by creating a directory to which only the owner has access.
There are a number of ... interesting ... other circumstances which you might want to consider:
- /tmp is mounted as a ramdisk / memory-only filesystem. This is guaranteed not to persist over reboots, though there may be residual artefacts in memory even after a power-off. That last isn't a significant concern for many people, though it may turn up for others.
- /tmp is a network share. This is uncommon, but NFS + sudo across shared systems means that a user on a remote system may be able to assume your credentials and access or modify your data. rootsquash means that root isn't available, but sudo means that any UID can be defined.
- Various filesystem permissions or limitations may or may not apply to /tmp. I tend to prefer mounting /tmp as its own filesystem, with nodev and nosuid set. There might also be noexec, which can foul up a lot of temporary installation scripts.
An alternative is for users to define their own preferred temporary directory. I usually include ~/tmp under $HOME.
I've read enough hacked-together bash BS to just despise the language.
My template script provides a way to make user-friendly shell scripts. In a script that uses the template, you define the dependencies and their sources as comma-separated values:
DEPENDENCIES=(
"gradle,https://gradle.org"
"warp-packer,https://github.com/Reisz/warp/releases"
"tar,https://www.gnu.org/software/tar"
"wine,https://www.winehq.org"
"unzip,http://infozip.sourceforge.net"
)
You define the command-line arguments: ARGUMENTS+=(
"a,arch,Target operating system architecture (amd64)"
"o,os,Target operating system (linux, windows, mac)"
"u,update,Java update version number (${ARG_JAVA_UPDATE})"
"v,version,Full Java version (${ARG_JAVA_VERSION})"
)
You define the "execute()" method that is called after the arguments are parsed: execute() {
// Make the computer do the work.
return 1
}
If the script takes arguments, handle each one individually: argument() {
local consume=2
case "$1" in
-a|--arch)
ARG_JAVA_ARCH="$2"
;;
-o|--os)
ARG_JAVA_OS="$2"
;;
esac
return ${consume}
}
Then call the template's main to start the script rolling: main "$@"
For 99% of the scripts I write, this provides:* Built-in software dependencies verification.
* Instructions to the user when requirements are missing.
* Simple command-line argument parsing.
* Help and logging using ANSI colour.
Here's a complete script that builds the Windows, Linux, and Mac installers for my Markdown editor:
https://github.com/DaveJarvis/KeenWrite/blob/main/installer....
There's a write-up about creating the script that has a lot more details about how the template works:
https://dave.autonoma.ca/blog/2019/05/22/typesetting-markdow...
Note that it is technically possible to improve the scripts such that handling individual arguments can be done in the template itself. This would require a slightly different argument definition semantics:
ARGUMENTS+=(
"ARG_JAVA_ARCH,a,arch,Target operating system architecture (amd64)"
"ARG_JAVA_OS,o,os,Target operating system (linux, windows, mac)"
"usage=utile_usage,h,help,Show this help message then exit"
)
By detecting an `=` symbol for the first item in the lists, it's possible to know whether a command-line argument is assigning a variable value, or whether it means to perform additional functionality. (PR welcome!)Enjoy. (blog post is mine)
trap EXIT WHATEVER -- cmd args
But as it is common with bash interfaces crystalized before good practices became apparent.3 keywords for normal, abnormal, and all exits would be semantically clear.
The best practice for a problematic language is to use something else.