Index: tests/pybin/tools.py
===================================================================
--- tests/pybin/tools.py	(revision 2b10f958db325dea85128cd85f1c1c9c8a596962)
+++ tests/pybin/tools.py	(revision ce3d3059a1a1ced28d1733bd16988b1a45444407)
@@ -1,4 +1,5 @@
 import __main__
 import argparse
+import contextlib
 import fileinput
 import multiprocessing
@@ -21,26 +22,33 @@
 
 # helper functions to run terminal commands
-def sh(cmd, print2stdout = True, timeout = False, output = None, input = None):
-	# add input redirection if needed
-	if input and os.path.isfile(input):
-		cmd += " < %s" % input
-
-	# add output redirection if needed
-	if output:
-		cmd += " > %s" % output
+def sh(*cmd, timeout = False, output = None, input = None, error = subprocess.STDOUT):
+	cmd = list(cmd)
 
 	# if this is a dry_run, only print the commands that would be ran
 	if settings.dry_run :
-		print("cmd: %s" % cmd)
+		print("cmd: %s" % ' '.join(cmd))
 		return 0, None
 
-	# otherwise create a pipe and run the desired command
-	else :
+	with contextlib.ExitStack() as onexit:
+		# add input redirection if needed
+		if input and input != subprocess.DEVNULL:
+			if os.path.isfile(input):
+				input = open(input)
+				onexit.push(input)
+			else:
+				input = None
+
+		# add output redirection if needed
+		if output and output != subprocess.DEVNULL and output != subprocess.PIPE:
+			output = open(output, 'w')
+			onexit.push(output)
+
+		# run the desired command
 		try:
 			proc = subprocess.run(
 				cmd,
-				stdout=None if print2stdout else PIPE,
+				stdin =input,
+				stdout=output,
 				stderr=STDOUT,
-				shell=True,
 				timeout=settings.timeout.single if timeout else None
 			)
@@ -57,5 +65,5 @@
 		return False
 
-	code, out = sh("file %s" % fname, print2stdout = False)
+	code, out = sh("file %s" % fname, output=subprocess.PIPE)
 	if code != 0:
 		return False
@@ -75,5 +83,5 @@
 	if isinstance(files, str ): files = [ files ]
 	for file in files:
-		sh("rm -f %s > /dev/null 2>&1" % file )
+		sh( 'rm', '-f', file, output=subprocess.DEVNULL, error=subprocess.DEVNULL )
 
 # Create 1 or more directory
@@ -83,5 +91,5 @@
 		p = os.path.normpath( file )
 		d = os.path.dirname ( p )
-		sh( "mkdir -p {}".format(d) )
+		sh( 'mkdir', '-p', d, output=subprocess.DEVNULL, error=subprocess.DEVNULL )
 
 
@@ -93,29 +101,26 @@
 # diff two files
 def diff( lhs, rhs ):
-	# diff the output of the files
-	diff_cmd = ("diff --text "
-			"--old-group-format='\t\tmissing lines :\n"
-			"%%<' \\\n"
-			"--new-group-format='\t\tnew lines :\n"
-			"%%>' \\\n"
-			"--unchanged-group-format='%%=' \\"
-			"--changed-group-format='\t\texpected :\n"
-			"%%<"
-			"\t\tgot :\n"
-			"%%>\n' \\\n"
-			"--new-line-format='\t\t%%dn\t%%L' \\\n"
-			"--old-line-format='\t\t%%dn\t%%L' \\\n"
-			"--unchanged-line-format='' \\\n"
-			"%s %s")
-
 	# fetch return code and error from the diff command
-	return sh(diff_cmd % (lhs, rhs), False)
+	return sh(
+		'''diff''',
+		'''--text''',
+		'''--old-group-format=\t\tmissing lines :\n%<''',
+		'''--new-line-format=\t\t%dn\t%L''',
+		'''--new-group-format=\t\tnew lines : \n%>''',
+		'''--old-line-format=\t\t%dn\t%L''',
+		'''--unchanged-group-format=%=''',
+		'''--changed-group-format=\t\texpected :\n%<\t\tgot :\n%>''',
+		'''--unchanged-line-format=''',
+		lhs,
+		rhs,
+		output=subprocess.PIPE
+	)
 
 # call make
-def make(target, flags = '', redirects = '', error_file = None, silent = False):
-	test_param = """test="%s" """ % (error_file) if error_file else ''
-	cmd = ' '.join([
-		settings.make,
-		'-s' if silent else '',
+def make(target, *, flags = '', output = None, error = None, error_file = None, silent = False):
+	test_param = """test="%s" """ % (error_file) if error_file else None
+	cmd = [
+		*settings.make,
+		'-s' if silent else None,
 		test_param,
 		settings.arch.flags,
@@ -123,8 +128,8 @@
 		settings.install.flags,
 		flags,
-		target,
-		redirects
-	])
-	return sh(cmd)
+		target
+	]
+	cmd = [s for s in cmd if s]
+	return sh(*cmd, output=output, error=error)
 
 def which(program):
@@ -146,10 +151,10 @@
 # move a file
 def mv(source, dest):
-	ret, _ = sh("mv %s %s" % (source, dest))
+	ret, _ = sh("mv", source, dest)
 	return ret
 
 # cat one file into the other
 def cat(source, dest):
-	ret, _ = sh("cat %s > %s" % (source, dest))
+	ret, _ = sh("cat", source, output=dest)
 	return ret
 
@@ -253,5 +258,5 @@
 		return 1, "ERR No core dump"
 
-	return sh("gdb -n %s %s -batch -x %s" % (path, core, cmd), print2stdout=False)
+	return sh('gdb', '-n', path, core, '-batch', '-x', cmd, output=subprocess.PIPE)
 
 class Timed:
