Overview¶
linecook
is a command-line tool that transforms lines of text into a form
that’s pleasant to consume.
The core goal of linecook
is to make it easy to create your own transforms to
parse whatever text you have. For example, take an app.log
file that looks
like:
$ tail app.log
2018-06-09 13:55:26 INFO Dependencies loaded successfully
2018-06-09 13:55:26 WARN Could not find version number for app
2018-06-09 13:55:27 INFO Starting app...
2018-06-09 13:55:27 ERROR SyntaxError: invalid syntax
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^
SyntaxError: invalid syntax
If you want to highlight the log type and mute the dates/times, then you can create a custom recipe in one of your Configuration files like the following:
from linecook import patterns as rx
from linecook.transforms import color_text
LINECOOK_CONFIG = {
'recipes': {
'my-logs': [
color_text(rx.any_of(rx.date, rx.time), color='blue'),
color_text('INFO', color='cyan'),
color_text('WARN', color='grey', on_color='on_yellow'),
color_text('ERROR', on_color='on_red'),
],
},
}
To use this recipe, you can just pipe the log output to linecook
with your
new recipe as an argument:
$ tail app.log | linecook my-logs 2018-06-09 13:55:26 INFO Dependencies loaded successfully 2018-06-09 13:55:26 WARN Could not find version number for app 2018-06-09 13:55:27 INFO Starting app... 2018-06-09 13:55:27 ERROR SyntaxError: invalid syntax >>> while True print('Hello world') File "<stdin>", line 1 while True print('Hello world') ^ SyntaxError: invalid syntax
That’s all there is to it!
Configuration¶
Configuration files¶
The following configuration files are loaded, in order:
linecook.config.core
: Package configuration~/.linecook/config.py
: User configuration./.linecook/config.py
: Local configuration
Each file should define a dictionary named LINECOOK_CONFIG
containing keys
such as transforms
and recipes
.
Files loaded later (lower on the list) override values loaded earlier. Note
that the overriding happens at the second level of dictionaries. For example,
if ~/.linecook/config.py
is defined as:
from linecook.transforms.core import color_text
LINECOOK_CONFIG = {
'transforms': {
'warn_color': color_text(' WARN ', color='yellow'),
'error_color': color_text(' ERROR ', on_color='on_red'),
},
'recipes': {
'logs': ['warn_color', 'error_color'],
'default': ['warn_color', 'error_color'],
},
}
And then, ./.linecook/config.py
is defined as:
from linecook.transforms.core import filter_line
LINECOOK_CONFIG = {
'recipes': {
'default': [filter_line(' DEBUG '), 'error_color']
},
}
The loaded result would roughly translate to:
from linecook.transforms.core import color_text, filter_line
LINECOOK_CONFIG = {
'transforms': {
'warn_color': color_text(' WARN ', color='yellow'),
'error_color': color_text(' ERROR ', on_color='on_red'),
},
'recipes': {
'logs': ['warn_color', 'error_color'],
'default': [filter_line(' DEBUG '), 'error_color']
},
}
You’ll notice that recipes
doesn’t match the recipes
in the second config
file: Instead, the second file only overrode the 'default'
value in the first
config file, but preseved the 'logs'
value.
Developer’s Guide¶
Prerequisites¶
The linecook
package uses poetry for
dependency management and distribution. You call install poetry
using:
curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python
Setup¶
Clone from github:
git clone https://github.com/tonysyu/linecook.git
Install development requirements:
cd linecook
poetry install
For building the documentation locally, you’ll also need to run:
poetry install --extras "docs"
Development¶
For local development, you’ll also want to install pre-commit hooks using:
poetry run pre-commit install
By default, this will run the black code formatter on changed files on every commit. To run black on all files:
poetry run pre-commit run --all-files
Running tests¶
The test suite can be run without installing dev requirements using:
$ tox
To run tests with a specific Python version, run:
$ tox --env py36
You can isolate specific test files/functions/methods with:
tox PATH/TO/TEST.py
tox PATH/TO/TEST.py::TEST_FUNCTION
tox PATH/TO/TEST.py::TEST_CLASS::TEST_METHOD
Documentation¶
Documentation is built from within the docs directory:
cd docs
make html
After building, you can view the docs at docs/_build/html/index.html
.
Debugging¶
It turns out that breakpoints are a bit tricky when processing streamed input.
A simple pdb.set_trace()
will fail, so you’ll need to try one of the
solutions described on StackOverflow [1], [2] (answer that worked for me).
Better yet, if you can use a single line of text can be passed in to test
an issue, you can use the --text
(-t
) flag instead of piping text:
linecook <RECIPE> --text 'Line of text to test'
[1] | https://stackoverflow.com/questions/17074177/how-to-debug-python-cli-that-takes-stdin |
[2] | https://stackoverflow.com/questions/9178751/use-pdb-set-trace-in-a-script-that-reads-stdin-via-a-pipe |
Release¶
A reminder for the maintainers on how to deploy.
Update the version and push:
$ bumpversion patch # possible: major / minor / patch $ git push $ git push --tags
Build release, deploy to PyPI, and clean
$ make release $ make clean
linecook¶
linecook package¶
Subpackages¶
linecook.config package¶
Submodules¶
linecook.config.core module¶
-
class
linecook.config.core.
LineCookConfig
(config_dicts)¶ Bases:
object
Configuration for
linecook
parsed from known configuration files.-
transforms
¶ Named transforms available for recipes.
Type: dict
-
recipes
¶ Named recipes, which are simply text transforms, or sequences of transforms, that are applied to each line of text.
Type: dict
See
load_config
for a description of known configuration files, and hierarchical configuration works.-
-
linecook.config.core.
load_config
()¶ Return
LineCookConfig
reduced from all known configuration files.The following configuration files are loaded, in order:
linecook.config.core
~/.linecook/config.py
./.linecook/config.py
Each file should define a dictionary named
LINECOOK_CONFIG
containing keys such astransforms
andrecipes
.Files loaded later (lower on the list) override values loaded earlier. Note that the overriding happens at the second level of dictionaries. For example, if
~/.linecook/config.py
is defined as:from linecook.transforms.core import color_text LINECOOK_CONFIG = { 'transforms': { 'warn_color': color_text(' WARN ', color='yellow'), 'error_color': color_text(' ERROR ', on_color='on_red'), }, 'recipes': { 'logs': ['warn_color', 'error_color'], 'default': ['warn_color', 'error_color'], }, }
And then,
./.linecook/config.py
is defined as:from linecook.transforms.core import filter_line LINECOOK_CONFIG = { 'recipes': { 'default': [filter_line(' DEBUG '), 'error_color'] }, }
The loaded result would roughly translate to:
LINECOOK_CONFIG = { 'transforms': { 'warn_color': color_text(' WARN ', color='yellow'), 'error_color': color_text(' ERROR ', on_color='on_red'), }, 'recipes': { 'logs': ['warn_color', 'error_color'], 'default': [filter_line(' DEBUG '), 'error_color'] }, }
You’ll notice that
recipes
doesn’t match therecipes
in the second config file: Instead, the second file only overrode the'default'
value in the first config file, but preseved the'logs'
value.
linecook.config.parsers module¶
-
linecook.config.parsers.
collect_recipes
(config_dicts, transforms_registry)¶ Return recipe dictionary from a list of configuration dictionaries.
Parameters: - config_dicts (list(dict)) – Unparsed configuration dictionaries. For each dictionary, only use the ‘recipes’ value, which itself is a dictionary, where the keys are recipe names and values are lists of transform functions or transform names.
- transforms_registry (dict) – Dictionary containing named transform
functions. See also
collect_tranforms
, which build this registry.
-
linecook.config.parsers.
collect_tranforms
(config_dicts)¶ Return transform dictionary from a list of configuration dictionaries.
Parameters: config_dicts (list(dict)) – Unparsed configuration dictionaries. For each dictionary, this applies parsers registered with register_transform_parser
that convert configuration data into named transform functions.
-
linecook.config.parsers.
get_value_from_each
(key, dict_list)¶ Return list of values for
key
in a list of dictionaries.
-
linecook.config.parsers.
parse_colorizers
(colorizers_dict_seq)¶ Return dictionary of transforms based on
colorizers
in config_dict.This converts
colorizers
field in a configuration dict into color transforms. For example, take the following configuration:'colorizers': { 'warn_color': { 'match_pattern': ' WARN ', 'on_color': 'on_yellow', }, },
That configuration is parsed to return the transform:
from linecook.transforms.core import color_text color_text(' WARN ', on_color='on_yellow')
-
linecook.config.parsers.
parse_transforms
(transforms_dict_seq)¶ Return dictionary of transforms based on
transforms
in config_dict.All this really does is merge the transforms defined in multiple configuration dictionaries.
-
linecook.config.parsers.
register_transform_parser
(config_type)¶ Add parser to registry of known linecook config parsers.
Parameters: config_type (str) – The config type that is parsed by this decorated function The resulting output will be stored in the parsed config under this name. A config parser takes a sequence representing updated the an object containing the parsed data.
You can register the a parser with the same name multiple times, which will simply override older instances.
-
linecook.config.parsers.
resolve_recipe
(recipe, transforms_registry)¶
Module contents¶
linecook.recipes package¶
Submodules¶
linecook.recipes.dpkg_log module¶
linecook recipe for dpkg.log.
-
linecook.recipes.dpkg_log.
emphasize_dpkg_actions
()¶ Return transform that emphasizes packaging actions
linecook.recipes.python module¶
Example of linecook recipe for python code.
This is a toy example: Actual syntax highlighting isn’t possible since linecook doesn’t (easily) store state between different lines, which prevents proper highlighting of things like multi-line strings.
Module contents¶
linecook.transforms package¶
Submodules¶
linecook.transforms.core module¶
Text formatters match the signature of basic regex functions, taking a text/regex match pattern and an input string.
-
class
linecook.transforms.core.
CountLines
(line_template='{count_label} {line}', count_template='{count:>3}:', color_kwargs={'attrs': ['bold'], 'color': 'grey'})¶ Bases:
object
Tranformation returning line of text with line count added.
-
reset
()¶
-
-
linecook.transforms.core.
color_text
(match_pattern='.*', color=None, on_color=None, attrs=None)¶ Return color transform that returns colorized version of input string.
Parameters: - color (str) – Text color. Any of the following values grey, red, green, yellow, blue, magenta, cyan, white.
- on_color (str) – Background color. Any of the following values on_grey, on_red, on_green, on_yellow, on_blue, on_magenta, on_cyan, on_white
- attrs (list(str)) – Text attributes. Any of the following values: bold, dark, underline, blink, reverse, concealed.
-
linecook.transforms.core.
delete_text
(match_pattern, *, replacement='')¶
-
linecook.transforms.core.
filter_line
(match_pattern, on_match=None, on_mismatch=None)¶ Return transform that filters lines by match pattern.
If neither
on_match
oron_mismatch
are given, return input string if it matches the given pattern. Otherwise, the input string is passed to those callback functions and the output is returned.Parameters: - on_match (callable) – A text transform that is called with the input
string
if the string matches thematch_pattern
. - on_mismatch (callable) – A text transform that is called with the input
string
if the string does not match thematch_pattern
.
- on_match (callable) – A text transform that is called with the input
-
linecook.transforms.core.
partition
(match_pattern, on_match=None, on_mismatch=None)¶ Return line partitioned by pattern and re-joined after transformation.
Parameters: - on_match (callable) – A text transform that is called with each
substring that matches the
match_pattern
. - on_mismatch (callable) – A text transform that is called with each
substring that does not match the
match_pattern
.
- on_match (callable) – A text transform that is called with each
substring that matches the
-
linecook.transforms.core.
replace_text
(match_pattern, replacement)¶
-
linecook.transforms.core.
split_on
(match_pattern, *, replacement='\n')¶
linecook.transforms.logging module¶
Submodules¶
linecook.cli module¶
linecook
cli to prepare lines of text for easy consumption.
-
linecook.cli.
build_parser
()¶
-
linecook.cli.
main
()¶
-
linecook.cli.
print_available_recipes
(linecook_config)¶
-
linecook.cli.
recipe_not_found_msg
(recipe_name)¶
-
linecook.cli.
run
(args)¶
linecook.parsers module¶
-
linecook.parsers.
create_regex_factory
(format_string=None, regex_type=None, ignore_case=False)¶ Return a
create_regex
function that compiles a pattern to a regex.
-
linecook.parsers.
resolve_match_pattern
(pattern)¶ Return a compiled regex, parsing known regex shorthands.
linecook.patterns module¶
-
linecook.patterns.
any_of
(*args)¶ Return regex that matches any of the input regex patterns.
The returned value is equivalent to writing:
r'(<arg1>|<arg2>|...)'
-
linecook.patterns.
anything
= '.*'¶ Pattern matching any text
-
linecook.patterns.
bounded_word
(string)¶ Return regex that matches the input string as a bounded word.
The returned value is equivalent to writing:
r'\b<string>\b'
-
linecook.patterns.
date
= '\\b\\d{4}-\\d{2}-\\d{2}\\b'¶ Pattern matching calendar dates in ISO 8601 format (
YYYY-MM-DD
)
-
linecook.patterns.
day
= '\\d{2}'¶ Pattern matching a numeric day
-
linecook.patterns.
double_quoted_strings
= '(?<!["\\w])"[^"]*"(?!["\\w])'¶ Pattern matching strings surrounded by double-quotes
-
linecook.patterns.
exact_match
(string)¶ Return regex that matches the input string exactly.
The returned value is equivalent to writing:
r'^<string>$'
-
linecook.patterns.
exact_template
= '^{}$'¶ Template string to match text exactly (start-to-end)
-
linecook.patterns.
first_word
= '^\\s*\\w+'¶ Pattern matching first word
-
linecook.patterns.
indent
= '^\\s*'¶ Pattern matching indent at start of string
-
linecook.patterns.
month
= '\\d{2}'¶ Pattern matching a numeric month
-
linecook.patterns.
num_float
= '\\b[+-]?(\\d*[.])?\\d+\\b'¶ Pattern matching floating point number
-
linecook.patterns.
num_int
= '\\b[+-]?\\d\\b'¶ Pattern matching integers
-
linecook.patterns.
number
= '(\\b[+-]?\\d\\b|\\b[+-]?(\\d*[.])?\\d+\\b)'¶ Pattern matching integers or floats
-
linecook.patterns.
single_quoted_strings
= "(?<!['\\w])'[^']*'(?!['\\w])"¶ Pattern matching strings surrounded by single-quotes
-
linecook.patterns.
start
= '^'¶ Pattern matching start of string
-
linecook.patterns.
strings
= '((?<![\'\\w])\'[^\']*\'(?![\'\\w])|(?<!["\\w])"[^"]*"(?!["\\w]))'¶ Pattern matching strings surrounded by single- or double-quotes
-
linecook.patterns.
time
= '\\b(\\d{2}:\\d{2}(:\\d{2})?)\\b'¶ Pattern matching numeric time
-
linecook.patterns.
time_ms
= '\\b\\d{2}:\\d{2}:\\d{2}(,|.)\\d{3}\\b'¶ Pattern matching numeric time with milliseconds
-
linecook.patterns.
whitespace
= '\\s*'¶ Pattern matching any whitespace
-
linecook.patterns.
word_template
= '\\b{}\\b'¶ Template string to match text surrounded by word boundaries
-
linecook.patterns.
year
= '\\d{4}'¶ Pattern matching a numeric year