summaryrefslogtreecommitdiffstats
path: root/dh_assistant
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-09 12:53:53 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-09 12:53:53 +0000
commit90169463f86997737ed5b9c0ea2b311cd3b056b7 (patch)
tree281a0f8d9850ea58cf2a3ddb8bf087fb52520925 /dh_assistant
parentInitial commit. (diff)
downloaddebhelper-90169463f86997737ed5b9c0ea2b311cd3b056b7.tar.xz
debhelper-90169463f86997737ed5b9c0ea2b311cd3b056b7.zip
Adding upstream version 13.15.3.upstream/13.15.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dh_assistant')
-rwxr-xr-xdh_assistant1449
1 files changed, 1449 insertions, 0 deletions
diff --git a/dh_assistant b/dh_assistant
new file mode 100755
index 0000000..3d3337f
--- /dev/null
+++ b/dh_assistant
@@ -0,0 +1,1449 @@
+#!/usr/bin/perl
+
+=head1 NAME
+
+dh_assistant - tool for supporting debhelper tools and provide introspection
+
+=cut
+
+use strict;
+use warnings;
+use constant EXIT_CODE_LINT_ISSUES_FOUND => 2;
+use Debian::Debhelper::Dh_Lib;
+use JSON::PP ();
+
+=head1 SYNOPSIS
+
+B<dh_assistant> B<I<command>> [S<I<additional options>>]
+
+=head1 DESCRIPTION
+
+B<dh_assistant> is a debhelper program that provides introspection into the
+debhelper stack to assist third-party tools (e.g. linters) or third-party
+debhelper implementations not using the debhelper script API (e.g., because
+they are not written in Perl).
+
+=head1 COMMANDS
+
+The B<dh_assistant> supports the following commands:
+
+=head2 active-compat-level (AJSON)
+
+B<Synopsis>: B<dh_assistant> B<active-compat-level>
+
+Outputs information about which compat level the package is using.
+
+For packages without valid debhelper compatibility information (whether missing, ambiguous,
+not supported or simply invalid), this command operates on a "best effort" basis and may abort
+when error instead of providing data.
+
+The returned JSON dictionary contains the following key-value pairs:
+
+=over 4
+
+=item active-compat-level
+
+The compat level that debhelper will be using. This is the same as B<DH_COMPAT> when present
+or else B<declared-compat-level>. This can be B<null> when no compat level can be detected.
+
+=item declared-compat-level
+
+The compat level that the package declared as its default compat level. This can be B<null>
+if the package does not declare any compat level at all.
+
+=item declared-compat-level-source
+
+Defines how the compat level was declared. This is null (for the same reason as
+B<declared-compat-level>) or one of:
+
+=over 4
+
+=item debian/compat
+
+The compatibility level was declared in the first line F<debian/compat> file.
+
+=item X-DH-Compat: <C>
+
+The compatibility was declared in the F<debian/control> via a the B<X-DH-Compat>
+field. In the output, the B<C> is replaced by the actual compatibility level.
+A full example value would be:
+
+ X-DH-Compat: 15
+
+=item Build-Depends: debhelper-compat (= <C>)
+
+The compatibility was declared in the F<debian/control> via a build dependency on the
+B<< debhelper-compat (= <C>) >> package in the B<Build-Depends> field. In the output,
+the B<C> is replaced by the actual compatibility level. A full example value would be:
+
+ Build-Depends: debhelper-compat (= 15)
+
+=back
+
+=back
+
+=head2 supported-compat-levels (AJSON, CRFA)
+
+B<Synopsis>: B<dh_assistant> B<supported-compat-levels>
+
+Outputs information about which compat levels, this build of debhelper knows
+about.
+
+This command accepts no options or arguments.
+
+=head2 which-build-system (AJSON)
+
+B<Synopsis>: B<dh_assistant> B<which-build-system> [S<I<build step>>] [S<I<build system options>>]
+
+Output information about which build system would be used for a particular build step. The build step
+must be one of B<configure>, B<build>, B<test>, B<install> or B<clean> and must be the first argument
+after B<which-build-system> when provided. If omitted, it defaults to B<configure> as it is the
+most reliable step to use auto-detection on in a clean source directory. Note that build steps do not
+always agree when using auto-detection - particularly if the B<configure> step has not been run.
+
+Additionally, the B<clean> step can also provide "surprising" results for builds that rely on
+a separate build directory. In such cases, debhelper will return the first build system that
+uses a separate build directory rather than the one build system that B<configure> would detect.
+This is generally a cosmetic issue as both build systems are all basically a glorified
+B<rm -fr builddir> and more precise detection is functionally irrelevant as far as debhelper is
+concerned.
+
+The option accepts all debhelper build system arguments - i.e., options you can pass to all of
+the B<dh_auto_*> commands plus (for the B<install> step) the B<--destdir> option. These options
+affect the output and auto-detection in various ways. Passing B<-S> or B<--buildsystem>
+overrides the auto-detection (as it does for B<dh_auto_*>) but it still provides introspection
+into the chosen build system.
+
+Things that are useful to know about the output:
+
+=over 4
+
+=item *
+
+The key B<build-system> is the build system that would be used by debhelper for the given
+step (with the given options, debhelper compat level, environment variables and the given
+working directory). When B<-S> and B<--buildsystem> are omitted, this is the result of
+debhelper's auto-detection logic.
+
+The value is valid as a parameter for the B<--buildsystem> option.
+
+The special value B<none> is used to denote that no build system would be used. This value
+is not present in B<--list> parameter for the B<dh_auto_*> commands, but since debhelper/12.9
+the value is accepted for the B<--buildsystem> option.
+
+Note that auto-detection is subject to limitations in regards to third-party build systems.
+While debhelper I<does> support auto-detecting some third-party build systems, they must be
+installed for the detection to work. If they are not installed, the detection logic silently
+skips that build system (often resulting in B<build-system> being B<none> in the output).
+
+=item *
+
+The B<build-directory> and B<buildpath> values serve different but related purposes. The
+B<build-directory> generally mirrors the B<--builddirectory> option where as B<buildpath>
+is the output directory that debhelper will use. Therefore the former will often be null
+when B<--builddirectory> has not been passed while the latter will generally not be null
+(except when B<build-system> is B<none>).
+
+=item *
+
+The B<dest-directory> (B<--destdir>) is undefined for all build steps except the B<install> build
+step (will be output as null or absent). For the same reason, B<--destdir> should only be
+passed for B<install> build step.
+
+Note that if not specified, this value is currently null by default.
+
+=item *
+
+The B<parallel> value is subject to B<DEB_BUILD_OPTIONS>. Notably, if that does not include
+the B<parallel> keyword, then B<parallel> field in the output will always be 1.
+
+=item *
+
+Most fields in the output I<can> be null. Particular if there is no build system is detected
+(or when B<--buildsystem=none>). Additionally, many of the fields can be null even if there
+is a build system if the build system does not use/set/define that variable.
+
+=back
+
+=head2 detect-hook-targets (AJSON)
+
+B<Synopsis>: B<dh_assistant> B<detect-hook-targets>
+
+Detects possible override targets and hook targets that L<dh(1)> might use (provided that the
+relevant command is in the sequence).
+
+The detection is based on scanning the rules file for any target that I<might look> like a hook
+target and can therefore list targets that are in fact not hook targets (or are but will never
+be triggered for other reasons).
+
+The detection uses a similar logic for scanning the rules file and is therefore subject to
+makefile conditionals (i.e., the truth value of makefile conditionals can change whether a hook
+target is visible in the output of this command). In theory, you would have to setup up the
+environment to look like it would during a build for getting the most accurate output. Though,
+a lot of packages will not have conditional hook targets, so the "out of the box" behaviour
+will work well in most cases.
+
+The output looks something like this:
+
+ {
+ "commands-not-in-path": [
+ "dh_foo"
+ ],
+ "hook-targets": [
+ {
+ "command": "dh_strip_nondeterminism",
+ "is-empty": true,
+ "package-section-param": null,
+ "filename": "debian/rules",
+ "target-name": "override_dh_strip_nondeterminism"
+ },
+ {
+ "command": "dh_foo",
+ "is-empty": false,
+ "package-section-param": "-a",
+ "filename": "debian/rules",
+ "target-name": "override_dh_foo-arch"
+ }
+ ]
+ }
+
+In more details:
+
+=over 4
+
+=item commands-not-in-path
+
+This attribute lists all the commands related to hook targets, which B<dh_assistant> could B<not>
+find in PATH. These are usually caused by either the command not being installed on the system
+where B<dh_assistant> is run or by the command not existing at all.
+
+If you are using this command to verify an hook target is present, please double check that the
+command is spelled correctly.
+
+=item hook-targets
+
+List over hook targets found along with additional information about them.
+
+=over 4
+
+=item command
+
+Attribute that lists which command this hook target is related too.
+
+=item target-name
+
+The actual target name detected in the F<debian/rules> file.
+
+=item is-empty
+
+A boolean that determines whether L<dh(1)> will optimize the hook out at runtime (see "Completely empty targets" in
+L<dh(1)>). Note that empty override targets will still cause L<dh(1)> to skip the original command.
+
+=item package-section-param
+
+This attribute defines what package selection parameter should be passed to B<dh_*> commands used
+in the hook target. It can either be B<-a>, B<-i> or (if no parameter should be used) C<null>.
+
+=item filename
+
+This attribute reports which file the target was found it. In most cases, this will always be "debian/rules"
+though in case of include files, the target could appear in an include file. Note this attribute is not
+super reliable as L<make(1)> only reports it for targets with a "recipe" (targets with commands inside
+them). When B<make> does not provide the filename, B<dh_assistant> blindly assumes the filename is
+"debian/rules" (as overrides via includes is not a commonly used feature).
+
+Note this accuracy of this attribute is limited about what data B<dh_assistant> can read out from the
+following command:
+
+ LC_ALL=C make -Rrnpsf debian/rules debhelper-fail-me 2>/dev/null
+
+=back
+
+=back
+
+This command accepts no options or arguments.
+
+
+=head2 detect-unknown-hook-targets (AJSON, LINT)
+
+B<Synopsis>: B<dh_assistant> B<detect-unknown-hook-targets> [--output-format=json] [command-options]
+
+Detects unknown and possibly misspelled override targets and hook targets in F<debian/rules> that
+will most likely not be used by L<dh(1)>.
+
+This command differs from B<detect-hook-targets> subtly in the scope. The B<detect-hook-targets>
+will list all targets that looks like hook targets whether they are applicable or not. This
+command show all hook targets, for which a command cannot be found in any sequence. Accordingly,
+this command is better for linting purposes whereas B<detect-hook-targets> is better if you want
+to know which hook targets are present. All the limitations listed in B<detect-hook-targets>
+about scanning the rules file apply equally to this command.
+
+This command will attempt will attempt to load any sequence add-on listed via build-dependencies
+and therefore these must be installed. Additional modules can be passed via B<--with> like with
+L<dh(1)> as needed.
+
+This command will also need one of the following perl modules to be available:
+L<Text::Levenshtein>, L<Text::LevenshteinXS>, L<Text::Levenshtein::XS>. The first one can be
+installed via B<apt install libtext-levenshtein-perl>.
+
+The text output is intended for human consumption and should be self-explanatory. Since it is
+not stable, it will not be documented. The JSON output looks something like this:
+
+ {
+ "unknown-hook-targets": [
+ {
+ "target-name": "execute_before_dh_instlal",
+ "filename": "debian/rules",
+ "candidates": [
+ "execute_before_dh_install"
+ ]
+ }
+ ]
+ }
+
+In more details:
+
+=over 4
+
+=item unknown-hook-targets
+
+List of all the unknown hook targets found along with additional information about them.
+
+=over 4
+
+=item target-name
+
+The actual target name detected in the file (usually F<debian/rules>).
+
+=item filename
+
+This attribute reports which file the target was found it. In most cases, this will always be "debian/rules"
+though in case of include files, the target could appear in an include file. Note this attribute is not
+super reliable as L<make(1)> only reports it for targets with a "recipe" (targets with commands inside
+them). When B<make> does not provide the filename, B<dh_assistant> blindly assumes the filename is
+"debian/rules" (as overrides via includes is not a commonly used feature).
+
+Note this accuracy of this attribute is limited about what data B<dh_assistant> can read out from the
+following command:
+
+ LC_ALL=C make -Rrnpsf debian/rules debhelper-fail-me 2>/dev/null
+
+=item candidates
+
+When not null and not empty, each element in this list are names for likely candidates for the
+"correct" name of this target.
+
+=item filename
+
+=back
+
+=item issues
+
+If present, then it is a list of one or more reasons why this output is definitely incomplete. Each element
+in the list is an object with the following keys:
+
+=over 4
+
+=item issue
+
+A key defining the issue. Currently, it is always B<load-addon>, which signals that B<dh_assistant> could
+not load the add-on listed in the B<addon> key.
+
+Parsers should assume new issue types may appear in the future.
+
+=item addon
+
+If present, it defines the name of a B<dh> sequence add-on that is related to the failure.
+
+=back
+
+=back
+
+This command accepts the following options:
+
+=over 4
+
+=item B<--output-format=>I<FORMAT>
+
+Request a certain type of output format. Valid values are B<text> or B<json>.
+
+The text format is intended for human consumption and may change between versions without any
+regard for machine consumption. If you want to use this command for machine consumption, please
+use the JSON format.
+
+=item B<--no-linter-exit-code>, B<--linter-exit-code>
+
+These options control whether the command should exit with the linter exit code (2) or not (0)
+when an unknown target is found. By default, it uses the linter exit code when an unknown target
+is found.
+
+=item B<--with> I<addon>, B<--without> I<addon>
+
+These options behave the same as the L<dh(1)> options with the same name.
+
+=back
+
+=head2 list-commands (RJSON)
+
+B<Synopsis>: B<dh_assistant> B<list-commands> [--output-format=json] [command-options]
+
+Load all B<dh> sequence add-ons and extract a full list of all commands that will be invoked across
+all sequences. The command makes no attempt to filter out commands that will not be run due to
+override targets or due to certain sequences not being run (by B<dh> or at all).
+
+As the command will attempt to load all plugins, they must be installed.
+
+The text output is intended for human consumption and should be self-explanatory. Since it is
+not stable, it will not be documented. The JSON output looks something like this:
+
+ {
+ "commands": [
+ {
+ "command": "dh_auto_build"
+ },
+ {
+ "command": "dh_auto_clean"
+ },
+ [... more commands listed here... ]
+ ],
+ "issues": [
+ {
+ "issue": "load-addon",
+ "addon": "foo"
+ }
+ ]
+ }
+
+=over 4
+
+=item commands
+
+The top level key containing the list of all commands. Each element in the list are an object and
+can have the following keys:
+
+=over 4
+
+=item command
+
+The name of the command.
+
+While most commands are resolved via PATH, a sequence add-on could register a command via a full path
+(by passing the path search). If so, the command provided in this output will also use the full path.
+
+=back
+
+=item issues
+
+If present, then it is a list of one or more reasons why this output is definitely incomplete. Each element
+in the list is an object with the following keys:
+
+=over 4
+
+=item issue
+
+A key defining the issue. Currently, it is always B<load-addon>, which signals that B<dh_assistant> could
+not load the add-on listed in the B<addon> key.
+
+Parsers should assume new issue types may appear in the future.
+
+=item addon
+
+If present, it defines the name of a B<dh> sequence add-on that is related to the failure.
+
+=back
+
+=back
+
+This command accepts the following options:
+
+=over 4
+
+=item B<--output-format=>I<FORMAT>
+
+Request a certain type of output format. Valid values are B<text> or B<json>.
+
+The text format is intended for human consumption and may change between versions without any
+regard for machine consumption. If you want to use this command for machine consumption, please
+use the JSON format.
+
+=item B<--with> I<addon>, B<--without> I<addon>
+
+These options behave the same as the L<dh(1)> options with the same name.
+
+=back
+
+=head2 list-guessed-dh-config-files (AJSON)
+
+B<Synopsis>: B<dh_assistant> B<list-guessed-dh-config-files> [command-options]
+
+Load all B<dh> sequence add-ons, determine the full list of commands could be used by this
+source package and for each command used, then attempt to I<guess> which "config files"
+these commands are interested in.
+
+Note this command only guesses "per command config files". Standard global config files
+such as F<debian/control>, F<debian/rules>, and F<debian/compat> are not included in this
+output.
+
+As the command name implies, the resulting output is not a full list (and will never be).
+The B<dh_assistant> tool have to derive this from optional metadata that commands can
+choose to provide and B<dh_assistant> has no means to validate that this metadata is up
+to date.
+
+As the command will attempt to load all plugins, they must be installed.
+
+The text output is intended for human consumption and should be self-explanatory. Since it is
+not stable, it will not be documented. The JSON output looks something like this:
+
+ {
+ "config-files": [
+ {
+ "commands": [
+ {
+ "command": "dh_autoreconf_clean"
+ }
+ ],
+ "file-type": "pkgfile",
+ "pkgfile": "autoreconf.before"
+ },
+ {
+ "commands": [
+ {
+ "command": "dh_installgsettings"
+ }
+ ],
+ "file-type": "pkgfile",
+ "pkgfile": "gsettings-override"
+ },
+ # [ ... more entries here ...]
+ ],
+ "issues": [
+ {
+ "issue": "load-addon",
+ "addon": "foo"
+ }
+ ]
+ }
+
+
+=over 4
+
+=item config-files
+
+The top level key containing the list of all config-files. Each element in the list are an object and
+can have the following keys:
+
+=over 4
+
+=item file-type
+
+The type of config file detected. At the time of writing, this will always be B<pkgfile>. However,
+other values may appear in the future.
+
+The B<pkgfile> key means that the config file is a B<debhelper pkgfile> (named after the B<pkgfile> sub
+in L<Debian::Debhelper::Dh_Lib> that locates the file).
+
+=item pkgfile
+
+When B<file-type> is B<pkgfile>, this key defines the name stem of the B<pkgfile>. An example, this will
+be B<install> for L<dh_install(1)>'s config file and B<docs> for L<dh_installdocs(1)>'s config file.
+
+When B<file-type> is B<not> B<pkgfile>, then this key will be absent.
+
+Typically names for these files are:
+
+ debian/PKGFILE
+ debian/PACKAGE.PKGFILE
+
+However, there are more variants caused by B<--name> plus architecture specific suffixes.
+
+=item internal
+
+This key may exist and any value for it is not standardized. Use at own peril.
+
+It used for document certain specific implementation details such as bug compatibility and may change
+as the situation changes.
+
+=item commands
+
+This key will be a list with each element in it being an object with the following keys:
+
+=over 4
+
+=item command
+
+Name of the command that is interested in this config file. Multiple commands can be interested in the same
+config file. An example of this would be B<dh_installinit>, B<dh_installsystemd> and B<dh_installtmpfiles>,
+which all reacts to (the now) deprecated B<tmpfile> pkgfile. In the particular case, only one command reacts
+to the file for a given compat level (but that information is not available to B<dh_assistant> and therefore
+is not available in this output either).
+
+=back
+
+=back
+
+=item issues
+
+If present, then it is a list of one or more reasons why this output is definitely incomplete. Each element
+in the list is an object with the following keys:
+
+=over 4
+
+=item issue
+
+A key defining the issue. Currently, it is always B<load-addon>, which signals that B<dh_assistant> could
+not load the add-on listed in the B<addon> key.
+
+Parsers should assume new issue types may appear in the future.
+
+=item addon
+
+If present, it defines the name of a B<dh> sequence add-on that is related to the failure.
+
+=back
+
+=back
+
+This command accepts the following options:
+
+=over 4
+
+=item B<--with> I<addon>, B<--without> I<addon>
+
+These options behave the same as the L<dh(1)> options with the same name.
+
+=back
+
+=head2 log-installed-files (BLD)
+
+B<Synopsis>: B<dh_assistant> B<log-installed-files> B<< -pI<pkg> >> I<[--on-behalf-of-cmd=dh_foo]> B<path ...>
+
+Mark one or more paths as installed for a given package. This is useful for telling L<dh_missing(1)> that the
+paths have been installed manually.
+
+The B<--on-behalf-of-cmd> option can be used by third-party tools to have B<dh_assistant> list them as the
+installer of the provided paths. The convention is to use the basename of the tool itself as its name
+(e.g. B<dh_install>).
+
+Please keep in mind that:
+
+=over 4
+
+=item *
+
+B<No> glob or substitution expansion is done by B<dh_assistant> on the provided paths. If you want to use globs,
+have the shell perform the expansion first.
+
+=item *
+
+Paths must be given as relative to the source root directory (e.g., F<debian/tmp/...>)
+
+=item *
+
+You I<can> provide a directory. If you do, the directory and anything recursively below it will be considered
+as installed. Note that it is fine to provide the directory even if paths inside of it has been excluded as long
+as the directory is fully "covered".
+
+=item *
+
+Do not worry about providing the same filename twice in different invocations to B<dh_assistant> due to B<-arch> /
+B<-indep> overrides. While it will be recorded multiple internally, L<dh_missing(1)> will deduplicate when it
+parses the records.
+
+=back
+
+Note this command only I<marks> paths as installed. It does not actually install them - the caller should ensure
+that the paths are in fact handled (or installed).
+
+=head2 restore-file-on-clean (BLD)
+
+B<Synopsis>: B<dh_assistant> B<restore-file-on-clean> B<FILE ...>
+
+This command will take a backup of listed files and tell L<dh_clean(1)> to restore them when it runs.
+
+Note that generally you do not need to restore modified files on clean. Often you can get away with just
+removing them if they are regenerated anyway (which is the most common case for files being modified during
+builds). Use this command when something taints a file and the build does not cope with the file being
+removed.
+
+The file is stored in B<debian/.debhelper>. If you remove this directory manually without calling
+L<dh_clean(1)> then your B<dh_assistant> provided backup is gone permanently and the restore will never
+occur. At this point, only a version control system or another backup can restore the files.
+
+The command has the following limitations:
+
+=over 4
+
+=item No thread-safety - concurrency will corrupt the restore
+
+The command relies on updating an internal index and concurrent writes will cause it to be corrupt.
+
+While most B<dh_*> commands does not use the underlying function, any of them could do so. Avoid running
+another B<dh_*> command while B<dh_assistant> processes this command (especially running multiple concurrent
+instances of B<dh_assistant restore-file-on-clean> is asking for corruption!).
+
+=item Files only, not directories nor symlinks to files
+
+This command will only restore files; not directories or symlinks to files. It will reject any non-files.
+
+Additionally, if the directory containing the file is removed, the restore will fail (as B<debhelper>
+does not track the directory, it cannot restore it reliably). If this happens, you can do a B<mkdir>
+to restore the directory and run L<dh_clean(1)> again to get the files back. After that, consider
+what went wrong and whether you are using the correct tool(s).
+
+=item Strict file names
+
+All filenames must be relative to the package root (without using the B<./> prefix). No hidden files (that
+is any file starting with a period B<.>) and no version control directories (such as B<CVS>). The checks
+are best effort.
+
+These checks are here to ensure you do not accidentally trash important data that would help you undo
+mistakes.
+
+=item Heavy duty
+
+The command takes a B<full copy> of all files you pass it. This is fine for a handful of small files,
+which is the intended use-case. If you find yourself passing 10+ files or very large files, you might
+be applying a sledgehammer where you needed a different tool.
+
+=back
+
+=head2 supports (CFFA)
+
+B<Synopsis>: B<dh_assistant> B<supports> B<COMMAND>
+
+This command is a scripting aid to programmatically determine whether B<dh_assistant> knows about a given
+subcommand. Pass the name of a subcommand and this command will exit successfully if the subcommand was known
+and unsuccessfully otherwise.
+
+=head1 COMMAND TAGS
+
+Most commands have one or more of the following "tags" associated with them. Their
+meaning is defined here.
+
+=over 4
+
+=item AJSON
+
+The command always provides JSON output. See L</JSON OUTPUT> for details.
+
+=item OJSON
+
+The command *can* provide JSON output via B<--output-format=json>, but does not
+do so by default. See L</JSON OUTPUT> for details when using B<--output-format=json>.
+
+=item LINT
+
+The command is or can be used for linting purposes. This command will exit with code 2 when an important
+issue is found.
+
+Note that commands may have options that redefine what is considered an "important" issue.
+
+=item CRFA
+
+I<Mnemonic "Can be Run From Anywhere">
+
+Most commands must be run inside a source package root directory (a directory
+containing F<debian/control>) because debhelper will need the package metadata
+to lookup the information. Any command with this tag are exempt from this
+requirement and is expected to work regardless of where they are run.
+
+=item BLD
+
+The command is intended to be used as a part of a package build. It may leave
+artifacts behind that will need a L<dh_clean(1)> invocation to remove.
+
+=back
+
+=head1 JSON OUTPUT
+
+Most commands uses JSON format as output. Consumers need to be aware that:
+
+=over 4
+
+=item *
+
+Additional keys may be added at any time. For backwards compatibility, the absence
+of a key should in general be interpreted as null unless another default is documented
+or would be "obvious" for that case.
+
+=item *
+
+Many keys can be null/undefined in special cases. As an example, some information may
+be unavailable when this command is run directly from the debhelper source (git repository).
+
+=back
+
+The output will be prettified when stdout is detected as a terminal. If
+you need to pipe the output to a pager/file (etc.) and still want it
+prettified, please use an external JSON formatter. An example of this:
+
+ dh_assistant supported-compat-levels | json_pp | less
+
+=cut
+
+my $JSON_ENCODER = JSON::PP->new->utf8;
+
+# Prettify if we think the user is reading this.
+$JSON_ENCODER = $JSON_ENCODER->pretty->space_before(0)->canonical if -t STDOUT;
+
+# We never use the log file for this tool
+inhibit_log();
+$Debian::Debhelper::Dh_Lib::PARSE_DH_SEQUENCE_INFO = 1;
+
+my %COMMANDS = (
+ 'help' => \&_do_help,
+ '-h' => \&_do_help,
+ '--help' => \&_do_help,
+ 'active-compat-level' => \&active_compat_level,
+ 'supported-compat-levels' => \&supported_compat_levels,
+ 'which-build-system' => \&which_build_system,
+ 'detect-hook-targets' => \&detect_hook_targets,
+ 'detect-unknown-hook-targets' => \&detect_unknown_hook_targets,
+ 'list-commands' => \&list_commands,
+ 'list-guessed-dh-config-files' => \&list_guessed_dh_config_files,
+ 'log-installed-files' => \&log_installed_files_cmd,
+ 'restore-file-on-clean' => \&dh_assistant_restore_file_on_clean,
+ 'supports' => \&supports,
+);
+
+my ($COMMAND) = shift(@ARGV);
+for my $arg (@ARGV) {
+ if ($arg eq '--help' or $arg eq '-h') {
+ $COMMAND = 'help';
+ last;
+ }
+}
+
+
+sub _do_help {
+ my $me = basename($0);
+ print <<"EOF";
+${me}: Tool for supporting debhelper tools and provide introspection
+
+Usage: ${me} <command> [... addition arguments or options ...]
+
+The following commands are available:
+ help Show this help
+ active-compat-level Output information about which compat level is declared/active (AJSON)
+ supported-compat-levels Output information about supported compat levels (AJSON, CRFA)
+ which-build-system Determine which build system will be used (AJSON)
+ detect-hook-targets Detect and output possible override and hook targets (AJSON)
+ detect-unknown-hook-targets
+ Detect unknown / typos of known hook targets (RJSON)
+ list-commands List all commands across all sequences (RJSON)
+ list-guessed-dh-config-files
+ List guessed "config files" for debhelper commands (AJSON)
+ log-installed-files Mark one or more paths as "installed" so dh_missing is aware (BLD)
+ restore-file-on-clean Mark one or more files as to be restored by dh_clean (BLD)
+ supports Script aid: Test whether dh_assistant knows a particular command (CRFA)
+
+Command tags:
+
+ * AJSON The command always provides JSON output.
+ * RJSON The command *can* provide JSON output via --output-format=json.
+ * LINT The command is or can be used for linting purposes. This command will exit with code 2
+ when an important issue is found.
+ * CRFA Command does not need to be run from a package source directory
+ (Mnemonic "Can be Run From Anywhere")
+ * BLD The command is intended to be used as a part of a package build.
+ It may leave artifacts behind that will need a dh_clean invocation to remove.
+
+
+Its primary purpose is to provide support for third-party debhelper implementations
+not using the debhelper script API or provide introspection for third-party tools
+(e.g., linters). Unless stated otherwise, commands must be run inside a source
+package root directory - that is, the directory containing "debian/control".
+
+Most commands use or can provide JSON output. When stdout is a TTY, the JSON will be
+prettified. See the manpage if you want formatting in other cases.
+EOF
+ return;
+}
+
+sub _assert_debian_control_exists {
+ return if -f 'debian/control';
+ require Cwd;
+ my $cwd = Cwd::getcwd();
+ warning("$cwd does not look like a package source directory (expected $cwd/debian/control to exist and be a file)");
+ error("$COMMAND must be run inside a package source directory");
+ return;
+}
+
+sub _output {
+ my ($kvpairs) = @_;
+ print $JSON_ENCODER->encode($kvpairs);
+ return;
+}
+
+sub active_compat_level {
+ if (@ARGV) {
+ error("$COMMAND: No arguments supported (please remove everything after the command)");
+ }
+ _assert_debian_control_exists();
+ my ($active_compat, $declared_compat, $declared_compat_source) = Debian::Debhelper::Dh_Lib::get_compat_info();
+ if (not defined($declared_compat_source)) {
+ $declared_compat = undef;
+ $active_compat = undef if not exists($ENV{DH_COMPAT});
+ }
+ my %compat_info = (
+ 'active-compat-level' => $active_compat,
+ 'declared-compat-level' => $declared_compat,
+ 'declared-compat-level-source' => $declared_compat_source,
+ );
+ _output(\%compat_info);
+ return;
+}
+
+sub supported_compat_levels {
+ if (@ARGV) {
+ error("$COMMAND: No arguments supported (please remove everything after the command)");
+ }
+ my %compat_levels = (
+ 'MIN_COMPAT_LEVEL' => Debian::Debhelper::Dh_Lib::MIN_COMPAT_LEVEL,
+ 'LOWEST_NON_DEPRECATED_COMPAT_LEVEL' => Debian::Debhelper::Dh_Lib::LOWEST_NON_DEPRECATED_COMPAT_LEVEL,
+ 'LOWEST_VIRTUAL_DEBHELPER_COMPAT_LEVEL' => Debian::Debhelper::Dh_Lib::LOWEST_VIRTUAL_DEBHELPER_COMPAT_LEVEL,
+ 'MAX_COMPAT_LEVEL' => Debian::Debhelper::Dh_Lib::MAX_COMPAT_LEVEL,
+ 'HIGHEST_STABLE_COMPAT_LEVEL' => Debian::Debhelper::Dh_Lib::HIGHEST_STABLE_COMPAT_LEVEL,
+ 'MIN_COMPAT_LEVEL_NOT_SCHEDULED_FOR_REMOVAL' => Debian::Debhelper::Dh_Lib::MIN_COMPAT_LEVEL_NOT_SCHEDULED_FOR_REMOVAL,
+ );
+ _output(\%compat_levels);
+ return;
+}
+
+sub which_build_system {
+ my ($opt_buildsys, $destdir);
+ my $first_argv = @ARGV ? $ARGV[0] : '';
+ my %options = (
+ # Emulate dh_auto_install's --destdir
+ "destdir=s" => \$destdir,
+ );
+ _assert_debian_control_exists();
+ # We never want the build system initialization to modify anything (e.g. create "HOME")
+ $dh{NO_ACT} = 1;
+ require Debian::Debhelper::Dh_Buildsystems;
+ Debian::Debhelper::Dh_Buildsystems::buildsystems_init(options => \%options);
+ my @non_options = grep { !m/^-/ } @ARGV;
+ my $step = @non_options ? $non_options[0] : 'configure';
+ if (@non_options && $first_argv =~ m/^-/) {
+ error("$COMMAND: If the build step is provided, it must be before any options");
+ }
+ if (@non_options > 1) {
+ error("$COMMAND: At most one positional argument is supported");
+ }
+ if (defined($destdir) and $step ne 'install') {
+ warning("$COMMAND: --destdir is not defined for build step \"$step\". Ignoring option")
+ }
+ {
+ no warnings qw(once);
+ $opt_buildsys = $Debian::Debhelper::Dh_Buildsystems::opt_buildsys;
+ }
+ my $build_system = Debian::Debhelper::Dh_Buildsystems::load_buildsystem($opt_buildsys, $step);
+ my %result = (
+ 'build-system' => defined($build_system) ? $build_system->NAME : 'none',
+ 'for-build-step' => $step,
+ 'source-directory' => defined($build_system) ? $build_system->get_sourcedir : undef,
+ 'build-directory' => defined($build_system) ? $build_system->get_builddir : undef,
+ 'dest-directory' => defined($build_system) ? $destdir : undef,
+ 'buildpath' => defined($build_system) ? $build_system->get_buildpath : undef,
+ 'parallel' => defined($build_system) ? $build_system->get_parallel : undef,
+ 'upstream-arguments' => $dh{U_PARAMS},
+ );
+ _output(\%result);
+ return;
+}
+
+sub _in_path {
+ my ($cmd) = @_;
+ for my $dir (split(':', $ENV{PATH})) {
+ return 1 if -x "${dir}/${cmd}";
+ }
+ return 0;
+}
+
+sub _load_hook_targets {
+ require Debian::Debhelper::SequencerUtil;
+ Debian::Debhelper::SequencerUtil::rules_explicit_target('does-not-matter');
+ my ($explicit_targets);
+ {
+ no warnings qw(once);
+ $explicit_targets = \%Debian::Debhelper::SequencerUtil::EXPLICIT_TARGETS;
+ }
+ return $explicit_targets;
+}
+
+sub _hook_target_variants {
+ my ($name) = @_;
+ my @base = (
+ "override_${name}",
+ "execute_before_${name}",
+ "execute_after_${name}",
+ );
+ return map {
+ (
+ $_,
+ "${_}-arch",
+ "${_}-indep",
+ )
+ } @base;
+}
+
+
+sub _load_levenshtein {
+ my @modules = (qw(
+ Text::LevenshteinXS
+ Text::Levenshtein::XS
+ Text::Levenshtein
+ ));
+ my $err;
+ for my $module (@modules) {
+ my $distance_func = eval "use $module (); \\&${module}::distance";
+ $err = $@;
+ if (defined($distance_func)) {
+ return $distance_func;
+ }
+ }
+ my $module_names = join(', ', @modules);
+ warning("Could not load any of the modules ${module_names}");
+ warning("Usually, `apt install libtext-levenshtein-perl` will fix this problem.");
+ error("This subcommand requires one of the following modules to be available: ${module_names}. Last failure was: $@");
+}
+
+
+sub _all_sequence_commands {
+ my ($forgive_errors, @addon_requests) = @_;
+ my ($sequences, @all_commands, @unloadable);
+ Debian::Debhelper::SequencerUtil::load_sequence_addon('root-sequence', 'both');
+ my @addons = Debian::Debhelper::SequencerUtil::compute_selected_addons('binary', @addon_requests);
+ # Load addons, which can modify sequences.
+ foreach my $addon (@addons) {
+ my $addon_name = $addon->{'name'};
+ my $addon_type = $addon->{'addon-type'};
+ eval {
+ Debian::Debhelper::SequencerUtil::load_sequence_addon($addon_name, $addon_type);
+ };
+ if (my $err = $@) {
+ die($err) if not $forgive_errors;
+ push(@unloadable, $addon_name);
+ }
+ }
+ {
+ no warnings qw(once);
+ $sequences = \%Debian::Debhelper::DH::SequenceState::sequences;
+ }
+ my %seen;
+ for my $sequence(values(%{$sequences})) {
+ my @commands = map {$_->[0]} $sequence->flatten_sequence('both', 0);
+ for my $command (@commands) {
+ next if $command =~ m{^(?:debian/rules|create-stamp)};
+ next if exists($seen{$command});
+ $seen{$command} = 1;
+ push(@all_commands, $command);
+ }
+ }
+ return \@all_commands, \@unloadable;
+}
+
+sub list_commands {
+ _assert_debian_control_exists();
+ require Getopt::Long;
+ Getopt::Long::config('no_ignore_case');
+ require Debian::Debhelper::SequencerUtil;
+ my $output_format = "text";
+ my (@addon_requests);
+ my %options=(
+ "output-format=s" => \$output_format,
+ "with=s" => sub {
+ my ($option, $value) = @_;
+ push(@addon_requests, map { "+${_}" } split(",", $value));
+ },
+ "without=s" => sub {
+ my ($option, $value) = @_;
+ push(@addon_requests, map { "-${_}" } split(",", $value));
+ },
+ );
+ Getopt::Long::GetOptionsFromArray(\@ARGV, %options)
+ or error("Could not parse the arguments");
+
+ if (@ARGV) {
+ my $value = $ARGV[0];
+ error("$COMMAND: No non-options supported - please remove ${value}");
+ }
+
+ my ($all_commands, $unloadables) = _all_sequence_commands(1, @addon_requests);
+ if ($output_format eq 'json') {
+ my @commands_json;
+ for my $command (sort(@{$all_commands})) {
+ push(@commands_json, {
+ 'command' => $command,
+ });
+ }
+ my %result = (
+ "commands" => \@commands_json,
+ );
+ if (@{$unloadables}) {
+ my @issues;
+ for my $addon (@{$unloadables}) {
+ push(@issues, {
+ "issue" => "load-addon",
+ "addon" => $addon,
+ });
+ };
+ $result{'issues'} = \@issues;
+ }
+ _output(\%result);
+ } elsif ($output_format eq 'text') {
+ print("Commands present in at least one sequence for this source package (sorted by name):\n");
+ for my $command (sort(@{$all_commands})) {
+ print("\t${command}\n");
+ }
+ if (@{$unloadables}) {
+ my $addon_names = join(" ", @{$unloadables});
+ print("\n");
+ warning("Incomplete result. The following sequence add-ons could not be loaded: $addon_names");
+ }
+ } else {
+ error("Internal error: Missing case for ${output_format}");
+ }
+}
+
+sub _extract_annotations {
+ my ($command) = @_;
+ my @annotations;
+
+ foreach my $dir (split(':', $ENV{PATH})) {
+ if (open (my $h, "<", "$dir/$command")) {
+ while (<$h>) {
+ if (m/PROMISE: DH NOOP( WITHOUT\s+(.*))?\s*$/) {
+ if (defined($2)) {
+ push(@annotations, split(' ', $2));
+ } else {
+ push(@annotations, 'always-skip');
+ }
+ }
+ if (m/INTROSPECTABLE: CONFIG-FILES\s+(.*)\s*$/) {
+ push(@annotations, split(' ', $1));
+ }
+ }
+ close $h;
+ return @annotations;
+ }
+ }
+ return;
+}
+
+sub list_guessed_dh_config_files {
+ _assert_debian_control_exists();
+ require Getopt::Long;
+ Getopt::Long::config('no_ignore_case');
+ require Debian::Debhelper::SequencerUtil;
+ my (@addon_requests);
+ my %options=(
+ "with=s" => sub {
+ my ($option, $value) = @_;
+ push(@addon_requests, map { "+${_}" } split(",", $value));
+ },
+ "without=s" => sub {
+ my ($option, $value) = @_;
+ push(@addon_requests, map { "-${_}" } split(",", $value));
+ },
+ );
+ Getopt::Long::GetOptionsFromArray(\@ARGV, %options)
+ or error("Could not parse the arguments");
+
+ if (@ARGV) {
+ my $value = $ARGV[0];
+ error("$COMMAND: No non-options supported - please remove ${value}");
+ }
+
+ my ($all_commands, $unloadables) = _all_sequence_commands(1, @addon_requests);
+ my $pkg_files = {};
+ my $bug_950723;
+ for my $command (@{$all_commands}) {
+ my @annotations = _extract_annotations($command);
+ next if not @annotations or $annotations[0] eq 'always-skip';
+ for my $annotation (@annotations) {
+ my $type = 'pkgfile';
+ my $need = $annotation;
+ if ($annotation =~ m/^([a-zA-Z0-9-_]+)\((.*)\)$/) {
+ ($type, $need) = ($1, $2);
+ }
+ if ($type eq 'internal') {
+ $bug_950723 = 1 if $need eq 'bug#950723';
+ }
+ next if $type ne 'pkgfile' and $type ne 'pkgfile-logged';
+ my $key = "pkgfile/${need}";
+ my $existing = $pkg_files->{$key};
+ if (defined($existing)) {
+ push(@{$existing->{'commands'}}, {
+ 'command' => $command,
+ });
+ } else {
+ $existing = {
+ 'file-type' => 'pkgfile',
+ 'pkgfile' => $need,
+ 'commands' => [{
+ 'command' => $command,
+ }],
+ };
+ $pkg_files->{$key} = $existing;
+ }
+ if ($bug_950723) {
+ $existing->{"internal"}{"bug#950723"} = JSON::PP::true;
+ }
+ }
+ }
+
+ my @config_files = values(%{$pkg_files});
+ my %result = (
+ "config-files" => \@config_files,
+ );
+ if (@{$unloadables}) {
+ my @issues;
+ for my $addon (@{$unloadables}) {
+ push(@issues, {
+ "issue" => "load-addon",
+ "addon" => $addon,
+ });
+ };
+ $result{'issues'} = \@issues;
+ }
+ _output(\%result);
+}
+
+
+sub detect_unknown_hook_targets {
+ _assert_debian_control_exists();
+ my $distance_func = _load_levenshtein();
+ require Getopt::Long;
+ Getopt::Long::config('no_ignore_case');
+ require Debian::Debhelper::SequencerUtil;
+ my $output_format = "text";
+ my (@addon_requests, %all_overrides, %unknown_hooks);
+ my $lint_exit = 1;
+ my %options=(
+ "output-format=s" => \$output_format,
+ "linter-exit-code!" => \$lint_exit,
+ "with=s" => sub {
+ my ($option, $value) = @_;
+ push(@addon_requests, map { "+${_}" } split(",", $value));
+ },
+ "without=s" => sub {
+ my ($option, $value) = @_;
+ push(@addon_requests, map { "-${_}" } split(",", $value));
+ },
+ );
+
+ Getopt::Long::GetOptionsFromArray(\@ARGV, %options)
+ or error("Could not parse the arguments");
+ if ($output_format ne 'text' and $output_format ne 'json') {
+ error("--output-format must be either text or json\n");
+ }
+ if (@ARGV) {
+ my $value = $ARGV[0];
+ error("$COMMAND: No non-options supported - please remove ${value}");
+ }
+ my $explicit_targets = _load_hook_targets();
+ my %missing_targets = map {
+ $_ => 1
+ } grep {
+ $_ ne 'debhelper-fail-me' and !m{^(?:debian/rules|create-stamp)}
+ } keys(%{$explicit_targets});
+
+ my ($all_commands, $unloadables) = _all_sequence_commands(1, @addon_requests);
+ for my $command (@{$all_commands}) {
+ for my $variant (_hook_target_variants($command)) {
+ $all_overrides{$variant} = 1;
+ delete($missing_targets{$variant});
+ }
+ }
+ my @variants = sort(keys(%all_overrides));
+ for my $target (keys(%missing_targets)) {
+ my @closest_variants;
+ my $closest_variant_distance = 9999;
+ for my $variant (@variants) {
+ next if abs(length($target) - length($variant)) > 3;
+ my $dist = $distance_func->($target, $variant);
+ next if $dist > $closest_variant_distance or $dist > 3;
+ if ($dist < $closest_variant_distance) {
+ $closest_variant_distance = $dist;
+ @closest_variants = ();
+ }
+ push(@closest_variants, $variant);
+ }
+ next if not @closest_variants and $target !~ m{^(?:override|execute_before|execute_after)_};
+ @closest_variants = sort(@closest_variants);
+ $unknown_hooks{$target} = \@closest_variants;
+ }
+
+ if ($output_format eq 'json') {
+ my @hook_target_data;
+ for my $target (sort(keys(%unknown_hooks))) {
+ my $options = $unknown_hooks{$target};
+ my (undef, $filename) = @{$explicit_targets->{$target}};
+ push(@hook_target_data, {
+ 'target-name' => $target,
+ 'filename' => $filename,
+ 'candidates' => $options,
+ });
+ }
+ my %result = (
+ "unknown-hook-targets" => \@hook_target_data,
+ );
+ if (@{$unloadables}) {
+ my @issues;
+ for my $addon (@{$unloadables}) {
+ push(@issues, {
+ "issue" => "load-addon",
+ "addon" => $addon,
+ });
+ };
+ $result{'issues'} = \@issues;
+ }
+ _output(\%result);
+ } elsif ($output_format eq 'text') {
+ for my $target (sort(keys(%unknown_hooks))) {
+ my $options = $unknown_hooks{$target};
+ my (undef, $filename) = @{$explicit_targets->{$target}};
+ my $help = '';
+ if (@{$options}) {
+ if (scalar(@{$options}) == 1) {
+ my $name = $options->[0];
+ $help = " Likely a typo of ${name}";
+ } else {
+ my $names = join(', ', @{$options});
+ $help = " Likely a typo of one of ${names}";
+ }
+ }
+ print("The hook target ${target} in ${filename} does not seem to match any known commands. ${help}\n");
+ }
+ if (@{$unloadables}) {
+ my $addon_names = join(" ", @{$unloadables});
+ print("\n");
+ warning("Incomplete result. The following sequence add-ons could not be loaded: $addon_names");
+ }
+ } else {
+ error("Internal error: Missing case for ${output_format}");
+ }
+ if ($lint_exit && (%unknown_hooks or @{$unloadables})) {
+ exit(EXIT_CODE_LINT_ISSUES_FOUND);
+ }
+ exit(0);
+}
+
+sub detect_hook_targets {
+ if (@ARGV) {
+ error("$COMMAND: No arguments supported (please remove everything after the command)");
+ }
+ _assert_debian_control_exists();
+ my $explicit_targets = _load_hook_targets();
+ my (%result, @targets, @unverifiable_commands, %seen_cmds);
+ while (my ($target, $rule_details) = each(%{$explicit_targets})) {
+ next if $target !~ m{^(?:execute_before_|execute_after_|override_)(\S+?)(-indep|-arch)?$};
+ my ($command, $archness) = ($1, $2);
+ my $param;
+ if ($archness) {
+ $param = ($archness eq '-arch') ? '-a' : '-i' ;
+ }
+ my ($non_empty, $filename) = @{$rule_details};
+
+ my $target_info = {
+ 'target-name' => $target,
+ 'command' => $command,
+ 'package-section-param' => $param,
+ 'is-empty' => $non_empty ? JSON::PP::false : JSON::PP::true,
+ 'filename' => $filename,
+ };
+ push(@targets, $target_info);
+ push(@unverifiable_commands, $command) if not exists($seen_cmds{$command}) and not _in_path($command);
+ $seen_cmds{$command} = 1;
+ }
+ $result{'hook-targets'} = \@targets;
+ $result{'commands-not-in-path'} = \@unverifiable_commands;
+ _output(\%result);
+}
+
+sub dh_assistant_restore_file_on_clean {
+ init(inhibit_log => 1);
+ if (not @ARGV) {
+ error("At least one file name is required");
+ }
+ for my $file (@ARGV) {
+ lstat($file);
+ if ( ! -f _ ) {
+ error("The path ${file} was a symlink. It must be a file; not a symlink to a file") if -l _;
+ error("The path ${file} does not exist") if not -e _;
+ error("The path ${file} was not a file and this command only supports files");
+ }
+ if ($file =~ m{[.][.]}) {
+ # Someone can provide a patch when there is a use-case for "..foo".
+ # Said patch will need to ensure the file is inside the package root dir.
+ error("Files containing \"..\" (which ${file} does) are not supported.");
+ }
+ if ($file =~ m{^/}) {
+ error("Files must be relative to the package root (which ${file} was not)")
+ }
+ if ($file =~ m{^\.} or $file =~ m{/CVS/} or $file =~ m{/\.}) {
+ error("Cowardly refusing to track hidden files / version control files (${file}).");
+ }
+ Debian::Debhelper::Dh_Lib::restore_file_on_clean($file)
+ }
+}
+
+sub log_installed_files_cmd {
+ my $on_behalf_of = 'manually-via-dh_assistant';
+ init(
+ options => {
+ 'on-behalf-of-cmd=s' => \$on_behalf_of,
+ },
+ inhibit_log => 1,
+ );
+ if (index($on_behalf_of, '/') >= 0) {
+ error('The value for --on-behalf-of-cmd must not contain slashes');
+ }
+ if (@{$dh{DOPACKAGES}} != 1) {
+ error('The log-installed-files command must act on exactly one package (use -p<pkg> to define which)');
+ }
+ my $package = $dh{DOPACKAGES}[0];
+ for my $arg (@ARGV) {
+ $arg =~ tr:/:/:s;
+ if (! -e $arg) {
+ warning("The path ${arg} does not exist - double check it is correct. Note: it will recorded anyway.");
+ }
+ }
+ log_installed_files({
+ 'package' => $package,
+ 'tool_name' => $on_behalf_of,
+ }, @ARGV);
+}
+
+sub supports {
+ my ($command, @more) = @ARGV;
+ if (@more or not defined($command)) {
+ error("$COMMAND: Please provide exactly one argument");
+ }
+ exit(0) if exists($COMMANDS{$command});
+ exit(2);
+}
+
+if (not defined($COMMAND)) {
+ error('Usage: ' . basename($0) . ' <command>');
+}
+my $handler = $COMMANDS{$COMMAND};
+if (not defined($handler)) {
+ warning("Arguments/options must not be the first argument (except for --help)")
+ if $COMMAND =~ m/^-/;
+ my $available_cmds = join(' ', sort(grep { $_ ne '-h' and $_ ne '--help' and $_ ne 'help' } keys(%COMMANDS)));
+ error("Unknown command: $COMMAND. Use \"help\" or \"--help\" as first argument for usage. Available commands: ${available_cmds}");
+}
+
+$handler->();
+
+=head1 SEE ALSO
+
+L<debhelper(7)>
+
+This program is a part of debhelper.
+
+=cut
+
+1;