diff --git a/bin/aws-s3 b/bin/aws-s3
new file mode 120000
index 00000000..3768bef6
--- /dev/null
+++ b/bin/aws-s3
@@ -0,0 +1 @@
+../scripts/utils/aws-s3.php
\ No newline at end of file
diff --git a/scripts/utils/aws-s3.php b/scripts/utils/aws-s3.php
new file mode 100755
index 00000000..10d2b67c
--- /dev/null
+++ b/scripts/utils/aws-s3.php
@@ -0,0 +1,22 @@
+#!/usr/bin/env php
+setTagline(pht('AWS CLI Client for S3'));
+$args->setSynopsis(<<parseStandardArguments();
+
+$workflows = id(new PhutilClassMapQuery())
+ ->setAncestorClass('PhutilAWSS3ManagementWorkflow')
+ ->execute();
+
+$workflows[] = new PhutilHelpArgumentWorkflow();
+$args->parseWorkflows($workflows);
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index b2ffbb55..d7863365 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,914 +1,928 @@
2,
'class' => array(
'AASTNode' => 'parser/aast/api/AASTNode.php',
'AASTNodeList' => 'parser/aast/api/AASTNodeList.php',
'AASTToken' => 'parser/aast/api/AASTToken.php',
'AASTTree' => 'parser/aast/api/AASTTree.php',
'AbstractDirectedGraph' => 'utils/AbstractDirectedGraph.php',
'AbstractDirectedGraphTestCase' => 'utils/__tests__/AbstractDirectedGraphTestCase.php',
'AphrontAccessDeniedQueryException' => 'aphront/storage/exception/AphrontAccessDeniedQueryException.php',
'AphrontBaseMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php',
'AphrontCharacterSetQueryException' => 'aphront/storage/exception/AphrontCharacterSetQueryException.php',
'AphrontConnectionLostQueryException' => 'aphront/storage/exception/AphrontConnectionLostQueryException.php',
'AphrontConnectionQueryException' => 'aphront/storage/exception/AphrontConnectionQueryException.php',
'AphrontCountQueryException' => 'aphront/storage/exception/AphrontCountQueryException.php',
'AphrontDatabaseConnection' => 'aphront/storage/connection/AphrontDatabaseConnection.php',
'AphrontDatabaseTransactionState' => 'aphront/storage/connection/AphrontDatabaseTransactionState.php',
'AphrontDeadlockQueryException' => 'aphront/storage/exception/AphrontDeadlockQueryException.php',
'AphrontDuplicateKeyQueryException' => 'aphront/storage/exception/AphrontDuplicateKeyQueryException.php',
'AphrontIsolatedDatabaseConnection' => 'aphront/storage/connection/AphrontIsolatedDatabaseConnection.php',
'AphrontLockTimeoutQueryException' => 'aphront/storage/exception/AphrontLockTimeoutQueryException.php',
'AphrontMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php',
'AphrontMySQLiDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php',
'AphrontNotSupportedQueryException' => 'aphront/storage/exception/AphrontNotSupportedQueryException.php',
'AphrontObjectMissingQueryException' => 'aphront/storage/exception/AphrontObjectMissingQueryException.php',
'AphrontParameterQueryException' => 'aphront/storage/exception/AphrontParameterQueryException.php',
'AphrontQueryException' => 'aphront/storage/exception/AphrontQueryException.php',
'AphrontRecoverableQueryException' => 'aphront/storage/exception/AphrontRecoverableQueryException.php',
'AphrontSchemaQueryException' => 'aphront/storage/exception/AphrontSchemaQueryException.php',
'AphrontScopedUnguardedWriteCapability' => 'aphront/writeguard/AphrontScopedUnguardedWriteCapability.php',
'AphrontWriteGuard' => 'aphront/writeguard/AphrontWriteGuard.php',
'BaseHTTPFuture' => 'future/http/BaseHTTPFuture.php',
'CaseInsensitiveArray' => 'utils/CaseInsensitiveArray.php',
'CaseInsensitiveArrayTestCase' => 'utils/__tests__/CaseInsensitiveArrayTestCase.php',
'CommandException' => 'future/exec/CommandException.php',
'ConduitClient' => 'conduit/ConduitClient.php',
'ConduitClientException' => 'conduit/ConduitClientException.php',
'ConduitClientTestCase' => 'conduit/__tests__/ConduitClientTestCase.php',
'ConduitFuture' => 'conduit/ConduitFuture.php',
'ExecFuture' => 'future/exec/ExecFuture.php',
'ExecFutureTestCase' => 'future/exec/__tests__/ExecFutureTestCase.php',
'ExecPassthruTestCase' => 'future/exec/__tests__/ExecPassthruTestCase.php',
'FileFinder' => 'filesystem/FileFinder.php',
'FileFinderTestCase' => 'filesystem/__tests__/FileFinderTestCase.php',
'FileList' => 'filesystem/FileList.php',
'Filesystem' => 'filesystem/Filesystem.php',
'FilesystemException' => 'filesystem/FilesystemException.php',
'FilesystemTestCase' => 'filesystem/__tests__/FilesystemTestCase.php',
'Future' => 'future/Future.php',
'FutureIterator' => 'future/FutureIterator.php',
'FutureIteratorTestCase' => 'future/__tests__/FutureIteratorTestCase.php',
'FutureProxy' => 'future/FutureProxy.php',
'HTTPFuture' => 'future/http/HTTPFuture.php',
'HTTPFutureCURLResponseStatus' => 'future/http/status/HTTPFutureCURLResponseStatus.php',
'HTTPFutureCertificateResponseStatus' => 'future/http/status/HTTPFutureCertificateResponseStatus.php',
'HTTPFutureHTTPResponseStatus' => 'future/http/status/HTTPFutureHTTPResponseStatus.php',
'HTTPFutureParseResponseStatus' => 'future/http/status/HTTPFutureParseResponseStatus.php',
'HTTPFutureResponseStatus' => 'future/http/status/HTTPFutureResponseStatus.php',
'HTTPFutureTransportResponseStatus' => 'future/http/status/HTTPFutureTransportResponseStatus.php',
'HTTPSFuture' => 'future/http/HTTPSFuture.php',
'ImmediateFuture' => 'future/ImmediateFuture.php',
'LibphutilUSEnglishTranslation' => 'internationalization/translation/LibphutilUSEnglishTranslation.php',
'LinesOfALarge' => 'filesystem/linesofalarge/LinesOfALarge.php',
'LinesOfALargeExecFuture' => 'filesystem/linesofalarge/LinesOfALargeExecFuture.php',
'LinesOfALargeExecFutureTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php',
'LinesOfALargeFile' => 'filesystem/linesofalarge/LinesOfALargeFile.php',
'LinesOfALargeFileTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php',
'MFilterTestHelper' => 'utils/__tests__/MFilterTestHelper.php',
'PHPASTParserTestCase' => 'parser/xhpast/__tests__/PHPASTParserTestCase.php',
'PhageAgentBootloader' => 'phage/bootloader/PhageAgentBootloader.php',
'PhageAgentTestCase' => 'phage/__tests__/PhageAgentTestCase.php',
'PhagePHPAgent' => 'phage/agent/PhagePHPAgent.php',
'PhagePHPAgentBootloader' => 'phage/bootloader/PhagePHPAgentBootloader.php',
'Phobject' => 'object/Phobject.php',
'PhobjectTestCase' => 'object/__tests__/PhobjectTestCase.php',
'PhutilAPCKeyValueCache' => 'cache/PhutilAPCKeyValueCache.php',
'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php',
'PhutilAWSException' => 'future/aws/PhutilAWSException.php',
'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php',
+ 'PhutilAWSManagementWorkflow' => 'future/aws/management/PhutilAWSManagementWorkflow.php',
+ 'PhutilAWSS3DeleteManagementWorkflow' => 'future/aws/management/PhutilAWSS3DeleteManagementWorkflow.php',
'PhutilAWSS3Future' => 'future/aws/PhutilAWSS3Future.php',
+ 'PhutilAWSS3GetManagementWorkflow' => 'future/aws/management/PhutilAWSS3GetManagementWorkflow.php',
+ 'PhutilAWSS3ManagementWorkflow' => 'future/aws/management/PhutilAWSS3ManagementWorkflow.php',
+ 'PhutilAWSS3PutManagementWorkflow' => 'future/aws/management/PhutilAWSS3PutManagementWorkflow.php',
+ 'PhutilAWSv4Signature' => 'future/aws/PhutilAWSv4Signature.php',
+ 'PhutilAWSv4SignatureTestCase' => 'future/aws/__tests__/PhutilAWSv4SignatureTestCase.php',
'PhutilAggregateException' => 'error/PhutilAggregateException.php',
'PhutilAllCapsEnglishLocale' => 'internationalization/locales/PhutilAllCapsEnglishLocale.php',
'PhutilAmazonAuthAdapter' => 'auth/PhutilAmazonAuthAdapter.php',
'PhutilArgumentParser' => 'parser/argument/PhutilArgumentParser.php',
'PhutilArgumentParserException' => 'parser/argument/exception/PhutilArgumentParserException.php',
'PhutilArgumentParserTestCase' => 'parser/argument/__tests__/PhutilArgumentParserTestCase.php',
'PhutilArgumentSpecification' => 'parser/argument/PhutilArgumentSpecification.php',
'PhutilArgumentSpecificationException' => 'parser/argument/exception/PhutilArgumentSpecificationException.php',
'PhutilArgumentSpecificationTestCase' => 'parser/argument/__tests__/PhutilArgumentSpecificationTestCase.php',
'PhutilArgumentUsageException' => 'parser/argument/exception/PhutilArgumentUsageException.php',
'PhutilArgumentWorkflow' => 'parser/argument/workflow/PhutilArgumentWorkflow.php',
'PhutilArray' => 'utils/PhutilArray.php',
'PhutilArrayTestCase' => 'utils/__tests__/PhutilArrayTestCase.php',
'PhutilArrayWithDefaultValue' => 'utils/PhutilArrayWithDefaultValue.php',
'PhutilAsanaAuthAdapter' => 'auth/PhutilAsanaAuthAdapter.php',
'PhutilAsanaFuture' => 'future/asana/PhutilAsanaFuture.php',
'PhutilAuthAdapter' => 'auth/PhutilAuthAdapter.php',
'PhutilAuthConfigurationException' => 'auth/exception/PhutilAuthConfigurationException.php',
'PhutilAuthCredentialException' => 'auth/exception/PhutilAuthCredentialException.php',
'PhutilAuthException' => 'auth/exception/PhutilAuthException.php',
'PhutilAuthUserAbortedException' => 'auth/exception/PhutilAuthUserAbortedException.php',
'PhutilBallOfPHP' => 'phage/util/PhutilBallOfPHP.php',
'PhutilBitbucketAuthAdapter' => 'auth/PhutilBitbucketAuthAdapter.php',
'PhutilBootloader' => 'moduleutils/PhutilBootloader.php',
'PhutilBootloaderException' => 'moduleutils/PhutilBootloaderException.php',
'PhutilBritishEnglishLocale' => 'internationalization/locales/PhutilBritishEnglishLocale.php',
'PhutilBufferedIterator' => 'utils/PhutilBufferedIterator.php',
'PhutilBufferedIteratorTestCase' => 'utils/__tests__/PhutilBufferedIteratorTestCase.php',
'PhutilBugtraqParser' => 'parser/PhutilBugtraqParser.php',
'PhutilBugtraqParserTestCase' => 'parser/__tests__/PhutilBugtraqParserTestCase.php',
'PhutilCIDRBlock' => 'ip/PhutilCIDRBlock.php',
'PhutilCIDRList' => 'ip/PhutilCIDRList.php',
'PhutilCLikeCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCLikeCodeSnippetContextFreeGrammar.php',
'PhutilCallbackFilterIterator' => 'utils/PhutilCallbackFilterIterator.php',
'PhutilChannel' => 'channel/PhutilChannel.php',
'PhutilChannelChannel' => 'channel/PhutilChannelChannel.php',
'PhutilChannelTestCase' => 'channel/__tests__/PhutilChannelTestCase.php',
'PhutilChunkedIterator' => 'utils/PhutilChunkedIterator.php',
'PhutilChunkedIteratorTestCase' => 'utils/__tests__/PhutilChunkedIteratorTestCase.php',
'PhutilClassMapQuery' => 'symbols/PhutilClassMapQuery.php',
'PhutilCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCodeSnippetContextFreeGrammar.php',
'PhutilCommandString' => 'xsprintf/PhutilCommandString.php',
'PhutilConsole' => 'console/PhutilConsole.php',
'PhutilConsoleBlock' => 'console/view/PhutilConsoleBlock.php',
'PhutilConsoleConcatenatedView' => 'console/view/PhutilConsoleConcatenatedView.php',
'PhutilConsoleFormatter' => 'console/PhutilConsoleFormatter.php',
'PhutilConsoleList' => 'console/view/PhutilConsoleList.php',
'PhutilConsoleMessage' => 'console/PhutilConsoleMessage.php',
'PhutilConsoleProgressBar' => 'console/PhutilConsoleProgressBar.php',
'PhutilConsoleServer' => 'console/PhutilConsoleServer.php',
'PhutilConsoleServerChannel' => 'console/PhutilConsoleServerChannel.php',
'PhutilConsoleStdinNotInteractiveException' => 'console/PhutilConsoleStdinNotInteractiveException.php',
'PhutilConsoleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php',
'PhutilConsoleTable' => 'console/view/PhutilConsoleTable.php',
'PhutilConsoleView' => 'console/view/PhutilConsoleView.php',
'PhutilConsoleWrapTestCase' => 'console/__tests__/PhutilConsoleWrapTestCase.php',
'PhutilContextFreeGrammar' => 'grammar/PhutilContextFreeGrammar.php',
'PhutilCowsay' => 'utils/PhutilCowsay.php',
'PhutilCowsayTestCase' => 'utils/__tests__/PhutilCowsayTestCase.php',
'PhutilCsprintfTestCase' => 'xsprintf/__tests__/PhutilCsprintfTestCase.php',
'PhutilCzechLocale' => 'internationalization/locales/PhutilCzechLocale.php',
'PhutilDaemon' => 'daemon/PhutilDaemon.php',
'PhutilDaemonHandle' => 'daemon/PhutilDaemonHandle.php',
'PhutilDaemonOverseer' => 'daemon/PhutilDaemonOverseer.php',
'PhutilDaemonOverseerModule' => 'daemon/PhutilDaemonOverseerModule.php',
'PhutilDefaultSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDefaultSyntaxHighlighter.php',
'PhutilDefaultSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php',
'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'markup/syntax/highlighter/pygments/PhutilDefaultSyntaxHighlighterEnginePygmentsFuture.php',
'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'markup/syntax/engine/__tests__/PhutilDefaultSyntaxHighlighterEngineTestCase.php',
'PhutilDeferredLog' => 'filesystem/PhutilDeferredLog.php',
'PhutilDeferredLogTestCase' => 'filesystem/__tests__/PhutilDeferredLogTestCase.php',
'PhutilDirectedScalarGraph' => 'utils/PhutilDirectedScalarGraph.php',
'PhutilDirectoryFixture' => 'filesystem/PhutilDirectoryFixture.php',
'PhutilDirectoryKeyValueCache' => 'cache/PhutilDirectoryKeyValueCache.php',
'PhutilDisqusAuthAdapter' => 'auth/PhutilDisqusAuthAdapter.php',
'PhutilDivinerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDivinerSyntaxHighlighter.php',
'PhutilDocblockParser' => 'parser/PhutilDocblockParser.php',
'PhutilDocblockParserTestCase' => 'parser/__tests__/PhutilDocblockParserTestCase.php',
'PhutilEditDistanceMatrix' => 'utils/PhutilEditDistanceMatrix.php',
'PhutilEditDistanceMatrixTestCase' => 'utils/__tests__/PhutilEditDistanceMatrixTestCase.php',
'PhutilEditorConfig' => 'parser/PhutilEditorConfig.php',
'PhutilEditorConfigTestCase' => 'parser/__tests__/PhutilEditorConfigTestCase.php',
'PhutilEmailAddress' => 'parser/PhutilEmailAddress.php',
'PhutilEmailAddressTestCase' => 'parser/__tests__/PhutilEmailAddressTestCase.php',
'PhutilEmptyAuthAdapter' => 'auth/PhutilEmptyAuthAdapter.php',
'PhutilErrorHandler' => 'error/PhutilErrorHandler.php',
'PhutilErrorHandlerTestCase' => 'error/__tests__/PhutilErrorHandlerTestCase.php',
'PhutilErrorTrap' => 'error/PhutilErrorTrap.php',
'PhutilEvent' => 'events/PhutilEvent.php',
'PhutilEventConstants' => 'events/constant/PhutilEventConstants.php',
'PhutilEventEngine' => 'events/PhutilEventEngine.php',
'PhutilEventListener' => 'events/PhutilEventListener.php',
'PhutilEventType' => 'events/constant/PhutilEventType.php',
'PhutilExampleBufferedIterator' => 'utils/PhutilExampleBufferedIterator.php',
'PhutilExcessiveServiceCallsDaemon' => 'daemon/torture/PhutilExcessiveServiceCallsDaemon.php',
'PhutilExecChannel' => 'channel/PhutilExecChannel.php',
'PhutilExecPassthru' => 'future/exec/PhutilExecPassthru.php',
'PhutilExecutionEnvironment' => 'utils/PhutilExecutionEnvironment.php',
'PhutilExtensionsTestCase' => 'moduleutils/__tests__/PhutilExtensionsTestCase.php',
'PhutilFacebookAuthAdapter' => 'auth/PhutilFacebookAuthAdapter.php',
'PhutilFatalDaemon' => 'daemon/torture/PhutilFatalDaemon.php',
'PhutilFileLock' => 'filesystem/PhutilFileLock.php',
'PhutilFileLockTestCase' => 'filesystem/__tests__/PhutilFileLockTestCase.php',
'PhutilFileTree' => 'filesystem/PhutilFileTree.php',
'PhutilGitHubAuthAdapter' => 'auth/PhutilGitHubAuthAdapter.php',
'PhutilGitURI' => 'parser/PhutilGitURI.php',
'PhutilGitURITestCase' => 'parser/__tests__/PhutilGitURITestCase.php',
'PhutilGoogleAuthAdapter' => 'auth/PhutilGoogleAuthAdapter.php',
'PhutilHangForeverDaemon' => 'daemon/torture/PhutilHangForeverDaemon.php',
'PhutilHelpArgumentWorkflow' => 'parser/argument/workflow/PhutilHelpArgumentWorkflow.php',
'PhutilHgsprintfTestCase' => 'xsprintf/__tests__/PhutilHgsprintfTestCase.php',
'PhutilHighIntensityIntervalDaemon' => 'daemon/torture/PhutilHighIntensityIntervalDaemon.php',
'PhutilINIParserException' => 'parser/exception/PhutilINIParserException.php',
'PhutilIPAddress' => 'ip/PhutilIPAddress.php',
'PhutilIPAddressTestCase' => 'ip/__tests__/PhutilIPAddressTestCase.php',
'PhutilInRequestKeyValueCache' => 'cache/PhutilInRequestKeyValueCache.php',
'PhutilInteractiveEditor' => 'console/PhutilInteractiveEditor.php',
'PhutilInvalidRuleParserGeneratorException' => 'parser/generator/exception/PhutilInvalidRuleParserGeneratorException.php',
'PhutilInvalidStateException' => 'exception/PhutilInvalidStateException.php',
'PhutilInvalidStateExceptionTestCase' => 'exception/__tests__/PhutilInvalidStateExceptionTestCase.php',
'PhutilInvisibleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilInvisibleSyntaxHighlighter.php',
'PhutilIrreducibleRuleParserGeneratorException' => 'parser/generator/exception/PhutilIrreducibleRuleParserGeneratorException.php',
'PhutilJIRAAuthAdapter' => 'auth/PhutilJIRAAuthAdapter.php',
'PhutilJSON' => 'parser/PhutilJSON.php',
'PhutilJSONFragmentLexer' => 'lexer/PhutilJSONFragmentLexer.php',
'PhutilJSONFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilJSONFragmentLexerHighlighterTestCase.php',
'PhutilJSONParser' => 'parser/PhutilJSONParser.php',
'PhutilJSONParserException' => 'parser/exception/PhutilJSONParserException.php',
'PhutilJSONParserTestCase' => 'parser/__tests__/PhutilJSONParserTestCase.php',
'PhutilJSONProtocolChannel' => 'channel/PhutilJSONProtocolChannel.php',
'PhutilJSONProtocolChannelTestCase' => 'channel/__tests__/PhutilJSONProtocolChannelTestCase.php',
'PhutilJSONTestCase' => 'parser/__tests__/PhutilJSONTestCase.php',
'PhutilJavaCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilJavaCodeSnippetContextFreeGrammar.php',
'PhutilKeyValueCache' => 'cache/PhutilKeyValueCache.php',
'PhutilKeyValueCacheNamespace' => 'cache/PhutilKeyValueCacheNamespace.php',
'PhutilKeyValueCacheProfiler' => 'cache/PhutilKeyValueCacheProfiler.php',
'PhutilKeyValueCacheProxy' => 'cache/PhutilKeyValueCacheProxy.php',
'PhutilKeyValueCacheStack' => 'cache/PhutilKeyValueCacheStack.php',
'PhutilKeyValueCacheTestCase' => 'cache/__tests__/PhutilKeyValueCacheTestCase.php',
'PhutilKoreanLocale' => 'internationalization/locales/PhutilKoreanLocale.php',
'PhutilLDAPAuthAdapter' => 'auth/PhutilLDAPAuthAdapter.php',
'PhutilLanguageGuesser' => 'parser/PhutilLanguageGuesser.php',
'PhutilLanguageGuesserTestCase' => 'parser/__tests__/PhutilLanguageGuesserTestCase.php',
'PhutilLexer' => 'lexer/PhutilLexer.php',
'PhutilLexerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php',
'PhutilLibraryConflictException' => 'moduleutils/PhutilLibraryConflictException.php',
'PhutilLibraryMapBuilder' => 'moduleutils/PhutilLibraryMapBuilder.php',
'PhutilLibraryTestCase' => '__tests__/PhutilLibraryTestCase.php',
'PhutilLipsumContextFreeGrammar' => 'grammar/PhutilLipsumContextFreeGrammar.php',
'PhutilLocale' => 'internationalization/PhutilLocale.php',
'PhutilLocaleTestCase' => 'internationalization/__tests__/PhutilLocaleTestCase.php',
'PhutilLock' => 'filesystem/PhutilLock.php',
'PhutilLockException' => 'filesystem/PhutilLockException.php',
'PhutilLogFileChannel' => 'channel/PhutilLogFileChannel.php',
'PhutilLunarPhase' => 'utils/PhutilLunarPhase.php',
'PhutilLunarPhaseTestCase' => 'utils/__tests__/PhutilLunarPhaseTestCase.php',
'PhutilMarkupEngine' => 'markup/PhutilMarkupEngine.php',
'PhutilMarkupTestCase' => 'markup/__tests__/PhutilMarkupTestCase.php',
'PhutilMemcacheKeyValueCache' => 'cache/PhutilMemcacheKeyValueCache.php',
'PhutilMethodNotImplementedException' => 'error/PhutilMethodNotImplementedException.php',
'PhutilMetricsChannel' => 'channel/PhutilMetricsChannel.php',
'PhutilMissingSymbolException' => 'symbols/exception/PhutilMissingSymbolException.php',
'PhutilModuleUtilsTestCase' => 'moduleutils/__tests__/PhutilModuleUtilsTestCase.php',
'PhutilNiceDaemon' => 'daemon/torture/PhutilNiceDaemon.php',
'PhutilNumber' => 'internationalization/PhutilNumber.php',
'PhutilOAuth1AuthAdapter' => 'auth/PhutilOAuth1AuthAdapter.php',
'PhutilOAuth1Future' => 'future/oauth/PhutilOAuth1Future.php',
'PhutilOAuth1FutureTestCase' => 'future/oauth/__tests__/PhutilOAuth1FutureTestCase.php',
'PhutilOAuthAuthAdapter' => 'auth/PhutilOAuthAuthAdapter.php',
'PhutilOnDiskKeyValueCache' => 'cache/PhutilOnDiskKeyValueCache.php',
'PhutilOpaqueEnvelope' => 'error/PhutilOpaqueEnvelope.php',
'PhutilOpaqueEnvelopeKey' => 'error/PhutilOpaqueEnvelopeKey.php',
'PhutilOpaqueEnvelopeTestCase' => 'error/__tests__/PhutilOpaqueEnvelopeTestCase.php',
'PhutilPHPCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilPHPCodeSnippetContextFreeGrammar.php',
'PhutilPHPFragmentLexer' => 'lexer/PhutilPHPFragmentLexer.php',
'PhutilPHPFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php',
'PhutilPHPFragmentLexerTestCase' => 'lexer/__tests__/PhutilPHPFragmentLexerTestCase.php',
'PhutilPHPObjectProtocolChannel' => 'channel/PhutilPHPObjectProtocolChannel.php',
'PhutilPHPObjectProtocolChannelTestCase' => 'channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php',
'PhutilParserGenerator' => 'parser/PhutilParserGenerator.php',
'PhutilParserGeneratorException' => 'parser/generator/exception/PhutilParserGeneratorException.php',
'PhutilParserGeneratorTestCase' => 'parser/__tests__/PhutilParserGeneratorTestCase.php',
'PhutilPayPalAPIFuture' => 'future/paypal/PhutilPayPalAPIFuture.php',
'PhutilPerson' => 'internationalization/PhutilPerson.php',
'PhutilPersonTest' => 'internationalization/__tests__/PhutilPersonTest.php',
'PhutilPersonaAuthAdapter' => 'auth/PhutilPersonaAuthAdapter.php',
'PhutilPhabricatorAuthAdapter' => 'auth/PhutilPhabricatorAuthAdapter.php',
'PhutilPhtTestCase' => 'internationalization/__tests__/PhutilPhtTestCase.php',
'PhutilPirateEnglishLocale' => 'internationalization/locales/PhutilPirateEnglishLocale.php',
'PhutilPregsprintfTestCase' => 'xsprintf/__tests__/PhutilPregsprintfTestCase.php',
'PhutilProcessGroupDaemon' => 'daemon/torture/PhutilProcessGroupDaemon.php',
'PhutilProtocolChannel' => 'channel/PhutilProtocolChannel.php',
'PhutilProxyException' => 'error/PhutilProxyException.php',
'PhutilPygmentsSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php',
'PhutilPythonFragmentLexer' => 'lexer/PhutilPythonFragmentLexer.php',
'PhutilQsprintfInterface' => 'xsprintf/PhutilQsprintfInterface.php',
'PhutilQueryStringParser' => 'parser/PhutilQueryStringParser.php',
'PhutilQueryStringParserTestCase' => 'parser/__tests__/PhutilQueryStringParserTestCase.php',
'PhutilRainbowSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php',
'PhutilRawEnglishLocale' => 'internationalization/locales/PhutilRawEnglishLocale.php',
'PhutilReadableSerializer' => 'readableserializer/PhutilReadableSerializer.php',
'PhutilReadableSerializerTestCase' => 'readableserializer/__tests__/PhutilReadableSerializerTestCase.php',
'PhutilRealNameContextFreeGrammar' => 'grammar/PhutilRealNameContextFreeGrammar.php',
'PhutilRemarkupBlockInterpreter' => 'markup/engine/remarkup/blockrule/PhutilRemarkupBlockInterpreter.php',
'PhutilRemarkupBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupBlockRule.php',
'PhutilRemarkupBlockStorage' => 'markup/engine/remarkup/PhutilRemarkupBlockStorage.php',
'PhutilRemarkupBoldRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupBoldRule.php',
'PhutilRemarkupCodeBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupCodeBlockRule.php',
'PhutilRemarkupDefaultBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupDefaultBlockRule.php',
'PhutilRemarkupDelRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupDelRule.php',
'PhutilRemarkupDocumentLinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupDocumentLinkRule.php',
'PhutilRemarkupEngine' => 'markup/engine/PhutilRemarkupEngine.php',
'PhutilRemarkupEngineTestCase' => 'markup/engine/__tests__/PhutilRemarkupEngineTestCase.php',
'PhutilRemarkupEscapeRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupEscapeRemarkupRule.php',
'PhutilRemarkupHeaderBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHeaderBlockRule.php',
'PhutilRemarkupHighlightRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php',
'PhutilRemarkupHorizontalRuleBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHorizontalRuleBlockRule.php',
'PhutilRemarkupHyperlinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRule.php',
'PhutilRemarkupInlineBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInlineBlockRule.php',
'PhutilRemarkupInterpreterBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInterpreterBlockRule.php',
'PhutilRemarkupItalicRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupItalicRule.php',
'PhutilRemarkupLinebreaksRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupLinebreaksRule.php',
'PhutilRemarkupListBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupListBlockRule.php',
'PhutilRemarkupLiteralBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupLiteralBlockRule.php',
'PhutilRemarkupMonospaceRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupMonospaceRule.php',
'PhutilRemarkupNoteBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupNoteBlockRule.php',
'PhutilRemarkupQuotesBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupQuotesBlockRule.php',
'PhutilRemarkupReplyBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupReplyBlockRule.php',
'PhutilRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRule.php',
'PhutilRemarkupSimpleTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupSimpleTableBlockRule.php',
'PhutilRemarkupTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupTableBlockRule.php',
'PhutilRemarkupTestInterpreterRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupTestInterpreterRule.php',
'PhutilRemarkupUnderlineRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupUnderlineRule.php',
'PhutilRope' => 'utils/PhutilRope.php',
'PhutilRopeTestCase' => 'utils/__tests__/PhutilRopeTestCase.php',
'PhutilSafeHTML' => 'markup/PhutilSafeHTML.php',
'PhutilSafeHTMLProducerInterface' => 'markup/PhutilSafeHTMLProducerInterface.php',
'PhutilSafeHTMLTestCase' => 'markup/__tests__/PhutilSafeHTMLTestCase.php',
'PhutilSaturateStdoutDaemon' => 'daemon/torture/PhutilSaturateStdoutDaemon.php',
'PhutilServiceProfiler' => 'serviceprofiler/PhutilServiceProfiler.php',
'PhutilShellLexer' => 'lexer/PhutilShellLexer.php',
'PhutilShellLexerTestCase' => 'lexer/__tests__/PhutilShellLexerTestCase.php',
'PhutilSimpleOptions' => 'parser/PhutilSimpleOptions.php',
'PhutilSimpleOptionsLexer' => 'lexer/PhutilSimpleOptionsLexer.php',
'PhutilSimpleOptionsLexerTestCase' => 'lexer/__tests__/PhutilSimpleOptionsLexerTestCase.php',
'PhutilSimpleOptionsTestCase' => 'parser/__tests__/PhutilSimpleOptionsTestCase.php',
'PhutilSocketChannel' => 'channel/PhutilSocketChannel.php',
'PhutilSprite' => 'sprites/PhutilSprite.php',
'PhutilSpriteSheet' => 'sprites/PhutilSpriteSheet.php',
'PhutilSymbolLoader' => 'symbols/PhutilSymbolLoader.php',
'PhutilSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilSyntaxHighlighter.php',
'PhutilSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilSyntaxHighlighterEngine.php',
'PhutilSyntaxHighlighterException' => 'markup/syntax/highlighter/PhutilSyntaxHighlighterException.php',
'PhutilSystem' => 'utils/PhutilSystem.php',
'PhutilSystemTestCase' => 'utils/__tests__/PhutilSystemTestCase.php',
'PhutilTerminalString' => 'xsprintf/PhutilTerminalString.php',
'PhutilTestPhobject' => 'object/__tests__/PhutilTestPhobject.php',
'PhutilTortureTestDaemon' => 'daemon/torture/PhutilTortureTestDaemon.php',
'PhutilTranslation' => 'internationalization/PhutilTranslation.php',
'PhutilTranslationTestCase' => 'internationalization/__tests__/PhutilTranslationTestCase.php',
'PhutilTranslator' => 'internationalization/PhutilTranslator.php',
'PhutilTranslatorTestCase' => 'internationalization/__tests__/PhutilTranslatorTestCase.php',
'PhutilTsprintfTestCase' => 'xsprintf/__tests__/PhutilTsprintfTestCase.php',
'PhutilTwitchAuthAdapter' => 'auth/PhutilTwitchAuthAdapter.php',
'PhutilTwitchFuture' => 'future/twitch/PhutilTwitchFuture.php',
'PhutilTwitterAuthAdapter' => 'auth/PhutilTwitterAuthAdapter.php',
'PhutilTypeCheckException' => 'parser/exception/PhutilTypeCheckException.php',
'PhutilTypeExtraParametersException' => 'parser/exception/PhutilTypeExtraParametersException.php',
'PhutilTypeLexer' => 'lexer/PhutilTypeLexer.php',
'PhutilTypeMissingParametersException' => 'parser/exception/PhutilTypeMissingParametersException.php',
'PhutilTypeSpec' => 'parser/PhutilTypeSpec.php',
'PhutilTypeSpecTestCase' => 'parser/__tests__/PhutilTypeSpecTestCase.php',
'PhutilURI' => 'parser/PhutilURI.php',
'PhutilURITestCase' => 'parser/__tests__/PhutilURITestCase.php',
'PhutilUSEnglishLocale' => 'internationalization/locales/PhutilUSEnglishLocale.php',
'PhutilUTF8StringTruncator' => 'utils/PhutilUTF8StringTruncator.php',
'PhutilUTF8TestCase' => 'utils/__tests__/PhutilUTF8TestCase.php',
'PhutilUnknownSymbolParserGeneratorException' => 'parser/generator/exception/PhutilUnknownSymbolParserGeneratorException.php',
'PhutilUnreachableRuleParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableRuleParserGeneratorException.php',
'PhutilUnreachableTerminalParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableTerminalParserGeneratorException.php',
'PhutilUrisprintfTestCase' => 'xsprintf/__tests__/PhutilUrisprintfTestCase.php',
'PhutilUtilsTestCase' => 'utils/__tests__/PhutilUtilsTestCase.php',
'PhutilVeryWowEnglishLocale' => 'internationalization/locales/PhutilVeryWowEnglishLocale.php',
'PhutilWordPressAuthAdapter' => 'auth/PhutilWordPressAuthAdapter.php',
'PhutilWordPressFuture' => 'future/wordpress/PhutilWordPressFuture.php',
'PhutilXHPASTBinary' => 'parser/xhpast/bin/PhutilXHPASTBinary.php',
'PhutilXHPASTSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php',
'PhutilXHPASTSyntaxHighlighterFuture' => 'markup/syntax/highlighter/xhpast/PhutilXHPASTSyntaxHighlighterFuture.php',
'PhutilXHPASTSyntaxHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php',
'QueryFuture' => 'future/query/QueryFuture.php',
'TempFile' => 'filesystem/TempFile.php',
'TestAbstractDirectedGraph' => 'utils/__tests__/TestAbstractDirectedGraph.php',
'XHPASTNode' => 'parser/xhpast/api/XHPASTNode.php',
'XHPASTNodeTestCase' => 'parser/xhpast/api/__tests__/XHPASTNodeTestCase.php',
'XHPASTSyntaxErrorException' => 'parser/xhpast/api/XHPASTSyntaxErrorException.php',
'XHPASTToken' => 'parser/xhpast/api/XHPASTToken.php',
'XHPASTTree' => 'parser/xhpast/api/XHPASTTree.php',
'XHPASTTreeTestCase' => 'parser/xhpast/api/__tests__/XHPASTTreeTestCase.php',
'XsprintfUnknownConversionException' => 'xsprintf/exception/XsprintfUnknownConversionException.php',
),
'function' => array(
'array_fuse' => 'utils/utils.php',
'array_interleave' => 'utils/utils.php',
'array_mergev' => 'utils/utils.php',
'array_select_keys' => 'utils/utils.php',
'assert_instances_of' => 'utils/utils.php',
'assert_stringlike' => 'utils/utils.php',
'coalesce' => 'utils/utils.php',
'csprintf' => 'xsprintf/csprintf.php',
'exec_manual' => 'future/exec/execx.php',
'execx' => 'future/exec/execx.php',
'head' => 'utils/utils.php',
'head_key' => 'utils/utils.php',
'hgsprintf' => 'xsprintf/hgsprintf.php',
'hsprintf' => 'markup/render.php',
'id' => 'utils/utils.php',
'idx' => 'utils/utils.php',
'ifilter' => 'utils/utils.php',
'igroup' => 'utils/utils.php',
'ipull' => 'utils/utils.php',
'isort' => 'utils/utils.php',
'jsprintf' => 'xsprintf/jsprintf.php',
'last' => 'utils/utils.php',
'last_key' => 'utils/utils.php',
'ldap_sprintf' => 'xsprintf/ldapsprintf.php',
'mfilter' => 'utils/utils.php',
'mgroup' => 'utils/utils.php',
'mpull' => 'utils/utils.php',
'msort' => 'utils/utils.php',
'newv' => 'utils/utils.php',
'nonempty' => 'utils/utils.php',
'phlog' => 'error/phlog.php',
'pht' => 'internationalization/pht.php',
'phutil_censor_credentials' => 'utils/utils.php',
'phutil_console_confirm' => 'console/format.php',
'phutil_console_format' => 'console/format.php',
'phutil_console_get_terminal_width' => 'console/format.php',
'phutil_console_prompt' => 'console/format.php',
'phutil_console_require_tty' => 'console/format.php',
'phutil_console_wrap' => 'console/format.php',
'phutil_count' => 'internationalization/pht.php',
'phutil_date_format' => 'utils/viewutils.php',
'phutil_deprecated' => 'moduleutils/moduleutils.php',
'phutil_error_listener_example' => 'error/phlog.php',
'phutil_escape_html' => 'markup/render.php',
'phutil_escape_html_newlines' => 'markup/render.php',
'phutil_escape_uri' => 'markup/render.php',
'phutil_escape_uri_path_component' => 'markup/render.php',
'phutil_fnmatch' => 'utils/utils.php',
'phutil_format_bytes' => 'utils/viewutils.php',
'phutil_format_relative_time' => 'utils/viewutils.php',
'phutil_format_relative_time_detailed' => 'utils/viewutils.php',
'phutil_format_units_generic' => 'utils/viewutils.php',
'phutil_fwrite_nonblocking_stream' => 'utils/utils.php',
'phutil_get_current_library_name' => 'moduleutils/moduleutils.php',
'phutil_get_library_name_for_root' => 'moduleutils/moduleutils.php',
'phutil_get_library_root' => 'moduleutils/moduleutils.php',
'phutil_get_library_root_for_path' => 'moduleutils/moduleutils.php',
'phutil_get_signal_name' => 'future/exec/execx.php',
'phutil_hashes_are_identical' => 'utils/utils.php',
'phutil_implode_html' => 'markup/render.php',
'phutil_ini_decode' => 'utils/utils.php',
'phutil_is_hiphop_runtime' => 'utils/utils.php',
'phutil_is_utf8' => 'utils/utf8.php',
'phutil_is_utf8_slowly' => 'utils/utf8.php',
'phutil_is_utf8_with_only_bmp_characters' => 'utils/utf8.php',
'phutil_is_windows' => 'utils/utils.php',
'phutil_json_decode' => 'utils/utils.php',
'phutil_json_encode' => 'utils/utils.php',
'phutil_load_library' => 'moduleutils/core.php',
'phutil_loggable_string' => 'utils/utils.php',
'phutil_parse_bytes' => 'utils/viewutils.php',
'phutil_passthru' => 'future/exec/execx.php',
'phutil_register_library' => 'moduleutils/core.php',
'phutil_register_library_map' => 'moduleutils/core.php',
'phutil_safe_html' => 'markup/render.php',
'phutil_split_lines' => 'utils/utils.php',
'phutil_tag' => 'markup/render.php',
'phutil_tag_div' => 'markup/render.php',
'phutil_unescape_uri_path_component' => 'markup/render.php',
'phutil_units' => 'utils/utils.php',
'phutil_utf8_console_strlen' => 'utils/utf8.php',
'phutil_utf8_convert' => 'utils/utf8.php',
'phutil_utf8_hard_wrap' => 'utils/utf8.php',
'phutil_utf8_hard_wrap_html' => 'utils/utf8.php',
'phutil_utf8_is_combining_character' => 'utils/utf8.php',
'phutil_utf8_strlen' => 'utils/utf8.php',
'phutil_utf8_strtolower' => 'utils/utf8.php',
'phutil_utf8_strtoupper' => 'utils/utf8.php',
'phutil_utf8_strtr' => 'utils/utf8.php',
'phutil_utf8_ucwords' => 'utils/utf8.php',
'phutil_utf8ize' => 'utils/utf8.php',
'phutil_utf8v' => 'utils/utf8.php',
'phutil_utf8v_codepoints' => 'utils/utf8.php',
'phutil_utf8v_combine_characters' => 'utils/utf8.php',
'phutil_utf8v_combined' => 'utils/utf8.php',
'phutil_validate_json' => 'utils/utils.php',
'phutil_var_export' => 'utils/utils.php',
'ppull' => 'utils/utils.php',
'pregsprintf' => 'xsprintf/pregsprintf.php',
'qsprintf' => 'xsprintf/qsprintf.php',
'qsprintf_check_scalar_type' => 'xsprintf/qsprintf.php',
'qsprintf_check_type' => 'xsprintf/qsprintf.php',
'queryfx' => 'xsprintf/queryfx.php',
'queryfx_all' => 'xsprintf/queryfx.php',
'queryfx_one' => 'xsprintf/queryfx.php',
'tsprintf' => 'xsprintf/tsprintf.php',
'urisprintf' => 'xsprintf/urisprintf.php',
'vcsprintf' => 'xsprintf/csprintf.php',
'vjsprintf' => 'xsprintf/jsprintf.php',
'vqsprintf' => 'xsprintf/qsprintf.php',
'vurisprintf' => 'xsprintf/urisprintf.php',
'xhp_parser_node_constants' => 'parser/xhpast/parser_nodes.php',
'xhpast_parser_token_constants' => 'parser/xhpast/parser_tokens.php',
'xsprintf' => 'xsprintf/xsprintf.php',
'xsprintf_callback_example' => 'xsprintf/xsprintf.php',
'xsprintf_command' => 'xsprintf/csprintf.php',
'xsprintf_javascript' => 'xsprintf/jsprintf.php',
'xsprintf_ldap' => 'xsprintf/ldapsprintf.php',
'xsprintf_mercurial' => 'xsprintf/hgsprintf.php',
'xsprintf_query' => 'xsprintf/qsprintf.php',
'xsprintf_regex' => 'xsprintf/pregsprintf.php',
'xsprintf_terminal' => 'xsprintf/tsprintf.php',
'xsprintf_uri' => 'xsprintf/urisprintf.php',
),
'xmap' => array(
'AASTNode' => 'Phobject',
'AASTNodeList' => array(
'Phobject',
'Countable',
'Iterator',
),
'AASTToken' => 'Phobject',
'AASTTree' => 'Phobject',
'AbstractDirectedGraph' => 'Phobject',
'AbstractDirectedGraphTestCase' => 'PhutilTestCase',
'AphrontAccessDeniedQueryException' => 'AphrontRecoverableQueryException',
'AphrontBaseMySQLDatabaseConnection' => 'AphrontDatabaseConnection',
'AphrontCharacterSetQueryException' => 'AphrontQueryException',
'AphrontConnectionLostQueryException' => 'AphrontRecoverableQueryException',
'AphrontConnectionQueryException' => 'AphrontQueryException',
'AphrontCountQueryException' => 'AphrontQueryException',
'AphrontDatabaseConnection' => array(
'Phobject',
'PhutilQsprintfInterface',
),
'AphrontDatabaseTransactionState' => 'Phobject',
'AphrontDeadlockQueryException' => 'AphrontRecoverableQueryException',
'AphrontDuplicateKeyQueryException' => 'AphrontQueryException',
'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection',
'AphrontLockTimeoutQueryException' => 'AphrontRecoverableQueryException',
'AphrontMySQLDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
'AphrontMySQLiDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
'AphrontNotSupportedQueryException' => 'AphrontQueryException',
'AphrontObjectMissingQueryException' => 'AphrontQueryException',
'AphrontParameterQueryException' => 'AphrontQueryException',
'AphrontQueryException' => 'Exception',
'AphrontRecoverableQueryException' => 'AphrontQueryException',
'AphrontSchemaQueryException' => 'AphrontQueryException',
'AphrontScopedUnguardedWriteCapability' => 'Phobject',
'AphrontWriteGuard' => 'Phobject',
'BaseHTTPFuture' => 'Future',
'CaseInsensitiveArray' => 'PhutilArray',
'CaseInsensitiveArrayTestCase' => 'PhutilTestCase',
'CommandException' => 'Exception',
'ConduitClient' => 'Phobject',
'ConduitClientException' => 'Exception',
'ConduitClientTestCase' => 'PhutilTestCase',
'ConduitFuture' => 'FutureProxy',
'ExecFuture' => 'Future',
'ExecFutureTestCase' => 'PhutilTestCase',
'ExecPassthruTestCase' => 'PhutilTestCase',
'FileFinder' => 'Phobject',
'FileFinderTestCase' => 'PhutilTestCase',
'FileList' => 'Phobject',
'Filesystem' => 'Phobject',
'FilesystemException' => 'Exception',
'FilesystemTestCase' => 'PhutilTestCase',
'Future' => 'Phobject',
'FutureIterator' => array(
'Phobject',
'Iterator',
),
'FutureIteratorTestCase' => 'PhutilTestCase',
'FutureProxy' => 'Future',
'HTTPFuture' => 'BaseHTTPFuture',
'HTTPFutureCURLResponseStatus' => 'HTTPFutureResponseStatus',
'HTTPFutureCertificateResponseStatus' => 'HTTPFutureResponseStatus',
'HTTPFutureHTTPResponseStatus' => 'HTTPFutureResponseStatus',
'HTTPFutureParseResponseStatus' => 'HTTPFutureResponseStatus',
'HTTPFutureResponseStatus' => 'Exception',
'HTTPFutureTransportResponseStatus' => 'HTTPFutureResponseStatus',
'HTTPSFuture' => 'BaseHTTPFuture',
'ImmediateFuture' => 'Future',
'LibphutilUSEnglishTranslation' => 'PhutilTranslation',
'LinesOfALarge' => array(
'Phobject',
'Iterator',
),
'LinesOfALargeExecFuture' => 'LinesOfALarge',
'LinesOfALargeExecFutureTestCase' => 'PhutilTestCase',
'LinesOfALargeFile' => 'LinesOfALarge',
'LinesOfALargeFileTestCase' => 'PhutilTestCase',
'MFilterTestHelper' => 'Phobject',
'PHPASTParserTestCase' => 'PhutilTestCase',
'PhageAgentBootloader' => 'Phobject',
'PhageAgentTestCase' => 'PhutilTestCase',
'PhagePHPAgent' => 'Phobject',
'PhagePHPAgentBootloader' => 'PhageAgentBootloader',
'Phobject' => 'Iterator',
'PhobjectTestCase' => 'PhutilTestCase',
'PhutilAPCKeyValueCache' => 'PhutilKeyValueCache',
'PhutilAWSEC2Future' => 'PhutilAWSFuture',
'PhutilAWSException' => 'Exception',
'PhutilAWSFuture' => 'FutureProxy',
+ 'PhutilAWSManagementWorkflow' => 'PhutilArgumentWorkflow',
+ 'PhutilAWSS3DeleteManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
'PhutilAWSS3Future' => 'PhutilAWSFuture',
+ 'PhutilAWSS3GetManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
+ 'PhutilAWSS3ManagementWorkflow' => 'PhutilAWSManagementWorkflow',
+ 'PhutilAWSS3PutManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
+ 'PhutilAWSv4Signature' => 'Phobject',
+ 'PhutilAWSv4SignatureTestCase' => 'PhutilTestCase',
'PhutilAggregateException' => 'Exception',
'PhutilAllCapsEnglishLocale' => 'PhutilLocale',
'PhutilAmazonAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilArgumentParser' => 'Phobject',
'PhutilArgumentParserException' => 'Exception',
'PhutilArgumentParserTestCase' => 'PhutilTestCase',
'PhutilArgumentSpecification' => 'Phobject',
'PhutilArgumentSpecificationException' => 'PhutilArgumentParserException',
'PhutilArgumentSpecificationTestCase' => 'PhutilTestCase',
'PhutilArgumentUsageException' => 'PhutilArgumentParserException',
'PhutilArgumentWorkflow' => 'Phobject',
'PhutilArray' => array(
'Phobject',
'Countable',
'ArrayAccess',
'Iterator',
),
'PhutilArrayTestCase' => 'PhutilTestCase',
'PhutilArrayWithDefaultValue' => 'PhutilArray',
'PhutilAsanaAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilAsanaFuture' => 'FutureProxy',
'PhutilAuthAdapter' => 'Phobject',
'PhutilAuthConfigurationException' => 'PhutilAuthException',
'PhutilAuthCredentialException' => 'PhutilAuthException',
'PhutilAuthException' => 'Exception',
'PhutilAuthUserAbortedException' => 'PhutilAuthException',
'PhutilBallOfPHP' => 'Phobject',
'PhutilBitbucketAuthAdapter' => 'PhutilOAuth1AuthAdapter',
'PhutilBootloaderException' => 'Exception',
'PhutilBritishEnglishLocale' => 'PhutilLocale',
'PhutilBufferedIterator' => array(
'Phobject',
'Iterator',
),
'PhutilBufferedIteratorTestCase' => 'PhutilTestCase',
'PhutilBugtraqParser' => 'Phobject',
'PhutilBugtraqParserTestCase' => 'PhutilTestCase',
'PhutilCIDRBlock' => 'Phobject',
'PhutilCIDRList' => 'Phobject',
'PhutilCLikeCodeSnippetContextFreeGrammar' => 'PhutilCodeSnippetContextFreeGrammar',
'PhutilCallbackFilterIterator' => 'FilterIterator',
'PhutilChannel' => 'Phobject',
'PhutilChannelChannel' => 'PhutilChannel',
'PhutilChannelTestCase' => 'PhutilTestCase',
'PhutilChunkedIterator' => array(
'Phobject',
'Iterator',
),
'PhutilChunkedIteratorTestCase' => 'PhutilTestCase',
'PhutilClassMapQuery' => 'Phobject',
'PhutilCodeSnippetContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhutilCommandString' => 'Phobject',
'PhutilConsole' => 'Phobject',
'PhutilConsoleBlock' => 'PhutilConsoleView',
'PhutilConsoleConcatenatedView' => 'PhutilConsoleView',
'PhutilConsoleFormatter' => 'Phobject',
'PhutilConsoleList' => 'PhutilConsoleView',
'PhutilConsoleMessage' => 'Phobject',
'PhutilConsoleProgressBar' => 'Phobject',
'PhutilConsoleServer' => 'Phobject',
'PhutilConsoleServerChannel' => 'PhutilChannelChannel',
'PhutilConsoleStdinNotInteractiveException' => 'Exception',
'PhutilConsoleSyntaxHighlighter' => 'Phobject',
'PhutilConsoleTable' => 'PhutilConsoleView',
'PhutilConsoleView' => 'Phobject',
'PhutilConsoleWrapTestCase' => 'PhutilTestCase',
'PhutilContextFreeGrammar' => 'Phobject',
'PhutilCowsay' => 'Phobject',
'PhutilCowsayTestCase' => 'PhutilTestCase',
'PhutilCsprintfTestCase' => 'PhutilTestCase',
'PhutilCzechLocale' => 'PhutilLocale',
'PhutilDaemon' => 'Phobject',
'PhutilDaemonHandle' => 'Phobject',
'PhutilDaemonOverseer' => 'Phobject',
'PhutilDaemonOverseerModule' => 'Phobject',
'PhutilDefaultSyntaxHighlighter' => 'Phobject',
'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine',
'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'FutureProxy',
'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'PhutilTestCase',
'PhutilDeferredLog' => 'Phobject',
'PhutilDeferredLogTestCase' => 'PhutilTestCase',
'PhutilDirectedScalarGraph' => 'AbstractDirectedGraph',
'PhutilDirectoryFixture' => 'Phobject',
'PhutilDirectoryKeyValueCache' => 'PhutilKeyValueCache',
'PhutilDisqusAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilDivinerSyntaxHighlighter' => 'Phobject',
'PhutilDocblockParser' => 'Phobject',
'PhutilDocblockParserTestCase' => 'PhutilTestCase',
'PhutilEditDistanceMatrix' => 'Phobject',
'PhutilEditDistanceMatrixTestCase' => 'PhutilTestCase',
'PhutilEditorConfig' => 'Phobject',
'PhutilEditorConfigTestCase' => 'PhutilTestCase',
'PhutilEmailAddress' => 'Phobject',
'PhutilEmailAddressTestCase' => 'PhutilTestCase',
'PhutilEmptyAuthAdapter' => 'PhutilAuthAdapter',
'PhutilErrorHandler' => 'Phobject',
'PhutilErrorHandlerTestCase' => 'PhutilTestCase',
'PhutilErrorTrap' => 'Phobject',
'PhutilEvent' => 'Phobject',
'PhutilEventConstants' => 'Phobject',
'PhutilEventEngine' => 'Phobject',
'PhutilEventListener' => 'Phobject',
'PhutilEventType' => 'PhutilEventConstants',
'PhutilExampleBufferedIterator' => 'PhutilBufferedIterator',
'PhutilExcessiveServiceCallsDaemon' => 'PhutilTortureTestDaemon',
'PhutilExecChannel' => 'PhutilChannel',
'PhutilExecPassthru' => 'Phobject',
'PhutilExecutionEnvironment' => 'Phobject',
'PhutilExtensionsTestCase' => 'PhutilTestCase',
'PhutilFacebookAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilFatalDaemon' => 'PhutilTortureTestDaemon',
'PhutilFileLock' => 'PhutilLock',
'PhutilFileLockTestCase' => 'PhutilTestCase',
'PhutilFileTree' => 'Phobject',
'PhutilGitHubAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilGitURI' => 'Phobject',
'PhutilGitURITestCase' => 'PhutilTestCase',
'PhutilGoogleAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon',
'PhutilHelpArgumentWorkflow' => 'PhutilArgumentWorkflow',
'PhutilHgsprintfTestCase' => 'PhutilTestCase',
'PhutilHighIntensityIntervalDaemon' => 'PhutilTortureTestDaemon',
'PhutilINIParserException' => 'Exception',
'PhutilIPAddress' => 'Phobject',
'PhutilIPAddressTestCase' => 'PhutilTestCase',
'PhutilInRequestKeyValueCache' => 'PhutilKeyValueCache',
'PhutilInteractiveEditor' => 'Phobject',
'PhutilInvalidRuleParserGeneratorException' => 'PhutilParserGeneratorException',
'PhutilInvalidStateException' => 'Exception',
'PhutilInvalidStateExceptionTestCase' => 'PhutilTestCase',
'PhutilInvisibleSyntaxHighlighter' => 'Phobject',
'PhutilIrreducibleRuleParserGeneratorException' => 'PhutilParserGeneratorException',
'PhutilJIRAAuthAdapter' => 'PhutilOAuth1AuthAdapter',
'PhutilJSON' => 'Phobject',
'PhutilJSONFragmentLexer' => 'PhutilLexer',
'PhutilJSONFragmentLexerHighlighterTestCase' => 'PhutilTestCase',
'PhutilJSONParser' => 'Phobject',
'PhutilJSONParserException' => 'Exception',
'PhutilJSONParserTestCase' => 'PhutilTestCase',
'PhutilJSONProtocolChannel' => 'PhutilProtocolChannel',
'PhutilJSONProtocolChannelTestCase' => 'PhutilTestCase',
'PhutilJSONTestCase' => 'PhutilTestCase',
'PhutilJavaCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar',
'PhutilKeyValueCache' => 'Phobject',
'PhutilKeyValueCacheNamespace' => 'PhutilKeyValueCacheProxy',
'PhutilKeyValueCacheProfiler' => 'PhutilKeyValueCacheProxy',
'PhutilKeyValueCacheProxy' => 'PhutilKeyValueCache',
'PhutilKeyValueCacheStack' => 'PhutilKeyValueCache',
'PhutilKeyValueCacheTestCase' => 'PhutilTestCase',
'PhutilKoreanLocale' => 'PhutilLocale',
'PhutilLDAPAuthAdapter' => 'PhutilAuthAdapter',
'PhutilLanguageGuesser' => 'Phobject',
'PhutilLanguageGuesserTestCase' => 'PhutilTestCase',
'PhutilLexer' => 'Phobject',
'PhutilLexerSyntaxHighlighter' => 'PhutilSyntaxHighlighter',
'PhutilLibraryConflictException' => 'Exception',
'PhutilLibraryMapBuilder' => 'Phobject',
'PhutilLibraryTestCase' => 'PhutilTestCase',
'PhutilLipsumContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhutilLocale' => 'Phobject',
'PhutilLocaleTestCase' => 'PhutilTestCase',
'PhutilLock' => 'Phobject',
'PhutilLockException' => 'Exception',
'PhutilLogFileChannel' => 'PhutilChannelChannel',
'PhutilLunarPhase' => 'Phobject',
'PhutilLunarPhaseTestCase' => 'PhutilTestCase',
'PhutilMarkupEngine' => 'Phobject',
'PhutilMarkupTestCase' => 'PhutilTestCase',
'PhutilMemcacheKeyValueCache' => 'PhutilKeyValueCache',
'PhutilMethodNotImplementedException' => 'Exception',
'PhutilMetricsChannel' => 'PhutilChannelChannel',
'PhutilMissingSymbolException' => 'Exception',
'PhutilModuleUtilsTestCase' => 'PhutilTestCase',
'PhutilNiceDaemon' => 'PhutilTortureTestDaemon',
'PhutilNumber' => 'Phobject',
'PhutilOAuth1AuthAdapter' => 'PhutilAuthAdapter',
'PhutilOAuth1Future' => 'FutureProxy',
'PhutilOAuth1FutureTestCase' => 'PhutilTestCase',
'PhutilOAuthAuthAdapter' => 'PhutilAuthAdapter',
'PhutilOnDiskKeyValueCache' => 'PhutilKeyValueCache',
'PhutilOpaqueEnvelope' => 'Phobject',
'PhutilOpaqueEnvelopeKey' => 'Phobject',
'PhutilOpaqueEnvelopeTestCase' => 'PhutilTestCase',
'PhutilPHPCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar',
'PhutilPHPFragmentLexer' => 'PhutilLexer',
'PhutilPHPFragmentLexerHighlighterTestCase' => 'PhutilTestCase',
'PhutilPHPFragmentLexerTestCase' => 'PhutilTestCase',
'PhutilPHPObjectProtocolChannel' => 'PhutilProtocolChannel',
'PhutilPHPObjectProtocolChannelTestCase' => 'PhutilTestCase',
'PhutilParserGenerator' => 'Phobject',
'PhutilParserGeneratorException' => 'Exception',
'PhutilParserGeneratorTestCase' => 'PhutilTestCase',
'PhutilPayPalAPIFuture' => 'FutureProxy',
'PhutilPersonTest' => array(
'Phobject',
'PhutilPerson',
),
'PhutilPersonaAuthAdapter' => 'PhutilAuthAdapter',
'PhutilPhabricatorAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilPhtTestCase' => 'PhutilTestCase',
'PhutilPirateEnglishLocale' => 'PhutilLocale',
'PhutilPregsprintfTestCase' => 'PhutilTestCase',
'PhutilProcessGroupDaemon' => 'PhutilTortureTestDaemon',
'PhutilProtocolChannel' => 'PhutilChannelChannel',
'PhutilProxyException' => 'Exception',
'PhutilPygmentsSyntaxHighlighter' => 'Phobject',
'PhutilPythonFragmentLexer' => 'PhutilLexer',
'PhutilQueryStringParser' => 'Phobject',
'PhutilQueryStringParserTestCase' => 'PhutilTestCase',
'PhutilRainbowSyntaxHighlighter' => 'Phobject',
'PhutilRawEnglishLocale' => 'PhutilLocale',
'PhutilReadableSerializer' => 'Phobject',
'PhutilReadableSerializerTestCase' => 'PhutilTestCase',
'PhutilRealNameContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhutilRemarkupBlockInterpreter' => 'Phobject',
'PhutilRemarkupBlockRule' => 'Phobject',
'PhutilRemarkupBlockStorage' => 'Phobject',
'PhutilRemarkupBoldRule' => 'PhutilRemarkupRule',
'PhutilRemarkupCodeBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupDefaultBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupDelRule' => 'PhutilRemarkupRule',
'PhutilRemarkupDocumentLinkRule' => 'PhutilRemarkupRule',
'PhutilRemarkupEngine' => 'PhutilMarkupEngine',
'PhutilRemarkupEngineTestCase' => 'PhutilTestCase',
'PhutilRemarkupEscapeRemarkupRule' => 'PhutilRemarkupRule',
'PhutilRemarkupHeaderBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupHighlightRule' => 'PhutilRemarkupRule',
'PhutilRemarkupHorizontalRuleBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupHyperlinkRule' => 'PhutilRemarkupRule',
'PhutilRemarkupInlineBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupInterpreterBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupItalicRule' => 'PhutilRemarkupRule',
'PhutilRemarkupLinebreaksRule' => 'PhutilRemarkupRule',
'PhutilRemarkupListBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupLiteralBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupMonospaceRule' => 'PhutilRemarkupRule',
'PhutilRemarkupNoteBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupQuotesBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupReplyBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupRule' => 'Phobject',
'PhutilRemarkupSimpleTableBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupTableBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupTestInterpreterRule' => 'PhutilRemarkupBlockInterpreter',
'PhutilRemarkupUnderlineRule' => 'PhutilRemarkupRule',
'PhutilRope' => 'Phobject',
'PhutilRopeTestCase' => 'PhutilTestCase',
'PhutilSafeHTML' => 'Phobject',
'PhutilSafeHTMLTestCase' => 'PhutilTestCase',
'PhutilSaturateStdoutDaemon' => 'PhutilTortureTestDaemon',
'PhutilServiceProfiler' => 'Phobject',
'PhutilShellLexer' => 'PhutilLexer',
'PhutilShellLexerTestCase' => 'PhutilTestCase',
'PhutilSimpleOptions' => 'Phobject',
'PhutilSimpleOptionsLexer' => 'PhutilLexer',
'PhutilSimpleOptionsLexerTestCase' => 'PhutilTestCase',
'PhutilSimpleOptionsTestCase' => 'PhutilTestCase',
'PhutilSocketChannel' => 'PhutilChannel',
'PhutilSprite' => 'Phobject',
'PhutilSpriteSheet' => 'Phobject',
'PhutilSyntaxHighlighter' => 'Phobject',
'PhutilSyntaxHighlighterEngine' => 'Phobject',
'PhutilSyntaxHighlighterException' => 'Exception',
'PhutilSystem' => 'Phobject',
'PhutilSystemTestCase' => 'PhutilTestCase',
'PhutilTerminalString' => 'Phobject',
'PhutilTestPhobject' => 'Phobject',
'PhutilTortureTestDaemon' => 'PhutilDaemon',
'PhutilTranslation' => 'Phobject',
'PhutilTranslationTestCase' => 'PhutilTestCase',
'PhutilTranslator' => 'Phobject',
'PhutilTranslatorTestCase' => 'PhutilTestCase',
'PhutilTsprintfTestCase' => 'PhutilTestCase',
'PhutilTwitchAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilTwitchFuture' => 'FutureProxy',
'PhutilTwitterAuthAdapter' => 'PhutilOAuth1AuthAdapter',
'PhutilTypeCheckException' => 'Exception',
'PhutilTypeExtraParametersException' => 'Exception',
'PhutilTypeLexer' => 'PhutilLexer',
'PhutilTypeMissingParametersException' => 'Exception',
'PhutilTypeSpec' => 'Phobject',
'PhutilTypeSpecTestCase' => 'PhutilTestCase',
'PhutilURI' => 'Phobject',
'PhutilURITestCase' => 'PhutilTestCase',
'PhutilUSEnglishLocale' => 'PhutilLocale',
'PhutilUTF8StringTruncator' => 'Phobject',
'PhutilUTF8TestCase' => 'PhutilTestCase',
'PhutilUnknownSymbolParserGeneratorException' => 'PhutilParserGeneratorException',
'PhutilUnreachableRuleParserGeneratorException' => 'PhutilParserGeneratorException',
'PhutilUnreachableTerminalParserGeneratorException' => 'PhutilParserGeneratorException',
'PhutilUrisprintfTestCase' => 'PhutilTestCase',
'PhutilUtilsTestCase' => 'PhutilTestCase',
'PhutilVeryWowEnglishLocale' => 'PhutilLocale',
'PhutilWordPressAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilWordPressFuture' => 'FutureProxy',
'PhutilXHPASTBinary' => 'Phobject',
'PhutilXHPASTSyntaxHighlighter' => 'Phobject',
'PhutilXHPASTSyntaxHighlighterFuture' => 'FutureProxy',
'PhutilXHPASTSyntaxHighlighterTestCase' => 'PhutilTestCase',
'QueryFuture' => 'Future',
'TempFile' => 'Phobject',
'TestAbstractDirectedGraph' => 'AbstractDirectedGraph',
'XHPASTNode' => 'AASTNode',
'XHPASTNodeTestCase' => 'PhutilTestCase',
'XHPASTSyntaxErrorException' => 'Exception',
'XHPASTToken' => 'AASTToken',
'XHPASTTree' => 'AASTTree',
'XHPASTTreeTestCase' => 'PhutilTestCase',
'XsprintfUnknownConversionException' => 'InvalidArgumentException',
),
));
diff --git a/src/future/aws/PhutilAWSFuture.php b/src/future/aws/PhutilAWSFuture.php
index 6d0b9799..54432d52 100644
--- a/src/future/aws/PhutilAWSFuture.php
+++ b/src/future/aws/PhutilAWSFuture.php
@@ -1,147 +1,168 @@
awsAccessKey = $access;
- $this->awsPrivateKey = $private;
+ public function setAccessKey($access_key) {
+ $this->accessKey = $access_key;
return $this;
}
- public function getAWSAccessKey() {
- return $this->awsAccessKey;
+ public function getAccessKey() {
+ return $this->accessKey;
}
- public function getAWSPrivateKey() {
- return $this->awsPrivateKey;
+ public function setSecretKey(PhutilOpaqueEnvelope $secret_key) {
+ $this->secretKey = $secret_key;
+ return $this;
+ }
+
+ public function getSecretKey() {
+ return $this->secretKey;
}
- public function getAWSRegion() {
- return $this->awsRegion;
+ public function getRegion() {
+ return $this->region;
+ }
+
+ public function setRegion($region) {
+ $this->region = $region;
+ return $this;
}
- public function setAWSRegion($region) {
- $this->awsRegion = $region;
+ public function setEndpoint($endpoint) {
+ $this->endpoint = $endpoint;
return $this;
}
- public function getHost() {
- $host = $this->getServiceName().'.'.$this->awsRegion.'.amazonaws.com';
- return $host;
+ public function getEndpoint() {
+ return $this->endpoint;
}
- public function setRawAWSQuery($action, array $params = array()) {
- $this->params = $params;
- $this->params['Action'] = $action;
+ public function setHTTPMethod($method) {
+ $this->httpMethod = $method;
return $this;
}
- protected function getProxiedFuture() {
- if (!$this->future) {
- $params = $this->params;
+ public function getHTTPMethod() {
+ return $this->httpMethod;
+ }
- if (!$this->params) {
- throw new Exception(
- pht(
- 'You must %s!',
- 'setRawAWSQuery()'));
- }
+ public function setPath($path) {
+ $this->path = $path;
+ return $this;
+ }
- if (!$this->getAWSAccessKey()) {
- throw new Exception(
- pht(
- 'You must %s!',
- 'setAWSKeys()'));
- }
+ public function getPath() {
+ return $this->path;
+ }
+
+ public function setData($data) {
+ $this->data = $data;
+ return $this;
+ }
+
+ public function getData() {
+ return $this->data;
+ }
+
+ protected function getParameters() {
+ $params = $this->params;
+ return $params;
+ }
- $params['AWSAccessKeyId'] = $this->getAWSAccessKey();
- $params['Version'] = '2013-10-15';
- $params['Timestamp'] = date('c');
+ public function addHeader($key, $value) {
+ $this->headers[] = array($key, $value);
+ return $this;
+ }
- $params = $this->sign($params);
+ protected function getProxiedFuture() {
+ if (!$this->future) {
+ $params = $this->getParameters();
+ $method = $this->getHTTPMethod();
+ $host = $this->getEndpoint();
+ $path = $this->getPath();
+ $data = $this->getData();
+
+ $uri = id(new PhutilURI("https://{$host}/"))
+ ->setPath($path)
+ ->setQueryParams($params);
+
+ $future = id(new HTTPSFuture($uri, $data))
+ ->setMethod($method);
+
+ foreach ($this->headers as $header) {
+ list($key, $value) = $header;
+ $future->addHeader($key, $value);
+ }
- $uri = new PhutilURI('http://'.$this->getHost().'/');
- $uri->setQueryParams($params);
+ $this->signRequest($future);
- $this->future = new HTTPFuture($uri);
+ $this->future = $future;
}
return $this->future;
}
+ protected function signRequest(HTTPSFuture $future) {
+ $access_key = $this->getAccessKey();
+ $secret_key = $this->getSecretKey();
+
+ $region = $this->getRegion();
+
+ id(new PhutilAWSv4Signature())
+ ->setRegion($region)
+ ->setService($this->getServiceName())
+ ->setAccessKey($access_key)
+ ->setSecretKey($secret_key)
+ ->signRequest($future);
+ }
+
protected function didReceiveResult($result) {
list($status, $body, $headers) = $result;
try {
$xml = @(new SimpleXMLElement($body));
} catch (Exception $ex) {
$xml = null;
}
if ($status->isError() || !$xml) {
if (!($status instanceof HTTPFutureHTTPResponseStatus)) {
throw $status;
}
$params = array(
'body' => $body,
);
if ($xml) {
$params['RequestID'] = $xml->RequestID[0];
- foreach ($xml->Errors[0] as $error) {
+ $errors = array($xml->Error);
+ foreach ($errors as $error) {
$params['Errors'][] = array($error->Code, $error->Message);
}
}
throw new PhutilAWSException($status->getStatusCode(), $params);
}
return $xml;
}
- /**
- * http://bit.ly/wU0JFh
- */
- private function sign(array $params) {
-
- $params['SignatureMethod'] = 'HmacSHA256';
- $params['SignatureVersion'] = '2';
-
- ksort($params);
-
- $pstr = array();
- foreach ($params as $key => $value) {
- $pstr[] = rawurlencode($key).'='.rawurlencode($value);
- }
- $pstr = implode('&', $pstr);
-
- $sign = "GET"."\n".
- strtolower($this->getHost())."\n".
- "/"."\n".
- $pstr;
-
- $hash = hash_hmac(
- 'sha256',
- $sign,
- $this->getAWSPrivateKey(),
- $raw_ouput = true);
-
- $params['Signature'] = base64_encode($hash);
-
- return $params;
- }
-
}
diff --git a/src/future/aws/PhutilAWSS3Future.php b/src/future/aws/PhutilAWSS3Future.php
index b3e7c3eb..6941829e 100644
--- a/src/future/aws/PhutilAWSS3Future.php
+++ b/src/future/aws/PhutilAWSS3Future.php
@@ -1,9 +1,66 @@
bucket = $bucket;
+ return $this;
+ }
+
+ public function getBucket() {
+ return $this->bucket;
+ }
+
+ public function setParametersForGetObject($key) {
+ $bucket = $this->getBucket();
+
+ $this->setHTTPMethod('GET');
+ $this->setPath($bucket.'/'.$key);
+
+ return $this;
+ }
+
+ public function setParametersForPutObject($key, $value) {
+ $bucket = $this->getBucket();
+
+ $this->setHTTPMethod('PUT');
+ $this->setPath($bucket.'/'.$key);
+
+ $this->addHeader('X-Amz-ACL', 'private');
+ $this->addHeader('Content-Type', 'application/octet-stream');
+
+ $this->setData($value);
+
+ return $this;
+ }
+
+ public function setParametersForDeleteObject($key) {
+ $bucket = $this->getBucket();
+
+ $this->setHTTPMethod('DELETE');
+ $this->setPath($bucket.'/'.$key);
+
+ return $this;
+ }
+
+ protected function didReceiveResult($result) {
+ list($status, $body, $headers) = $result;
+
+ if (!$status->isError()) {
+ return $body;
+ }
+
+ if ($status->getStatusCode() === 404) {
+ return null;
+ }
+
+ return parent::didReceiveResult($result);
+ }
+
}
diff --git a/src/future/aws/PhutilAWSv4Signature.php b/src/future/aws/PhutilAWSv4Signature.php
new file mode 100644
index 00000000..50248b02
--- /dev/null
+++ b/src/future/aws/PhutilAWSv4Signature.php
@@ -0,0 +1,256 @@
+accessKey = $access_key;
+ return $this;
+ }
+
+ public function setSecretKey(PhutilOpaqueEnvelope $secret_key) {
+ $this->secretKey = $secret_key;
+ return $this;
+ }
+
+ public function setDate($date) {
+ $this->date = $date;
+ return $this;
+ }
+
+ public function getDate() {
+ if ($this->date === null) {
+ $this->date = gmdate('Ymd\THis\Z', time());
+ }
+ return $this->date;
+ }
+
+ public function setRegion($region) {
+ $this->region = $region;
+ return $this;
+ }
+
+ public function getRegion() {
+ return $this->region;
+ }
+
+ public function setService($service) {
+ $this->service = $service;
+ return $this;
+ }
+
+ public function getService() {
+ return $this->service;
+ }
+
+ public function setSigningKey($signing_key) {
+ $this->signingKey = $signing_key;
+ return $this;
+ }
+
+ public function getSigningKey() {
+ if ($this->signingKey === null) {
+ $this->signingKey = $this->computeSigningKey();
+ }
+
+ return $this->signingKey;
+ }
+
+ private function getAlgorithm() {
+ return 'AWS4-HMAC-SHA256';
+ }
+
+ private function getHost(HTTPSFuture $future) {
+ $uri = new PhutilURI($future->getURI());
+ return $uri->getDomain();
+ }
+
+ private function getPath(HTTPSFuture $future) {
+ $uri = new PhutilURI($future->getURI());
+ return $uri->getPath();
+ }
+
+ public function signRequest(HTTPSFuture $future) {
+ $body_signature = $this->getBodySignature($future);
+
+ $future->addHeader('X-Amz-Content-sha256', $body_signature);
+ $future->addHeader('X-Amz-Date', $this->getDate());
+
+ $request_signature = $this->getCanonicalRequestSignature(
+ $future,
+ $body_signature);
+
+ $string_to_sign = $this->getStringToSign($request_signature);
+
+ $signing_key = $this->getSigningKey();
+
+ $signature = hash_hmac('sha256', $string_to_sign, $signing_key);
+
+ $algorithm = $this->getAlgorithm();
+ $credential = $this->getCredential();
+ $signed_headers = $this->getSignedHeaderList($future);
+
+ $authorization =
+ $algorithm.' '.
+ 'Credential='.$credential.','.
+ 'SignedHeaders='.$signed_headers.','.
+ 'Signature='.$signature;
+
+ $future->addHeader('Authorization', $authorization);
+
+ return $future;
+ }
+
+ private function getBodySignature(HTTPSFuture $future) {
+ $http_body = $future->getData();
+
+ if (is_array($http_body)) {
+ $http_body = '';
+ }
+
+ return hash('sha256', $http_body);
+ }
+
+ private function getCanonicalRequestSignature(
+ HTTPSFuture $future,
+ $body_signature) {
+
+ $http_method = $future->getMethod();
+
+ $path = $this->getPath($future);
+ $path = rawurlencode($path);
+ $path = str_replace('%2F', '/', $path);
+
+ $canonical_parameters = $this->getCanonicalParameterList($future);
+ $canonical_headers = $this->getCanonicalHeaderList($future);
+ $signed_headers = $this->getSignedHeaderList($future);
+
+ $canonical_request =
+ $http_method."\n".
+ $path."\n".
+ $canonical_parameters."\n".
+ $canonical_headers."\n".
+ "\n".
+ $signed_headers."\n".
+ $body_signature;
+
+ return hash('sha256', $canonical_request);
+ }
+
+ private function getStringToSign($request_signature) {
+ $algorithm = $this->getAlgorithm();
+ $date = $this->getDate();
+ $scope_parts = $this->getScopeParts();
+ $scope = implode('/', $scope_parts);
+
+ $string_to_sign =
+ $algorithm."\n".
+ $date."\n".
+ $scope."\n".
+ $request_signature;
+
+ return $string_to_sign;
+ }
+
+ private function getScopeParts() {
+ return array(
+ substr($this->getDate(), 0, 8),
+ $this->getRegion(),
+ $this->getService(),
+ 'aws4_request',
+ );
+ }
+
+ private function computeSigningKey() {
+ $secret_key = $this->secretKey;
+ if (!$secret_key) {
+ throw new Exception(
+ pht(
+ 'You must either provide a signing key with setSigningKey(), or '.
+ 'provide a secret key with setSecretKey().'));
+ }
+
+ // NOTE: This part of the algorithm uses the raw binary hashes, and the
+ // result is not human-readable.
+ $raw_hash = true;
+
+ $signing_key = 'AWS4'.$secret_key->openEnvelope();
+
+ $scope_parts = $this->getScopeParts();
+ foreach ($scope_parts as $scope_part) {
+ $signing_key = hash_hmac('sha256', $scope_part, $signing_key, $raw_hash);
+ }
+
+ return $signing_key;
+ }
+
+ private function getCanonicalHeaderList(HTTPSFuture $future) {
+ $headers = $this->getCanonicalHeaderMap($future);
+
+ $canonical_headers = array();
+ foreach ($headers as $header => $header_value) {
+ $canonical_headers[] = $header.':'.trim($header_value);
+ }
+
+ return implode("\n", $canonical_headers);
+ }
+
+ private function getCanonicalHeaderMap(HTTPSFuture $future) {
+ $headers = $future->getHeaders();
+ $headers[] = array(
+ 'Host',
+ $this->getHost($future),
+ );
+
+ $header_map = array();
+ foreach ($headers as $header) {
+ list($key, $value) = $header;
+ $key = phutil_utf8_strtolower($key);
+ $header_map[$key] = $value;
+ }
+
+ ksort($header_map);
+
+ return $header_map;
+ }
+
+ private function getSignedHeaderList(HTTPSFuture $future) {
+ $headers = $this->getCanonicalHeaderMap($future);
+ return implode(';', array_keys($headers));
+ }
+
+ private function getCanonicalParameterList(HTTPSFuture $future) {
+ $uri = new PhutilURI($future->getURI());
+ $params = $uri->getQueryParams();
+
+ ksort($params);
+ $canonical_parameters = array();
+ foreach ($params as $key => $value) {
+ $canonical_parameters[] = rawurlencode($key).'='.rawurlencode($value);
+ }
+ $canonical_parameters = implode('&', $canonical_parameters);
+
+ return $canonical_parameters;
+ }
+
+ private function getCredential() {
+ $access_key = $this->accessKey;
+ if (!strlen($access_key)) {
+ throw new PhutilInvalidStateException('setAccessKey');
+ }
+
+ $parts = $this->getScopeParts();
+ array_unshift($parts, $access_key);
+
+ return implode('/', $parts);
+ }
+
+}
diff --git a/src/future/aws/__tests__/PhutilAWSv4SignatureTestCase.php b/src/future/aws/__tests__/PhutilAWSv4SignatureTestCase.php
new file mode 100644
index 00000000..f09dbf1e
--- /dev/null
+++ b/src/future/aws/__tests__/PhutilAWSv4SignatureTestCase.php
@@ -0,0 +1,159 @@
+setMethod($method)
+ ->addHeader('Range', 'bytes=0-9');
+
+ $signature = id(new PhutilAWSv4Signature())
+ ->setAccessKey($access_key)
+ ->setSecretKey(new PhutilOpaqueEnvelope($secret_key))
+ ->setDate($date)
+ ->setRegion($region)
+ ->setService($service);
+
+ $signature->signRequest($future);
+
+ $expect = <<assertSignature($expect, $future);
+ }
+
+
+ public function testAWSv4SignaturesS3PutObject() {
+ $access_key = 'AKIAIOSFODNN7EXAMPLE';
+ $secret_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
+ $date = '20130524T000000Z';
+ $region = 'us-east-1';
+ $service = 's3';
+ $uri = 'https://examplebucket.s3.amazonaws.com/test$file.text';
+ $method = 'PUT';
+ $body = 'Welcome to Amazon S3.';
+
+ $future = id(new HTTPSFuture($uri, $body))
+ ->setMethod($method)
+ ->addHeader('X-Amz-Storage-Class', 'REDUCED_REDUNDANCY')
+ ->addHeader('Date', 'Fri, 24 May 2013 00:00:00 GMT');
+
+ $signature = id(new PhutilAWSv4Signature())
+ ->setAccessKey($access_key)
+ ->setSecretKey(new PhutilOpaqueEnvelope($secret_key))
+ ->setDate($date)
+ ->setRegion($region)
+ ->setService($service);
+
+ $signature->signRequest($future);
+
+ $expect = <<assertSignature($expect, $future);
+ }
+
+
+ public function testAWSv4SignaturesS3GetBucketLifecycle() {
+ $access_key = 'AKIAIOSFODNN7EXAMPLE';
+ $secret_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
+ $date = '20130524T000000Z';
+ $region = 'us-east-1';
+ $service = 's3';
+ $uri = 'https://examplebucket.s3.amazonaws.com/?lifecycle';
+ $method = 'GET';
+
+ $future = id(new HTTPSFuture($uri))
+ ->setMethod($method);
+
+ $signature = id(new PhutilAWSv4Signature())
+ ->setAccessKey($access_key)
+ ->setSecretKey(new PhutilOpaqueEnvelope($secret_key))
+ ->setDate($date)
+ ->setRegion($region)
+ ->setService($service);
+
+ $signature->signRequest($future);
+
+ $expect = <<assertSignature($expect, $future);
+ }
+
+
+ public function testAWSv4SignaturesS3GetBucket() {
+ $access_key = 'AKIAIOSFODNN7EXAMPLE';
+ $secret_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
+ $date = '20130524T000000Z';
+ $region = 'us-east-1';
+ $service = 's3';
+ $uri = 'https://examplebucket.s3.amazonaws.com/?max-keys=2&prefix=J';
+ $method = 'GET';
+
+ $future = id(new HTTPSFuture($uri))
+ ->setMethod($method);
+
+ $signature = id(new PhutilAWSv4Signature())
+ ->setAccessKey($access_key)
+ ->setSecretKey(new PhutilOpaqueEnvelope($secret_key))
+ ->setDate($date)
+ ->setRegion($region)
+ ->setService($service);
+
+ $signature->signRequest($future);
+
+ $expect = <<assertSignature($expect, $future);
+ }
+
+
+ private function assertSignature($expect, HTTPSFuture $signed) {
+ $authorization = null;
+ foreach ($signed->getHeaders() as $header) {
+ list($key, $value) = $header;
+ if (phutil_utf8_strtolower($key) === 'authorization') {
+ $authorization = $value;
+ break;
+ }
+ }
+
+ $expect = str_replace("\n\n", ' ', $expect);
+ $expect = str_replace("\n", '', $expect);
+
+ $this->assertEqual($expect, $authorization);
+ }
+
+
+}
diff --git a/src/future/aws/management/PhutilAWSManagementWorkflow.php b/src/future/aws/management/PhutilAWSManagementWorkflow.php
new file mode 100644
index 00000000..b7f56617
--- /dev/null
+++ b/src/future/aws/management/PhutilAWSManagementWorkflow.php
@@ -0,0 +1,83 @@
+getArgv();
+
+ $access_key = $argv->getArg('access-key');
+ $secret_key = $argv->getArg('secret-key');
+
+ $has_root = (strlen($access_key) || strlen($secret_key));
+ if ($has_root) {
+ if (!strlen($access_key) || !strlen($secret_key)) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'When specifying AWS credentials with --access-key and '.
+ '--secret-key, you must provide both keys.'));
+ }
+
+ $template->setAccessKey($access_key);
+ $template->setSecretKey(new PhutilOpaqueEnvelope($secret_key));
+ }
+
+ $has_any = ($has_root);
+ if (!$has_any) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'You must specify AWS credentials. Use --access-key and '.
+ '--secret-key to provide root credentials.'));
+ }
+
+ $region = $argv->getArg('region');
+ if (!strlen($region)) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'You must specify an AWS region with --region.'));
+ }
+
+ $template->setRegion($region);
+
+ $endpoint = $argv->getArg('endpoint');
+ if (!strlen($endpoint)) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'You must specify an AWS endpoint with --endpoint.'));
+ }
+
+ $template->setEndpoint($endpoint);
+
+ return $template;
+ }
+
+ protected function getAWSArguments() {
+ return array(
+ array(
+ 'name' => 'access-key',
+ 'param' => 'key',
+ 'help' => pht('AWS access key.'),
+ ),
+ array(
+ 'name' => 'secret-key',
+ 'param' => 'file',
+ 'help' => pht('AWS secret key.'),
+ ),
+ array(
+ 'name' => 'region',
+ 'param' => 'region',
+ 'help' => pht('AWS region.'),
+ ),
+ array(
+ 'name' => 'endpoint',
+ 'param' => 'endpoint',
+ 'help' => pht('Name of the AWS region to access.'),
+ ),
+ );
+ }
+
+}
diff --git a/src/future/aws/management/PhutilAWSS3DeleteManagementWorkflow.php b/src/future/aws/management/PhutilAWSS3DeleteManagementWorkflow.php
new file mode 100644
index 00000000..f2f1e8d6
--- /dev/null
+++ b/src/future/aws/management/PhutilAWSS3DeleteManagementWorkflow.php
@@ -0,0 +1,45 @@
+setName('delete')
+ ->setExamples(
+ '**delete** --key __key__')
+ ->setSynopsis(pht('Delete an object from S3.'))
+ ->setArguments(
+ array_merge(
+ $this->getAWSArguments(),
+ $this->getAWSS3BucketArguments(),
+ array(
+ array(
+ 'name' => 'key',
+ 'param' => 'key',
+ 'help' => pht('Specify a key to delete.'),
+ ),
+ )));
+ }
+
+ public function execute(PhutilArgumentParser $args) {
+ $key = $args->getArg('key');
+ if (!strlen($key)) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Specify an AWS S3 object key to access with --key.'));
+ }
+
+ $future = $this->newAWSFuture(new PhutilAWSS3Future())
+ ->setParametersForDeleteObject($key);
+
+ $future->resolve();
+
+ echo tsprintf(
+ "%s\n",
+ pht('Deleted "%s".', $key));
+
+ return 0;
+ }
+
+}
diff --git a/src/future/aws/management/PhutilAWSS3GetManagementWorkflow.php b/src/future/aws/management/PhutilAWSS3GetManagementWorkflow.php
new file mode 100644
index 00000000..fab9af10
--- /dev/null
+++ b/src/future/aws/management/PhutilAWSS3GetManagementWorkflow.php
@@ -0,0 +1,57 @@
+setName('get')
+ ->setExamples(
+ '**get** --key __key__')
+ ->setSynopsis(pht('Download content from S3.'))
+ ->setArguments(
+ array_merge(
+ $this->getAWSArguments(),
+ $this->getAWSS3BucketArguments(),
+ array(
+ array(
+ 'name' => 'key',
+ 'param' => 'key',
+ 'help' => pht('Specify a key to retrieve.'),
+ ),
+ )));
+ }
+
+ public function execute(PhutilArgumentParser $args) {
+ $bucket = $args->getArg('bucket');
+ if (!strlen($bucket)) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Specify an AWS S3 bucket to access with --bucket.'));
+ }
+
+ $endpoint = $args->getArg('endpoint');
+ if (!strlen($endpoint)) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Specify an AWS S3 endpoint with --endpoint.'));
+ }
+
+ $key = $args->getArg('key');
+ if (!strlen($key)) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Specify an AWS S3 object key to access with --key.'));
+ }
+
+ $future = $this->newAWSFuture(new PhutilAWSS3Future())
+ ->setBucket($bucket)
+ ->setEndpoint($endpoint)
+ ->setParametersForGetObject($key);
+
+ echo $future->resolve();
+
+ return 0;
+ }
+
+}
diff --git a/src/future/aws/management/PhutilAWSS3ManagementWorkflow.php b/src/future/aws/management/PhutilAWSS3ManagementWorkflow.php
new file mode 100644
index 00000000..35d32538
--- /dev/null
+++ b/src/future/aws/management/PhutilAWSS3ManagementWorkflow.php
@@ -0,0 +1,27 @@
+ 'bucket',
+ 'param' => 'bucket',
+ 'help' => pht('Name of the S3 bucket to access.'),
+ ),
+ );
+ }
+
+ protected function newAWSFuture($future) {
+ $future = parent::newAWSFuture($future);
+
+ $argv = $this->getArgv();
+ $bucket = $argv->getArg('bucket');
+
+ $future->setBucket($bucket);
+
+ return $future;
+ }
+
+}
diff --git a/src/future/aws/management/PhutilAWSS3PutManagementWorkflow.php b/src/future/aws/management/PhutilAWSS3PutManagementWorkflow.php
new file mode 100644
index 00000000..199bf125
--- /dev/null
+++ b/src/future/aws/management/PhutilAWSS3PutManagementWorkflow.php
@@ -0,0 +1,52 @@
+setName('put')
+ ->setExamples(
+ '**put** --key __key__')
+ ->setSynopsis(pht('Upload content to S3.'))
+ ->setArguments(
+ array_merge(
+ $this->getAWSArguments(),
+ $this->getAWSS3BucketArguments(),
+ array(
+ array(
+ 'name' => 'key',
+ 'param' => 'key',
+ 'help' => pht('Specify a key to upload.'),
+ ),
+ )));
+ }
+
+ public function execute(PhutilArgumentParser $args) {
+ $key = $args->getArg('key');
+ if (!strlen($key)) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Specify an AWS S3 object key to access with --key.'));
+ }
+
+ $future = $this->newAWSFuture(new PhutilAWSS3Future());
+
+ echo tsprintf(
+ "%s\n",
+ pht('Reading data from stdin...'));
+
+ $data = file_get_contents('php://stdin');
+
+ $future->setParametersForPutObject($key, $data);
+
+ $result = $future->resolve();
+
+ echo tsprintf(
+ "%s\n",
+ pht('Uploaded "%s".', $key));
+
+ return 0;
+ }
+
+}
diff --git a/src/markup/engine/__tests__/remarkup/highlight.txt b/src/markup/engine/__tests__/remarkup/highlight.txt
index e0c78eed..5fb8895d 100644
--- a/src/markup/engine/__tests__/remarkup/highlight.txt
+++ b/src/markup/engine/__tests__/remarkup/highlight.txt
@@ -1,7 +1,9 @@
how about we !!highlight!! some !!TEXT!!!
wow this must be **!!very important!!**
+omg!!!!!
~~~~~~~~~~
how about we some
-wow this must be
+wow this must be
+omg!!!!!
~~~~~~~~~~
-how about we !!highlight!! some !!TEXT!!! wow this must be **!!very important!!**
+how about we !!highlight!! some !!TEXT!!! wow this must be **!!very important!!** omg!!!!!
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php
index 9abbb9bb..900e355c 100644
--- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php
+++ b/src/markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php
@@ -1,30 +1,37 @@
getEngine()->isTextMode()) {
return $text;
}
return $this->replaceHTML(
'@!!(.+?)(!{2,})@',
array($this, 'applyCallback'),
$text);
}
protected function applyCallback(array $matches) {
// Remove the two exclamation points that represent syntax.
$excitement = substr($matches[2], 2);
+ // If the internal content consists of ONLY exclamation points, leave it
+ // untouched so "!!!!!" is five exclamation points instead of one
+ // highlighted exclamation point.
+ if (preg_match('/^!+\z/', $matches[1])) {
+ return $matches[0];
+ }
+
// $excitement now has two fewer !'s than we started with.
return hsprintf('',
$matches[1], $excitement);
}
}
diff --git a/src/parser/argument/PhutilArgumentParser.php b/src/parser/argument/PhutilArgumentParser.php
index 7a4ae934..706be52a 100644
--- a/src/parser/argument/PhutilArgumentParser.php
+++ b/src/parser/argument/PhutilArgumentParser.php
@@ -1,825 +1,827 @@
setTagline('make an new dog')
* $args->setSynopsis(<<parse(
* array(
* array(
* 'name' => 'name',
* 'param' => 'dogname',
* 'default' => 'Rover',
* 'help' => 'Set the dog\'s name. By default, the dog will be '.
* 'named "Rover".',
* ),
* array(
* 'name' => 'big',
* 'short' => 'b',
* 'help' => 'If set, create a large dog.',
* ),
* ));
*
* $dog_name = $args->getArg('name');
* $dog_size = $args->getArg('big') ? 'big' : 'small';
*
* // ... etc ...
*
* (For detailed documentation on supported keys in argument specifications,
* see @{class:PhutilArgumentSpecification}.)
*
* This will handle argument parsing, and generate appropriate usage help if
* the user provides an unsupported flag. @{class:PhutilArgumentParser} also
* supports some builtin "standard" arguments:
*
* $args->parseStandardArguments();
*
* See @{method:parseStandardArguments} for details. Notably, this includes
* a "--help" flag, and an "--xprofile" flag for profiling command-line scripts.
*
* Normally, when the parser encounters an unknown flag, it will exit with
* an error. However, you can use @{method:parsePartial} to consume only a
* set of flags:
*
* $args->parsePartial($spec_list);
*
* This allows you to parse some flags before making decisions about other
* parsing, or share some flags across scripts. The builtin standard arguments
* are implemented in this way.
*
* There is also builtin support for "workflows", which allow you to build a
* script that operates in several modes (e.g., by accepting commands like
* `install`, `upgrade`, etc), like `arc` does. For detailed documentation on
* workflows, see @{class:PhutilArgumentWorkflow}.
*
* @task parse Parsing Arguments
* @task read Reading Arguments
* @task help Command Help
* @task internal Internals
*/
final class PhutilArgumentParser extends Phobject {
private $bin;
private $argv;
private $specs = array();
private $results = array();
private $parsed;
private $tagline;
private $synopsis;
private $workflows;
private $showHelp;
const PARSE_ERROR_CODE = 77;
/* -( Parsing Arguments )-------------------------------------------------- */
/**
* Build a new parser. Generally, you start a script with:
*
* $args = new PhutilArgumentParser($argv);
*
* @param list Argument vector to parse, generally the $argv global.
* @task parse
*/
public function __construct(array $argv) {
$this->bin = $argv[0];
$this->argv = array_slice($argv, 1);
}
/**
* Parse and consume a list of arguments, removing them from the argument
* vector but leaving unparsed arguments for later consumption. You can
* retrieve unconsumed arguments directly with
* @{method:getUnconsumedArgumentVector}. Doing a partial parse can make it
* easier to share common flags across scripts or workflows.
*
* @param list List of argument specs, see
* @{class:PhutilArgumentSpecification}.
* @return this
* @task parse
*/
public function parsePartial(array $specs) {
$specs = PhutilArgumentSpecification::newSpecsFromList($specs);
$this->mergeSpecs($specs);
$specs_by_name = mpull($specs, null, 'getName');
$specs_by_short = mpull($specs, null, 'getShortAlias');
unset($specs_by_short[null]);
$argv = $this->argv;
$len = count($argv);
for ($ii = 0; $ii < $len; $ii++) {
$arg = $argv[$ii];
$map = null;
if (!is_string($arg)) {
// Non-string argument; pass it through as-is.
} else if ($arg == '--') {
// This indicates "end of flags".
break;
} else if ($arg == '-') {
// This is a normal argument (e.g., stdin).
continue;
} else if (!strncmp('--', $arg, 2)) {
$pre = '--';
$arg = substr($arg, 2);
$map = $specs_by_name;
} else if (!strncmp('-', $arg, 1) && strlen($arg) > 1) {
$pre = '-';
$arg = substr($arg, 1);
$map = $specs_by_short;
}
if ($map) {
$val = null;
$parts = explode('=', $arg, 2);
if (count($parts) == 2) {
list($arg, $val) = $parts;
}
if (isset($map[$arg])) {
$spec = $map[$arg];
unset($argv[$ii]);
$param_name = $spec->getParamName();
if ($val !== null) {
if ($param_name === null) {
throw new PhutilArgumentUsageException(
pht(
"Argument '%s' does not take a parameter.",
"{$pre}{$arg}"));
}
} else {
if ($param_name !== null) {
if ($ii + 1 < $len) {
$val = $argv[$ii + 1];
unset($argv[$ii + 1]);
$ii++;
} else {
throw new PhutilArgumentUsageException(
pht(
"Argument '%s' requires a parameter.",
"{$pre}{$arg}"));
}
} else {
$val = true;
}
}
if (!$spec->getRepeatable()) {
if (array_key_exists($spec->getName(), $this->results)) {
throw new PhutilArgumentUsageException(
pht(
"Argument '%s' was provided twice.",
"{$pre}{$arg}"));
}
}
$conflicts = $spec->getConflicts();
foreach ($conflicts as $conflict => $reason) {
if (array_key_exists($conflict, $this->results)) {
if (!is_string($reason) || !strlen($reason)) {
$reason = '.';
} else {
$reason = ': '.$reason.'.';
}
throw new PhutilArgumentUsageException(
pht(
"Argument '%s' conflicts with argument '%s'%s",
"{$pre}{$arg}",
"--{$conflict}",
$reason));
}
}
if ($spec->getRepeatable()) {
if ($spec->getParamName() === null) {
if (empty($this->results[$spec->getName()])) {
$this->results[$spec->getName()] = 0;
}
$this->results[$spec->getName()]++;
} else {
$this->results[$spec->getName()][] = $val;
}
} else {
$this->results[$spec->getName()] = $val;
}
}
}
}
foreach ($specs as $spec) {
if ($spec->getWildcard()) {
$this->results[$spec->getName()] = $this->filterWildcardArgv($argv);
$argv = array();
break;
}
}
$this->argv = array_values($argv);
return $this;
}
/**
* Parse and consume a list of arguments, throwing an exception if there is
* anything left unconsumed. This is like @{method:parsePartial}, but raises
* a {class:PhutilArgumentUsageException} if there are leftovers.
*
* Normally, you would call @{method:parse} instead, which emits a
* user-friendly error. You can also use @{method:printUsageException} to
* render the exception in a user-friendly way.
*
* @param list List of argument specs, see
* @{class:PhutilArgumentSpecification}.
* @return this
* @task parse
*/
public function parseFull(array $specs) {
$this->parsePartial($specs);
if (count($this->argv)) {
$arg = head($this->argv);
throw new PhutilArgumentUsageException(
pht("Unrecognized argument '%s'.", $arg));
}
if ($this->showHelp) {
$this->printHelpAndExit();
}
return $this;
}
/**
* Parse and consume a list of arguments, raising a user-friendly error if
* anything remains. See also @{method:parseFull} and @{method:parsePartial}.
*
* @param list List of argument specs, see
* @{class:PhutilArgumentSpecification}.
* @return this
* @task parse
*/
public function parse(array $specs) {
try {
return $this->parseFull($specs);
} catch (PhutilArgumentUsageException $ex) {
$this->printUsageException($ex);
exit(self::PARSE_ERROR_CODE);
}
}
/**
* Parse and execute workflows, raising a user-friendly error if anything
* remains. See also @{method:parseWorkflowsFull}.
*
* See @{class:PhutilArgumentWorkflow} for details on using workflows.
*
* @param list List of argument specs, see
* @{class:PhutilArgumentSpecification}.
* @return this
* @task parse
*/
public function parseWorkflows(array $workflows) {
try {
return $this->parseWorkflowsFull($workflows);
} catch (PhutilArgumentUsageException $ex) {
$this->printUsageException($ex);
exit(self::PARSE_ERROR_CODE);
}
}
/**
* Select a workflow. For commands that may operate in several modes, like
* `arc`, the modes can be split into "workflows". Each workflow specifies
* the arguments it accepts. This method takes a list of workflows, selects
* the chosen workflow, parses its arguments, and either executes it (if it
* is executable) or returns it for handling.
*
* See @{class:PhutilArgumentWorkflow} for details on using workflows.
*
* @param list List of @{class:PhutilArgumentWorkflow}s.
* @return PhutilArgumentWorkflow|no Returns the chosen workflow if it is
* not executable, or executes it and
* exits with a return code if it is.
* @task parse
*/
public function parseWorkflowsFull(array $workflows) {
assert_instances_of($workflows, 'PhutilArgumentWorkflow');
// Clear out existing workflows. We need to do this to permit the
// construction of sub-workflows.
$this->workflows = array();
foreach ($workflows as $workflow) {
$name = $workflow->getName();
if ($name === null) {
throw new PhutilArgumentSpecificationException(
pht('Workflow has no name!'));
}
if (isset($this->workflows[$name])) {
throw new PhutilArgumentSpecificationException(
pht("Two workflows with name '%s!", $name));
}
$this->workflows[$name] = $workflow;
}
$argv = $this->argv;
if (empty($argv)) {
// TODO: this is kind of hacky / magical.
if (isset($this->workflows['help'])) {
$argv = array('help');
} else {
throw new PhutilArgumentUsageException(pht('No workflow selected.'));
}
}
$flow = array_shift($argv);
$flow = strtolower($flow);
if (empty($this->workflows[$flow])) {
$workflow_names = array();
foreach ($this->workflows as $wf) {
$workflow_names[] = $wf->getName();
}
sort($workflow_names);
$command_list = implode(', ', $workflow_names);
$ex_msg = pht(
"Invalid command '%s'. Valid commands are: %s.",
$flow,
$command_list);
if (in_array('help', $workflow_names)) {
$bin = basename($this->bin);
$ex_msg .= "\n".pht(
'For more details on available commands, run `%s`.', "{$bin} help");
}
throw new PhutilArgumentUsageException($ex_msg);
}
$workflow = $this->workflows[$flow];
if ($this->showHelp) {
// Make "cmd flow --help" behave like "cmd help flow", not "cmd help".
$help_flow = idx($this->workflows, 'help');
if ($help_flow) {
if ($help_flow !== $workflow) {
$workflow = $help_flow;
$argv = array($flow);
// Prevent parse() from dumping us back out to standard help.
$this->showHelp = false;
}
} else {
$this->printHelpAndExit();
}
}
$this->argv = array_values($argv);
if ($workflow->shouldParsePartial()) {
$this->parsePartial($workflow->getArguments());
} else {
$this->parse($workflow->getArguments());
}
+
if ($workflow->isExecutable()) {
+ $workflow->setArgv($this);
$err = $workflow->execute($this);
exit($err);
} else {
return $workflow;
}
}
/**
* Parse "standard" arguments and apply their effects:
*
* --trace Enable service call tracing.
* --no-ansi Disable ANSI color/style sequences.
* --xprofile Write out an XHProf profile.
* --help Show help.
*
* @return this
*
* @phutil-external-symbol function xhprof_enable
*/
public function parseStandardArguments() {
try {
$this->parsePartial(
array(
array(
'name' => 'trace',
'help' => pht('Trace command execution and show service calls.'),
'standard' => true,
),
array(
'name' => 'no-ansi',
'help' => pht(
'Disable ANSI terminal codes, printing plain text with '.
'no color or style.'),
'conflicts' => array(
'ansi' => null,
),
'standard' => true,
),
array(
'name' => 'ansi',
'help' => pht(
"Use formatting even in environments which probably ".
"don't support it."),
'standard' => true,
),
array(
'name' => 'xprofile',
'param' => 'profile',
'help' => pht(
'Profile script execution and write results to a file.'),
'standard' => true,
),
array(
'name' => 'help',
'short' => 'h',
'help' => pht('Show this help.'),
'standard' => true,
),
array(
'name' => 'show-standard-options',
'help' => pht(
'Show every option, including standard options like this one.'),
'standard' => true,
),
array(
'name' => 'recon',
'help' => pht('Start in remote console mode.'),
'standard' => true,
),
));
} catch (PhutilArgumentUsageException $ex) {
$this->printUsageException($ex);
exit(self::PARSE_ERROR_CODE);
}
if ($this->getArg('trace')) {
PhutilServiceProfiler::installEchoListener();
}
if ($this->getArg('no-ansi')) {
PhutilConsoleFormatter::disableANSI(true);
}
if ($this->getArg('ansi')) {
PhutilConsoleFormatter::disableANSI(false);
}
if ($this->getArg('help')) {
$this->showHelp = true;
}
$xprofile = $this->getArg('xprofile');
if ($xprofile) {
if (!function_exists('xhprof_enable')) {
throw new Exception(
pht("To use '%s', you must install XHProf.", '--xprofile'));
}
xhprof_enable(0);
register_shutdown_function(array($this, 'shutdownProfiler'));
}
$recon = $this->getArg('recon');
if ($recon) {
$remote_console = PhutilConsole::newRemoteConsole();
$remote_console->beginRedirectOut();
PhutilConsole::setConsole($remote_console);
} else if ($this->getArg('trace')) {
$server = new PhutilConsoleServer();
$server->setEnableLog(true);
$console = PhutilConsole::newConsoleForServer($server);
PhutilConsole::setConsole($console);
}
return $this;
}
/* -( Reading Arguments )-------------------------------------------------- */
public function getArg($name) {
if (empty($this->specs[$name])) {
throw new PhutilArgumentSpecificationException(
pht("No specification exists for argument '%s'!", $name));
}
if (idx($this->results, $name) !== null) {
return $this->results[$name];
}
return $this->specs[$name]->getDefault();
}
public function getUnconsumedArgumentVector() {
return $this->argv;
}
/* -( Command Help )------------------------------------------------------- */
public function setSynopsis($synopsis) {
$this->synopsis = $synopsis;
return $this;
}
public function setTagline($tagline) {
$this->tagline = $tagline;
return $this;
}
public function printHelpAndExit() {
echo $this->renderHelp();
exit(self::PARSE_ERROR_CODE);
}
public function renderHelp() {
$out = array();
$more = array();
if ($this->bin) {
$out[] = $this->format('**%s**', pht('NAME'));
$name = $this->indent(6, '**%s**', basename($this->bin));
if ($this->tagline) {
$name .= $this->format(' - '.$this->tagline);
}
$out[] = $name;
$out[] = null;
}
if ($this->synopsis) {
$out[] = $this->format('**%s**', pht('SYNOPSIS'));
$out[] = $this->indent(6, $this->synopsis);
$out[] = null;
}
if ($this->workflows) {
$has_help = false;
$out[] = $this->format('**%s**', pht('WORKFLOWS'));
$out[] = null;
$flows = $this->workflows;
ksort($flows);
foreach ($flows as $workflow) {
if ($workflow->getName() == 'help') {
$has_help = true;
}
$out[] = $this->renderWorkflowHelp(
$workflow->getName(),
$show_details = false);
}
if ($has_help) {
$more[] = pht(
'Use **%s** __command__ for a detailed command reference.', 'help');
}
}
$specs = $this->renderArgumentSpecs($this->specs);
if ($specs) {
$out[] = $this->format('**%s**', pht('OPTION REFERENCE'));
$out[] = null;
$out[] = $specs;
}
// If we have standard options but no --show-standard-options, print out
// a quick hint about it.
if (!empty($this->specs['show-standard-options']) &&
!$this->getArg('show-standard-options')) {
$more[] = pht(
'Use __%s__ to show additional options.', '--show-standard-options');
}
$out[] = null;
if ($more) {
foreach ($more as $hint) {
$out[] = $this->indent(0, $hint);
}
$out[] = null;
}
return implode("\n", $out);
}
public function renderWorkflowHelp(
$workflow_name,
$show_details = false) {
$out = array();
$indent = ($show_details ? 0 : 6);
$workflow = idx($this->workflows, strtolower($workflow_name));
if (!$workflow) {
$out[] = $this->indent(
$indent,
pht('There is no **%s** workflow.', $workflow_name));
} else {
$out[] = $this->indent($indent, $workflow->getExamples());
$out[] = $this->indent($indent, $workflow->getSynopsis());
if ($show_details) {
$full_help = $workflow->getHelp();
if ($full_help) {
$out[] = null;
$out[] = $this->indent($indent, $full_help);
}
$specs = $this->renderArgumentSpecs($workflow->getArguments());
if ($specs) {
$out[] = null;
$out[] = $specs;
}
}
}
$out[] = null;
return implode("\n", $out);
}
public function printUsageException(PhutilArgumentUsageException $ex) {
fwrite(
STDERR,
$this->format("**%s** %s\n", pht('Usage Exception:'), $ex->getMessage()));
}
/* -( Internals )---------------------------------------------------------- */
private function filterWildcardArgv(array $argv) {
foreach ($argv as $key => $value) {
if ($value == '--') {
unset($argv[$key]);
break;
} else if (
is_string($value) &&
!strncmp($value, '-', 1) &&
strlen($value) > 1) {
throw new PhutilArgumentUsageException(
pht(
"Argument '%s' is unrecognized. Use '%s' to indicate ".
"the end of flags.",
$value,
'--'));
}
}
return array_values($argv);
}
private function mergeSpecs(array $specs) {
$short_map = mpull($this->specs, null, 'getShortAlias');
unset($short_map[null]);
$wildcard = null;
foreach ($this->specs as $spec) {
if ($spec->getWildcard()) {
$wildcard = $spec;
break;
}
}
foreach ($specs as $spec) {
$spec->validate();
$name = $spec->getName();
if (isset($this->specs[$name])) {
throw new PhutilArgumentSpecificationException(
pht("Two argument specifications have the same name ('%s').", $name));
}
$short = $spec->getShortAlias();
if ($short) {
if (isset($short_map[$short])) {
throw new PhutilArgumentSpecificationException(
pht(
"Two argument specifications have the same short alias ('%s').",
$short));
}
$short_map[$short] = $spec;
}
if ($spec->getWildcard()) {
if ($wildcard) {
throw new PhutilArgumentSpecificationException(
pht(
'Two argument specifications are marked as wildcard arguments. '.
'You can have a maximum of one wildcard argument.'));
} else {
$wildcard = $spec;
}
}
$this->specs[$name] = $spec;
}
foreach ($this->specs as $name => $spec) {
foreach ($spec->getConflicts() as $conflict => $reason) {
if (empty($this->specs[$conflict])) {
throw new PhutilArgumentSpecificationException(
pht(
"Argument '%s' conflicts with unspecified argument '%s'.",
$name,
$conflict));
}
if ($conflict == $name) {
throw new PhutilArgumentSpecificationException(
pht("Argument '%s' conflicts with itself!", $name));
}
}
}
}
private function renderArgumentSpecs(array $specs) {
foreach ($specs as $key => $spec) {
if ($spec->getWildcard()) {
unset($specs[$key]);
}
}
$out = array();
$no_standard_options =
!empty($this->specs['show-standard-options']) &&
!$this->getArg('show-standard-options');
$specs = msort($specs, 'getName');
foreach ($specs as $spec) {
if ($spec->getStandard() && $no_standard_options) {
// If this is a standard argument and the user didn't pass
// --show-standard-options, skip it.
continue;
}
$name = $this->indent(6, '__--%s__', $spec->getName());
$short = null;
if ($spec->getShortAlias()) {
$short = $this->format(', __-%s__', $spec->getShortAlias());
}
if ($spec->getParamName()) {
$param = $this->format(' __%s__', $spec->getParamName());
$name .= $param;
if ($short) {
$short .= $param;
}
}
$out[] = $name.$short;
$out[] = $this->indent(10, $spec->getHelp());
$out[] = null;
}
return implode("\n", $out);
}
private function format($str /* , ... */) {
$args = func_get_args();
return call_user_func_array(
'phutil_console_format',
$args);
}
private function indent($level, $str /* , ... */) {
$args = func_get_args();
$args = array_slice($args, 1);
$text = call_user_func_array(array($this, 'format'), $args);
return phutil_console_wrap($text, $level);
}
/**
* @phutil-external-symbol function xhprof_disable
*/
public function shutdownProfiler() {
$data = xhprof_disable();
$data = json_encode($data);
Filesystem::writeFile($this->getArg('xprofile'), $data);
}
}
diff --git a/src/parser/argument/workflow/PhutilArgumentWorkflow.php b/src/parser/argument/workflow/PhutilArgumentWorkflow.php
index d994de10..98e4d958 100644
--- a/src/parser/argument/workflow/PhutilArgumentWorkflow.php
+++ b/src/parser/argument/workflow/PhutilArgumentWorkflow.php
@@ -1,181 +1,191 @@
setTagline('simple calculator example');
* $args->setSynopsis(<<setName('add')
* ->setExamples('**add** __n__ ...')
* ->setSynopsis('Compute the sum of a list of numbers.')
* ->setArguments(
* array(
* array(
* 'name' => 'numbers',
* 'wildcard' => true,
* ),
* ));
*
* $mul_workflow = id(new PhutilArgumentWorkflow())
* ->setName('mul')
* ->setExamples('**mul** __n__ ...')
* ->setSynopsis('Compute the product of a list of numbers.')
* ->setArguments(
* array(
* array(
* 'name' => 'numbers',
* 'wildcard' => true,
* ),
* ));
*
* $flow = $args->parseWorkflows(
* array(
* $add_workflow,
* $mul_workflow,
* new PhutilHelpArgumentWorkflow(),
* ));
*
* $nums = $args->getArg('numbers');
* if (empty($nums)) {
* echo "You must provide one or more numbers!\n";
* exit(1);
* }
*
* foreach ($nums as $num) {
* if (!is_numeric($num)) {
* echo "Number '{$num}' is not numeric!\n";
* exit(1);
* }
* }
*
* switch ($flow->getName()) {
* case 'add':
* echo array_sum($nums)."\n";
* break;
* case 'mul':
* echo array_product($nums)."\n";
* break;
* }
*
* You can also subclass this class and return `true` from
* @{method:isExecutable}. In this case, the parser will automatically select
* your workflow when the user invokes it.
*
* @concrete-extensible
*/
class PhutilArgumentWorkflow extends Phobject {
private $name;
private $synopsis;
private $specs = array();
private $examples;
private $help;
+ private $argv;
final public function __construct() {
$this->didConstruct();
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
/**
* Provide brief usage examples of common calling conventions, like:
*
* $workflow->setExamples("**delete** __file__ [__options__]");
*
* This text is shown in both brief and detailed help, and should give the
* user a quick reference for common uses. You can separate several common
* uses with newlines, but usually should not provide more than 2-3 examples.
*/
final public function setExamples($examples) {
$this->examples = $examples;
return $this;
}
final public function getExamples() {
if (!$this->examples) {
return '**'.$this->name.'**';
}
return $this->examples;
}
/**
* Provide a brief description of the command, like "Delete a file.".
*
* This text is shown in both brief and detailed help, and should give the
* user a general idea of what the workflow does.
*/
final public function setSynopsis($synopsis) {
$this->synopsis = $synopsis;
return $this;
}
final public function getSynopsis() {
return $this->synopsis;
}
/**
* Provide a full explanation of the command. This text is shown only in
* detailed help.
*/
final public function getHelp() {
return $this->help;
}
final public function setHelp($help) {
$this->help = $help;
return $this;
}
final public function setArguments(array $specs) {
$specs = PhutilArgumentSpecification::newSpecsFromList($specs);
$this->specs = $specs;
return $this;
}
final public function getArguments() {
return $this->specs;
}
+ final public function setArgv(PhutilArgumentParser $argv) {
+ $this->argv = $argv;
+ return $this;
+ }
+
+ final public function getArgv() {
+ return $this->argv;
+ }
+
protected function didConstruct() {
return null;
}
public function isExecutable() {
return false;
}
public function execute(PhutilArgumentParser $args) {
throw new Exception(pht("This workflow isn't executable!"));
}
/**
* Normally, workflow arguments are parsed fully, so unexpected arguments will
* raise an error. You can return `true` from this method to parse workflow
* arguments only partially. This will allow you to manually parse remaining
* arguments or delegate to a second level of workflows.
*
* @return bool True to partially parse workflow arguments (default false).
*/
public function shouldParsePartial() {
return false;
}
}