Stéphane Chazelas has already shown a sed-based solution. I came across a slightly different sed expression by ack that I customize below to answer this question. Specifically, I restrict it to path components and handle the possibility of newlines in the path components. I then demonstrate using it to decompose path specs into longest common leading path components + remaining path components.
We'll start with ack's sed expression (I switched it to ERE syntax ①):
sed -E '$!{N;s/^(.*).*\n\1.*$/\1\n\1/;D;}' <<"EOF'
/abc/bcd/cdf
/abc/bcd/cdf/foo
/abc/bcd/chi/hij
/abc/bcd/cdd
EOF
⇒ /abc/bcd/c
as expected. ✔️
To restrict it to path components:
sed -E '$!{N;s|^(.*/).*\n\1.*$|\1\n\1|;D;};s|/$||' <<'EOF'
/abc/bcd/cdf
/abc/bcd/cdf/foo
/abc/bcd/chi/hij
/abc/bcd/cdd
EOF
⇒ /abc/bcd
as expected. ✔️
Handle path components with newlines
For testing purposes, we will use this array of path specs:
a=(
$'/a\n/b/\nc d\n/\n\ne/f'
$'/a\n/b/\nc d\n/\ne/f'
$'/a\n/b/\nc d\n/\ne\n/f'
$'/a\n/b/\nc d\n/\nef'
)
By inspection we can see that the longest common leading path component is:
$'/a\n/b/\nc d\n'
This can be computed and captured in a variable with the following:
longest_common_leading_path_component=$(
printf '%s\0' "${a[@]}" \
| sed -zE '$!{N;s|^(.*/).*\x00\1.*$|\1\x00\1|;D;};s|/$||' \
| tr \\0 x # replace trailing NUL with a dummy character ②
)
# Remove the dummy character
longest_common_leading_path_component=${longest_common_leading_path_component%x}
# Inspect result
echo "${longest_common_leading_path_component@Q}" # ③
Result:
$'/a\n/b/\nc d\n'
as expected. ✔️
Continuing with our test case, we now illustrate how to decompose the path specs into longest common leading path components + remaining path components with the following:
for e in "${a[@]}"; do
remainder=${e#"$longest_common_leading_path_component/"}
printf '%-26s -> %s + %s\n' \
"${e@Q}" \
"${longest_common_leading_path_component@Q}" \
"${remainder@Q}"
done
Result:
$'/a\n/b/\nc d\n/\n\ne/f' -> $'/a\n/b/\nc d\n' + $'\n\ne/f'
$'/a\n/b/\nc d\n/\ne/f' -> $'/a\n/b/\nc d\n' + $'\ne/f'
$'/a\n/b/\nc d\n/\ne\n/f' -> $'/a\n/b/\nc d\n' + $'\ne\n/f'
$'/a\n/b/\nc d\n/\nef' -> $'/a\n/b/\nc d\n' + $'\nef'
① I always add the -E
option to sed and grep to switch them to ERE syntax for better consistency with other tools/languages I use, e.g., awk, bash, perl, javascript, and java.
② To preserve any trailing newlines in this command substitution, we used the usual technique of appending a dummy character that is chopped off afterwards. We combined the removal of the trailing NUL with the addition of the dummy character (we chose x
) in one step using tr \\0 x
.
③ The ${parameter@Q}
expansion results in "a string that is the value of parameter quoted in a format that can be reused as input." – bash reference manual. Requires bash 4.4+ (discussion). Otherwise, you can inspect the result using one of the following:
printf '%q' "$longest_common_leading_path_component"
printf '%s' "$longest_common_leading_path_component" | od -An -tc
od -An -tc < <(printf %s "$longest_common_leading_path_component")
od -An -tc <<<$longest_common_leading_path_component # ④
④ Be aware that here-strings add a newline (discussion).