From 3cf3bd1cbb62982d3b891f74945c4027e9c19714 Mon Sep 17 00:00:00 2001
From: Tomas Kopecek <tkopecek@redhat.com>
Date: Jun 27 2022 15:51:53 +0000
Subject: beautify logged commands issued by koji


Related: https://pagure.io/koji/issue/929

---

diff --git a/builder/kojid b/builder/kojid
index cd2e1ed..e7ba188 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 01981f5..2e0e239 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 0979ec5..cf8d882 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()

