1037 lines
26 KiB
Rust
1037 lines
26 KiB
Rust
use std::env;
|
||
use std::fs::File;
|
||
use std::io::{self, Read, Write};
|
||
use std::process;
|
||
|
||
macro_rules! println_stderr(
|
||
($($arg:tt)*) => (
|
||
if let Err(e) = writeln!(&mut io::stderr(), $($arg)* ) {
|
||
panic!("Unable to write to stderr: {}", e);
|
||
}
|
||
)
|
||
);
|
||
|
||
fn write_bytes_or_panic<L: Write>(locked_io: &mut L, bytes: &[u8]) {
|
||
if let Err(e) = locked_io.write_all(bytes) {
|
||
panic!("Unable to write to stderr: {}", e);
|
||
}
|
||
}
|
||
|
||
fn blame_path(path: std::ffi::OsString, blame: &str) {
|
||
let printable = path.to_string_lossy();
|
||
println_stderr!("{}: {}", printable, blame);
|
||
}
|
||
|
||
fn blame_path_io(path: std::ffi::OsString, e: &std::io::Error) {
|
||
let printable = path.to_string_lossy();
|
||
println_stderr!("{}: {}", printable, e);
|
||
}
|
||
|
||
fn main() {
|
||
let mut args: std::env::ArgsOs = env::args_os();
|
||
args.next();
|
||
|
||
let mut sett = Settings {
|
||
osel: OutputSelector::DIFF,
|
||
syntax: true,
|
||
};
|
||
|
||
let mut exit_code: i32 = 0;
|
||
loop {
|
||
let arg = match args.next() {
|
||
Some(arg) => arg,
|
||
None => { break; }
|
||
};
|
||
let nonopt = match arg.into_string() {
|
||
Ok(comparable) => {
|
||
match comparable.as_ref() {
|
||
"--suggest" => {
|
||
sett.osel=OutputSelector::DIFF;
|
||
sett.syntax=false;
|
||
None
|
||
},
|
||
"--syntax" => {
|
||
sett.osel=OutputSelector::ORIGINAL;
|
||
sett.syntax=true;
|
||
None
|
||
},
|
||
"--syntax-suggest" => {
|
||
sett.osel=OutputSelector::DIFF;
|
||
sett.syntax=true;
|
||
None
|
||
},
|
||
"--besserwisser" => {
|
||
sett.osel=OutputSelector::BESSERWISSER;
|
||
sett.syntax=false;
|
||
None
|
||
},
|
||
"--help" => {
|
||
println!(
|
||
"Naziquote: A bash syntax highlighter that encourages\n\
|
||
(and can fix) proper quoting of variales.\n\
|
||
\n\
|
||
Usage:\n\
|
||
naziquote filename.bash\n\
|
||
cat filename.bash | naziquote ''\n\
|
||
\n\
|
||
Options:\n\
|
||
--suggest Output a colored diff suggesting changes.\n\
|
||
--syntax Output syntax highlighting with ANSI colors.\n\
|
||
--syntax-suggest Diff with syntax highlighting (default mode).\n\
|
||
--besserwisser Output suggested changes.\n\
|
||
"
|
||
);
|
||
None
|
||
},
|
||
_ => Some(std::ffi::OsString::from(comparable))
|
||
}
|
||
},
|
||
Err(same) => Some(same)
|
||
};
|
||
if let Some(path) = nonopt {
|
||
if let Err(e) = treatfile(&path, &sett) {
|
||
println!("\x1b[m");
|
||
perror_error(path, &e);
|
||
exit_code = 1;
|
||
}
|
||
}
|
||
}
|
||
process::exit(exit_code);
|
||
}
|
||
|
||
#[derive(Clone)]
|
||
#[derive(Copy)]
|
||
enum OutputSelector {
|
||
ORIGINAL,
|
||
DIFF,
|
||
BESSERWISSER,
|
||
}
|
||
|
||
struct Settings {
|
||
osel :OutputSelector,
|
||
syntax :bool,
|
||
}
|
||
|
||
struct UnsupportedSyntax {
|
||
ctx: Vec<u8>,
|
||
pos: usize,
|
||
typ: &'static str,
|
||
msg: &'static str,
|
||
}
|
||
|
||
enum Error {
|
||
Stdio(std::io::Error),
|
||
Syntax(UnsupportedSyntax),
|
||
}
|
||
|
||
type ParseResult = Result<WhatNow, UnsupportedSyntax>;
|
||
|
||
fn perror_error(path: std::ffi::OsString, e: &Error) {
|
||
match e {
|
||
&Error::Stdio(ref fail) => { blame_path_io(path, &fail); },
|
||
&Error::Syntax(ref fail) => {
|
||
blame_path(path, fail.typ);
|
||
blame_syntax(fail);
|
||
},
|
||
}
|
||
}
|
||
|
||
fn blame_syntax(fail: &UnsupportedSyntax) {
|
||
if fail.pos < fail.ctx.len() {
|
||
let mut i = fail.pos;
|
||
while i > 0 {
|
||
i -= 1;
|
||
if fail.ctx[i] == b'\n' {
|
||
break;
|
||
}
|
||
}
|
||
let failing_line_begin = if fail.ctx[i] == b'\n' { i + 1 } else { 0 };
|
||
let mut i = fail.pos;
|
||
while i < fail.ctx.len() && fail.ctx[i] != b'\n' {
|
||
i += 1;
|
||
}
|
||
let failing_line_end = i;
|
||
// FIXME: This counts codepoints, not displayed width.
|
||
let mut width = 0;
|
||
for c in &fail.ctx[failing_line_begin .. fail.pos] {
|
||
if c >> b'\x06' != b'\x02' {
|
||
width += 1;
|
||
}
|
||
}
|
||
let width = width;
|
||
|
||
let stderr = io::stderr();
|
||
let mut stderr_lock = stderr.lock();
|
||
write_bytes_or_panic(&mut stderr_lock, &fail.ctx[.. failing_line_end]);
|
||
write_bytes_or_panic(&mut stderr_lock, b"\n");
|
||
for _ in 0 .. width {
|
||
write_bytes_or_panic(&mut stderr_lock, b" ");
|
||
}
|
||
write_bytes_or_panic(&mut stderr_lock, b"^\n");
|
||
}
|
||
println_stderr!("{}", fail.msg);
|
||
}
|
||
|
||
enum FileOrStdinInput<'a> {
|
||
File(std::fs::File),
|
||
Stdin(std::io::StdinLock<'a>),
|
||
}
|
||
|
||
trait OpenAndRead {
|
||
fn open_file(path: &std::ffi::OsString) -> Result<FileOrStdinInput, std::io::Error>;
|
||
fn open_stdin(stdin: &std::io::Stdin) -> FileOrStdinInput;
|
||
fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error>;
|
||
}
|
||
|
||
impl<'a> OpenAndRead for FileOrStdinInput<'a> {
|
||
fn open_file(path: &std::ffi::OsString) -> Result<FileOrStdinInput, std::io::Error> {
|
||
Ok(FileOrStdinInput::File(try!(File::open(path))))
|
||
}
|
||
fn open_stdin(stdin: &std::io::Stdin) -> FileOrStdinInput {
|
||
FileOrStdinInput::Stdin(io::Stdin::lock(&stdin))
|
||
}
|
||
fn read(&mut self, mut buf: &mut [u8]) -> Result<usize, std::io::Error> {
|
||
match self {
|
||
&mut FileOrStdinInput::Stdin(ref mut fh) => fh.read(&mut buf),
|
||
&mut FileOrStdinInput::File (ref mut fh) => fh.read(&mut buf),
|
||
}
|
||
}
|
||
}
|
||
|
||
fn treatfile(path: &std::ffi::OsString, sett: &Settings) -> Result<(), Error> {
|
||
const BUFSIZE :usize = 128;
|
||
let mut fill :usize = 0;
|
||
let mut buf = [0; BUFSIZE];
|
||
|
||
let mut state :Vec<Box<Situation>> = vec!{Box::new(SitCommand{
|
||
end_trigger: 0x100,
|
||
end_replace: None
|
||
})};
|
||
|
||
let stdin = io::stdin();
|
||
let mut fh: FileOrStdinInput = if path.is_empty() {
|
||
FileOrStdinInput::open_stdin(&stdin)
|
||
} else {
|
||
try!(FileOrStdinInput::open_file(path).map_err(|e| Error::Stdio(e)))
|
||
};
|
||
let stdout = io::stdout();
|
||
let mut out = stdout.lock();
|
||
loop {
|
||
let bytes = try!(fh.read(&mut buf[fill ..]).map_err(|e| Error::Stdio(e)));
|
||
fill += bytes;
|
||
let eof = bytes == 0;
|
||
let consumed = try!(stackmachine(&mut state, &mut out, &buf[0 .. fill], eof, &sett));
|
||
let remain = fill - consumed;
|
||
if eof {
|
||
assert!(remain == 0);
|
||
break;
|
||
}
|
||
for i in 0 .. remain {
|
||
buf[i] = buf[consumed + i];
|
||
}
|
||
fill = remain;
|
||
}
|
||
if state.len() == 1 {
|
||
Ok(())
|
||
} else {
|
||
Err(Error::Syntax(UnsupportedSyntax{
|
||
typ: "Unexpected end of file",
|
||
ctx: buf[0 .. fill].to_owned(),
|
||
pos: fill,
|
||
msg: "The file's end was reached without closing all sytactic scopes.\n\
|
||
Either, the parser got lost, or the file is truncated or malformed.",
|
||
}))
|
||
}
|
||
}
|
||
|
||
fn stackmachine(
|
||
state: &mut Vec<Box<Situation>>,
|
||
out: &mut std::io::StdoutLock,
|
||
buf: &[u8],
|
||
eof: bool,
|
||
sett: &Settings,
|
||
) -> Result<usize, Error> {
|
||
let mut pos :usize = 0;
|
||
loop {
|
||
let horizon :&[u8] = &buf[pos .. buf.len()];
|
||
let is_horizon_lengthenable = pos > 0 && !eof;
|
||
let whatnow :WhatNow = try!(state.last_mut().unwrap().as_mut().whatnow(
|
||
&horizon, is_horizon_lengthenable
|
||
).map_err(|e| Error::Syntax(e)));
|
||
|
||
try!(out.write(&horizon[.. whatnow.pre]).map_err(|e| Error::Stdio(e)));
|
||
let replaceable = &horizon[whatnow.pre .. whatnow.pre + whatnow.len];
|
||
let progress = whatnow.pre + whatnow.len;
|
||
let whatnow = match whatnow.tri {
|
||
Transition::FlushPopOnEof => {
|
||
if eof {
|
||
WhatNow{
|
||
tri: Transition::Pop,
|
||
pre: whatnow.pre,
|
||
len: whatnow.len,
|
||
alt: whatnow.alt,
|
||
}
|
||
} else {
|
||
WhatNow{
|
||
tri: Transition::Flush,
|
||
pre: whatnow.pre,
|
||
len: 0,
|
||
alt: None,
|
||
}
|
||
}
|
||
},
|
||
_ => whatnow
|
||
};
|
||
match whatnow.tri {
|
||
Transition::Flush => {
|
||
if progress == 0 {
|
||
break;
|
||
}
|
||
if pos == buf.len() {
|
||
break;
|
||
}
|
||
}
|
||
Transition::FlushPopOnEof => {
|
||
panic!("This case shall be filtered out");
|
||
}
|
||
Transition::Replace(newstate) => {
|
||
let ix = state.len() - 1;
|
||
let color_pre;
|
||
let color_final;
|
||
if sett.syntax {
|
||
color_pre = state[ix].get_color();
|
||
color_final = newstate.get_color();
|
||
} else {
|
||
color_pre = COLOR_NORMAL;
|
||
color_final = COLOR_NORMAL;
|
||
};
|
||
try!(write_transition(
|
||
out, sett, replaceable, whatnow.alt,
|
||
color_pre, color_pre, color_final,
|
||
).map_err(|e| Error::Stdio(e)));
|
||
state[ix] = newstate;
|
||
}
|
||
Transition::Push(newstate) => {
|
||
let color_final = if sett.syntax {
|
||
newstate.get_color()
|
||
} else {
|
||
COLOR_NORMAL
|
||
};
|
||
state.push(newstate);
|
||
try!(write_transition(
|
||
out, sett, replaceable, whatnow.alt,
|
||
COLOR_NORMAL, color_final, color_final,
|
||
).map_err(|e| Error::Stdio(e)));
|
||
}
|
||
Transition::Pop => {
|
||
let color_pre;
|
||
let color_final;
|
||
if sett.syntax {
|
||
color_pre = state[state.len() - 1].get_color();
|
||
color_final = state[state.len() - 2].get_color();
|
||
} else {
|
||
color_pre = COLOR_NORMAL;
|
||
color_final = COLOR_NORMAL;
|
||
};
|
||
state.pop();
|
||
try!(write_transition(
|
||
out, sett, replaceable, whatnow.alt,
|
||
color_pre, color_pre, color_final,
|
||
).map_err(|e| Error::Stdio(e)));
|
||
}
|
||
}
|
||
pos += progress;
|
||
}
|
||
Ok(pos)
|
||
}
|
||
|
||
const COLOR_NORMAL: u32 = 0xff000000;
|
||
|
||
fn write_transition(
|
||
out: &mut std::io::StdoutLock,
|
||
sett: &Settings,
|
||
replaceable: &[u8],
|
||
alternative: Option<&[u8]>,
|
||
color_pre: u32,
|
||
color_transition: u32,
|
||
color_final: u32,
|
||
) -> Result<(), std::io::Error> {
|
||
let mut color_cur = color_pre;
|
||
try!(match (alternative, sett.osel) {
|
||
(Some(replacement), OutputSelector::DIFF) => {
|
||
write_diff(out, &mut color_cur, color_transition, replaceable, &replacement)
|
||
},
|
||
(Some(replacement), OutputSelector::BESSERWISSER) => {
|
||
write_colored_slice(out, &mut color_cur, color_transition, replacement)
|
||
},
|
||
(_, _) => {
|
||
write_colored_slice(out, &mut color_cur, color_transition, replaceable)
|
||
},
|
||
});
|
||
if color_cur != color_final {
|
||
try!(write_color(out, color_final));
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
// Edit distance without replacement; greedy, but that suffices.
|
||
fn write_diff(
|
||
out: &mut std::io::StdoutLock,
|
||
mut color_cur: &mut u32,
|
||
color_neutral: u32,
|
||
replaceable: &[u8],
|
||
replacement: &[u8],
|
||
) -> Result<(), std::io::Error> {
|
||
let color_a = 0x02800000;
|
||
let color_b = 0x02008000;
|
||
let remain_a = replaceable;
|
||
let mut remain_b = replacement;
|
||
for i in 0 .. remain_a.len() {
|
||
let color_next;
|
||
let a: u8 = remain_a[i];
|
||
if let Some(pivot_b) = remain_b.iter().position(|&b| b == a) {
|
||
color_next = color_neutral;
|
||
try!(write_colored_slice(out, &mut color_cur, color_b, &remain_b[0 .. pivot_b]));
|
||
remain_b = &remain_b[pivot_b+1 ..];
|
||
} else {
|
||
color_next = color_a;
|
||
}
|
||
try!(write_colored_slice(out, &mut color_cur, color_next, &remain_a[i .. i+1]));
|
||
}
|
||
write_colored_slice(out, &mut color_cur, color_b, &remain_b)
|
||
}
|
||
|
||
fn write_colored_slice(
|
||
out: &mut std::io::StdoutLock,
|
||
color_cur: &mut u32,
|
||
color: u32,
|
||
slice: &[u8],
|
||
) -> Result<(), std::io::Error> {
|
||
if slice.len() > 0 && *color_cur != color {
|
||
try!(write_color(out, color));
|
||
*color_cur = color;
|
||
}
|
||
try!(out.write(slice));
|
||
Ok(())
|
||
}
|
||
|
||
fn write_color(out :&mut std::io::StdoutLock, code :u32) -> Result<(), std::io::Error> {
|
||
if code == COLOR_NORMAL {
|
||
write!(out, "\x1b[m")
|
||
} else {
|
||
let b = code & 0xff;
|
||
let g = (code >> 8) & 0xff;
|
||
let r = (code >> 16) & 0xff;
|
||
let bold = (code >> 24) & 0x1;
|
||
let bg = (code >> 25) & 0x7f;
|
||
write!(out, "\x1b[{};{}8;2;{};{};{}m", bold, bg+3, r, g, b)
|
||
}
|
||
}
|
||
|
||
//------------------------------------------------------------------------------
|
||
|
||
trait Situation {
|
||
fn whatnow(&mut self, horizon: &[u8], is_horizon_lengthenable: bool) -> ParseResult;
|
||
fn get_color(&self) -> u32;
|
||
}
|
||
|
||
enum Transition {
|
||
Flush,
|
||
FlushPopOnEof,
|
||
Replace(Box<Situation>),
|
||
Push(Box<Situation>),
|
||
Pop,
|
||
}
|
||
|
||
struct WhatNow {
|
||
tri :Transition,
|
||
pre :usize,
|
||
len :usize,
|
||
alt :Option<&'static [u8]>,
|
||
}
|
||
|
||
//------------------------------------------------------------------------------
|
||
|
||
struct SitCommand {
|
||
end_trigger :u16,
|
||
end_replace :Option<&'static [u8]>,
|
||
}
|
||
|
||
impl Situation for SitCommand {
|
||
fn whatnow(&mut self, horizon: &[u8], is_horizon_lengthenable: bool) -> ParseResult {
|
||
for i in 0 .. horizon.len() {
|
||
if horizon[i] as u16 == self.end_trigger {
|
||
return Ok(WhatNow {
|
||
tri: Transition::Pop, pre: i, len: 1,
|
||
alt: self.end_replace
|
||
});
|
||
}
|
||
if horizon[i] == b'#' {
|
||
return Ok(WhatNow{
|
||
tri: Transition::Push(Box::new(SitUntilByte{
|
||
until: b'\n', color: 0x01282828, end_replace: None
|
||
})),
|
||
pre: i, len: 1, alt: None
|
||
});
|
||
}
|
||
if horizon[i] == b'\'' {
|
||
return Ok(WhatNow{
|
||
tri: Transition::Push(Box::new(SitUntilByte{
|
||
until: b'\'', color: 0x00ffff00, end_replace: None
|
||
})),
|
||
pre: i, len: 1, alt: None
|
||
});
|
||
}
|
||
if horizon[i] == b'\"' {
|
||
return Ok(WhatNow{
|
||
tri: Transition::Push(Box::new(SitStrDq{})),
|
||
pre: i, len: 1, alt: None
|
||
});
|
||
}
|
||
match common_str_cmd(&horizon, i, is_horizon_lengthenable, true) {
|
||
CommonStrCmdResult::None => {},
|
||
CommonStrCmdResult::Err(e) => { return Err(e); },
|
||
CommonStrCmdResult::Ok(consult)
|
||
| CommonStrCmdResult::OnlyWithoutQuotes(consult)=> {
|
||
return Ok(consult);
|
||
},
|
||
CommonStrCmdResult::OnlyWithQuotes(_) => {
|
||
return Ok(WhatNow{
|
||
tri: Transition::Push(Box::new(SitStrPhantom{
|
||
cmd_end_trigger: self.end_trigger,
|
||
})), pre: i, len: 0, alt: Some(b"\"")
|
||
});
|
||
},
|
||
}
|
||
let (ate, delimiter) = find_heredoc(&horizon[i ..]);
|
||
if i + ate == horizon.len() {
|
||
if is_horizon_lengthenable {
|
||
return Ok(flush(i));
|
||
}
|
||
} else if delimiter.len() > 0 {
|
||
return Ok(WhatNow{
|
||
tri: Transition::Push(Box::new(
|
||
SitVec{terminator: delimiter, color: 0x0077ff00}
|
||
)),
|
||
pre: i, len: ate, alt: None
|
||
});
|
||
} else if ate > 0 {
|
||
return Ok(flush(i + ate));
|
||
}
|
||
}
|
||
Ok(flush(horizon.len()))
|
||
}
|
||
fn get_color(&self) -> u32 {
|
||
COLOR_NORMAL
|
||
}
|
||
}
|
||
|
||
struct SitStrPhantom {
|
||
cmd_end_trigger: u16,
|
||
}
|
||
|
||
impl Situation for SitStrPhantom {
|
||
fn whatnow(&mut self, horizon: &[u8], is_horizon_lengthenable: bool) -> ParseResult {
|
||
let mouthful = predlen(&is_phantomstringfood, &horizon);
|
||
if mouthful == horizon.len() {
|
||
if is_horizon_lengthenable {
|
||
return Ok(flush(0));
|
||
}
|
||
} else if horizon[mouthful] as u16 != self.cmd_end_trigger {
|
||
match horizon[mouthful] {
|
||
b'\"' => {
|
||
return Ok(WhatNow{
|
||
tri: Transition::Replace(Box::new(SitStrDq{})),
|
||
pre: mouthful, len: 1, alt: Some(b"")
|
||
});
|
||
}
|
||
b'$' | b'\\' | b'`' => {
|
||
match common_str_cmd(&horizon, mouthful, is_horizon_lengthenable, true) {
|
||
CommonStrCmdResult::None => {},
|
||
CommonStrCmdResult::Err(e) => { return Err(e); },
|
||
CommonStrCmdResult::Ok(consult) |
|
||
CommonStrCmdResult::OnlyWithQuotes(consult) => {
|
||
match &consult.tri {
|
||
&Transition::Flush | &Transition::FlushPopOnEof => {
|
||
return Ok(WhatNow{
|
||
tri: Transition::FlushPopOnEof,
|
||
pre: 0, len: 0, alt: Some(b"\"")
|
||
});
|
||
}
|
||
&Transition::Pop | &Transition::Replace(_) => {}
|
||
&Transition::Push(_) => {
|
||
return Ok(consult);
|
||
}
|
||
}
|
||
},
|
||
CommonStrCmdResult::OnlyWithoutQuotes(_) => {},
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
// Dutifully end the string.
|
||
return Ok(WhatNow{
|
||
tri: Transition::Pop, pre: 0, len: 0, alt: Some(b"\"")
|
||
});
|
||
}
|
||
fn get_color(&self) -> u32{
|
||
0x00ff0000
|
||
}
|
||
}
|
||
|
||
struct SitStrDq {}
|
||
|
||
impl Situation for SitStrDq {
|
||
fn whatnow(&mut self, horizon: &[u8], is_horizon_lengthenable: bool) -> ParseResult {
|
||
for i in 0 .. horizon.len() {
|
||
if horizon[i] == b'\"' {
|
||
return Ok(WhatNow{tri: Transition::Pop, pre: i, len: 1, alt: None});
|
||
}
|
||
match common_str_cmd(&horizon, i, is_horizon_lengthenable, false) {
|
||
CommonStrCmdResult::None => {},
|
||
CommonStrCmdResult::Err(e) => { return Err(e); },
|
||
CommonStrCmdResult::Ok(x) => { return Ok(x); },
|
||
CommonStrCmdResult::OnlyWithQuotes(x) => { return Ok(x); },
|
||
CommonStrCmdResult::OnlyWithoutQuotes(_) => {
|
||
panic!("Unreachability assertion failed");
|
||
},
|
||
}
|
||
}
|
||
Ok(flush(horizon.len()))
|
||
}
|
||
fn get_color(&self) -> u32{
|
||
0x00ff0000
|
||
}
|
||
}
|
||
|
||
fn flush(i: usize) -> WhatNow {
|
||
WhatNow{tri: Transition::Flush, pre: i, len: 0, alt: None}
|
||
}
|
||
|
||
enum CommonStrCmdResult {
|
||
None,
|
||
Err(UnsupportedSyntax),
|
||
Ok(WhatNow),
|
||
OnlyWithQuotes(WhatNow),
|
||
OnlyWithoutQuotes(WhatNow),
|
||
}
|
||
|
||
fn common_str_cmd(
|
||
horizon: &[u8],
|
||
i: usize,
|
||
is_horizon_lengthenable: bool,
|
||
ctx_cmd: bool,
|
||
) -> CommonStrCmdResult {
|
||
if horizon[i] == b'`' {
|
||
let cmd = Box::new(SitCommand{
|
||
end_trigger: b'`' as u16,
|
||
end_replace: Some(b")")
|
||
});
|
||
return CommonStrCmdResult::OnlyWithQuotes(WhatNow{
|
||
tri: Transition::Push(cmd), pre: i, len: 1, alt: Some(b"$(")
|
||
});
|
||
}
|
||
if horizon[i] == b'\\' {
|
||
let esc = Box::new(SitExtent{len: 1, color: 0x01ff0080, end_insert: None});
|
||
return CommonStrCmdResult::Ok(WhatNow{
|
||
tri: Transition::Push(esc), pre: i, len: 1, alt: None
|
||
});
|
||
}
|
||
if horizon[i] != b'$' {
|
||
return CommonStrCmdResult::None;
|
||
}
|
||
if i+1 >= horizon.len() {
|
||
if is_horizon_lengthenable {
|
||
return CommonStrCmdResult::Ok(flush(i));
|
||
}
|
||
return CommonStrCmdResult::None;
|
||
}
|
||
let c = horizon[i+1];
|
||
if c == b'\'' {
|
||
if ctx_cmd {
|
||
return CommonStrCmdResult::OnlyWithoutQuotes(WhatNow {
|
||
tri: Transition::Push(Box::new(SitStrSqEsc{})),
|
||
pre: i, len: 2, alt: None
|
||
});
|
||
}
|
||
} else if c == b'(' {
|
||
let cand: &[u8] = &horizon[i+2 ..];
|
||
let (idlen, pos_hazard) = pos_tailhazard(cand, b')');
|
||
if pos_hazard == cand.len() {
|
||
if is_horizon_lengthenable {
|
||
return CommonStrCmdResult::Ok(flush(i));
|
||
}
|
||
} else if idlen == 3 && pos_hazard >= 4 && cand[.. 3].eq(b"pwd") {
|
||
let tailhazard = is_identifiertail(cand[pos_hazard]);
|
||
let replacement: &'static [u8] = if tailhazard {
|
||
b"${PWD}"
|
||
} else {
|
||
b"$PWD"
|
||
};
|
||
let sit = Box::new(SitExtent{
|
||
len: 0,
|
||
color: 0x000000ff,
|
||
end_insert: None,
|
||
});
|
||
return CommonStrCmdResult::OnlyWithQuotes(WhatNow{
|
||
tri: Transition::Push(sit),
|
||
pre: i, len: 6,
|
||
alt: Some(replacement)
|
||
});
|
||
} else if cand.len() >= 1 && cand[0] == b'(' {
|
||
let sit = Box::new(SitVec{
|
||
terminator: vec!{b')', b')'},
|
||
color: 0x00007fff,
|
||
});
|
||
return CommonStrCmdResult::Ok(WhatNow{
|
||
tri: Transition::Push(sit),
|
||
pre: i, len: 3,
|
||
alt: None
|
||
});
|
||
}
|
||
|
||
let cmd = Box::new(SitCommand{
|
||
end_trigger: b')' as u16,
|
||
end_replace: None
|
||
});
|
||
return CommonStrCmdResult::OnlyWithQuotes(WhatNow{
|
||
tri: Transition::Push(cmd),
|
||
pre: i, len: 2, alt: None
|
||
});
|
||
} else if c == b'#' || c == b'?' {
|
||
let ext = Box::new(SitExtent{
|
||
len: 2,
|
||
color: 0x000000ff,
|
||
end_insert: None
|
||
});
|
||
return CommonStrCmdResult::Ok(WhatNow{
|
||
tri: Transition::Push(ext),
|
||
pre: i, len: 0, alt: None
|
||
});
|
||
} else if c == b'*' {
|
||
let ext = Box::new(SitExtent{
|
||
len: 0,
|
||
color: 0x000000ff,
|
||
end_insert: None
|
||
});
|
||
return CommonStrCmdResult::OnlyWithQuotes(WhatNow{
|
||
tri: Transition::Push(ext),
|
||
pre: i, len: 2, alt: Some(b"$@")
|
||
});
|
||
} else if predlen(&|c|{c >= b'0' && c <= b'9'}, &horizon[i+1 ..]) > 1 {
|
||
return CommonStrCmdResult::Err(UnsupportedSyntax {
|
||
typ: "Unsuported syntax: Syntactic pitfall",
|
||
ctx: horizon.to_owned(),
|
||
pos: i+2,
|
||
msg: "This does not mean what it looks like. You may be forgiven to think that the full string of \
|
||
numerals is the variable name. Only the fist is.\n\
|
||
\n\
|
||
Try this and be shocked: f() { echo \"$9\" \"$10\"; }; f a b c d e f g h i j\n\
|
||
\n\
|
||
Here is where braces should be used to disambiguate, \
|
||
e.g. \"${10}\" vs \"${1}0\".\n\
|
||
\n\
|
||
Syntactic pitfalls are deemed too dangerous to fix automatically\n\
|
||
(the purpose of Naziquote is to fix brittle code – code that mostly \
|
||
does what it looks like, as opposed to code that never does what it looks like):\n\
|
||
* Fixing what it does would be 100% subtle \
|
||
and might slip through code review unnoticed.\n\
|
||
* Fixing its look would make a likely bug look intentional."
|
||
});
|
||
} else if c == b'@' || (c >= b'0' && c <= b'9') {
|
||
let ext = Box::new(SitExtent{
|
||
len: 2,
|
||
color: 0x000000ff,
|
||
end_insert: None
|
||
});
|
||
return CommonStrCmdResult::OnlyWithQuotes(WhatNow{
|
||
tri: Transition::Push(ext),
|
||
pre: i, len: 0, alt: None
|
||
});
|
||
} else if is_identifierhead(c) {
|
||
let tailhazard;
|
||
if ctx_cmd {
|
||
let cand: &[u8] = &horizon[i+1 ..];
|
||
let (_, pos_hazard) = pos_tailhazard(cand, b'\"');
|
||
if pos_hazard == cand.len() {
|
||
if is_horizon_lengthenable {
|
||
return CommonStrCmdResult::Ok(flush(i));
|
||
}
|
||
tailhazard = true;
|
||
} else {
|
||
tailhazard = is_identifiertail(cand[pos_hazard]);
|
||
}
|
||
} else {
|
||
tailhazard = false;
|
||
}
|
||
return CommonStrCmdResult::OnlyWithQuotes(WhatNow{
|
||
tri: Transition::Push(Box::new(SitVarIdent{
|
||
end_insert: if_needed(tailhazard, b"}")
|
||
})), pre: i, len: 1, alt: if_needed(tailhazard, b"${")
|
||
});
|
||
} else if c == b'{' {
|
||
let cand: &[u8] = &horizon[i+2 ..];
|
||
let (idlen, pos_hazard) = pos_tailhazard(cand, b'}');
|
||
let mut rm_braces = false;
|
||
let mut is_number = false;
|
||
if pos_hazard == cand.len() {
|
||
if is_horizon_lengthenable {
|
||
return CommonStrCmdResult::Ok(flush(i));
|
||
}
|
||
} else if idlen < pos_hazard {
|
||
rm_braces = !is_identifiertail(cand[pos_hazard]);
|
||
} else if idlen == 0 && (cand[0] == b'#' || cand[0] == b'?') {
|
||
is_number = true;
|
||
}
|
||
let wn = WhatNow{
|
||
tri: Transition::Push(Box::new(SitUntilByte{
|
||
until: b'}', color: 0x000000ff, end_replace: if_needed(rm_braces, b"")
|
||
})), pre: i, len: 2, alt: if_needed(rm_braces, b"$")
|
||
};
|
||
return if is_number {
|
||
CommonStrCmdResult::Ok(wn)
|
||
} else {
|
||
CommonStrCmdResult::OnlyWithQuotes(wn)
|
||
};
|
||
}
|
||
return CommonStrCmdResult::Ok(flush(i+1));
|
||
}
|
||
|
||
fn if_needed<T>(needed: bool, val: T) -> Option<T> {
|
||
return if needed { Some(val) } else { None };
|
||
}
|
||
|
||
struct SitExtent{
|
||
len : usize,
|
||
color: u32,
|
||
end_insert :Option<&'static [u8]>,
|
||
}
|
||
|
||
impl Situation for SitExtent {
|
||
#[allow(unused_variables)]
|
||
fn whatnow(&mut self, horizon: &[u8], is_horizon_lengthenable: bool) -> ParseResult {
|
||
if horizon.len() >= self.len {
|
||
return Ok(WhatNow{tri: Transition::Pop, pre: self.len, len: 0, alt: self.end_insert});
|
||
}
|
||
self.len -= horizon.len();
|
||
return Ok(flush(horizon.len()));
|
||
}
|
||
fn get_color(&self) -> u32{
|
||
self.color
|
||
}
|
||
}
|
||
|
||
struct SitUntilByte {
|
||
until: u8,
|
||
color: u32,
|
||
end_replace :Option<&'static [u8]>,
|
||
}
|
||
|
||
impl Situation for SitUntilByte {
|
||
#[allow(unused_variables)]
|
||
fn whatnow(&mut self, horizon: &[u8], is_horizon_lengthenable: bool) -> ParseResult {
|
||
let len = predlen(&|x| x != self.until, &horizon);
|
||
return Ok(if len < horizon.len() {
|
||
WhatNow{tri: Transition::Pop, pre: len, len: 1, alt: self.end_replace}
|
||
} else {
|
||
WhatNow{
|
||
tri: if is_controlcharacter(self.until) {
|
||
Transition::FlushPopOnEof
|
||
} else {
|
||
Transition::Flush
|
||
}, pre: len, len: 0, alt: None
|
||
}
|
||
});
|
||
}
|
||
fn get_color(&self) -> u32{
|
||
self.color
|
||
}
|
||
}
|
||
|
||
struct SitStrSqEsc {}
|
||
|
||
impl Situation for SitStrSqEsc {
|
||
#[allow(unused_variables)]
|
||
fn whatnow(&mut self, horizon: &[u8], is_horizon_lengthenable: bool) -> ParseResult {
|
||
for i in 0 .. horizon.len() {
|
||
if horizon[i] == b'\\' {
|
||
let esc = Box::new(SitExtent{len: 1, color: 0x01ff0080, end_insert: None});
|
||
return Ok(WhatNow{tri: Transition::Push(esc), pre: i, len: 1, alt: None});
|
||
}
|
||
if horizon[i] == b'\'' {
|
||
return Ok(WhatNow{tri: Transition::Pop, pre: i, len: 1, alt: None});
|
||
}
|
||
}
|
||
Ok(flush(horizon.len()))
|
||
}
|
||
fn get_color(&self) -> u32{
|
||
0x00ff8000
|
||
}
|
||
}
|
||
|
||
struct SitVarIdent {
|
||
end_insert: Option<&'static [u8]>,
|
||
}
|
||
|
||
impl Situation for SitVarIdent {
|
||
#[allow(unused_variables)]
|
||
fn whatnow(&mut self, horizon: &[u8], is_horizon_lengthenable: bool) -> ParseResult {
|
||
let len = predlen(&is_identifiertail, &horizon);
|
||
if len < horizon.len() {
|
||
return Ok(WhatNow{tri: Transition::Pop, pre: len, len: 0, alt: self.end_insert});
|
||
}
|
||
Ok(WhatNow{
|
||
tri: Transition::FlushPopOnEof,
|
||
pre: horizon.len(), len: 0, alt: self.end_insert
|
||
})
|
||
}
|
||
fn get_color(&self) -> u32{
|
||
0x000000ff
|
||
}
|
||
}
|
||
|
||
struct SitVec {
|
||
terminator :Vec<u8>,
|
||
color: u32,
|
||
}
|
||
|
||
impl Situation for SitVec {
|
||
fn whatnow(&mut self, horizon: &[u8], is_horizon_lengthenable: bool) -> ParseResult {
|
||
if horizon.len() < self.terminator.len() {
|
||
if is_horizon_lengthenable {
|
||
return Ok(flush(0));
|
||
}
|
||
}
|
||
else if &horizon[0 .. self.terminator.len()] == &self.terminator[..] {
|
||
return Ok(WhatNow{tri: Transition::Pop, pre: 0, len: self.terminator.len(), alt: None});
|
||
}
|
||
return Ok(flush(1));
|
||
}
|
||
fn get_color(&self) -> u32{
|
||
self.color
|
||
}
|
||
}
|
||
|
||
//------------------------------------------------------------------------------
|
||
|
||
fn pos_tailhazard(horizon: &[u8], end: u8) -> (usize, usize) {
|
||
let idlen = identifierlen(&horizon);
|
||
let mut pos = idlen;
|
||
if idlen < horizon.len() {
|
||
if horizon[pos] == end {
|
||
pos += 1;
|
||
if pos < horizon.len() {
|
||
pos += predlen(&|x| x == b'\"', &horizon[pos ..]);
|
||
}
|
||
}
|
||
}
|
||
return (idlen, pos);
|
||
}
|
||
|
||
fn identifierlen(horizon: &[u8]) -> usize {
|
||
return if horizon.len() > 0 && is_identifierhead(horizon[0]) {
|
||
1 + predlen(&is_identifiertail, &horizon[1 ..])
|
||
} else {
|
||
0
|
||
}
|
||
}
|
||
|
||
fn predlen(pred: &Fn(u8) -> bool, horizon: &[u8]) -> usize {
|
||
let mut i: usize = 0;
|
||
while i < horizon.len() && pred(horizon[i]) {
|
||
i += 1;
|
||
}
|
||
i
|
||
}
|
||
|
||
fn is_identifierhead(c: u8) -> bool {
|
||
if (c >= b'a' && c <= b'z')
|
||
|| (c >= b'A' && c <= b'Z')
|
||
|| (c == b'_')
|
||
{
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
fn is_identifiertail(c: u8) -> bool {
|
||
if (c >= b'a' && c <= b'z')
|
||
|| (c >= b'A' && c <= b'Z')
|
||
|| (c >= b'0' && c <= b'9')
|
||
|| (c == b'_')
|
||
{
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
fn is_controlcharacter(c: u8) -> bool {
|
||
return c <= b' ';
|
||
}
|
||
|
||
fn is_phantomstringfood(c: u8) -> bool {
|
||
c >= b'+'
|
||
&& c != b';' && c != b'<' && c != b'>'
|
||
&& c != b'\\' && c != b'`' && c != b'|'
|
||
}
|
||
|
||
fn find_heredoc(horizon: &[u8]) -> (usize, Vec<u8>) {
|
||
let mut ate = predlen(&|x| x == b'<', &horizon);
|
||
let mut found = Vec::<u8>::new();
|
||
if ate != 2 {
|
||
return (ate, found);
|
||
}
|
||
ate += predlen(&|x| x == b'-', &horizon[ate ..]);
|
||
ate += predlen(&is_controlcharacter, &horizon[ate ..]);
|
||
|
||
// Lex one word.
|
||
let herein = &horizon[ate ..];
|
||
found.reserve(herein.len());
|
||
|
||
#[derive(Clone)]
|
||
#[derive(Copy)]
|
||
enum DelimiterSyntax {
|
||
WORD,
|
||
WORDESC,
|
||
SQ,
|
||
DQ,
|
||
DQESC,
|
||
}
|
||
let mut state = DelimiterSyntax::WORD;
|
||
|
||
for byte_ref in herein {
|
||
let byte: u8 = *byte_ref;
|
||
state = match (state, byte) {
|
||
(DelimiterSyntax::WORD, b' ' ) => break,
|
||
(DelimiterSyntax::WORD, b'\n') => break,
|
||
(DelimiterSyntax::WORD, b'\t') => break,
|
||
(DelimiterSyntax::WORD, b'\\') => DelimiterSyntax::WORDESC,
|
||
(DelimiterSyntax::WORD, b'\'') => DelimiterSyntax::SQ,
|
||
(DelimiterSyntax::WORD, b'\"') => DelimiterSyntax::DQ,
|
||
(DelimiterSyntax::SQ, b'\'') => DelimiterSyntax::WORD,
|
||
(DelimiterSyntax::DQ, b'\"') => DelimiterSyntax::WORD,
|
||
(DelimiterSyntax::DQ, b'\\') => DelimiterSyntax::DQESC,
|
||
(DelimiterSyntax::WORDESC, b'\n') => DelimiterSyntax::WORD,
|
||
(DelimiterSyntax::WORDESC, _) => {
|
||
found.push(byte);
|
||
DelimiterSyntax::WORD
|
||
},
|
||
(DelimiterSyntax::DQESC, b'\n') => DelimiterSyntax::DQ,
|
||
(DelimiterSyntax::DQESC, _) => {
|
||
if byte != b'\"' && byte != b'\\' {
|
||
found.push(b'\\');
|
||
}
|
||
found.push(byte);
|
||
DelimiterSyntax::DQ
|
||
},
|
||
(_, _) => {
|
||
found.push(byte);
|
||
state
|
||
},
|
||
};
|
||
ate += 1;
|
||
}
|
||
return (ate, found);
|
||
}
|