pw_env_setup: make all Python code 3.7-compatible

Make all Python code compatible with 3.7. Specifically, remove
dependency on shlex.join(), a one-line function added in Python 3.8, and
typing.Literal.

Change-Id: I38f57c9f0ee7b8ef1c3e3d9ead456b2e60a4b42d
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/13461
Reviewed-by: Michael Spang <spang@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Rob Mohr <mohrr@google.com>
diff --git a/pw_build/py/exec.py b/pw_build/py/exec.py
index e30840e..0b74458 100644
--- a/pw_build/py/exec.py
+++ b/pw_build/py/exec.py
@@ -145,7 +145,7 @@
         _LOG.error('')
         _LOG.error('Full command:')
         _LOG.error('')
-        _LOG.error('  %s', shlex.join(command))
+        _LOG.error('  %s', ' '.join(shlex.quote(arg) for arg in command))
         _LOG.error('')
         _LOG.error('Process output:')
         print(flush=True)
diff --git a/pw_build/py/python_runner.py b/pw_build/py/python_runner.py
index 62061b6..0b2a123 100755
--- a/pw_build/py/python_runner.py
+++ b/pw_build/py/python_runner.py
@@ -169,7 +169,7 @@
         return 1
 
     command = [sys.executable] + resolved_command
-    _LOG.debug('RUN %s', shlex.join(command))
+    _LOG.debug('RUN %s', ' '.join(shlex.quote(arg) for arg in command))
 
     if args.capture_output:
         completed_process = subprocess.run(
diff --git a/pw_cli/py/pw_cli/envparse.py b/pw_cli/py/pw_cli/envparse.py
index 2450992..91445af 100644
--- a/pw_cli/py/pw_cli/envparse.py
+++ b/pw_cli/py/pw_cli/envparse.py
@@ -15,7 +15,7 @@
 
 import argparse
 import os
-from typing import Callable, Dict, Generic, IO, List, Literal, Mapping
+from typing import Callable, Dict, Generic, IO, List, Mapping
 from typing import NamedTuple, Optional, TypeVar
 
 
@@ -172,7 +172,9 @@
             or value in _BOOLEAN_TRUE_EMOJI)
 
 
-OpenMode = Literal['r', 'rb', 'w', 'wb']
+# TODO(mohrr) Switch to Literal when no longer supporting Python 3.7.
+# OpenMode = Literal['r', 'rb', 'w', 'wb']
+OpenMode = str
 
 
 class FileType:
diff --git a/pw_cli/py/pw_cli/process.py b/pw_cli/py/pw_cli/process.py
index d86dcfc..825dc08 100644
--- a/pw_cli/py/pw_cli/process.py
+++ b/pw_cli/py/pw_cli/process.py
@@ -83,7 +83,8 @@
     Returns a CompletedProcess with details from the process.
     """
 
-    _LOG.debug('Running `%s`', shlex.join([program, *args]))
+    _LOG.debug('Running `%s`',
+               ' '.join(shlex.quote(arg) for arg in [program, *args]))
 
     env = os.environ.copy()
     env[PW_SUBPROCESS_ENV] = '1'
diff --git a/pw_presubmit/py/pw_presubmit/tools.py b/pw_presubmit/py/pw_presubmit/tools.py
index c7f89e3..6cb804f 100644
--- a/pw_presubmit/py/pw_presubmit/tools.py
+++ b/pw_presubmit/py/pw_presubmit/tools.py
@@ -148,7 +148,7 @@
 
 def format_command(args: Sequence, kwargs: dict) -> Tuple[str, str]:
     attr = ', '.join(f'{k}={_truncate(v)}' for k, v in sorted(kwargs.items()))
-    return attr, shlex.join(str(arg) for arg in args)
+    return attr, ' '.join(shlex.quote(str(arg)) for arg in args)
 
 
 def log_run(args, **kwargs) -> subprocess.CompletedProcess:
diff --git a/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py b/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
index 9499520..6dc72d3 100644
--- a/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
+++ b/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
@@ -52,12 +52,13 @@
         *proto_paths,
     )
 
-    _LOG.debug('%s', shlex.join(str(c) for c in cmd))
+    _LOG.debug('%s', ' '.join(shlex.quote(str(c)) for c in cmd))
     process = subprocess.run(cmd, capture_output=True)
 
     if process.returncode:
         _LOG.error('protoc invocation failed!\n%s\n%s',
-                   shlex.join(str(c) for c in cmd), process.stderr.decode())
+                   ' '.join(shlex.quote(str(c)) for c in cmd),
+                   process.stderr.decode())
         process.check_returncode()
 
 
diff --git a/pw_watch/py/pw_watch/watch.py b/pw_watch/py/pw_watch/watch.py
index 3c285bc..287661f 100755
--- a/pw_watch/py/pw_watch/watch.py
+++ b/pw_watch/py/pw_watch/watch.py
@@ -94,7 +94,7 @@
         return (str(self.build_dir), *self.targets)
 
     def __str__(self) -> str:
-        return shlex.join(self.args())
+        return ' '.join(shlex.quote(arg) for arg in self.args())
 
 
 class PigweedBuildWatcher(FileSystemEventHandler, DebouncedFunction):