Skip to content
pb-diff Command-Line Tool Reference

Command-Line Tool Reference

pb-diff-cli.exe is the headless half of pb-diff: everything the desktop application computes, without a window. It never creates a user interface, never prompts and never waits on standard input, so it is safe in a build step, a hook or an agent loop. Results go to standard output, as JSON when asked. Progress and errors go to standard error, and the outcome is reported as an exit code.

This document has two parts. The first, Using pb-diff-cli from a script or an AI agent, is for whoever writes a build step, a CI gate or a hook, or sets up an agent: the typical workflows, and how to read what comes back. The second is the complete reference, command by command, from Global options on. The workflows link to the reference rather than repeat it.

pb-diff-ci.exe is its sibling for build machines: the same commands where it has them, no licence, no network, and answers that stop at the level of the object. It has a part of its own below, pb-diff-ci: the tool for build machines, and every command section says whether the command is also in pb-diff-ci.

The desktop application is pb-diff.exe and takes a separate switch set, for source-control clients and the PowerBuilder IDE; see The desktop command line in pb-diff Help. This document covers pb-diff-cli.exe and pb-diff-ci.exe.

pb-diff-cli <command> [arguments] [options]

Using pb-diff-cli from a script or an AI agent

pb-diff-cli reads PowerBuilder libraries (.pbl, .pbd, and libraries appended to an .exe or .dll), exported objects and folders of sources, compares them section by section rather than line by line, and merges three versions of an object. It needs no PowerBuilder installation and no ORCA.

The one rule

Always pass --format json. Text output is written for people and may be reworded between releases; only the JSON is a contract. Every JSON payload declares schemaVersion: 1 and has a published schema, and report honours --format json like every other command. Parse the JSON; never scrape the text.

Then read the exit code before the payload, and branch on the specific code: 0 is success or no differences, 1 is differences found, 2 is unusable input, 3 is a merge that left conflicts, 4 is a bug and 5 is no valid licence. Exit code 1 is an answer, not a failure: treating "non-zero" as "something went wrong" turns "these two releases differ" into a broken build. On exit 2 standard output is normally empty and the error object is on standard error; branch on its code. validate is the exception: when a library it was given is damaged, unreadable or missing, it exits 2 with its payload on standard output and no error envelope on standard error. See Exit codes and Errors.

Prefer --out <file> to a pipe when another program reads the JSON. In Git Bash on Windows, a pipe from a .NET program such as pb-diff-cli into the Windows Python interpreter can arrive empty: pb-diff-cli help --format json | python -c "import json, sys; json.load(sys.stdin)" fails at line 1, column 1, and nothing says why. Microsoft's own dotnet command does the same, while the same pipe works in PowerShell and cmd.exe, and cat, wc and node read it whole in Git Bash. A file written with --out reads the same in every shell. Nothing else changes: read the exit code first, because on an input error the file is empty and the error object is still on standard error, which 2> error.json keeps in a file too. For validate's exception, --out keeps the payload in the named file even on exit 2.

The commands

pb-diff-cli info        <container>              # flavour, format version, comment, counts
pb-diff-cli list        <container>[!<glob>]     # enumerate entries
pb-diff-cli export      <container>[!<glob>]     # write importable .sr* files
pb-diff-cli extract     <container>[!<glob>]     # write raw payloads (compiled objects, resources)
pb-diff-cli validate    <path>...                # libraries checked for damage; one state each
pb-diff-cli parse       <file|container!entry>   # the object's hierarchy: functions, events, controls
pb-diff-cli compare     <old> <new>              # two-way compare, section by section
pb-diff-cli compare-lib <old> <new>              # summary compare of libraries, folders or files
pb-diff-cli merge3      <file1> <base> <file2>   # three-way auto-merge; conflicts reported, not written
pb-diff-cli report      <kind> <old> <new>       # pdf, html, csv, json, rtf or markdown
pb-diff-cli check       <path>...                # sources left broken: conflict markers, unclosed blocks
pb-diff-cli tokens      <file> | --line "<text>" --lang <lang>   # the lexer behind the colouring
pb-diff-cli license     [status|activate <key>|deactivate]       # this machine's licence
pb-diff-cli schema      [name]                   # the published names, or one schema
pb-diff-cli help        [command]                # usage; add --format json for the full catalogue

An entry inside a library is addressed as container!entry, such as ui.pbl!w_main.srw; see Addressing entries. The tool describes itself, so a script or an agent can discover the surface without reading this document: pb-diff-cli help --format json is the whole catalogue as data, the only machine-readable help (there is no --json alias), and pb-diff-cli schema <name> prints the JSON Schema of a payload.

Inventory a library before touching it

pb-diff-cli info ui.pbl --format json
pb-diff-cli list ui.pbl --kind source --format json
pb-diff-cli validate ui.pbl --format json

info gives the flavour, the format version and the counts. list gives every entry with its kind and a finer type; only source entries can be exported or compared in detail, and compiled entries are P-code that pb-diff does not decompile. validate walks the structure: exit 0 is clean, 1 is warnings only, 2 means a library is damaged or could not be verified. Run it before trusting a library you did not build, and over a whole application - pb-diff-cli validate app.pbw - to find the damaged ones among them.

To read the code itself, export it:

pb-diff-cli export ui.pbl --out-dir ./src --format json      # every source object
pb-diff-cli export "ui.pbl!*.srw" --out-dir ./windows        # just the windows
pb-diff-cli export "ui.pbl!w_main.srw" --stdout              # one object to standard output

Understand an object before changing it

pb-diff-cli parse w_main.srw --format json
pb-diff-cli parse "ui.pbl!w_main.srw" --lines --format json      # straight out of the library
pb-diff-cli parse d_customer.srd --format json                   # bands, controls, database columns, properties

You get every function, event, control and script, each with a stable key and a contentHash of its comparable text, the cheap way to ask whether something changed. A file whose name a source-control tool mangled is read with --detect:

pb-diff-cli parse 3f9a2c.tmp --detect H --format json            # read its $PBExportHeader$, or what its content opens as
pb-diff-cli parse d_test.srd.tmp --detect F --format json        # trust the first extension

See parse for what the document holds, and what usedFallback and diagnostics mean.

See what changed between two releases

pb-diff-cli compare-lib old/ui.pbl new/ui.pbl --changed-only --format json
pb-diff-cli compare-lib release-1.5/ui.pbl src/ws_objects --format json   # a library against its source
pb-diff-cli compare "old/ui.pbl!w_main.srw" "new/ui.pbl!w_main.srw" --format json
pb-diff-cli compare old/w_main.srw new/w_main.srw --unified         # a unified diff, for reading

compare-lib takes a library, a folder or a single file on each side, and compares them as the desktop application's Compare does: a ws_objects folder, a PowerBuilder 2025 Solution or one of its PBL folders, a folder of exported .sr* files or a folder holding libraries, against another or against a library. A name two inputs on one side share is not paired by guessing; both rows are listed, under their addresses, and notes says why.

compare-lib lists the entries with a status, one of six: unchanged, new, modified, deleted, renamed-unchanged and renamed-modified. The last two arrive only for entries you paired with --pair old=new, because renames are never inferred; counts folds renamed-modified into modified, so the per-entry status is the only place a rename reads as a rename, and a script that branches on four of the six loses a renamed object. compare returns the object's hierarchy with a status per node, and only the changed nodes unless --all is given, so walking children gives exactly what changed. See compare-lib and compare.

Gate a build on what changed

compare-lib --changed-only is the whole gate: exit 0 nothing changed, exit 1 something did, exit 2 the input was unusable. Branch on the three, and do not collapse 1 and 2 into "failed". Either side may be a folder, so a repository's ws_objects or Solution is gated the same way as a library. On a build machine the same gate runs in pb-diff-ci, which needs no licence; see pb-diff-ci. In PowerShell, which works in Windows PowerShell 5.1 and in PowerShell 7:

$raw = pb-diff-cli compare-lib baseline/ui.pbl build/ui.pbl --changed-only --format json | Out-String
switch ($LASTEXITCODE) {
    0       { 'No object changed.' }
    1       { ($raw | ConvertFrom-Json).entries | ForEach-Object { '{0,-9} {1}' -f $_.status, $_.name } }
    default { throw "Comparing the libraries failed with exit code $LASTEXITCODE." }
}

The same gate in a POSIX shell with jq:

out=$(pb-diff-cli compare-lib baseline/ui.pbl build/ui.pbl --changed-only --format json)
case $? in
  0) echo "No object changed." ;;
  1) printf '%s\n' "$out" | jq -r '.entries[] | "\(.status)\t\(.name)"' ;;
  *) echo "Comparing the libraries failed." >&2; exit 2 ;;
esac

Run from the Order Desk examples installed with pb-diff (docs/examples/order-desk beside the tool), the gate over libraries/release-1.4/orders.pbl and libraries/release-1.5/orders.pbl prints eleven lines, from modified d_order_lines.srd to deleted w_print_preview.srw. The examples folder holds the published result of every command pb-diff Help runs, under results/, each produced by this version of the tool from the files beside it.

Merge three versions and check the result

pb-diff-cli merge3 mine.srw base.srw theirs.srw --output merged.srw --format json

The order is file1, base, file2: the common ancestor is in the middle, and all three must be the same object type. Exit 0 means the merge was clean and merged.srw was written. Exit 3 means conflicts remain and nothing was written; conflicts carries one entry per unresolved region, with the three versions of it, so there is everything needed to decide without reading the files again. The merge of three-way/mine, three-way/base and three-way/theirs in the Order Desk examples exits 3 with this entry:

{
  "key": "function:of_discount(decimal)",
  "name": "of_discount (decimal ad_total)",
  "baseStart": 5,
  "baseLength": 1,
  "file1Start": 5,
  "file1Length": 1,
  "file2Start": 5,
  "file2Length": 1,
  "baseLines": [
    "\treturn 0.05"
  ],
  "file1Lines": [
    "\treturn 0.06"
  ],
  "file2Lines": [
    "\treturn 0.07"
  ]
}

A script that stops for a decision, and shows the three versions of each region:

$raw = pb-diff-cli merge3 mine.srw base.srw theirs.srw --output merged.srw --format json | Out-String
switch ($LASTEXITCODE) {
    0       { 'Merged cleanly into merged.srw.' }
    3       {
        foreach ($conflict in ($raw | ConvertFrom-Json).conflicts) {
            "$($conflict.name), base line $($conflict.baseStart)"
            "  base:  $($conflict.baseLines -join ' / ')"
            "  file1: $($conflict.file1Lines -join ' / ')"
            "  file2: $($conflict.file2Lines -join ' / ')"
        }
    }
    default { throw "merge3 failed (exit code $LASTEXITCODE)." }
}

Resolving the conflicts is the caller's part, in two steps. Decide each one from the three versions, and write the whole merged object; never leave a conflict marker in it. Then check what was written: pb-diff-cli compare base.srw merged.srw --format json, read twice over. inputDiagnostics names what a parser found wrong with either file, with side saying which and a stable code: parse.conflict-markers if a marker was left behind, parse.unterminated-block if a paste landed short. The hunks are where anything else that was not meant to be written shows up, whatever caused it. pb-diff-cli parse merged.srw --format json gives the same codes for one file with nothing to compare it against, and pb-diff-cli check merged.srw --format json the codes alone, for a whole folder at once: exit code 1 when something was found. A diagnostic is a signal in one direction: an entry means the file is damaged, an empty list means nothing was noticed.

--force-partial writes a file despite conflicts, taking the base for each one; use it only when a partial result is genuinely useful downstream. See merge3 for what the written file is and the warnings a merge reports.

Write a report for a person

--report-format chooses the rendition a reader gets; --format json still describes, on standard output, the file that was written:

pb-diff-cli report summary-changes old.pbl new.pbl --output changes.html --format json
pb-diff-cli report changes old/w_main.srw new/w_main.srw --output w_main.pdf --format json
pb-diff-cli report summary-changes old.pbl src/ws_objects --report-format markdown >> summary.md

The Markdown rendition of the two summary reports is written for a CI job summary or a pull-request comment. See report.

Run it on a build server or in CI

A CI job uses pb-diff-ci: copy pb-diff-ci.exe to the build machine, hosted runners included, and run it. It needs no licence and no network, and it has the gate, the summary reports and check; see pb-diff-ci, and Run pb-diff in CI in pb-diff Help for complete pipelines.

pb-diff-cli needs a valid licence for every command but help, schema and license, and without one exits 5 with LICENSE_REQUIRED before it reads an input or writes an output. A build server that needs a command only pb-diff-cli has - compare, merge3, export - activates its service account on a named subscription seat, as a person does: pb-diff-cli license activate <key> --label "build server" once, run as that account. The machine then verifies its licence online at most once a day, and keeps working for 7 days without the licensing service.

pb-diff-cli license status --format json says which licence is in use and why a command would be refused; it exits 0 when licensed and 5 when not. See license.

What pb-diff-cli will not do

Setting up an AI agent

An agent follows the same rules as a script. The package installs an agent skill beside the tool, in skills/pb-diff/: operating rules an agent loads, pointing into this document. To use it with Claude Code, copy that folder into .claude/skills/pb-diff/ inside the repository being worked on, or into ~/.claude/skills/pb-diff/ for every repository at once. The skill drives the pb-diff-cli already on the machine and finds the rest of the surface through pb-diff-cli help --format json and pb-diff-cli schema.


pb-diff-ci: the tool for build machines

pb-diff-ci.exe is pb-diff for a build machine. It is one file, installed beside pb-diff-cli.exe and shipped in the ZIP, and it runs copied on its own to any Windows machine, a hosted CI runner included, with nothing else installed: no .NET, no PowerBuilder, no pb-diff.

pb-diff-ci info        <container>              # flavour, format version, comment, counts
pb-diff-ci list        <container>[!<glob>]     # enumerate entries
pb-diff-ci validate    <path>...                # libraries checked for damage; one state each
pb-diff-ci compare-lib <old> <new>              # summary compare of libraries, folders or files
pb-diff-ci report      <summary|summary-changes> <old> <new>   # pdf, html, csv, json, rtf or markdown
pb-diff-ci check       <path>...                # sources left broken
pb-diff-ci schema      [name]                   # the published names, or one schema
pb-diff-ci help        [command]                # usage; add --format json for its catalogue

The same commands. Each of these is the pb-diff-cli command of the same name, with the same arguments, options, defaults, payload and exit codes, so a pipeline moves from one tool to the other by changing the executable's name. The sections under Commands describe both and say which commands pb-diff-ci has. Two differences, both because of what pb-diff-ci never shows: report renders the summary and summary-changes kinds only and has no --node, and schema publishes the schemas of its own commands and the error envelope. Naming a command it does not have - compare, merge3, parse, export, extract, tokens, license - exits 2 with UNKNOWN_COMMAND, and the hint says that the command is in pb-diff-cli.

Exit codes. The codes of Exit codes with the same meanings, less the two it can never return: no merge leaves conflicts, since it has none, and no licence is ever missing. pb-diff-ci help --format json lists the four it has.

The footer. Every report pb-diff-ci writes, in every rendition but JSON, ends with this line, linked where the rendition has links:

Generated by pb-diff-ci version. To see what changed inside each object, open the comparison in pb-diff: https://pbdiff.com

In CSV it is the last row, alone in the first column after an empty row, so a script that reads the file by column count stops at the empty row. pb-diff-cli reports carry no footer.

Usage terms. pb-diff-ci may be used under the terms on the licence page, which pb-diff-ci help names.


Global options

Accepted by every command.

Option Meaning
--format <text|json> Output format. text is for humans and may be reworded between releases; json is the contract.
--out, -o <path> Write the payload to a file instead of standard output. The file is written, in one replacement, only when the command has run to its end, whatever its exit code; a command that fails with an error leaves an existing file as it was. A path naming a file stream (app.pbl:notes) is refused.
--quiet Suppress successful human-readable text written to standard output. JSON, raw payloads, errors and files named with --out are unchanged. Overrides --verbose.
--verbose Report progress on standard error. Never on standard output. --quiet takes precedence.
--help, -h, -? Show help for the command.
@<file> Read further arguments from a command file. Not the value of an option that takes one: --line "@x" is the text @x.
-- Ends the options: every argument after it is a positional taken as written, so a file whose name starts with - or @ can be named.

Options that take a value accept a separate token or =, for example --format json or --format=json. The separate token is the canonical spelling, and the one every example in this reference uses; = is an accepted alternative. A flag (an option declared with takesValue: false in the catalogue) never consumes the following token: check --quiet broken.srw clean.srw checks both files. To set a flag explicitly, use =true or =false, for example --quiet=true or --quiet=false (also with its short spelling, where available). Only true and false, case-insensitively, are accepted after =; another value is BAD_ARGUMENTS (exit 2). The old --quiet false form no longer sets the flag's value: false is a positional argument.

An empty argument, or an option given an empty or blank value - what an unset variable in a script produces - is BAD_ARGUMENTS (exit 2).

Unknown options are rejected rather than ignored: a typo fails with exit code 2 instead of silently changing what the command does. Excess positional arguments are BAD_ARGUMENTS (exit 2), unless the command declares a repeatable positional argument, such as the paths of check.

Addressing entries

A path with no ! is a plain file. container!entry addresses one entry inside a PowerBuilder library, which may be a .pbl, a .pbd, or a library appended to an .exe or .dll:

pb-diff-cli parse "ui.pbl!w_main.srw"

Entry names are compared case-insensitively. Where a list of entries is meaningful, the entry part accepts the * and ? wildcards:

pb-diff-cli export "ui.pbl!*.srw" --out-dir .\windows
pb-diff-cli list   "ui.pbl!d_cust*"

Quote the argument in a shell that would otherwise expand ! or * itself.

PowerBuilder 2025 Solutions

pb-diff compares, reports on and merges the object sources of PowerBuilder 2025 Solutions, as PowerBuilder 2025 and later write them, and a source it merges builds in the Solution. In a Solution each library is a PBL folder named like the library file it replaced (app.pbl\), holding one UTF-8 source per object with no $PBExportHeader$ line and the object comment as a first line //objectcomments <text>. Name those files wherever a file is asked for:

pb-diff-cli compare old\app.pbl\w_main.srw new\app.pbl\w_main.srw --format json
pb-diff-cli merge3 mine\w_main.srw base\app.pbl\w_main.srw theirs\w_main.srw --output base\app.pbl\w_main.srw --format json

Project and build files

The files PowerBuilder and its build tools keep beside the libraries are compared by what they hold, with compare, parse, report and merge3, exactly as an object is:

pb-diff-cli compare release-1.4\src\app.pbr release-1.5\src\app.pbr --format json
File fileType Nodes
Resource file .pbr Resource One Resource node per line, in the groups group:datawindows, group:pictures, group:icons, group:pointers and group:other, keyed resource:<name> in lower case. The order of the lines is not a difference; the case of a name is.
Workspace .pbw Workspace A Setting per setting (setting:defaulttarget, setting:save-format, ...), and a Target per target under block:targets, keyed target:<path> and not by its number.
Solution .pbsln Solution A ProjectReference per project under block:projects, keyed project:<path>; the Projects line itself names the default project.
Project .pbproj SolutionProject A Setting per one-line element (setting:type, setting:application, ...), a ProjectReference per deploy configuration under block:deployconfigurations, keyed deploy:<name>, and setting:libraries, the whole <Libraries> element, with setting:appentry and a Library per library beneath it. A library moved is setting:libraries modified with no library added or removed.
PowerGen project .gen PowerGen Best-effort, outside the certified scope. It must open with $PowerGenVersion=; a .pbt holding one is an invalid Target, not a PowerGen. A Setting per header tag (setting:powergenversion, ...), an Application per application keyed application:<name>, and beneath it a Setting per tag and setting:libraries, the run of $Library lines, with a Library beneath it for each; every key inside an application begins application:<name>/.
Target .pbt Target A Setting per setting (setting:appname, setting:applib, setting:type, ...), a ProjectReference per project under block:projects, keyed project:<name>, and setting:liblist, the library search path, with a Library beneath it for each library, keyed library:<path>. A library moved in the list is setting:liblist modified with no library added or removed.

A file is the format its extension names. A build file or an object export whose content is not that format is not read as another one: it is compared as text, and inputDiagnostics carries a parse.fallback entry saying which format it is not and why (Not a PowerBuilder target: it does not open with a Save Format line; compared as text.). A .pbl or .pbd that is not a PowerBuilder library is refused by every command with exit code 2 and NOT_A_CONTAINER. Only a file whose name says nothing, such as Git's temporary files under --detect H, is typed from its content.

merge3 writes a merged build file - and any other file that is not a PowerBuilder object export, an INI, XML or text file - in the encoding its Base was read in, with its own line endings, rather than the UTF-16LE of an object export: an ASCII file stays ASCII, an ANSI one stays ANSI, and a Solution's UTF-8 keeps its byte-order mark or its absence. What each file is for, and what a change to it does to an application, is in pb-diff Help under Project and build files.

Argument files

Any argument may be replaced by @<file>, a text file holding further arguments. It is the way round a command line too long for the shell or too awkward to quote:

pb-diff-cli compare @nightly-args.txt

A workspace or a target may not be one: pb-diff-cli @app.pbw is refused with exit code 2 and BAD_ARGUMENTS, and its text is never tokenised into arguments. Named without the @, the same file is compared.

Expansion happens before the rest of the command line is read, and a failure in it — a file that is not there, one that nests too deeply, or a workspace — still answers in the format that was asked for: --format json on the command line gives the usual error envelope. A --format written inside a command file cannot apply to a failure reading that same file.

The GUI accepts command files too, and the two are not interchangeable. This tool rejects a missing file rather than ignoring it, because a script that names a file it did not write should stop rather than run with fewer arguments than its author intended; it takes # as its only comment marker; and it does not consult the command-file folder from the application's Options, because it has no settings file to read. The GUI differs on two of the three counts: it also takes ; as a comment marker, and it looks in the command-file folder from its Options. A missing command file it reports and refuses, as this tool does (CLI-9).

Exit codes

Code Meaning
0 Success; for compares, no differences; for check, nothing noticed
1 Compares: differences found. Validate: warnings only. Check: findings
2 Invalid input, missing file or entry, or bad arguments
3 Three-way merge left conflicts
4 Internal error
5 No valid licence

Exit code 1 is not an error. compare, compare-lib, validate and check use it to report a finding, so a script that treats any non-zero code as a failure will misread a perfectly good compare. pb-diff-ci returns the same codes with the same meanings, except 3 and 5, which it never returns.

Errors

In --format json mode an error is a single JSON object on standard error, and nothing is written to standard output:

{ "schemaVersion": 1, "error": { "code": "ENTRY_NOT_FOUND", "message": "…", "hint": "…" } }

Branch on code, never on message. The message is prose and may be reworded.

Code Cause
BAD_ARGUMENTS The arguments do not form a valid command, or name a .pbw/.pbt as a command file (@app.pbw), or give a Solution PBL folder where an object was expected, or name an output (--out, --output) that is a PowerBuilder library or a file holding one, or an --out or a report --output that is one of the inputs.
FILE_NOT_FOUND A path does not exist. A folder given where a file was expected is not this: a command that reads a library answers NOT_A_CONTAINER, and one that reads one object BAD_ARGUMENTS, with a hint naming compare-lib.
NOT_A_CONTAINER The file is not a readable PowerBuilder library, or is a folder - a Solution PBL folder or any other - where a library file was expected.
NO_SOURCES check: an input holds no object source or build file to read, so it could not pass by checking nothing.
NO_LIBRARIES validate: an input stands for no library - an empty folder, a folder of sources, a Solution, or a list that could not be read - so it could not pass by verifying nothing.
ENTRY_NOT_FOUND The container has no entry of that name.
FILE_EXISTS An output file exists and --overwrite was not given.
UNKNOWN_COMMAND No such command.
UNKNOWN_OPTION No such option for this command.
UNKNOWN_SCHEMA No output schema is published for that command; the hint lists the ones that are.
IO_ERROR A read or write failed, or was refused - including export and extract given an entry name that is not a plain file name, two entries that would be written to the same file, or an entry whose chain holds fewer bytes than it declares, which would be written short.
INTERNAL_ERROR A bug. The exit code is 4 and the message is worth reporting.

merge3 additionally reports TYPE_MISMATCH when the three inputs are not the same object type, and MERGE_REFUSED when the comparison cannot produce a merge at all - among others when one or two of the three could not be read as their format and were read as text (their parse.fallback diagnostic says why), since a file read as text has nothing in common with the structure of the others.

With exit code 5, the licence codes:

Code Cause
LICENSE_REQUIRED No valid licence: none, expired, suspended, moved to another machine, not valid for this build, or a trial ended. The message says which, and the hint how to activate - with none at all, also that a trial is started from the desktop application and where to buy. license activate also uses it when the key is refused.
LICENSE_ACTIVE_ELSEWHERE license activate: the licence is active on another machine, described in the message. Run it again with --take-over to move it.
LICENSING_UNREACHABLE license activate or license deactivate: the licensing service could not be reached, gave no answer this build can verify, or did not confirm the deactivation. Nothing was changed.
TERMS_APPROVAL_REQUIRED license activate: the licence terms in force have not been accepted, and their clauses specifically approved, on this machine. Only the desktop application asks for them: activate the key there, in Help > Licence. Nothing was changed.

Output contract


Size and performance

There is no size limit on an input. A file larger than anything below is accepted and processed on a best-effort basis rather than refused, with no promise about how long it takes or how much memory it needs.

What is certified is an envelope of three loads, and only those:

Load Envelope
Open and enumerate a library 50 MB
Two-way parsed compare of a .srw 500 KB a side
Library-to-library summary two libraries of 2,000 entries

Two things follow from how the container is read, and they are worth knowing before pointing this tool at a large library. Opening one and listing its entries reads the header and the directory nodes and no entry's payload at all, so what pb-diff-cli list costs follows the number of entries and not what the file weighs — about 64 bytes an entry, so the 400-entry fixture the tests use is enumerated by reading about 25 KB whether the file is 0.2 MB or 51 MB. Ten thousand entries cost more than four hundred, at any size. An object you then ask for, by contrast, is read into memory whole: parse, compare and export materialise the object they are given, and that is deliberate rather than a limitation to work around.

The published targets are objectives, not guarantees: under a second for the parsed compare and under ten seconds for the 2,000-entry library summary. On the reference runner they are held to as ceilings - a release candidate whose median is above one of them fails - and nowhere else, because a median belongs to the machine it was taken on: the benchmark declares that runner in build/bench/profiles.json and stamps a measurement taken anywhere else as exploratory. No median is quoted here for the same reason. The objectives and the loads are both declared in that one file rather than described in prose, so what is measured cannot drift from what is stated.


Commands

info

pb-diff-cli info <container>

Container metadata: flavour (ANSI or Unicode), format version, header comment and entry counts. Also in pb-diff-ci. A source-control block a library still carries is opaque (S04): it is neither decoded nor reported, and there is no option to skip a decoding that does not happen.

list

pb-diff-cli list <container>[!<glob>]

Enumerate the entries of a container: name, kind, type, size and modification time. Also in pb-diff-ci.

Option Meaning
--kind <source|compiled|resource> Only list entries of this kind.
--comments Include entry comments in JSON and in a text table column. This reads every entry's data chain, so it is slower.

Every entry has a kind (source, compiled or resource) and a finer type such as source/window or compiled/datawindow. Only source entries can be exported as .sr*. size is the payload without the entry comment; the comment's length is reported separately.

An exact entry selection that matches nothing is ENTRY_NOT_FOUND (exit 2), before the kind filter is applied. A wildcard selection that matches nothing succeeds with zero entries.

export

pb-diff-cli export <container>[!<glob>] [extra entry...]

Write source entries as importable .sr* files: $PBExportHeader$, the entry's comments, the right encoding, and, where the object has a companion .bin, its binary data section. Not in pb-diff-ci.

Entry names after the address are added to what the address already selected. A bare container already selects every entry, so naming entries after one narrows nothing - put the glob or the entry in the address, after the !, when you mean to select.

Each exact entry selection, including extra entries, must exist or the command fails with ENTRY_NOT_FOUND (exit 2) before writing any files. An empty wildcard match is allowed and adds no entries; if nothing is selected, the command succeeds with zero files.

Option Meaning
--out-dir, -d <dir> Directory to write into. Default: the current directory.
--stdout Write a single entry's bytes to standard output instead of to a file. Bytes, not JSON: it cannot be combined with --format json or with --out.
--overwrite Overwrite existing files. Without it, an existing file is FILE_EXISTS.

Every entry is written under its own name, unchanged. An entry whose name is not a plain file name, or two entries that would land on the same file (names compare without regard to case), refuse the whole command with IO_ERROR before anything is written.

extract

pb-diff-cli extract <container>[!<glob>] [extra entry...]

Write the raw payload bytes of any entry, including compiled objects and resources. This is not a decompiler: a compiled entry's bytes are P-code, and pb-diff does not interpret them. Not in pb-diff-ci.

Selections are added together as in export. Each exact entry selection must exist or the command fails with ENTRY_NOT_FOUND (exit 2) before writing any files. An empty wildcard match is allowed; if nothing is selected, the command succeeds with zero files.

Option Meaning
--out-dir, -d <dir> Directory to write into. Default: the current directory.
--stdout Write a single entry's bytes to standard output instead of to a file. Bytes, not JSON: it cannot be combined with --format json or with --out.
--overwrite Overwrite existing files. Without it, an existing file is FILE_EXISTS.

validate

pb-diff-cli validate <path>...

Check libraries for damage: walk each one's structure and report what it finds. Also in pb-diff-ci. Each path is one of:

A library reached twice is verified once. Each library ends in one state: valid, warnings, damaged, unreadable or missing. Damaged means at least one error; unreadable, that the file is there and cannot be read as a PowerBuilder library; missing, that a library list names it and it is not there. A workspace naming a target that is not there, a target or project whose library list cannot be read, and a folder that cannot be read to the bottom are notes, and each leaves libraries unverified: the run is not valid, and summary.unverified counts them. A note's detail is why, for an unreadable-list or unreadable-folder, and the list that names the path, for a missing-list or source-library-folder. The results keep the order of the inputs, so a series of builds of one library - validate build-768\arst.pbl build-769\arst.pbl ... - reads as its history.

Each finding has a severity (info, warning, error) and a stable code such as NODE_FREE_SPACE, CHAIN_LENGTH (an entry declaring more than its chain holds), CHAIN_SHARED (a data block two entries' chains reach) or BITMAP_UNMARKED_BLOCK. Branch on the code; the message is prose. A .pbd whose bitmap still marks only an empty library's blocks gets the warning BITMAP_UNMAINTAINED rather than that error: a compiled library is written whole by a build, and Appeon's own pb_sdk.pbd ships that way.

Option Meaning
--no-chains Skip walking every entry's data chain.
--no-bitmap Skip cross-checking the free bitmap.
--output <path> Also write the result as a report to this file: the counts, a row per library and every finding. The payload or the text still goes to standard output.
--report-format <fmt> The report's rendition: Markdown, HTML or CSV (--report-format markdown). Default: from the --output file name. Without --output the rendition replaces the text on standard output, which is how a job summary takes it; under --format json it needs --output.

The report is a result, not one of the five reports of report: it has no sides and compares nothing. pb-diff-ci ends it with its footer, as it ends every report it writes.

The worst library is the exit code: 0 when every library is valid, 1 when the worst has warnings, 2 when any is damaged, unreadable or missing, or a list or folder the inputs stand for could not be read. A path that is not there is FILE_NOT_FOUND, a file that is neither a library nor a list of them is NOT_A_CONTAINER, and an input that stands for no library is NO_LIBRARIES; each ends the command before any result, with exit code 2 and the error object on standard error.

compare

pb-diff-cli compare <old> <new>

Compare two objects section by section. Either side may be a file or a container!entry. Not in pb-diff-ci, which never shows what changed inside an object.

compare refuses a library named on its own, with exit code 2 and BAD_ARGUMENTS: a .pbl or a .pbd holds objects and is not one of them. Use compare-lib for two libraries, or address the object you mean (ui.pbl!w_main.srw). An .exe or a .dll is judged on whether it really carries an appended library. merge3, parse and the file reports draw the same line.

Option Meaning
--context, -c <n> Non-negative context rows around each change. Default 3; values larger than the available row count are clamped to that count.
--unified Emit a unified diff of the pretty-printed sources instead of the structured result. Its line numbers are the pretty-print's - sections in a fixed order, continuation lines joined - so it reads like any diff -u but is not a patch to apply to the files. It is text: it cannot be combined with --format json.
--all Include unchanged nodes, not only the changed ones.
--ignore-whitespace, -w Ignore all whitespace when comparing lines.
--ignore-case Ignore letter case when comparing lines.
--resync-threshold <0..1> How alike two lines must be to count as modified rather than replaced. Default 0.5; 0 pairs everything.
--inline <token|character|none> Which engine highlights what changed inside a line.
--no-sync-names Do not pair a function whose signature changed but whose name did not.
--detect <F|H> Identify a mangled filename by its first extension (F) or by its export header (H), which for a PowerBuilder 2025 Solution source is how the file opens.
--no-normalise-case Keep DataWindow attribute and value case as written.
--ignore-xml-attribute-order Sort XML attributes before comparing.
--ignore-xml-namespaces Drop XML namespace declarations and prefixes.
--ignore-xml-comments Drop XML comments.

The result is the parsed hierarchy with a status per node (unchanged, new, modified or deleted) plus hasChanges, true when anything beneath a node changed. Unchanged nodes are omitted unless --all is given, so walking children gives exactly the changed items. Each changed node carries hunks. A hunk has oldStart, oldLength, newStart, newLength and a rows array; each row carries kind, oldLine/newLine, the text of both sides, and, on a modified row, oldRanges and newRanges - the character spans that actually differ within the line.

Exit code 1 means differences were found. It does not mean anything went wrong.

Two lists come back beside the tree, and they answer different questions.

Ignore options affect the comparison only. The text reported for each side is always the text as it is in the file.

compare-lib

pb-diff-cli compare-lib <old> <new>

The compare summary: two libraries, folders or files compared object by object, on the same model and the same engine as the desktop application's Compare, with a status per object and aggregate counts. Also in pb-diff-ci.

Each side is one path:

The two sides are matched by name without regard to case, an object against its own name whatever form it is in, so a library compares with a folder of its exports and with the Solution converted from it. A name two inputs on one side share is never paired by guessing: each of them is listed, named by its address, and notes says why.

Option Meaning
--mode <parsed|binary|metadata> How thoroughly each object is checked. Default parsed.
--changed-only List only new, modified, deleted and renamed-modified objects.
--pair <old=new> Pair two differently named rows as a rename: by object name, or by address when two inputs share the name. Repeatable.
--ignore-whitespace, -w Ignore all whitespace when comparing lines.
--ignore-case Ignore letter case when comparing lines.
--no-normalise-case Keep DataWindow attribute and value case as written.
--ignore-xml-attribute-order Sort XML attributes before comparing.
--ignore-xml-namespaces Drop XML namespace declarations and prefixes.
--ignore-xml-comments Drop XML comments.

parsed compares the parsed objects, so a reordered but unchanged export, the framing that differs between a library export and a Solution source, and the case of a DataWindow attribute do not show up as modified. binary compares the bytes. metadata compares size and modification time, a fast survey that reports an object as changed when only its timestamp moved; for a folder checked out by Git or SVN the time is the checkout's, so use parsed there. The options are the desktop application's Options, as switches, with its defaults; pb-diff-cli reads no settings file, so it agrees with a desktop whose Options are at their defaults or set to match the switches given.

The payload names each side as old and new, with the path given and its kind: library, folder or file. Each entry carries name, status, type, oldSize and newSize, the modification times, oldName and newName, and oldPath and newPath, the address of each side - a file's path, or container!entry - which tells two namesakes apart. notes lists what had to be said about the inputs, each with a kind, a side, the name, the paths concerned, a message and, where there is one, a detail: ambiguous-name, compiled-set-aside, compiled-beside-source, source-control-copy, not-a-library, not-its-format, unreadable-folder and missing-input. The set may grow; ignore a kind you do not know.

Renames are never detected automatically: content similarity cannot distinguish a renamed object from a copied-and-edited one. Pair them yourself with --pair.

A pairing has to be one the two sides can carry out, and an unambiguous one. --pair naming a row neither side holds, or a name two inputs share without its address, is ENTRY_NOT_FOUND; naming the same row on the same side of two pairs is BAD_ARGUMENTS. A pair takes the two rows it names and nothing else: a row whose counterpart a pair claimed is reported as new or deleted, so every object of both sides appears exactly once whatever pairing you ask for.

A path that does not exist is FILE_NOT_FOUND; a library named outright that is not one - a .pbl another tool wrote, an .exe with no library in it - is NOT_A_CONTAINER, while the same file found in a folder is left out and noted; and container!entry is BAD_ARGUMENTS, since one entry is not a set. --verbose reports on standard error how far a long compare has got.

merge3

pb-diff-cli merge3 <file1> <base> <file2> [--output <merged>]

Three-way merge. A change is auto-mergeable when only one side made it, or when the two sides changed the same item without overlapping. Every auto-mergeable change is applied automatically; what is left is reported as a conflict list with the node, the base line range and the text each side wanted.

Option Meaning
--output <path> Where to write the merged result: written whole to a temporary file beside it and then moved into place, so a failed write leaves an existing file as it was. It may be one of the inputs, for a merge in place; a library or a file holding one is refused with BAD_ARGUMENTS.
--force-partial Write even with conflicts, taking the base for each one.
--no-sync-names Do not pair a routine whose signature changed but whose name did not.
--ignore-whitespace, -w Ignore all whitespace when comparing lines: a change only in whitespace is not a change, and the base's text is kept.
--ignore-case Ignore letter case when comparing lines: a change only in case is not a change, and the base's text is kept.
--detect <F|H> Identify a mangled filename by its first extension, or by its export header or its content: a Solution source, or a build file other than a .pbr.

The two ignore options decide what counts as a change, not only what is shown. A side that only re-indented a line, or only changed its case, has not changed it under them, so the merge keeps the base's line, as it keeps every region it did not decide, and a real change the other side made to the same item is taken whole. Leave them off to merge a re-indentation.

All three inputs must be the same object type, or the command fails with TYPE_MISMATCH.

Each of the three is a file or a library entry, written the usual way, so a merge can start from the library as it shipped without exporting it first:

pb-diff-cli merge3 mine.srw "release-1.4\ui.pbl!w_main.srw" theirs.srw --output merged.srw

An address that selects more than one entry is refused with BAD_ARGUMENTS: a merge has exactly three sides.

merge3 reports what a parser found wrong with one of the three files in inputDiagnostics, each entry naming which — side is file1, base or file2 — with the same code and message parse gives for that file. A merge started from a file that still carries conflict markers is named there before anything is decided. diagnostics remains what it was: prose about the merge itself, not about any one file.

Exit code 0 means the merge was clean. With --output, the merged file is written; without it, only the merge summary is emitted (JSON or text), not the merged source text. JSON written is true only when a merged file was actually written, and false when no --output was requested. --out redirects the summary, not the merged source. Exit code 3 means conflicts remain, and in that case no output file is written unless both --force-partial and --output are given. pb-diff never writes a conflict marker: markers are not valid PowerScript, and a file containing them fails to import. A merge copies what it did not decide, though, so markers one of the three files already carried can reach the result. Then the merge is not clean either: exit code 3, a merge.conflict-markers warning, and no output file unless --force-partial is given.

The output file is written in the canonical PowerBuilder form — UTF-16LE with a byte-order mark and CRLF line endings — whatever File1 and File2 used, and it is the same file the desktop application's Save produces. A file that is not an object export - a build file such as a .pbr, an INI, XML or text file - is written in its Base's own encoding instead (see Project and build files). The other exception is a UTF-8 Base, which is what a PowerBuilder 2025 Solution holds: the output stays UTF-8, with a byte-order mark exactly when the Base had one, gains no $PBExportHeader$ line, and keeps every byte the merge did not decide, bare LF line endings included. The Solution compiler reads UTF-8 only, and a UTF-16LE file in a PBL folder fails the build. The JSON on standard output is unaffected.

The output is the base file rewritten: only the regions a merge action changed are composed again, and everything else — trailing whitespace, wrapped continuation lines, the order the file wrote its sections in — is copied from the base. merge3 f f f therefore returns f, in that encoding.

A control the merge carries into the base is made consistent with the object around it: it is instantiated in the owning object's .create script, released in its .destroy script and put top-most in the control list, and a control the merge takes out loses exactly those three lines. An object no merge action moved a control in or out of is not touched at all. What is not invented is the control's property block, its entry in the object's own type block or its line in the forward block: a control that arrives without them is reported in warnings under merge.undeclared-control, and the object will not compile until they are merged too. warnings also carries merge.missing-create-script, for an owner with no .create script to rewrite, merge.unresolved-conflict for each conflict left unresolved - the base is what a partial result keeps for it, and without --force-partial it is listed although nothing is written - and merge.conflict-markers when the result carries markers an input already had.

A change one side made inside a control or an XML element the other side deleted - an event added or edited in a button the other side removed - is a conflict, because merged on its own it has nowhere to go. A partial result leaves it out with its container rather than writing the base version where it would read as part of something else, and its merge.unresolved-conflict message says so.

That the file is well formed is measured on every build. That the PowerBuilder IDE accepts a merged file rests on D26's two frozen results, which were imported through the Library painter and regenerated in PowerBuilder 19 and 25 on 23 August 2026; the claim covers PowerBuilder 19 and later, and 10 to 18 is expected and unverified. Reading, comparing and exporting libraries is a separate scope and is certified from PowerBuilder 10 onwards.

report

pb-diff-cli report <summary|summary-changes|full|changes|script> <old> <new>

Render a comparison as a report. pb-diff-ci has the command with the summary and summary-changes kinds only, and without --node.

Kind Inputs Contents
summary two libraries, folders or files Every object of the compare compare-lib runs, and its notes.
summary-changes two libraries, folders or files New, modified, deleted and renamed-modified objects, and the notes.
full two objects The whole object, changed or not, item by item.
changes two objects The changed items only, each complete, with its changes highlighted.
script two objects One item, named by --node.

Object report bodies use the same exact decoded-source projection as the desktop compare panes. PowerScript continuation lines, original whitespace, JavaScript fragments and raw XML element text are reported as written; semantic normalisation decides whether something changed but is not printed in place of either input.

The inputs column is enforced. The summary kinds take what compare-lib takes, with the same rules; a kind that takes two objects refuses a library named on its own and points at the summary kind that does take one. An address naming an entry the container does not hold is ENTRY_NOT_FOUND, and one selecting several entries is BAD_ARGUMENTS - a report covers one comparison.

Option Meaning
--output <path> Write to a file. Without it, the text formats go to standard output.
--report-format <fmt> pdf, html, csv, json, rtf or markdown; markdown for the summary kinds only. Default: taken from the output file name, and HTML when its extension names no format; without --output, JSON under --format json and HTML otherwise.
--node <key> Which item a script report covers.
--mode <parsed|binary|metadata> The summary kinds: how thoroughly each object is checked, as for compare-lib. Default parsed.
--pair <old=new> The summary kinds: pair two rows as a rename, as for compare-lib. Repeatable.
--ignore-whitespace, -w Ignore all whitespace when comparing lines.
--ignore-case Ignore letter case when comparing lines.
--no-normalise-case Keep DataWindow attribute and value case as written.
--ignore-xml-attribute-order Sort XML attributes before comparing.
--ignore-xml-namespaces Drop XML namespace declarations and prefixes.
--ignore-xml-comments Drop XML comments.

The keys --node takes are the ones pb-diff-cli parse reports for the object; naming one the compare does not hold is exit code 2 and BAD_ARGUMENTS, like any other bad argument. --mode and --pair with an object kind, and --node with a summary kind, are BAD_ARGUMENTS too.

PDF must be written to a file. The formats are PDF, HTML, CSV, JSON, RTF and Markdown, not legacy binary .xls or .doc files; those need a large third-party library, and pb-diff ships with no third-party runtime dependencies.

Markdown is GitHub-flavoured, written for a GitHub Actions job summary, an Azure DevOps build summary or a pull-request comment: the two inputs, the counts, one table row per object with its status, type and sizes, and the notes. Every name is escaped, so a name cannot add a column or start a tag. It is the summary kinds' rendition only - an object report asked for it is BAD_ARGUMENTS - and an --output file ending in .md chooses it. Without --output it goes to standard output, which is how a pipeline appends it to a job summary (>> $env:GITHUB_STEP_SUMMARY). A job summary holds 1 MiB and a pull-request comment 65,536 characters; summary-changes keeps a large application within them where summary may not.

Reports pb-diff-ci writes end with its footer, in every rendition but JSON; see pb-diff-ci.

--format json and --report-format json are different questions. The first is how the CLI answers — the same switch every other command takes — and the second is which rendition a reader gets.

check

pb-diff-cli check <path>...

Report sources left broken: to sources what validate is to libraries. Also in pb-diff-ci. Each path is a file or a folder, and a folder is walked as compare-lib walks it; every object source and build file it holds is read as parse reads it, and what the parsers noticed is reported - never the hierarchy, and never a line of the file.

pb-diff-cli check src\ws_objects --format json
pb-diff-cli check merged\w_main.srw app.pbl\ --format json

Each finding names the file, carries one of the codes parse tabulates and its message, and, where the parser knows it, line, the first line concerned:

Code What was found
parse.conflict-markers Merge conflict markers left in the file; line is the first of them.
parse.unterminated-block A PowerScript block opened and never closed; line is where it opened.
parse.unterminated-statement A DataWindow statement opened and never closed.
parse.fallback The file is not the format its extension names, or could not be read as one.
parse.unrecognised-line A DataWindow line the parser does not recognise; it is kept as text and compared, and line is where it is.

The set of codes may grow; ignore a code you do not know. The check payload also carries inputs, each with the number of files it stood for, fileCount, each file counted once however it was reached, clean, findingCount and the notes of the walk.

Exit code 0 means nothing was noticed, 1 that there are findings, 2 that an input is unusable: a path that does not exist is FILE_NOT_FOUND, a library is BAD_ARGUMENTS with a hint naming validate, and an input that holds no source to read is NO_SOURCES, so that a mistyped folder cannot pass a gate.

check works in one direction, as parse does: a finding means the file is damaged; no finding means nothing was noticed, not that the file compiles.

parse

pb-diff-cli parse <file|container!entry>

Show how pb-diff parses an object: the hierarchy of sections and scripts that every compare is built on. Not in pb-diff-ci. parse refuses a library named on its own: a library has no hierarchy of its own, and list is what shows what one holds.

Option Meaning
--lines Include each node's text, not just its structure.
--detect <F|H> Identify a mangled filename by its first extension, or by its export header or its content: a Solution source, or a build file other than a .pbr.
--no-normalise-case Keep DataWindow attribute and value case as written.
--no-key-features Omit the derived DataWindow Overview. The spelling is kept as published.
--ignore-xml-attribute-order Sort XML attributes before comparing.
--ignore-xml-namespaces Drop XML namespace declarations and prefixes.
--ignore-xml-comments Drop XML comments.

Each node has a kind (Function, Event, Control, Band, TypeDefinition, …), a display name, a key (the stable identity used to pair a node with its counterpart on the other side of a compare) and a contentHash of its comparable text, which is the cheap way to ask whether something changed.

Two things parse does deliberately. Sections come out in a fixed order, not the order the file used, which is why a reorganised but unchanged object parses identically. And a file that will not parse still returns a document, with usedFallback true and fallbackReason set, compared as plain text.

usedFallback marks a parser that gave up altogether: a DataWindow in which no statement was recognised, XML that is not well formed, a .sr* file in which no PowerScript declaration was recognised — no forward, no type block, no variables block, no routine, no event — and any parser that threw. A file rejected that way is not a damaged object but something else under an object's name: a note left in place of an export, a fragment, a file of the wrong kind.

diagnostics reports what a parser noticed without giving up. Each entry carries a stable code and a message, and the code is what to branch on; the message is prose and may be reworded.

Code What it means
parse.conflict-markers Unresolved merge conflict markers. The file is not importable.
parse.unterminated-block A PowerScript block was opened and never closed — a truncated file.
parse.unterminated-statement A DataWindow statement was opened and never closed.
parse.fallback The parser gave up; usedFallback and fallbackReason say the same thing.
parse.unrecognised-line A DataWindow line no statement accounts for, kept as a text node so a change to it is still a difference.

The marker check runs for every file type, not only for PowerScript. A marker is a run of seven or more <, = or > at column 0, followed by end of line or by a space — the shape every merge tool writes. A row of equals signs on its own is also how a plain-text file underlines a heading, so it counts only in a file that carries an angle-bracket marker too. The file is still parsed, not degraded: the hierarchy is what shows where a marker landed.

The same five codes reach compare and merge3, in their own inputDiagnostics member, each entry naming the side it belongs to. parse is the narrower question — one file, no counterpart — and not a step a caller has to remember before the damage has a name.

Both members work in one direction only. An entry in diagnostics, or usedFallback true, means something is wrong; an empty list and a false flag mean nothing was noticed, not that nothing is wrong. A truncated object in particular does not set usedFallback: it is still mostly parseable, and its hierarchy is more use than a text compare would be, so it is reported by its diagnostic instead.

tokens

pb-diff-cli tokens <file>
pb-diff-cli tokens --line "<text>" --lang <language>

Tokenise a file or a single line with the lexer behind syntax colouring and the token diff. A debugging aid, not part of a normal workflow. Not in pb-diff-ci.

A plain .pbl or .pbd path that is not a PowerBuilder library is refused with NOT_A_CONTAINER (exit 2), rather than tokenised as text, and a whole PowerBuilder library with BAD_ARGUMENTS: address one entry as <library>!<entry>. A file and --line together are BAD_ARGUMENTS.

Option Meaning
--line <text> Tokenise this text instead of a file.
--lang <language> powerscript, datawindow, xml, html, javascript, ini, text.
--detect <F|H> Identify a mangled filename by its first extension, or by its export header or its content: a Solution source, or a build file other than a .pbr.

license

pb-diff-cli license [status]                      # this machine's licence, or the one PBDIFF_LICENSE names
pb-diff-cli license activate <key> [--label <text>] [--take-over]
pb-diff-cli license deactivate

Needs no licence itself, and is not in pb-diff-ci, which has no licence to show. status, the default, reports the licence: its state, the holder it was issued to, its kind, when the subscription ends, and for a machine activation the label of this machine, when it was last verified online and until when it stays licensed without verifying again. It exits 0 when the licence is valid and 5 when it is not, with the same payload either way. When the machine file is more than a day old it verifies it online first, for a few seconds at most; a verification that cannot be completed or saved in that time leaves the licence as it was.

activate licenses this machine - this computer and this Windows user - with a licence key, and needs the licensing service. A 30-day trial is not started here but in the desktop application's Licence window (Help > Licence), with an e-mail address; once started it licenses pb-diff-cli on the same machine too, and its kind is trial. A key bought at https://pbdiff.com/buy replaces it with activate. A key is activated once the licence terms in force are accepted, and their clauses specifically approved, on this machine, which only the desktop application's Licence window asks for: without that, activate refuses with TERMS_APPROVAL_REQUIRED, and changes nothing. A machine that approved them - with a trial, or a key activated in the desktop application - is not asked again; when the terms cannot be read, activate goes ahead and the desktop application asks at its next start. When the licence is already active on another machine, it refuses with LICENSE_ACTIVE_ELSEWHERE, naming that machine by its label or its dates; --take-over moves the licence here, and the other machine stops being licensed at its next online verification. deactivate frees the licence from this machine in the service and deletes the machine file; it too needs the service. The machine file is deleted only when the service confirms the deletion, or answers that the machine is already gone; with any other answer it is kept and the command fails with LICENSING_UNREACHABLE.

Option Meaning
--label <text> A name of your choosing for this machine, shown to whoever the licence is moved from instead of dates. Nothing else about the machine is sent: not its name, not the user's.
--take-over Move the licence here when it is active on another machine.

The payload's state is valid, none, invalid, expired, suspended, moved or unverified (not verified online for 7 days), and source says whether the licence is this machine's activation, a licence file from the environment, or nothing at all. No output of any command ever contains the licence key or any part of a licence file. The activation is kept per Windows user under %LOCALAPPDATA%\pb-diff\licence\.

The environment variables PBDIFF_LICENSE (a licence file's content) and PBDIFF_LICENSE_FILE (its path) give pb-diff-cli a licence file bound to no machine, verified offline. They exist for the licence files RESTORE uses in its own build pipelines, and no such file is issued to customers: a CI job uses pb-diff-ci, and a build server activates its service account. They are described here because a variable that is set changes what pb-diff-cli does: while either is set it is the only licence used, and a file that is damaged, expired or suspended fails the command rather than falling back to the machine's activation.

schema

pb-diff-cli schema              # the published names
pb-diff-cli schema <name>       # one schema

With no argument, the published names, one per line, or as { "schemaVersion": 1, "schemas": [ … ] } under --format json. Listing them is an answer of its own, not something to be read out of an error message — and being an answer, it is a payload like any other, with a schema of its own under the name schema.

With a name, that schema. Sixteen are published, one per command that emits JSON plus the error envelope: check, compare, compare-lib, error, export, extract, help, info, license, list, merge3, parse, report, schema, tokens, validate. Names are matched case-insensitively; asking for anything else fails with UNKNOWN_SCHEMA and a hint listing the ones that exist. Also in pb-diff-ci, which publishes the eight of its own commands and the error envelope.

The schemas are Draft 2020-12 documents. They live in the repository at docs/cli/schemas/v1/, and pb-diff-cli embeds and serves those very files, so the schema you read and the schema the tool prints are the same bytes. Every payload they describe carries schemaVersion: 1. Within v1 they may gain optional members; removing a member, changing its type or changing what it means requires schemaVersion: 2.

For that reason none of them sets additionalProperties: false. A member added within v1 is a compatible change, and a schema that forbade unknown members would turn every such addition into a breaking one for readers holding an older copy. Validate what you rely on; ignore what you do not recognise.

Two keywords of their own say how a schema may grow within v1. x-open-set marks a set of values that may gain members, such as compare-lib's note kinds and check's codes: ignore a value you do not know. x-grows-with marks a value that echoes an option, such as report's reportFormat, which may gain a value only together with a new value of report --report-format, so that a caller who never asks for the new rendition never receives it.

What they do not cover, by decision: text output, the unified diff of compare --unified, the raw bytes of export --stdout and extract --stdout, the PDF, HTML, CSV, RTF and Markdown report renditions, and what pb-diff-cli schema <name> prints — a JSON Schema, and validated as one rather than against a schema of its own.

help

pb-diff-cli help [command]

Usage, in both tools; pb-diff-ci help also names its usage terms. pb-diff-cli help --format json emits the whole command catalogue as JSON, with its commands, options and exit codes, so a program can discover the surface without parsing this document. That is the only machine-readable help; there is no --json alias.

help is validated like every other command, and the format never decides whether an invocation succeeded:

In JSON the answer is always the complete catalogue: a topic narrows the prose, not the contract.


See also