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).