diff --git a/.gitignore b/.gitignore
index 9839280..c2a9c3f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,162 @@
-*.pyc
+## Python gitignore
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
build/
+develop-eggs/
dist/
-colout.egg-info/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+
+## Jetbrains
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# SonarLint
+.idea/sonarlint
+
+# CMake
+cmake-build-*/
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..b7997b5
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,12 @@
+language: python
+python:
+ - "3.5"
+ - "3.6"
+ - "3.7"
+ - "3.8"
+ - "pypy3"
+install:
+ - pip install .
+script:
+ - colout --help
+ - echo heyoo | colout hey yellow
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..1aba38f
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include LICENSE
diff --git a/README.md b/README.md
index 4b8f741..10bde37 100644
--- a/README.md
+++ b/README.md
@@ -1,61 +1,117 @@
-colout(1) -- Color Up Arbitrary Command Ouput
-=============================================
+colout — Color Up Arbitrary Command Output
+==========================================
-## SYNOPSIS
+
+
+
-`colout` [-h] [-e] [-g] [-t] [-s] [-l] PATTERN [COLOR(S)] [STYLE(S)]
+## Synopsis
+`colout [-h] [-r [RESOURCE]]`
-## DESCRIPTION
+`colout [-g] [-c] [-l min,max] [-a] [-t] [-T DIR] [-P DIR] [-d COLORMAP] [-s] [-e CHAR] [-E CHAR] [--debug] PATTERN [COLOR(S) [STYLE(S)]]`
-`colout` read lines of text stream on the standard input and output characters
-matching a given regular expression *PATTERN* in given and *STYLE*.
+## Description
+
+`colout` read lines of text stream on the standard input and output characters
+matching a given regular expression *PATTERN* in given *COLOR* and *STYLE*.
If groups are specified in the regular expression pattern, only them are taken
into account, else the whole matching pattern is colored.
-You can specify severall colors or styles when using groups by separating them
+You can specify several colors or styles when using groups by separating them
with commas. If you indicate more colors than groups, the last ones will be ignored.
-If you ask for less colors, the last one will be duplicated across remaining
+If you ask for fewer colors, the last one will be duplicated across remaining
groups.
Available colors are: blue, black, yellow, cyan, green, magenta, white, red,
-rainbow, random, Random, scale, none or any number between 0 and 255.
+rainbow, random, Random, Spectrum, spectrum, scale, Scale, hash, Hash, none, an
+RGB hexadecimal triplet (`#11aaff`, for example) or any number between 0 and 255.
Available styles are: normal, bold, faint, italic, underline, blink,
-rapid_blink, reverse, conceal or random.
+rapid_blink, reverse, conceal or random (some styles may have no effect, depending
+on your terminal).
-`Random` will color each matching pattern with a random color among the 255
-available in the ANSI table. `random` will do the same in 8 colors mode.
+In some case, you can indicate a foreground and a background color, by indicating both colors
+separated by a period (for example: `red.blue`). You can also use this system to combine two styles
+(for example, for a bold style that also blinks: `bold.blink`).
-`rainbow` will cycle over a 8 colors rainbow at each matching pattern.
+`rainbow` will cycle over a the default colormap at each matching pattern.
+`Rainbow` will do the same over the default colormap for the 256-colors mode
+(this requires a terminal that supports the 256 color escape sequences).
-`scale` will parse the matching text as a decimal number and apply the rainbow
-colormap according to its position on a scale defined by the `-l` option (see
-below, [0-100] by default).
+`Random` will color each matching pattern with a random color among the default colormap
+(the 255 available in the ANSI table, by default).
+`random` will do the same in 8 colors mode.
+
+`spectrum` and `Spectrum` are like rainbows, but with more colors (8 and 36
+colors).
+
+`scale` (8 colors) and `Scale` (256 colors) will parse the numbers characters in
+the matching text as a decimal number and apply the default colormap according
+to its position on the scale defined by the `-l` option (see below, "0,100" by
+default).
+
+`hash` (8 colors) and `Hash` (256 colors) will take a fingerprint of the matching
+text and apply the default colormap according to it. This ensure that matching
+texts appearing several times will always get the same color.
+
+Before interpreting the matched string as a number, colout will remove any
+character not supposed to be used to write down numbers. This permits to apply
+this special color on a large group, while interpreting only its numerical part.
+
+You can use the name of a syntax-coloring ["lexer"](http://pygments.org/docs/lexers/)
+as a color (for example: "Cpp", "ruby", "xml+django", etc.).
+
+If GIMP palettes files (*.gpl) are available, you can also use their names as a
+colormap (see the `-P` switch below).
+
+Note that the RGB colors (either the hex triplets or the palettes's colors) will
+be converted to their nearest ANSI 256 color mode equivalents.
When not specified, a *COLOR* defaults to _red_ and a *STYLE* defaults to _bold_.
-`colout` comes with some predefined themes to rapidely color well-known outputs
+`colout` comes with some predefined themes to rapidly color well-known outputs
(see the `-t` switch below).
-If the python-pygments library is available, `colout` can be used as an interface
-to it (see also the `-s` switch below).
+`colout` can be used as an interface to pygments (see also the `--source` switch below).
+
+To have a list of all colors, styles, special colormaps, themes, palettes and lexers,
+use the `-r` switch (see below).
`colout` is released under the GNU Public License v3.
-## INSTALLATION
- sudo python setup.py install
+## Installation
-and then soft link `/usr/local/bin/colout` to your colout.py under your installaion directory, which usually like
+The recomended method is using pip to install the package for the local user:
- /usr/local/lib/python2.7/dist-packages/colout-0.1-py2.7.egg/colout/colout.py
+```console
+$ pip install --user colout
+```
-## OPTIONS
+Another method is using [pipsi](https://github.com/mitsuhiko/pipsi)
+(_pipsi is no longer maintained, _)
+```console
+$ pipsi install colout
+```
+
+There is also a PPA for Ubuntu 16.04 (Xenial)/18.04 (Bionic) (@`0.6.1-3~dist7`, not actively maintained)
+
+```console
+$ sudo add-apt-repository ppa:csaba-kertesz/random
+$ sudo apt-get update
+$ sudo apt-get/aptitude install colout
+```
+
+## Options
* `-h`, `--help`:
- Show an help message and exit
+ Show a help message and exit
* `-g`, `--groups`:
For color maps (like "rainbow"), iterate over matching groups in the pattern instead of over patterns.
@@ -63,54 +119,91 @@ and then soft link `/usr/local/bin/colout` to your colout.py under your installa
* `-c`, `--colormap`:
Use the given list of comma-separated colors as a colormap (cycle the colors at each match).
-* `-l`, `--scale`:
- When using the 'scale' colormap, parse matches as decimal numbers (taking your locale into account)
- and apply the rainbow colormap linearly between the given SCALE=min,max (SCALE=0,100, by default).
+* `-l min,max`, `--scale min,max`:
+ When using the 'scale' colormap, parse matches as decimal numbers (taking your locale into
+ account) or as arithmetic expression (like "1+2/0.9*3") and apply the rainbow colormap linearly
+ between the given min,max (0,100, by default).
* `-a`, `--all`:
Color the whole input at once instead of line per line
(really useful for coloring a source code file with strings on multiple lines).
* `-t`, `--theme`:
- Interpret PATTERN as a predefined theme (perm, cmake, g++, etc.)
+ Interpret PATTERN as a predefined theme (perm, cmake, g++, etc.).
+
+* `-T DIR`, `--themes-dir DIR`:
+ Search for additional themes (colout_*.py files) in this directory.
+
+* `-P DIR`, `--palettes-dir DIR`:
+ Search for additional palettes (*.gpl files) in this directory.
+
+* `-d COLORMAP`, `--default COLORMAP`:
+ When using special colormaps (`random`, `scale` or `hash`), use this COLORMAP instead of the default one.
+ This can be either one of the available colormaps or a comma-separated list of colors.
+ WARNING: be sure to specify a default colormap that is compatible with the special colormap's mode,
+ or else the colors may not appear the same.
+ Also, external palettes are converted from RGB to 256-ANSI and will thus not work if you use
+ them as default colormaps for a 8-colors mode special color.
+
+* `-r [TYPE(S)]`, `--resources [TYPE(S)]`:
+ Print the names of available resources. Use a comma-separated list of resources names
+ (styles, colors, special, themes, palettes, colormaps or lexers),
+ use 'all' (or no argument) to print all resources.
* `-s`, `--source`:
- Interpret PATTERN as a source code readable by the Pygments library. If the first letter of PATTERN
- is upper case, use the 256 colors mode, if it is lower case, use the 8 colors mode.
- In 256 colors, interpret COLOR as a Pygments style (e.g. "default").
+ Interpret PATTERN as source code readable by the Pygments library. If the first letter of PATTERN
+ is upper case, use the 256 color mode, if it is lower case, use the 8 colors mode.
+ In 256 color mode, interpret COLOR as a Pygments style (e.g. "default").
+
+* `-e CHAR`, `--sep-list CHAR`:
+ Use this character as a separator for list of colors/resources/numbers (instead of comma).
+
+* `-E CHAR`, `--sep-pair CHAR`:
+ Use this character as a separator for foreground/background pairs (instead of period).
+
+* `--debug`:
+ Debug mode: print what's going on internally, if you want to check what features are available.
-## REGULAR EXPRESSIONS
+## Regular expressions
A regular expression (or _regex_) is a pattern that describes a set of strings
that matches it.
-`colout` understands regex as specifed in the _re_ python module. Given that
+`colout` understands regex as specified in the _re_ python module. Given that
`colout` is generally called by the command line, you may have to escape
special characters that would be recognize by your shell.
-## DEPENDENCIES
+## Dependencies
-Recommended packages :
+Necessary Python modules:
-* `argparse` for a usable arguments parsing
* `pygments` for the source code syntax coloring
* `babel` for a locale-aware number parsing
-## EXAMPLES
+## Limitations
-* Color in bold red every occurence of the word _color_ in colout sources:
+Don't use nested groups or colout will duplicate the corresponding input text
+with each matching colors.
+
+Using a default colormap that is incompatible with the special colormap's mode
+(i.e. number of colors) will end badly.
+
+Color pairs (`foreground.background`) work in 8-colors mode for simple coloring, but may fail with `--colormap`.
+
+## Examples
+
+### Simple
+
+* Color in bold red every occurrence of the word _color_ in colout sources:
`cat colout.py | colout color red bold`
* Color in bold violet home directories in _/etc/passwd_:
`colout '/home/[a-z]+' 135 < /etc/passwd`
-* Use a different color for each line of the auth log
- `grep user /var/log/auth.log | colout "^.*$" rainbow`
-
-* Color in yellow user/groups id, in bold green name and in bold red home directories in _/etc/passwd_:
+* Color in yellow user/groups id, in bold green name and in bold red home directories in `/etc/passwd`:
`colout ':x:([0-9]+:[0-9]+):([^:]+).*(/home/[a-z]+)' yellow,green,red normal,bold < /etc/passwd`
* Color in yellow file permissions with read rights for everyone:
@@ -119,34 +212,40 @@ Recommended packages :
* Color in green read permission, in bold red write and execution ones:
`ls -l | colout '(r)(w*)(x*)' green,red normal,bold`
-* Color permissions with a predefined template:
- `ls -l | colout -t perm`
-
* Color in green comments in colout sources:
`colout '.*(#.*)$' green normal < colout.py`
-* Color in light green comments in non-empty colout sources, with the sharp in bold green:
- `grep -v '^\s*$' colout.py | colout '.*(#)(.*)$' green,119 bold,normal`
-
* Color in bold green every numbers and in bold red the words _error_ in make output:
`make 2>&1 | colout '[0-9]+' green normal | colout error`
-* Color a make output, line numbers in yellow, errors in bold red, warning in magenta, pragma in green and C++ file base names in cyan:
- `make 2>&1 | colout ':([0-9]+):[0-9]*' yellow normal | colout error | colout warning magenta | colout pragma green normal | colout '/(\w+)*\.(h|cpp)' cyan normal`
- Or using themes:
- `make 2>&³ | colout -t cmake | colout -t g++`
-* Color each word in the head of auth.log with a rainbow color map, starting a new colormap at each new line (the
- begining of the command is just bash magic to repeat the string "(\\w+)\\W+":
- `L=$(seq 10) ; P=${L//??/(\\w+)\\W+} ; head /var/log/auth.log | colout -g "^${P}(.*)$" rainbow`
+### Somewhat useful
+
+* Use a different color for each line of the auth log
+ `grep user /var/log/auth.log | colout "^.*$" rainbow`
* Color each line of a file with a different color among a 256 color gradient from cyan to green:
`head /var/log/auth.log | colout -c "^.*$" 39,38,37,36,35,34`
-* Color a source code in 8 colors mode, without seeing comments:
+* Color permissions with a predefined template:
+ `ls -l | colout -t perm`
+
+* Color in light green comments in non-empty colout sources, with the sharp in bold green:
+ `grep -v '^\s*$' colout.py | colout '.*(#)(.*)$' green,119 bold,normal`
+
+* Color a make output, line numbers in yellow, errors in bold red, warning in magenta, pragma in green and C++ file base names in cyan:
+ `make 2>&1 | colout ':([0-9]+):[0-9]*' yellow normal | colout error | colout warning magenta | colout pragma green normal | colout '/(\w+)*\.(h|cpp)' cyan normal`
+ Or using themes:
+ `make 2>&1 | colout -t cmake | colout -t g++`
+
+* Color each word in the head of auth.log with a rainbow color map, starting a new colormap at each new line (the
+ beginning of the command is just bash magic to repeat the string "(\\w+)\\W+":
+ `L=$(seq 10) ; P=${L//??/(\\w+)\\W+} ; head /var/log/auth.log | colout -g "^${P}(.*)$" rainbow`
+
+* Color source code in 8 colors mode, without seeing comments:
`cat colout.py | grep -v "#" | colout -s python`
-* Color a source code in 256 colors mode:
+* Color source code in 256 color mode:
`cat colout.py | colout -s Python monokai`
* Color a JSON stream:
@@ -155,3 +254,144 @@ Recommended packages :
* Color a source code substring:
`echo "There is an error in 'static void Functor::operator()( EOT& indiv ) { return indiv; }' you should fix it" | colout "'(.*)'" Cpp monokai`
+* Color the percent of progress part of a CMake's makefile output, with a color
+ related to the value of the progress (from 0%=blue to 100%=red):
+ `cmake .. && make | colout "^(\[\s*[0-9]+%\])" Scale`
+
+* Color hosts and users in `auth.log`, with consistent colors:
+ `cat /var/log/auth.log | colout "^(\S+\s+){3}(\S+)\s(\S+\s+){3}(\S+)\s+(\S+\s+){2}(\S+)\s*" none,hash,none,hash,none,hash`
+
+
+### Bash alias
+
+The following bash function color the output of any command with the
+cmake and g++ themes:
+
+```bash
+function cm()
+{
+ set -o pipefail
+ $@ 2>&1 | colout -t cmake | colout -t g++
+}
+```
+
+You then can use the `cm` alias as a prefix to your build command,
+for example: `cm make test`
+
+
+### GDB integration
+
+You can use `colout` within the GNU debuger (`gbd`) to color its output.
+For example, the following script `.gdbinit` configuration will color
+the output of the backtrace command:
+
+```gdb
+set confirm off
+
+# Don't wrap line or the coloring regexp won't work.
+set width 0
+
+# Create a named pipe to get outputs from gdb
+shell test -e /tmp/coloutPipe && rm /tmp/coloutPipe
+shell mkfifo /tmp/coloutPipe
+
+define logging_on
+ # Instead of printing on stdout only, log everything...
+ set logging redirect on
+ # ... in our named pipe.
+ set logging on /tmp/coloutPipe
+end
+
+define logging_off
+ set logging off
+ set logging redirect off
+ # Because both gdb and our commands are writing on the same pipe at the same
+ # time, it is more than probable that gdb will end before our (higher level)
+ # commands. The gdb prompt will thus render before the result of the command,
+ # which is highly akward. To prevent this, we need to wait before displaying
+ # the prompt again. The more your commands are complex, the higher you will
+ # need to set this.
+ shell sleep 0.4s
+end
+
+define hook-backtrace
+ # Note: match path = [path]file[.ext] = (.*/)?(?:$|(.+?)(?:(\.[^.]*)|))
+ # This line color highlights:
+ # – lines that link to source code,
+ # – function call in green,
+ # – arguments names in yellow, values in magenta,
+ # — the parent directory in bold red (assuming that the debug session would be in a "project/build/" directory).
+ shell cat /tmp/coloutPipe | colout "^(#)([0-9]+)\s+(0x\S+ )*(in )*(.*) (\(.*\)) (at) (.*/)?(?:$|(.+?)(?:(\.[^.]*)|)):([0-9]+)" red,red,blue,red,green,magenta,red,none,white,white,yellow normal,bold,normal,normal,normal,normal,normal,bold,bold,bold | colout "([\w\s]*?)(=)([^,]*?)([,\)])" yellow,blue,magenta,blue normal | colout "/($(basename $(dirname $(pwd))))/" red bold &
+ logging_on
+end
+define hookpost-backtrace
+ logging_off
+end
+
+# Don't forget to clean the adhoc pipe.
+define hook-quit
+ set confirm off
+ shell rm -f /tmp/coloutPipe
+end
+```
+
+Take a look at the `example.gdbinit` file distributed with colout for more gdb commands.
+
+
+
+### Themes
+
+You can easily add your own theme to colout.
+A theme is basically a module with a function named `theme` that take the configuration context as
+an argument and return back the (modified) context and a list of triplets.
+Each triplet figures the same arguments than those of the command line interface.
+
+```python
+def theme(context):
+ return context,[ [regexp, colors, styles] ]
+```
+
+With the context dictionary at hand, you have access to the internal configuration of colout, you
+can thus change colormaps for special keywords, the scale, even the available colors, styles or
+themes.
+
+See the cmake them for how to modify an existing colormap if (and only if) the user didn't ask for an alternative one.
+See the ninja theme for how to extend an existing theme with more regexps and a different configuration.
+See the gcc theme for an example of how to use the localization of existing softwares to build translated regexp.
+
+
+### Buffering
+
+Note that when you use colout within real time streams (like `tail -f X | grep Y | colout Z`) of
+commands, you may observe that the lines are printed by large chunks and not one by one, in real
+time.
+This is not due to colout but to the buffering behavior of your shell.
+
+To fix that, use `stdbuf`, for example: `tail -f X | stdbuf -o0 grep Y | colout Z`.
+
+## Authors
+
+* nojhan : original idea, main developer, maintainer.
+* Adrian Sadłocha
+* Alex Burka
+* Brian Foley
+* Charles Lewis
+* DainDwarf
+* Dimitri Merejkowsky
+* Dong Wei Ming
+* Fabien MARTY
+* Jason Green
+* John Anderson
+* Jonathan Poelen
+* Louis-Kenzo Furuya Cahier
+* Mantas
+* Martin Ueding
+* Nicolas Pouillard
+* Nurono
+* Oliver Bristow
+* orzrd <61966225@qq.com>
+* Philippe Daouadi
+* Piotr Staroszczyk
+* Scott Lawrence
+* Xu Di
+* https://github.com/stdedos: maintainer.
diff --git a/bin/colout b/bin/colout
deleted file mode 100644
index 9e09ae9..0000000
--- a/bin/colout
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/bash
-# Copyright © 2013 Martin Ueding
-
-# Small launcher script for the main module.
-
-# Licence: GPL 3
-
-set -e
-set -u
-
-python3 -m colout.colout "$@"
diff --git a/colout/colout.py b/colout/colout.py
index 0f7cfc3..2d51fac 100755
--- a/colout/colout.py
+++ b/colout/colout.py
@@ -1,45 +1,219 @@
#!/usr/bin/env python3
#encoding: utf-8
-# Color Up Arbitrary Command Ouput
+# Color Up Arbitrary Command Output
# Licensed under the GPL version 3
# 2012 (c) nojhan
-import re
-import random
import os
+import re
+import sys
+import copy
import glob
import math
+import pprint
+import random
+import signal
+import string
+import hashlib
+import logging
+import argparse
+import importlib
+import functools
+import babel.numbers as bn
-###########
-# Library #
-###########
+# set the SIGPIPE handler to kill the program instead of
+# ending in a write error when a broken pipe occurs
+signal.signal( signal.SIGPIPE, signal.SIG_DFL )
+
+
+###############################################################################
+# Global variable(s)
+###############################################################################
+
+context = {}
+debug = False
# Available styles
-styles = {
+context["styles"] = {
"normal": 0, "bold": 1, "faint": 2, "italic": 3, "underline": 4,
"blink": 5, "rapid_blink": 6,
"reverse": 7, "conceal": 8
}
-# Available color names in 8-colors mode
-colors = {
- "black": 0, "red": 1, "green": 2, "yellow": 3, "blue": 4,
- "magenta": 5, "cyan": 6, "white": 7, "none": -1
-}
+error_codes = {"UnknownColor": 1, "DuplicatedPalette": 2, "MixedModes": 3, "UnknownLexer": 4, "UnknownResource": 5}
-ansi_min = 16
-ansi_max = 232
+# Available color names in 8-colors mode.
+eight_colors = ["black","red","green","yellow","blue","magenta","cyan","white"]
+# Given in that order, the ASCII code is the index.
+eight_color_codes = {n:i for i,n in enumerate(eight_colors)}
+# One can add synonyms.
+eight_color_codes["orange"] = eight_color_codes["yellow"]
+eight_color_codes["purple"] = eight_color_codes["magenta"]
-def rgb_rainbow( x, freq = 1.0/(256.0/math.pi) ):
- scope = (ansi_max - ansi_min)/2.0
- red = ansi_min + scope * (1+math.sin( 2*freq*x + math.pi/2 ))
- green = ansi_min + scope * (1+math.sin( 2*freq*x - math.pi/2 ))
- blue = ansi_min + scope * (1+math.sin( freq*x - math.pi/2 ))
- return ( red, green, blue )
+# Foreground colors has a special "none" item.
+# Note: use copy to avoid having the same reference over fore/background.
+context["colors"] = copy.copy(eight_color_codes)
+context["colors"]["none"] = -1
+
+# Background has the same colors than foreground, but without the none code.
+context["backgrounds"] = copy.copy(eight_color_codes)
+
+context["themes"] = {}
+
+# pre-defined colormaps
+# 8-colors mode should start with a lower-case letter (and can contains either named or indexed colors)
+# 256-colors mode should start with an upper-case letter (and should contains indexed colors)
+context["colormaps"] = {
+ # Rainbows
+ "rainbow" : ["magenta", "blue", "cyan", "green", "yellow", "red"],
+ "Rainbow" : [92, 93, 57, 21, 27, 33, 39, 45, 51, 50, 49, 48, 47, 46, 82, 118, 154, 190, 226, 220, 214, 208, 202, 196],
+
+ # From magenta to red, with white in the middle
+ "spectrum" : ["magenta", "blue", "cyan", "white", "green", "yellow", "red"],
+ "Spectrum" : [91, 92, 56, 57, 21, 27, 26, 32, 31, 37, 36, 35, 41, 40, 41, 77, 83, 84, 120, 121, 157, 194, 231, 254, 255, 231, 230, 229, 228, 227, 226, 220, 214, 208, 202, 196],
+
+ # All the colors are available for the default `random` special
+ "random" : context["colors"],
+ "Random" : list(range(256))
+} # colormaps
+
+context["colormaps"]["scale"] = context["colormaps"]["spectrum"]
+context["colormaps"]["Scale"] = context["colormaps"]["Spectrum"]
+context["colormaps"]["hash"] = context["colormaps"]["rainbow"]
+context["colormaps"]["Hash"] = context["colormaps"]["Rainbow"]
+context["colormaps"]["default"] = context["colormaps"]["spectrum"]
+context["colormaps"]["Default"] = context["colormaps"]["Spectrum"]
+
+context["user_defined_colormaps"] = False
+
+context["colormap_idx"] = 0
+
+context["scale"] = (0,100)
+
+context["lexers"] = []
+
+# Character use as a delimiter
+# between foreground and background.
+context["sep_pair"]="."
+context["sep_list"]=","
+
+class UnknownColor(Exception):
+ pass
+
+class DuplicatedPalette(Exception):
+ pass
+
+class DuplicatedTheme(Exception):
+ pass
+
+class MixedModes(Exception):
+ pass
-def rgb_to_ansi( red, green, blue ):
+###############################################################################
+# Ressource parsing helpers
+###############################################################################
+
+def make_colormap( colors, sep_list = context["sep_list"] ):
+ cmap = colors.split(sep_list)
+
+ # Check unicity of mode.
+ modes = [mode(c) for c in cmap]
+ if len(uniq(modes)) > 1:
+ # Format a list of color:mode, for error display.
+ raise MixedModes(", ".join(["%s:%s" % cm for cm in zip(cmap,modes)]))
+
+ return cmap
+
+
+def set_special_colormaps( cmap, sep_list = context["sep_list"] ):
+ """Change all the special colors to a single colormap (which must be a list of colors)."""
+ global context
+ context["colormaps"]["scale"] = cmap
+ context["colormaps"]["Scale"] = cmap
+ context["colormaps"]["hash"] = cmap
+ context["colormaps"]["Hash"] = cmap
+ context["colormaps"]["default"] = cmap
+ context["colormaps"]["Default"] = cmap
+ context["colormaps"]["random"] = cmap
+ context["colormaps"]["Random"] = cmap
+ context["user_defined_colormaps"] = True
+ logging.debug("user-defined special colormap: %s" % sep_list.join([str(i) for i in cmap]) )
+
+
+def parse_gimp_palette( filename ):
+ """
+ Parse the given filename as a GIMP palette (.gpl)
+
+ Return the filename (without path and extension) and a list of ordered
+ colors.
+ Generally, the colors are RGB triplets, thus this function returns:
+ (name, [ [R0,G0,B0], [R1,G1,B1], ... , [RN,GN,BN] ])
+ """
+
+ logging.debug("parse GIMP palette file: %s" % filename)
+ fd = open(filename)
+ # remove path and extension, only keep the file name itself
+ name = os.path.splitext( os.path.basename(filename ))[0]
+
+ # The first .gpl line is a header
+ assert( fd.readline().strip() == "GIMP Palette" )
+
+ # Then the full name of the palette
+ long_name = fd.readline().strip()
+
+ # Then the columns number.
+ # split on colon, take the second argument as an int
+ line = fd.readline()
+ if "Columns:" in line:
+ columns = int( line.strip().split(":")[1].strip() )
+ lines = fd.readlines()
+ else:
+ columns=3
+ lines = [line] + fd.readlines()
+
+ # Then the colors themselves.
+ palette = []
+ for line in lines:
+ # skip lines with only a comment
+ if re.match(r"^\s*#.*$", line ):
+ continue
+ # decode the columns-ths codes. Generally [R G B] followed by a comment
+ colors = [ int(c) for c in line.split()[:columns] ]
+ palette.append( colors )
+
+ logging.debug("parsed %i RGB colors from palette %s" % (len(palette), name) )
+ return name,palette
+
+
+def uniq( lst ):
+ """Build a list with uniques consecutive elements in the argument.
+
+ >>> uniq([1,1,2,2,2,3])
+ [1,2,3]
+ >>> uniq([0,1,1,2,3,3,3])
+ [0,1,2,3]
+ """
+ assert( len(lst) > 0 )
+ uniq = [ lst[0] ]
+ for i in range(1,len(lst)):
+ if lst[i] != lst[i-1]:
+ uniq.append(lst[i])
+ return uniq
+
+
+def rgb_to_ansi( r, g, b ):
+ """Convert a RGB color to its closest 256-colors ANSI index"""
+
+ # Range limits for the *colored* section of ANSI,
+ # this does not include the *gray* section.
+ ansi_min = 16
+ ansi_max = 234
+
+ # ansi_max is the higher possible RGB value for ANSI *colors*
+ # limit RGB values to ansi_max
+ red,green,blue = tuple([ansi_max if c>ansi_max else c for c in (r,g,b)])
offset = 42.5
is_gray = True
@@ -51,55 +225,298 @@ def rgb_to_ansi( red, green, blue ):
if all_gray:
val = ansi_max + round( (red + green + blue)/33.0 )
- return int(val)
+ res = int(val)
else:
val = ansi_min
for color,modulo in zip( [red, green, blue], [6*6, 6, 1] ):
val += round(6.0 * (color / 256.0)) * modulo
- return int(val)
+ res = int(val)
+
+ return res
-rainbow = ["magenta", "blue", "cyan", "green", "yellow", "red"]
-colormap = rainbow # default colormap to rainbow
-colormap_idx = 0
+def hex_to_rgb(h):
+ assert( h[0] == "#" )
+ h = h.lstrip('#')
+ lh = len(h)
+ return tuple( int(h[i:i+lh//3], 16) for i in range(0, lh, lh//3) )
-scale = (0,100)
-# Escaped end markers for given color modes
-endmarks = {8: ";", 256: ";38;5;"}
+###############################################################################
+# Load available extern resources
+###############################################################################
-# load available themes
-themes = {}
-themes_dir=os.path.dirname(os.path.realpath(__file__))
-os.chdir( themes_dir )
-for f in glob.iglob("colout_*.py"):
- module = ".".join(f.split(".")[:-1])
- name = "_".join(module.split("_")[1:])
- themes[name] = __import__(module)
+def load_themes( themes_dir):
+ global context
+ logging.debug("search for themes in: %s" % themes_dir)
+ sys.path.append( themes_dir )
-# load available pygments lexers
-lexers = []
-try:
- from pygments.lexers import get_all_lexers
- from pygments.lexers import get_lexer_by_name
- from pygments import highlight
- from pygments.formatters import Terminal256Formatter
- from pygments.formatters import TerminalFormatter
-except ImportError:
- pass
-else:
- for lexer in get_all_lexers():
+ # load available themes
+ for f in glob.iglob(os.path.join(themes_dir, "colout_*.py")):
+ basename = os.path.basename(f) # Remove path.
+ module = os.path.splitext(basename)[0] # Remove extension.
+ name = "_".join(module.split("_")[1:]) # Remove the 'colout_' prefix.
+ if name in context["themes"]:
+ raise DuplicatedTheme(name)
+ logging.debug("load theme %s" % name)
+ context["themes"][name] = importlib.import_module(module)
+
+
+def load_palettes( palettes_dir, ignore_duplicates = True ):
+ global context
+ logging.debug("search for palettes in: %s" % palettes_dir)
+
+ # load available colormaps (GIMP palettes format)
+ for p in glob.iglob(os.path.join(palettes_dir, "*.gpl")):
try:
- lexers.append(lexer[1][0])
- except IndexError:
- pass
+ name,palette = parse_gimp_palette(p)
+ except Exception as e:
+ logging.warning("error while parsing palette %s: %s" % ( p,e ) )
+ continue
+ if name in context["colormaps"]:
+ if ignore_duplicates:
+ logging.warning("ignore this duplicated palette name: %s" % name)
+ else:
+ raise DuplicatedPalette(name)
+ # Convert the palette to ANSI
+ ansi_palette = [ rgb_to_ansi(r,g,b) for r,g,b in palette ]
+ # Compress it so that there isn't two consecutive identical colors
+ compressed = uniq(ansi_palette)
+ logging.debug("load %i ANSI colors in palette %s: %s" % (len(compressed), name, compressed))
+ context["colormaps"][name] = compressed
+
+
+def load_lexers():
+ global context
+ # load available pygments lexers
+ lexers = []
+ global get_lexer_by_name
+ from pygments.lexers import get_lexer_by_name
+
+ global highlight
+ from pygments import highlight
+
+ global Terminal256Formatter
+ from pygments.formatters import Terminal256Formatter
+
+ global TerminalFormatter
+ from pygments.formatters import TerminalFormatter
+
+ from pygments.lexers import get_all_lexers
+ try:
+ for lexer in get_all_lexers():
+ l = None
+ # If the tuple has one-word aliases
+ # (which are usually a better option than the long names
+ # for a command line argument).
+ if lexer[1]:
+ l = lexer[1][0] # Take the first one.
+ else:
+ assert(lexer[0])
+ l = lexer[0] # Take the long name, which should alway exists.
+ if not l:
+ logging.warning("cannot load lexer: %s" % lexer[1][0])
+ pass # Forget about this lexer.
+ else:
+ assert(" " not in l) # Should be very rare, but probably a source of bugs.
+ lexers.append(l)
+ except:
+ logging.warning("error while executing the pygment module, syntax coloring is not available")
+
lexers.sort()
+ logging.debug("loaded %i lexers: %s" % (len(lexers), ", ".join(lexers)))
+
+ context["lexers"] = lexers
-def colorin(text, color="red", style="normal"):
+def load_resources( themes_dir, palettes_dir ):
+ load_themes( themes_dir )
+ load_palettes( palettes_dir )
+ load_lexers()
+
+
+###############################################################################
+# Library
+###############################################################################
+
+def mode( color ):
+ global context
+ if type(color) is int:
+ if 0 <= color and color <= 255 :
+ return 256
+ else:
+ raise UnknownColor(color)
+ elif color in context["colors"]:
+ return 8
+ elif color in context["colormaps"].keys():
+ if color[0].islower():
+ return 8
+ elif color[0].isupper():
+ return 256
+ elif color.lower() in ("scale","hash","random") or color.lower() in context["lexers"]:
+ if color[0].islower():
+ return 8
+ elif color[0].isupper():
+ return 256
+ elif color[0] == "#":
+ return 256
+ elif color.isdigit() and (0 <= int(color) and int(color) <= 255) :
+ return 256
+ else:
+ raise UnknownColor(color)
+
+
+def next_in_map( name ):
+ global context
+ # loop over indices in colormap
+ return (context["colormap_idx"]+1) % len(context["colormaps"][name])
+
+
+def color_random( color ):
+ global context
+ m = mode(color)
+ if m == 8:
+ color_name = random.choice(list(context["colormaps"]["random"]))
+ color_code = context["colors"][color_name]
+ color_code = str(30 + color_code)
+
+ elif m == 256:
+ color_nb = random.choice(context["colormaps"]["Random"])
+ color_code = str(color_nb)
+
+ return color_code
+
+
+def color_in_colormaps( color ):
+ global context
+ m = mode(color)
+ if m == 8:
+ c = context["colormaps"][color][context["colormap_idx"]]
+ if c.isdigit():
+ color_code = str(30 + c)
+ else:
+ color_code = str(30 + context["colors"][c])
+
+ else:
+ color_nb = context["colormaps"][color][context["colormap_idx"]]
+ color_code = str( color_nb )
+
+ context["colormap_idx"] = next_in_map(color)
+
+ return color_code
+
+
+def color_scale( name, text ):
+ # filter out everything that does not seem to be necessary to interpret the string as a number
+ # this permits to transform "[ 95%]" to "95" before number conversion,
+ # and thus allows to color a group larger than the matched number
+ chars_in_numbers = "-+.,e/*"
+ allowed = string.digits + chars_in_numbers
+ nb = "".join([i for i in filter(allowed.__contains__, text)])
+
+ # interpret as decimal
+ f = None
+ try:
+ f = float(bn.parse_decimal(nb))
+ except bn.NumberFormatError:
+ pass
+ if f is not None:
+ # normalize with scale if it's a number
+ f = (f - context["scale"][0]) / (context["scale"][1]-context["scale"][0])
+ else:
+ # interpret as float between 0 and 1 otherwise
+ f = eval(nb)
+
+ # if out of scale, do not color
+ if f < 0 or f > 1:
+ return None
+
+ # normalize and scale over the nb of colors in cmap
+ colormap = context["colormaps"][name]
+ i = int( math.ceil( f * (len(colormap)-1) ) )
+ color = colormap[i]
+
+ # infer mode from the color in the colormap
+ m = mode(color)
+
+ if m == 8:
+ color_code = str(30 + context["colors"][color])
+ else:
+ color_code = str(color)
+
+ return color_code
+
+
+def color_hash( name, text ):
+ hasher = hashlib.md5()
+ hasher.update(text.encode('utf-8'))
+ hash = hasher.hexdigest()
+
+ f = float(functools.reduce(lambda x, y: x+ord(y), hash, 0) % 101)
+
+ # normalize and scale over the nb of colors in cmap
+ colormap = context["colormaps"][name]
+ i = int( math.ceil( (f - context["scale"][0]) / (context["scale"][1]-context["scale"][0]) * (len(colormap)-1) ) )
+ color = colormap[i]
+
+ # infer mode from the color in the colormap
+ m = mode(color)
+
+ if m == 8:
+ color_code = str(30 + context["colors"][color])
+ else:
+ color_code = str(color)
+
+ return color_code
+
+
+def color_map(name):
+ global context
+ # current color
+ color = context["colormaps"][name][ context["colormap_idx"] ]
+
+ m = mode(color)
+ if m == 8:
+ color_code = str(30 + context["colors"][color])
+ else:
+ color_nb = int(color)
+ assert( 0 <= color_nb <= 255 )
+ color_code = str(color_nb)
+
+ context["colormap_idx"] = next_in_map(name)
+
+ return color,color_code
+
+
+def color_lexer( name, style, text ):
+ lexer = get_lexer_by_name(name.lower())
+ # Python => 256 colors, python => 8 colors
+ m = mode(name)
+ if m == 256:
+ try:
+ formatter = Terminal256Formatter(style=style)
+ except: # style not found
+ formatter = Terminal256Formatter()
+ else:
+ if style not in ("light","dark"):
+ style = "dark" # dark color scheme by default
+ formatter = TerminalFormatter(bg=style)
+ # We should return all but the last character,
+ # because Pygments adds a newline char.
+ if not debug:
+ return highlight(text, lexer, formatter)[:-1]
+ else:
+ return "<"+name+">"+ highlight(text, lexer, formatter)[:-1] + ""+name+">"
+
+
+def colorin(text, color="red", style="normal", sep_pair=context["sep_pair"]):
"""
Return the given text, surrounded by the given color ASCII markers.
+ The given color may be either a single name, encoding the foreground color,
+ or a pair of names, delimited by the given sep_pair,
+ encoding foreground and background, e.g. "red.blue".
+
If the given color is a name that exists in available colors,
a 8-colors mode is assumed, else, a 256-colors mode.
@@ -110,125 +527,118 @@ def colorin(text, color="red", style="normal"):
>>> colout.colorin("Faites chier la vache", 41, "normal")
'\x1b[0;38;5;41mFaites chier la vache\x1b[0m'
"""
- global colormap_idx
+
+ assert( type(color) is str )
+
+ global debug
# Special characters.
start = "\033["
stop = "\033[0m"
+ # Escaped end markers for given color modes
+ endmarks = {8: ";", 256: ";38;5;"}
+
color_code = ""
style_code = ""
+ background_code = ""
+ style_codes = []
# Convert the style code
if style == "random" or style == "Random":
- style = random.choice(list(styles.keys()))
+ style = random.choice(list(context["styles"].keys()))
else:
- if style in styles:
- style_code = str(styles[style])
+ styles = style.split(sep_pair)
+ for astyle in styles:
+ if astyle in context["styles"]:
+ style_codes.append(str(context["styles"][astyle]))
+ style_code = ";".join(style_codes)
- if color == "none":
+ color_pair = color.strip().split(sep_pair)
+ color = color_pair[0]
+ background = color_pair[1] if len(color_pair) == 2 else "none"
+
+ if color == "none" and background == "none":
# if no color, style cannot be applied
- return text
-
- elif color == "random":
- mode = 8
- color_code = random.choice(list(colors.values()))
- color_code = str(30 + color_code)
-
- elif color == "Random":
- mode = 256
- color_nb = random.randint(0, 255)
- color_code = str(color_nb)
-
- elif color == "rainbow":
- mode = 8
- color = colormap[colormap_idx]
- color_code = str(30 + colors[color])
-
- if colormap_idx < len(colormap)-1:
- colormap_idx += 1
- else:
- colormap_idx = 0
-
- elif color == "Rainbow":
- mode = 256
- color_nb = rgb_to_ansi( *rgb_rainbow( colormap_idx ) )
- color_code = str( color_nb )
-
- if colormap_idx < 255:
- colormap_idx += 1
- else:
- colormap_idx = 0
-
- elif color == "scale":
- try:
- import babel.numbers as bn
- f = float(bn.parse_decimal(text))
- except ImportError:
- f = float(text)
-
- # if out of scale, do not color
- if f < scale[0] or f > scale[1]:
+ if not debug:
return text
+ else:
+ return ""+text+""
- # normalize and scale over the nb of colors in colormap
- i = int( math.ceil( (f - scale[0]) / (scale[1]-scale[0]) * (len(colormap)-1) ) )
+ elif color.lower() == "random":
+ color_code = color_random( color )
- mode = 8
- color = colormap[i]
- color_code = str(30 + colors[color])
+ elif color.lower() == "scale": # "scale" or "Scale"
+ color_code = color_scale( color, text )
+ # "hash" or "Hash"; useful to randomly but consistently color strings
+ elif color.lower() == "hash":
+ color_code = color_hash( color, text )
+
+ # The user can change the "colormap" variable to its favorite one before calling colorin.
elif color == "colormap":
- color = colormap[colormap_idx]
- if color in colors:
- mode = 8
- color_code = str(30 + colors[color])
- else:
- mode = 256
- color_nb = int(color)
- assert(0 <= color_nb <= 255)
- color_code = str(color_nb)
+ # "default" should have been set to the user-defined colormap.
+ color,color_code = color_map("default")
- if colormap_idx < len(colormap)-1:
- colormap_idx += 1
- else:
- colormap_idx = 0
+ # Registered colormaps should be tested after special colors,
+ # because special tags are also registered as colormaps,
+ # but do not have the same simple behavior.
+ elif color in context["colormaps"].keys():
+ color_code = color_in_colormaps( color )
# 8 colors modes
- elif color in colors:
- mode = 8
- color_code = str(30 + colors[color])
+ elif color in context["colors"]:
+ color_code = str(30 + context["colors"][color])
+
+ # hexadecimal color
+ elif color[0] == "#":
+ color_nb = rgb_to_ansi(*hex_to_rgb(color))
+ assert(0 <= color_nb <= 255)
+ color_code = str(color_nb)
# 256 colors mode
elif color.isdigit():
- mode = 256
color_nb = int(color)
assert(0 <= color_nb <= 255)
color_code = str(color_nb)
# programming language
- elif color.lower() in lexers:
- lexer = get_lexer_by_name(color.lower())
- # Python => 256 colors, python => 8 colors
- ask_256 = color[0].isupper()
- if ask_256:
- try:
- formatter = Terminal256Formatter(style=style)
- except: # style not found
- formatter = Terminal256Formatter()
- else:
- if style not in ("light","dark"):
- style = "dark" # dark color scheme by default
- formatter = TerminalFormatter(bg=style)
- # We should return all but the last character,
- # because Pygments adds a newline char.
- return highlight(text, lexer, formatter)[:-1]
+ elif color.lower() in context["lexers"]:
+ # bypass color encoding and return text colored by the lexer
+ return color_lexer(color,style,text)
# unrecognized
else:
- raise Exception('Unrecognized color %s' % color)
+ raise UnknownColor(color)
- return start + style_code + endmarks[mode] + color_code + "m" + text + stop
+ m = mode(color)
+
+ if background in context["backgrounds"] and m == 8:
+ background_code = endmarks[m] + str(40 + context["backgrounds"][background])
+ elif background == "none":
+ background_code = ""
+ else:
+ raise UnknownColor(background)
+
+ if color_code is not None:
+ if not debug:
+ return start + style_code + endmarks[m] + color_code + background_code + "m" + text + stop
+ else:
+ return start + style_code + endmarks[m] + color_code + background_code + "m" \
+ + "" \
+ + text + "" + stop
+ else:
+ if not debug:
+ return text
+ else:
+ return "" + text + ""
def colorout(text, match, prev_end, color="red", style="normal", group=0):
@@ -244,7 +654,7 @@ def colorout(text, match, prev_end, color="red", style="normal", group=0):
return colored_text, end
-def colorup(text, pattern, color="red", style="normal", on_groups=False):
+def colorup(text, pattern, color="red", style="normal", on_groups=False, sep_list=context["sep_list"]):
"""
Color up every characters that match the given regexp patterns.
If groups are specified, only color up them and not the whole pattern.
@@ -253,22 +663,15 @@ def colorup(text, pattern, color="red", style="normal", on_groups=False):
in which case the different matching groups may be formatted differently.
If there is less colors/styles than groups, the last format is used
for the additional groups.
-
- >>> colorup("Fetchez la vache", "vache", "red", "bold")
- 'Fetchez la \x1b[1;31mvache\x1b[0m'
- >>> colorup("Faites chier la vache", "[Fv]a", "red", "bold")
- '\x1b[1;31mFa\x1b[0mites chier la \x1b[1;31mva\x1b[0mche'
- >>> colorup("Faites Chier la Vache", "[A-Z](\S+)\s", "red", "bold")
- 'F\x1b[1;31maites\x1b[0m C\x1b[1;31mhier\x1b[0m la Vache'
- >>> colorup("Faites Chier la Vache", "([A-Z])(\S+)\s", "red,green", "bold")
- '\x1b[1;31mF\x1b[0m\x1b[1;32maites\x1b[0m \x1b[1;31mC\x1b[0m\x1b[1;32mhier\x1b[0m la Vache'
- >>> colorup("Faites Chier la Vache", "([A-Z])(\S+)\s", "green")
- '\x1b[0;32mF\x1b[0m\x1b[0;32maites\x1b[0m \x1b[0;32mC\x1b[0m\x1b[0;32mhier\x1b[0m la Vache'
- >>> colorup("Faites Chier la Vache", "([A-Z])(\S+)\s", "blue", "bold,italic")
- '\x1b[1;34mF\x1b[0m\x1b[3;34maites\x1b[0m \x1b[1;34mC\x1b[0m\x1b[3;34mhier\x1b[0m la Vache'
"""
- global colormap_idx
- regex = re.compile(pattern) # , re.IGNORECASE)
+
+ global context
+ global debug
+
+ if not debug:
+ regex = re.compile(pattern)
+ else:
+ regex = re.compile(pattern, re.DEBUG)
# Prepare the colored text.
colored_text = ""
@@ -287,25 +690,27 @@ def colorup(text, pattern, color="red", style="normal", on_groups=False):
# Build a list of colors that match the number of grouped,
# if there is not enough colors, duplicate the last one.
- colors_l = color.split(",")
+ colors_l = color.split(sep_list)
group_colors = colors_l + [colors_l[-1]] * (nb_groups - len(colors_l))
# Same for styles
- styles_l = style.split(",")
+ styles_l = style.split(sep_list)
group_styles = styles_l + [styles_l[-1]] * (nb_groups - len(styles_l))
# If we want to iterate colormaps on groups instead of patterns
if on_groups:
# Reset the counter at the beginning of each match
- colormap_idx = 0
+ context["colormap_idx"] = 0
# For each group index.
# Note that match.groups returns a tuple (thus being indexed in [0,n[),
# but that match.start(0) refers to the whole match, the groups being indexed in [1,n].
# Thus, we need to range in [1,n+1[.
for group in range(1, nb_groups+1):
- partial, end = colorout(text, match, end, group_colors[group-1], group_styles[group-1], group)
- colored_text += partial
+ # If a group didn't match, there's nothing to color
+ if match.group(group) is not None:
+ partial, end = colorout(text, match, end, group_colors[group-1], group_styles[group-1], group)
+ colored_text += partial
# Append the remaining part of the text, if any.
colored_text += text[end:]
@@ -324,20 +729,29 @@ def colortheme(item, theme):
Used to read themes, which can be something like:
[ [ pattern, colors, styles ], [ pattern ], [ pattern, colors ] ]
"""
+ # logging.debug("use a theme with %i arguments" % len(theme))
for args in theme:
item = colorup(item, *args)
return item
-def write(colored):
+def write(colored, stream = sys.stdout):
"""
Write "colored" on sys.stdout, then flush.
"""
- sys.stdout.write(colored)
- sys.stdout.flush()
+ try:
+ stream.write(colored)
+ stream.flush()
+
+ # Silently handle broken pipes
+ except IOError:
+ try:
+ stream.close()
+ except IOError:
+ pass
-def map_write( stream, function, *args ):
+def map_write( stream_in, stream_out, function, *args ):
"""
Read the given file-like object as a non-blocking stream
and call the function on each item (line),
@@ -350,15 +764,17 @@ def map_write( stream, function, *args ):
"""
while True:
try:
- item = stream.readline()
+ item = stream_in.readline()
+ except UnicodeDecodeError:
+ continue
except KeyboardInterrupt:
break
if not item:
break
- write( function(item, *args) )
+ write( function(item, *args), stream_out )
-def colorgen(stream, pattern, color="red", style="normal", on_groups=False):
+def colorgen(stream, pattern, color="red", style="normal", on_groups=False, sep_list=context["sep_list"]):
"""
A generator that colors the items given in an iterable input.
@@ -374,72 +790,14 @@ def colorgen(stream, pattern, color="red", style="normal", on_groups=False):
break
if not item:
break
- yield colorup(item, pattern, color, style, on_groups)
+ yield colorup(item, pattern, color, style, on_groups, sep_list)
######################
# Command line tools #
######################
-def __args_dirty__(argv, usage=""):
- """
- Roughly extract options from the command line arguments.
- To be used only when argparse is not available.
-
- Returns a tuple of (pattern,color,style,on_stderr).
-
- >>> colout.__args_dirty__(["colout","pattern"],"usage")
- ('pattern', 'red', 'normal', False)
- >>> colout.__args_dirty__(["colout","pattern","colors","styles"],"usage")
- ('pattern', 'colors', 'styles', False)
- >>> colout.__args_dirty__(["colout","pattern","colors","styles","True"],"usage")
- ('pattern', 'colors', 'styles', True)
- """
- import sys
-
- # Use a dirty argument picker
- # Check for bad usage or an help flag
- if len(argv) < 2 \
- or len(argv) > 10 \
- or argv[1] == "--help" \
- or argv[1] == "-h":
- print(usage+"\n")
- print("Usage:", argv[0], " [