najkrótszy sposób zamiany znaków w zmiennej

18

Istnieje wiele sposobów zamiany znaków w zmiennej.

Najkrótsza droga, jaką się dowiedziałem, to tr:

OUTPUT=a\'b\"c\`d_123and_a_lot_more
OUTPUT=$(echo "$OUTPUT"|tr -d "'\`\"")
echo $OUTPUT

Czy jest szybszy sposób? I jest to bezpieczne dla cytowanie cytatów jak ', "i `sama?

rubo77
źródło
Myślę, że możesz dalej używać tr. PEH BASH jest dobry, ale w tym przypadku tr jest znacznie szybszy. np. echo "$OUTPUT" | tr -dc '[[:alpha:]]' ponieważ chcesz mieć tylko alfanumeryczne
Valentin Bajrami
2
Ponieważ jesteś zainteresowany rozwagą: Zawsze podawaj swoje zmienne! echo "$OUTPUT". Albo lepiej: printf "%s\n" "$OUTPUT". (Co się stanie, kiedy OUTPUT="-n"?)
Musiphil
Możesz również rozważyć przeglądanie codegolfa , szczególnie wskazówki bash .
hoosierEE

Odpowiedzi:

22

Zobaczmy. Najkrótsze, jakie mogę wymyślić, to ulepszenie twojego trrozwiązania:

OUTPUT="$(tr -d "\"\`'" <<<$OUTPUT)"

Inne alternatywy obejmują wspomniane już podstawienie zmiennych, które może być krótsze niż dotychczas pokazane:

OUTPUT="${OUTPUT//[\'\"\`]}"

I sedoczywiście jest to dłuższe pod względem postaci:

OUTPUT="$(sed s/[\'\"\`]//g <<<$OUTPUT)"

Nie jestem pewien, czy masz na myśli najkrótszą długość lub czas. Jeśli chodzi o długość, te dwa są tak krótkie, jak to możliwe (lub tak czy inaczej mogę je zdobyć), jeśli chodzi o usuwanie tych konkretnych postaci. Który jest najszybszy? Przetestowałem, ustawiając OUTPUTzmienną na wartość z przykładu, ale powtórzyłem kilkadziesiąt razy:

$ echo ${#OUTPUT} 
4900

$ time tr -d "\"\`'" <<<$OUTPUT
real    0m0.002s
user    0m0.004s
sys     0m0.000s
$ time sed s/[\'\"\`]//g <<<$OUTPUT
real    0m0.005s
user    0m0.000s
sys     0m0.000s
$ time echo ${OUTPUT//[\'\"\`]}
real    0m0.027s
user    0m0.028s
sys     0m0.000s

Jak widać, trjest zdecydowanie najszybszy, a tuż za nim sed. Wygląda na to, że używanie echojest nieco szybsze niż używanie <<<:

$ for i in {1..10}; do 
    ( time echo $OUTPUT | tr -d "\"\`'" > /dev/null ) 2>&1
done | grep -oP 'real.*m\K[\d.]+' | awk '{k+=$1;} END{print k/NR}'; 
0.0025
$ for i in {1..10}; do 
    ( time tr -d "\"\`'" <<<$OUTPUT > /dev/null ) 2>&1 
  done | grep -oP 'real.*m\K[\d.]+' | awk '{k+=$1;} END{print k/NR}'; 
0.0029

Ponieważ różnica jest niewielka, powyższe testy przeprowadziłem 10 razy dla każdego z nich i okazuje się, że najszybszy jest rzeczywiście ten, od którego musiałeś zacząć:

echo $OUTPUT | tr -d "\"\`'" 

Zmienia się to jednak, gdy weźmie się pod uwagę narzut związany z przypisywaniem zmiennej, tutaj użycie trjest nieco wolniejsze niż zwykła zamiana:

$ for i in {1..10}; do
    ( time OUTPUT=${OUTPUT//[\'\"\`]} ) 2>&1
  done | grep -oP 'real.*m\K[\d.]+' | awk '{k+=$1;} END{print k/NR}'; 
0.0032

$ for i in {1..10}; do
    ( time OUTPUT=$(echo $OUTPUT | tr -d "\"\`'")) 2>&1
  done | grep -oP 'real.*m\K[\d.]+' | awk '{k+=$1;} END{print k/NR}'; 
0.0044

Podsumowując, gdy chcesz po prostu wyświetlić wyniki, użyj, trale jeśli chcesz ponownie przypisać do zmiennej, korzystanie z funkcji manipulacji ciągiem powłoki jest szybsze, ponieważ pozwalają uniknąć nakładania się na osobną podpowłokę.

terdon
źródło
4
Ponieważ OP jest zainteresowany przywróceniem zmodyfikowanej wartości OUTPUT, będziesz musiał wziąć pod uwagę narzuty zastępcze poleceń związane z trsed
podstawami
@ 1_CR tak, ale ponieważ tak będzie w przypadku dowolnej metody, uznałem, że nie ma to znaczenia.
terdon
1
Niezupełnie, OUTPUT="${OUTPUT//[`\"\']/}" nie obejmuje zastępowania poleceń
iruvar
@ 1_CR ah, rozumiem, masz rację i to zmienia wynik. Dziękuję, edytowano odpowiedź.
terdon
2
Metody polegające na podstawianiu poleceń mają wadę polegającą na tym, że nieco zakłócają łańcuch. (Można tego uniknąć, ale kosztem uczynienia polecenia znacznie bardziej złożonym.) W szczególności podstawienie polecenia usuwa końcowe znaki nowej linii.
Gilles „SO- przestań być zły”
15

Możesz użyć podstawienia zmiennej :

$ OUTPUT=a\'b\"c\`d
$ echo "$OUTPUT"
a'b"c`d

Użyj tej składni: ${parameter//pattern/string}aby zastąpić wszystkie wystąpienia wzorca łańcuchem.

$ echo "${OUTPUT//\'/x}"
axb"c`d
$ echo "${OUTPUT//\"/x}"
a'bxc`d
$ echo "${OUTPUT//\`/x}"
a'b"cxd
$ echo "${OUTPUT//[\'\"\`]/x}"
axbxcxd
chaos
źródło
@ rubo77 echo ${OUTPUT//[`\"\']/x}dajeaxbxcxa
chaos
Niepoprawne jest nazywanie rozszerzenia „zmiennym rozszerzeniem”. Nazywa się to „rozszerzaniem parametrów”.
gena2x
@ gena2x - Nie rozumiem, co oznacza twój komentarz tutaj?
slm
12

W bash lub zsh jest to:

OUTPUT="${OUTPUT//[\`\"\']/}"

Zauważ, że ${VAR//PATTERN/} usuwa wszystkie wystąpienia wzorca. Aby uzyskać więcej informacji , rozszerzenie parametru bash

To rozwiązanie powinno być najszybsze w przypadku krótkich ciągów, ponieważ nie wymaga uruchamiania żadnych programów zewnętrznych. Jednak w przypadku bardzo długich ciągów jest odwrotnie - lepiej jest używać dedykowanego narzędzia do operacji tekstowych, na przykład:

$ OUTPUT="$(cat /usr/src/linux/.config)"

$ time (echo $OUTPUT | OUTPUT="${OUTPUT//set/abc}")
real    0m1.766s
user    0m1.681s
sys     0m0.002s

$ time (echo $OUTPUT | sed s/set/abc/g >/dev/null)
real    0m0.094s
user    0m0.078s
sys     0m0.006s
gena2x
źródło
1
W rzeczywistości trjest szybszy. Regeksy i globusy są drogie i chociaż nie ma tutaj zewnętrznego programu, bash zawsze będzie wolniejszy niż coś w tym rodzaju tr.
terdon
To bardzo zależy od danych wejściowych i implementacji wyrażenia regularnego. W swojej odpowiedzi wziąłeś konkretny duży zestaw danych - ale zestaw danych może być mały. Lub inny. Ponadto nie mierzysz czasu wyrażenia regularnego, ale czas echa, więc nie jestem pewien, czy twoje porównanie jest naprawdę uczciwe.
gena2x
Słuszne uwagi. Nie można jednak twierdzić o prędkości bez testowania. W rzeczywistości przy przypisywaniu do zmiennej wydaje się to szybsze, ale podczas drukowania na ekranie trwygrywa (zobacz moją odpowiedź). Zgadzam się, że będzie to zależeć od wielu czynników, ale właśnie dlatego nie można stwierdzić, który z nich wygra, nie testując go.
terdon
6

Jeśli przypadkowo próbujesz poradzić sobie z cytatami dotyczącymi ponownego użycia powłoki, możesz to zrobić bez usuwania ich, a to również jest bardzo proste:

aq() { sh -c 'for a do
       alias "$((i=$i+1))=$a"
       done; alias' -- "$@"
}

Ta funkcja powłoki cytuje każdą tablicę arg, którą jej podajesz, i zwiększa jej wynik na iterowalny argument.

Oto kilka argumentów:

aq \
"here's an
ugly one" \
"this one is \$PATHpretty bad, too" \
'this one```****```; totally sucks'

WYNIK

1='here'"'"'s an
ugly one'
2='this one is $PATHpretty bad, too'
3='this one```****```; totally sucks'

To wyjście, z dashktórego typowo bezpieczne cytaty zawierają pojedyncze cytaty '"'"'.bashzrobiłby '\''.

Zastąpienie wyboru pojedynczych bajtów niepustych białych znakami o wartości innej niż null innym pojedynczym bajtem można prawdopodobnie zrobić najszybciej w dowolnej powłoce POSIX za pomocą $IFSi $*.

set -f; IFS=\"\'\`; set -- $var; printf %s "$*"

WYNIK

"some ""crazy """"""""string ""here

Tam właśnie printfto widzę, ale oczywiście, gdybym to zrobił:

var="$*"

... zamiast printfpolecenia$var „s wartość byłaby co widać w tam wyjście.

Kiedy set -finstruuję powłokę, aby nie globowała - w przypadku gdy łańcuch zawiera znaki, które można by interpretować jako wzorce globu. Robię to, ponieważ parser powłok rozszerza wzorce globu po dokonaniu podziału pól na zmienne. globbing może być ponownie włączony jak set +f. Ogólnie rzecz biorąc - w skryptach - przydatne jest ustawienie huku w następujący sposób:

#!/usr/bin/sh -f

A następnie, aby jawnie włączyć globowanie z set +fdowolną linią, której bym tego chciał.

Podział pola następuje na podstawie znaków w $IFS.

Istnieją dwa rodzaje $IFSwartości - $IFSbiałe znaki i $IFSinne znaki. $IFSspacje (spacja, tabulator, nowa linia) pola rozdzielane są określane tak, aby następowały po nich sekwencje do pojedynczego pola (lub wcale, jeśli nie poprzedzają czegoś innego) - więc ...

IFS=\ ; var='      '; printf '<%s>' $var
<>

Ale wszystkie inne są określone, aby oceniać do jednego pola na wystąpienie - nie są obcinane.

IFS=/; var='/////'; printf '<%s>' $var
<><><><><>

Wszystkie rozszerzenia zmiennych są domyślnie $IFSograniczonymi tablicami danych - są one podzielone na osobne pola zgodnie z $IFS. Kiedy ty" cytujesz jedną, zastępujesz tę właściwość tablicy i oceniasz ją jako pojedynczy ciąg.

Więc kiedy to zrobię ...

IFS=\"\'\`; set -- $var

Ustawiam tablicę argumentów powłoki na wiele $IFSrozdzielanych pól generowanych przez $varrozszerzenie. Po rozwinięciu jego wartości składowe dla zawartych w nim znaków $IFStracone - są one teraz tylko separatorami pól - są\0NUL .

"$*"- podobnie jak inne podwójnie cytowane rozwinięcia zmiennych - również zastępuje właściwości podziału pola na $IFS. Ale dodatkowo zastępuje pierwszy bajt w $IFS każdym rozdzielanym polu w "$@". Tak, ponieważ "był pierwszy wartość $IFS wszystkich kolejnych ograniczniki stać "w "$*". I "nie trzeba też być $IFSprzy podziale. Można zmieniać $IFS po set -- $args innej wartości w całości i jego nowy pierwszy bajt by następnie pokazać się na ograniczniki polowych w "$*". Co więcej, możesz całkowicie usunąć wszystkie ich ślady:

set -- $var; IFS=; printf %s "$*"

WYNIK

some crazy string here
mikeserv
źródło
Bardzo fajnie, +1. Zastanawiam się, czy to rzeczywiście jest szybsze. Czy mógłbyś dodać jakieś testy czasowe, porównując je z podejściami z mojej odpowiedzi? Oczekuję, że twój będzie szybszy, ale chciałbym zobaczyć.
terdon
@terdon - to zależy od powłoki. To prawie na pewno szybciej niż trw jakiejkolwiek powłoki, ale różnica jest niepewna w bashdla ${var//$c/$newc/}sprawy. Spodziewam się, że nawet w takim przypadku będzie to nieco szybsze, ale zwykle nie przejmuję się tym, ponieważ do tych rzeczy zawsze używam dash- co jest szybsze o rząd wielkości pod każdym względem. Trudno to porównać.
mikeserv
@terdon - próbowałem. Ale - nawet w bashdziałaniu - time (IFS=\"\'`; set -- $var; printf %s "$*")i time (var=${var//\'`/\"/})oba dają 0.0000swyniki dla wszystkich pól. Czy robię coś złego, myślisz? Tam powinien być odwrotny ukośnik przed odwrotnym cytatem, ale nie wiem, jak wstawić odwrotny cytat w polu kodu komentarza.
mikeserv