mirror of https://github.com/cheat/cheat.git
Add classes for better readability
This commit is contained in:
parent
a651426075
commit
7814de96d2
16
bin/cheat
16
bin/cheat
|
@ -36,9 +36,10 @@ Examples:
|
|||
|
||||
# require the dependencies
|
||||
from __future__ import print_function
|
||||
from cheat import sheets, sheet
|
||||
from cheat.utils import colorize
|
||||
from cheat.utils import die
|
||||
from cheat.sheets import Sheets
|
||||
from cheat.sheet import Sheet
|
||||
from cheat.utils import Utils
|
||||
from cheat.configuration import Configuration
|
||||
from docopt import docopt
|
||||
import os
|
||||
|
||||
|
@ -59,6 +60,11 @@ if __name__ == '__main__':
|
|||
# parse the command-line options
|
||||
options = docopt(__doc__, version='cheat 2.3.1')
|
||||
|
||||
config = Configuration()
|
||||
sheets = Sheets(config.get_default_cheat_dir(),config.get_cheatpath())
|
||||
sheet = Sheet(config.get_default_cheat_dir(),config.get_cheatpath(),config.get_editor())
|
||||
utils = Utils(config.get_cheatcolors(),config.get_editor())
|
||||
|
||||
# list directories
|
||||
if options['--directories']:
|
||||
print("\n".join(sheets.paths()))
|
||||
|
@ -73,8 +79,8 @@ if __name__ == '__main__':
|
|||
|
||||
# search among the cheatsheets
|
||||
elif options['--search']:
|
||||
print(colorize(sheets.search(options['<keyword>'])), end="")
|
||||
print(utils.colorize(sheets.search(options['<keyword>'])), end="")
|
||||
|
||||
# print the cheatsheet
|
||||
else:
|
||||
print(colorize(sheet.read(options['<cheatsheet>'])), end="")
|
||||
print(utils.colorize(sheet.read(options['<cheatsheet>'])), end="")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
from cheat.utils import warn
|
||||
from cheat.utils import Utils
|
||||
import json
|
||||
|
||||
class Configuration:
|
||||
|
@ -15,12 +15,12 @@ class Configuration:
|
|||
try:
|
||||
merged_config.update(self._read_configuration_file('/etc/cheat'))
|
||||
except Exception:
|
||||
warn('error while parsing global configuration')
|
||||
Utils.warn('error while parsing global configuration')
|
||||
|
||||
try:
|
||||
merged_config.update(self._read_configuration_file(os.path.expanduser(os.path.join('~','.config','cheat','cheat'))))
|
||||
except Exception:
|
||||
warn('error while parsing user configuration')
|
||||
Utils.warn('error while parsing user configuration')
|
||||
|
||||
merged_config.update(self._read_env_vars_config())
|
||||
|
||||
|
|
121
cheat/sheet.py
121
cheat/sheet.py
|
@ -1,76 +1,85 @@
|
|||
import os
|
||||
import shutil
|
||||
|
||||
from cheat import sheets
|
||||
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.')
|
||||
from cheat.sheets import Sheets
|
||||
from cheat.utils import Utils
|
||||
|
||||
|
||||
def create_or_edit(sheet):
|
||||
""" Creates or edits a cheatsheet """
|
||||
|
||||
# if the cheatsheet does not exist
|
||||
if not exists(sheet):
|
||||
create(sheet)
|
||||
|
||||
# if the cheatsheet exists but not in the default_path, copy it to the
|
||||
# default path before editing
|
||||
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
|
||||
else:
|
||||
edit(sheet)
|
||||
class Sheet:
|
||||
|
||||
|
||||
def create(sheet):
|
||||
""" Creates a cheatsheet """
|
||||
new_sheet_path = os.path.join(sheets.default_path(), sheet)
|
||||
open_with_editor(new_sheet_path)
|
||||
def __init__(self,default_cheat_dir,cheatpath,editor_exec):
|
||||
self.sheets_instance = Sheets(default_cheat_dir,cheatpath)
|
||||
self.utils_instance = Utils(None,editor_exec)
|
||||
|
||||
|
||||
def edit(sheet):
|
||||
""" Opens a cheatsheet for editing """
|
||||
open_with_editor(path(sheet))
|
||||
def copy(self,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:
|
||||
Utils.die('Could not copy cheatsheet for editing.')
|
||||
|
||||
|
||||
def exists(sheet):
|
||||
""" Predicate that returns true if the sheet exists """
|
||||
return sheet in sheets.get() and os.access(path(sheet), os.R_OK)
|
||||
def create_or_edit(self,sheet):
|
||||
""" Creates or edits a cheatsheet """
|
||||
|
||||
# if the cheatsheet does not exist
|
||||
if not self.exists(sheet):
|
||||
self.create(sheet)
|
||||
|
||||
# if the cheatsheet exists but not in the default_path, copy it to the
|
||||
# default path before editing
|
||||
elif self.exists(sheet) and not self.exists_in_default_path(sheet):
|
||||
self.copy(self.path(sheet), os.path.join(self.sheets_instance.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_in_default_path(sheet):
|
||||
""" Predicate that returns true if the sheet exists in default_path"""
|
||||
default_path_sheet = os.path.join(sheets.default_path(), sheet)
|
||||
return sheet in sheets.get() and os.access(default_path_sheet, os.R_OK)
|
||||
def create(self,sheet):
|
||||
""" Creates a cheatsheet """
|
||||
new_sheet_path = os.path.join(self.sheets_instance.default_path(), sheet)
|
||||
self.utils_instance.open_with_editor(new_sheet_path)
|
||||
|
||||
|
||||
def is_writable(sheet):
|
||||
""" Predicate that returns true if the sheet is writeable """
|
||||
return sheet in sheets.get() and os.access(path(sheet), os.W_OK)
|
||||
def edit(self,sheet):
|
||||
""" Opens a cheatsheet for editing """
|
||||
self.utils_instance.open_with_editor(self.path(sheet))
|
||||
|
||||
|
||||
def path(sheet):
|
||||
""" Returns a sheet's filesystem path """
|
||||
return sheets.get()[sheet]
|
||||
def exists(self,sheet):
|
||||
""" Predicate that returns true if the sheet exists """
|
||||
return sheet in self.sheets_instance.get() and os.access(self.path(sheet), os.R_OK)
|
||||
|
||||
|
||||
def read(sheet):
|
||||
""" Returns the contents of the cheatsheet as a String """
|
||||
if not exists(sheet):
|
||||
die('No cheatsheet found for ' + sheet)
|
||||
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_instance.default_path(), sheet)
|
||||
return sheet in self.sheets_instance.get() and os.access(default_path_sheet, os.R_OK)
|
||||
|
||||
with open(path(sheet)) as cheatfile:
|
||||
return cheatfile.read()
|
||||
|
||||
def is_writable(self,sheet):
|
||||
""" Predicate that returns true if the sheet is writeable """
|
||||
return sheet in self.sheets_instance.get() and os.access(self.path(sheet), os.W_OK)
|
||||
|
||||
|
||||
def path(self,sheet):
|
||||
""" Returns a sheet's filesystem path """
|
||||
return self.sheets_instance.get()[sheet]
|
||||
|
||||
|
||||
def read(self,sheet):
|
||||
""" Returns the contents of the cheatsheet as a String """
|
||||
if not self.exists(sheet):
|
||||
Utils.die('No cheatsheet found for ' + sheet)
|
||||
|
||||
with open(self.path(sheet)) as cheatfile:
|
||||
return cheatfile.read()
|
||||
|
|
158
cheat/sheets.py
158
cheat/sheets.py
|
@ -1,94 +1,100 @@
|
|||
import os
|
||||
|
||||
from cheat.utils import die
|
||||
from cheat.utils import highlight
|
||||
from cheat.configuration import Configuration
|
||||
from cheat.utils import Utils
|
||||
|
||||
def default_path():
|
||||
""" Returns the default cheatsheet path """
|
||||
|
||||
# determine the default cheatsheet dir
|
||||
default_sheets_dir = Configuration().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
|
||||
class Sheets:
|
||||
|
||||
|
||||
def get():
|
||||
""" Assembles a dictionary of cheatsheets as name => file-path """
|
||||
cheats = {}
|
||||
|
||||
# otherwise, scan the filesystem
|
||||
for cheat_dir in reversed(paths()):
|
||||
cheats.update(
|
||||
dict([
|
||||
(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 __init__(self,default_cheat_dir,cheatpath):
|
||||
self.default_cheat_dir = default_cheat_dir
|
||||
self.cheatpath = cheatpath
|
||||
|
||||
|
||||
def paths():
|
||||
""" Assembles a list of directories containing cheatsheets """
|
||||
sheet_paths = [
|
||||
default_path(),
|
||||
'/usr/share/cheat',
|
||||
]
|
||||
def default_path(self):
|
||||
""" Returns the default cheatsheet path """
|
||||
|
||||
# merge the CHEATPATH paths into the sheet_paths
|
||||
if Configuration().get_cheatpath():
|
||||
for path in Configuration().get_cheatpath().split(os.pathsep):
|
||||
if os.path.isdir(path):
|
||||
sheet_paths.append(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))
|
||||
|
||||
if not sheet_paths:
|
||||
die('The DEFAULT_CHEAT_DIR dir does not exist or the CHEATPATH is not set.')
|
||||
# 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)
|
||||
|
||||
return sheet_paths
|
||||
except OSError:
|
||||
Utils.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):
|
||||
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 the default dir
|
||||
return default_sheets_dir
|
||||
|
||||
|
||||
def list():
|
||||
""" Lists the available cheatsheets """
|
||||
sheet_list = ''
|
||||
pad_length = max([len(x) for x in get().keys()]) + 4
|
||||
for sheet in sorted(get().items()):
|
||||
sheet_list += sheet[0].ljust(pad_length) + sheet[1] + "\n"
|
||||
return sheet_list
|
||||
def get(self):
|
||||
""" Assembles a dictionary of cheatsheets as name => file-path """
|
||||
cheats = {}
|
||||
|
||||
# otherwise, scan the filesystem
|
||||
for cheat_dir in reversed(self.paths()):
|
||||
cheats.update(
|
||||
dict([
|
||||
(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 search(term):
|
||||
""" Searches all cheatsheets for the specified term """
|
||||
result = ''
|
||||
lowered_term = term.lower()
|
||||
def paths(self):
|
||||
""" Assembles a list of directories containing cheatsheets """
|
||||
sheet_paths = [
|
||||
self.default_path(),
|
||||
'/usr/share/cheat',
|
||||
]
|
||||
|
||||
for cheatsheet in sorted(get().items()):
|
||||
match = ''
|
||||
for line in open(cheatsheet[1]):
|
||||
if term in line:
|
||||
match += ' ' + highlight(term, line)
|
||||
# merge the CHEATPATH paths into the sheet_paths
|
||||
if self.cheatpath:
|
||||
for path in self.cheatpath.split(os.pathsep):
|
||||
if os.path.isdir(path):
|
||||
sheet_paths.append(path)
|
||||
|
||||
if match != '':
|
||||
result += cheatsheet[0] + ":\n" + match + "\n"
|
||||
if not sheet_paths:
|
||||
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 += ' ' + line
|
||||
|
||||
if match != '':
|
||||
result += cheatsheet[0] + ":\n" + match + "\n"
|
||||
|
||||
return result
|
||||
|
|
155
cheat/utils.py
155
cheat/utils.py
|
@ -3,94 +3,99 @@ import os
|
|||
import subprocess
|
||||
import sys
|
||||
|
||||
from cheat.configuration import Configuration
|
||||
|
||||
def highlight(needle, haystack):
|
||||
""" Highlights a search term matched within a line """
|
||||
|
||||
# if a highlight color is not configured, exit early
|
||||
if not 'CHEAT_HIGHLIGHT' in os.environ:
|
||||
return haystack
|
||||
|
||||
# otherwise, attempt to import the termcolor library
|
||||
try:
|
||||
from termcolor import colored
|
||||
|
||||
# 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, os.environ.get('CHEAT_HIGHLIGHT')));
|
||||
class Utils:
|
||||
|
||||
|
||||
def colorize(sheet_content):
|
||||
""" Colorizes cheatsheet content if so configured """
|
||||
def __init__(self,cheatcolors,editor_executable):
|
||||
self.displaycolors = cheatcolors
|
||||
self.editor_executable = editor_executable
|
||||
|
||||
# only colorize if configured to do so, and if stdout is a tty
|
||||
if not Configuration().get_cheatcolors() or not sys.stdout.isatty():
|
||||
return sheet_content
|
||||
|
||||
# don't attempt to colorize an empty cheatsheet
|
||||
if not sheet_content.strip():
|
||||
return ""
|
||||
def highlight(self, needle, haystack):
|
||||
""" Highlights a search term matched within a line """
|
||||
|
||||
# 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 a highlight color is not configured, exit early
|
||||
if not 'CHEAT_HIGHLIGHT' in os.environ:
|
||||
return haystack
|
||||
|
||||
# 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])
|
||||
# otherwise, attempt to import the termcolor library
|
||||
try:
|
||||
lexer = get_lexer_by_name(first_line[3:])
|
||||
except Exception:
|
||||
pass
|
||||
from termcolor import colored
|
||||
|
||||
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, os.environ.get('CHEAT_HIGHLIGHT')))
|
||||
|
||||
|
||||
def die(message):
|
||||
""" Prints a message to stderr and then terminates """
|
||||
warn(message)
|
||||
exit(1)
|
||||
def colorize(self,sheet_content):
|
||||
""" Colorizes cheatsheet content if so configured """
|
||||
|
||||
# only colorize if configured to do so, and if stdout is a tty
|
||||
if not self.displaycolors 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:
|
||||
lexer = get_lexer_by_name(first_line[3:])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return highlight(sheet_content, lexer, TerminalFormatter())
|
||||
|
||||
|
||||
def editor():
|
||||
""" Determines the user's preferred editor """
|
||||
|
||||
# determine which editor to use
|
||||
editor = Configuration().get_editor()
|
||||
|
||||
# assert that the editor is set
|
||||
if (not editor):
|
||||
die(
|
||||
'You must set a CHEAT_EDITOR, VISUAL, or EDITOR environment '
|
||||
'variable or setting in order to create/edit a cheatsheet.'
|
||||
)
|
||||
|
||||
return editor
|
||||
@staticmethod
|
||||
def die(message):
|
||||
""" Prints a message to stderr and then terminates """
|
||||
Utils.warn(message)
|
||||
exit(1)
|
||||
|
||||
|
||||
def open_with_editor(filepath):
|
||||
""" Open `filepath` using the EDITOR specified by the environment variables """
|
||||
editor_cmd = editor().split()
|
||||
try:
|
||||
subprocess.call(editor_cmd + [filepath])
|
||||
except OSError:
|
||||
die('Could not launch ' + editor())
|
||||
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 warn(message):
|
||||
""" Prints a message to stderr """
|
||||
print((message), file=sys.stderr)
|
||||
def open_with_editor(self,filepath):
|
||||
""" Open `filepath` using the EDITOR specified by the environment 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)
|
||||
|
|
Loading…
Reference in New Issue