Edit and Test XML Catalogue Mappings with xmlcatalog

One missing flag and xmlcatalog will happily overwrite the working catalogue you meant to only preview. You will create a catalogue, map an identifier to a URI, verify the lookup, then remove the entry without clobbering the file. The examples use xmlcatalog from libxml2-utils, version 2.9.14+dfsg-1.3ubuntu3.9.

Allow about ten minutes. You need a shell and a writable directory. Nothing here needs sudo; use elevated privileges only if the catalogue deliberately lives somewhere your account cannot write, and review the target path first.

1. Create a working catalogue

Start in a scratch directory, not a real one. --create writes a new catalogue to standard output by default; add --noout when you actually want it saved to the filename you gave.

$ workdir=$(mktemp -d)
$ catalog="$workdir/catalog.xml"
$ xmlcatalog --create --noout "$catalog"
$ sed -n '1,8p' "$catalog"
<?xml version="1.0"?>
<!DOCTYPE catalog PUBLIC "-//OASIS//DTD Entity Resolution XML Catalog V1.0//EN" "http://www.oasis-open.org/committees/entity/release/1.0/catalog.dtd">
<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog"/>

Checkpoint: the file should hold an empty catalogue with the XML catalogue namespace. Omit --noout and the same document just prints to standard output instead: the named file is never created or replaced.

2. Add a URI mapping

--add takes three arguments for an XML entry: the type, the original identifier, and its replacement URI. A uri mapping earns its keep when software asks libxml2 to resolve a known identifier and you want that redirected to a local or controlled resource.

$ xmlcatalog --add uri urn:example:doc https://example.test/doc.xml --noout "$catalog"
$ sed -n '1,12p' "$catalog"
<?xml version="1.0"?>
<!DOCTYPE catalog PUBLIC "-//OASIS//DTD Entity Resolution XML Catalog V1.0//EN" "http://www.oasis-open.org/committees/entity/release/1.0/catalog.dtd">
<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">
  <uri name="urn:example:doc" uri="https://example.test/doc.xml"/>
</catalog>

Safety warning: never use a production catalogue as your first test target. Saving an entry changes resolver behaviour for every application that reads that file. Keep a backup before editing an existing one:

$ cp --preserve=all /path/to/catalog.xml /path/to/catalog.xml.bak

Run the command without --noout and it is a preview: the changed catalogue prints to standard output, the input file stays untouched. Worth doing before you save anything:

$ xmlcatalog --add uri urn:example:next https://example.test/next.xml "$catalog"

Add --noout only once the displayed change is the one you actually want to keep.

3. Verify the mapping and its exit status

Give xmlcatalog the catalogue followed by the identifier and, with no --add or --del, it just performs a lookup:

$ xmlcatalog "$catalog" urn:example:doc
No entry for SYSTEM urn:example:doc
https://example.test/doc.xml
$ printf '%s\n' "$?"
0

The extra system lookup message is normal for a URI-only entry; the URI result and status 0 are what actually matter. A missing identifier produces messages like No entry for URI and returns status 4 instead.

In a script, grab the status immediately after the lookup:

if resolved=$(xmlcatalog "$catalog" urn:example:doc 2>/dev/null); then
    printf '%s\n' "$resolved"
else
    status=$?
    printf 'catalog lookup failed with status %s\n' "$status" >&2
    exit "$status"
fi

Do not assume the first output line is the replacement: a catalogue can report more than one attempted lookup. Need a quiet, machine-friendly interface instead? Use libxml2's resolver in your application and treat this command as an inspection tool, not a library.

4. Remove an entry without guessing

--del takes the value to remove, and like --add it prints the resulting catalogue unless you supply --noout. Preview the deletion first:

$ xmlcatalog --del urn:example:doc "$catalog"
<?xml version="1.0"?>
...
<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog"/>
$ xmlcatalog "$catalog" urn:example:doc
No entry for SYSTEM urn:example:doc
No entry for URI urn:example:doc

Destructive action: the preview above did not touch the file. This one does:

$ xmlcatalog --del urn:example:doc --noout "$catalog"
$ xmlcatalog "$catalog" urn:example:doc
No entry for SYSTEM urn:example:doc
No entry for URI urn:example:doc

Removed the wrong entry? Restore the backup you made before editing:

$ cp --preserve=all /path/to/catalog.xml.bak /path/to/catalog.xml

For a shared catalogue, coordinate the change with the applications reading it. There is no transaction or service reload built into xmlcatalog: a process may see the old or new contents purely depending on when it happened to open the file.

5. Inspect a catalogue interactively

Reach for --shell when you want several read or edit operations in one session. Its commands include dump, public, system, resolve, add, del, debug, quiet, and exit.

$ xmlcatalog --shell "$catalog"
> dump
<?xml version="1.0"?>
...
> system "-//Example//DTD Widget 1.0//EN"
> exit

Quote identifiers that contain spaces or punctuation. This shell still operates on the named catalogue, so treat its add and del as state-changing, and keep the backup until you have verified the result.

6. Check environment and common failures

libxml2 also consults the XML_CATALOG_FILES environment variable: a space-separated list of catalogues, with percent-encoding for spaces and other special characters. Set it to empty and you disable loading the default /etc/xml/catalog for the relevant resolver behaviour.

$ printf '%s\n' "${XML_CATALOG_FILES-}<unset>"
$ env XML_CATALOG_FILES= xmlcatalog "" urn:example:doc
No entry for SYSTEM urn:example:doc
No entry for URI urn:example:doc

That last command deliberately queries the default system catalogue shortcut with the override empty. Do not set this variable globally just to make one test pass: it changes how applications resolve external entities and schemas everywhere.

Done means