2016-04-21 06:00:54 +00:00
|
|
|
// The fish_indent program.
|
2007-04-22 10:03:12 +00:00
|
|
|
/*
|
2014-12-23 23:29:42 +00:00
|
|
|
Copyright (C) 2014 ridiculous_fish
|
2007-04-22 10:03:12 +00:00
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License version 2 as
|
|
|
|
published by the Free Software Foundation.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with this program; if not, write to the Free Software
|
2013-12-13 20:51:52 +00:00
|
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
2007-04-22 10:03:12 +00:00
|
|
|
*/
|
2016-05-18 22:30:21 +00:00
|
|
|
#include "config.h" // IWYU pragma: keep
|
|
|
|
|
2016-05-18 15:37:13 +00:00
|
|
|
#include <errno.h>
|
2016-05-01 01:37:19 +00:00
|
|
|
#include <getopt.h>
|
2015-07-25 15:14:25 +00:00
|
|
|
#include <locale.h>
|
|
|
|
#include <stddef.h>
|
2016-05-01 01:37:19 +00:00
|
|
|
#include <stdio.h>
|
|
|
|
#include <stdlib.h>
|
2016-04-21 06:00:54 +00:00
|
|
|
#include <wctype.h>
|
2017-02-14 04:37:27 +00:00
|
|
|
|
2019-10-13 22:50:48 +00:00
|
|
|
#include <cstring>
|
2019-03-12 21:06:01 +00:00
|
|
|
#include <cwchar>
|
2016-05-01 01:37:19 +00:00
|
|
|
#include <memory>
|
2019-02-25 01:40:03 +00:00
|
|
|
#include <stack>
|
2019-05-05 10:09:25 +00:00
|
|
|
#include <string>
|
2019-02-25 01:40:03 +00:00
|
|
|
#include <tuple>
|
2019-05-05 10:09:25 +00:00
|
|
|
#include <vector>
|
2007-04-22 10:03:12 +00:00
|
|
|
|
2020-06-20 22:27:10 +00:00
|
|
|
#include "ast.h"
|
2015-07-25 15:14:25 +00:00
|
|
|
#include "color.h"
|
2014-12-23 23:29:42 +00:00
|
|
|
#include "common.h"
|
|
|
|
#include "env.h"
|
2019-06-11 17:35:49 +00:00
|
|
|
#include "expand.h"
|
2016-05-01 01:37:19 +00:00
|
|
|
#include "fish_version.h"
|
2020-01-19 12:38:47 +00:00
|
|
|
#include "flog.h"
|
2016-05-01 01:37:19 +00:00
|
|
|
#include "highlight.h"
|
2020-01-16 01:14:47 +00:00
|
|
|
#include "operation_context.h"
|
2016-05-01 01:37:19 +00:00
|
|
|
#include "output.h"
|
|
|
|
#include "parse_constants.h"
|
2020-06-28 21:48:16 +00:00
|
|
|
#include "parse_util.h"
|
2007-04-22 10:03:12 +00:00
|
|
|
#include "print_help.h"
|
2016-05-01 01:37:19 +00:00
|
|
|
#include "wutil.h" // IWYU pragma: keep
|
2007-04-22 10:03:12 +00:00
|
|
|
|
2020-04-11 17:44:17 +00:00
|
|
|
// The number of spaces per indent isn't supposed to be configurable.
|
|
|
|
// See discussion at https://github.com/fish-shell/fish-shell/pull/6790
|
2014-12-23 23:29:42 +00:00
|
|
|
#define SPACES_PER_INDENT 4
|
2007-04-22 10:03:12 +00:00
|
|
|
|
2016-04-05 22:43:24 +00:00
|
|
|
static bool dump_parse_tree = false;
|
2018-12-11 13:40:57 +00:00
|
|
|
static int ret = 0;
|
2014-12-23 23:29:42 +00:00
|
|
|
|
2016-04-05 22:43:24 +00:00
|
|
|
// Read the entire contents of a file into the specified string.
|
2016-05-01 01:37:19 +00:00
|
|
|
static wcstring read_file(FILE *f) {
|
2014-12-23 23:29:42 +00:00
|
|
|
wcstring result;
|
2019-11-26 00:36:13 +00:00
|
|
|
while (true) {
|
2019-03-12 21:06:01 +00:00
|
|
|
wint_t c = std::fgetwc(f);
|
2018-12-11 13:40:57 +00:00
|
|
|
|
2016-05-01 01:37:19 +00:00
|
|
|
if (c == WEOF) {
|
|
|
|
if (ferror(f)) {
|
2018-12-11 13:40:57 +00:00
|
|
|
if (errno == EILSEQ) {
|
|
|
|
// Illegal byte sequence. Try to skip past it.
|
|
|
|
clearerr(f);
|
2019-05-05 10:09:25 +00:00
|
|
|
int ch = fgetc(f); // for printing the warning, and seeks forward 1 byte.
|
2020-01-19 12:38:47 +00:00
|
|
|
FLOGF(warning, "%s (byte=%X)", std::strerror(errno), ch);
|
2018-12-11 13:40:57 +00:00
|
|
|
ret = 1;
|
|
|
|
continue;
|
|
|
|
} else {
|
|
|
|
wperror(L"fgetwc");
|
|
|
|
exit(1);
|
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2019-11-19 01:08:16 +00:00
|
|
|
result.push_back(static_cast<wchar_t>(c));
|
2012-11-18 10:23:22 +00:00
|
|
|
}
|
2014-12-23 23:29:42 +00:00
|
|
|
return result;
|
2007-04-22 10:03:12 +00:00
|
|
|
}
|
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
namespace {
|
|
|
|
/// From C++14.
|
|
|
|
template <bool B, typename T = void>
|
|
|
|
using enable_if_t = typename std::enable_if<B, T>::type;
|
|
|
|
|
|
|
|
/// \return the number of escaping backslashes before a character.
|
|
|
|
/// \p idx may be "one past the end."
|
|
|
|
size_t count_preceding_backslashes(const wcstring &text, size_t idx) {
|
|
|
|
assert(idx <= text.size() && "Out of bounds");
|
|
|
|
size_t backslashes = 0;
|
|
|
|
while (backslashes < idx && text.at(idx - backslashes - 1) == L'\\') {
|
|
|
|
backslashes++;
|
|
|
|
}
|
|
|
|
return backslashes;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// \return whether a character at a given index is escaped.
|
|
|
|
/// A character is escaped if it has an odd number of backslashes.
|
|
|
|
bool char_is_escaped(const wcstring &text, size_t idx) {
|
|
|
|
return count_preceding_backslashes(text, idx) % 2 == 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
using namespace ast;
|
|
|
|
struct pretty_printer_t {
|
|
|
|
// Note: this got somewhat more complicated after introducing the new AST, because that AST no
|
|
|
|
// longer encodes detailed lexical information (e.g. every newline). This feels more complex
|
|
|
|
// than necessary and would probably benefit from a more layered approach where we identify
|
|
|
|
// certain runs, weight line breaks, have a cost model, etc.
|
|
|
|
pretty_printer_t(const wcstring &src, bool do_indent)
|
|
|
|
: source(src),
|
|
|
|
indents(do_indent ? parse_util_compute_indents(source) : std::vector<int>(src.size(), 0)),
|
|
|
|
ast(ast_t::parse(src, parse_flags())),
|
|
|
|
do_indent(do_indent),
|
|
|
|
gaps(compute_gaps()),
|
|
|
|
preferred_semi_locations(compute_preferred_semi_locations()) {
|
|
|
|
assert(indents.size() == source.size() && "indents and source should be same length");
|
|
|
|
}
|
|
|
|
|
2018-05-07 22:52:04 +00:00
|
|
|
// Original source.
|
|
|
|
const wcstring &source;
|
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
// The indents of our string.
|
|
|
|
// This has the same length as 'source' and describes the indentation level.
|
|
|
|
const std::vector<int> indents;
|
|
|
|
|
|
|
|
// The parsed ast.
|
|
|
|
const ast_t ast;
|
|
|
|
|
2018-05-07 22:52:04 +00:00
|
|
|
// The prettifier output.
|
|
|
|
wcstring output;
|
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
// The indent of the source range which we are currently emitting.
|
|
|
|
int current_indent{0};
|
|
|
|
|
2018-05-07 22:52:04 +00:00
|
|
|
// Whether to indent, or just insert spaces.
|
|
|
|
const bool do_indent;
|
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
// Whether the next gap text should hide the first newline.
|
|
|
|
bool gap_text_mask_newline{false};
|
|
|
|
|
|
|
|
// The "gaps": a sorted set of ranges between tokens.
|
|
|
|
// These contain whitespace, comments, semicolons, and other lexical elements which are not
|
|
|
|
// present in the ast.
|
|
|
|
const std::vector<source_range_t> gaps;
|
|
|
|
|
|
|
|
// The sorted set of source offsets of nl_semi_t which should be set as semis, not newlines.
|
|
|
|
// This is computed ahead of time for convenience.
|
|
|
|
const std::vector<uint32_t> preferred_semi_locations;
|
|
|
|
|
|
|
|
// Flags we support.
|
|
|
|
using gap_flags_t = uint32_t;
|
|
|
|
enum {
|
|
|
|
default_flags = 0,
|
|
|
|
|
|
|
|
// Whether to allow line splitting via escaped newlines.
|
|
|
|
// For example, in argument lists:
|
|
|
|
//
|
|
|
|
// echo a \
|
|
|
|
// b
|
|
|
|
//
|
|
|
|
// If this is not set, then split-lines will be joined.
|
|
|
|
allow_escaped_newlines = 1 << 0,
|
|
|
|
|
|
|
|
// Whether to require a space before this token.
|
|
|
|
// This is used when emitting semis:
|
|
|
|
// echo a; echo b;
|
|
|
|
// No space required between 'a' and ';', or 'b' and ';'.
|
|
|
|
skip_space = 1 << 1,
|
|
|
|
};
|
2018-05-07 22:52:04 +00:00
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
// \return gap text flags for the gap text that comes *before* a given node type.
|
|
|
|
static gap_flags_t gap_text_flags_before_node(const node_t &node) {
|
|
|
|
gap_flags_t result = default_flags;
|
|
|
|
switch (node.type) {
|
|
|
|
// Allow escaped newlines in argument and redirection lists.
|
|
|
|
case type_t::argument:
|
|
|
|
case type_t::redirection:
|
|
|
|
result |= allow_escaped_newlines;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case type_t::token_base:
|
|
|
|
// Allow escaped newlines before && and ||, and also pipes.
|
|
|
|
switch (node.as<token_base_t>()->type) {
|
2020-07-08 18:12:15 +00:00
|
|
|
case parse_token_type_t::andand:
|
|
|
|
case parse_token_type_t::oror:
|
|
|
|
case parse_token_type_t::pipe:
|
2020-06-28 21:48:16 +00:00
|
|
|
result |= allow_escaped_newlines;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
break;
|
2019-05-03 17:00:55 +00:00
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
2018-05-07 22:52:04 +00:00
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
// \return whether we are at the start of a new line.
|
|
|
|
bool at_line_start() const { return output.empty() || output.back() == L'\n'; }
|
|
|
|
|
|
|
|
// \return whether we have a space before the output.
|
|
|
|
// This ignores escaped spaces and escaped newlines.
|
|
|
|
bool has_preceding_space() const {
|
|
|
|
long idx = static_cast<long>(output.size()) - 1;
|
|
|
|
// Skip escaped newlines.
|
|
|
|
// This is historical. Example:
|
|
|
|
//
|
|
|
|
// cmd1 \
|
|
|
|
// | cmd2
|
|
|
|
//
|
|
|
|
// we want the pipe to "see" the space after cmd1.
|
|
|
|
// TODO: this is too tricky, we should factor this better.
|
|
|
|
while (idx >= 0 && output.at(idx) == L'\n') {
|
|
|
|
size_t backslashes = count_preceding_backslashes(source, idx);
|
|
|
|
if (backslashes % 2 == 0) {
|
|
|
|
// Not escaped.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
idx -= (1 + backslashes);
|
|
|
|
}
|
|
|
|
return idx >= 0 && output.at(idx) == L' ' && !char_is_escaped(output, idx);
|
|
|
|
}
|
2018-05-07 22:52:04 +00:00
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
// Entry point. Prettify our source code and return it.
|
|
|
|
wcstring prettify() {
|
|
|
|
output = wcstring{};
|
|
|
|
node_visitor(*this).accept(ast.top());
|
2018-05-07 22:52:04 +00:00
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
// Trailing gap text.
|
|
|
|
emit_gap_text_before(source_range_t{(uint32_t)source.size(), 0}, default_flags);
|
2018-05-07 22:52:04 +00:00
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
// Replace all trailing newlines with just a single one.
|
|
|
|
while (!output.empty() && at_line_start()) {
|
|
|
|
output.pop_back();
|
2018-05-07 22:52:04 +00:00
|
|
|
}
|
2020-06-28 21:48:16 +00:00
|
|
|
emit_newline();
|
2018-05-07 22:52:04 +00:00
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
wcstring result = std::move(output);
|
|
|
|
return result;
|
2018-05-07 22:52:04 +00:00
|
|
|
}
|
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
// \return a substring of source.
|
|
|
|
wcstring substr(source_range_t r) const { return source.substr(r.start, r.length); }
|
|
|
|
|
|
|
|
// Return the gap ranges from our ast.
|
|
|
|
std::vector<source_range_t> compute_gaps() const {
|
|
|
|
auto range_compare = [](source_range_t r1, source_range_t r2) {
|
|
|
|
if (r1.start != r2.start) return r1.start < r2.start;
|
|
|
|
return r1.length < r2.length;
|
|
|
|
};
|
|
|
|
// Collect the token ranges into a list.
|
|
|
|
std::vector<source_range_t> tok_ranges;
|
|
|
|
for (const node_t &node : ast) {
|
|
|
|
if (node.category == category_t::leaf) {
|
|
|
|
auto r = node.source_range();
|
|
|
|
if (r.length > 0) tok_ranges.push_back(r);
|
|
|
|
}
|
2018-05-07 22:52:04 +00:00
|
|
|
}
|
2020-06-28 21:48:16 +00:00
|
|
|
// Place a zero length range at end to aid in our inverting.
|
|
|
|
tok_ranges.push_back(source_range_t{(uint32_t)source.size(), 0});
|
|
|
|
|
|
|
|
// Our tokens should be sorted.
|
|
|
|
assert(std::is_sorted(tok_ranges.begin(), tok_ranges.end(), range_compare));
|
|
|
|
|
|
|
|
// For each range, add a gap range between the previous range and this range.
|
|
|
|
std::vector<source_range_t> gaps;
|
|
|
|
uint32_t prev_end = 0;
|
|
|
|
for (source_range_t tok_range : tok_ranges) {
|
|
|
|
assert(tok_range.start >= prev_end &&
|
|
|
|
"Token range should not overlap or be out of order");
|
|
|
|
if (tok_range.start >= prev_end) {
|
|
|
|
gaps.push_back(source_range_t{prev_end, tok_range.start - prev_end});
|
|
|
|
}
|
|
|
|
prev_end = tok_range.start + tok_range.length;
|
2018-05-07 22:52:04 +00:00
|
|
|
}
|
2020-06-28 21:48:16 +00:00
|
|
|
return gaps;
|
2018-05-07 22:52:04 +00:00
|
|
|
}
|
2007-04-22 10:03:12 +00:00
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
// Return sorted list of semi-preferring semi_nl nodes.
|
|
|
|
std::vector<uint32_t> compute_preferred_semi_locations() const {
|
|
|
|
std::vector<uint32_t> result;
|
|
|
|
auto mark_as_semi = [&result](const optional_t<semi_nl_t> &n) {
|
|
|
|
if (n && n->has_source()) result.push_back(n->range.start);
|
|
|
|
};
|
|
|
|
|
|
|
|
// andor_job_lists get semis if they are short enough.
|
|
|
|
for (const auto &node : ast) {
|
|
|
|
// See if we have a condition and an andor_job_list.
|
|
|
|
const optional_t<semi_nl_t> *condition = nullptr;
|
|
|
|
const andor_job_list_t *andors = nullptr;
|
|
|
|
if (const auto *ifc = node.try_as<if_clause_t>()) {
|
|
|
|
condition = &ifc->condition.semi_nl;
|
|
|
|
andors = &ifc->andor_tail;
|
|
|
|
} else if (const auto *wc = node.try_as<while_header_t>()) {
|
|
|
|
condition = &wc->condition.semi_nl;
|
|
|
|
andors = &wc->andor_tail;
|
|
|
|
}
|
|
|
|
|
|
|
|
// This describes the heuristic of when to place and_or job lists on separate lines.
|
|
|
|
// That is, do we want:
|
|
|
|
// if true; and false
|
|
|
|
// or do we want:
|
|
|
|
// if true
|
|
|
|
// and false
|
|
|
|
// Lists with two or fewer get semis.
|
|
|
|
// Note the effective count is then three, because this list does not include the main
|
|
|
|
// condition.
|
|
|
|
if (andors && andors->count() > 0 && andors->count() <= 2) {
|
|
|
|
if (condition) mark_as_semi(*condition);
|
|
|
|
// Mark all but last of the andor list.
|
|
|
|
for (uint32_t i = 0; i + 1 < andors->count(); i++) {
|
|
|
|
mark_as_semi(andors->at(i)->job.semi_nl);
|
|
|
|
}
|
|
|
|
}
|
2016-10-09 21:43:25 +00:00
|
|
|
}
|
2016-04-05 22:43:24 +00:00
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
// `x ; and y` gets semis if it has them already, and they are on the same line.
|
|
|
|
for (const auto &node : ast) {
|
|
|
|
if (const auto *job_list = node.try_as<job_list_t>()) {
|
|
|
|
const semi_nl_t *prev_job_semi_nl = nullptr;
|
|
|
|
for (const job_conjunction_t &job : *job_list) {
|
|
|
|
// Set up prev_job_semi_nl for the next iteration to make control flow easier.
|
|
|
|
const semi_nl_t *prev = prev_job_semi_nl;
|
|
|
|
prev_job_semi_nl = job.semi_nl.contents.get();
|
|
|
|
|
|
|
|
// Is this an 'and' or 'or' job?
|
|
|
|
if (!job.decorator) continue;
|
|
|
|
|
|
|
|
// Now see if we want to mark 'prev' as allowing a semi.
|
|
|
|
// Did we have a previous semi_nl which was a newline?
|
|
|
|
if (!prev || substr(prev->range) != L";") continue;
|
|
|
|
|
|
|
|
// Is there a newline between them?
|
|
|
|
assert(prev->range.start <= job.decorator->range.start &&
|
|
|
|
"Ranges out of order");
|
|
|
|
auto start = source.begin() + prev->range.start;
|
|
|
|
auto end = source.begin() + job.decorator->range.end();
|
|
|
|
if (std::find(start, end, L'\n') == end) {
|
|
|
|
// We're going to allow the previous semi_nl to be a semi.
|
|
|
|
result.push_back(prev->range.start);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-02-25 01:40:03 +00:00
|
|
|
}
|
2020-06-28 21:48:16 +00:00
|
|
|
std::sort(result.begin(), result.end());
|
|
|
|
return result;
|
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
// Emit a space or indent as necessary, depending on the previous output.
|
|
|
|
void emit_space_or_indent(gap_flags_t flags = default_flags) {
|
|
|
|
if (at_line_start()) {
|
|
|
|
output.append(SPACES_PER_INDENT * current_indent, L' ');
|
|
|
|
} else if (!(flags & skip_space) && !has_preceding_space()) {
|
|
|
|
output.append(1, L' ');
|
2020-03-15 14:25:34 +00:00
|
|
|
}
|
2020-06-28 21:48:16 +00:00
|
|
|
}
|
2018-05-07 22:52:04 +00:00
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
// Emit "gap text:" newlines and comments from the original source.
|
|
|
|
// Gap text may be a few things:
|
|
|
|
//
|
|
|
|
// 1. Just a space is common. We will trim the spaces to be empty.
|
|
|
|
//
|
|
|
|
// Here the gap text is the comment, followed by the newline:
|
|
|
|
//
|
|
|
|
// echo abc # arg
|
|
|
|
// echo def
|
|
|
|
//
|
|
|
|
// 2. It may also be an escaped newline:
|
|
|
|
// Here the gap text is a space, backslash, newline, space.
|
|
|
|
//
|
|
|
|
// echo \
|
|
|
|
// hi
|
|
|
|
//
|
|
|
|
// 3. Lastly it may be an error, if there was an error token. Here the gap text is the pipe:
|
|
|
|
//
|
|
|
|
// begin | stuff
|
|
|
|
//
|
|
|
|
// We do not handle errors here - instead our caller does.
|
2020-08-09 19:14:00 +00:00
|
|
|
bool emit_gap_text(const wcstring &gap_text, gap_flags_t flags) {
|
2020-06-28 21:48:16 +00:00
|
|
|
// Common case: if we are only spaces, do nothing.
|
2020-08-09 19:14:00 +00:00
|
|
|
if (gap_text.find_first_not_of(L' ') == wcstring::npos) return false;
|
2020-06-28 21:48:16 +00:00
|
|
|
|
|
|
|
// Look to see if there is an escaped newline.
|
|
|
|
// Emit it if either we allow it, or it comes before the first comment.
|
|
|
|
// Note we do not have to be concerned with escaped backslashes or escaped #s. This is gap
|
|
|
|
// text - we already know it has no semantic significance.
|
|
|
|
size_t escaped_nl = gap_text.find(L"\\\n");
|
2020-08-09 21:37:09 +00:00
|
|
|
bool have_line_continuation = false;
|
2020-06-28 21:48:16 +00:00
|
|
|
if (escaped_nl != wcstring::npos) {
|
|
|
|
size_t comment_idx = gap_text.find(L'#');
|
|
|
|
if ((flags & allow_escaped_newlines) ||
|
|
|
|
(comment_idx != wcstring::npos && escaped_nl < comment_idx)) {
|
|
|
|
// Emit a space before the escaped newline.
|
|
|
|
if (!at_line_start() && !has_preceding_space()) {
|
|
|
|
output.append(L" ");
|
|
|
|
}
|
|
|
|
output.append(L"\\\n");
|
2020-08-09 21:37:09 +00:00
|
|
|
// Indent the line continuation and any comment before it (#7252).
|
|
|
|
have_line_continuation = true;
|
2020-08-09 01:29:50 +00:00
|
|
|
current_indent += 1;
|
|
|
|
emit_space_or_indent();
|
2019-02-25 01:40:03 +00:00
|
|
|
}
|
2014-12-23 23:29:42 +00:00
|
|
|
}
|
2012-11-18 10:23:22 +00:00
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
// It seems somewhat ambiguous whether we always get a newline after a comment. Ensure we
|
|
|
|
// always emit one.
|
|
|
|
bool needs_nl = false;
|
|
|
|
|
|
|
|
tokenizer_t tokenizer(gap_text.c_str(), TOK_SHOW_COMMENTS | TOK_SHOW_BLANK_LINES);
|
|
|
|
while (maybe_t<tok_t> tok = tokenizer.next()) {
|
|
|
|
wcstring tok_text = tokenizer.text_of(*tok);
|
|
|
|
|
|
|
|
if (needs_nl) {
|
|
|
|
emit_newline();
|
|
|
|
needs_nl = false;
|
|
|
|
if (tok_text == L"\n") continue;
|
|
|
|
} else if (gap_text_mask_newline) {
|
|
|
|
// We only respect mask_newline the first time through the loop.
|
|
|
|
gap_text_mask_newline = false;
|
|
|
|
if (tok_text == L"\n") continue;
|
2019-05-03 17:00:55 +00:00
|
|
|
}
|
2020-06-28 21:48:16 +00:00
|
|
|
|
|
|
|
if (tok->type == token_type_t::comment) {
|
|
|
|
emit_space_or_indent();
|
|
|
|
output.append(tok_text);
|
|
|
|
needs_nl = true;
|
|
|
|
} else if (tok->type == token_type_t::end) {
|
|
|
|
// This may be either a newline or semicolon.
|
|
|
|
// Semicolons found here are not part of the ast and can simply be removed.
|
|
|
|
// Newlines are preserved unless mask_newline is set.
|
|
|
|
if (tok_text == L"\n") {
|
|
|
|
emit_newline();
|
2019-05-05 10:51:49 +00:00
|
|
|
}
|
2020-06-28 21:48:16 +00:00
|
|
|
} else {
|
|
|
|
fprintf(stderr,
|
|
|
|
"Gap text should only have comments and newlines - instead found token "
|
|
|
|
"type %d with text: %ls\n",
|
|
|
|
(int)tok->type, tok_text.c_str());
|
|
|
|
DIE("Gap text should only have comments and newlines");
|
2019-05-03 17:00:55 +00:00
|
|
|
}
|
2020-06-28 21:48:16 +00:00
|
|
|
}
|
|
|
|
if (needs_nl) emit_newline();
|
2020-08-09 21:37:09 +00:00
|
|
|
if (have_line_continuation) {
|
|
|
|
emit_space_or_indent();
|
|
|
|
current_indent -= 1;
|
|
|
|
}
|
2020-08-09 19:14:00 +00:00
|
|
|
return needs_nl;
|
2020-06-28 21:48:16 +00:00
|
|
|
}
|
2019-05-05 10:51:49 +00:00
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
/// \return the gap text ending at a given index into the string, or empty if none.
|
|
|
|
source_range_t gap_text_to(uint32_t end) const {
|
|
|
|
auto where = std::lower_bound(
|
|
|
|
gaps.begin(), gaps.end(), end,
|
|
|
|
[](source_range_t r, uint32_t end) { return r.start + r.length < end; });
|
|
|
|
if (where == gaps.end() || where->start + where->length != end) {
|
|
|
|
// Not found.
|
|
|
|
return source_range_t{0, 0};
|
|
|
|
} else {
|
|
|
|
return *where;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// \return whether a range \p r overlaps an error range from our ast.
|
|
|
|
bool range_contained_error(source_range_t r) const {
|
|
|
|
const auto &errs = ast.extras().errors;
|
|
|
|
auto range_is_before = [](source_range_t x, source_range_t y) {
|
|
|
|
return x.start + x.length <= y.start;
|
|
|
|
};
|
|
|
|
assert(std::is_sorted(errs.begin(), errs.end(), range_is_before) &&
|
|
|
|
"Error ranges should be sorted");
|
|
|
|
return std::binary_search(errs.begin(), errs.end(), r, range_is_before);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Emit the gap text before a source range.
|
2020-08-09 19:14:00 +00:00
|
|
|
bool emit_gap_text_before(source_range_t r, gap_flags_t flags) {
|
2020-06-28 21:48:16 +00:00
|
|
|
assert(r.start <= source.size() && "source out of bounds");
|
2020-08-09 19:14:00 +00:00
|
|
|
bool added_newline = false;
|
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
// Find the gap text which ends at start.
|
2020-08-08 23:16:02 +00:00
|
|
|
source_range_t range = gap_text_to(r.start);
|
2020-06-28 21:48:16 +00:00
|
|
|
if (range.length > 0) {
|
2020-08-08 23:16:02 +00:00
|
|
|
// Set the indent from the beginning of this gap text.
|
|
|
|
// For example:
|
|
|
|
// begin
|
|
|
|
// cmd
|
|
|
|
// # comment
|
|
|
|
// end
|
|
|
|
// Here the comment is the gap text before the end, but we want the indent from the
|
|
|
|
// command.
|
|
|
|
if (range.start < indents.size()) current_indent = indents.at(range.start);
|
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
// If this range contained an error, append the gap text without modification.
|
|
|
|
// For example in: echo foo "
|
|
|
|
// We don't want to mess with the quote.
|
|
|
|
if (range_contained_error(range)) {
|
|
|
|
output.append(substr(range));
|
|
|
|
} else {
|
2020-08-09 19:14:00 +00:00
|
|
|
added_newline = emit_gap_text(substr(range), flags);
|
2016-04-05 22:43:24 +00:00
|
|
|
}
|
2014-12-23 23:29:42 +00:00
|
|
|
}
|
2020-06-28 21:48:16 +00:00
|
|
|
// Always clear gap_text_mask_newline after emitting even empty gap text.
|
|
|
|
gap_text_mask_newline = false;
|
2020-08-09 19:14:00 +00:00
|
|
|
return added_newline;
|
2020-06-28 21:48:16 +00:00
|
|
|
}
|
2012-11-19 08:31:03 +00:00
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
/// Given a string \p input, remove unnecessary quotes, etc.
|
|
|
|
wcstring clean_text(const wcstring &input) {
|
|
|
|
// Unescape the string - this leaves special markers around if there are any
|
|
|
|
// expansions or anything. We specifically tell it to not compute backslash-escapes
|
|
|
|
// like \U or \x, because we want to leave them intact.
|
|
|
|
wcstring unescaped = input;
|
|
|
|
unescape_string_in_place(&unescaped, UNESCAPE_SPECIAL | UNESCAPE_NO_BACKSLASHES);
|
|
|
|
|
|
|
|
// Remove INTERNAL_SEPARATOR because that's a quote.
|
|
|
|
auto quote = [](wchar_t ch) { return ch == INTERNAL_SEPARATOR; };
|
|
|
|
unescaped.erase(std::remove_if(unescaped.begin(), unescaped.end(), quote), unescaped.end());
|
|
|
|
|
|
|
|
// If no non-"good" char is left, use the unescaped version.
|
|
|
|
// This can be extended to other characters, but giving the precise list is tough,
|
|
|
|
// can change over time (see "^", "%" and "?", in some cases "{}") and it just makes
|
|
|
|
// people feel more at ease.
|
|
|
|
auto goodchars = [](wchar_t ch) {
|
|
|
|
return fish_iswalnum(ch) || ch == L'_' || ch == L'-' || ch == L'/';
|
|
|
|
};
|
|
|
|
if (std::find_if_not(unescaped.begin(), unescaped.end(), goodchars) == unescaped.end() &&
|
|
|
|
!unescaped.empty()) {
|
|
|
|
return unescaped;
|
|
|
|
} else {
|
|
|
|
return input;
|
2019-02-25 01:40:03 +00:00
|
|
|
}
|
2014-12-23 23:29:42 +00:00
|
|
|
}
|
2020-06-28 21:48:16 +00:00
|
|
|
|
|
|
|
// Emit a range of original text. This indents as needed, and also inserts preceding gap text.
|
|
|
|
// If \p tolerate_line_splitting is set, then permit escaped newlines; otherwise collapse such
|
|
|
|
// lines.
|
|
|
|
void emit_text(source_range_t r, gap_flags_t flags) {
|
|
|
|
emit_gap_text_before(r, flags);
|
|
|
|
current_indent = indents.at(r.start);
|
|
|
|
if (r.length > 0) {
|
|
|
|
emit_space_or_indent(flags);
|
|
|
|
output.append(clean_text(substr(r)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
template <type_t Type>
|
|
|
|
void emit_node_text(const leaf_t<Type> &node) {
|
|
|
|
emit_text(node.range, gap_text_flags_before_node(node));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Emit one newline.
|
|
|
|
void emit_newline() { output.push_back(L'\n'); }
|
|
|
|
|
|
|
|
// Emit a semicolon.
|
|
|
|
void emit_semi() { output.push_back(L';'); }
|
|
|
|
|
|
|
|
// For branch and list nodes, default is to visit their children.
|
|
|
|
template <typename Node>
|
|
|
|
enable_if_t<Node::Category == category_t::branch> visit(const Node &node) {
|
|
|
|
node_visitor(*this).accept_children_of(node);
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename Node>
|
|
|
|
enable_if_t<Node::Category == ast::category_t::list> visit(const Node &node) {
|
|
|
|
node_visitor(*this).accept_children_of(node);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Leaf nodes we just visit their text.
|
|
|
|
void visit(const keyword_base_t &node) { emit_node_text(node); }
|
|
|
|
void visit(const token_base_t &node) { emit_node_text(node); }
|
|
|
|
void visit(const argument_t &node) { emit_node_text(node); }
|
|
|
|
void visit(const variable_assignment_t &node) { emit_node_text(node); }
|
|
|
|
|
|
|
|
void visit(const semi_nl_t &node) {
|
|
|
|
// These are semicolons or newlines which are part of the ast. That means it includes e.g.
|
|
|
|
// ones terminating a job or 'if' header, but not random semis in job lists. We respect
|
|
|
|
// preferred_semi_locations to decide whether or not these should stay as newlines or
|
|
|
|
// become semicolons.
|
|
|
|
|
|
|
|
// Check if we should prefer a semicolon.
|
|
|
|
bool prefer_semi = node.range.length > 0 &&
|
|
|
|
std::binary_search(preferred_semi_locations.begin(),
|
|
|
|
preferred_semi_locations.end(), node.range.start);
|
|
|
|
emit_gap_text_before(node.range, gap_text_flags_before_node(node));
|
|
|
|
|
|
|
|
// Don't emit anything if the gap text put us on a newline (because it had a comment).
|
|
|
|
if (!at_line_start()) {
|
|
|
|
prefer_semi ? emit_semi() : emit_newline();
|
|
|
|
|
|
|
|
// If it was a semi but we emitted a newline, swallow a subsequent newline.
|
|
|
|
if (!prefer_semi && substr(node.range) == L";") {
|
|
|
|
gap_text_mask_newline = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void visit(const redirection_t &node) {
|
|
|
|
// No space between a redirection operator and its target (#2899).
|
|
|
|
emit_text(node.oper.range, default_flags);
|
|
|
|
emit_text(node.target.range, skip_space);
|
|
|
|
}
|
|
|
|
|
|
|
|
void visit(const maybe_newlines_t &node) {
|
|
|
|
// Our newlines may have comments embedded in them, example:
|
|
|
|
// cmd |
|
|
|
|
// # something
|
|
|
|
// cmd2
|
|
|
|
// Treat it as gap text.
|
|
|
|
if (node.range.length > 0) {
|
|
|
|
auto flags = gap_text_flags_before_node(node);
|
|
|
|
current_indent = indents.at(node.range.start);
|
2020-08-09 19:14:00 +00:00
|
|
|
bool added_newline = emit_gap_text_before(node.range, flags);
|
2020-06-28 21:48:16 +00:00
|
|
|
wcstring text = source.substr(node.range.start, node.range.length);
|
2020-08-09 19:14:00 +00:00
|
|
|
if (added_newline && !text.empty() && text.front() == L'\n') {
|
|
|
|
text = text.substr(strlen("\n"));
|
|
|
|
}
|
2020-06-28 21:48:16 +00:00
|
|
|
emit_gap_text(text, flags);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void visit(const begin_header_t &node) {
|
|
|
|
// 'begin' does not require a newline after it, but we insert one.
|
|
|
|
node_visitor(*this).accept_children_of(node);
|
|
|
|
if (!at_line_start()) {
|
|
|
|
emit_newline();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// The flags we use to parse.
|
|
|
|
static parse_tree_flags_t parse_flags() {
|
|
|
|
return parse_flag_continue_after_error | parse_flag_include_comments |
|
|
|
|
parse_flag_leave_unterminated | parse_flag_show_blank_lines;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
} // namespace
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2019-03-13 01:05:56 +00:00
|
|
|
static const char *highlight_role_to_string(highlight_role_t role) {
|
|
|
|
#define TEST_ROLE(x) \
|
|
|
|
case highlight_role_t::x: \
|
|
|
|
return #x;
|
|
|
|
switch (role) {
|
|
|
|
TEST_ROLE(normal)
|
|
|
|
TEST_ROLE(error)
|
|
|
|
TEST_ROLE(command)
|
|
|
|
TEST_ROLE(statement_terminator)
|
|
|
|
TEST_ROLE(param)
|
|
|
|
TEST_ROLE(comment)
|
|
|
|
TEST_ROLE(search_match)
|
|
|
|
TEST_ROLE(operat)
|
|
|
|
TEST_ROLE(escape)
|
|
|
|
TEST_ROLE(quote)
|
|
|
|
TEST_ROLE(redirection)
|
|
|
|
TEST_ROLE(autosuggestion)
|
|
|
|
TEST_ROLE(selection)
|
|
|
|
TEST_ROLE(pager_progress)
|
|
|
|
TEST_ROLE(pager_background)
|
|
|
|
TEST_ROLE(pager_prefix)
|
|
|
|
TEST_ROLE(pager_completion)
|
|
|
|
TEST_ROLE(pager_description)
|
|
|
|
TEST_ROLE(pager_secondary_background)
|
|
|
|
TEST_ROLE(pager_secondary_prefix)
|
|
|
|
TEST_ROLE(pager_secondary_completion)
|
|
|
|
TEST_ROLE(pager_secondary_description)
|
|
|
|
TEST_ROLE(pager_selected_background)
|
|
|
|
TEST_ROLE(pager_selected_prefix)
|
|
|
|
TEST_ROLE(pager_selected_completion)
|
|
|
|
TEST_ROLE(pager_selected_description)
|
2019-04-12 13:38:38 +00:00
|
|
|
default:
|
|
|
|
DIE("UNKNOWN ROLE");
|
2019-03-13 01:05:56 +00:00
|
|
|
}
|
|
|
|
#undef TEST_ROLE
|
|
|
|
}
|
|
|
|
|
|
|
|
// Entry point for Pygments CSV output.
|
|
|
|
// Our output is a newline-separated string.
|
|
|
|
// Each line is of the form `start,end,role`
|
|
|
|
// start and end is the half-open token range, value is a string from highlight_role_t.
|
|
|
|
// Example:
|
|
|
|
// 3,7,command
|
|
|
|
static std::string make_pygments_csv(const wcstring &src) {
|
|
|
|
const size_t len = src.size();
|
|
|
|
std::vector<highlight_spec_t> colors;
|
2020-08-03 21:10:37 +00:00
|
|
|
highlight_shell(src, colors, src.size(), operation_context_t::globals());
|
2019-03-13 01:05:56 +00:00
|
|
|
assert(colors.size() == len && "Colors and src should have same size");
|
|
|
|
|
|
|
|
struct token_range_t {
|
|
|
|
unsigned long start;
|
|
|
|
unsigned long end;
|
|
|
|
highlight_role_t role;
|
|
|
|
};
|
|
|
|
|
|
|
|
std::vector<token_range_t> token_ranges;
|
|
|
|
for (size_t i = 0; i < len; i++) {
|
|
|
|
highlight_role_t role = colors.at(i).foreground;
|
|
|
|
// See if we can extend the last range.
|
|
|
|
if (!token_ranges.empty()) {
|
|
|
|
auto &last = token_ranges.back();
|
|
|
|
if (last.role == role && last.end == i) {
|
|
|
|
last.end = i + 1;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// We need a new range.
|
|
|
|
token_ranges.push_back(token_range_t{i, i + 1, role});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now render these to a string.
|
|
|
|
std::string result;
|
|
|
|
for (const auto &range : token_ranges) {
|
|
|
|
char buff[128];
|
|
|
|
snprintf(buff, sizeof buff, "%lu,%lu,%s\n", range.start, range.end,
|
|
|
|
highlight_role_to_string(range.role));
|
|
|
|
result.append(buff);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2016-05-01 01:37:19 +00:00
|
|
|
// Entry point for prettification.
|
|
|
|
static wcstring prettify(const wcstring &src, bool do_indent) {
|
2017-06-13 00:59:44 +00:00
|
|
|
if (dump_parse_tree) {
|
2020-06-20 22:27:10 +00:00
|
|
|
auto ast =
|
|
|
|
ast::ast_t::parse(src, parse_flag_leave_unterminated | parse_flag_include_comments |
|
|
|
|
parse_flag_show_extra_semis);
|
|
|
|
wcstring ast_dump = ast.dump(src);
|
|
|
|
std::fwprintf(stderr, L"%ls\n", ast_dump.c_str());
|
2014-12-23 23:29:42 +00:00
|
|
|
}
|
2012-11-19 08:31:03 +00:00
|
|
|
|
2020-06-28 21:48:16 +00:00
|
|
|
pretty_printer_t printer{src, do_indent};
|
|
|
|
wcstring output = printer.prettify();
|
|
|
|
return output;
|
2014-12-23 23:29:42 +00:00
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
|
2016-05-01 01:37:19 +00:00
|
|
|
/// Given a string and list of colors of the same size, return the string with HTML span elements
|
|
|
|
/// for the various colors.
|
|
|
|
static const wchar_t *html_class_name_for_color(highlight_spec_t spec) {
|
|
|
|
#define P(x) L"fish_color_" #x
|
2019-03-04 01:34:00 +00:00
|
|
|
switch (spec.foreground) {
|
|
|
|
case highlight_role_t::normal: {
|
2016-05-01 01:37:19 +00:00
|
|
|
return P(normal);
|
|
|
|
}
|
2019-03-04 01:34:00 +00:00
|
|
|
case highlight_role_t::error: {
|
2016-05-01 01:37:19 +00:00
|
|
|
return P(error);
|
|
|
|
}
|
2019-03-04 01:34:00 +00:00
|
|
|
case highlight_role_t::command: {
|
2016-05-01 01:37:19 +00:00
|
|
|
return P(command);
|
|
|
|
}
|
2019-03-04 01:34:00 +00:00
|
|
|
case highlight_role_t::statement_terminator: {
|
2016-05-01 01:37:19 +00:00
|
|
|
return P(statement_terminator);
|
|
|
|
}
|
2019-03-04 01:34:00 +00:00
|
|
|
case highlight_role_t::param: {
|
2016-05-01 01:37:19 +00:00
|
|
|
return P(param);
|
|
|
|
}
|
2019-03-04 01:34:00 +00:00
|
|
|
case highlight_role_t::comment: {
|
2016-05-01 01:37:19 +00:00
|
|
|
return P(comment);
|
|
|
|
}
|
2019-03-04 01:34:00 +00:00
|
|
|
case highlight_role_t::search_match: {
|
2016-05-01 01:37:19 +00:00
|
|
|
return P(search_match);
|
|
|
|
}
|
2019-03-04 01:34:00 +00:00
|
|
|
case highlight_role_t::operat: {
|
2016-05-01 01:37:19 +00:00
|
|
|
return P(operator);
|
|
|
|
}
|
2019-03-04 01:34:00 +00:00
|
|
|
case highlight_role_t::escape: {
|
2016-05-01 01:37:19 +00:00
|
|
|
return P(escape);
|
|
|
|
}
|
2019-03-04 01:34:00 +00:00
|
|
|
case highlight_role_t::quote: {
|
2016-05-01 01:37:19 +00:00
|
|
|
return P(quote);
|
|
|
|
}
|
2019-03-04 01:34:00 +00:00
|
|
|
case highlight_role_t::redirection: {
|
2016-05-01 01:37:19 +00:00
|
|
|
return P(redirection);
|
|
|
|
}
|
2019-03-04 01:34:00 +00:00
|
|
|
case highlight_role_t::autosuggestion: {
|
2016-05-01 01:37:19 +00:00
|
|
|
return P(autosuggestion);
|
|
|
|
}
|
2019-03-04 01:34:00 +00:00
|
|
|
case highlight_role_t::selection: {
|
2016-05-01 01:37:19 +00:00
|
|
|
return P(selection);
|
|
|
|
}
|
2019-05-05 10:09:25 +00:00
|
|
|
default: {
|
|
|
|
return P(other);
|
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2007-04-22 10:03:12 +00:00
|
|
|
}
|
|
|
|
|
2016-05-01 01:37:19 +00:00
|
|
|
static std::string html_colorize(const wcstring &text,
|
|
|
|
const std::vector<highlight_spec_t> &colors) {
|
|
|
|
if (text.empty()) {
|
2014-12-23 23:29:42 +00:00
|
|
|
return "";
|
|
|
|
}
|
2007-04-22 18:54:29 +00:00
|
|
|
|
2014-12-23 23:29:42 +00:00
|
|
|
assert(colors.size() == text.size());
|
2014-12-24 21:08:16 +00:00
|
|
|
wcstring html = L"<pre><code>";
|
2019-03-04 01:34:00 +00:00
|
|
|
highlight_spec_t last_color = highlight_role_t::normal;
|
2016-05-01 01:37:19 +00:00
|
|
|
for (size_t i = 0; i < text.size(); i++) {
|
|
|
|
// Handle colors.
|
2014-12-23 23:29:42 +00:00
|
|
|
highlight_spec_t color = colors.at(i);
|
2016-05-01 01:37:19 +00:00
|
|
|
if (i > 0 && color != last_color) {
|
2014-12-23 23:29:42 +00:00
|
|
|
html.append(L"</span>");
|
|
|
|
}
|
2016-05-01 01:37:19 +00:00
|
|
|
if (i == 0 || color != last_color) {
|
2014-12-23 23:29:42 +00:00
|
|
|
append_format(html, L"<span class=\"%ls\">", html_class_name_for_color(color));
|
|
|
|
}
|
|
|
|
last_color = color;
|
2012-11-18 10:23:22 +00:00
|
|
|
|
2016-05-01 01:37:19 +00:00
|
|
|
// Handle text.
|
2014-12-23 23:29:42 +00:00
|
|
|
wchar_t wc = text.at(i);
|
2016-05-01 01:37:19 +00:00
|
|
|
switch (wc) {
|
|
|
|
case L'&': {
|
2014-12-23 23:29:42 +00:00
|
|
|
html.append(L"&");
|
|
|
|
break;
|
2016-05-01 01:37:19 +00:00
|
|
|
}
|
|
|
|
case L'\'': {
|
2014-12-23 23:29:42 +00:00
|
|
|
html.append(L"'");
|
|
|
|
break;
|
2016-05-01 01:37:19 +00:00
|
|
|
}
|
|
|
|
case L'"': {
|
2014-12-23 23:29:42 +00:00
|
|
|
html.append(L""");
|
|
|
|
break;
|
2016-05-01 01:37:19 +00:00
|
|
|
}
|
|
|
|
case L'<': {
|
2014-12-23 23:29:42 +00:00
|
|
|
html.append(L"<");
|
|
|
|
break;
|
2016-05-01 01:37:19 +00:00
|
|
|
}
|
|
|
|
case L'>': {
|
2014-12-23 23:29:42 +00:00
|
|
|
html.append(L">");
|
|
|
|
break;
|
2016-05-01 01:37:19 +00:00
|
|
|
}
|
|
|
|
default: {
|
2014-12-23 23:29:42 +00:00
|
|
|
html.push_back(wc);
|
|
|
|
break;
|
2016-05-01 01:37:19 +00:00
|
|
|
}
|
2014-12-23 23:29:42 +00:00
|
|
|
}
|
|
|
|
}
|
2014-12-24 22:17:06 +00:00
|
|
|
html.append(L"</span></code></pre>");
|
2014-12-23 23:29:42 +00:00
|
|
|
return wcs2string(html);
|
2007-04-22 18:54:29 +00:00
|
|
|
}
|
|
|
|
|
2016-05-01 01:37:19 +00:00
|
|
|
static std::string no_colorize(const wcstring &text) { return wcs2string(text); }
|
2007-04-22 18:54:29 +00:00
|
|
|
|
2016-05-01 01:37:19 +00:00
|
|
|
int main(int argc, char *argv[]) {
|
2018-11-28 14:08:24 +00:00
|
|
|
program_name = L"fish_indent";
|
2012-11-19 00:30:30 +00:00
|
|
|
set_main_thread();
|
2012-03-07 19:35:22 +00:00
|
|
|
setup_fork_guards();
|
2016-06-04 02:05:13 +00:00
|
|
|
// Using the user's default locale could be a problem if it doesn't use UTF-8 encoding. That's
|
|
|
|
// because the fish project assumes Unicode UTF-8 encoding in all of its scripts.
|
|
|
|
//
|
|
|
|
// TODO: Auto-detect the encoding of the script. We should look for a vim style comment
|
|
|
|
// (e.g., "# vim: set fileencoding=<encoding-name>:") or an emacs style comment
|
|
|
|
// (e.g., "# -*- coding: <encoding-name> -*-").
|
|
|
|
setlocale(LC_ALL, "");
|
2014-12-23 23:29:42 +00:00
|
|
|
env_init();
|
|
|
|
|
2016-05-01 01:37:19 +00:00
|
|
|
// Types of output we support.
|
|
|
|
enum {
|
2014-12-23 23:29:42 +00:00
|
|
|
output_type_plain_text,
|
2016-05-18 15:37:13 +00:00
|
|
|
output_type_file,
|
2014-12-23 23:29:42 +00:00
|
|
|
output_type_ansi,
|
2019-03-13 01:05:56 +00:00
|
|
|
output_type_pygments_csv,
|
2020-08-08 18:22:18 +00:00
|
|
|
output_type_check,
|
2014-12-23 23:29:42 +00:00
|
|
|
output_type_html
|
|
|
|
} output_type = output_type_plain_text;
|
2016-06-10 14:39:05 +00:00
|
|
|
const char *output_location = "";
|
2014-12-23 23:29:42 +00:00
|
|
|
bool do_indent = true;
|
|
|
|
|
2020-08-08 18:22:18 +00:00
|
|
|
const char *short_opts = "+d:hvwicD:";
|
2019-11-19 02:34:50 +00:00
|
|
|
const struct option long_opts[] = {{"debug-level", required_argument, nullptr, 'd'},
|
|
|
|
{"debug-stack-frames", required_argument, nullptr, 'D'},
|
|
|
|
{"dump-parse-tree", no_argument, nullptr, 'P'},
|
|
|
|
{"no-indent", no_argument, nullptr, 'i'},
|
|
|
|
{"help", no_argument, nullptr, 'h'},
|
|
|
|
{"version", no_argument, nullptr, 'v'},
|
|
|
|
{"write", no_argument, nullptr, 'w'},
|
|
|
|
{"html", no_argument, nullptr, 1},
|
|
|
|
{"ansi", no_argument, nullptr, 2},
|
|
|
|
{"pygments", no_argument, nullptr, 3},
|
2020-08-08 18:22:18 +00:00
|
|
|
{"check", no_argument, nullptr, 'c'},
|
2019-11-19 02:34:50 +00:00
|
|
|
{nullptr, 0, nullptr, 0}};
|
2016-03-03 23:36:17 +00:00
|
|
|
|
|
|
|
int opt;
|
2019-11-19 02:34:50 +00:00
|
|
|
while ((opt = getopt_long(argc, argv, short_opts, long_opts, nullptr)) != -1) {
|
2016-05-01 01:37:19 +00:00
|
|
|
switch (opt) {
|
2016-07-06 03:22:44 +00:00
|
|
|
case 'P': {
|
2016-04-05 22:43:24 +00:00
|
|
|
dump_parse_tree = true;
|
|
|
|
break;
|
|
|
|
}
|
2016-05-01 01:37:19 +00:00
|
|
|
case 'h': {
|
2012-11-19 08:31:03 +00:00
|
|
|
print_help("fish_indent", 1);
|
2016-05-18 15:37:13 +00:00
|
|
|
exit(0);
|
2012-11-19 08:31:03 +00:00
|
|
|
}
|
2016-05-01 01:37:19 +00:00
|
|
|
case 'v': {
|
2020-05-04 20:53:50 +00:00
|
|
|
std::fwprintf(stdout, _(L"%ls, version %s\n"), program_name, get_fish_version());
|
2012-11-19 08:31:03 +00:00
|
|
|
exit(0);
|
2016-05-18 15:37:13 +00:00
|
|
|
}
|
|
|
|
case 'w': {
|
|
|
|
output_type = output_type_file;
|
2014-10-31 05:40:35 +00:00
|
|
|
break;
|
2012-11-19 08:31:03 +00:00
|
|
|
}
|
2016-05-01 01:37:19 +00:00
|
|
|
case 'i': {
|
2014-12-23 23:29:42 +00:00
|
|
|
do_indent = false;
|
2012-11-19 08:31:03 +00:00
|
|
|
break;
|
|
|
|
}
|
2016-05-01 01:37:19 +00:00
|
|
|
case 1: {
|
2014-12-23 23:29:42 +00:00
|
|
|
output_type = output_type_html;
|
|
|
|
break;
|
|
|
|
}
|
2016-05-01 01:37:19 +00:00
|
|
|
case 2: {
|
2014-12-23 23:29:42 +00:00
|
|
|
output_type = output_type_ansi;
|
|
|
|
break;
|
|
|
|
}
|
2019-03-13 01:05:56 +00:00
|
|
|
case 3: {
|
|
|
|
output_type = output_type_pygments_csv;
|
|
|
|
break;
|
|
|
|
}
|
2020-08-08 18:22:18 +00:00
|
|
|
case 'c': {
|
|
|
|
output_type = output_type_check;
|
|
|
|
break;
|
|
|
|
}
|
2016-07-06 03:22:44 +00:00
|
|
|
case 'd': {
|
|
|
|
char *end;
|
|
|
|
long tmp;
|
|
|
|
|
|
|
|
errno = 0;
|
|
|
|
tmp = strtol(optarg, &end, 10);
|
|
|
|
|
|
|
|
if (tmp >= 0 && tmp <= 10 && !*end && !errno) {
|
2019-11-19 01:08:16 +00:00
|
|
|
debug_level = static_cast<int>(tmp);
|
2016-07-06 03:22:44 +00:00
|
|
|
} else {
|
2019-03-12 21:06:01 +00:00
|
|
|
std::fwprintf(stderr, _(L"Invalid value '%s' for debug-level flag"), optarg);
|
2016-07-06 03:22:44 +00:00
|
|
|
exit(1);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'D': {
|
|
|
|
char *end;
|
|
|
|
long tmp;
|
|
|
|
|
|
|
|
errno = 0;
|
|
|
|
tmp = strtol(optarg, &end, 10);
|
|
|
|
|
|
|
|
if (tmp > 0 && tmp <= 128 && !*end && !errno) {
|
2019-11-19 01:08:16 +00:00
|
|
|
set_debug_stack_frames(static_cast<int>(tmp));
|
2016-07-06 03:22:44 +00:00
|
|
|
} else {
|
2019-05-05 10:09:25 +00:00
|
|
|
std::fwprintf(stderr, _(L"Invalid value '%s' for debug-stack-frames flag"),
|
|
|
|
optarg);
|
2016-07-06 03:22:44 +00:00
|
|
|
exit(1);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2016-05-01 01:37:19 +00:00
|
|
|
default: {
|
2016-03-03 23:36:17 +00:00
|
|
|
// We assume getopt_long() has already emitted a diagnostic msg.
|
2016-05-18 15:37:13 +00:00
|
|
|
exit(1);
|
2012-11-19 08:31:03 +00:00
|
|
|
}
|
2012-11-19 00:30:30 +00:00
|
|
|
}
|
2012-11-18 10:23:22 +00:00
|
|
|
}
|
2007-04-22 10:03:12 +00:00
|
|
|
|
2016-05-18 15:37:13 +00:00
|
|
|
argc -= optind;
|
|
|
|
argv += optind;
|
|
|
|
|
2020-08-10 20:02:03 +00:00
|
|
|
int retval = 0;
|
|
|
|
|
2016-05-18 15:37:13 +00:00
|
|
|
wcstring src;
|
2019-05-28 02:47:13 +00:00
|
|
|
for (int i = 0; i < argc || (argc == 0 && i == 0); i++) {
|
2019-05-22 20:20:56 +00:00
|
|
|
if (argc == 0 && i == 0) {
|
|
|
|
if (output_type == output_type_file) {
|
2019-05-28 02:47:13 +00:00
|
|
|
std::fwprintf(
|
|
|
|
stderr, _(L"Expected file path to read/write for -w:\n\n $ %ls -w foo.fish\n"),
|
|
|
|
program_name);
|
2019-05-22 20:20:56 +00:00
|
|
|
exit(1);
|
|
|
|
}
|
|
|
|
src = read_file(stdin);
|
|
|
|
} else {
|
2019-05-20 19:04:51 +00:00
|
|
|
FILE *fh = fopen(argv[i], "r");
|
2016-05-18 15:37:13 +00:00
|
|
|
if (fh) {
|
2019-05-20 19:04:51 +00:00
|
|
|
src = read_file(fh);
|
2016-05-18 15:37:13 +00:00
|
|
|
fclose(fh);
|
2019-05-20 19:04:51 +00:00
|
|
|
output_location = argv[i];
|
2016-05-18 15:37:13 +00:00
|
|
|
} else {
|
2020-05-19 18:42:00 +00:00
|
|
|
std::fwprintf(stderr, _(L"Opening \"%s\" failed: %s\n"), argv[i],
|
2019-05-28 02:47:13 +00:00
|
|
|
std::strerror(errno));
|
2016-05-18 15:37:13 +00:00
|
|
|
exit(1);
|
|
|
|
}
|
2019-05-22 20:20:56 +00:00
|
|
|
}
|
2019-05-20 19:04:51 +00:00
|
|
|
|
2019-05-22 20:20:56 +00:00
|
|
|
if (output_type == output_type_pygments_csv) {
|
|
|
|
std::string output = make_pygments_csv(src);
|
|
|
|
fputs(output.c_str(), stdout);
|
2019-05-25 09:38:05 +00:00
|
|
|
continue;
|
2019-05-22 20:20:56 +00:00
|
|
|
}
|
2019-05-20 19:04:51 +00:00
|
|
|
|
2019-05-22 20:20:56 +00:00
|
|
|
const wcstring output_wtext = prettify(src, do_indent);
|
2019-05-20 19:04:51 +00:00
|
|
|
|
2019-05-22 20:20:56 +00:00
|
|
|
// Maybe colorize.
|
|
|
|
std::vector<highlight_spec_t> colors;
|
|
|
|
if (output_type != output_type_plain_text) {
|
2020-08-03 21:10:37 +00:00
|
|
|
highlight_shell(output_wtext, colors, output_wtext.size(),
|
|
|
|
operation_context_t::globals());
|
2019-05-22 20:20:56 +00:00
|
|
|
}
|
2019-05-20 19:04:51 +00:00
|
|
|
|
2019-05-22 20:20:56 +00:00
|
|
|
std::string colored_output;
|
|
|
|
switch (output_type) {
|
2019-05-28 02:47:13 +00:00
|
|
|
case output_type_plain_text: {
|
|
|
|
colored_output = no_colorize(output_wtext);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case output_type_file: {
|
|
|
|
FILE *fh = fopen(output_location, "w");
|
|
|
|
if (fh) {
|
|
|
|
std::fputws(output_wtext.c_str(), fh);
|
|
|
|
fclose(fh);
|
|
|
|
} else {
|
|
|
|
std::fwprintf(stderr, _(L"Opening \"%s\" failed: %s\n"), output_location,
|
|
|
|
std::strerror(errno));
|
|
|
|
exit(1);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case output_type_ansi: {
|
2020-08-04 00:34:27 +00:00
|
|
|
colored_output = colorize(output_wtext, colors, env_stack_t::globals());
|
2019-05-28 02:47:13 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case output_type_html: {
|
|
|
|
colored_output = html_colorize(output_wtext, colors);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case output_type_pygments_csv: {
|
|
|
|
DIE("pygments_csv should have been handled above");
|
2019-05-20 19:04:51 +00:00
|
|
|
}
|
2020-08-08 18:22:18 +00:00
|
|
|
case output_type_check: {
|
|
|
|
if (output_wtext != src) {
|
2020-08-10 20:02:03 +00:00
|
|
|
if (argc) {
|
|
|
|
std::fwprintf(stderr, _(L"%s\n"), argv[i]);
|
|
|
|
}
|
|
|
|
retval++;
|
2020-08-08 18:22:18 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2019-05-22 20:20:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
std::fputws(str2wcstring(colored_output).c_str(), stdout);
|
2014-12-23 23:29:42 +00:00
|
|
|
}
|
2020-08-10 20:02:03 +00:00
|
|
|
return retval;
|
2007-04-22 10:03:12 +00:00
|
|
|
}
|