Index: src/xsprintf/__tests__/PhutilcsprintfTestCase.php =================================================================== --- src/xsprintf/__tests__/PhutilcsprintfTestCase.php +++ src/xsprintf/__tests__/PhutilcsprintfTestCase.php @@ -2,6 +2,20 @@ final class PhutilcsprintfTestCase extends ArcanistTestCase { + public function testCommandReadableEscapes() { + // For arguments comprised of only characters which are safe in any context, + // %R this should avoid adding quotes. + $this->assertEqual( + true, + ('ab' === (string)csprintf('%R', 'ab'))); + + // For arguments which have any characters which are not safe in some + // context, %R should apply standard escaping. + $this->assertEqual( + false, + ('a b' === (string)csprintf('%R', 'a b'))); + } + public function testPasswords() { // Normal "%s" doesn't do anything special. @@ -10,7 +24,6 @@ true, strpos($command, 'hunter2trustno1') !== false); - // "%P" takes a PhutilOpaqueEnvelope. $caught = null; try { Index: src/xsprintf/csprintf.php =================================================================== --- src/xsprintf/csprintf.php +++ src/xsprintf/csprintf.php @@ -15,6 +15,12 @@ * %C (Raw Command) * Passes the argument through without escaping. Dangerous! * + * %R + * A more "readable" version of "%s". This will try to print the command + * without any escaping if it contains only characters which are safe + * in any context. The intent is to produce prettier human-readable + * commands. + * * Generally, you should invoke shell commands via execx() rather than by * calling csprintf() directly. * @@ -80,6 +86,12 @@ // Convert the list of strings to a single string. $value = implode(' ', array_map('escapeshellarg', $value)); break; + case 'R': + if (!preg_match('(^[a-zA-Z0-9:/@._-]+$)', $value)) { + $value = escapeshellarg($value); + } + $type = 's'; + break; case 's': $value = escapeshellarg($value); $type = 's';