Ver Fonte

Fixed basic cases of commandline argument escaping of Sys.command and haxe.io.Process. (#3603)

Andy Li há 9 anos atrás
pai
commit
767b3fb402

+ 1 - 1
libs

@@ -1 +1 @@
-Subproject commit b58b56733386c86ec19c85242287093f6b2c23c3
+Subproject commit 803a97c581d8cce4649d62204498b84a758ca2c6

+ 100 - 0
std/StringTools.hx

@@ -414,6 +414,106 @@ class StringTools {
 		#end
 	}
 
+	/**
+		Returns a String that can be used as a single command line argument
+		on Unix.
+		The input will be quoted, or escaped if necessary.
+	*/
+	public static function quoteUnixArg(argument:String):String {
+		// Based on cpython's shlex.quote().
+		// https://hg.python.org/cpython/file/a3f076d4f54f/Lib/shlex.py#l278
+
+		if (argument == "")
+			return "''";
+
+		if (!~/[^a-zA-Z0-9_@%+=:,.\/-]/.match(argument))
+			return argument;
+
+		// use single quotes, and put single quotes into double quotes
+		// the string $'b is then quoted as '$'"'"'b'
+		return "'" + replace(argument, "'", "'\"'\"'") + "'";
+	}
+
+	/**
+		Character codes of the characters that will be escaped by `quoteWinArg(_, true)`.
+	*/
+	public static var winMetaCharacters = [" ".code, "(".code, ")".code, "%".code, "!".code, "^".code, "\"".code, "<".code, ">".code, "&".code, "|".code, "\n".code, "\r".code];
+
+	/**
+		Returns a String that can be used as a single command line argument
+		on Windows.
+		The input will be quoted, or escaped if necessary, such that the output
+		will be parsed as a single argument using the rule specified in
+		http://msdn.microsoft.com/en-us/library/ms880421
+
+		Examples:
+		```
+		quoteWinArg("abc") == "abc";
+		quoteWinArg("ab c") == '"ab c"';
+		```
+	*/
+	public static function quoteWinArg(argument:String, escapeMetaCharacters:Bool):String {
+		// If there is no space, tab, back-slash, or double-quotes, and it is not an empty string.
+		if (!~/^[^ \t\\"]+$/.match(argument)) {
+			
+			// Based on cpython's subprocess.list2cmdline().
+			// https://hg.python.org/cpython/file/50741316dd3a/Lib/subprocess.py#l620
+
+			var result = new StringBuf();
+			var needquote = argument.indexOf(" ") != -1 || argument.indexOf("\t") != -1 || argument == "";
+
+			if (needquote)
+				result.add('"');
+
+			var bs_buf = new StringBuf();
+			for (i in 0...argument.length) {
+				switch (argument.charCodeAt(i)) {
+					case "\\".code:
+						// Don't know if we need to double yet.
+						bs_buf.add("\\");
+					case '"'.code:
+						// Double backslashes.
+						var bs = bs_buf.toString();
+						result.add(bs);
+						result.add(bs);
+						bs_buf = new StringBuf();
+						result.add('\\"');
+					case c:
+						// Normal char
+						if (bs_buf.length > 0) {
+							result.add(bs_buf.toString());
+							bs_buf = new StringBuf();
+						}
+						result.addChar(c);
+				}
+			}
+
+			// Add remaining backslashes, if any.
+			result.add(bs_buf.toString());
+
+			if (needquote) {
+				result.add(bs_buf.toString());
+				result.add('"');
+			}
+
+			argument = result.toString();
+		}
+
+		if (escapeMetaCharacters) {
+			var result = new StringBuf();
+			for (i in 0...argument.length) {
+				var c = argument.charCodeAt(i);
+				if (winMetaCharacters.indexOf(c) >= 0) {
+					result.addChar("^".code);
+				}
+				result.addChar(c);
+			}
+			return result.toString();
+		} else {
+			return argument;
+		}
+	}
+
 	#if java
 	private static inline function _charAt(str:String, idx:Int):java.StdTypes.Char16 return untyped str._charAt(idx);
 	#end

+ 14 - 20
std/cpp/_std/Sys.hx

@@ -81,28 +81,22 @@
 		return sys_string();
 	}
 
-	static function escapeArgument( arg : String ) : String {
-		var ok = true;
-		for( i in 0...arg.length )
-			switch( arg.charCodeAt(i) ) {
-			case ' '.code, '\t'.code, '"'.code, '&'.code, '|'.code, '<'.code, '>'.code, '#'.code , ';'.code, '*'.code, '?'.code, '('.code, ')'.code, '{'.code, '}'.code, '$'.code:
-				ok = false;
-			case 0, 13, 10: // [eof] [cr] [lf]
-				arg = arg.substr(0,i);
-			}
-		if( ok )
-			return arg;
-		return '"'+arg.split('\\').join("\\\\").split('"').join('\\"')+'"';
-	}
-
 	public static function command( cmd : String, ?args : Array<String> ) : Int {
-		if( args != null ) {
-			cmd = escapeArgument(cmd);
-			for( a in args )
-				cmd += " "+escapeArgument(a);
+		if (args == null) {
+			return sys_command(cmd);
+		} else {
+			switch (systemName()) {
+				case "Windows":
+					cmd = [
+						for (a in [StringTools.replace(cmd, "/", "\\")].concat(args))
+						StringTools.quoteWinArg(a, true)
+					].join(" ");
+					return sys_command(cmd);
+				case _:
+					cmd = [cmd].concat(args).map(StringTools.quoteUnixArg).join(" ");
+					return sys_command(cmd);
+			}
 		}
-		if (systemName() == "Windows") cmd = '"$cmd"';
-		return sys_command(cmd);
 	}
 
 	public static function exit( code : Int ) : Void {

+ 4 - 8
std/cs/_std/sys/io/Process.hx

@@ -41,14 +41,10 @@ class Process {
 		this.native = new NativeProcess();
 		native.StartInfo.FileName = cmd;
 		native.StartInfo.CreateNoWindow = true;
-		var buf = new StringBuf();
-		for (arg in args)
-		{
-			buf.add("\"");
-			buf.add(StringTools.replace(arg, "\"", "\\\""));
-			buf.add("\" ");
-		}
-		native.StartInfo.Arguments = buf.toString();
+		native.StartInfo.Arguments = [
+			for (a in args)
+			StringTools.quoteWinArg(a, false)
+		].join(" ");
 		native.StartInfo.RedirectStandardError = native.StartInfo.RedirectStandardInput = native.StartInfo.RedirectStandardOutput = true;
 		native.StartInfo.UseShellExecute = false;
 

+ 13 - 4
std/java/_std/sys/io/Process.hx

@@ -39,10 +39,19 @@ class Process {
 	public function new( cmd : String, args : Array<String> ) : Void
 	{
 		var pargs = new NativeArray(args.length + 1);
-		pargs[0] = cmd;
-		for (i in 0...args.length)
-		{
-			pargs[i + 1] = args[i];
+		switch (Sys.systemName()) {
+			case "Windows":
+				pargs[0] = StringTools.quoteWinArg(cmd, false);
+				for (i in 0...args.length)
+				{
+					pargs[i + 1] = StringTools.quoteWinArg(args[i], false);
+				}
+			case _:
+				pargs[0] = cmd;
+				for (i in 0...args.length)
+				{
+					pargs[i + 1] = args[i];
+				}
 		}
 
 		try

+ 19 - 22
std/neko/_std/Sys.hx

@@ -91,30 +91,22 @@
 		return new String(sys_string());
 	}
 
-	static function escapeArgument( arg : String, windows : Bool ) : String {
-		var ok = true;
-		for( i in 0...arg.length )
-			switch( arg.charCodeAt(i) ) {
-			case ' '.code, '\t'.code, '"'.code, '&'.code, '|'.code, '<'.code, '>'.code, '#'.code , ';'.code, '*'.code, '?'.code, '('.code, ')'.code, '{'.code, '}'.code, '$'.code:
-				ok = false;
-			case 0, 13, 10: // [eof] [cr] [lf]
-				arg = arg.substr(0,i);
-				break;
-			}
-		if( ok )
-			return arg;
-		return windows ? '"'+arg.split('"').join('""').split("%").join('"%"')+'"' : "'"+arg.split("'").join("'\\''")+"'";
-	}
-
 	public static function command( cmd : String, ?args : Array<String> ) : Int {
-		var win = systemName() == "Windows";
-		cmd = escapeArgument(cmd, win);
-		if( args != null ) {
-			for( a in args )
-				cmd += " "+escapeArgument(a, win);
+		if (args == null) {
+			return sys_command(untyped cmd.__s);
+		} else {
+			switch (systemName()) {
+				case "Windows":
+					cmd = [
+						for (a in [StringTools.replace(cmd, "/", "\\")].concat(args))
+						StringTools.quoteWinArg(a, true)
+					].join(" ");
+					return sys_command(untyped cmd.__s);
+				case _:
+					cmd = [cmd].concat(args).map(StringTools.quoteUnixArg).join(" ");
+					return sys_command(untyped cmd.__s);
+			}
 		}
-		if (win) cmd = '"$cmd"';
-		return sys_command(untyped cmd.__s);
 	}
 
 	public static function exit( code : Int ) : Void {
@@ -151,6 +143,11 @@
 	private static var set_cwd = neko.Lib.load("std","set_cwd",1);
 	private static var sys_string = neko.Lib.load("std","sys_string",0);
 	private static var sys_command = neko.Lib.load("std","sys_command",1);
+	private static var sys_command_safe = try {
+		neko.Lib.load("std","sys_command_safe",2);
+	} catch(e:Dynamic) {
+		null;
+	};
 	private static var sys_exit = neko.Lib.load("std","sys_exit",1);
 	private static var sys_time = neko.Lib.load("std","sys_time",0);
 	private static var sys_cpu_time = neko.Lib.load("std","sys_cpu_time",0);

+ 100 - 0
std/php/_std/StringTools.hx

@@ -95,4 +95,104 @@
 		return untyped __physeq__(c, 0);
 	}
 
+	/**
+		Returns a String that can be used as a single command line argument
+		on Unix.
+		The input will be quoted, or escaped if necessary.
+	*/
+	public static function quoteUnixArg(argument:String):String {
+		// Based on cpython's shlex.quote().
+		// https://hg.python.org/cpython/file/a3f076d4f54f/Lib/shlex.py#l278
+
+		if (argument == "")
+			return "''";
+
+		if (!~/[^a-zA-Z0-9_@%+=:,.\/-]/.match(argument))
+			return argument;
+
+		// use single quotes, and put single quotes into double quotes
+		// the string $'b is then quoted as '$'"'"'b'
+		return "'" + replace(argument, "'", "'\"'\"'") + "'";
+	}
+
+	/**
+		Character codes of the characters that will be escaped by `quoteWinArg(_, true)`.
+	*/
+	public static var winMetaCharacters = [" ".code, "(".code, ")".code, "%".code, "!".code, "^".code, "\"".code, "<".code, ">".code, "&".code, "|".code, "\n".code, "\r".code];
+
+	/**
+		Returns a String that can be used as a single command line argument
+		on Windows.
+		The input will be quoted, or escaped if necessary, such that the output
+		will be parsed as a single argument using the rule specified in
+		http://msdn.microsoft.com/en-us/library/ms880421
+
+		Examples:
+		```
+		quoteWinArg("abc") == "abc";
+		quoteWinArg("ab c") == '"ab c"';
+		```
+	*/
+	public static function quoteWinArg(argument:String, escapeMetaCharacters:Bool):String {
+		// If there is no space, tab, back-slash, or double-quotes, and it is not an empty string.
+		if (!~/^[^ \t\\"]+$/.match(argument)) {
+			
+			// Based on cpython's subprocess.list2cmdline().
+			// https://hg.python.org/cpython/file/50741316dd3a/Lib/subprocess.py#l620
+
+			var result = new StringBuf();
+			var needquote = argument.indexOf(" ") != -1 || argument.indexOf("\t") != -1 || argument == "";
+
+			if (needquote)
+				result.add('"');
+
+			var bs_buf = new StringBuf();
+			for (i in 0...argument.length) {
+				switch (argument.charCodeAt(i)) {
+					case "\\".code:
+						// Don't know if we need to double yet.
+						bs_buf.add("\\");
+					case '"'.code:
+						// Double backslashes.
+						var bs = bs_buf.toString();
+						result.add(bs);
+						result.add(bs);
+						bs_buf = new StringBuf();
+						result.add('\\"');
+					case c:
+						// Normal char
+						if (bs_buf.length > 0) {
+							result.add(bs_buf.toString());
+							bs_buf = new StringBuf();
+						}
+						result.addChar(c);
+				}
+			}
+
+			// Add remaining backslashes, if any.
+			result.add(bs_buf.toString());
+
+			if (needquote) {
+				result.add(bs_buf.toString());
+				result.add('"');
+			}
+
+			argument = result.toString();
+		}
+
+		if (escapeMetaCharacters) {
+			var result = new StringBuf();
+			for (i in 0...argument.length) {
+				var c = argument.charCodeAt(i);
+				if (winMetaCharacters.indexOf(c) >= 0) {
+					result.addChar("^".code);
+				}
+				result.addChar(c);
+			}
+			return result.toString();
+		} else {
+			return argument;
+		}
+	}
+
 }

+ 10 - 19
std/php/_std/Sys.hx

@@ -70,27 +70,18 @@
 			return s;
 	}
 
-	static function escapeArgument( arg : String ) : String {
-		var ok = true;
-		for( i in 0...arg.length )
-			switch( arg.charCodeAt(i) ) {
-			case ' '.code, '\t'.code, '"'.code, '&'.code, '|'.code, '<'.code, '>'.code, '#'.code , ';'.code, '*'.code, '?'.code, '('.code, ')'.code, '{'.code, '}'.code, '$'.code:
-				ok = false;
-			case 0, 13, 10: // [eof] [cr] [lf]
-				arg = arg.substr(0,i);
-			}
-		if( ok )
-			return arg;
-		return '"'+arg.split('\\').join("\\\\").split('"').join('\\"')+'"';
-	}
-
 	public static function command( cmd : String, ?args : Array<String> ) : Int {
-		if( args != null ) {
-			cmd = escapeArgument(cmd);
-			for( a in args )
-				cmd += " "+escapeArgument(a);
+		if (args != null) {
+			switch (systemName()) {
+				case "Windows":
+					cmd = [
+						for (a in [StringTools.replace(cmd, "/", "\\")].concat(args))
+						StringTools.quoteWinArg(a, true)
+					].join(" ");
+				case _:
+					cmd = [cmd].concat(args).map(StringTools.quoteUnixArg).join(" ");
+			}
 		}
-		if (systemName() == "Windows") cmd = '"$cmd"';
 		var result = 0;
 		untyped __call__("system", cmd, result);
 		return result;

+ 14 - 13
std/php/_std/sys/io/Process.hx

@@ -92,7 +92,18 @@ class Process {
 			array('pipe', 'w'),
 			array('pipe', 'w')
 		)");
-		p = untyped __call__('proc_open', cmd+sargs(args), descriptorspec, pipes);
+		if (args != null) {
+			switch (Sys.systemName()) {
+				case "Windows":
+					cmd = [
+						for (a in [StringTools.replace(cmd, "/", "\\")].concat(args))
+						StringTools.quoteWinArg(a, true)
+					].join(" ");
+				case _:
+					cmd = [cmd].concat(args).map(StringTools.quoteUnixArg).join(" ");
+			}
+		}
+		p = untyped __call__('proc_open', cmd, descriptorspec, pipes);
 		if(untyped __physeq__(p, false)) throw "Process creation failure : "+cmd;
 		stdin  = new Stdin( pipes[0]);
 		stdout = new Stdout(pipes[1]);
@@ -104,18 +115,8 @@ class Process {
 			st = untyped __call__('proc_get_status', p);
 		replaceStream(stderr);
 		replaceStream(stdout);
-		cl = untyped __call__('proc_close', p);
-	}
-
-	function sargs(args : Array<String>) : String {
-		var b = '';
-		for(arg in args) {
-			arg = arg.split('"').join('\"');
-			if(arg.indexOf(' ') >= 0)
-				arg = '"'+arg+'"';
-			b += ' '+arg;
-		}
-		return b;
+		if(null == cl)
+			cl = untyped __call__('proc_close', p);
 	}
 
 	public function getPid() : Int {

+ 0 - 58
tests/sys/args.xml

@@ -1,58 +0,0 @@
-<!-- 
-This file is read by the sys test at compile-time via `-resource`.
-We may compare and update the test cases of other popular langs/libs: https://gist.github.com/andyli/d55ae9ea1327bbbf749d
--->
-<args>
-
-<arg><![CDATA[foo]]></arg>
-
-<arg><![CDATA[12]]></arg>
-
-<!-- symbols -->
-<arg><![CDATA[&]]></arg>
-<arg><![CDATA[&&]]></arg>
-<arg><![CDATA[|]]></arg>
-<arg><![CDATA[||]]></arg>
-<arg><![CDATA[.]]></arg>
-<arg><![CDATA[<]]></arg>
-<arg><![CDATA[>]]></arg>
-<arg><![CDATA[<<]]></arg>
-<arg><![CDATA[>>]]></arg>
-
-<!-- backslashes -->
-<!-- <arg><![CDATA[\]]></arg> -->
-<!-- <arg><![CDATA[\\]]></arg> -->
-<!-- <arg><![CDATA[\\\]]></arg> -->
-
-<!-- single quote -->
-<!-- <arg><![CDATA[']]></arg> -->
-
-<!-- kind of an escaped single quote -->
-<!-- <arg><![CDATA[\']]></arg> -->
-
-<!-- double quote -->
-<!-- <arg><![CDATA["]]></arg> -->
-
-<!-- kind of an escaped double quote -->
-<!-- <arg><![CDATA[\"]]></arg> -->
-
-<!-- space -->
-<arg><![CDATA[ ]]></arg>
-
-<!-- kind of an escaped space -->
-<!-- <arg><![CDATA[\ ]]></arg> -->
-
-<!-- empty string -->
-<!-- <arg><![CDATA[]]></arg> -->
-
-<!-- linebreak -->
-<!-- <arg><![CDATA[
-]]></arg> -->
-
-<!-- Chinese, Japanese -->
-<!-- <arg><![CDATA[中文,にほんご]]></arg> -->
-
-<!-- complex stuff -->
-<!-- <arg><![CDATA[a b  %PATH% $HOME c\&<>[\"]#{}|%$\""]]></arg> -->
-
-</args>

+ 8 - 2
tests/sys/compile-each.hxml

@@ -1,3 +1,9 @@
--debug
 -cp src
--resource args.xml
+-main ExitCode
+-neko bin/neko/ExitCode.n
+-cmd nekotools boot bin/neko/ExitCode.n
+
+--next
+
+-debug
+-cp src

+ 4 - 4
tests/sys/compile-neko.hxml

@@ -7,7 +7,7 @@ compile-each.hxml
 -main TestArguments
 -neko bin/neko/TestArguments.n
 
---next
-compile-each.hxml
--main ExitCode
--neko bin/neko/ExitCode.n
+# --next
+# compile-each.hxml
+# -main ExitCode
+# -neko bin/neko/ExitCode.n

+ 10 - 0
tests/sys/src/ExitCode.c

@@ -0,0 +1,10 @@
+#include <stdlib.h>
+
+int main(int argc, char *argv[])
+{
+	if (argc == 2) {
+		return atoi(argv[1]);
+	} else {
+		return -1;
+	}
+}

+ 50 - 0
tests/sys/src/ExitCode.hx

@@ -1,3 +1,7 @@
+import sys.*;
+import sys.io.*;
+import haxe.io.*;
+
 /**
 	This is intented to be used by TestSys and io.TestProcess.
 */
@@ -30,6 +34,52 @@ class ExitCode {
 	#else
 		null;
 	#end
+
+	static public function getNative():String {
+		// This is just a script that behaves like ExitCode.hx, 
+		// which exits with the code same as the first given argument. 
+		// var scriptContent = switch (Sys.systemName()) {
+		// 	case "Windows":
+		// 		'@echo off\nexit /b %1';
+		// 	case "Mac", "Linux", _:
+		// 		'#!/bin/sh\nexit $1';
+		// }
+		// var scriptExt = switch (Sys.systemName()) {
+		// 	case "Windows":
+		// 		".bat";
+		// 	case "Mac", "Linux", _:
+		// 		".sh";
+		// }
+
+		var binExt = switch (Sys.systemName()) {
+			case "Windows":
+				".exe";
+			case "Mac", "Linux", _:
+				"";
+		}
+
+		var binPath = Path.join(["bin", "ExitCode"]) + binExt;
+		if (FileSystem.exists(binPath)) {
+			return binPath;
+		}
+
+		if (!FileSystem.exists("bin"))
+			FileSystem.createDirectory("bin");
+
+		switch (Sys.systemName()) {
+			case "Windows":
+				// var gcc = Sys.command("cl", ["src/ExitCode.c", "/Fobin", "/link", "/out:bin/ExitCode.exe"]);
+				// if (gcc != 0)
+				// 	throw "cannot compile ExitCode";
+				File.copy(Path.join(["bin", "neko", "ExitCode"]) + binExt, binPath);
+			case "Mac", "Linux", _:
+				var gcc = Sys.command("gcc", ["src/ExitCode.c", "-o", "bin/ExitCode"]);
+				if (gcc != 0)
+					throw "cannot compile ExitCode";
+		}
+
+		return binPath;
+	}
 	
 	static function main():Void {
 		Sys.exit(Std.parseInt(Sys.args()[0]));

+ 22 - 5
tests/sys/src/FileNames.hx

@@ -1,15 +1,32 @@
 class FileNames {
 	static public var names(default, never) = [
 		"ok",
+		"ok2",
+		"ok3",
+		"ok4",
 
 		// a space inside
 		"two words",
 
 		// Chinese, Japanese
-		// "中文,にほんご",
+		#if !(cs || python || php || neko || cpp || java)
+		"中文,にほんご",
+		#end
 
-		// "aaa...a" that has 255 characters
-		// 255 bytes is the max filename length according to http://en.wikipedia.org/wiki/Comparison_of_file_systems
-		// [for (i in 0...255) "a"].join("")
-	];
+		// "aaa...a"
+		[for (i in 0...100) "a"].join(""),
+	]
+	// long file name
+	.concat(switch (Sys.systemName()) {
+		case "Windows":
+			// http://stackoverflow.com/a/265782/267998
+			[];
+		case _:
+		[
+			// 255 bytes is the max filename length according to http://en.wikipedia.org/wiki/Comparison_of_file_systems
+			#if !(python || neko || cpp || java)
+			[for (i in 0...255) "a"].join(""),
+			#end
+		];
+	});
 }

+ 65 - 7
tests/sys/src/TestArguments.hx

@@ -3,13 +3,71 @@
 	It will write the result to "temp/TestArguments.txt" (for debugging).
 */
 class TestArguments extends haxe.unit.TestCase {
-	static public var expectedArgs(get, null):Array<String>;
-	static function get_expectedArgs() {
-		return expectedArgs != null ? expectedArgs : expectedArgs = [
-			for (arg in new haxe.xml.Fast(Xml.parse(haxe.Resource.getString("args.xml"))).node.args.nodes.arg)
-			arg.innerData
+	// We may compare and update the test cases of other popular langs/libs: https://gist.github.com/andyli/d55ae9ea1327bbbf749d
+	static public var expectedArgs(default, never):Array<String> = [
+		"foo",
+		"12",
+
+		// symbols
+		"&",
+		"&&",
+		"|",
+		"||",
+		".",
+		"<",
+		">",
+		"<<",
+		">>",
+
+		// double quote
+		'"',
+		// kind of an escaped double quote
+		'\\"',
+
+		// space
+		" ",
+		// kind of an escaped space
+		"\\ ",
+
+		// empty string
+		"",
+
+		// complex stuff
+		"a b  %PATH% $HOME c\\&<>[\\\"]#{}|%$\\\"\"",
+	].concat(switch (Sys.systemName()) {
+		case "Windows":
+		[
+			// backslashes
+			"\\",
+			"\\\\",
+			"\\\\\\",
+
+			// single quote
+			"'",
+			// kind of an escaped single quote
+			"\\'",
 		];
-	}
+		case _:
+		[
+			#if !cs
+			// backslashes
+			"\\",
+			"\\\\",
+			"\\\\\\",
+
+			// single quote
+			"'",
+			// kind of an escaped single quote
+			"\\'",
+
+			// linebreak
+			"\n",
+			#end
+
+			// Chinese, Japanese
+			"中文,にほんご",
+		];
+	});
 
 	static public var bin:String =
 	#if neko
@@ -64,4 +122,4 @@ class TestArguments extends haxe.unit.TestCase {
 		log.close();
 		Sys.exit(code);
 	}
-}
+}

+ 125 - 0
tests/sys/src/TestCommandBase.hx

@@ -0,0 +1,125 @@
+import sys.*;
+
+class TestCommandBase extends haxe.unit.TestCase {
+	function run(cmd:String, ?args:Array<String>):Int {
+		throw "should be overridden";
+	}
+
+	function testCommand() {
+		var bin = FileSystem.absolutePath(TestArguments.bin);
+		var args = TestArguments.expectedArgs;
+
+		#if !cs
+		var exitCode = run("haxe", ["compile-each.hxml", "--run", "TestArguments"].concat(args));
+		if (exitCode != 0)
+			trace(sys.io.File.getContent(TestArguments.log));
+		assertEquals(0, exitCode);
+		#end
+
+		var exitCode =
+			#if (macro || interp)
+				run("haxe", ["compile-each.hxml", "--run", "TestArguments"].concat(args));
+			#elseif cpp
+				run(bin, args);
+			#elseif cs
+				switch (Sys.systemName()) {
+					case "Windows":
+						run(bin, args);
+					case _:
+						run("mono", [bin].concat(args));
+				}
+			#elseif java
+				run("java", ["-jar", bin].concat(args));
+			#elseif python
+				run("python3", [bin].concat(args));
+			#elseif neko
+				run("neko", [bin].concat(args));
+			#elseif php
+				run("php", [bin].concat(args));
+			#else
+				-1;
+			#end
+		if (exitCode != 0)
+			trace(sys.io.File.getContent(TestArguments.log));
+		assertEquals(0, exitCode);
+	}
+
+	function testCommandName() {		
+		var binExt = switch (Sys.systemName()) {
+			case "Windows":
+				".exe";
+			case "Mac", "Linux", _:
+				"";
+		}
+
+		for (name in FileNames.names) {
+			if ((name + binExt).length < 256) {
+				var path = FileSystem.absolutePath("temp/" + name + binExt);
+				switch (Sys.systemName()) {
+					case "Windows":
+						sys.io.File.copy(ExitCode.getNative(), path);
+					case "Mac", "Linux", _:
+						var exitCode = run("cp", [ExitCode.getNative(), path]);
+						assertEquals(0, exitCode);
+				}
+
+				Sys.sleep(0.1);
+
+				var random = Std.random(256);
+				var exitCode = try {
+					run(path, [Std.string(random)]);
+				} catch (e:Dynamic) {
+					trace(e);
+					trace(name);
+					throw e;
+				}
+				if (exitCode != random)
+					trace(name);
+				assertEquals(random, exitCode);
+				FileSystem.deleteFile(path);
+			}
+		}
+	}
+
+	function testExitCode() {
+		var bin = FileSystem.absolutePath(ExitCode.bin);
+
+		// Just test only a few to save time.
+		// They have special meanings: http://tldp.org/LDP/abs/html/exitcodes.html
+		var codes = [0, 1, 2, 126, 127, 128, 130, 255];
+
+		for (code in codes) {
+			var args = [Std.string(code)];
+			var exitCode = run(ExitCode.getNative(), args);
+			assertEquals(code, exitCode);
+		}
+
+		for (code in codes) {
+			var args = [Std.string(code)];
+			var exitCode =
+				#if (macro || interp)
+					run("haxe", ["compile-each.hxml", "--run", "ExitCode"].concat(args));
+				#elseif cpp
+					run(bin, args);
+				#elseif cs
+					switch (Sys.systemName()) {
+						case "Windows":
+							run(bin, args);
+						case _:
+							run("mono", [bin].concat(args));
+					}
+				#elseif java
+					run("java", ["-jar", bin].concat(args));
+				#elseif python
+					run("python3", [bin].concat(args));
+				#elseif neko
+					run("neko", [bin].concat(args));
+				#elseif php
+					run("php", [bin].concat(args));
+				#else
+					-1;
+				#end
+			assertEquals(code, exitCode);
+		}
+	}
+}

+ 3 - 146
tests/sys/src/TestSys.hx

@@ -1,151 +1,8 @@
-class TestSys extends haxe.unit.TestCase {
-	#if !php //FIXME https://github.com/HaxeFoundation/haxe/issues/3603#issuecomment-86437474
-	function testCommand() {
-		var bin = sys.FileSystem.absolutePath(TestArguments.bin);
-		var args = TestArguments.expectedArgs;
-
-		var exitCode = Sys.command("haxe", ["compile-each.hxml", "--run", "TestArguments"].concat(args));
-		if (exitCode != 0)
-			trace(sys.io.File.getContent(TestArguments.log));
-		assertEquals(0, exitCode);
-
-		var exitCode =
-			#if (macro || interp)
-				Sys.command("haxe", ["compile-each.hxml", "--run", "TestArguments"].concat(args));
-			#elseif cpp
-				Sys.command(bin, args);
-			#elseif cs
-				switch (Sys.systemName()) {
-					case "Windows":
-						Sys.command(bin, args);
-					case _:
-						Sys.command("mono", [bin].concat(args));
-				}
-			#elseif java
-				Sys.command("java", ["-jar", bin].concat(args));
-			#elseif python
-				Sys.command("python3", [bin].concat(args));
-			#elseif neko
-				Sys.command("neko", [bin].concat(args));
-			#elseif php
-				Sys.command("php", [bin].concat(args));
-			#else
-				-1;
-			#end
-		if (exitCode != 0)
-			trace(sys.io.File.getContent(TestArguments.log));
-		assertEquals(0, exitCode);
+class TestSys extends TestCommandBase {
+	override function run(cmd:String, ?args:Array<String>):Int {
+		return Sys.command(cmd, args);
 	}
 
-	#if !cs //FIXME
-	function testCommandName() {
-		// This is just a script that behaves like ExitCode.hx, 
-		// which exits with the code same as the first given argument. 
-		var scriptContent = switch (Sys.systemName()) {
-			case "Windows":
-				'@echo off\nexit /b %1';
-			case "Mac", "Linux", _:
-				'#!/bin/sh\nexit $1';
-		}
-		for (name in FileNames.names) {
-			//call with ext
-			var scriptExt = switch (Sys.systemName()) {
-				case "Windows":
-					".bat";
-				case "Mac", "Linux", _:
-					".sh";
-			}
-			var path = sys.FileSystem.absolutePath("temp/" + name + scriptExt);
-			sys.io.File.saveContent(path, scriptContent);
-
-			switch (Sys.systemName()) {
-				case "Mac", "Linux":
-					var exitCode = Sys.command("chmod", ["a+x", path]);
-					assertEquals(0, exitCode);
-				case "Windows":
-					//pass
-			}
-
-			var random = Std.random(256);
-			var exitCode = Sys.command(path, [Std.string(random)]);
-			if (exitCode != random)
-				trace(name);
-			assertEquals(random, exitCode);
-			sys.FileSystem.deleteFile(path);
-
-
-
-			//call without ext
-			switch (Sys.systemName()) {
-				case "Windows":
-					//pass
-				case "Mac", "Linux", _:
-					var scriptExt = "";
-					var path = sys.FileSystem.absolutePath("temp/" + name + scriptExt);
-					sys.io.File.saveContent(path, scriptContent);
-
-					switch (Sys.systemName()) {
-						case "Mac", "Linux":
-							var exitCode = Sys.command("chmod", ["a+x", path]);
-							assertEquals(0, exitCode);
-						case "Windows":
-							//pass
-					}
-
-					var random = Std.random(256);
-					var exitCode = Sys.command(path, [Std.string(random)]);
-					if (exitCode != random)
-						trace(name);
-					assertEquals(random, exitCode);
-					sys.FileSystem.deleteFile(path);
-			}
-		}
-	}
-	#end //!cs
-
-	function testExitCode() {
-		var bin = sys.FileSystem.absolutePath(ExitCode.bin);
-
-		// Just test only a few to save time.
-		// They have special meanings: http://tldp.org/LDP/abs/html/exitcodes.html
-		var codes = [0, 1, 2, 126, 127, 128, 130, 255];
-
-		for (code in codes) {
-			var args = [Std.string(code)];
-			var exitCode = Sys.command("haxe", ["compile-each.hxml", "--run", "ExitCode"].concat(args));
-			assertEquals(code, exitCode);
-		}
-
-		for (code in codes) {
-			var args = [Std.string(code)];
-			var exitCode =
-				#if (macro || interp)
-					Sys.command("haxe", ["compile-each.hxml", "--run", "ExitCode"].concat(args));
-				#elseif cpp
-					Sys.command(bin, args);
-				#elseif cs
-					switch (Sys.systemName()) {
-						case "Windows":
-							Sys.command(bin, args);
-						case _:
-							Sys.command("mono", [bin].concat(args));
-					}
-				#elseif java
-					Sys.command("java", ["-jar", bin].concat(args));
-				#elseif python
-					Sys.command("python3", [bin].concat(args));
-				#elseif neko
-					Sys.command("neko", [bin].concat(args));
-				#elseif php
-					Sys.command("php", [bin].concat(args));
-				#else
-					-1;
-				#end
-			assertEquals(code, exitCode);
-		}
-	}
-	#end
-
 	function testEnv() {
 		#if !(java || php)
 		Sys.putEnv("foo", "value");

+ 7 - 150
tests/sys/src/io/TestProcess.hx

@@ -2,154 +2,11 @@ package io;
 
 import sys.io.Process;
 
-class TestProcess extends haxe.unit.TestCase {
-	#if !php //FIXME
-	function testArguments() {
-		var bin = sys.FileSystem.absolutePath(TestArguments.bin);
-		var args = TestArguments.expectedArgs;
-
-		var process = new Process("haxe", ["compile-each.hxml", "--run", "TestArguments"].concat(args));
-		var exitCode = process.exitCode();
-		if (exitCode != 0)
-			trace(sys.io.File.getContent(TestArguments.log));
-		assertEquals(0, exitCode);
-
-		var process =
-			#if (macro || interp)
-				new Process("haxe", ["compile-each.hxml", "--run", "TestArguments"].concat(args));
-			#elseif cpp
-				new Process(bin, args);
-			#elseif cs
-				switch (Sys.systemName()) {
-					case "Windows":
-						new Process(bin, args);
-					case _:
-						new Process("mono", [bin].concat(args));
-				}
-			#elseif java
-				new Process("java", ["-jar", bin].concat(args));
-			#elseif python
-				new Process("python3", [bin].concat(args));
-			#elseif neko
-				new Process("neko", [bin].concat(args));
-			#elseif php
-				new Process("php", [bin].concat(args));
-			#else
-				null;
-			#end
-		var exitCode = process.exitCode();
-		if (exitCode != 0)
-			trace(sys.io.File.getContent(TestArguments.log));
-		assertEquals(0, exitCode);
-	}
-
-	#if !(neko || cpp || cs) //FIXME
-	function testCommandName() {
-		// This is just a script that behaves like ExitCode.hx, 
-		// which exits with the code same as the first given argument. 
-		var scriptContent = switch (Sys.systemName()) {
-			case "Windows":
-				'@echo off\nexit /b %1';
-			case "Mac", "Linux", _:
-				'#!/bin/sh\nexit $1';
-		}
-		for (name in FileNames.names) {
-			//call with ext
-			var scriptExt = switch (Sys.systemName()) {
-				case "Windows":
-					".bat";
-				case "Mac", "Linux", _:
-					".sh";
-			}
-			var path = sys.FileSystem.absolutePath("temp/" + name + scriptExt);
-			sys.io.File.saveContent(path, scriptContent);
-
-			switch (Sys.systemName()) {
-				case "Mac", "Linux":
-					var exitCode = Sys.command("chmod", ["a+x", path]);
-					assertEquals(0, exitCode);
-				case "Windows":
-					//pass
-			}
-
-			var random = Std.random(256);
-			var exitCode = new Process(path, [Std.string(random)]).exitCode();
-			if (exitCode != random)
-				trace(name);
-			assertEquals(random, exitCode);
-			sys.FileSystem.deleteFile(path);
-
-
-
-			//call without ext
-			var scriptExt = switch (Sys.systemName()) {
-				case "Windows":
-					".bat";
-				case "Mac", "Linux", _:
-					"";
-			}
-			var path = sys.FileSystem.absolutePath("temp/" + name + scriptExt);
-			sys.io.File.saveContent(path, scriptContent);
-
-			switch (Sys.systemName()) {
-				case "Mac", "Linux":
-					var exitCode = Sys.command("chmod", ["a+x", path]);
-					assertEquals(0, exitCode);
-				case "Windows":
-					//pass
-			}
-
-			var random = Std.random(256);
-			var exitCode = new Process(path, [Std.string(random)]).exitCode();
-			if (exitCode != random)
-				trace(name);
-			assertEquals(random, exitCode);
-			sys.FileSystem.deleteFile(path);
-		}
-	}
-	#end //!neko
-
-	#end //!php
-
-	function testExitCode() {
-		var bin = sys.FileSystem.absolutePath(ExitCode.bin);
-
-		// Just test only a few to save time.
-		// They have special meanings: http://tldp.org/LDP/abs/html/exitcodes.html
-		var codes = [0, 1, 2, 126, 127, 128, 130, 255];
-
-		for (code in codes) {
-			var args = [Std.string(code)];
-			var process = new Process("haxe", ["compile-each.hxml", "--run", "ExitCode"].concat(args));
-			assertEquals(code, process.exitCode());
-		}
-
-		for (code in codes) {
-			var args = [Std.string(code)];
-			var process =
-				#if (macro || interp)
-					new Process("haxe", ["compile-each.hxml", "--run", "ExitCode"].concat(args));
-				#elseif cpp
-					new Process(bin, args);
-				#elseif cs
-					switch (Sys.systemName()) {
-						case "Windows":
-							new Process(bin, args);
-						case _:
-							new Process("mono", [bin].concat(args));
-					}
-				#elseif java
-					new Process("java", ["-jar", bin].concat(args));
-				#elseif python
-					new Process("python3", [bin].concat(args));
-				#elseif neko
-					new Process("neko", [bin].concat(args));
-				#elseif php
-					new Process("php", [bin].concat(args));
-				#else
-					null;
-				#end
-			assertEquals(code, process.exitCode());
-		}
+class TestProcess extends TestCommandBase {
+	override function run(cmd:String, ?args:Array<String>):Int {
+		var p = new Process(cmd, args);
+		var exitCode = p.exitCode();
+		p.close();
+		return exitCode;
 	}
-}
+}