2 # -*- coding: utf-8 -*-
3 # Copyright 2013 Google Inc. All Rights Reserved.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 """Main module for Google Cloud Storage command line tool."""
18 from __future__
import absolute_import
33 # Load the gsutil version number and append it to boto.UserAgent so the value is
34 # set before anything instantiates boto. This has to run after THIRD_PARTY_DIR
35 # is modified (done in gsutil.py) but before any calls are made that would cause
36 # boto.s3.Connection to be loaded - otherwise the Connection class would end up
37 # with a static reference to the pre-modified version of the UserAgent field,
38 # so boto requests would not include gsutil/version# in the UserAgent string.
41 # TODO: gsutil-beta: Cloud SDK scans for this string and performs
42 # substitution; ensure this works with both apitools and boto.
43 boto
.UserAgent
+= ' gsutil/%s (%s)' % (gslib
.VERSION
, sys
.platform
)
44 if os
.environ
.get('CLOUDSDK_WRAPPER') == '1':
45 boto
.UserAgent
+= ' Cloud SDK Command Line Tool'
46 if os
.environ
.get('CLOUDSDK_VERSION'):
47 boto
.UserAgent
+= ' %s' % os
.environ
.get('CLOUDSDK_VERSION')
49 # pylint: disable=g-bad-import-order
50 # pylint: disable=g-import-not-at-top
53 from gslib
import wildcard_iterator
54 from gslib
.cloud_api
import AccessDeniedException
55 from gslib
.cloud_api
import ArgumentException
56 from gslib
.cloud_api
import BadRequestException
57 from gslib
.cloud_api
import ProjectIdException
58 from gslib
.cloud_api
import ServiceException
59 from gslib
.command_runner
import CommandRunner
60 import gslib
.exception
61 from gslib
.exception
import CommandException
62 import apitools
.base
.py
.exceptions
as apitools_exceptions
63 from gslib
.util
import CreateLock
64 from gslib
.util
import GetBotoConfigFileList
65 from gslib
.util
import GetCertsFile
66 from gslib
.util
import GetCleanupFiles
67 from gslib
.util
import GsutilStreamHandler
68 from gslib
.util
import ProxyInfoFromEnvironmentVar
69 from gslib
.sig_handling
import GetCaughtSignals
70 from gslib
.sig_handling
import InitializeSignalHandling
71 from gslib
.sig_handling
import RegisterSignalHandler
73 GSUTIL_CLIENT_ID
= '909320924072.apps.googleusercontent.com'
74 # Google OAuth2 clients always have a secret, even if the client is an installed
75 # application/utility such as gsutil. Of course, in such cases the "secret" is
76 # actually publicly known; security depends entirely on the secrecy of refresh
77 # tokens, which effectively become bearer tokens.
78 GSUTIL_CLIENT_NOTSOSECRET
= 'p3RlpR10xMFh9ZXBS/ZNLYUu'
79 if os
.environ
.get('CLOUDSDK_WRAPPER') == '1':
80 # Cloud SDK installs have a separate client ID / secret.
81 GSUTIL_CLIENT_ID
= '32555940559.apps.googleusercontent.com'
82 GSUTIL_CLIENT_NOTSOSECRET
= 'ZmssLNjJy2998hD4CTg2ejr2'
84 CONFIG_KEYS_TO_REDACT
= ['proxy', 'proxy_port', 'proxy_user', 'proxy_pass']
87 # We don't use the oauth2 authentication plugin directly; importing it here
88 # ensures that it's loaded and available by default when an operation requiring
89 # authentication is performed.
91 # pylint: disable=unused-import,g-import-not-at-top
92 import gcs_oauth2_boto_plugin
97 ***************************** WARNING *****************************
98 *** You are running gsutil with debug output enabled.
99 *** Be aware that debug output includes authentication credentials.
100 *** Make sure to remove the value of the Authorization header for
101 *** each HTTP request printed to the console prior to posting to
102 *** a public medium such as a forum post or Stack Overflow.
103 ***************************** WARNING *****************************
107 ***************************** WARNING *****************************
108 *** You are running gsutil with the "https_validate_certificates" config
109 *** variable set to False. This option should always be set to True in
110 *** production environments to protect against man-in-the-middle attacks,
111 *** and leaking of user data.
112 ***************************** WARNING *****************************
116 test_exception_traces
= False
119 # pylint: disable=unused-argument
120 def _CleanupSignalHandler(signal_num
, cur_stack_frame
):
121 """Cleans up if process is killed with SIGINT, SIGQUIT or SIGTERM."""
126 for fname
in GetCleanupFiles():
129 except: # pylint: disable=bare-except
133 def _OutputAndExit(message
):
134 """Outputs message and exists with code 1."""
135 from gslib
.util
import UTF8
# pylint: disable=g-import-not-at-top
136 if debug
>= 2 or test_exception_traces
:
137 stack_trace
= traceback
.format_exc()
138 err
= ('DEBUG: Exception stack trace:\n %s\n' %
139 re
.sub('\\n', '\n ', stack_trace
))
141 err
= '%s\n' % message
143 sys
.stderr
.write(err
.encode(UTF8
))
144 except UnicodeDecodeError:
145 # Can happen when outputting invalid Unicode filenames.
146 sys
.stderr
.write(err
)
150 def _OutputUsageAndExit(command_runner
):
151 command_runner
.RunNamedCommand('help')
155 class GsutilFormatter(logging
.Formatter
):
156 """A logging.Formatter that supports logging microseconds (%f)."""
158 def formatTime(self
, record
, datefmt
=None):
160 return datetime
.datetime
.fromtimestamp(record
.created
).strftime(datefmt
)
162 # Use default implementation if datefmt is not specified.
163 return super(GsutilFormatter
, self
).formatTime(record
, datefmt
=datefmt
)
166 def _ConfigureLogging(level
=logging
.INFO
):
167 """Similar to logging.basicConfig() except it always adds a handler."""
168 log_format
= '%(levelname)s %(asctime)s %(filename)s] %(message)s'
169 date_format
= '%m%d %H:%M:%S.%f'
170 formatter
= GsutilFormatter(fmt
=log_format
, datefmt
=date_format
)
171 handler
= GsutilStreamHandler()
172 handler
.setFormatter(formatter
)
173 root_logger
= logging
.getLogger()
174 root_logger
.addHandler(handler
)
175 root_logger
.setLevel(level
)
179 InitializeSignalHandling()
180 # Any modules used in initializing multiprocessing variables must be
181 # imported after importing gslib.__main__.
182 # pylint: disable=redefined-outer-name,g-import-not-at-top
183 import gslib
.boto_translation
186 from gslib
.util
import BOTO_IS_SECURE
187 from gslib
.util
import CERTIFICATE_VALIDATION_ENABLED
188 # pylint: disable=unused-variable
189 from gcs_oauth2_boto_plugin
import oauth2_client
190 # pylint: enable=unused-variable
191 from gslib
.util
import MultiprocessingIsAvailable
192 if MultiprocessingIsAvailable()[0]:
193 # These setup methods must be called, and, on Windows, they can only be
194 # called from within an "if __name__ == '__main__':" block.
195 gslib
.util
.InitializeMultiprocessingVariables()
196 gslib
.command
.InitializeMultiprocessingVariables()
197 gslib
.boto_translation
.InitializeMultiprocessingVariables()
199 # This needs to be done after gslib.util.InitializeMultiprocessingVariables(),
200 # since otherwise we can't call gslib.util.CreateLock.
202 # pylint: disable=unused-import,g-import-not-at-top
203 import gcs_oauth2_boto_plugin
204 gcs_oauth2_boto_plugin
.oauth2_helper
.SetFallbackClientIdAndSecret(
205 GSUTIL_CLIENT_ID
, GSUTIL_CLIENT_NOTSOSECRET
)
206 gcs_oauth2_boto_plugin
.oauth2_helper
.SetLock(CreateLock())
211 global test_exception_traces
213 if not (2, 6) <= sys
.version_info
[:3] < (3,):
214 raise gslib
.exception
.CommandException(
215 'gsutil requires python 2.6 or 2.7.')
217 # In gsutil 4.0 and beyond, we don't use the boto library for the JSON
218 # API. However, we still store gsutil configuration data in the .boto
219 # config file for compatibility with previous versions and user convenience.
220 # Many users have a .boto configuration file from previous versions, and it
221 # is useful to have all of the configuration for gsutil stored in one place.
222 command_runner
= CommandRunner()
223 if not BOTO_IS_SECURE
:
224 raise CommandException('\n'.join(textwrap
.wrap(
225 'Your boto configuration has is_secure = False. Gsutil cannot be '
226 'run this way, for security reasons.')))
229 parallel_operations
= False
233 test_exception_traces
= False
235 # If user enters no commands just print the usage info.
236 if len(sys
.argv
) == 1:
237 sys
.argv
.append('help')
239 # Change the default of the 'https_validate_certificates' boto option to
240 # True (it is currently False in boto).
241 if not boto
.config
.has_option('Boto', 'https_validate_certificates'):
242 if not boto
.config
.has_section('Boto'):
243 boto
.config
.add_section('Boto')
244 boto
.config
.setbool('Boto', 'https_validate_certificates', True)
246 gslib
.util
.certs_file_lock
= CreateLock()
247 for signal_num
in GetCaughtSignals():
248 RegisterSignalHandler(signal_num
, _CleanupSignalHandler
)
253 opts
, args
= getopt
.getopt(sys
.argv
[1:], 'dDvo:h:mq',
254 ['debug', 'detailedDebug', 'version', 'option',
255 'help', 'header', 'multithreaded', 'quiet',
256 'testexceptiontraces'])
257 except getopt
.GetoptError
as e
:
258 _HandleCommandException(gslib
.exception
.CommandException(e
.msg
))
260 if o
in ('-d', '--debug'):
261 # Passing debug=2 causes boto to include httplib header output.
263 elif o
in ('-D', '--detailedDebug'):
264 # We use debug level 3 to ask gsutil code to output more detailed
265 # debug output. This is a bit of a hack since it overloads the same
266 # flag that was originally implemented for boto use. And we use -DD
267 # to ask for really detailed debugging (i.e., including HTTP payload).
272 elif o
in ('-?', '--help'):
273 _OutputUsageAndExit(command_runner
)
274 elif o
in ('-h', '--header'):
275 (hdr_name
, _
, hdr_val
) = a
.partition(':')
277 _OutputUsageAndExit(command_runner
)
278 headers
[hdr_name
.lower()] = hdr_val
279 elif o
in ('-m', '--multithreaded'):
280 parallel_operations
= True
281 elif o
in ('-q', '--quiet'):
283 elif o
in ('-v', '--version'):
285 elif o
== '--testexceptiontraces': # Hidden flag for integration tests.
286 test_exception_traces
= True
287 elif o
in ('-o', '--option'):
288 (opt_section_name
, _
, opt_value
) = a
.partition('=')
289 if not opt_section_name
:
290 _OutputUsageAndExit(command_runner
)
291 (opt_section
, _
, opt_name
) = opt_section_name
.partition(':')
292 if not opt_section
or not opt_name
:
293 _OutputUsageAndExit(command_runner
)
294 if not boto
.config
.has_section(opt_section
):
295 boto
.config
.add_section(opt_section
)
296 boto
.config
.set(opt_section
, opt_name
, opt_value
)
297 httplib2
.debuglevel
= debug
299 sys
.stderr
.write(DEBUG_WARNING
)
301 _ConfigureLogging(level
=logging
.DEBUG
)
302 command_runner
.RunNamedCommand('ver', ['-l'])
305 config_items
.extend(boto
.config
.items('Boto'))
306 config_items
.extend(boto
.config
.items('GSUtil'))
307 except ConfigParser
.NoSectionError
:
309 for i
in xrange(len(config_items
)):
310 config_item_key
= config_items
[i
][0]
311 if config_item_key
in CONFIG_KEYS_TO_REDACT
:
312 config_items
[i
] = (config_item_key
, 'REDACTED')
313 sys
.stderr
.write('Command being run: %s\n' % ' '.join(sys
.argv
))
314 sys
.stderr
.write('config_file_list: %s\n' % GetBotoConfigFileList())
315 sys
.stderr
.write('config: %s\n' % str(config_items
))
317 _ConfigureLogging(level
=logging
.WARNING
)
319 _ConfigureLogging(level
=logging
.INFO
)
320 # oauth2client uses info logging in places that would better
321 # correspond to gsutil's debug logging (e.g., when refreshing
323 oauth2client
.client
.logger
.setLevel(logging
.WARNING
)
325 if not CERTIFICATE_VALIDATION_ENABLED
:
326 sys
.stderr
.write(HTTP_WARNING
)
329 command_name
= 'version'
331 command_name
= 'help'
333 command_name
= args
[0]
335 _CheckAndWarnForProxyDifferences()
337 if os
.environ
.get('_ARGCOMPLETE', '0') == '1':
338 return _PerformTabCompletion(command_runner
)
340 return _RunNamedCommandAndHandleExceptions(
341 command_runner
, command_name
, args
=args
[1:], headers
=headers
,
342 debug_level
=debug
, parallel_operations
=parallel_operations
)
347 def _CheckAndWarnForProxyDifferences():
348 # If there are both boto config and environment variable config present for
349 # proxies, unset the environment variable and warn if it differs.
350 boto_port
= boto
.config
.getint('Boto', 'proxy_port', 0)
351 if boto
.config
.get('Boto', 'proxy', None) or boto_port
:
352 for proxy_env_var
in ['http_proxy', 'https_proxy', 'HTTPS_PROXY']:
353 if proxy_env_var
in os
.environ
and os
.environ
[proxy_env_var
]:
354 differing_values
= []
355 proxy_info
= ProxyInfoFromEnvironmentVar(proxy_env_var
)
356 if proxy_info
.proxy_host
!= boto
.config
.get('Boto', 'proxy', None):
357 differing_values
.append(
358 'Boto proxy host: "%s" differs from %s proxy host: "%s"' %
359 (boto
.config
.get('Boto', 'proxy', None), proxy_env_var
,
360 proxy_info
.proxy_host
))
361 if (proxy_info
.proxy_user
!=
362 boto
.config
.get('Boto', 'proxy_user', None)):
363 differing_values
.append(
364 'Boto proxy user: "%s" differs from %s proxy user: "%s"' %
365 (boto
.config
.get('Boto', 'proxy_user', None), proxy_env_var
,
366 proxy_info
.proxy_user
))
367 if (proxy_info
.proxy_pass
!=
368 boto
.config
.get('Boto', 'proxy_pass', None)):
369 differing_values
.append(
370 'Boto proxy password differs from %s proxy password' %
372 # Only compare ports if at least one is present, since the
373 # boto logic for selecting default ports has not yet executed.
374 if ((proxy_info
.proxy_port
or boto_port
) and
375 proxy_info
.proxy_port
!= boto_port
):
376 differing_values
.append(
377 'Boto proxy port: "%s" differs from %s proxy port: "%s"' %
378 (boto_port
, proxy_env_var
, proxy_info
.proxy_port
))
380 sys
.stderr
.write('\n'.join(textwrap
.wrap(
381 'WARNING: Proxy configuration is present in both the %s '
382 'environment variable and boto configuration, but '
383 'configuration differs. boto configuration proxy values will '
384 'be used. Differences detected:' % proxy_env_var
)))
385 sys
.stderr
.write('\n%s\n' % '\n'.join(differing_values
))
386 # Regardless of whether the proxy configuration values matched,
387 # delete the environment variable so as not to confuse boto.
388 del os
.environ
[proxy_env_var
]
391 def _HandleUnknownFailure(e
):
392 # Called if we fall through all known/handled exceptions. Allows us to
393 # print a stacktrace if -D option used.
395 stack_trace
= traceback
.format_exc()
396 sys
.stderr
.write('DEBUG: Exception stack trace:\n %s\n' %
397 re
.sub('\\n', '\n ', stack_trace
))
399 _OutputAndExit('Failure: %s.' % e
)
402 def _HandleCommandException(e
):
404 _OutputAndExit(e
.reason
)
406 _OutputAndExit('CommandException: %s' % e
.reason
)
409 # pylint: disable=unused-argument
410 def _HandleControlC(signal_num
, cur_stack_frame
):
411 """Called when user hits ^C.
413 This function prints a brief message instead of the normal Python stack trace
414 (unless -D option is used).
417 signal_num: Signal that was caught.
418 cur_stack_frame: Unused.
421 stack_trace
= ''.join(traceback
.format_list(traceback
.extract_stack()))
423 'DEBUG: Caught signal %d - Exception stack trace:\n'
424 ' %s' % (signal_num
, re
.sub('\\n', '\n ', stack_trace
)))
426 _OutputAndExit('Caught signal %d - exiting' % signal_num
)
429 def _HandleSigQuit(signal_num
, cur_stack_frame
):
430 """Called when user hits ^\\, so we can force breakpoint a running gsutil."""
431 import pdb
# pylint: disable=g-import-not-at-top
435 def _ConstructAccountProblemHelp(reason
):
436 """Constructs a help string for an access control error.
439 reason: e.reason string from caught exception.
442 Contructed help text.
444 default_project_id
= boto
.config
.get_value('GSUtil', 'default_project_id')
445 # pylint: disable=line-too-long, g-inconsistent-quotes
447 "Your request resulted in an AccountProblem (403) error. Usually this "
448 "happens if you attempt to create a bucket without first having "
449 "enabled billing for the project you are using. Please ensure billing is "
450 "enabled for your project by following the instructions at "
451 "`Google Developers Console<https://developers.google.com/console/help/billing>`. ")
452 if default_project_id
:
454 "In the project overview, ensure that the Project Number listed for "
455 "your project matches the project ID (%s) from your boto config file. "
456 % default_project_id
)
458 "If the above doesn't resolve your AccountProblem, please send mail to "
459 "gs-team@google.com requesting assistance, noting the exact command you "
460 "ran, the fact that you received a 403 AccountProblem error, and your "
461 "project ID. Please do not post your project ID on StackOverflow. "
462 "Note: It's possible to use Google Cloud Storage without enabling "
463 "billing if you're only listing or reading objects for which you're "
464 "authorized, or if you're uploading objects to a bucket billed to a "
465 "project that has billing enabled. But if you're attempting to create "
466 "buckets or upload objects to a bucket owned by your own project, you "
467 "must first enable billing for that project.")
471 def _CheckAndHandleCredentialException(e
, args
):
472 # Provide detail to users who have no boto config file (who might previously
473 # have been using gsutil only for accessing publicly readable buckets and
475 # pylint: disable=g-import-not-at-top
476 from gslib
.util
import HasConfiguredCredentials
477 if (not HasConfiguredCredentials() and
478 not boto
.config
.get_value('Tests', 'bypass_anonymous_access_warning',
480 # The check above allows tests to assert that we get a particular,
481 # expected failure, rather than always encountering this error message
482 # when there are no configured credentials. This allows tests to
483 # simulate a second user without permissions, without actually requiring
484 # two separate configured users.
485 if os
.environ
.get('CLOUDSDK_WRAPPER') == '1':
486 _OutputAndExit('\n'.join(textwrap
.wrap(
487 'You are attempting to access protected data with no configured '
488 'credentials. Please visit '
489 'https://cloud.google.com/console#/project and sign up for an '
490 'account, and then run the "gcloud auth login" command to '
491 'configure gsutil to use these credentials.')))
493 _OutputAndExit('\n'.join(textwrap
.wrap(
494 'You are attempting to access protected data with no configured '
495 'credentials. Please visit '
496 'https://cloud.google.com/console#/project and sign up for an '
497 'account, and then run the "gsutil config" command to configure '
498 'gsutil to use these credentials.')))
500 (e
.reason
== 'AccountProblem' or e
.reason
== 'Account disabled.' or
501 'account for the specified project has been disabled' in e
.reason
)
502 and ','.join(args
).find('gs://') != -1):
503 _OutputAndExit('\n'.join(textwrap
.wrap(
504 _ConstructAccountProblemHelp(e
.reason
))))
507 def _RunNamedCommandAndHandleExceptions(command_runner
, command_name
, args
=None,
508 headers
=None, debug_level
=0,
509 parallel_operations
=False):
510 """Runs the command with the given command runner and arguments."""
511 # pylint: disable=g-import-not-at-top
512 from gslib
.util
import GetConfigFilePath
513 from gslib
.util
import IS_WINDOWS
514 from gslib
.util
import IsRunningInteractively
516 # Catch ^C so we can print a brief message instead of the normal Python
517 # stack trace. Register as a final signal handler because this handler kills
518 # the main gsutil process (so it must run last).
519 RegisterSignalHandler(signal
.SIGINT
, _HandleControlC
, is_final_handler
=True)
520 # Catch ^\ so we can force a breakpoint in a running gsutil.
522 RegisterSignalHandler(signal
.SIGQUIT
, _HandleSigQuit
)
523 return command_runner
.RunNamedCommand(command_name
, args
, headers
,
524 debug_level
, parallel_operations
)
525 except AttributeError as e
:
526 if str(e
).find('secret_access_key') != -1:
527 _OutputAndExit('Missing credentials for the given URI(s). Does your '
528 'boto config file contain all needed credentials?')
530 _OutputAndExit(str(e
))
531 except gslib
.exception
.CommandException
as e
:
532 _HandleCommandException(e
)
533 except getopt
.GetoptError
as e
:
534 _HandleCommandException(gslib
.exception
.CommandException(e
.msg
))
535 except boto
.exception
.InvalidUriError
as e
:
536 _OutputAndExit('InvalidUriError: %s.' % e
.message
)
537 except gslib
.exception
.InvalidUrlError
as e
:
538 _OutputAndExit('InvalidUrlError: %s.' % e
.message
)
539 except boto
.auth_handler
.NotReadyToAuthenticate
:
540 _OutputAndExit('NotReadyToAuthenticate')
542 _OutputAndExit('OSError: %s.' % e
.strerror
)
544 if (e
.errno
== errno
.EPIPE
or (IS_WINDOWS
and e
.errno
== errno
.EINVAL
)
545 and not IsRunningInteractively()):
546 # If we get a pipe error, this just means that the pipe to stdout or
547 # stderr is broken. This can happen if the user pipes gsutil to a command
548 # that doesn't use the entire output stream. Instead of raising an error,
549 # just swallow it up and exit cleanly.
553 except wildcard_iterator
.WildcardException
as e
:
554 _OutputAndExit(e
.reason
)
555 except ProjectIdException
as e
:
557 'You are attempting to perform an operation that requires a '
558 'project id, with none configured. Please re-run '
559 'gsutil config and make sure to follow the instructions for '
560 'finding and entering your default project id.')
561 except BadRequestException
as e
:
562 if e
.reason
== 'MissingSecurityHeader':
563 _CheckAndHandleCredentialException(e
, args
)
565 except AccessDeniedException
as e
:
566 _CheckAndHandleCredentialException(e
, args
)
568 except ArgumentException
as e
:
570 except ServiceException
as e
:
572 except apitools_exceptions
.HttpError
as e
:
573 # These should usually be retried by the underlying implementation or
574 # wrapped by CloudApi ServiceExceptions, but if we do get them,
575 # print something useful.
576 _OutputAndExit('HttpError: %s, %s' % (getattr(e
.response
, 'status', ''),
578 except socket
.error
as e
:
579 if e
.args
[0] == errno
.EPIPE
:
580 # Retrying with a smaller file (per suggestion below) works because
581 # the library code send loop (in boto/s3/key.py) can get through the
582 # entire file and then request the HTTP response before the socket
583 # gets closed and the response lost.
585 'Got a "Broken pipe" error. This can happen to clients using Python '
586 '2.x, when the server sends an error response and then closes the '
587 'socket (see http://bugs.python.org/issue5542). If you are trying to '
588 'upload a large object you might retry with a small (say 200k) '
589 'object, and see if you get a more specific error code.'
592 _HandleUnknownFailure(e
)
593 except Exception as e
:
594 # Check for two types of errors related to service accounts. These errors
595 # appear to be the same except for their messages, but they are caused by
596 # different problems and both have unhelpful error messages. Moreover,
597 # the error type belongs to PyOpenSSL, which is not necessarily installed.
598 if 'mac verify failure' in str(e
):
600 'Encountered an error while refreshing access token. '
601 'If you are using a service account,\nplease verify that the '
602 'gs_service_key_file_password field in your config file,'
603 '\n%s, is correct.' % GetConfigFilePath())
604 elif 'asn1 encoding routines' in str(e
):
606 'Encountered an error while refreshing access token. '
607 'If you are using a service account,\nplease verify that the '
608 'gs_service_key_file field in your config file,\n%s, is correct.'
609 % GetConfigFilePath())
610 _HandleUnknownFailure(e
)
613 def _PerformTabCompletion(command_runner
):
614 """Performs gsutil-specific tab completion for the shell."""
615 # argparse and argcomplete are bundled with the Google Cloud SDK.
616 # When gsutil is invoked from the Google Cloud SDK, both should be available.
620 except ImportError as e
:
621 _OutputAndExit('A library required for performing tab completion was'
622 ' not found.\nCause: %s' % e
)
623 parser
= argparse
.ArgumentParser(add_help
=False)
624 subparsers
= parser
.add_subparsers()
625 command_runner
.ConfigureCommandArgumentParsers(subparsers
)
626 argcomplete
.autocomplete(parser
, exit_method
=sys
.exit
)
630 if __name__
== '__main__':