Merge branch 'TomasKorbar-changes'

This commit is contained in:
Chris Lane 2019-01-18 14:28:06 -05:00
commit 87448c49fa
10 changed files with 460 additions and 230 deletions

View File

@ -36,29 +36,24 @@ Examples:
# require the dependencies # require the dependencies
from __future__ import print_function from __future__ import print_function
from cheat import sheets, sheet from cheat.sheets import Sheets
from cheat.utils import colorize from cheat.sheet import Sheet
from cheat.utils import die from cheat.utils import Utils
from cheat.configuration import Configuration
from docopt import docopt from docopt import docopt
import os import os
if __name__ == '__main__': if __name__ == '__main__':
# validate CHEAT_HIGHLIGHT values if set
colors = [
'grey' , 'red' , 'green' , 'yellow' ,
'blue' , 'magenta' , 'cyan' , 'white' ,
]
if (
os.environ.get('CHEAT_HIGHLIGHT') and
os.environ.get('CHEAT_HIGHLIGHT') not in colors
):
die("%s %s" %('CHEAT_HIGHLIGHT must be one of:', colors))
# parse the command-line options # parse the command-line options
options = docopt(__doc__, version='cheat 2.3.1') options = docopt(__doc__, version='cheat 2.3.1')
config = Configuration()
sheets = Sheets(config)
utils = Utils(config)
sheet = Sheet(sheets, utils)
# list directories # list directories
if options['--directories']: if options['--directories']:
print("\n".join(sheets.paths())) print("\n".join(sheets.paths()))
@ -73,8 +68,8 @@ if __name__ == '__main__':
# search among the cheatsheets # search among the cheatsheets
elif options['--search']: elif options['--search']:
print(colorize(sheets.search(options['<keyword>'])), end="") print(utils.colorize(sheets.search(options['<keyword>'])), end="")
# print the cheatsheet # print the cheatsheet
else: else:
print(colorize(sheet.read(options['<cheatsheet>'])), end="") print(utils.colorize(sheet.read(options['<cheatsheet>'])), end="")

View File

@ -1,3 +1,4 @@
from . import sheet from . import sheet
from . import sheets from . import sheets
from . import utils from . import utils
from . import configuration

View File

@ -1,4 +0,0 @@
import os
def sheets_dir():
return os.path.split(__file__)

134
cheat/configuration.py Normal file
View File

@ -0,0 +1,134 @@
import os
from cheat.utils import Utils
import json
class Configuration:
def __init__(self):
self._get_global_conf_file_path()
self._get_local_conf_file_path()
self._saved_configuration = self._get_configuration()
def _get_configuration(self):
# get options from config files and environment vairables
merged_config = {}
try:
merged_config.update(
self._read_configuration_file(self.glob_config_path)
)
except Exception as e:
Utils.warn('error while parsing global configuration Reason: '
+ e.message
)
try:
merged_config.update(
self._read_configuration_file(self.local_config_path)
)
except Exception as e:
Utils.warn('error while parsing user configuration Reason: '
+ e.message
)
merged_config.update(self._read_env_vars_config())
self._check_configuration(merged_config)
return merged_config
def _read_configuration_file(self, path):
# Reads configuration file and returns list of set variables
read_config = {}
if (os.path.isfile(path)):
with open(path) as config_file:
read_config.update(json.load(config_file))
return read_config
def _read_env_vars_config(self):
read_config = {}
# NOTE: These variables are left here because of backwards
# compatibility and are supported only as env vars but not in
# configuration file
if (os.environ.get('VISUAL')):
read_config['EDITOR'] = os.environ.get('VISUAL')
# variables supported both in environment and configuration file
# NOTE: Variables without CHEAT_ prefix are legacy
# key is variable name and value is its legacy_alias
# if variable has no legacy alias then set to None
variables = {'CHEAT_DEFAULT_DIR': 'DEFAULT_CHEAT_DIR',
'CHEAT_PATH': 'CHEATPATH',
'CHEAT_COLORS': 'CHEATCOLORS',
'CHEAT_EDITOR': 'EDITOR',
'CHEAT_HIGHLIGHT': None
}
for (k, v) in variables.items():
self._read_env_var(read_config, k, v)
return read_config
def _check_configuration(self, config):
""" Check values in config and warn user or die """
# validate CHEAT_HIGHLIGHT values if set
colors = [
'grey', 'red', 'green', 'yellow',
'blue', 'magenta', 'cyan', 'white'
]
if (
config.get('CHEAT_HIGHLIGHT') and
config.get('CHEAT_HIGHLIGHT') not in colors
):
Utils.die("%s %s" % ('CHEAT_HIGHLIGHT must be one of:', colors))
def _read_env_var(self, current_config, key, alias=None):
if os.environ.get(key) is not None:
current_config[key] = os.environ.get(key)
return
elif alias is not None and os.environ.get(alias) is not None:
current_config[key] = os.environ.get(alias)
return
def _get_global_conf_file_path(self):
self.glob_config_path = (os.environ.get('CHEAT_GLOBAL_CONF_PATH')
or '/etc/cheat')
def _get_local_conf_file_path(self):
path = (os.environ.get('CHEAT_LOCAL_CONF_PATH')
or os.path.expanduser('~/.config/cheat/cheat'))
self.local_config_path = path
def _choose_value(self, primary_value_name, secondary_value_name):
""" Return primary or secondary value in saved_configuration
If primary value is in configuration then return it. If it is not
then return secondary. In the absence of both values return None
"""
primary_value = self._saved_configuration.get(primary_value_name)
secondary_value = self._saved_configuration.get(secondary_value_name)
if primary_value is not None:
return primary_value
else:
return secondary_value
def get_default_cheat_dir(self):
return self._choose_value('CHEAT_DEFAULT_DIR', 'DEFAULT_CHEAT_DIR')
def get_cheatpath(self):
return self._choose_value('CHEAT_PATH', 'CHEATPATH')
def get_cheatcolors(self):
return self._choose_value('CHEAT_COLORS', 'CHEATCOLORS')
def get_editor(self):
return self._choose_value('CHEAT_EDITOR', 'EDITOR')
def get_highlight(self):
return self._saved_configuration.get('CHEAT_HIGHLIGHT')

View File

@ -1,76 +1,78 @@
import os import os
import shutil import shutil
from cheat import sheets from cheat.utils import Utils
from cheat.utils import die, open_with_editor
def copy(current_sheet_path, new_sheet_path):
""" Copies a sheet to a new path """
# attempt to copy the sheet to DEFAULT_CHEAT_DIR
try:
shutil.copy(current_sheet_path, new_sheet_path)
# fail gracefully if the cheatsheet cannot be copied. This can happen if
# DEFAULT_CHEAT_DIR does not exist
except IOError:
die('Could not copy cheatsheet for editing.')
def create_or_edit(sheet): class Sheet:
""" Creates or edits a cheatsheet """
# if the cheatsheet does not exist def __init__(self, sheets, utils):
if not exists(sheet): self._sheets = sheets
create(sheet) self._utils = utils
# if the cheatsheet exists but not in the default_path, copy it to the def copy(self, current_sheet_path, new_sheet_path):
# default path before editing """ Copies a sheet to a new path """
elif exists(sheet) and not exists_in_default_path(sheet):
copy(path(sheet), os.path.join(sheets.default_path(), sheet))
edit(sheet)
# if it exists and is in the default path, then just open it # attempt to copy the sheet to DEFAULT_CHEAT_DIR
else: try:
edit(sheet) shutil.copy(current_sheet_path, new_sheet_path)
# fail gracefully if the cheatsheet cannot be copied. This can happen
# if DEFAULT_CHEAT_DIR does not exist
except IOError:
Utils.die('Could not copy cheatsheet for editing.')
def create(sheet): def create_or_edit(self, sheet):
""" Creates a cheatsheet """ """ Creates or edits a cheatsheet """
new_sheet_path = os.path.join(sheets.default_path(), sheet)
open_with_editor(new_sheet_path)
# if the cheatsheet does not exist
if not self.exists(sheet):
self.create(sheet)
def edit(sheet): # if the cheatsheet exists but not in the default_path, copy it to the
""" Opens a cheatsheet for editing """ # default path before editing
open_with_editor(path(sheet)) elif self.exists(sheet) and not self.exists_in_default_path(sheet):
self.copy(self.path(sheet),
os.path.join(self._sheets.default_path(), sheet))
self.edit(sheet)
# if it exists and is in the default path, then just open it
else:
self.edit(sheet)
def exists(sheet): def create(self, sheet):
""" Predicate that returns true if the sheet exists """ """ Creates a cheatsheet """
return sheet in sheets.get() and os.access(path(sheet), os.R_OK) new_sheet_path = os.path.join(self._sheets.default_path(), sheet)
self._utils.open_with_editor(new_sheet_path)
def edit(self, sheet):
""" Opens a cheatsheet for editing """
self._utils.open_with_editor(self.path(sheet))
def exists_in_default_path(sheet): def exists(self, sheet):
""" Predicate that returns true if the sheet exists in default_path""" """ Predicate that returns true if the sheet exists """
default_path_sheet = os.path.join(sheets.default_path(), sheet) return (sheet in self._sheets.get() and
return sheet in sheets.get() and os.access(default_path_sheet, os.R_OK) os.access(self.path(sheet), os.R_OK))
def exists_in_default_path(self, sheet):
""" Predicate that returns true if the sheet exists in default_path"""
default_path_sheet = os.path.join(self._sheets.default_path(), sheet)
return (sheet in self._sheets.get() and
os.access(default_path_sheet, os.R_OK))
def is_writable(sheet): def is_writable(self, sheet):
""" Predicate that returns true if the sheet is writeable """ """ Predicate that returns true if the sheet is writeable """
return sheet in sheets.get() and os.access(path(sheet), os.W_OK) return (sheet in self._sheets.get() and
os.access(self.path(sheet), os.W_OK))
def path(self, sheet):
""" Returns a sheet's filesystem path """
return self._sheets.get()[sheet]
def path(sheet): def read(self, sheet):
""" Returns a sheet's filesystem path """ """ Returns the contents of the cheatsheet as a String """
return sheets.get()[sheet] if not self.exists(sheet):
Utils.die('No cheatsheet found for ' + sheet)
with open(self.path(sheet)) as cheatfile:
def read(sheet): return cheatfile.read()
""" Returns the contents of the cheatsheet as a String """
if not exists(sheet):
die('No cheatsheet found for ' + sheet)
with open(path(sheet)) as cheatfile:
return cheatfile.read()

View File

@ -1,94 +1,102 @@
import os import os
from cheat import cheatsheets from cheat.utils import Utils
from cheat.utils import die
from cheat.utils import highlight
def default_path():
""" Returns the default cheatsheet path """
# determine the default cheatsheet dir
default_sheets_dir = os.environ.get('DEFAULT_CHEAT_DIR') or os.path.join('~', '.cheat')
default_sheets_dir = os.path.expanduser(os.path.expandvars(default_sheets_dir))
# create the DEFAULT_CHEAT_DIR if it does not exist
if not os.path.isdir(default_sheets_dir):
try:
# @kludge: unclear on why this is necessary
os.umask(0000)
os.mkdir(default_sheets_dir)
except OSError:
die('Could not create DEFAULT_CHEAT_DIR')
# assert that the DEFAULT_CHEAT_DIR is readable and writable
if not os.access(default_sheets_dir, os.R_OK):
die('The DEFAULT_CHEAT_DIR (' + default_sheets_dir +') is not readable.')
if not os.access(default_sheets_dir, os.W_OK):
die('The DEFAULT_CHEAT_DIR (' + default_sheets_dir +') is not writable.')
# return the default dir
return default_sheets_dir
def get(): class Sheets:
""" Assembles a dictionary of cheatsheets as name => file-path """
cheats = {}
# otherwise, scan the filesystem def __init__(self, config):
for cheat_dir in reversed(paths()): self._default_cheat_dir = config.get_default_cheat_dir()
cheats.update( self._cheatpath = config.get_cheatpath()
dict([ self._utils = Utils(config)
(cheat, os.path.join(cheat_dir, cheat))
for cheat in os.listdir(cheat_dir)
if not cheat.startswith('.')
and not cheat.startswith('__')
])
)
return cheats def default_path(self):
""" Returns the default cheatsheet path """
# determine the default cheatsheet dir
default_sheets_dir = (self._default_cheat_dir or
os.path.join('~', '.cheat'))
default_sheets_dir = os.path.expanduser(
os.path.expandvars(default_sheets_dir))
def paths(): # create the DEFAULT_CHEAT_DIR if it does not exist
""" Assembles a list of directories containing cheatsheets """ if not os.path.isdir(default_sheets_dir):
sheet_paths = [ try:
default_path(), # @kludge: unclear on why this is necessary
cheatsheets.sheets_dir()[0], os.umask(0000)
] os.mkdir(default_sheets_dir)
# merge the CHEATPATH paths into the sheet_paths except OSError:
if 'CHEATPATH' in os.environ and os.environ['CHEATPATH']: Utils.die('Could not create DEFAULT_CHEAT_DIR')
for path in os.environ['CHEATPATH'].split(os.pathsep):
if os.path.isdir(path):
sheet_paths.append(path)
if not sheet_paths: # assert that the DEFAULT_CHEAT_DIR is readable and writable
die('The DEFAULT_CHEAT_DIR dir does not exist or the CHEATPATH is not set.') if not os.access(default_sheets_dir, os.R_OK):
Utils.die('The DEFAULT_CHEAT_DIR ('
+ default_sheets_dir
+ ') is not readable.')
if not os.access(default_sheets_dir, os.W_OK):
Utils.die('The DEFAULT_CHEAT_DIR ('
+ default_sheets_dir
+ ') is not writable.')
return sheet_paths # return the default dir
return default_sheets_dir
def get(self):
""" Assembles a dictionary of cheatsheets as name => file-path """
cheats = {}
def list(): # otherwise, scan the filesystem
""" Lists the available cheatsheets """ for cheat_dir in reversed(self.paths()):
sheet_list = '' cheats.update(
pad_length = max([len(x) for x in get().keys()]) + 4 dict([
for sheet in sorted(get().items()): (cheat, os.path.join(cheat_dir, cheat))
sheet_list += sheet[0].ljust(pad_length) + sheet[1] + "\n" for cheat in os.listdir(cheat_dir)
return sheet_list if not cheat.startswith('.')
and not cheat.startswith('__')
])
)
return cheats
def search(term): def paths(self):
""" Searches all cheatsheets for the specified term """ """ Assembles a list of directories containing cheatsheets """
result = '' sheet_paths = [
lowered_term = term.lower() self.default_path(),
'/usr/share/cheat',
]
for cheatsheet in sorted(get().items()): # merge the CHEATPATH paths into the sheet_paths
match = '' if self._cheatpath:
for line in open(cheatsheet[1]): for path in self._cheatpath.split(os.pathsep):
if term in line: if os.path.isdir(path):
match += ' ' + highlight(term, line) sheet_paths.append(path)
if match != '': if not sheet_paths:
result += cheatsheet[0] + ":\n" + match + "\n" Utils.die('The DEFAULT_CHEAT_DIR dir does not exist '
+ 'or the CHEATPATH is not set.')
return result return sheet_paths
def list(self):
""" Lists the available cheatsheets """
sheet_list = ''
pad_length = max([len(x) for x in self.get().keys()]) + 4
for sheet in sorted(self.get().items()):
sheet_list += sheet[0].ljust(pad_length) + sheet[1] + "\n"
return sheet_list
def search(self, term):
""" Searches all cheatsheets for the specified term """
result = ''
for cheatsheet in sorted(self.get().items()):
match = ''
for line in open(cheatsheet[1]):
if term in line:
match += ' ' + self._utils.highlight(term, line)
if match != '':
result += cheatsheet[0] + ":\n" + match + "\n"
return result

View File

@ -3,95 +3,98 @@ import os
import subprocess import subprocess
import sys import sys
def highlight(needle, haystack):
""" Highlights a search term matched within a line """
# if a highlight color is not configured, exit early class Utils:
if not 'CHEAT_HIGHLIGHT' in os.environ:
return haystack
# otherwise, attempt to import the termcolor library def __init__(self, config):
try: self._displaycolors = config.get_cheatcolors()
from termcolor import colored self._editor_executable = config.get_editor()
self._highlight_color = config.get_highlight()
# if the import fails, return uncolored text def highlight(self, needle, haystack):
except ImportError: """ Highlights a search term matched within a line """
return haystack
# if the import succeeds, colorize the needle in haystack # if a highlight color is not configured, exit early
return haystack.replace(needle, colored(needle, os.environ.get('CHEAT_HIGHLIGHT'))); if not self._highlight_color:
return haystack
# otherwise, attempt to import the termcolor library
def colorize(sheet_content):
""" Colorizes cheatsheet content if so configured """
# only colorize if configured to do so, and if stdout is a tty
if os.environ.get('CHEATCOLORS') != 'true' or not sys.stdout.isatty():
return sheet_content
# don't attempt to colorize an empty cheatsheet
if not sheet_content.strip():
return ""
# otherwise, attempt to import the pygments library
try:
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import TerminalFormatter
# if the import fails, return uncolored text
except ImportError:
return sheet_content
# otherwise, attempt to colorize
first_line = sheet_content.splitlines()[0]
lexer = get_lexer_by_name('bash')
# apply syntax-highlighting if the first line is a code-fence
if first_line.startswith('```'):
sheet_content = '\n'.join(sheet_content.split('\n')[1:-2])
try: try:
lexer = get_lexer_by_name(first_line[3:]) from termcolor import colored
except Exception:
pass
return highlight(sheet_content, lexer, TerminalFormatter()) # if the import fails, return uncolored text
except ImportError:
return haystack
# if the import succeeds, colorize the needle in haystack
return haystack.replace(needle, colored(needle, self._highlight_color))
def die(message): def colorize(self, sheet_content):
""" Prints a message to stderr and then terminates """ """ Colorizes cheatsheet content if so configured """
warn(message)
exit(1)
# cover all possible positive values to be safe
positive_values = ["True", "true", "1", 1, True]
def editor(): # only colorize if configured to do so, and if stdout is a tty
""" Determines the user's preferred editor """ if (self._displaycolors not in positive_values or
not sys.stdout.isatty()):
return sheet_content
# determine which editor to use # don't attempt to colorize an empty cheatsheet
editor = os.environ.get('CHEAT_EDITOR') \ if not sheet_content.strip():
or os.environ.get('VISUAL') \ return ""
or os.environ.get('EDITOR') \
or False
# assert that the editor is set # otherwise, attempt to import the pygments library
if editor == False: try:
die( from pygments import highlight
'You must set a CHEAT_EDITOR, VISUAL, or EDITOR environment ' from pygments.lexers import get_lexer_by_name
'variable in order to create/edit a cheatsheet.' from pygments.formatters import TerminalFormatter
)
return editor # if the import fails, return uncolored text
except ImportError:
return sheet_content
# otherwise, attempt to colorize
first_line = sheet_content.splitlines()[0]
lexer = get_lexer_by_name('bash')
def open_with_editor(filepath): # apply syntax-highlighting if the first line is a code-fence
""" Open `filepath` using the EDITOR specified by the environment variables """ if first_line.startswith('```'):
editor_cmd = editor().split() sheet_content = '\n'.join(sheet_content.split('\n')[1:-2])
try: try:
subprocess.call(editor_cmd + [filepath]) lexer = get_lexer_by_name(first_line[3:])
except OSError: except Exception:
die('Could not launch ' + editor()) pass
return highlight(sheet_content, lexer, TerminalFormatter())
def warn(message): @staticmethod
""" Prints a message to stderr """ def die(message):
print((message), file=sys.stderr) """ Prints a message to stderr and then terminates """
Utils.warn(message)
exit(1)
def editor(self):
""" Determines the user's preferred editor """
# assert that the editor is set
if (not self._editor_executable):
Utils.die(
'You must set a CHEAT_EDITOR, VISUAL, or EDITOR environment '
'variable or setting in order to create/edit a cheatsheet.'
)
return self._editor_executable
def open_with_editor(self, filepath):
""" Open `filepath` using the EDITOR specified by the env variables """
editor_cmd = self.editor().split()
try:
subprocess.call(editor_cmd + [filepath])
except OSError:
Utils.die('Could not launch ' + self.editor())
@staticmethod
def warn(message):
""" Prints a message to stderr """
print((message), file=sys.stderr)

1
config/cheat Normal file
View File

@ -0,0 +1 @@
{"CHEAT_COLORS":true,"CHEAT_EDITOR":"vi"}

View File

@ -1,6 +1,10 @@
from distutils.core import setup from distutils.core import setup
import os import os
cheat_files = []
for f in os.listdir('cheat/cheatsheets/'):
cheat_files.append(os.path.join('cheat/cheatsheets/',f))
setup( setup(
name = 'cheat', name = 'cheat',
version = '2.3.1', version = '2.3.1',
@ -14,15 +18,15 @@ setup(
url = 'https://github.com/chrisallenlane/cheat', url = 'https://github.com/chrisallenlane/cheat',
packages = [ packages = [
'cheat', 'cheat',
'cheat.cheatsheets',
'cheat.test', 'cheat.test',
], ],
package_data = {
'cheat.cheatsheets': [f for f in os.listdir('cheat/cheatsheets') if '.' not in f]
},
scripts = ['bin/cheat'], scripts = ['bin/cheat'],
install_requires = [ install_requires = [
'docopt >= 0.6.1', 'docopt >= 0.6.1',
'pygments >= 1.6.0', 'pygments >= 1.6.0',
] ],
data_files = [
('/usr/share/cheat', cheat_files),
('/etc', ['config/cheat']),
],
) )

View File

@ -0,0 +1,86 @@
import unittest2
import os
import shutil
from cheat.configuration import Configuration
def _set_loc_conf(key, value):
_path = os.path.dirname(os.path.abspath(__file__)) + '/home/.config/cheat/cheat'
if value == None:
os.remove(_path)
else:
if not os.path.exists(os.path.dirname(_path)):
os.makedirs(os.path.dirname(_path))
f = open(_path,"w+")
f.write('{"'+ key +'":"'+ value +'"}')
f.close()
def _set_glob_conf(key, value):
_path = os.path.dirname(os.path.abspath(__file__))+ "/etc/cheat"
if value == None:
os.remove(_path)
else:
if not os.path.exists(os.path.dirname(_path)):
os.mkdir(os.path.dirname(_path))
f = open(_path,"w+")
f.write('{"'+ key +'":"'+ value +'"}' )
f.close()
def _set_env_var(key, value):
if value == None:
del os.environ[key]
else:
os.environ[key] = value
def _configuration_key_test(TestConfiguration, key,values, conf_get_method):
for glob_conf in values:
_set_glob_conf(key,glob_conf)
for loc_conf in values:
_set_loc_conf(key,loc_conf)
for env_conf in values:
_set_env_var(key,env_conf)
if env_conf:
TestConfiguration.assertEqual(conf_get_method(Configuration()),env_conf)
elif loc_conf:
TestConfiguration.assertEqual(conf_get_method(Configuration()),loc_conf)
elif glob_conf:
TestConfiguration.assertEqual(conf_get_method(Configuration()),glob_conf)
else:
TestConfiguration.assertEqual(conf_get_method(Configuration()),None)
class ConfigurationTestCase(unittest2.TestCase):
def setUp(self):
os.environ['CHEAT_GLOBAL_CONF_PATH'] = os.path.dirname(os.path.abspath(__file__)) \
+ '/etc/cheat'
os.environ['CHEAT_LOCAL_CONF_PATH'] = os.path.dirname(os.path.abspath(__file__)) \
+ '/home/.config/cheat/cheat'
def test_get_editor(self):
_configuration_key_test(self,"EDITOR",["nano","vim","gedit",None],
Configuration.get_editor)
def test_get_cheatcolors(self):
_configuration_key_test(self,"CHEATCOLORS",["true",None],
Configuration.get_cheatcolors)
def test_get_cheatpath(self):
_configuration_key_test(self,"CHEATPATH",["/etc/myglobalcheats",
"/etc/anotherglobalcheats","/rootcheats",None],Configuration.get_cheatpath)
def test_get_defaultcheatdir(self):
_configuration_key_test(self,"DEFAULT_CHEAT_DIR",["/etc/myglobalcheats",
"/etc/anotherglobalcheats","/rootcheats",None],Configuration.get_default_cheat_dir)
def tearDown(self):
shutil.rmtree(os.path.dirname(os.path.abspath(__file__)) +'/etc')
shutil.rmtree(os.path.dirname(os.path.abspath(__file__)) +'/home')