1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-22 06:42:41 +01:00

Require "--" as an argument terminator when running noninteractively

Summary:
Depends on D21001. Ref T13490.

  - Require "--" to terminate arguments when running noninteractively.
  - Don't correct spelling when running noninteractively (we still suggest corrections).
  - Remove old "phage" wrapper script.
  - Fix an issue where "argv" could implicitly generate a parseable "--argv" flag.
  - Accept "--" as an argument terminator even when there is no argument list.
  - Update some strings to use more modern quoting style.
  - Make workflows decline to handle signals by default.
  - Allow "arc weld" to weld non-UTF8 files together.

Test Plan: Ran various commands. Welded non-UTF8 files.

Maniphest Tasks: T13490

Differential Revision: https://secure.phabricator.com/D21002
This commit is contained in:
epriestley 2020-02-14 18:22:43 -08:00
parent db8419f19b
commit d3b77af8a5
9 changed files with 130 additions and 51 deletions

View file

@ -1,29 +0,0 @@
#!/usr/bin/env php
<?php
require_once dirname(__FILE__).'/__init_script__.php';
ini_set('memory_limit', -1);
$args = new PhutilArgumentParser($argv);
$args->parseStandardArguments();
$args->parsePartial(array());
// TODO: This is pretty minimal and should be shared with "arc".
$working_directory = getcwd();
$working_copy = ArcanistWorkingCopyIdentity::newFromPath($working_directory);
$config = id(new ArcanistConfigurationManager())
->setWorkingCopyIdentity($working_copy);
foreach ($config->getProjectConfig('load') as $load) {
$load = Filesystem::resolvePath($working_copy->getProjectRoot().'/'.$load);
phutil_load_library($load);
}
$workflows = id(new PhutilClassMapQuery())
->setAncestorClass('PhageWorkflow')
->execute();
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);

View file

@ -301,6 +301,7 @@ phutil_register_library_map(array(
'ArcanistMergeConflictLinter' => 'lint/linter/ArcanistMergeConflictLinter.php', 'ArcanistMergeConflictLinter' => 'lint/linter/ArcanistMergeConflictLinter.php',
'ArcanistMergeConflictLinterTestCase' => 'lint/linter/__tests__/ArcanistMergeConflictLinterTestCase.php', 'ArcanistMergeConflictLinterTestCase' => 'lint/linter/__tests__/ArcanistMergeConflictLinterTestCase.php',
'ArcanistMessageRevisionHardpointLoader' => 'loader/ArcanistMessageRevisionHardpointLoader.php', 'ArcanistMessageRevisionHardpointLoader' => 'loader/ArcanistMessageRevisionHardpointLoader.php',
'ArcanistMissingArgumentTerminatorException' => 'exception/ArcanistMissingArgumentTerminatorException.php',
'ArcanistMissingLinterException' => 'lint/linter/exception/ArcanistMissingLinterException.php', 'ArcanistMissingLinterException' => 'lint/linter/exception/ArcanistMissingLinterException.php',
'ArcanistModifierOrderingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistModifierOrderingXHPASTLinterRule.php', 'ArcanistModifierOrderingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistModifierOrderingXHPASTLinterRule.php',
'ArcanistModifierOrderingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistModifierOrderingXHPASTLinterRuleTestCase.php', 'ArcanistModifierOrderingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistModifierOrderingXHPASTLinterRuleTestCase.php',
@ -885,6 +886,7 @@ phutil_register_library_map(array(
'phutil_ini_decode' => 'utils/utils.php', 'phutil_ini_decode' => 'utils/utils.php',
'phutil_is_hiphop_runtime' => 'utils/utils.php', 'phutil_is_hiphop_runtime' => 'utils/utils.php',
'phutil_is_natural_list' => 'utils/utils.php', 'phutil_is_natural_list' => 'utils/utils.php',
'phutil_is_noninteractive' => 'utils/utils.php',
'phutil_is_system_locale_available' => 'utils/utf8.php', 'phutil_is_system_locale_available' => 'utils/utf8.php',
'phutil_is_utf8' => 'utils/utf8.php', 'phutil_is_utf8' => 'utils/utf8.php',
'phutil_is_utf8_slowly' => 'utils/utf8.php', 'phutil_is_utf8_slowly' => 'utils/utf8.php',
@ -1240,6 +1242,7 @@ phutil_register_library_map(array(
'ArcanistMergeConflictLinter' => 'ArcanistLinter', 'ArcanistMergeConflictLinter' => 'ArcanistLinter',
'ArcanistMergeConflictLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistMergeConflictLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistMessageRevisionHardpointLoader' => 'ArcanistHardpointLoader', 'ArcanistMessageRevisionHardpointLoader' => 'ArcanistHardpointLoader',
'ArcanistMissingArgumentTerminatorException' => 'Exception',
'ArcanistMissingLinterException' => 'Exception', 'ArcanistMissingLinterException' => 'Exception',
'ArcanistModifierOrderingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistModifierOrderingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistModifierOrderingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistModifierOrderingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',

View file

@ -0,0 +1,4 @@
<?php
final class ArcanistMissingArgumentTerminatorException
extends Exception {}

View file

@ -78,6 +78,8 @@ final class PhutilArgumentParser extends Phobject {
private $synopsis; private $synopsis;
private $workflows; private $workflows;
private $showHelp; private $showHelp;
private $requireArgumentTerminator = false;
private $sawTerminator = false;
const PARSE_ERROR_CODE = 77; const PARSE_ERROR_CODE = 77;
@ -129,8 +131,20 @@ final class PhutilArgumentParser extends Phobject {
$specs = PhutilArgumentSpecification::newSpecsFromList($specs); $specs = PhutilArgumentSpecification::newSpecsFromList($specs);
$this->mergeSpecs($specs); $this->mergeSpecs($specs);
$specs_by_name = mpull($specs, null, 'getName'); // Wildcard arguments have a name like "argv", but we don't want to
$specs_by_short = mpull($specs, null, 'getShortAlias'); // parse a corresponding flag like "--argv". Filter them out before
// building a list of available flags.
$non_wildcard = array();
foreach ($specs as $spec_key => $spec) {
if ($spec->getWildcard()) {
continue;
}
$non_wildcard[$spec_key] = $spec;
}
$specs_by_name = mpull($non_wildcard, null, 'getName');
$specs_by_short = mpull($non_wildcard, null, 'getShortAlias');
unset($specs_by_short[null]); unset($specs_by_short[null]);
$argv = $this->argv; $argv = $this->argv;
@ -144,6 +158,7 @@ final class PhutilArgumentParser extends Phobject {
// Non-string argument; pass it through as-is. // Non-string argument; pass it through as-is.
} else if ($arg == '--') { } else if ($arg == '--') {
// This indicates "end of flags". // This indicates "end of flags".
$this->sawTerminator = true;
break; break;
} else if ($arg == '-') { } else if ($arg == '-') {
// This is a normal argument (e.g., stdin). // This is a normal argument (e.g., stdin).
@ -174,7 +189,8 @@ final class PhutilArgumentParser extends Phobject {
$corrections = PhutilArgumentSpellingCorrector::newFlagCorrector() $corrections = PhutilArgumentSpellingCorrector::newFlagCorrector()
->correctSpelling($arg, $options); ->correctSpelling($arg, $options);
if (count($corrections) == 1) { $should_autocorrect = $this->shouldAutocorrect();
if (count($corrections) == 1 && $should_autocorrect) {
$corrected = head($corrections); $corrected = head($corrections);
$this->logMessage( $this->logMessage(
@ -206,7 +222,7 @@ final class PhutilArgumentParser extends Phobject {
if ($param_name === null) { if ($param_name === null) {
throw new PhutilArgumentUsageException( throw new PhutilArgumentUsageException(
pht( pht(
"Argument '%s' does not take a parameter.", 'Argument "%s" does not take a parameter.',
"{$pre}{$arg}")); "{$pre}{$arg}"));
} }
} else { } else {
@ -218,7 +234,7 @@ final class PhutilArgumentParser extends Phobject {
} else { } else {
throw new PhutilArgumentUsageException( throw new PhutilArgumentUsageException(
pht( pht(
"Argument '%s' requires a parameter.", 'Argument "%s" requires a parameter.',
"{$pre}{$arg}")); "{$pre}{$arg}"));
} }
} else { } else {
@ -230,7 +246,7 @@ final class PhutilArgumentParser extends Phobject {
if (array_key_exists($spec->getName(), $this->results)) { if (array_key_exists($spec->getName(), $this->results)) {
throw new PhutilArgumentUsageException( throw new PhutilArgumentUsageException(
pht( pht(
"Argument '%s' was provided twice.", 'Argument "%s" was provided twice.',
"{$pre}{$arg}")); "{$pre}{$arg}"));
} }
} }
@ -247,7 +263,7 @@ final class PhutilArgumentParser extends Phobject {
throw new PhutilArgumentUsageException( throw new PhutilArgumentUsageException(
pht( pht(
"Argument '%s' conflicts with argument '%s'%s", 'Argument "%s" conflicts with argument "%s"%s',
"{$pre}{$arg}", "{$pre}{$arg}",
"--{$conflict}", "--{$conflict}",
$reason)); $reason));
@ -279,6 +295,7 @@ final class PhutilArgumentParser extends Phobject {
} }
$this->argv = array_values($argv); $this->argv = array_values($argv);
return $this; return $this;
} }
@ -300,10 +317,20 @@ final class PhutilArgumentParser extends Phobject {
public function parseFull(array $specs) { public function parseFull(array $specs) {
$this->parseInternal($specs, true, false); $this->parseInternal($specs, true, false);
if (count($this->argv)) { // If we have remaining unconsumed arguments other than a single "--",
$arg = head($this->argv); // fail.
$argv = $this->filterWildcardArgv($this->argv);
if ($argv) {
throw new PhutilArgumentUsageException( throw new PhutilArgumentUsageException(
pht("Unrecognized argument '%s'.", $arg)); pht(
'Unrecognized argument "%s".',
head($argv)));
}
if ($this->getRequireArgumentTerminator()) {
if (!$this->sawTerminator) {
throw new ArcanistMissingArgumentTerminatorException();
}
} }
if ($this->showHelp) { if ($this->showHelp) {
@ -408,7 +435,8 @@ final class PhutilArgumentParser extends Phobject {
$corrected = PhutilArgumentSpellingCorrector::newCommandCorrector() $corrected = PhutilArgumentSpellingCorrector::newCommandCorrector()
->correctSpelling($flow, array_keys($this->workflows)); ->correctSpelling($flow, array_keys($this->workflows));
if (count($corrected) == 1) { $should_autocorrect = $this->shouldAutocorrect();
if (count($corrected) == 1 && $should_autocorrect) {
$corrected = head($corrected); $corrected = head($corrected);
$this->logMessage( $this->logMessage(
@ -551,7 +579,7 @@ final class PhutilArgumentParser extends Phobject {
if ($xprofile) { if ($xprofile) {
if (!function_exists('xhprof_enable')) { if (!function_exists('xhprof_enable')) {
throw new Exception( throw new Exception(
pht("To use '%s', you must install XHProf.", '--xprofile')); pht('To use "--xprofile", you must install XHProf.'));
} }
xhprof_enable(0); xhprof_enable(0);
@ -580,7 +608,7 @@ final class PhutilArgumentParser extends Phobject {
public function getArg($name) { public function getArg($name) {
if (empty($this->specs[$name])) { if (empty($this->specs[$name])) {
throw new PhutilArgumentSpecificationException( throw new PhutilArgumentSpecificationException(
pht("No specification exists for argument '%s'!", $name)); pht('No specification exists for argument "%s"!', $name));
} }
if (idx($this->results, $name) !== null) { if (idx($this->results, $name) !== null) {
@ -602,6 +630,14 @@ final class PhutilArgumentParser extends Phobject {
/* -( Command Help )------------------------------------------------------- */ /* -( Command Help )------------------------------------------------------- */
public function setRequireArgumentTerminator($require) {
$this->requireArgumentTerminator = $require;
return $this;
}
public function getRequireArgumentTerminator() {
return $this->requireArgumentTerminator;
}
public function setSynopsis($synopsis) { public function setSynopsis($synopsis) {
$this->synopsis = $synopsis; $this->synopsis = $synopsis;
@ -750,8 +786,8 @@ final class PhutilArgumentParser extends Phobject {
throw new PhutilArgumentUsageException( throw new PhutilArgumentUsageException(
pht( pht(
"Argument '%s' is unrecognized. Use '%s' to indicate ". 'Argument "%s" is unrecognized. Use "%s" to indicate '.
"the end of flags.", 'the end of flags.',
$value, $value,
'--')); '--'));
} }
@ -778,7 +814,9 @@ final class PhutilArgumentParser extends Phobject {
if (isset($this->specs[$name])) { if (isset($this->specs[$name])) {
throw new PhutilArgumentSpecificationException( throw new PhutilArgumentSpecificationException(
pht("Two argument specifications have the same name ('%s').", $name)); pht(
'Two argument specifications have the same name ("%s").',
$name));
} }
$short = $spec->getShortAlias(); $short = $spec->getShortAlias();
@ -786,7 +824,7 @@ final class PhutilArgumentParser extends Phobject {
if (isset($short_map[$short])) { if (isset($short_map[$short])) {
throw new PhutilArgumentSpecificationException( throw new PhutilArgumentSpecificationException(
pht( pht(
"Two argument specifications have the same short alias ('%s').", 'Two argument specifications have the same short alias ("%s").',
$short)); $short));
} }
$short_map[$short] = $spec; $short_map[$short] = $spec;
@ -811,13 +849,15 @@ final class PhutilArgumentParser extends Phobject {
if (empty($this->specs[$conflict])) { if (empty($this->specs[$conflict])) {
throw new PhutilArgumentSpecificationException( throw new PhutilArgumentSpecificationException(
pht( pht(
"Argument '%s' conflicts with unspecified argument '%s'.", 'Argument "%s" conflicts with unspecified argument "%s".',
$name, $name,
$conflict)); $conflict));
} }
if ($conflict == $name) { if ($conflict == $name) {
throw new PhutilArgumentSpecificationException( throw new PhutilArgumentSpecificationException(
pht("Argument '%s' conflicts with itself!", $name)); pht(
'Argument "%s" conflicts with itself!',
$name));
} }
} }
} }
@ -925,11 +965,15 @@ final class PhutilArgumentParser extends Phobject {
"%B\n%s", "%B\n%s",
$message, $message,
pht( pht(
'For details on available commands, run `%s`.', 'For details on available commands, run "%s".',
"{$binary} help")); "{$binary} help"));
} }
throw new PhutilArgumentUsageException($message); throw new PhutilArgumentUsageException($message);
} }
private function shouldAutocorrect() {
return !phutil_is_noninteractive();
}
} }

View file

@ -68,6 +68,14 @@ final class ArcanistRuntime {
$args = id(new PhutilArgumentParser($argv)) $args = id(new PhutilArgumentParser($argv))
->parseStandardArguments(); ->parseStandardArguments();
// If we can test whether STDIN is a TTY, and it isn't, require that "--"
// appear in the argument list. This is intended to make it very hard to
// write unsafe scripts on top of Arcanist.
if (phutil_is_noninteractive()) {
$args->setRequireArgumentTerminator(true);
}
$is_trace = $args->getArg('trace'); $is_trace = $args->getArg('trace');
$log->setShowTraceMessages($is_trace); $log->setShowTraceMessages($is_trace);
@ -138,6 +146,29 @@ final class ArcanistRuntime {
try { try {
return $args->parseWorkflowsFull($phutil_workflows); return $args->parseWorkflowsFull($phutil_workflows);
} catch (ArcanistMissingArgumentTerminatorException $terminator_exception) {
$log->writeHint(
pht('USAGE'),
pht(
'"%s" is being run noninteractively, but the argument list is '.
'missing "--" to indicate end of flags.',
$toolset->getToolsetKey()));
$log->writeHint(
pht('USAGE'),
pht(
'When running noninteractively, you MUST provide "--" to all '.
'commands (even if they take no arguments).'));
$log->writeHint(
pht('USAGE'),
tsprintf(
'%s <__%s__>',
pht('Learn More:'),
'https://phurl.io/u/noninteractive'));
throw new PhutilArgumentUsageException(
pht('Missing required "--" in argument list.'));
} catch (PhutilArgumentUsageException $usage_exception) { } catch (PhutilArgumentUsageException $usage_exception) {
// TODO: This is very, very hacky; we're trying to let errors like // TODO: This is very, very hacky; we're trying to let errors like

View file

@ -145,6 +145,8 @@ EOTEXT
// TOOLSETS: Check if the user already has an alias for this trigger, and // TOOLSETS: Check if the user already has an alias for this trigger, and
// prompt them to overwrite it. Needs prompting to work. // prompt them to overwrite it. Needs prompting to work.
// TOOLSETS: Don't let users set aliases which don't resolve to anything.
$aliases[] = $alias; $aliases[] = $alias;
$this->writeAliases($aliases); $this->writeAliases($aliases);

View file

@ -1918,3 +1918,11 @@ function phutil_escape_uri_path_component($string) {
function phutil_unescape_uri_path_component($string) { function phutil_unescape_uri_path_component($string) {
return rawurldecode($string); return rawurldecode($string);
} }
function phutil_is_noninteractive() {
if (function_exists('posix_isatty') && !posix_isatty(STDIN)) {
return true;
}
return false;
}

View file

@ -87,8 +87,20 @@ EOTEXT
$u = rtrim($u, "\r\n"); $u = rtrim($u, "\r\n");
$v = rtrim($v, "\r\n"); $v = rtrim($v, "\r\n");
// If the inputs are UTF8, split glyphs (so two valid UTF8 inputs always
// produce a sensible, valid UTF8 output). If they aren't, split bytes.
if (phutil_is_utf8($u)) {
$u = phutil_utf8v_combined($u); $u = phutil_utf8v_combined($u);
} else {
$u = str_split($u);
}
if (phutil_is_utf8($v)) {
$v = phutil_utf8v_combined($v); $v = phutil_utf8v_combined($v);
} else {
$v = str_split($v);
}
$len = max(count($u), count($v)); $len = max(count($u), count($v));

View file

@ -2282,4 +2282,8 @@ abstract class ArcanistWorkflow extends Phobject {
return $this->getConfigurationSourceList()->getConfig($key); return $this->getConfigurationSourceList()->getConfig($key);
} }
final public function canHandleSignal($signo) {
return false;
}
} }