forked from lix-project/lix
Merge pull request #5945 from afishhh/master
Make `nix search` highlight all regexes and matches
This commit is contained in:
commit
4530574363
5 changed files with 154 additions and 26 deletions
38
src/libutil/fmt.cc
Normal file
38
src/libutil/fmt.cc
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
#include <regex>
|
||||||
|
|
||||||
|
namespace nix {
|
||||||
|
|
||||||
|
std::string hiliteMatches(const std::string &s, std::vector<std::smatch> matches, std::string prefix, std::string postfix) {
|
||||||
|
// Avoid copy on zero matches
|
||||||
|
if (matches.size() == 0)
|
||||||
|
return s;
|
||||||
|
|
||||||
|
std::sort(matches.begin(), matches.end(), [](const auto &a, const auto &b) {
|
||||||
|
return a.position() < b.position();
|
||||||
|
});
|
||||||
|
|
||||||
|
std::string out;
|
||||||
|
ssize_t last_end = 0;
|
||||||
|
|
||||||
|
for (auto it = matches.begin(); it != matches.end();) {
|
||||||
|
auto m = *it;
|
||||||
|
size_t start = m.position();
|
||||||
|
out.append(s.substr(last_end, m.position() - last_end));
|
||||||
|
// Merge continous matches
|
||||||
|
ssize_t end = start + m.length();
|
||||||
|
while(++it != matches.end() && (*it).position() <= end) {
|
||||||
|
auto n = *it;
|
||||||
|
ssize_t nend = start + (n.position() - start + n.length());
|
||||||
|
if(nend > end)
|
||||||
|
end = nend;
|
||||||
|
}
|
||||||
|
out.append(prefix);
|
||||||
|
out.append(s.substr(start, end - start));
|
||||||
|
out.append(postfix);
|
||||||
|
last_end = end;
|
||||||
|
}
|
||||||
|
out.append(s.substr(last_end));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <boost/format.hpp>
|
#include <boost/format.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <regex>
|
||||||
#include "ansicolor.hh"
|
#include "ansicolor.hh"
|
||||||
|
|
||||||
|
|
||||||
|
@ -154,4 +155,13 @@ inline hintformat hintfmt(std::string plain_string)
|
||||||
// we won't be receiving any args in this case, so just print the original string
|
// we won't be receiving any args in this case, so just print the original string
|
||||||
return hintfmt("%s", normaltxt(plain_string));
|
return hintfmt("%s", normaltxt(plain_string));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlight all the given matches in the given string `s` by wrapping them
|
||||||
|
* between `prefix` and `postfix`.
|
||||||
|
*
|
||||||
|
* If some matches overlap, then their union will be wrapped rather than the
|
||||||
|
* individual matches.
|
||||||
|
*/
|
||||||
|
std::string hiliteMatches(const std::string &s, std::vector<std::smatch> matches, std::string prefix, std::string postfix);
|
||||||
}
|
}
|
||||||
|
|
68
src/libutil/tests/fmt.cc
Normal file
68
src/libutil/tests/fmt.cc
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
#include "fmt.hh"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <regex>
|
||||||
|
|
||||||
|
namespace nix {
|
||||||
|
/* ----------- tests for fmt.hh -------------------------------------------------*/
|
||||||
|
|
||||||
|
TEST(hiliteMatches, noHighlight) {
|
||||||
|
ASSERT_STREQ(hiliteMatches("Hello, world!", std::vector<std::smatch>(), "(", ")").c_str(), "Hello, world!");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(hiliteMatches, simpleHighlight) {
|
||||||
|
std::string str = "Hello, world!";
|
||||||
|
std::regex re = std::regex("world");
|
||||||
|
auto matches = std::vector(std::sregex_iterator(str.begin(), str.end(), re), std::sregex_iterator());
|
||||||
|
ASSERT_STREQ(
|
||||||
|
hiliteMatches(str, matches, "(", ")").c_str(),
|
||||||
|
"Hello, (world)!"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(hiliteMatches, multipleMatches) {
|
||||||
|
std::string str = "Hello, world, world, world, world, world, world, Hello!";
|
||||||
|
std::regex re = std::regex("world");
|
||||||
|
auto matches = std::vector(std::sregex_iterator(str.begin(), str.end(), re), std::sregex_iterator());
|
||||||
|
ASSERT_STREQ(
|
||||||
|
hiliteMatches(str, matches, "(", ")").c_str(),
|
||||||
|
"Hello, (world), (world), (world), (world), (world), (world), Hello!"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(hiliteMatches, overlappingMatches) {
|
||||||
|
std::string str = "world, Hello, world, Hello, world, Hello, world, Hello, world!";
|
||||||
|
std::regex re = std::regex("Hello, world");
|
||||||
|
std::regex re2 = std::regex("world, Hello");
|
||||||
|
auto v = std::vector(std::sregex_iterator(str.begin(), str.end(), re), std::sregex_iterator());
|
||||||
|
for(auto it = std::sregex_iterator(str.begin(), str.end(), re2); it != std::sregex_iterator(); ++it) {
|
||||||
|
v.push_back(*it);
|
||||||
|
}
|
||||||
|
ASSERT_STREQ(
|
||||||
|
hiliteMatches(str, v, "(", ")").c_str(),
|
||||||
|
"(world, Hello, world, Hello, world, Hello, world, Hello, world)!"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(hiliteMatches, complexOverlappingMatches) {
|
||||||
|
std::string str = "legacyPackages.x86_64-linux.git-crypt";
|
||||||
|
std::vector regexes = {
|
||||||
|
std::regex("t-cry"),
|
||||||
|
std::regex("ux\\.git-cry"),
|
||||||
|
std::regex("git-c"),
|
||||||
|
std::regex("pt"),
|
||||||
|
};
|
||||||
|
std::vector<std::smatch> matches;
|
||||||
|
for(auto regex : regexes)
|
||||||
|
{
|
||||||
|
for(auto it = std::sregex_iterator(str.begin(), str.end(), regex); it != std::sregex_iterator(); ++it) {
|
||||||
|
matches.push_back(*it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ASSERT_STREQ(
|
||||||
|
hiliteMatches(str, matches, "(", ")").c_str(),
|
||||||
|
"legacyPackages.x86_64-lin(ux.git-crypt)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@
|
||||||
#include "shared.hh"
|
#include "shared.hh"
|
||||||
#include "eval-cache.hh"
|
#include "eval-cache.hh"
|
||||||
#include "attr-path.hh"
|
#include "attr-path.hh"
|
||||||
|
#include "fmt.hh"
|
||||||
|
|
||||||
#include <regex>
|
#include <regex>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
@ -20,16 +21,6 @@ std::string wrap(std::string prefix, std::string s)
|
||||||
return prefix + s + ANSI_NORMAL;
|
return prefix + s + ANSI_NORMAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string hilite(const std::string & s, const std::smatch & m, std::string postfix)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
m.empty()
|
|
||||||
? s
|
|
||||||
: std::string(m.prefix())
|
|
||||||
+ ANSI_GREEN + std::string(m.str()) + postfix
|
|
||||||
+ std::string(m.suffix());
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CmdSearch : InstallableCommand, MixJSON
|
struct CmdSearch : InstallableCommand, MixJSON
|
||||||
{
|
{
|
||||||
std::vector<std::string> res;
|
std::vector<std::string> res;
|
||||||
|
@ -100,8 +91,6 @@ struct CmdSearch : InstallableCommand, MixJSON
|
||||||
};
|
};
|
||||||
|
|
||||||
if (cursor.isDerivation()) {
|
if (cursor.isDerivation()) {
|
||||||
size_t found = 0;
|
|
||||||
|
|
||||||
DrvName name(cursor.getAttr("name")->getString());
|
DrvName name(cursor.getAttr("name")->getString());
|
||||||
|
|
||||||
auto aMeta = cursor.maybeGetAttr("meta");
|
auto aMeta = cursor.maybeGetAttr("meta");
|
||||||
|
@ -110,21 +99,31 @@ struct CmdSearch : InstallableCommand, MixJSON
|
||||||
std::replace(description.begin(), description.end(), '\n', ' ');
|
std::replace(description.begin(), description.end(), '\n', ' ');
|
||||||
auto attrPath2 = concatStringsSep(".", attrPath);
|
auto attrPath2 = concatStringsSep(".", attrPath);
|
||||||
|
|
||||||
std::smatch attrPathMatch;
|
std::vector<std::smatch> attrPathMatches;
|
||||||
std::smatch descriptionMatch;
|
std::vector<std::smatch> descriptionMatches;
|
||||||
std::smatch nameMatch;
|
std::vector<std::smatch> nameMatches;
|
||||||
|
bool found = false;
|
||||||
|
|
||||||
for (auto & regex : regexes) {
|
for (auto & regex : regexes) {
|
||||||
std::regex_search(attrPath2, attrPathMatch, regex);
|
found = false;
|
||||||
std::regex_search(name.name, nameMatch, regex);
|
auto add_all = [&found](std::sregex_iterator it, std::vector<std::smatch>& vec){
|
||||||
std::regex_search(description, descriptionMatch, regex);
|
const auto end = std::sregex_iterator();
|
||||||
if (!attrPathMatch.empty()
|
while(it != end) {
|
||||||
|| !nameMatch.empty()
|
vec.push_back(*it++);
|
||||||
|| !descriptionMatch.empty())
|
found = true;
|
||||||
found++;
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
add_all(std::sregex_iterator(attrPath2.begin(), attrPath2.end(), regex), attrPathMatches);
|
||||||
|
add_all(std::sregex_iterator(name.name.begin(), name.name.end(), regex), nameMatches);
|
||||||
|
add_all(std::sregex_iterator(description.begin(), description.end(), regex), descriptionMatches);
|
||||||
|
|
||||||
|
if(!found)
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (found == res.size()) {
|
if (found)
|
||||||
|
{
|
||||||
results++;
|
results++;
|
||||||
if (json) {
|
if (json) {
|
||||||
auto jsonElem = jsonOut->object(attrPath2);
|
auto jsonElem = jsonOut->object(attrPath2);
|
||||||
|
@ -132,15 +131,15 @@ struct CmdSearch : InstallableCommand, MixJSON
|
||||||
jsonElem.attr("version", name.version);
|
jsonElem.attr("version", name.version);
|
||||||
jsonElem.attr("description", description);
|
jsonElem.attr("description", description);
|
||||||
} else {
|
} else {
|
||||||
auto name2 = hilite(name.name, nameMatch, "\e[0;2m");
|
auto name2 = hiliteMatches(name.name, std::move(nameMatches), ANSI_GREEN, "\e[0;2m");
|
||||||
if (results > 1) logger->cout("");
|
if (results > 1) logger->cout("");
|
||||||
logger->cout(
|
logger->cout(
|
||||||
"* %s%s",
|
"* %s%s",
|
||||||
wrap("\e[0;1m", hilite(attrPath2, attrPathMatch, "\e[0;1m")),
|
wrap("\e[0;1m", hiliteMatches(attrPath2, std::move(attrPathMatches), ANSI_GREEN, "\e[0;1m")),
|
||||||
name.version != "" ? " (" + name.version + ")" : "");
|
name.version != "" ? " (" + name.version + ")" : "");
|
||||||
if (description != "")
|
if (description != "")
|
||||||
logger->cout(
|
logger->cout(
|
||||||
" %s", hilite(description, descriptionMatch, ANSI_NORMAL));
|
" %s", hiliteMatches(description, std::move(descriptionMatches), ANSI_GREEN, ANSI_NORMAL));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,3 +23,16 @@ clearCache
|
||||||
nix search -f search.nix '' |grep -q foo
|
nix search -f search.nix '' |grep -q foo
|
||||||
nix search -f search.nix '' |grep -q bar
|
nix search -f search.nix '' |grep -q bar
|
||||||
nix search -f search.nix '' |grep -q hello
|
nix search -f search.nix '' |grep -q hello
|
||||||
|
|
||||||
|
## Tests for multiple regex/match highlighting
|
||||||
|
|
||||||
|
e=$'\x1b' # grep doesn't support \e, \033 or even \x1b
|
||||||
|
# Multiple overlapping regexes
|
||||||
|
(( $(nix search -f search.nix '' 'oo' 'foo' 'oo' | grep "$e\[32;1mfoo$e\\[0;1m" | wc -l) == 1 ))
|
||||||
|
(( $(nix search -f search.nix '' 'broken b' 'en bar' | grep "$e\[32;1mbroken bar$e\\[0m" | wc -l) == 1 ))
|
||||||
|
|
||||||
|
# Multiple matches
|
||||||
|
# Searching for 'o' should yield the 'o' in 'broken bar', the 'oo' in foo and 'o' in hello
|
||||||
|
(( $(nix search -f search.nix '' 'o' | grep -Eo "$e\[32;1mo{1,2}$e\[(0|0;1)m" | wc -l) == 3 ))
|
||||||
|
# Searching for 'b' should yield the 'b' in bar and the two 'b's in 'broken bar'
|
||||||
|
(( $(nix search -f search.nix '' 'b' | grep -Eo "$e\[32;1mb$e\[(0|0;1)m" | wc -l) == 3 ))
|
||||||
|
|
Loading…
Reference in a new issue