💾 Archived View for thrig.me › blog › 2023 › 04 › 12 › unchecked-chdir-considered-harmful.gmi captured on 2023-09-28 at 16:22:48. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-04-19)
-=-=-=-=-=-=-
Too often one will see code along the lines of
#!/bin/sh cd somewhere something
At best this is a code smell, you'll maybe get some errors and go "whoops, my bad" while at worst it can be a total disaster.
The problem is that cd is not checked for errors, and something may run in the wrong directory. In other words, it's like saying "move the can of gasoline, then always play with the matches". Instead, use
#!/bin/sh cd somewhere && something
or
#!/bin/sh cd somewhere || exit 1 something
which is "move the gasoline, and only if that works play with the matches". There is also
#!/bin/sh set -e cd somewhere something
but some folks recommend against -e in scripts, especially longer ones.
http://mywiki.wooledge.org/BashFAQ/105
I use -e in short scripts, where short means a couple of lines and when there is nothing tricky like an if statement or a while loop.
/blog/2022/10/25/shell-while-loop-considered-harmful.gmi
The logic for the shell is maybe a bit weird in that 0 is success, so probably test this to get the logic aright.
$ false ; echo $? 1 $ true ;echo $? 0 $ false && echo okay $ true && echo okay okay
The && and || are just funny ways to say "and" and "or". Blame C, or maybe something earlier. The $? variable is a mangled form of the exit status word; blame sh for that.
In other languages, be sure to check the documentation to see exactly what happens so you can handle it.
http://man.openbsd.org/man2/chdir.2
https://perldoc.perl.org/functions/chdir
$ cfu 'if (chdir("/nowhere") == -1) exit(1); printf("okay\n")' $ cfu 'if (chdir("/etc") == -1) exit(1); printf("okay\n")' okay $ perl -E 'chdir "/etc" and say "okay"' okay $ perl -E 'chdir "/nowhere" and say "okay"' $ perl -E 'chdir "/nowhere" or die "nope: $!\n"; say "okay"' nope: No such file or directory
It is also nice to include the name of the directory that caused the failure so that someone does not need to process trace your code to figure out what directory is failing.
$ perl -E 'chdir "/x" or die "chdir failed '\''/x'\'': $!\n"; say "okay"' chdir failed '/x': No such file or directory
But that's getting a bit long for a one-liner, and probably should be uplifted to a script. Anywhere the shell quoting starts getting in the way, or the line is too long...
And here's something with an exception instead of a return value. Or rather the return value is fed into a condition system.
(require 'sb-posix) (defun saydir (dir) (handler-case (progn (sb-posix:chdir dir) (format t "~a~&" (sb-posix:getcwd))) (error (cond) (format *error-output* "~a - ~a~&" cond dir)))) (saydir "/etc") (saydir "/nowhere")
https://gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html
Or how about block-scoped working directories--the working directory is global to a process, but you can move it around automatically based on the scope of a variable.
#!/usr/bin/env perl use 5.36.0; use warnings; use Cwd 'getcwd'; use File::chdir; # $CWD $CWD = '/etc'; # chdir /etc (unchecked! but throws an error) say getcwd; eval { local $CWD = '/tmp'; say getcwd; # now in /tmp ... }; say getcwd; # we're back to /etc here
https://metacpan.org/pod/File::chdir
Directories can vanish or never be created for all sorts of reasons. Or maybe the code has a bug and has got the directory wrong. Or the permissions got borked after a restore from backups, or the filesystem is busy corrupting itself, or the hardware is failing. Do you continue assuming everything is okay, or stop and throw a noisy error that hopefully someone will see? Philosophies vary here.
Code gets copied and pasted, so you probably want the correct form in as many places as possible so there are better odds that the next intern copies something good. Therefore I always check my chdir calls, even if the subsequent commands are pretty harmless.
One approach is to have a build or configuration management system ensure that the necessary directories exist with the right permissions, so that chdir calls are less likely to fail. For example using Ansible you might ensure that /var/lock/mailman exists, as mailman may behave very poorly otherwise.
- name: create directory /var/lock/mailman file: path=/var/lock/mailman state=directory owner=root group=mailman mode=2775
A chaos monkey might remove random directories that software needs (maybe on a test system?) to see how well the software behaves and reports errors. If the software behaves poorly, then maybe it could be improved with better error messages and directory checks, or configuration management used to ensure that the environment is better known to be good.
tags #unix