ฉันพยายามเขียนฟังก์ชัน bash shell ที่จะอนุญาตให้ฉันลบสำเนาของไดเรกทอรีที่ซ้ำกันออกจากตัวแปรสภาพแวดล้อม PATH ของฉัน
ฉันถูกบอกว่าเป็นไปได้ที่จะบรรลุสิ่งนี้ด้วยคำสั่งหนึ่งบรรทัดโดยใช้awk
คำสั่ง แต่ฉันไม่สามารถหาวิธีที่จะทำได้ มีใครรู้บ้าง
ฉันพยายามเขียนฟังก์ชัน bash shell ที่จะอนุญาตให้ฉันลบสำเนาของไดเรกทอรีที่ซ้ำกันออกจากตัวแปรสภาพแวดล้อม PATH ของฉัน
ฉันถูกบอกว่าเป็นไปได้ที่จะบรรลุสิ่งนี้ด้วยคำสั่งหนึ่งบรรทัดโดยใช้awk
คำสั่ง แต่ฉันไม่สามารถหาวิธีที่จะทำได้ มีใครรู้บ้าง
คำตอบ:
หากคุณยังไม่มีรายการที่ซ้ำกันใน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=$PATH:x=b
x ใน PATH ดั้งเดิมอาจมีค่า a ดังนั้นเมื่อ iterate ตามลำดับดังนั้นค่าใหม่จะถูกละเว้น แต่เมื่อเรียงกลับกันใหม่ ค่าจะมีผล
PATH=x:$PATH
หากมูลค่าเพิ่มที่ได้รับควรจะไปก่อนที่มันจะได้รับการบันทึกเป็น
PATH=$PATH:...
PATH=...:$PATH
ดังนั้นจึงเหมาะสมกว่าที่จะวนคำสั่งกลับรายการ แม้ว่าวิธีการของคุณจะใช้งานได้ก็ตามผู้คนจะต่อท้ายด้วยวิธีย้อนกลับ
นี่คือเข้าใจวิธีการแก้ปัญหาหนึ่งซับที่ไม่ทุกสิ่งที่ถูกต้อง: เอารายการที่ซ้ำกันรักษาการสั่งซื้อของเส้นทางและไม่เพิ่มลำไส้ใหญ่ในตอนท้าย ดังนั้นจึงควรให้เส้นทางที่ซ้ำซ้อนกับคุณซึ่งให้พฤติกรรมเหมือนกับต้นฉบับ:
PATH="$(perl -e 'print join(":", grep { not $seen{$_}++ } split(/:/, $ENV{PATH}))')"
มันแยกบนโคลอน ( split(/:/, $ENV{PATH})
) ใช้grep { not $seen{$_}++ }
ในการกรองอินสแตนซ์ของเส้นทางที่ซ้ำ ๆ ยกเว้นการเกิดครั้งแรกจากนั้นรวมส่วนที่เหลือกลับมารวมกันโดยคั่นด้วยโคลอนและพิมพ์ผลลัพธ์ ( print join(":", ...)
)
หากคุณต้องการโครงสร้างเพิ่มเติมรอบ ๆ เช่นเดียวกับความสามารถในการขจัดความซ้ำซ้อนของตัวแปรอื่นเช่นกันลองตัวอย่างนี้ซึ่งฉันกำลังใช้ใน config ของฉัน:
# 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
เพื่อลบ newline ที่ต่อท้าย สิ่งนี้ใช้ได้กับฉัน:perl -ne 'chomp; print join(":", grep { !$seen{$_}++ } split(/:/))' <<<"$PATH"
นี่คือรูปแบบเพรียวบาง:
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 ตรวจสอบให้แน่ใจว่าไม่มีไดเรกทอรีที่มี ":" ในเส้นทางของคุณมิฉะนั้นจะถูกทำให้ยุ่งเหยิง
ให้เครดิตกับ:
echo -n
แต่สองคนแรกไม่ทำงานจนกว่าฉันจะใช้ ดูเหมือนว่าคำสั่งของคุณจะไม่ทำงานกับ "here strings" เช่นลอง:awk -v RS=: -v ORS=: '!arr[$0]++' <<< ".:/foo/bin:/bar/bin:/foo/bin"
นี่คือซับหนึ่ง AWK
$ PATH=$(printf %s "$PATH" \
| awk -vRS=: -vORS= '!a[$0]++ {if (NR>1) printf(":"); printf("%s", $0) }' )
ที่อยู่:
printf %s "$PATH"
พิมพ์เนื้อหาของ $PATH
โดยไม่ขึ้นบรรทัดใหม่RS=:
เปลี่ยนอักขระตัวคั่นเรคคอร์ดอินพุต (ค่าดีฟอลต์คือ newline)ORS=
เปลี่ยนตัวคั่นเร็กคอร์ดเอาต์พุตเป็นสตริงว่างa
ชื่อของอาร์เรย์ที่สร้างขึ้นโดยนัย$0
อ้างอิงระเบียนปัจจุบันa[$0]
คือการอ้างอิงอาเรย์แบบเชื่อมโยง++
เป็นตัวดำเนินการภายหลังการเพิ่ม!a[$0]++
ปกป้องด้านขวามือนั่นคือตรวจสอบให้แน่ใจว่ามีการพิมพ์ระเบียนปัจจุบันเท่านั้นหากไม่ได้พิมพ์มาก่อนNR
หมายเลขระเบียนปัจจุบันเริ่มต้นด้วย 1ซึ่งหมายความว่า AWK จะใช้ในการแบ่งPATH
เนื้อหาตาม:
ตัวคั่นและเพื่อกรองรายการที่ซ้ำกันโดยไม่ต้องแก้ไขคำสั่ง
เนื่องจากอาร์เรย์เชื่อมโยงของ AWK ถูกนำไปใช้งานเป็นตารางแฮชรันไทม์จึงเป็นแบบเส้นตรง (เช่นใน O (n))
โปรดทราบว่าเราไม่จำเป็นต้องมองหา:
ตัวละครที่ยกมาเพราะเปลือกหอยไม่ได้ให้การอ้างถึงการสนับสนุนไดเรกทอรีที่มี:
ชื่อในPATH
ตัวแปร
ข้างต้นสามารถทำให้ง่ายขึ้นด้วยการวาง:
$ 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)))' )
paste
คำสั่งไม่ทำงานสำหรับฉันจนกว่าฉันเพิ่มต่อท้าย-
จะใช้ STDIN
-v
อื่นหรือฉันได้รับข้อผิดพลาด -v RS=: -v ORS=
. awk
ไวยากรณ์ที่แตกต่างกันเพียงรสชาติ
ได้มีการอภิปรายที่คล้ายกันเกี่ยวกับเรื่องนี้ที่นี่
ฉันใช้แนวทางที่ต่างออกไปเล็กน้อย แทนที่จะยอมรับเส้นทางที่ตั้งจากไฟล์เริ่มต้นที่แตกต่างกันทั้งหมดที่ได้รับการติดตั้งฉันชอบใช้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
:
ไปPATH
(เช่นรายการสตริงที่ว่างเปล่า) PATH
แล้วเพราะไดเรกทอรีการทำงานปัจจุบันเป็นส่วนหนึ่งของคุณ
ตราบใดที่เราเพิ่มผู้บุกรุกที่ไม่ใช่ 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
)PATH=`perl -e 'print join ":", grep {!$h{$_}++} split ":", $ENV{PATH}'`
export PATH
สิ่งนี้ใช้ Perl และมีประโยชน์หลายประการ:
/usr/bin:/sbin:/usr/bin
จะส่งผลให้/usr/bin:/sbin
)นอกจากนี้sed
(ที่นี่ใช้sed
ไวยากรณ์GNU ) สามารถทำงาน:
MYPATH=$(printf '%s\n' "$MYPATH" | sed ':b;s/:\([^:]*\)\(:.*\):\1/:\1\2/;tb')
อันนี้ใช้ได้ดีเฉพาะในกรณีที่เส้นทางแรกเป็น.
เหมือนตัวอย่างของ dogbane
ในกรณีทั่วไปคุณต้องเพิ่ม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
ในขณะที่คนอื่น ๆ แสดงให้เห็นว่ามันเป็นไปได้ในหนึ่งบรรทัดโดยใช้ awk, sed, perl, zsh หรือ 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}"
}
การใช้
วิธีลบ dups ออกจาก PATH
PATH=$(remove_dups "$PATH")
นี่คือรุ่นของฉัน:
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$
เวอร์ชันทุบตีล่าสุด (> = 4) รวมถึงอาร์เรย์ที่เชื่อมโยงกันนั่นคือคุณสามารถใช้ 'หนึ่งซับ' สำหรับทุบตีได้:
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 _
) - ในขณะที่นิพจน์ว่างจะประเมินว่าเป็นเท็จ${a[$i]+_}
โดยแก้ไขคำตอบและเพิ่มหัวข้อย่อย ที่เหลือเป็นที่เข้าใจได้อย่างสมบูรณ์แบบ แต่คุณทำให้ฉันไปที่นั่น ขอขอบคุณ.
PATH=`awk -F: '{for (i=1;i<=NF;i++) { if ( !x[$i]++ ) printf("%s:",$i); }}' <<< "$PATH"`
คำอธิบายของรหัส awk:
นอกเหนือจากความรัดกุมแล้วสายการบินหนึ่งยังเร็ว: awk ใช้แฮชตารางเพื่อให้ได้ประสิทธิภาพ O (1) ที่ตัดจำหน่ายแล้ว
ขึ้นอยู่กับการลบรายการ $ PATH ที่ซ้ำกัน
if ( !x[$i]++ )
ได้ไหม. ขอบคุณ
ใช้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
(อัปเดตเพื่อลบส่วนท้าย:
)
วิธีแก้ปัญหา - ไม่ใช่แบบที่สวยงามเทียบเท่ากับที่เปลี่ยนแปลงตัวแปร * 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บล็อกเพียง Echos 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
export PATH=$(echo -n "$PATH" | awk -v RS=':' '(!a[$0]++){if(b++)printf(RS);printf($0)}')
เฉพาะเหตุการณ์แรกเท่านั้นที่จะถูกเก็บไว้และลำดับที่สัมพันธ์กันจะได้รับการดูแลอย่างดี
ฉันจะทำมันด้วยเครื่องมือพื้นฐานเช่น tr, sort และ uniq:
NEW_PATH=`echo $PATH | tr ':' '\n' | sort | uniq | tr '\n' ':'`
หากไม่มีอะไรพิเศษหรือแปลกในเส้นทางของคุณมันควรจะทำงาน
sort -u
sort | uniq