Удалите дубликаты записей $ PATH с помощью команды awk

48

Я пытаюсь написать функцию оболочки bash, которая позволит мне удалять дубликаты копий каталогов из моей переменной среды PATH.

Мне сказали, что это можно сделать с помощью одной строки, используя awkкоманду, но я не могу понять, как это сделать. Кто-нибудь знает как?

Джонни Виллием
источник

Ответы:

37

Если у вас еще нет дубликатов в PATHи вы хотите добавлять каталоги только в том случае, если их еще нет, вы можете легко сделать это с помощью одной оболочки.

for x in /path/to/add …; do
  case ":$PATH:" in
    *":$x:"*) :;; # already there
    *) PATH="$x:$PATH";;
  esac
done

А вот фрагмент оболочки, из которого удаляются дубликаты $PATH. Он просматривает записи одну за другой и копирует те, которые еще не видели.

if [ -n "$PATH" ]; then
  old_PATH=$PATH:; PATH=
  while [ -n "$old_PATH" ]; do
    x=${old_PATH%%:*}       # the first remaining entry
    case $PATH: in
      *:"$x":*) ;;          # already there
      *) PATH=$PATH:$x;;    # not there yet
    esac
    old_PATH=${old_PATH#*:}
  done
  PATH=${PATH#:}
  unset old_PATH x
fi
Жиль "ТАК - перестань быть злым"
источник
Было бы лучше, если бы итерации элементов в $ PATH выполнялись в обратном порядке, поскольку обычно добавляются более поздние элементы, и они могут иметь значение до даты.
Эрик Ван
2
@EricWang Я не понимаю твоих рассуждений. Элементы PATH перемещаются спереди назад, поэтому при наличии дубликатов второй дубликат фактически игнорируется. Итерация от начала до конца изменила бы порядок.
Жиль "ТАК - перестань быть злым"
@Gilles Если у вас есть дублированная переменная в PATH, возможно, она добавлена ​​следующим образом: PATH=$PATH:x=bx в исходном PATH может иметь значение a, поэтому при повторении по порядку новое значение будет игнорироваться, а при обратном порядке новое значение вступит в силу.
Эрик Ван
4
@EricWang В этом случае добавленная стоимость не имеет никакого эффекта, поэтому следует игнорировать. Возвращаясь назад, вы создаете добавленную стоимость раньше. Если бы добавленная стоимость должна была идти раньше, она была бы добавлена ​​как PATH=x:$PATH.
Жиль "ТАК - перестать быть злым"
@Gilles Когда вы добавляете что-то, это означает, что его еще нет, или вы хотите переопределить старое значение, поэтому вы должны сделать новую добавленную переменную видимой. И, по соглашению, обычно это добавляется таким образом: PATH=$PATH:...нет PATH=...:$PATH. Таким образом, правильнее повторять в обратном порядке. Даже если ваш путь тоже сработает, люди присоединяются обратным путем.
Эрик Ван
23

Вот понятное однострочное решение, которое делает все правильно: удаляет дубликаты, сохраняет порядок путей и не добавляет двоеточие в конце. Поэтому он должен дать вам дедуплицированную переменную PATH, которая будет вести себя точно так же, как и оригинал:

PATH="$(perl -e 'print join(":", grep { not $seen{$_}++ } split(/:/, $ENV{PATH}))')"

Он просто разделяется на двоеточие ( split(/:/, $ENV{PATH})), использует использование grep { not $seen{$_}++ }для фильтрации любых повторяющихся экземпляров путей, за исключением первого вхождения, а затем объединяет оставшиеся вместе, разделенные двоеточиями, и печатает результат ( print join(":", ...)).

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

# Deduplicate path variables
get_var () {
    eval 'printf "%s\n" "${'"$1"'}"'
}
set_var () {
    eval "$1=\"\$2\""
}
dedup_pathvar () {
    pathvar_name="$1"
    pathvar_value="$(get_var "$pathvar_name")"
    deduped_path="$(perl -e 'print join(":",grep { not $seen{$_}++ } split(/:/, $ARGV[0]))' "$pathvar_value")"
    set_var "$pathvar_name" "$deduped_path"
}
dedup_pathvar PATH
dedup_pathvar MANPATH

Этот код дедуплицирует как PATH, так и MANPATH, и вы можете легко вызывать dedup_pathvarдругие переменные, которые содержат разделенные двоеточиями списки путей (например, PYTHONPATH).

Райан Томпсон
источник
По какой-то причине мне пришлось добавить, chompчтобы удалить завершающий перевод строки. Это сработало для меня:perl -ne 'chomp; print join(":", grep { !$seen{$_}++ } split(/:/))' <<<"$PATH"
Håkon Hægland
12

Вот гладкий:

printf %s "$PATH" | awk -v RS=: -v ORS=: '!arr[$0]++'

Дольше (чтобы увидеть, как это работает):

printf %s "$PATH" | awk -v RS=: -v ORS=: '{ if (!arr[$0]++) { print $0 } }'

Хорошо, так как вы новичок в Linux, вот как на самом деле установить PATH без завершающего ":"

PATH=`printf %s "$PATH" | awk -v RS=: '{ if (!arr[$0]++) {printf("%s%s",!ln++?"":":",$0)}}'`

Кстати, убедитесь, что в вашей переменной PATH нет каталогов, содержащих «:», иначе это может привести к путанице.

некоторые кредиты:

akostadinov
источник
-1 это не работает Я все еще вижу дубликаты на моем пути.
Dogbane
4
@ Dogbane: он удаляет дубликаты для меня. Однако это имеет тонкую проблему. Вывод имеет:: в конце, который, если он установлен как $ PATH, означает, что текущему каталогу добавляется путь. Это имеет последствия для безопасности на многопользовательском компьютере.
Camh
@dogbane, это работает , и я отредактированное сообщение иметь команду на одну строку без завершающего:
akostadinov
@dogbane у вашего решения есть трейлинг: в выводе
akostadinov
хм, ваша третья команда работает, но первые две не работают, если я не использую echo -n. Кажется, что ваши команды не работают с «здесь строками», например try:awk -v RS=: -v ORS=: '!arr[$0]++' <<< ".:/foo/bin:/bar/bin:/foo/bin"
dogbane
6

Вот один лайнер AWK.

$ PATH=$(printf %s "$PATH" \
     | awk -vRS=: -vORS= '!a[$0]++ {if (NR>1) printf(":"); printf("%s", $0) }' )

где:

  • printf %s "$PATH"печатает содержимое $PATHбез завершающей строки
  • RS=: изменяет символ разделителя входной записи (по умолчанию - новая строка)
  • ORS= изменяет разделитель выходной записи на пустую строку
  • a имя неявно созданного массива
  • $0 ссылается на текущую запись
  • a[$0] является ассоциативным массивом разыменования
  • ++ это оператор постинкрементного
  • !a[$0]++ защищает правую сторону, то есть он гарантирует, что текущая запись будет напечатана, только если она не была напечатана ранее
  • NR номер текущей записи, начиная с 1

Это означает, что AWK используется для разделения PATHсодержимого по :символам разделителя и для фильтрации повторяющихся записей без изменения порядка.

Поскольку ассоциативные массивы AWK реализованы в виде хеш-таблиц, время выполнения является линейным (т. Е. В O (n)).

Обратите внимание, что нам не нужно искать :символы в кавычках, потому что оболочки не предоставляют кавычки для поддержки каталогов с :именем в PATHпеременной.

Awk + паста

Вышесказанное можно упростить с помощью пасты:

$ PATH=$(printf %s "$PATH" | awk -vRS=: '!a[$0]++' | paste -s -d:)

Команда pasteиспользуется для разбивки выходных данных awk двоеточиями. Это упрощает действие awk для печати (действие по умолчанию).

питон

Так же, как Python двухлинейный:

$ PATH=$(python3 -c 'import os; from collections import OrderedDict; \
    l=os.environ["PATH"].split(":"); print(":".join(OrderedDict.fromkeys(l)))' )
maxschlepzig
источник
хорошо, но удаляет ли это дубликаты из существующей строки, разделенной двоеточиями, или предотвращает добавление дубликатов в строку?
Александр Миллс
1
выглядит как бывший
Александр Миллс
2
@AlexanderMills, ну, OP только что спросил об удалении дубликатов, так что это то, что делает вызов awk.
maxschlepzig
1
Команда pasteне работает для меня, если я не добавлю трейлинг -для использования STDIN.
wisbucky
2
Кроме того, мне нужно добавить пробелы после -vили я получаю сообщение об ошибке. -v RS=: -v ORS=, Просто разные вкусы awkсинтаксиса.
wisbucky
4

Там было подобное обсуждение по этому поводу здесь .

Я придерживаюсь немного другого подхода. Вместо того, чтобы просто принимать PATH, установленный из всех устанавливаемых файлов инициализации, я предпочитаю getconfсначала указывать системный путь и размещать его, затем добавить предпочтительный порядок путей, а затем использовать awkдля удаления любых дубликатов. Это может или не может действительно ускорить выполнение команд (и в теории быть более безопасным), но это дает мне теплые размышления.

# I am entering my preferred PATH order here because it gets set,
# appended, reset, appended again and ends up in such a jumbled order.
# The duplicates get removed, preserving my preferred order.
#
PATH=$(command -p getconf PATH):/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# Remove duplicates
PATH="$(printf "%s" "${PATH}" | /usr/bin/awk -v RS=: -v ORS=: '!($0 in a) {a[$0]; print}')"
export PATH

[~]$ echo $PATH
/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/usr/lib64/ccache:/usr/games:/home/me/bin
Джордж М
источник
3
Это очень опасно , потому что вы добавляете завершающую :к PATH(т.е. пустой строки ввода), потому что тогда текущий рабочий каталог является частью вашей PATH.
maxschlepzig
3

Пока мы добавляем не-awk oneliners:

PATH=$(zsh -fc "typeset -TU P=$PATH p; echo \$P")

(Может быть так просто, PATH=$(zsh -fc 'typeset -U path; echo $PATH')но zsh всегда читает хотя бы один zshenvфайл конфигурации, который можно изменить PATH.)

Он использует две приятные функции Zsh:

  • скаляры, привязанные к массивам ( typeset -T)
  • и массивы, которые автоматически удаляют дублирующиеся значения ( typeset -U).
Михал Политовски
источник
отлично! кратчайший рабочий ответ и изначально без двоеточия в конце.
Яап
2
PATH=`perl -e 'print join ":", grep {!$h{$_}++} split ":", $ENV{PATH}'`
export PATH

Это использует Perl и имеет несколько преимуществ:

  1. Удаляет дубликаты
  2. Сохраняет порядок сортировки
  3. Сохраняет самый ранний вид ( /usr/bin:/sbin:/usr/binприведет к /usr/bin:/sbin)
vol7ron
источник
2

Также sed(здесь используется sedсинтаксис GNU ) можно выполнить работу:

MYPATH=$(printf '%s\n' "$MYPATH" | sed ':b;s/:\([^:]*\)\(:.*\):\1/:\1\2/;tb')

этот работает хорошо только в том случае, если первый путь .похож на пример с кланом.

В общем случае вам нужно добавить еще одну sкоманду:

MYPATH=$(printf '%s\n' "$MYPATH" | sed ':b;s/:\([^:]*\)\(:.*\):\1/:\1\2/;tb;s/^\([^:]*\)\(:.*\):\1/:\1\2/')

Работает даже на такой конструкции:

$ echo "/bin:.:/foo/bar/bin:/usr/bin:/foo/bar/bin:/foo/bar/bin:/bar/bin:/usr/bin:/bin" \
| sed ':b;s/:\([^:]*\)\(:.*\):\1/:\1\2/;tb;s/^\([^:]*\)\(:.*\):\1/\1\2/'

/bin:.:/foo/bar/bin:/usr/bin:/bar/bin
порыв
источник
2

Как продемонстрировали другие, в одной строке можно использовать awk, sed, perl, zsh или bash, в зависимости от вашего допуска к длинным строкам и читабельности. Вот функция bash, которая

  • удаляет дубликаты
  • сохраняет порядок
  • разрешает пробелы в именах каталогов
  • позволяет указать разделитель (по умолчанию ':')
  • может использоваться с другими переменными, а не только с PATH
  • работает в версиях bash <4, важно, если вы используете OS X, которая для лицензирования не поставляется bash версии 4

функция Баш

remove_dups() {
    local D=${2:-:} path= dir=
    while IFS= read -d$D dir; do
        [[ $path$D =~ .*$D$dir$D.* ]] || path+="$D$dir"
    done <<< "$1$D"
    printf %s "${path#$D}"
}

использование

Удалить пупы из PATH

PATH=$(remove_dups "$PATH")
amdn
источник
1

Это моя версия:

path_no_dup () 
{ 
    local IFS=: p=();

    while read -r; do
        p+=("$REPLY");
    done < <(sort -u <(read -ra arr <<< "$1" && printf '%s\n' "${arr[@]}"));

    # Do whatever you like with "${p[*]}"
    echo "${p[*]}"
}

Использование: path_no_dup "$PATH"

Образец вывода:

rany$ v='a:a:a:b:b:b:c:c:c:a:a:a:b:c:a'; path_no_dup "$v"
a:b:c
rany$
Рани Албег Вайн
источник
1

В последних версиях bash (> = 4) также есть ассоциативные массивы, то есть вы можете использовать для этого bash 'one liner':

PATH=$(IFS=:; set -f; declare -A a; NR=0; for i in $PATH; do NR=$((NR+1)); \
       if [ \! ${a[$i]+_} ]; then if [ $NR -gt 1 ]; then echo -n ':'; fi; \
                                  echo -n $i; a[$i]=1; fi; done)

где:

  • IFS изменяет разделитель поля ввода на :
  • declare -A объявляет ассоциативный массив
  • ${a[$i]+_}является расширением параметра, означающим: _подставляется тогда и только тогда, когда a[$i]установлено. Это похоже на то, ${parameter:+word}что также проверяет на ненулевое значение. Таким образом, в следующей оценке условия выражение _(то есть строка из одного символа) оценивается как истинное (это эквивалентно -n _), а пустое выражение оценивается как ложное.
maxschlepzig
источник
+1: хороший стиль сценария, но можете ли вы объяснить конкретный синтаксис: ${a[$i]+_}отредактировав свой ответ и добавив одну маркировку? Остальное совершенно понятно, но ты потерял меня там. Спасибо.
Cbhihe
1
@Cbhihe, я добавил пункт, который касается этого расширения.
maxschlepzig
Большое спасибо. Очень интересно. Я не думал, что это было возможно с массивами (не строки) ...
Cbhihe
1
PATH=`awk -F: '{for (i=1;i<=NF;i++) { if ( !x[$i]++ ) printf("%s:",$i); }}' <<< "$PATH"`

Объяснение кода awk:

  1. Разделите ввод двоеточиями.
  2. Добавьте новые записи пути в ассоциативный массив для быстрого поиска дубликатов.
  3. Печатает ассоциативный массив.

Помимо краткости, этот однострочный быстрый: awk использует цепочку хеш-таблиц для достижения амортизированной производительности O (1).

на основе удаления повторяющихся записей $ PATH

Leftium
источник
Старый пост, но не могли бы вы объяснить: if ( !x[$i]++ ). Благодарю.
Cbhihe
0

Используйте awkдля разделения пути :, затем зациклите каждое поле и сохраните его в массиве. Если вы встретите поле, которое уже находится в массиве, это означает, что вы видели его раньше, поэтому не печатайте его.

Вот пример:

$ MYPATH=.:/foo/bar/bin:/usr/bin:/foo/bar/bin
$ awk -F: '{for(i=1;i<=NF;i++) if(!($i in arr)){arr[$i];printf s$i;s=":"}}' <<< "$MYPATH"
.:/foo/bar/bin:/usr/bin

(Обновлено, чтобы удалить трейлинг :.)

кендырь
источник
0

Решение - не такое элегантное, как те, которые изменяют переменные * RS, но, возможно, достаточно ясное:

PATH=`awk 'BEGIN {np="";split(ENVIRON["PATH"],p,":"); for(x=0;x<length(p);x++) {  pe=p[x]; if(e[pe] != "") continue; e[pe] = pe; if(np != "") np=np ":"; np=np pe}} END { print np }' /dev/null`

Вся программа работает в блоках BEGIN и END . Он извлекает переменную PATH из среды, разделяя ее на единицы. Затем он выполняет итерацию по результирующему массиву p (который создается по порядку split()). Массив e является ассоциативным массивом, который используется для определения того, видели ли мы текущий элемент пути (например, / usr / local / bin ) и, если нет, добавляется к np , с логикой для добавления двоеточия к нп, если в нп уже есть текст . Блок END просто повторяет np . Это может быть еще более упрощено путем добавления-F:флаг, исключая третий аргумент split()(по умолчанию это FS ), и изменяясь np = np ":"на np = np FS, давая нам:

awk -F: 'BEGIN {np="";split(ENVIRON["PATH"],p); for(x=0;x<length(p);x++) {  pe=p[x]; if(e[pe] != "") continue; e[pe] = pe; if(np != "") np=np FS; np=np pe}} END { print np }' /dev/null

Наивно, я полагал, что for(element in array)это сохранит порядок, но это не так, поэтому мое оригинальное решение не сработает, так как люди расстроятся, если кто-то вдруг нарушит порядок их $PATH:

awk 'BEGIN {np="";split(ENVIRON["PATH"],p,":"); for(x in p) { pe=p[x]; if(e[pe] != "") continue; e[pe] = pe; if(np != "") np=np ":"; np=np pe}} END { print np }' /dev/null
Эндрю Билс
источник
0
export PATH=$(echo -n "$PATH" | awk -v RS=':' '(!a[$0]++){if(b++)printf(RS);printf($0)}')

Сохраняется только первое вхождение, а относительный порядок поддерживается.

Cyker
источник
-1

Я бы сделал это только с помощью основных инструментов, таких как tr, sort и uniq:

NEW_PATH=`echo $PATH | tr ':' '\n' | sort | uniq | tr '\n' ':'`

Если на вашем пути нет ничего особенного или странного, это должно сработать

ghm1014
источник
Кстати, вы можете использовать sort -uвместо sort | uniq.
пик
11
Поскольку порядок элементов PATH является значительным, это не очень полезно.
maxschlepzig