aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThéophane Hufschmitt <7226587+thufschmitt@users.noreply.github.com>2022-01-24 13:02:51 +0100
committerGitHub <noreply@github.com>2022-01-24 13:02:51 +0100
commit45305743634e11053b5b9428b7b1d09df2d47856 (patch)
treedc978678ff91ddb930180551b014bfa6f68c4b80
parentedf0cde1a7f733d473e68ba5be996f07e86ab08d (diff)
parentffb28eaa1e15f85d3fbf6bfc3a04a4010f9c80c9 (diff)
Merge pull request #5945 from afishhh/master
Make `nix search` highlight all regexes and matches
-rw-r--r--src/libutil/fmt.cc38
-rw-r--r--src/libutil/fmt.hh10
-rw-r--r--src/libutil/tests/fmt.cc68
-rw-r--r--src/nix/search.cc51
-rw-r--r--tests/search.sh13
5 files changed, 154 insertions, 26 deletions
diff --git a/src/libutil/fmt.cc b/src/libutil/fmt.cc
new file mode 100644
index 000000000..914fb62b2
--- /dev/null
+++ b/src/libutil/fmt.cc
@@ -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;
+}
+
+}
diff --git a/src/libutil/fmt.hh b/src/libutil/fmt.hh
index fd335b811..1f81bfcfb 100644
--- a/src/libutil/fmt.hh
+++ b/src/libutil/fmt.hh
@@ -2,6 +2,7 @@
#include <boost/format.hpp>
#include <string>
+#include <regex>
#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
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);
}
diff --git a/src/libutil/tests/fmt.cc b/src/libutil/tests/fmt.cc
new file mode 100644
index 000000000..33772162c
--- /dev/null
+++ b/src/libutil/tests/fmt.cc
@@ -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)"
+ );
+ }
+}
diff --git a/src/nix/search.cc b/src/nix/search.cc
index 0d8fdd5c2..0d10d8c2e 100644
--- a/src/nix/search.cc
+++ b/src/nix/search.cc
@@ -9,6 +9,7 @@
#include "shared.hh"
#include "eval-cache.hh"
#include "attr-path.hh"
+#include "fmt.hh"
#include <regex>
#include <fstream>
@@ -20,16 +21,6 @@ std::string wrap(std::string prefix, std::string s)
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
{
std::vector<std::string> res;
@@ -100,8 +91,6 @@ struct CmdSearch : InstallableCommand, MixJSON
};
if (cursor.isDerivation()) {
- size_t found = 0;
-
DrvName name(cursor.getAttr("name")->getString());
auto aMeta = cursor.maybeGetAttr("meta");
@@ -110,21 +99,31 @@ struct CmdSearch : InstallableCommand, MixJSON
std::replace(description.begin(), description.end(), '\n', ' ');
auto attrPath2 = concatStringsSep(".", attrPath);
- std::smatch attrPathMatch;
- std::smatch descriptionMatch;
- std::smatch nameMatch;
+ std::vector<std::smatch> attrPathMatches;
+ std::vector<std::smatch> descriptionMatches;
+ std::vector<std::smatch> nameMatches;
+ bool found = false;
for (auto & regex : regexes) {
- std::regex_search(attrPath2, attrPathMatch, regex);
- std::regex_search(name.name, nameMatch, regex);
- std::regex_search(description, descriptionMatch, regex);
- if (!attrPathMatch.empty()
- || !nameMatch.empty()
- || !descriptionMatch.empty())
- found++;
+ found = false;
+ auto add_all = [&found](std::sregex_iterator it, std::vector<std::smatch>& vec){
+ const auto end = std::sregex_iterator();
+ while(it != end) {
+ vec.push_back(*it++);
+ found = true;
+ }
+ };
+
+ 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++;
if (json) {
auto jsonElem = jsonOut->object(attrPath2);
@@ -132,15 +131,15 @@ struct CmdSearch : InstallableCommand, MixJSON
jsonElem.attr("version", name.version);
jsonElem.attr("description", description);
} 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("");
logger->cout(
"* %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 + ")" : "");
if (description != "")
logger->cout(
- " %s", hilite(description, descriptionMatch, ANSI_NORMAL));
+ " %s", hiliteMatches(description, std::move(descriptionMatches), ANSI_GREEN, ANSI_NORMAL));
}
}
}
diff --git a/tests/search.sh b/tests/search.sh
index ee3261687..52e12f381 100644
--- a/tests/search.sh
+++ b/tests/search.sh
@@ -23,3 +23,16 @@ clearCache
nix search -f search.nix '' |grep -q foo
nix search -f search.nix '' |grep -q bar
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 ))