diff --git a/builder/kojid b/builder/kojid index cd2e1ed7..e7ba1888 100755 --- a/builder/kojid +++ b/builder/kojid @@ -80,7 +80,7 @@ from koji.tasks import ( ServerExit, ServerRestart ) -from koji.util import dslice, dslice_ex, isSuccess, parseStatus, to_list +from koji.util import dslice, dslice_ex, isSuccess, parseStatus, to_list, format_shell_cmd try: import requests_gssapi as reqgssapi @@ -445,7 +445,7 @@ class BuildRoot(object): else: cmd.append('--old-chroot') cmd.extend(args) - self.logger.info(' '.join(cmd)) + self.logger.info(format_shell_cmd(cmd)) workdir = getattr(self, 'workdir', None) mocklog = 'mock_output.log' pid = os.fork() @@ -5638,7 +5638,7 @@ class CreaterepoTask(BaseTaskHandler): status = log_output(self.session, cmd[0], cmd, logfile, self.getUploadDir(), logerror=True) if not isSuccess(status): raise koji.GenericError('failed to create repo: %s' - % parseStatus(status, ' '.join(cmd))) + % parseStatus(status, format_shell_cmd(cmd))) def _get_mergerepo_c_version(self): cmd = ['/usr/bin/mergerepo_c', '--version'] @@ -5737,7 +5737,7 @@ class CreaterepoTask(BaseTaskHandler): logerror=True, env=env) if not isSuccess(status): raise koji.GenericError('failed to merge repos: %s' - % parseStatus(status, ' '.join(cmd))) + % parseStatus(status, format_shell_cmd(cmd))) class NewDistRepoTask(BaseTaskHandler): @@ -5988,7 +5988,7 @@ class createDistRepoTask(BaseTaskHandler): status = log_output(self.session, cmd[0], cmd, logfile, self.getUploadDir(), logerror=True) if not isSuccess(status): raise koji.GenericError('failed to create repo: %s' - % parseStatus(status, ' '.join(cmd))) + % parseStatus(status, format_shell_cmd(cmd))) def do_multilib(self, arch, ml_arch, conf): repodir = koji.pathinfo.distrepo(self.rinfo['id'], self.rinfo['tag_name']) diff --git a/koji/util.py b/koji/util.py index 01981f53..2e0e2393 100644 --- a/koji/util.py +++ b/koji/util.py @@ -940,3 +940,28 @@ def to_list(lst): return lst else: return list(lst) + + +def format_shell_cmd(cmd, text_width=80): + """ + Helper for wrapping shell command lists to human-readable form, while + they still can be copied (from logs) and run in shell. + + :param [str] cmd: command list + :returns str: + """ + # account for " \" + text_width -= 2 + s = [] + line = '' + for bit in cmd: + if len(line + bit) > text_width: + if line: + s.append(line) + line = '' + if line: + line += ' ' + line += bit + if line: + s.append(line) + return ' \\\n'.join(s) diff --git a/tests/test_lib/test_utils.py b/tests/test_lib/test_utils.py index 0979ec53..cf8d8827 100644 --- a/tests/test_lib/test_utils.py +++ b/tests/test_lib/test_utils.py @@ -3,6 +3,7 @@ from __future__ import absolute_import import calendar import errno import locale +from unittest.case import TestCase import mock import optparse import os @@ -20,6 +21,8 @@ from datetime import datetime import koji import koji.util +from koji.util import format_shell_cmd + class EnumTestCase(unittest.TestCase): @@ -1600,5 +1603,28 @@ class TestMoveAndSymlink(unittest.TestCase): ensuredir.assert_called_once_with('b') +class TestFormatShellCmd(unittest.TestCase): + def test_formats(self): + cases = ( + ([], ''), + (['random cmd'], 'random cmd'), + (['aa', 'bb'], 'aa bb'), + (['long', 'command', 'with', 'many', 'simple', 'options', + 'like', '--option', 'x', '--another-option=x', 'and', + 'many', 'more', 'others'], + 'long command with many simple options \\\n' + 'like --option x --another-option=x and \\\n' + 'many more others'), + (['one long line which exceeds the text_width by some amount'], + 'one long line which exceeds the text_width by some amount'), + (['one long line which exceeds the text_width by some amount', + 'second long line which exceeds the text_width by some amount'], + 'one long line which exceeds the text_width by some amount \\\n' + 'second long line which exceeds the text_width by some amount'), + ) + for inp, out in cases: + self.assertEqual(koji.util.format_shell_cmd(inp, text_width=40), out) + + if __name__ == '__main__': unittest.main()