Compare commits

..

39 Commits
2.4.2 ... 2.5.1

Author SHA1 Message Date
5487314676 Version bump: 2.5.1 2019-02-13 12:42:58 -05:00
ec360ccddd Merge pull request #435 from roachsinai/master
Expand ~ for 'CHEAT_USER_DIR', 'CHEAT_DEFAULT_DIR' and 'DEFAULT_CHEAT…
2019-02-13 12:40:25 -05:00
bb7dfd1028 Merge pull request #436 from avsej/patch-1
Fix typo in gcc sheet
2019-02-13 12:31:31 -05:00
b348724082 Update gcc 2019-02-13 13:52:33 +00:00
5053f07fd8 Expand ~ for 'CHEAT_USER_DIR', 'CHEAT_DEFAULT_DIR' and 'DEFAULT_CHEAT_DIR'. 2019-02-12 15:16:59 +08:00
4dd55105d2 Merge pull request #361 from aroonav/master
Enable starting the cheat python script on windows.
2019-02-06 10:25:29 -05:00
6148d64599 Merge pull request #430 from movd/rename
Update rename
2019-02-05 08:59:06 -05:00
cde8bcaa1d Update rename
Add cheat for mass rename with search and replace
2019-02-05 10:57:43 +01:00
fcb82778e4 * Corrected the logic to execute the cheat script.
* Check VISUAL, CHEAT_EDITOR, EDITOR environment variables before falling
back to wordpad as the default editor.
2019-02-05 12:18:26 +05:30
da92421948 Merge remote-tracking branch 'upstream/master' 2019-02-05 10:14:39 +05:30
d6c7863573 Merge pull request #428 from dufferzafar/patch-2
ssh: Improve SOCKS command
2019-02-04 14:54:52 -05:00
5812bca6b7 ssh: Improve SOCKS command
This doesn't give an SSH shell, but just forwards the ports - which is what one usually requires when setting up a SOCKS proxy.

The -q is to suppress messages etc.
2019-02-04 23:45:16 +05:30
074dba6e99 v2.5.0
`minor` version bump to `2.5.0`.
2019-02-04 12:04:52 -05:00
9d1dd15387 Merge pull request #427 from cheat/issue-349
Issue #349
2019-02-04 11:57:35 -05:00
caf355f142 Issue #349
Implements support for terminals with light backgrounds via a new
`CHEAT_COLORSCHEME` envvar.
2019-02-04 11:56:00 -05:00
2728ce4757 Merge pull request #426 from cheat/vagrant
Updated Vagrantfile
2019-02-04 11:54:06 -05:00
6ae76799f7 Updated Vagrantfile
Modified `Vagrantfile` to build an Ubuntu environment rather than
Alpine, after the latter exhibited weird behavior.
2019-02-04 11:50:25 -05:00
0b523a769f Merge pull request #425 from cheat/vagrant
Added Vagrantfile
2019-02-04 10:41:26 -05:00
f29cf03b68 Added Vagrantfile
Added a `Vagrantfile` which builds an alpine-based environment that can
be used for development and testing.
2019-02-04 10:40:10 -05:00
4347114e19 Merge pull request #424 from JensKorte/patch-1
link changed, added details
2019-02-04 10:05:36 -05:00
edc67e7819 Updated links to README
Re-pathed links from `chrisallenlane/cheat` to `cheat/cheat` as
appropriate, following the move.
2019-02-04 10:03:19 -05:00
aa33a36491 Merge pull request #422 from cheat/refactor
Refactor
2019-02-04 09:58:24 -05:00
8aac10dd8b link changed, added details
The table isn't available any more in the recent wiki page. The new link uses the old version of wikipedia. In my browser I have to "unhide" the box.
2019-02-03 20:56:57 +01:00
9931b78c5f Lint
- Added instruction to lint `setup.py` to `ci/lint.sh`
- Updated `setup.py` per linter suggestions
2019-02-01 15:24:04 -05:00
a37577ee85 Trivial: docstrings
Updated some docstring comments.
2019-02-01 15:18:23 -05:00
3ad923eff0 Refactored (11)
Renamed `CHEAT_DEFAULT_DIR` to `CHEAT_USER_DIR` because the latter more
accurately describes the purpose of the variable.
2019-02-01 15:10:03 -05:00
ba47dc2cbc Refactored (10)
- Added `ci/lint.sh`, which uses `flake8` to lint the relevant files
- Made changes to appease the linter.
- Bugfix in `cheat/configuration` (missing dependency)
2019-02-01 14:44:48 -05:00
df21731c02 Trivial Python style corrections 2019-02-01 11:43:38 -05:00
a657699a24 Refactored (9)
Moved some functionality into the `Util` class.
2019-02-01 11:43:11 -05:00
5793c1845a Refactored (8)
Refactored `Sheet` class:

- Removed unnecessary indirection and extraneous methods
- Renamed some methods to conform to Pythonic conventions
- Renamed the `create_or_edit` method to `edit` to be consistent with
  subcommand name (`--edit`)
2019-02-01 11:23:38 -05:00
e2b5728283 Refactored (7)
Refactored for general code-clarity, with particular focus on removing
needless abstraction within `Sheet` and `Sheets` classes.
2019-01-31 20:03:56 -05:00
d61e4e7c34 Refactored (6)
Standardized (mostly) how the various classes are initialized.
2019-01-31 18:08:19 -05:00
145a81dcd6 Var renames
Replaced more references to deprecated envvar names to their newer
counterparts.
2019-01-31 17:55:26 -05:00
7c4fc54681 Refactored (5)
- Extracted `Colorize` class out of `Util` class. (The latter now only
  contains static methods.)
- Renamed methods in `Colorize` class for improved clarity.
- Refactored as necessary to accommodate the changes above.
2019-01-31 17:43:21 -05:00
878d7e7e1b Refactored (4)
Improved handling of edge-cases in `configuration.py`.
2019-01-31 17:40:53 -05:00
928637c9db Refactored (3)
Removed unnecessary `import` calls.
2019-01-31 17:14:21 -05:00
ab87bb11c4 Refactored (2)
Created an `Editor` class out methods in the `Util` class to enhance
code clarity.
2019-01-31 17:03:21 -05:00
8f757d7735 Refactored (1)
Performed a general refactoring, focusing on the following:

- Removing layers of abstraction in config handling
- Stubbing out proper config validator
- Updating envvar names located throughout the project
2019-01-31 16:45:28 -05:00
d048ea5a10 Enable starting the cheat python script on windows.
For this add a small batch script in the same directory as the
cheat script.
2017-11-12 18:48:04 +05:30
19 changed files with 382 additions and 343 deletions

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
*.log
*.pyc *.pyc
.env .env
.vagrant
MANIFEST MANIFEST
build build
cheat.egg-info cheat.egg-info

View File

@ -83,13 +83,13 @@ with your [dotfiles][].
Configuring Configuring
----------- -----------
### Setting a CHEAT_DEFAULT_DIR ### ### Setting a CHEAT_USER_DIR ###
Personal cheatsheets are saved in the `~/.cheat` directory by default, but you Personal cheatsheets are saved in the `~/.cheat` directory by default, but you
can specify a different default by exporting a `CHEAT_DEFAULT_DIR` environment can specify a different default by exporting a `CHEAT_USER_DIR` environment
variable: variable:
```sh ```sh
export CHEAT_DEFAULT_DIR='/path/to/my/cheats' export CHEAT_USER_DIR='/path/to/my/cheats'
``` ```
### Setting a CHEAT_PATH ### ### Setting a CHEAT_PATH ###
@ -118,6 +118,13 @@ export CHEAT_COLORS=true
Note that [pygments][] must be installed on your system for this to work. Note that [pygments][] must be installed on your system for this to work.
`cheat` ships with both light and dark colorschemes to support terminals with
different background colors. A colorscheme may be selected via the
`CHEAT_COLORSCHEME` envvar:
```sh
export CHEAT_COLORSCHEME=light # must be 'light' (default) or 'dark'
```
#### Specifying a Syntax Highlighter #### #### Specifying a Syntax Highlighter ####
You may manually specify which syntax highlighter to use for each cheatsheet by You may manually specify which syntax highlighter to use for each cheatsheet by
@ -161,9 +168,9 @@ See Also:
- [Related Projects][related-projects] - [Related Projects][related-projects]
[autocompletion]: https://github.com/chrisallenlane/cheat/wiki/Enabling-Command-line-Autocompletion [autocompletion]: https://github.com/cheat/cheat/wiki/Enabling-Command-line-Autocompletion
[dotfiles]: http://dotfiles.github.io/ [dotfiles]: http://dotfiles.github.io/
[gfm]: https://help.github.com/articles/creating-and-highlighting-code-blocks/ [gfm]: https://help.github.com/articles/creating-and-highlighting-code-blocks/
[installing]: https://github.com/chrisallenlane/cheat/wiki/Installing [installing]: https://github.com/cheat/cheat/wiki/Installing
[pygments]: http://pygments.org/ [pygments]: http://pygments.org/
[related-projects]: https://github.com/chrisallenlane/cheat/wiki/Related-Projects [related-projects]: https://github.com/cheat/cheat/wiki/Related-Projects

17
Vagrantfile vendored Normal file
View File

@ -0,0 +1,17 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/bionic64"
config.vm.provider "virtualbox" do |vb|
vb.memory = "512"
end
config.vm.provision "shell", inline: <<-SHELL
sudo apt-get update
sudo apt-get install -y python-pip
su vagrant && sudo -H pip install docopt pygments termcolor flake8
cd /vagrant && sudo python setup.py install
SHELL
end

View File

@ -13,7 +13,7 @@ Usage:
cheat -v cheat -v
Options: Options:
-d --directories List directories on CHEATPATH -d --directories List directories on $CHEAT_PATH
-e --edit Edit cheatsheet -e --edit Edit cheatsheet
-l --list List cheatsheets -l --list List cheatsheets
-s --search Search cheatsheets for <keyword> -s --search Search cheatsheets for <keyword>
@ -36,27 +36,57 @@ Examples:
# require the dependencies # require the dependencies
from __future__ import print_function from __future__ import print_function
from cheat.sheets import Sheets from cheat.colorize import Colorize
from cheat.sheet import Sheet
from cheat.utils import Utils
from cheat.configuration import Configuration from cheat.configuration import Configuration
from cheat.sheet import Sheet
from cheat.sheets import Sheets
from cheat.utils import Utils
from docopt import docopt from docopt import docopt
import os import os
if __name__ == '__main__': if __name__ == '__main__':
# parse the command-line options # parse the command-line options
options = docopt(__doc__, version='cheat 2.4.2') options = docopt(__doc__, version='cheat 2.5.1')
# initialize and validate configs
config = Configuration() config = Configuration()
config.validate()
# create the CHEAT_USER_DIR if it does not exist
if not os.path.isdir(config.cheat_user_dir):
try:
os.mkdir(config.cheat_user_dir)
except OSError:
Utils.die("%s %s %s" % (
'Could not create CHEAT_USER_DIR (',
config.cheat_user_dir,
')')
)
# assert that the CHEAT_USER_DIR is readable and writable
if not os.access(config.cheat_user_dir, os.R_OK):
Utils.die("%s %s %s" % (
'The CHEAT_USER_DIR (',
config.cheat_user_dir,
') is not readable')
)
if not os.access(config.cheat_user_dir, os.W_OK):
Utils.die("%s %s %s" % (
'The CHEAT_USER_DIR (',
config.cheat_user_dir,
') is not writeable')
)
# bootsrap
sheets = Sheets(config) sheets = Sheets(config)
utils = Utils(config) sheet = Sheet(config, sheets)
sheet = Sheet(sheets, utils) colorize = Colorize(config)
# list directories # list directories
if options['--directories']: if options['--directories']:
print("\n".join(sheets.paths())) print("\n".join(sheets.directories()))
# list cheatsheets # list cheatsheets
elif options['--list']: elif options['--list']:
@ -64,12 +94,12 @@ if __name__ == '__main__':
# create/edit cheatsheet # create/edit cheatsheet
elif options['--edit']: elif options['--edit']:
sheet.create_or_edit(options['<cheatsheet>']) sheet.edit(options['<cheatsheet>'])
# search among the cheatsheets # search among the cheatsheets
elif options['--search']: elif options['--search']:
print(utils.colorize(sheets.search(options['<keyword>'])), end="") print(colorize.syntax(sheets.search(options['<keyword>'])), end="")
# print the cheatsheet # print the cheatsheet
else: else:
print(utils.colorize(sheet.read(options['<cheatsheet>'])), end="") print(colorize.syntax(sheet.read(options['<cheatsheet>'])), end="")

8
bin/cheat.bat Normal file
View File

@ -0,0 +1,8 @@
@echo OFF
if not defined CHEAT_EDITOR if not defined EDITOR if not defined VISUAL (
set CHEAT_EDITOR=write
)
REM %~dp0 is black magic for getting directory of script
python %~dp0cheat %*

View File

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

View File

@ -10,7 +10,7 @@ gcc -g
# Debug with all symbols. # Debug with all symbols.
gcc -ggdb3 gcc -ggdb3
# Build for 64 bytes # Build for 64 bits
gcc -m64 gcc -m64
# Include the directory {/usr/include/myPersonnal/lib/} to the list of path for #include <....> # Include the directory {/usr/include/myPersonnal/lib/} to the list of path for #include <....>

View File

@ -1,2 +1,5 @@
# Lowercase all files and folders in current directory # Lowercase all files and folders in current directory
rename 'y/A-Z/a-z/' * rename 'y/A-Z/a-z/' *
# Replace 'sometext' with 'replacedby' in all files in current directory
rename 's/sometext/replacedby/' *

View File

@ -21,7 +21,7 @@ ssh -f -L 8080:remote.example.com:5000 user@personal.server.com -N
ssh -X -t user@example.com 'chromium-browser' ssh -X -t user@example.com 'chromium-browser'
# To create a SOCKS proxy on localhost and port 9999 # To create a SOCKS proxy on localhost and port 9999
ssh -D 9999 user@example.com ssh -qND 9999 user@example.com
# To tunnel an ssh session over the SOCKS proxy on localhost and port 9999 # To tunnel an ssh session over the SOCKS proxy on localhost and port 9999
ssh -o "ProxyCommand nc -x 127.0.0.1:9999 -X 4 %h %p" username@example2.com ssh -o "ProxyCommand nc -x 127.0.0.1:9999 -X 4 %h %p" username@example2.com

View File

@ -19,5 +19,5 @@ youtube-dl -s example.com/watch?v=id
# To download audio in mp3 format with best quality available # To download audio in mp3 format with best quality available
youtube-dl --extract-audio --audio-format mp3 --audio-quality 0 example.com/watch?v=id youtube-dl --extract-audio --audio-format mp3 --audio-quality 0 example.com/watch?v=id
# For all video formats see # For all video formats see link below (unfold "Comparison of YouTube media encoding options")
# http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs # https://en.wikipedia.org/w/index.php?title=YouTube&oldid=723160791#Quality_and_formats

65
cheat/colorize.py Normal file
View File

@ -0,0 +1,65 @@
from __future__ import print_function
import sys
class Colorize:
def __init__(self, config):
self._config = config
def search(self, needle, haystack):
""" Colorizes search results matched within a line """
# if a highlight color is not configured, exit early
if not self._config.cheat_highlight:
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, self._config.cheat_highlight))
def syntax(self, sheet_content):
""" Applies syntax highlighting """
# only colorize if cheat_colors is true, and stdout is a tty
if self._config.cheat_colors is False 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(bg=self._config.cheat_colorscheme))

View File

@ -1,134 +1,119 @@
import os
from cheat.utils import Utils from cheat.utils import Utils
import json import json
import os
class Configuration: class Configuration:
def __init__(self): def __init__(self):
self._get_global_conf_file_path() # compute the location of the config files
self._get_local_conf_file_path() config_file_path_global = self._select([
self._saved_configuration = self._get_configuration() os.environ.get('CHEAT_GLOBAL_CONF_PATH'),
'/etc/cheat',
def _get_configuration(self): ])
# get options from config files and environment vairables config_file_path_local = self._select([
merged_config = {} os.environ.get('CHEAT_LOCAL_CONF_PATH'),
os.path.expanduser('~/.config/cheat/cheat'),
])
# attempt to read the global config file
config = {}
try: try:
merged_config.update( config.update(self._read_config_file(config_file_path_global))
self._read_configuration_file(self.glob_config_path)
)
except Exception as e: except Exception as e:
Utils.warn('error while parsing global configuration Reason: ' Utils.warn('Error while parsing global configuration: '
+ e.message + e.message)
)
# attempt to read the local config file
try: try:
merged_config.update( config.update(self._read_config_file(config_file_path_local))
self._read_configuration_file(self.local_config_path)
)
except Exception as e: except Exception as e:
Utils.warn('error while parsing user configuration Reason: ' Utils.warn('Error while parsing local configuration: ' + e.message)
+ e.message
)
merged_config.update(self._read_env_vars_config()) # With config files read, now begin to apply envvar overrides and
# default values
self._check_configuration(merged_config) # self.cheat_colors
self.cheat_colors = self._select([
Utils.boolify(os.environ.get('CHEAT_COLORS')),
Utils.boolify(os.environ.get('CHEATCOLORS')),
Utils.boolify(config.get('CHEAT_COLORS')),
True,
])
return merged_config # self.cheat_colorscheme
self.cheat_colorscheme = self._select([
os.environ.get('CHEAT_COLORSCHEME'),
config.get('CHEAT_COLORSCHEME'),
'light',
]).strip().lower()
def _read_configuration_file(self, path): # self.cheat_user_dir
# Reads configuration file and returns list of set variables self.cheat_user_dir = self._select(
read_config = {} map(os.path.expanduser,
if (os.path.isfile(path)): filter(None,
[os.environ.get('CHEAT_USER_DIR'),
os.environ.get('CHEAT_DEFAULT_DIR'),
os.environ.get('DEFAULT_CHEAT_DIR'),
# TODO: XDG home?
os.path.join('~', '.cheat')])))
# self.cheat_editor
self.cheat_editor = self._select([
os.environ.get('CHEAT_EDITOR'),
os.environ.get('EDITOR'),
os.environ.get('VISUAL'),
config.get('CHEAT_EDITOR'),
'vi',
])
# self.cheat_highlight
self.cheat_highlight = self._select([
os.environ.get('CHEAT_HIGHLIGHT'),
config.get('CHEAT_HIGHLIGHT'),
False,
])
if isinstance(self.cheat_highlight, str):
Utils.boolify(self.cheat_highlight)
# self.cheat_path
self.cheat_path = self._select([
os.environ.get('CHEAT_PATH'),
os.environ.get('CHEATPATH'),
config.get('CHEAT_PATH'),
'/usr/share/cheat',
])
def _read_config_file(self, path):
""" Reads configuration file and returns list of set variables """
config = {}
if os.path.isfile(path):
with open(path) as config_file: with open(path) as config_file:
read_config.update(json.load(config_file)) config.update(json.load(config_file))
return read_config return config
def _read_env_vars_config(self): def _select(self, values):
read_config = {} for v in values:
if v is not None:
return v
# NOTE: These variables are left here because of backwards def validate(self):
# compatibility and are supported only as env vars but not in """ Validates configuration parameters """
# configuration file
if (os.environ.get('VISUAL')): # assert that cheat_highlight contains a valid value
read_config['EDITOR'] = os.environ.get('VISUAL') highlights = [
# 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', 'grey', 'red', 'green', 'yellow',
'blue', 'magenta', 'cyan', 'white' 'blue', 'magenta', 'cyan', 'white',
False
] ]
if ( if self.cheat_highlight not in highlights:
config.get('CHEAT_HIGHLIGHT') and Utils.die("%s %s" %
config.get('CHEAT_HIGHLIGHT') not in colors ('CHEAT_HIGHLIGHT must be one of:', highlights))
):
Utils.die("%s %s" % ('CHEAT_HIGHLIGHT must be one of:', colors))
def _read_env_var(self, current_config, key, alias=None): # assert that the color scheme is valid
if os.environ.get(key) is not None: colorschemes = ['light', 'dark']
current_config[key] = os.environ.get(key) if self.cheat_colorscheme not in colorschemes:
return Utils.die("%s %s" %
elif alias is not None and os.environ.get(alias) is not None: ('CHEAT_COLORSCHEME must be one of:', colorschemes))
current_config[key] = os.environ.get(alias)
return
def _get_global_conf_file_path(self): return True
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')

29
cheat/editor.py Normal file
View File

@ -0,0 +1,29 @@
from __future__ import print_function
from cheat.utils import Utils
import subprocess
class Editor:
def __init__(self, config):
self._config = config
def editor(self):
""" Determines the user's preferred editor """
# assert that the editor is set
if not self._config.cheat_editor:
Utils.die(
'You must set a CHEAT_EDITOR, VISUAL, or EDITOR environment '
'variable or setting in order to create/edit a cheatsheet.'
)
return self._config.cheat_editor
def open(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())

View File

@ -1,79 +1,64 @@
from cheat.editor import Editor
from cheat.utils import Utils
import io import io
import os import os
import shutil import shutil
from cheat.utils import Utils
class Sheet: class Sheet:
def __init__(self, sheets, utils): def __init__(self, config, sheets):
self._config = config
self._editor = Editor(config)
self._sheets = sheets self._sheets = sheets
self._utils = utils
def copy(self, current_sheet_path, new_sheet_path): def _exists(self, sheet):
""" 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 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.default_path(), sheet))
self.edit(sheet)
# if it exists and is in the default path, then just open it
else:
self.edit(sheet)
def create(self, sheet):
""" Creates a cheatsheet """
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(self, sheet):
""" Predicate that returns true if the sheet exists """ """ Predicate that returns true if the sheet exists """
return (sheet in self._sheets.get() and return (sheet in self._sheets.get() and
os.access(self.path(sheet), os.R_OK)) os.access(self._path(sheet), os.R_OK))
def exists_in_default_path(self, sheet): def _exists_in_default_path(self, sheet):
""" Predicate that returns true if the sheet exists in default_path""" """ Predicate that returns true if the sheet exists in default_path"""
default_path_sheet = os.path.join(self._sheets.default_path(), sheet) default_path = os.path.join(self._config.cheat_user_dir, sheet)
return (sheet in self._sheets.get() and return (sheet in self._sheets.get() and
os.access(default_path_sheet, os.R_OK)) os.access(default_path, os.R_OK))
def is_writable(self, sheet): def _path(self, sheet):
""" Predicate that returns true if the sheet is writeable """
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 """ """ Returns a sheet's filesystem path """
return self._sheets.get()[sheet] return self._sheets.get()[sheet]
def edit(self, sheet):
""" Creates or edits a cheatsheet """
# if the cheatsheet does not exist
if not self._exists(sheet):
new_path = os.path.join(self._config.cheat_user_dir, sheet)
self._editor.open(new_path)
# 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):
try:
shutil.copy(
self._path(sheet),
os.path.join(self._config.cheat_user_dir, sheet)
)
# fail gracefully if the cheatsheet cannot be copied. This can
# happen if CHEAT_USER_DIR does not exist
except IOError:
Utils.die('Could not copy cheatsheet for editing.')
self._editor.open(self._path(sheet))
# if it exists and is in the default path, then just open it
else:
self._editor.open(self._path(sheet))
def read(self, sheet): def read(self, sheet):
""" Returns the contents of the cheatsheet as a String """ """ Returns the contents of the cheatsheet as a String """
if not self.exists(sheet): if not self._exists(sheet):
Utils.die('No cheatsheet found for ' + sheet) Utils.die('No cheatsheet found for ' + sheet)
with io.open(self.path(sheet), encoding='utf-8') as cheatfile: with io.open(self._path(sheet), encoding='utf-8') as cheatfile:
return cheatfile.read() return cheatfile.read()

View File

@ -1,55 +1,34 @@
from cheat.colorize import Colorize
from cheat.utils import Utils
import io import io
import os import os
from cheat.utils import Utils
class Sheets: class Sheets:
def __init__(self, config): def __init__(self, config):
self._default_cheat_dir = config.get_default_cheat_dir() self._config = config
self._cheatpath = config.get_cheatpath() self._colorize = Colorize(config)
self._utils = Utils(config)
def default_path(self): # Assembles a dictionary of cheatsheets as name => file-path
""" Returns the default cheatsheet path """ self._sheets = {}
sheet_paths = [
config.cheat_user_dir
]
# determine the default cheatsheet dir # merge the CHEAT_PATH paths into the sheet_paths
default_sheets_dir = (self._default_cheat_dir or if config.cheat_path:
os.path.join('~', '.cheat')) for path in config.cheat_path.split(os.pathsep):
default_sheets_dir = os.path.expanduser( if os.path.isdir(path):
os.path.expandvars(default_sheets_dir)) sheet_paths.append(path)
# create the DEFAULT_CHEAT_DIR if it does not exist if not sheet_paths:
if not os.path.isdir(default_sheets_dir): Utils.die('The CHEAT_USER_DIR dir does not exist '
try: + 'or the CHEAT_PATH is not set.')
# @kludge: unclear on why this is necessary
os.umask(0000)
os.mkdir(default_sheets_dir)
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 get(self):
""" Assembles a dictionary of cheatsheets as name => file-path """
cheats = {}
# otherwise, scan the filesystem # otherwise, scan the filesystem
for cheat_dir in reversed(self.paths()): for cheat_dir in reversed(sheet_paths):
cheats.update( self._sheets.update(
dict([ dict([
(cheat, os.path.join(cheat_dir, cheat)) (cheat, os.path.join(cheat_dir, cheat))
for cheat in os.listdir(cheat_dir) for cheat in os.listdir(cheat_dir)
@ -58,26 +37,22 @@ class Sheets:
]) ])
) )
return cheats def directories(self):
def paths(self):
""" Assembles a list of directories containing cheatsheets """ """ Assembles a list of directories containing cheatsheets """
sheet_paths = [ sheet_paths = [
self.default_path(), self._config.cheat_user_dir,
] ]
# merge the CHEATPATH paths into the sheet_paths # merge the CHEATPATH paths into the sheet_paths
if self._cheatpath: for path in self._config.cheat_path.split(os.pathsep):
for path in self._cheatpath.split(os.pathsep): sheet_paths.append(path)
if os.path.isdir(path):
sheet_paths.append(path)
if not sheet_paths:
Utils.die('The DEFAULT_CHEAT_DIR dir does not exist '
+ 'or the CHEATPATH is not set.')
return sheet_paths return sheet_paths
def get(self):
""" Returns a dictionary of cheatsheets as name => file-path """
return self._sheets
def list(self): def list(self):
""" Lists the available cheatsheets """ """ Lists the available cheatsheets """
sheet_list = '' sheet_list = ''
@ -94,7 +69,7 @@ class Sheets:
match = '' match = ''
for line in io.open(cheatsheet[1], encoding='utf-8'): for line in io.open(cheatsheet[1], encoding='utf-8'):
if term in line: if term in line:
match += ' ' + self._utils.highlight(term, line) match += ' ' + self._colorize.search(term, line)
if match != '': if match != '':
result += cheatsheet[0] + ":\n" + match + "\n" result += cheatsheet[0] + ":\n" + match + "\n"

View File

@ -1,100 +1,26 @@
from __future__ import print_function from __future__ import print_function
import os
import subprocess
import sys import sys
class Utils: class Utils:
def __init__(self, config):
self._displaycolors = config.get_cheatcolors()
self._editor_executable = config.get_editor()
self._highlight_color = config.get_highlight()
def highlight(self, needle, haystack):
""" Highlights a search term matched within a line """
# if a highlight color is not configured, exit early
if not self._highlight_color:
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, self._highlight_color))
def colorize(self, sheet_content):
""" Colorizes cheatsheet content if so configured """
# cover all possible positive values to be safe
positive_values = ["True", "true", "1", 1, True]
# only colorize if configured to do so, and if stdout is a tty
if (self._displaycolors not in positive_values 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())
@staticmethod @staticmethod
def die(message): def die(message):
""" Prints a message to stderr and then terminates """ """ Prints a message to stderr and then terminates """
Utils.warn(message) Utils.warn(message)
exit(1) 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 @staticmethod
def warn(message): def warn(message):
""" Prints a message to stderr """ """ Prints a message to stderr """
print((message), file=sys.stderr) print((message), file=sys.stderr)
@staticmethod
def boolify(value):
""" Type-converts 'true' and 'false' to Booleans """
# if `value` is not a string, return it as-is
if not isinstance(value, str):
return value
# otherwise, convert "true" and "false" to Boolean counterparts
return value.strip().lower() == "true"

10
ci/lint.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/sh
# Resolve the app root
SCRIPT=`realpath $0`
SCRIPTPATH=`dirname $SCRIPT`
APPROOT=`realpath "$SCRIPTPATH/.."`
flake8 $APPROOT/setup.py
flake8 $APPROOT/bin/cheat
flake8 $APPROOT/cheat/*.py

View File

@ -1,5 +1,6 @@
{ {
"CHEAT_COLORS": true, "CHEAT_COLORS" : true,
"CHEAT_EDITOR":"vi", "CHEAT_COLORSCHEME" : "light",
"CHEAT_PATH":"/usr/share/cheat" "CHEAT_EDITOR" : "vi",
"CHEAT_PATH" : "/usr/share/cheat"
} }

View File

@ -9,31 +9,31 @@ cheat_path = os.environ.get('CHEAT_PATH') or '/usr/share/cheat'
# aggregate the systme-wide cheatsheets # aggregate the systme-wide cheatsheets
cheat_files = [] cheat_files = []
for f in os.listdir('cheat/cheatsheets/'): for f in os.listdir('cheat/cheatsheets/'):
cheat_files.append(os.path.join('cheat/cheatsheets/',f)) cheat_files.append(os.path.join('cheat/cheatsheets/', f))
# specify build params # specify build params
setup( setup(
name = 'cheat', name='cheat',
version = '2.4.2', version='2.5.1',
author = 'Chris Lane', author='Chris Lane',
author_email = 'chris@chris-allen-lane.com', author_email='chris@chris-allen-lane.com',
license = 'GPL3', license='GPL3',
description = 'cheat allows you to create and view interactive cheatsheets ' description='cheat allows you to create and view interactive cheatsheets '
'on the command-line. It was designed to help remind *nix system ' 'on the command-line. It was designed to help remind *nix system '
'administrators of options for commands that they use frequently, but not ' 'administrators of options for commands that they use frequently, but not '
'frequently enough to remember.', 'frequently enough to remember.',
url = 'https://github.com/chrisallenlane/cheat', url='https://github.com/chrisallenlane/cheat',
packages = [ packages=[
'cheat', 'cheat',
'cheat.test', 'cheat.test',
], ],
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',
'termcolor >= 1.1.0', 'termcolor >= 1.1.0',
], ],
data_files = [ data_files=[
(cheat_path, cheat_files), (cheat_path, cheat_files),
('/etc', ['config/cheat']), ('/etc', ['config/cheat']),
], ],