on master help sign in
Feature: Highlight non-printable characters

Adds a new `-A`/`--show-all` option (in analogy to GNU Linux `cat`s option) that highlights non-printable characters like space, tab or newline. This works in two steps: - **Preprocessing**: replace space by `•`, replace tab by `├──┤`, replace newline by `␤`, etc. - **Highlighting**: Use a newly written Sublime syntax to highlight these special symbols. Note: This feature is not technically a drop-in replacement for GNU `cat`s `--show-all` but it has the same purpose.

commitecd862d9ff729a5de5e7b2771be02155567b72cf authorsharkdp <davidpeter@web.de> dateThu 1 Nov 2018, 13:02 (8 years ago) parentcbed338
6 files changed+108−5
assets/syntaxes/show-nonprintable.sublime-syntax+25 src/app.rs+11−1 src/clap_app.rs+12 src/controller.rs+18−1 src/preprocessor.rs+40−1 src/printer.rs+2−2
assets/syntaxes/show-nonprintable.sublime-syntaxnew file+25
@@ -0,0 +1,25 @@
1+%YAML 1.2
2+---
3+# http://www.sublimetext.com/docs/3/syntax.html
4+name: Highlight non-printables
5+file_extensions:
6+ - show-nonprintable
7+scope: whitespace
8+contexts:
9+ main:
10+ - match: "•"
11+ scope: support.function.show-nonprintable.space
12+ - match: "├─*┤"
13+ scope: constant.character.escape.show-nonprintable.tab
14+ - match: "␤"
15+ scope: keyword.operator.show-nonprintable.newline
16+ - match: "␍"
17+ scope: string.show-nonprintable.carriage-return
18+ - match: "␀"
19+ scope: entity.other.attribute-name.show-nonprintable.null
20+ - match: "␇"
21+ scope: entity.other.attribute-name.show-nonprintable.bell
22+ - match: "␛"
23+ scope: entity.other.attribute-name.show-nonprintable.escape
24+ - match: "␈"
25+ scope: entity.other.attribute-name.show-nonprintable.backspace
src/app.rs+11−1
@@ -37,6 +37,9 @@ pub struct Config<'a> {
3737 /// The explicitly configured language, if any
3838 pub language: Option<&'a str>,
3939
40+ /// Whether or not to show/replace non-printable characters like space, tab and newline.
41+ pub show_nonprintable: bool,
42+
4043 /// The character width of the terminal
4144 pub term_width: usize,
4245
@@ -169,7 +172,14 @@ impl App {
169172
170173 Ok(Config {
171174 true_color: is_truecolor_terminal(),
172− language: self.matches.value_of("language"),
175+ language: self.matches.value_of("language").or_else(|| {
176+ if self.matches.is_present("show-all") {
177+ Some("show-nonprintable")
178+ } else {
179+ None
180+ }
181+ }),
182+ show_nonprintable: self.matches.is_present("show-all"),
173183 output_wrap: if !self.interactive_output {
174184 // We don't have the tty width when piping to another program.
175185 // There's no point in wrapping when this is the case.
src/clap_app.rs+12
@@ -158,6 +158,18 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
158158 '--style=numbers'",
159159 ),
160160 )
161+ .arg(
162+ Arg::with_name("show-all")
163+ .long("show-all")
164+ .alias("show-nonprintable")
165+ .short("A")
166+ .conflicts_with("language")
167+ .help("Show non-printable characters (space, tab, newline, ..).")
168+ .long_help(
169+ "Show non-printable characters like space, tab or newline. \
170+ Use '--tabs' to control the width of the tab-placeholders.",
171+ ),
172+ )
161173 .arg(
162174 Arg::with_name("line-range")
163175 .long("line-range")
src/controller.rs+18−1
@@ -1,4 +1,5 @@
11use std::io::{self, Write};
2+use std::mem::swap;
23
34use app::Config;
45use assets::HighlightingAssets;
@@ -6,6 +7,7 @@ use errors::*;
67use inputfile::{InputFile, InputFileReader};
78use line_range::{LineRanges, RangeCheckResult};
89use output::OutputType;
10+use preprocessor::replace_nonprintable;
911use printer::{InteractivePrinter, Printer, SimplePrinter};
1012
1113pub struct Controller<'a> {
@@ -64,7 +66,14 @@ impl<'b> Controller<'b> {
6466 input_file: InputFile<'a>,
6567 ) -> Result<()> {
6668 printer.print_header(writer, input_file)?;
67− self.print_file_ranges(printer, writer, reader, &self.config.line_ranges)?;
69+ self.print_file_ranges(
70+ printer,
71+ writer,
72+ reader,
73+ &self.config.line_ranges,
74+ self.config.show_nonprintable,
75+ self.config.tab_width,
76+ )?;
6877 printer.print_footer(writer)?;
6978
7079 Ok(())
@@ -76,12 +85,20 @@ impl<'b> Controller<'b> {
7685 writer: &mut Write,
7786 mut reader: InputFileReader,
7887 line_ranges: &LineRanges,
88+ show_nonprintable: bool,
89+ tab_width: usize,
7990 ) -> Result<()> {
8091 let mut line_buffer = Vec::new();
92+ let mut line_buffer_processed = Vec::new();
8193
8294 let mut line_number: usize = 1;
8395
8496 while reader.read_line(&mut line_buffer)? {
97+ if show_nonprintable {
98+ replace_nonprintable(&mut line_buffer, &mut line_buffer_processed, tab_width);
99+ swap(&mut line_buffer, &mut line_buffer_processed);
100+ }
101+
85102 match line_ranges.check(line_number) {
86103 RangeCheckResult::OutsideRange => {
87104 // Call the printer in case we need to call the syntax highlighter
src/preprocessor.rs+40−1
@@ -1,7 +1,7 @@
11use console::AnsiCodeIterator;
22
33/// Expand tabs like an ANSI-enabled expand(1).
4−pub fn expand(line: &str, width: usize, cursor: &mut usize) -> String {
4+pub fn expand_tabs(line: &str, width: usize, cursor: &mut usize) -> String {
55 let mut buffer = String::with_capacity(line.len() * 2);
66
77 for chunk in AnsiCodeIterator::new(line) {
@@ -32,3 +32,42 @@ pub fn expand(line: &str, width: usize, cursor: &mut usize) -> String {
3232
3333 buffer
3434}
35+
36+pub fn replace_nonprintable(input: &mut Vec<u8>, output: &mut Vec<u8>, tab_width: usize) {
37+ output.clear();
38+
39+ let tab_width = if tab_width == 0 {
40+ 4
41+ } else if tab_width == 1 {
42+ 2
43+ } else {
44+ tab_width
45+ };
46+
47+ for chr in input {
48+ match *chr {
49+ // space
50+ b' ' => output.extend_from_slice("•".as_bytes()),
51+ // tab
52+ b'\t' => {
53+ output.extend_from_slice("├".as_bytes());
54+ output.extend_from_slice("─".repeat(tab_width - 2).as_bytes());
55+ output.extend_from_slice("┤".as_bytes());
56+ }
57+ // new line
58+ b'\n' => output.extend_from_slice("␤".as_bytes()),
59+ // carriage return
60+ b'\r' => output.extend_from_slice("␍".as_bytes()),
61+ // null
62+ 0x00 => output.extend_from_slice("␀".as_bytes()),
63+ // bell
64+ 0x07 => output.extend_from_slice("␇".as_bytes()),
65+ // backspace
66+ 0x08 => output.extend_from_slice("␈".as_bytes()),
67+ // escape
68+ 0x1B => output.extend_from_slice("␛".as_bytes()),
69+ // anything else
70+ _ => output.push(*chr),
71+ }
72+ }
73+}
src/printer.rs+2−2
@@ -22,7 +22,7 @@ use diff::get_git_diff;
2222use diff::LineChanges;
2323use errors::*;
2424use inputfile::{InputFile, InputFileReader};
25−use preprocessor::expand;
25+use preprocessor::expand_tabs;
2626use style::OutputWrap;
2727use terminal::{as_terminal_escaped, to_ansi_color};
2828
@@ -177,7 +177,7 @@ impl<'a> InteractivePrinter<'a> {
177177
178178 fn preprocess(&self, text: &str, cursor: &mut usize) -> String {
179179 if self.config.tab_width > 0 {
180− expand(text, self.config.tab_width, cursor)
180+ expand_tabs(text, self.config.tab_width, cursor)
181181 } else {
182182 text.to_string()
183183 }