Exit codes
==========
``disc`` follows the standard BSD ``sysexits.h`` exit-code vocabulary,
exposed in Python as the
`exit-codes `__ package's
:class:`ExitCode` enum. Using the well-known codes means scripts that
already understand ``sysexits.h`` from other tools — ``sendmail``,
``ssh``, ``rsync`` — recognise ``disc``'s codes without needing a
project-specific table.
The codes below are part of the CLI's public surface. New error
categories may map onto codes not yet listed here, but the meaning of
an existing code will not change without a major version bump.
Scripts may branch on any value listed here.
At a glance
-----------
.. list-table::
:header-rows: 1
:widths: 8 18 74
* - Code
- ``ExitCode``
- Meaning
* - ``0``
- ``OK``
- The command did what was asked.
* - ``64``
- ``USAGE``
- Command-line usage error: missing argument, unknown option,
bad option type, malformed in-image name. Emitted by Click
before any command logic runs, or by oaknut for name-validation
failures.
* - ``65``
- ``DATA_ERR``
- Invalid data on the disc itself — a corrupted catalogue,
inconsistent free-space map, repartition or merge structural
conflict, or an image that does not match its claimed filing
system. Also the fallback for any uncategorised
:class:`~oaknut.exception.DataError`.
* - ``70``
- ``SOFTWARE``
- An internal failure with no more specific category. This is
what an :class:`~oaknut.exception.InternalError` propagates
as when something escapes that ``disc`` was not expecting —
and the CLI prints a Python traceback alongside it, because
it is the report-an-issue signal.
* - ``72``
- ``OS_FILE``
- A ``INNER_PATH`` does not resolve to an entry on the disc (or
an AFS user does not exist).
* - ``73``
- ``CANT_CREATE``
- Cannot create the requested entry: name already in use, the
parent directory is full, the partition is full, the AFS
user's quota is exceeded, or the target directory is not
empty (when an ``rm`` would otherwise delete it).
* - ``74``
- ``IO_ERR``
- A host-side file operation failed (host permissions, host disc
full, missing host file, AFS host-tree import error). The
accompanying message names the host path.
* - ``77``
- ``NO_PERM``
- The target file is locked (and ``--force`` was not given), or
the current AFS user lacks the access rights for this
operation.
* - ``78``
- ``CONFIG``
- A runtime-environment / configuration problem. Not commonly
used by ``disc`` itself (which has little user-configurable
state) but reserved for programs built on top of the library.
Programming bugs (anything that escapes as an unhandled Python
exception) deliberately do *not* go through the catch-and-print path
— they propagate as a Python traceback to stderr, so the
report-an-issue path is obvious. A clean ``Error:`` line means the
failure is one ``disc`` knows about.
Error message format
--------------------
Every non-zero exit is accompanied by exactly one line on stderr,
prefixed with ``Error:`` and rendered in red on a colour-capable
terminal::
$ disc cat 'image.adl:$.MISSING'
Error: path not found: $.MISSING
$ echo $?
72
If the exception carries ``__notes__`` (PEP 678) or a ``__cause__``
chain, those follow as muted continuation lines underneath. Colour is
suppressed automatically when stderr is a pipe, when :envvar:`NO_COLOR`
is set, or when :envvar:`CLICOLOR` is ``0``.
``--debug``: see the traceback
------------------------------
``disc --debug …`` re-raises any caught
:class:`~oaknut.exception.DataError` or
:class:`~oaknut.exception.ConfigurationError` after printing it, so
the full Python traceback appears underneath. Use it during
development or when filing a bug report; users running normal scripts
should leave it off so a single ``Error:`` line is all they see.
:class:`~oaknut.exception.InternalError` is unaffected by
``--debug``: tracebacks are always shown for it, because that *is*
the signal a user should report.
Mapping to library exceptions
-----------------------------
Every filesystem-level failure raises a subclass of
:class:`oaknut.file.FSError`, which itself inherits from
:class:`oaknut.exception.DataError`. Each subclass carries its own
:class:`ExitCode` as a class attribute, so the CLI just reads
``exc.exit_code`` and exits — no separate mapping table is needed.
The same exception type that drives the code on the CLI side is what
library callers catch on the Python side — see
:doc:`/api/patterns/errors`. The two surfaces stay consistent: if a
script and an embedded Python program both wrap the same operation,
the error classification is the same.
Composing in scripts
--------------------
The most common idiom in shell scripts is "ignore the *expected*
miss, fail loudly on anything else". Branch on the specific code:
.. tab-set::
:sync-group: shell
.. tab-item:: bash
:sync: bash
.. code-block:: bash
# sysexits.h codes from /usr/include/sysexits.h on most Unixes;
# exit-codes ships the same numbers.
EX_OS_FILE=72
if disc cat "$IMAGE":'$.!BOOT' > boot.tmp 2>/dev/null; then
echo "has boot file"
else
status=$?
case "$status" in
$EX_OS_FILE) echo "no boot file" ;;
*) echo "disc cat failed: $status" >&2; exit "$status" ;;
esac
fi
.. tab-item:: PowerShell
:sync: powershell
.. code-block:: powershell
$ErrorActionPreference = 'Continue'
disc cat "${image}:`$.!BOOT" > boot.tmp 2> $null
switch ($LASTEXITCODE) {
0 { Write-Output 'has boot file' }
72 { Write-Output 'no boot file' }
default {
Write-Error "disc cat failed: $LASTEXITCODE"
exit $LASTEXITCODE
}
}
When you specifically want "tolerate this kind of miss", reach for
the per-command flag (``--force`` on ``disc rm``, ``-p`` on
``disc mkdir``, etc.) before branching on the exit code; the flag
downgrades the named class of error to a no-op while leaving
everything else loud.