Видаліть повторювані записи $ 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)}}'`

btw переконайтесь, що у вашому PATH НЕ є каталоги, що містять ":", інакше це буде зіпсовано.

деякий кредит:


-1 це не працює. Я досі бачу дублікати на своєму шляху.
dogbane

4
@dogbane: Це видаляє дублікати для мене. Однак у нього є тонка проблема. Вихід має: в кінці, що якщо встановлено як ваш $ PATH, означає поточний каталог додається шлях. Це має наслідки для безпеки для багатокористувацької машини.
camh

@dogbane, це працює, і я відредагував пост, щоб мати команду з одним рядком без
затримки

@dogbane ваше рішення має кінцевий результат: у виході
akostadinov

хм, ваша третя команда працює, але перші дві не працюють, якщо я не використовую echo -n. Ваші команди, здається, не працюють із "тут рядками", наприклад, спробуйте: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)))' )

Гаразд, але чи це видаляє дупи з існуючої розмежованої колони двокрапки, чи це не дозволяє дупам додаватись до рядка?
Олександр Міллс

1
виглядає як колишній
Олександр Міллс

2
@AlexanderMills, ну, ОП просто запитав про видалення дублікатів, так це і робить awk call.
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

До тих пір, поки ми додаємо oneliners non-awk:

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

приємно! найкоротша робоча відповідь, і споконвічно без двокрапки в кінці.
jaap

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)

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

функція bash

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

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 _) - тоді як порожній вираз оцінюється як хибний.

+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


Старий пост, але НЕ могли б ви пояснити: 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 , з логікою додавання двокрапки до np, якщо в 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)}')

Зберігається лише перше виникнення, а відносний порядок добре підтримується.


-1

Я б це зробив лише за допомогою основних інструментів, таких як tr, sort та uniq:

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

Якщо на вашому шляху немає нічого особливого чи дивного, воно повинно працювати


btw, ви можете використовувати sort -uзамість sort | uniq.
пік

11
Оскільки порядок елементів PATH є значним, це не дуже корисно.
maxschlepzig
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.