Bash: извлечение последних двух каналов для имени пути

Кажется, я провалился в чем-то довольно просто, в bash. У меня есть строковая переменная, которая содержит полный путь к каталогу. Я хотел бы назначить последние два каталога в другой строке. Например, если у меня есть:

DIRNAME = /a/b/c/d/e

Мне бы хотелось:

DIRNAME2 = d/e

Я уверен, что есть простая команда bash или sed, которая сделает это, но она ускользает от меня. Я бы вроде обобщенной версии basename или dirname, которая не просто возвращает крайние части имени.

Спасибо! Dave

Ответ 1

DIRNAME="/a/b/c/d/e"
D2=$(dirname "$DIRNAME")
DIRNAME2=$(basename "$D2")/$(basename "$DIRNAME")

Или в одну строку (но будьте осторожны со всеми двойными кавычками - легче разделить):

DIRNAME2=$(basename "$(dirname "$DIRNAME")")/$(basename "$DIRNAME")

Не пытайтесь использовать эту игру с обратными кавычками, если вы не сильно увлечены мазохизмом. И если в путях могут быть пробелы, используйте двойные кавычки вокруг имен переменных.

Это будет работать практически с любой оболочкой Korn Shell, а также с Bash. В bash доступны и другие механизмы - другие ответы иллюстрируют некоторые из многих опций, хотя expr также является решением старой школы (он также присутствовал в Unix 7-го издания). Этот код, использующий обратные кавычки, работает и в оболочке Bash и Korn, но не в оболочке Heirloom (которая аналогична оболочке Unix System V Release 2/3/4, IIRC).

DIRNAME2='basename "\'dirname \\"$DIRNAME\\"\'"'/'basename "$DIRNAME"'

(Два уровня вложенности не так уж ужасны, но это довольно плохо; три уровня становятся действительно сложными!)

тестирование

При тестировании манипуляции с именем пути, которая должна выдерживать пробелы в имени пути, стоит проверить с именем, содержащим двойные пробелы (а не одиночные пробелы или их использование). Например:

DIRNAME="/a b/ c d /  ee  ff  /  gg  hh  "
echo "DIRNAME=[[$DIRNAME]]"
echo "basename1=[[$(basename "$DIRNAME")]]"
echo "basename2=[['basename \"$DIRNAME\"']]"
echo
D2=$(dirname "$DIRNAME")
echo "D2=[[$D2]]"
DIRNAME2=$(basename "$D2")/$(basename "$DIRNAME")
echo "DIRNAME2=[[$DIRNAME2]]"
echo
DIRNAME3=$(basename "$(dirname "$DIRNAME")")/$(basename "$DIRNAME")
echo "DIRNAME3=[[$DIRNAME3]]"
DIRNAME4='basename "\'dirname \\"$DIRNAME\\"\'"'/'basename "$DIRNAME"'
echo "DIRNAME4=[[$DIRNAME2]]"

Выход из этого:

DIRNAME=[[/a b/ c d /  ee  ff  /  gg  hh  ]]
basename1=[[  gg  hh  ]]
basename2=[[  gg  hh  ]]

D2=[[/a b/ c d /  ee  ff  ]]
DIRNAME2=[[  ee  ff  /  gg  hh  ]]

DIRNAME3=[[  ee  ff  /  gg  hh  ]]
DIRNAME4=[[  ee  ff  /  gg  hh  ]]

Ответ 2

Я предпочитаю использовать встроенные функции как можно больше, чтобы избежать создания ненужных процессов. Поскольку ваш script может запускаться под Cygwin или другой ОС, создание которых очень дорого.

Я думаю, что это не так много, если вы просто хотите извлечь два диска:

base1="${DIRNAME##*/}"
dir1="${DIRNAME%/*}"
DIRNAME2="${dir1##*/}/$base1"

Это также позволяет избежать особых char проблем, связанных с выполнением других команд.

Ответ 3

Я не знаю метода, специально для обрезки путей, но вы можете это сделать с помощью bash соответствия регулярных выражений:

DIRNAME=/a/b/c/d/e
if [[ "$DIRNAME" =~ ([^/]+/+[^/]+)/*$ ]]; then
    echo "Last two: ${BASH_REMATCH[1]}"
else
    echo "No match"
fi

Примечание. Я сделал шаблон здесь немного более сложным, чем вы могли ожидать, чтобы обрабатывать некоторые допустимые, но не общие вещи в пути: он обрезает конечные косые черты и допускает множественные (избыточные) косые черты между двумя последними именами. Например, запуск его на "/a/b/c//d//" будет соответствовать "c//d".

Ответ 4

Я думаю, что более короткий способ использования глобусов, но:

$ DIRNAME='a/b/c/d/e'
$ LAST_TWO=$(expr "$DIRNAME" : '.*/\([^/]*/[^/]*\)$')
$ echo $LAST_TWO
d/e

Ответ 5

DIRNAME="a/b/c/d/e"
DIRNAME2=`echo $DIRNAME | awk -F/ '{print $(NF-1)"_"$(NF)}'`

DIRNAME2 then has the value d_e

Измените подчеркивание на все, что пожелаете.

Ответ 6

dirname=/a/b/c/d/e
IFS=/ read -a dirs <<< "$dirname"
printf "%s/%s\n" "${dirs[-2]}" "${dirs[-1]}"

Ответ 7

function getDir() {
echo $1 | awk -F/ '
{
    n=NF-'$2'+1;
    if(n<1)exit;
    for (i=n;i<=NF;i++) {
        printf("/%s",$i);
    }
}'
}

dir="/a/b/c/d/e"
dir2=`getDir $dir 1`
echo $dir2

Вы можете получить любое количество каталогов от последней из этой функции. Для вашего случая запустите его как,

dir2=`getDir $dir 2`;

Выход:/d/e