xref: /dokuwiki/lib/plugins/extension/cli.php (revision a8d2f3cb1b269c2542f4b804d54904bb2db3421c)
1*a8d2f3cbSAndreas Gohr<?php
2*a8d2f3cbSAndreas Gohr
3*a8d2f3cbSAndreas Gohruse splitbrain\phpcli\Colors;
4*a8d2f3cbSAndreas Gohr
5*a8d2f3cbSAndreas Gohr/**
6*a8d2f3cbSAndreas Gohr * Class cli_plugin_extension
7*a8d2f3cbSAndreas Gohr *
8*a8d2f3cbSAndreas Gohr * Command Line component for the extension manager
9*a8d2f3cbSAndreas Gohr *
10*a8d2f3cbSAndreas Gohr * @license GPL2
11*a8d2f3cbSAndreas Gohr * @author Andreas Gohr <andi@splitbrain.org>
12*a8d2f3cbSAndreas Gohr */
13*a8d2f3cbSAndreas Gohrclass cli_plugin_extension extends DokuWiki_CLI_Plugin
14*a8d2f3cbSAndreas Gohr{
15*a8d2f3cbSAndreas Gohr    /** @inheritdoc */
16*a8d2f3cbSAndreas Gohr    protected function setup(\splitbrain\phpcli\Options $options)
17*a8d2f3cbSAndreas Gohr    {
18*a8d2f3cbSAndreas Gohr        // general setup
19*a8d2f3cbSAndreas Gohr        $options->setHelp('Manage plugins and templates for this DokuWiki instance');
20*a8d2f3cbSAndreas Gohr
21*a8d2f3cbSAndreas Gohr        // search
22*a8d2f3cbSAndreas Gohr        $options->registerCommand('search', 'Search for an extension');
23*a8d2f3cbSAndreas Gohr        $options->registerOption('max', 'Maximum number of results (default 10)', 'm', 'number', 'search');
24*a8d2f3cbSAndreas Gohr        $options->registerOption('verbose', 'Show detailed extension information', 'v', false, 'search');
25*a8d2f3cbSAndreas Gohr        $options->registerArgument('query', 'The keyword(s) to search for', true, 'search');
26*a8d2f3cbSAndreas Gohr
27*a8d2f3cbSAndreas Gohr        // list
28*a8d2f3cbSAndreas Gohr        $options->registerCommand('list', 'List installed extensions');
29*a8d2f3cbSAndreas Gohr        $options->registerOption('verbose', 'Show detailed extension information', 'v', false, 'list');
30*a8d2f3cbSAndreas Gohr
31*a8d2f3cbSAndreas Gohr        // upgrade
32*a8d2f3cbSAndreas Gohr        $options->registerCommand('upgrade', 'Update all installed extensions to their latest versions');
33*a8d2f3cbSAndreas Gohr
34*a8d2f3cbSAndreas Gohr        // install
35*a8d2f3cbSAndreas Gohr        $options->registerCommand('install', 'Install or upgrade extensions');
36*a8d2f3cbSAndreas Gohr        $options->registerArgument('extensions...', 'One or more extensions to install', true, 'install');
37*a8d2f3cbSAndreas Gohr
38*a8d2f3cbSAndreas Gohr        // uninstall
39*a8d2f3cbSAndreas Gohr        $options->registerCommand('uninstall', 'Uninstall a new extension');
40*a8d2f3cbSAndreas Gohr        $options->registerArgument('extensions...', 'One or more extensions to install', true, 'uninstall');
41*a8d2f3cbSAndreas Gohr
42*a8d2f3cbSAndreas Gohr        // enable
43*a8d2f3cbSAndreas Gohr        $options->registerCommand('enable', 'Enable installed extensions');
44*a8d2f3cbSAndreas Gohr        $options->registerArgument('extensions...', 'One or more extensions to enable', true, 'enable');
45*a8d2f3cbSAndreas Gohr
46*a8d2f3cbSAndreas Gohr        // disable
47*a8d2f3cbSAndreas Gohr        $options->registerCommand('disable', 'Disable installed extensions');
48*a8d2f3cbSAndreas Gohr        $options->registerArgument('extensions...', 'One or more extensions to disable', true, 'disable');
49*a8d2f3cbSAndreas Gohr
50*a8d2f3cbSAndreas Gohr
51*a8d2f3cbSAndreas Gohr    }
52*a8d2f3cbSAndreas Gohr
53*a8d2f3cbSAndreas Gohr    /** @inheritdoc */
54*a8d2f3cbSAndreas Gohr    protected function main(\splitbrain\phpcli\Options $options)
55*a8d2f3cbSAndreas Gohr    {
56*a8d2f3cbSAndreas Gohr        switch ($options->getCmd()) {
57*a8d2f3cbSAndreas Gohr            case 'list':
58*a8d2f3cbSAndreas Gohr                $ret = $this->cmdList($options->getOpt('verbose'));
59*a8d2f3cbSAndreas Gohr                break;
60*a8d2f3cbSAndreas Gohr            case 'search':
61*a8d2f3cbSAndreas Gohr                $ret = $this->cmdSearch(
62*a8d2f3cbSAndreas Gohr                    implode(' ', $options->getArgs()),
63*a8d2f3cbSAndreas Gohr                    $options->getOpt('verbose'),
64*a8d2f3cbSAndreas Gohr                    (int)$options->getOpt('max', 10)
65*a8d2f3cbSAndreas Gohr                );
66*a8d2f3cbSAndreas Gohr                break;
67*a8d2f3cbSAndreas Gohr            case 'install':
68*a8d2f3cbSAndreas Gohr                $ret = $this->cmdInstall($options->getArgs());
69*a8d2f3cbSAndreas Gohr                break;
70*a8d2f3cbSAndreas Gohr            case 'uninstall':
71*a8d2f3cbSAndreas Gohr                $ret = $this->cmdUnInstall($options->getArgs());
72*a8d2f3cbSAndreas Gohr                break;
73*a8d2f3cbSAndreas Gohr            case 'enable':
74*a8d2f3cbSAndreas Gohr                $ret = $this->cmdEnable(true, $options->getArgs());
75*a8d2f3cbSAndreas Gohr                break;
76*a8d2f3cbSAndreas Gohr            case 'disable':
77*a8d2f3cbSAndreas Gohr                $ret = $this->cmdEnable(false, $options->getArgs());
78*a8d2f3cbSAndreas Gohr                break;
79*a8d2f3cbSAndreas Gohr            case 'upgrade':
80*a8d2f3cbSAndreas Gohr                $ret = $this->cmdUpgrade();
81*a8d2f3cbSAndreas Gohr                break;
82*a8d2f3cbSAndreas Gohr            default:
83*a8d2f3cbSAndreas Gohr                echo $options->help();
84*a8d2f3cbSAndreas Gohr                $ret = 0;
85*a8d2f3cbSAndreas Gohr        }
86*a8d2f3cbSAndreas Gohr
87*a8d2f3cbSAndreas Gohr        exit($ret);
88*a8d2f3cbSAndreas Gohr    }
89*a8d2f3cbSAndreas Gohr
90*a8d2f3cbSAndreas Gohr    /**
91*a8d2f3cbSAndreas Gohr     * Upgrade all extensions
92*a8d2f3cbSAndreas Gohr     *
93*a8d2f3cbSAndreas Gohr     * @return int
94*a8d2f3cbSAndreas Gohr     */
95*a8d2f3cbSAndreas Gohr    protected function cmdUpgrade()
96*a8d2f3cbSAndreas Gohr    {
97*a8d2f3cbSAndreas Gohr        /* @var helper_plugin_extension_extension $ext */
98*a8d2f3cbSAndreas Gohr        $ext = $this->loadHelper('extension_extension');
99*a8d2f3cbSAndreas Gohr        $list = $this->getInstalledExtensions();
100*a8d2f3cbSAndreas Gohr
101*a8d2f3cbSAndreas Gohr        $ok = 0;
102*a8d2f3cbSAndreas Gohr        foreach ($list as $extname) {
103*a8d2f3cbSAndreas Gohr            $ext->setExtension($extname);
104*a8d2f3cbSAndreas Gohr            $date = $ext->getInstalledVersion();
105*a8d2f3cbSAndreas Gohr            $avail = $ext->getLastUpdate();
106*a8d2f3cbSAndreas Gohr            if ($avail && $avail > $date) {
107*a8d2f3cbSAndreas Gohr                $ok += $this->cmdInstall([$extname]);
108*a8d2f3cbSAndreas Gohr            }
109*a8d2f3cbSAndreas Gohr        }
110*a8d2f3cbSAndreas Gohr
111*a8d2f3cbSAndreas Gohr        return $ok;
112*a8d2f3cbSAndreas Gohr    }
113*a8d2f3cbSAndreas Gohr
114*a8d2f3cbSAndreas Gohr    /**
115*a8d2f3cbSAndreas Gohr     * Enable or disable one or more extensions
116*a8d2f3cbSAndreas Gohr     *
117*a8d2f3cbSAndreas Gohr     * @param bool $set
118*a8d2f3cbSAndreas Gohr     * @param string[] $extensions
119*a8d2f3cbSAndreas Gohr     * @return int
120*a8d2f3cbSAndreas Gohr     */
121*a8d2f3cbSAndreas Gohr    protected function cmdEnable($set, $extensions)
122*a8d2f3cbSAndreas Gohr    {
123*a8d2f3cbSAndreas Gohr        /* @var helper_plugin_extension_extension $ext */
124*a8d2f3cbSAndreas Gohr        $ext = $this->loadHelper('extension_extension');
125*a8d2f3cbSAndreas Gohr
126*a8d2f3cbSAndreas Gohr        $ok = 0;
127*a8d2f3cbSAndreas Gohr        foreach ($extensions as $extname) {
128*a8d2f3cbSAndreas Gohr            $ext->setExtension($extname);
129*a8d2f3cbSAndreas Gohr            if (!$ext->isInstalled()) {
130*a8d2f3cbSAndreas Gohr                $this->error(sprintf('Extension %s is not installed', $ext->getID()));
131*a8d2f3cbSAndreas Gohr                $ok += 1;
132*a8d2f3cbSAndreas Gohr                continue;
133*a8d2f3cbSAndreas Gohr            }
134*a8d2f3cbSAndreas Gohr
135*a8d2f3cbSAndreas Gohr            if ($set) {
136*a8d2f3cbSAndreas Gohr                $status = $ext->enable();
137*a8d2f3cbSAndreas Gohr                $msg = 'msg_enabled';
138*a8d2f3cbSAndreas Gohr            } else {
139*a8d2f3cbSAndreas Gohr                $status = $ext->disable();
140*a8d2f3cbSAndreas Gohr                $msg = 'msg_disabled';
141*a8d2f3cbSAndreas Gohr            }
142*a8d2f3cbSAndreas Gohr
143*a8d2f3cbSAndreas Gohr            if ($status !== true) {
144*a8d2f3cbSAndreas Gohr                $this->error($status);
145*a8d2f3cbSAndreas Gohr                $ok += 1;
146*a8d2f3cbSAndreas Gohr                continue;
147*a8d2f3cbSAndreas Gohr            } else {
148*a8d2f3cbSAndreas Gohr                $this->success(sprintf($this->getLang($msg), $ext->getID()));
149*a8d2f3cbSAndreas Gohr            }
150*a8d2f3cbSAndreas Gohr        }
151*a8d2f3cbSAndreas Gohr
152*a8d2f3cbSAndreas Gohr        return $ok;
153*a8d2f3cbSAndreas Gohr    }
154*a8d2f3cbSAndreas Gohr
155*a8d2f3cbSAndreas Gohr    /**
156*a8d2f3cbSAndreas Gohr     * Uninstall one or more extensions
157*a8d2f3cbSAndreas Gohr     *
158*a8d2f3cbSAndreas Gohr     * @param string[] $extensions
159*a8d2f3cbSAndreas Gohr     * @return int
160*a8d2f3cbSAndreas Gohr     */
161*a8d2f3cbSAndreas Gohr    protected function cmdUnInstall($extensions)
162*a8d2f3cbSAndreas Gohr    {
163*a8d2f3cbSAndreas Gohr        /* @var helper_plugin_extension_extension $ext */
164*a8d2f3cbSAndreas Gohr        $ext = $this->loadHelper('extension_extension');
165*a8d2f3cbSAndreas Gohr
166*a8d2f3cbSAndreas Gohr        $ok = 0;
167*a8d2f3cbSAndreas Gohr        foreach ($extensions as $extname) {
168*a8d2f3cbSAndreas Gohr            $ext->setExtension($extname);
169*a8d2f3cbSAndreas Gohr            if (!$ext->isInstalled()) {
170*a8d2f3cbSAndreas Gohr                $this->error(sprintf('Extension %s is not installed', $ext->getID()));
171*a8d2f3cbSAndreas Gohr                $ok += 1;
172*a8d2f3cbSAndreas Gohr                continue;
173*a8d2f3cbSAndreas Gohr            }
174*a8d2f3cbSAndreas Gohr
175*a8d2f3cbSAndreas Gohr            $status = $ext->uninstall();
176*a8d2f3cbSAndreas Gohr            if ($status) {
177*a8d2f3cbSAndreas Gohr                $this->success(sprintf($this->getLang('msg_delete_success'), $ext->getID()));
178*a8d2f3cbSAndreas Gohr            } else {
179*a8d2f3cbSAndreas Gohr                $this->error(sprintf($this->getLang('msg_delete_failed'), hsc($ext->getID())));
180*a8d2f3cbSAndreas Gohr                $ok = 1;
181*a8d2f3cbSAndreas Gohr            }
182*a8d2f3cbSAndreas Gohr        }
183*a8d2f3cbSAndreas Gohr
184*a8d2f3cbSAndreas Gohr        return $ok;
185*a8d2f3cbSAndreas Gohr    }
186*a8d2f3cbSAndreas Gohr
187*a8d2f3cbSAndreas Gohr    /**
188*a8d2f3cbSAndreas Gohr     * Install one or more extensions
189*a8d2f3cbSAndreas Gohr     *
190*a8d2f3cbSAndreas Gohr     * @param string[] $extensions
191*a8d2f3cbSAndreas Gohr     * @return int
192*a8d2f3cbSAndreas Gohr     */
193*a8d2f3cbSAndreas Gohr    protected function cmdInstall($extensions)
194*a8d2f3cbSAndreas Gohr    {
195*a8d2f3cbSAndreas Gohr        /* @var helper_plugin_extension_extension $ext */
196*a8d2f3cbSAndreas Gohr        $ext = $this->loadHelper('extension_extension');
197*a8d2f3cbSAndreas Gohr
198*a8d2f3cbSAndreas Gohr        $ok = 0;
199*a8d2f3cbSAndreas Gohr        foreach ($extensions as $extname) {
200*a8d2f3cbSAndreas Gohr            $ext->setExtension($extname);
201*a8d2f3cbSAndreas Gohr
202*a8d2f3cbSAndreas Gohr            if (!$ext->getDownloadURL()) {
203*a8d2f3cbSAndreas Gohr                $ok += 1;
204*a8d2f3cbSAndreas Gohr                $this->error(
205*a8d2f3cbSAndreas Gohr                    sprintf('Could not find download for %s', $ext->getID())
206*a8d2f3cbSAndreas Gohr                );
207*a8d2f3cbSAndreas Gohr                continue;
208*a8d2f3cbSAndreas Gohr            }
209*a8d2f3cbSAndreas Gohr
210*a8d2f3cbSAndreas Gohr            try {
211*a8d2f3cbSAndreas Gohr                $installed = $ext->installOrUpdate();
212*a8d2f3cbSAndreas Gohr                foreach ($installed as $ext => $info) {
213*a8d2f3cbSAndreas Gohr                    $this->success(sprintf(
214*a8d2f3cbSAndreas Gohr                            $this->getLang('msg_' . $info['type'] . '_' . $info['action'] . '_success'),
215*a8d2f3cbSAndreas Gohr                            $info['base'])
216*a8d2f3cbSAndreas Gohr                    );
217*a8d2f3cbSAndreas Gohr                }
218*a8d2f3cbSAndreas Gohr            } catch (Exception $e) {
219*a8d2f3cbSAndreas Gohr                $this->error($e->getMessage());
220*a8d2f3cbSAndreas Gohr                $ok += 1;
221*a8d2f3cbSAndreas Gohr            }
222*a8d2f3cbSAndreas Gohr        }
223*a8d2f3cbSAndreas Gohr        return $ok;
224*a8d2f3cbSAndreas Gohr    }
225*a8d2f3cbSAndreas Gohr
226*a8d2f3cbSAndreas Gohr    /**
227*a8d2f3cbSAndreas Gohr     * Search for an extension
228*a8d2f3cbSAndreas Gohr     *
229*a8d2f3cbSAndreas Gohr     * @param string $query
230*a8d2f3cbSAndreas Gohr     * @param bool $showdetails
231*a8d2f3cbSAndreas Gohr     * @param int $max
232*a8d2f3cbSAndreas Gohr     * @return int
233*a8d2f3cbSAndreas Gohr     * @throws \splitbrain\phpcli\Exception
234*a8d2f3cbSAndreas Gohr     */
235*a8d2f3cbSAndreas Gohr    protected function cmdSearch($query, $showdetails, $max)
236*a8d2f3cbSAndreas Gohr    {
237*a8d2f3cbSAndreas Gohr        /** @var helper_plugin_extension_repository $repository */
238*a8d2f3cbSAndreas Gohr        $repository = $this->loadHelper('extension_repository');
239*a8d2f3cbSAndreas Gohr        $result = $repository->search($query);
240*a8d2f3cbSAndreas Gohr        if ($max) {
241*a8d2f3cbSAndreas Gohr            $result = array_slice($result, 0, $max);
242*a8d2f3cbSAndreas Gohr        }
243*a8d2f3cbSAndreas Gohr
244*a8d2f3cbSAndreas Gohr        $this->listExtensions($result, $showdetails);
245*a8d2f3cbSAndreas Gohr        return 0;
246*a8d2f3cbSAndreas Gohr    }
247*a8d2f3cbSAndreas Gohr
248*a8d2f3cbSAndreas Gohr    /**
249*a8d2f3cbSAndreas Gohr     * @param bool $showdetails
250*a8d2f3cbSAndreas Gohr     * @return int
251*a8d2f3cbSAndreas Gohr     * @throws \splitbrain\phpcli\Exception
252*a8d2f3cbSAndreas Gohr     */
253*a8d2f3cbSAndreas Gohr    protected function cmdList($showdetails)
254*a8d2f3cbSAndreas Gohr    {
255*a8d2f3cbSAndreas Gohr        $list = $this->getInstalledExtensions();
256*a8d2f3cbSAndreas Gohr        $this->listExtensions($list, $showdetails);
257*a8d2f3cbSAndreas Gohr
258*a8d2f3cbSAndreas Gohr        return 0;
259*a8d2f3cbSAndreas Gohr    }
260*a8d2f3cbSAndreas Gohr
261*a8d2f3cbSAndreas Gohr    /**
262*a8d2f3cbSAndreas Gohr     * Get all installed extensions
263*a8d2f3cbSAndreas Gohr     *
264*a8d2f3cbSAndreas Gohr     * @return array
265*a8d2f3cbSAndreas Gohr     */
266*a8d2f3cbSAndreas Gohr    protected function getInstalledExtensions()
267*a8d2f3cbSAndreas Gohr    {
268*a8d2f3cbSAndreas Gohr        /** @var Doku_Plugin_Controller $plugin_controller */
269*a8d2f3cbSAndreas Gohr        global $plugin_controller;
270*a8d2f3cbSAndreas Gohr        $pluginlist = $plugin_controller->getList('', true);
271*a8d2f3cbSAndreas Gohr        $tpllist = glob(DOKU_INC . 'lib/tpl/*', GLOB_ONLYDIR);
272*a8d2f3cbSAndreas Gohr        $tpllist = array_map(function ($path) {
273*a8d2f3cbSAndreas Gohr            return 'template:' . basename($path);
274*a8d2f3cbSAndreas Gohr        }, $tpllist);
275*a8d2f3cbSAndreas Gohr        $list = array_merge($pluginlist, $tpllist);
276*a8d2f3cbSAndreas Gohr        sort($list);
277*a8d2f3cbSAndreas Gohr        return $list;
278*a8d2f3cbSAndreas Gohr    }
279*a8d2f3cbSAndreas Gohr
280*a8d2f3cbSAndreas Gohr    /**
281*a8d2f3cbSAndreas Gohr     * List the given extensions
282*a8d2f3cbSAndreas Gohr     *
283*a8d2f3cbSAndreas Gohr     * @param string[] $list
284*a8d2f3cbSAndreas Gohr     * @param bool $details display details
285*a8d2f3cbSAndreas Gohr     * @throws \splitbrain\phpcli\Exception
286*a8d2f3cbSAndreas Gohr     */
287*a8d2f3cbSAndreas Gohr    protected function listExtensions($list, $details)
288*a8d2f3cbSAndreas Gohr    {
289*a8d2f3cbSAndreas Gohr        /** @var helper_plugin_extension_extension $ext */
290*a8d2f3cbSAndreas Gohr        $ext = $this->loadHelper('extension_extension');
291*a8d2f3cbSAndreas Gohr        $tr = new \splitbrain\phpcli\TableFormatter($this->colors);
292*a8d2f3cbSAndreas Gohr
293*a8d2f3cbSAndreas Gohr
294*a8d2f3cbSAndreas Gohr        foreach ($list as $name) {
295*a8d2f3cbSAndreas Gohr            $ext->setExtension($name);
296*a8d2f3cbSAndreas Gohr
297*a8d2f3cbSAndreas Gohr            $status = '';
298*a8d2f3cbSAndreas Gohr            if ($ext->isInstalled()) {
299*a8d2f3cbSAndreas Gohr                $date = $ext->getInstalledVersion();
300*a8d2f3cbSAndreas Gohr                $avail = $ext->getLastUpdate();
301*a8d2f3cbSAndreas Gohr                $status = 'i';
302*a8d2f3cbSAndreas Gohr                if ($avail && $avail > $date) {
303*a8d2f3cbSAndreas Gohr                    $color = Colors::C_RED;
304*a8d2f3cbSAndreas Gohr                } else {
305*a8d2f3cbSAndreas Gohr                    $color = Colors::C_GREEN;
306*a8d2f3cbSAndreas Gohr                }
307*a8d2f3cbSAndreas Gohr                if ($ext->isGitControlled()) $status = 'g';
308*a8d2f3cbSAndreas Gohr                if ($ext->isBundled()) $status = 'b';
309*a8d2f3cbSAndreas Gohr                if (!$ext->isEnabled()) $status .= 'd';
310*a8d2f3cbSAndreas Gohr            } else {
311*a8d2f3cbSAndreas Gohr                $date = $ext->getLastUpdate();
312*a8d2f3cbSAndreas Gohr                $color = null;
313*a8d2f3cbSAndreas Gohr            }
314*a8d2f3cbSAndreas Gohr
315*a8d2f3cbSAndreas Gohr
316*a8d2f3cbSAndreas Gohr            echo $tr->format(
317*a8d2f3cbSAndreas Gohr                [20, 3, 12, '*'],
318*a8d2f3cbSAndreas Gohr                [
319*a8d2f3cbSAndreas Gohr                    $ext->getID(),
320*a8d2f3cbSAndreas Gohr                    $status,
321*a8d2f3cbSAndreas Gohr                    $date,
322*a8d2f3cbSAndreas Gohr                    strip_tags(sprintf(
323*a8d2f3cbSAndreas Gohr                            $this->getLang('extensionby'),
324*a8d2f3cbSAndreas Gohr                            $ext->getDisplayName(),
325*a8d2f3cbSAndreas Gohr                            $this->colors->wrap($ext->getAuthor(), Colors::C_PURPLE))
326*a8d2f3cbSAndreas Gohr                    )
327*a8d2f3cbSAndreas Gohr                ],
328*a8d2f3cbSAndreas Gohr                [
329*a8d2f3cbSAndreas Gohr                    Colors::C_BROWN,
330*a8d2f3cbSAndreas Gohr                    Colors::C_YELLOW,
331*a8d2f3cbSAndreas Gohr                    $color,
332*a8d2f3cbSAndreas Gohr                    null,
333*a8d2f3cbSAndreas Gohr                ]
334*a8d2f3cbSAndreas Gohr            );
335*a8d2f3cbSAndreas Gohr
336*a8d2f3cbSAndreas Gohr            if (!$details) continue;
337*a8d2f3cbSAndreas Gohr
338*a8d2f3cbSAndreas Gohr            echo $tr->format(
339*a8d2f3cbSAndreas Gohr                [5, '*'],
340*a8d2f3cbSAndreas Gohr                ['', $ext->getDescription()],
341*a8d2f3cbSAndreas Gohr                [null, Colors::C_CYAN]
342*a8d2f3cbSAndreas Gohr            );
343*a8d2f3cbSAndreas Gohr        }
344*a8d2f3cbSAndreas Gohr    }
345*a8d2f3cbSAndreas Gohr}
346