2017-02-21 00:18:38 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
|
|
wakatime.arguments
|
|
|
|
~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
Command-line arguments.
|
|
|
|
|
|
|
|
:copyright: (c) 2016 Alan Hamlett.
|
|
|
|
:license: BSD, see LICENSE for more details.
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import print_function
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import time
|
|
|
|
import traceback
|
|
|
|
from .__about__ import __version__
|
2017-10-29 18:32:03 +00:00
|
|
|
from .compat import basestring
|
2017-02-21 00:18:38 +00:00
|
|
|
from .configs import parseConfigFile
|
|
|
|
from .constants import AUTH_ERROR
|
|
|
|
from .packages import argparse
|
|
|
|
|
|
|
|
|
|
|
|
class FileAction(argparse.Action):
|
|
|
|
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
2017-10-29 18:32:03 +00:00
|
|
|
if isinstance(values, basestring) and values.startswith('"'):
|
|
|
|
values = re.sub(r'\\"', '"', values.strip('"'))
|
2017-02-21 00:18:38 +00:00
|
|
|
try:
|
|
|
|
if os.path.isfile(values):
|
|
|
|
values = os.path.realpath(values)
|
|
|
|
except: # pragma: nocover
|
|
|
|
pass
|
|
|
|
setattr(namespace, self.dest, values)
|
|
|
|
|
|
|
|
|
2017-10-29 18:32:03 +00:00
|
|
|
class StoreWithoutQuotes(argparse.Action):
|
|
|
|
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
|
|
if isinstance(values, basestring) and values.startswith('"'):
|
|
|
|
values = re.sub(r'\\"', '"', values.strip('"'))
|
|
|
|
setattr(namespace, self.dest, values)
|
|
|
|
|
|
|
|
|
2017-11-09 07:12:05 +00:00
|
|
|
def parse_arguments():
|
2017-02-21 00:18:38 +00:00
|
|
|
"""Parse command line arguments and configs from ~/.wakatime.cfg.
|
|
|
|
Command line arguments take precedence over config file settings.
|
|
|
|
Returns instances of ArgumentParser and SafeConfigParser.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# define supported command line arguments
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
description='Common interface for the WakaTime api.')
|
|
|
|
parser.add_argument('--entity', dest='entity', metavar='FILE',
|
|
|
|
action=FileAction,
|
|
|
|
help='absolute path to file for the heartbeat; can also be a '+
|
|
|
|
'url, domain, or app when --entity-type is not file')
|
|
|
|
parser.add_argument('--file', dest='file', action=FileAction,
|
|
|
|
help=argparse.SUPPRESS)
|
2017-10-29 18:32:03 +00:00
|
|
|
parser.add_argument('--key', dest='key', action=StoreWithoutQuotes,
|
2017-02-21 00:18:38 +00:00
|
|
|
help='your wakatime api key; uses api_key from '+
|
|
|
|
'~/.wakatime.cfg by default')
|
|
|
|
parser.add_argument('--write', dest='is_write',
|
|
|
|
action='store_true',
|
|
|
|
help='when set, tells api this heartbeat was triggered from '+
|
|
|
|
'writing to a file')
|
2017-10-29 18:32:03 +00:00
|
|
|
parser.add_argument('--plugin', dest='plugin', action=StoreWithoutQuotes,
|
2017-02-21 00:18:38 +00:00
|
|
|
help='optional text editor plugin name and version '+
|
|
|
|
'for User-Agent header')
|
|
|
|
parser.add_argument('--time', dest='timestamp', metavar='time',
|
2017-10-29 18:32:03 +00:00
|
|
|
type=float, action=StoreWithoutQuotes,
|
2017-02-21 00:18:38 +00:00
|
|
|
help='optional floating-point unix epoch timestamp; '+
|
|
|
|
'uses current time by default')
|
2017-10-29 18:32:03 +00:00
|
|
|
parser.add_argument('--lineno', dest='lineno', action=StoreWithoutQuotes,
|
2017-02-21 00:18:38 +00:00
|
|
|
help='optional line number; current line being edited')
|
2017-10-29 18:32:03 +00:00
|
|
|
parser.add_argument('--cursorpos', dest='cursorpos', action=StoreWithoutQuotes,
|
2017-02-21 00:18:38 +00:00
|
|
|
help='optional cursor position in the current file')
|
2017-10-29 18:32:03 +00:00
|
|
|
parser.add_argument('--entity-type', dest='entity_type', action=StoreWithoutQuotes,
|
2017-02-21 00:18:38 +00:00
|
|
|
help='entity type for this heartbeat. can be one of "file", '+
|
|
|
|
'"domain", or "app"; defaults to file.')
|
2017-10-29 18:32:03 +00:00
|
|
|
parser.add_argument('--proxy', dest='proxy', action=StoreWithoutQuotes,
|
2017-02-21 00:18:38 +00:00
|
|
|
help='optional proxy configuration. Supports HTTPS '+
|
|
|
|
'and SOCKS proxies. For example: '+
|
|
|
|
'https://user:pass@host:port or '+
|
|
|
|
'socks5://user:pass@host:port or ' +
|
|
|
|
'domain\\user:pass')
|
2017-05-25 06:53:28 +00:00
|
|
|
parser.add_argument('--no-ssl-verify', dest='nosslverify',
|
|
|
|
action='store_true',
|
|
|
|
help='disables SSL certificate verification for HTTPS '+
|
|
|
|
'requests. By default, SSL certificates are verified.')
|
2017-10-29 18:32:03 +00:00
|
|
|
parser.add_argument('--project', dest='project', action=StoreWithoutQuotes,
|
2017-02-21 00:18:38 +00:00
|
|
|
help='optional project name')
|
2017-10-29 18:32:03 +00:00
|
|
|
parser.add_argument('--alternate-project', dest='alternate_project', action=StoreWithoutQuotes,
|
2017-02-21 00:18:38 +00:00
|
|
|
help='optional alternate project name; auto-discovered project '+
|
|
|
|
'takes priority')
|
2017-10-29 18:32:03 +00:00
|
|
|
parser.add_argument('--alternate-language', dest='alternate_language', action=StoreWithoutQuotes,
|
2017-02-21 00:18:38 +00:00
|
|
|
help=argparse.SUPPRESS)
|
2017-10-29 18:32:03 +00:00
|
|
|
parser.add_argument('--language', dest='language', action=StoreWithoutQuotes,
|
2017-02-21 00:18:38 +00:00
|
|
|
help='optional language name; if valid, takes priority over '+
|
|
|
|
'auto-detected language')
|
2017-10-29 18:32:03 +00:00
|
|
|
parser.add_argument('--hostname', dest='hostname', action=StoreWithoutQuotes, help='hostname of '+
|
2017-02-21 00:18:38 +00:00
|
|
|
'current machine.')
|
2018-01-05 07:33:07 +00:00
|
|
|
parser.add_argument('--disable-offline', dest='offline',
|
2017-02-21 00:18:38 +00:00
|
|
|
action='store_false',
|
|
|
|
help='disables offline time logging instead of queuing logged time')
|
2018-01-05 07:33:07 +00:00
|
|
|
parser.add_argument('--disableoffline', dest='offline_deprecated',
|
|
|
|
action='store_true', help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--hide-filenames', dest='hide_filenames',
|
|
|
|
action='store_true',
|
|
|
|
help='obfuscate filenames; will not send file names to api')
|
2017-02-21 00:18:38 +00:00
|
|
|
parser.add_argument('--hidefilenames', dest='hidefilenames',
|
|
|
|
action='store_true',
|
2018-01-05 07:33:07 +00:00
|
|
|
help=argparse.SUPPRESS)
|
2017-02-21 00:18:38 +00:00
|
|
|
parser.add_argument('--exclude', dest='exclude', action='append',
|
|
|
|
help='filename patterns to exclude from logging; POSIX regex '+
|
|
|
|
'syntax; can be used more than once')
|
|
|
|
parser.add_argument('--include', dest='include', action='append',
|
|
|
|
help='filename patterns to log; when used in combination with '+
|
|
|
|
'--exclude, files matching include will still be logged; '+
|
|
|
|
'POSIX regex syntax; can be used more than once')
|
2018-01-05 07:33:07 +00:00
|
|
|
parser.add_argument('--include-only-with-project-file',
|
|
|
|
dest='include_only_with_project_file',
|
|
|
|
action='store_true',
|
|
|
|
help='disables tracking folders unless they contain '+
|
|
|
|
'a .wakatime-project file; defaults to false')
|
2017-02-21 00:18:38 +00:00
|
|
|
parser.add_argument('--ignore', dest='ignore', action='append',
|
|
|
|
help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--extra-heartbeats', dest='extra_heartbeats',
|
|
|
|
action='store_true',
|
|
|
|
help='reads extra heartbeats from STDIN as a JSON array until EOF')
|
2018-01-05 07:33:07 +00:00
|
|
|
parser.add_argument('--log-file', dest='log_file', action=StoreWithoutQuotes,
|
2017-02-21 00:18:38 +00:00
|
|
|
help='defaults to ~/.wakatime.log')
|
2018-01-05 07:33:07 +00:00
|
|
|
parser.add_argument('--logfile', dest='logfile', action=StoreWithoutQuotes,
|
|
|
|
help=argparse.SUPPRESS)
|
|
|
|
parser.add_argument('--api-url', dest='api_url', action=StoreWithoutQuotes,
|
2017-02-21 00:18:38 +00:00
|
|
|
help='heartbeats api url; for debugging with a local server')
|
2018-01-05 07:33:07 +00:00
|
|
|
parser.add_argument('--apiurl', dest='apiurl', action=StoreWithoutQuotes,
|
|
|
|
help=argparse.SUPPRESS)
|
2017-10-29 18:32:03 +00:00
|
|
|
parser.add_argument('--timeout', dest='timeout', type=int, action=StoreWithoutQuotes,
|
2017-02-21 00:18:38 +00:00
|
|
|
help='number of seconds to wait when sending heartbeats to api; '+
|
|
|
|
'defaults to 60 seconds')
|
2017-10-29 18:32:03 +00:00
|
|
|
parser.add_argument('--config', dest='config', action=StoreWithoutQuotes,
|
2017-02-21 00:18:38 +00:00
|
|
|
help='defaults to ~/.wakatime.cfg')
|
|
|
|
parser.add_argument('--verbose', dest='verbose', action='store_true',
|
|
|
|
help='turns on debug messages in log file')
|
|
|
|
parser.add_argument('--version', action='version', version=__version__)
|
|
|
|
|
|
|
|
# parse command line arguments
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
# use current unix epoch timestamp by default
|
|
|
|
if not args.timestamp:
|
|
|
|
args.timestamp = time.time()
|
|
|
|
|
|
|
|
# parse ~/.wakatime.cfg file
|
|
|
|
configs = parseConfigFile(args.config)
|
|
|
|
|
|
|
|
# update args from configs
|
|
|
|
if not args.hostname:
|
|
|
|
if configs.has_option('settings', 'hostname'):
|
|
|
|
args.hostname = configs.get('settings', 'hostname')
|
|
|
|
if not args.key:
|
|
|
|
default_key = None
|
|
|
|
if configs.has_option('settings', 'api_key'):
|
|
|
|
default_key = configs.get('settings', 'api_key')
|
|
|
|
elif configs.has_option('settings', 'apikey'):
|
|
|
|
default_key = configs.get('settings', 'apikey')
|
|
|
|
if default_key:
|
|
|
|
args.key = default_key
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
parser.error('Missing api key. Find your api key from wakatime.com/settings.')
|
|
|
|
except SystemExit:
|
|
|
|
raise SystemExit(AUTH_ERROR)
|
|
|
|
|
|
|
|
is_valid = not not re.match(r'^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$', args.key, re.I)
|
|
|
|
if not is_valid:
|
|
|
|
try:
|
|
|
|
parser.error('Invalid api key. Find your api key from wakatime.com/settings.')
|
|
|
|
except SystemExit:
|
|
|
|
raise SystemExit(AUTH_ERROR)
|
|
|
|
|
|
|
|
if not args.entity:
|
|
|
|
if args.file:
|
|
|
|
args.entity = args.file
|
|
|
|
else:
|
|
|
|
parser.error('argument --entity is required')
|
|
|
|
|
|
|
|
if not args.language and args.alternate_language:
|
|
|
|
args.language = args.alternate_language
|
|
|
|
|
|
|
|
if not args.exclude:
|
|
|
|
args.exclude = []
|
|
|
|
if configs.has_option('settings', 'ignore'):
|
|
|
|
try:
|
|
|
|
for pattern in configs.get('settings', 'ignore').split("\n"):
|
|
|
|
if pattern.strip() != '':
|
|
|
|
args.exclude.append(pattern)
|
|
|
|
except TypeError: # pragma: nocover
|
|
|
|
pass
|
|
|
|
if configs.has_option('settings', 'exclude'):
|
|
|
|
try:
|
|
|
|
for pattern in configs.get('settings', 'exclude').split("\n"):
|
|
|
|
if pattern.strip() != '':
|
|
|
|
args.exclude.append(pattern)
|
|
|
|
except TypeError: # pragma: nocover
|
|
|
|
pass
|
2018-01-05 07:33:07 +00:00
|
|
|
if not args.include_only_with_project_file and configs.has_option('settings', 'include_only_with_project_file'):
|
|
|
|
args.include_only_with_project_file = configs.get('settings', 'include_only_with_project_file')
|
2017-02-21 00:18:38 +00:00
|
|
|
if not args.include:
|
|
|
|
args.include = []
|
|
|
|
if configs.has_option('settings', 'include'):
|
|
|
|
try:
|
|
|
|
for pattern in configs.get('settings', 'include').split("\n"):
|
|
|
|
if pattern.strip() != '':
|
|
|
|
args.include.append(pattern)
|
|
|
|
except TypeError: # pragma: nocover
|
|
|
|
pass
|
2018-01-05 07:33:07 +00:00
|
|
|
if not args.hide_filenames and args.hidefilenames:
|
|
|
|
args.hide_filenames = args.hidefilenames
|
|
|
|
if args.hide_filenames:
|
|
|
|
args.hide_filenames = ['.*']
|
2017-02-21 00:18:38 +00:00
|
|
|
else:
|
2018-01-05 07:33:07 +00:00
|
|
|
args.hide_filenames = []
|
|
|
|
option = None
|
2017-02-21 00:18:38 +00:00
|
|
|
if configs.has_option('settings', 'hidefilenames'):
|
|
|
|
option = configs.get('settings', 'hidefilenames')
|
2018-01-05 07:33:07 +00:00
|
|
|
if configs.has_option('settings', 'hide_filenames'):
|
|
|
|
option = configs.get('settings', 'hide_filenames')
|
|
|
|
if option is not None:
|
2017-02-21 00:18:38 +00:00
|
|
|
if option.strip().lower() == 'true':
|
2018-01-05 07:33:07 +00:00
|
|
|
args.hide_filenames = ['.*']
|
2017-02-21 00:18:38 +00:00
|
|
|
elif option.strip().lower() != 'false':
|
|
|
|
for pattern in option.split("\n"):
|
|
|
|
if pattern.strip() != '':
|
2018-01-05 07:33:07 +00:00
|
|
|
args.hide_filenames.append(pattern)
|
|
|
|
if args.offline_deprecated:
|
|
|
|
args.offline = False
|
2017-02-21 00:18:38 +00:00
|
|
|
if args.offline and configs.has_option('settings', 'offline'):
|
|
|
|
args.offline = configs.getboolean('settings', 'offline')
|
|
|
|
if not args.proxy and configs.has_option('settings', 'proxy'):
|
|
|
|
args.proxy = configs.get('settings', 'proxy')
|
|
|
|
if args.proxy:
|
|
|
|
pattern = r'^((https?|socks5)://)?([^:@]+(:([^:@])+)?@)?[^:]+(:\d+)?$'
|
|
|
|
if '\\' in args.proxy:
|
|
|
|
pattern = r'^.*\\.+$'
|
|
|
|
is_valid = not not re.match(pattern, args.proxy, re.I)
|
|
|
|
if not is_valid:
|
|
|
|
parser.error('Invalid proxy. Must be in format ' +
|
2017-10-29 18:32:03 +00:00
|
|
|
'https://user:pass@host:port or ' +
|
|
|
|
'socks5://user:pass@host:port or ' +
|
|
|
|
'domain\\user:pass.')
|
2017-05-25 06:53:28 +00:00
|
|
|
if configs.has_option('settings', 'no_ssl_verify'):
|
|
|
|
args.nosslverify = configs.getboolean('settings', 'no_ssl_verify')
|
2017-02-21 00:18:38 +00:00
|
|
|
if not args.verbose and configs.has_option('settings', 'verbose'):
|
|
|
|
args.verbose = configs.getboolean('settings', 'verbose')
|
|
|
|
if not args.verbose and configs.has_option('settings', 'debug'):
|
|
|
|
args.verbose = configs.getboolean('settings', 'debug')
|
2018-01-05 07:33:07 +00:00
|
|
|
if not args.log_file and args.logfile:
|
|
|
|
args.log_file = args.logfile
|
|
|
|
if not args.log_file and configs.has_option('settings', 'log_file'):
|
|
|
|
args.log_file = configs.get('settings', 'log_file')
|
|
|
|
if not args.log_file and os.environ.get('WAKATIME_HOME'):
|
2017-02-21 00:18:38 +00:00
|
|
|
home = os.environ.get('WAKATIME_HOME')
|
2018-01-05 07:33:07 +00:00
|
|
|
args.log_file = os.path.join(os.path.expanduser(home), '.wakatime.log')
|
|
|
|
if not args.api_url and args.apiurl:
|
|
|
|
args.api_url = args.apiurl
|
2017-02-21 00:18:38 +00:00
|
|
|
if not args.api_url and configs.has_option('settings', 'api_url'):
|
|
|
|
args.api_url = configs.get('settings', 'api_url')
|
|
|
|
if not args.timeout and configs.has_option('settings', 'timeout'):
|
|
|
|
try:
|
|
|
|
args.timeout = int(configs.get('settings', 'timeout'))
|
|
|
|
except ValueError:
|
|
|
|
print(traceback.format_exc())
|
|
|
|
|
|
|
|
return args, configs
|