bpo-22454: Add shlex.join() (the opposite of shlex.split()) (GH-7605)

diff --git a/Lib/shlex.py b/Lib/shlex.py
index 2c9786c..fb1130d 100644
--- a/Lib/shlex.py
+++ b/Lib/shlex.py
@@ -14,7 +14,7 @@
 
 from io import StringIO
 
-__all__ = ["shlex", "split", "quote"]
+__all__ = ["shlex", "split", "quote", "join"]
 
 class shlex:
     "A lexical analyzer class for simple shell-like syntaxes."
@@ -305,6 +305,11 @@
     return list(lex)
 
 
+def join(split_command):
+    """Return a shell-escaped string from *split_command*."""
+    return ' '.join(quote(arg) for arg in split_command)
+
+
 _find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.ASCII).search
 
 def quote(s):
diff --git a/Lib/test/test_shlex.py b/Lib/test/test_shlex.py
index fd35788..a432610 100644
--- a/Lib/test/test_shlex.py
+++ b/Lib/test/test_shlex.py
@@ -308,6 +308,26 @@
             self.assertEqual(shlex.quote("test%s'name'" % u),
                              "'test%s'\"'\"'name'\"'\"''" % u)
 
+    def testJoin(self):
+        for split_command, command in [
+            (['a ', 'b'], "'a ' b"),
+            (['a', ' b'], "a ' b'"),
+            (['a', ' ', 'b'], "a ' ' b"),
+            (['"a', 'b"'], '\'"a\' \'b"\''),
+        ]:
+            with self.subTest(command=command):
+                joined = shlex.join(split_command)
+                self.assertEqual(joined, command)
+
+    def testJoinRoundtrip(self):
+        all_data = self.data + self.posix_data
+        for command, *split_command in all_data:
+            with self.subTest(command=command):
+                joined = shlex.join(split_command)
+                resplit = shlex.split(joined)
+                self.assertEqual(split_command, resplit)
+
+
 # Allow this test to be used with old shlex.py
 if not getattr(shlex, "split", None):
     for methname in dir(ShlexTest):