Ustawienie IFS dla pojedynczej instrukcji

42

Wiem, że niestandardową wartość IFS można ustawić dla zakresu pojedynczego polecenia / wbudowanego. Czy istnieje sposób na ustawienie niestandardowej wartości IFS dla pojedynczej instrukcji? Najwyraźniej nie, ponieważ na podstawie poniższej próby wpływa to na globalną wartość IFS

#check environment IFS value, it is space-tab-newline
printf "%s" "$IFS" | od -bc
0000000 040 011 012
             \t  \n
0000003
#invoke built-in with custom IFS
IFS=$'\n' read -r -d '' -a arr <<< "$str"
#environment IFS value remains unchanged as seen below
printf "%s" "$IFS" | od -bc
0000000 040 011 012
             \t  \n
0000003

#now attempt to set IFS for a single statement
IFS=$'\n' a=($str)
#BUT environment IFS value is overwritten as seen below
printf "%s" "$IFS" | od -bc
0000000 012
         \n
     0000001
iruvar
źródło

Odpowiedzi:

39

W niektórych powłokach (w tym bash):

IFS=: command eval 'p=($PATH)'

(za pomocą bashmożna pominąć commandemulację sh / POSIX). Ale uważaj, że gdy używasz zmiennych niecytowanych, to również na ogół musisz set -f, i nie ma takiego zasięgu lokalnego w większości powłok.

Zsh możesz zrobić:

(){ local IFS=:; p=($=PATH); }

$=PATHwymusza dzielenie słów, które nie jest domyślnie wykonywane w zsh(globowanie przy rozszerzaniu zmiennych również nie jest wykonywane, więc nie potrzebujesz, set -fchyba że w emulacji).

(){...}(lub function {...}) są nazywane funkcjami anonimowymi i zwykle są używane do ustawiania zasięgu lokalnego. z innymi powłokami, które obsługują lokalny zasięg funkcji, możesz zrobić coś podobnego z:

e() { eval "$@"; }
e 'local IFS=:; p=($PATH)'

Aby zaimplementować zasięg lokalny dla zmiennych i opcji w powłokach POSIX, możesz także użyć funkcji podanych na stronie https://github.com/stephane-chazelas/misc-scripts/blob/master/locvar.sh . Następnie możesz użyć go jako:

. /path/to/locvar.sh
var=3,2,2
call eval 'locvar IFS; locopt -f; IFS=,; set -- $var; a=$1 b=$2 c=$3'

(nawiasem mówiąc, niepoprawne jest dzielenie w $PATHten sposób powyżej, z wyjątkiem tego, zshże w innych powłokach IFS jest separatorem pól, a nie separatorem pól).

IFS=$'\n' a=($str)

To tylko dwa zadania, jedno po drugim, tak jak a=1 b=2.

Uwaga na temat var=value cmd:

W:

var=value cmd arg

Powłoka wykonuje się /path/to/cmdw nowym procesie i przechodzi do cmdoraz argdo argv[]i var=valuedo envp[]. To nie jest tak naprawdę przypisanie zmiennych, ale więcej przekazywania zmiennych środowiskowych do wykonywanego polecenia. W powłoce Bourne'a lub Korna set -kmożesz nawet napisać cmd var=value arg.

Teraz nie dotyczy to wbudowanych funkcji lub funkcji, które nie są wykonywane . W Bourne shell, w var=value some-builtin, varkończy się ustawiony jest potem, podobnie jak w var=valuespokoju. Oznacza to na przykład, że zachowanie var=value echo foo(które nie jest przydatne) różni się w zależności od tego, czy echojest wbudowane, czy nie.

POSIX i / lub kshzmieniło to, że zachowanie Bourne'a występuje tylko dla kategorii wbudowań zwanych wbudowanymi specjalnymi . evaljest specjalnym wbudowanym, readnie jest. W przypadku niespecjalnego wbudowanego, var=value builtinzestawy vartylko do wykonania wbudowanego, dzięki czemu zachowuje się podobnie jak podczas wykonywania zewnętrznego polecenia.

commandKomenda może być używany do usuwania szczególną cechę tych szczególnych poleceń wbudowanych . POSIX przeoczył jednak to, że dla wbudowanych evali ., oznaczałoby to, że powłoki musiałyby implementować stos zmiennych (nawet jeśli nie określa poleceń ograniczających zakres locallub typesetzakres), ponieważ można wykonać:

a=0; a=1 command eval 'a=2 command eval echo \$a; echo $a'; echo $a

Lub nawet:

a=1 command eval myfunction

z myfunctionfunkcją używającą lub ustawiającą $ai potencjalnie wywołującą command eval.

To było naprawdę przeoczenie, ponieważ ksh(na której opiera się głównie specyfikacja) nie wdrożyło go (i AT&T kshi zshnadal tego nie robi), ale obecnie, z wyjątkiem tych dwóch, większość powłok go implementuje. Zachowanie różni się w zależności od muszli, chociaż może wyglądać tak:

a=0; a=1 command eval a=2; echo "$a"

chociaż. Używanie localw powłokach, które obsługują, jest bardziej niezawodnym sposobem na wdrożenie zasięgu lokalnego.

Stéphane Chazelas
źródło
Co dziwne, IFS=: command eval …ustawia IFStylko na czas określony evalprzez POSIX w dash, pdksh i bash, ale nie w ksh 93u. To niezwykłe, że ksh jest dziwnie niezgodny.
Gilles „SO- przestań być zły”
12

Standardowe zapisywanie i przywracanie pochodzi z „The Unix Programming Environment” Kernighana i Pike'a:

#!/bin/sh
old_IFS=$IFS
IFS="something_new"
some_program_or_builtin
IFS=${old_IFS}
msw
źródło
2
dziękuję i +1. Tak, znam tę opcję, ale chciałbym wiedzieć, czy istnieje opcja „czystszego”, jeśli wiesz, co mam na myśli
iruvar 24.09.2013
Możesz zaciąć go w jednej linii średnikami, ale nie sądzę, żeby to było czystsze. Byłoby miło, gdyby wszystko, co chciałbyś wyrazić, miało specjalne wsparcie syntaktyczne, ale wtedy prawdopodobnie musielibyśmy nauczyć się stolarstwa lub sumptiny zamiast kodować;)
msw
9
To nie przywraca $IFSpoprawnie, jeśli wcześniej było rozbrojone.
Stéphane Chazelas
2
Jeśli jest to ustawione, Bash traktuje ją jako $'\t\n'' ', jak wyjaśniono tutaj: wiki.bash-hackers.org/syntax/expansion/...
Davide
2
@davide, to by było $' \t\n'. przestrzeń musi być pierwsza, ponieważ jest używana "$*". Zauważ, że jest tak samo we wszystkich pociskach podobnych do Bourne'a.
Stéphane Chazelas
8

Umieść skrypt w funkcji i wywołaj tę funkcję, przekazując mu argumenty wiersza poleceń. Ponieważ IFS jest zdefiniowany lokalnie, zmiany w nim nie wpływają na globalny IFS.

main() {
  local IFS='/'

  # the rest goes here
}

main "$@"
helpermethod
źródło
6

Dla tego polecenia:

IFS=$'\n' a=($str)

Istnieje alternatywne rozwiązanie: dać pierwszemu przypisaniu ( IFS=$'\n') polecenie do wykonania (funkcję):

$ split(){ a=( $str ); }
$ IFS=$'\n' split

Spowoduje to umieszczenie IFS w środowisku w celu wywołania podziału, ale nie zostanie zachowane w obecnym środowisku.

Pozwala to również uniknąć zawsze ryzykownego użycia eval.


źródło
W ksh93 i mksh, oraz bash i zsh w trybie POSIX, to wciąż pozostawia $IFSustawione na $'\n'później, zgodnie z wymaganiami POSIX.
Stéphane Chazelas
4

Proponowana odpowiedź z @helpermethod jest z pewnością interesującym podejściem. Ale to także trochę pułapka, ponieważ w zmiennej BASH zakres zmiennej lokalnej rozciąga się od wywołującego do wywoływanej funkcji. Dlatego ustawienie IFS w main () spowoduje utrwalenie tej wartości w funkcjach wywoływanych z main (). Oto przykład:

#!/usr/bin/env bash
#
func() {
  # local IFS='\'

  local args=${@}
  echo -n "$FUNCNAME A"
  for ((i=0; i<${#args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${args[$i]}"
  done
  echo

  local f_args=( $(echo "${args[0]}") )
  echo -n "$FUNCNAME B"
  for ((i=0; i<${#f_args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${f_args[$i]}  "
  done
  echo
}

main() {
  local IFS='/'

  # the rest goes here
  local args=${@}
  echo -n "$FUNCNAME A"
  for ((i=0; i<${#args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${args[$i]}"
  done
  echo

  local m_args=( $(echo "${args[0]}") )
  echo -n "$FUNCNAME B"
  for ((i=0; i<${#m_args[@]}; i++)); do
    printf "[%s]: %s" "${i}" "${m_args[$i]}  "
  done
  echo

  func "${m_args[*]}"
}

main "$@"

A wynik ...

main A[0]: ick/blick/flick
main B[0]: ick  [1]: blick  [2]: flick
func A[0]: ick/blick/flick
func B[0]: ick  [1]: blick  [2]: flick

Gdyby IFS zadeklarowany w main () nie był jeszcze objęty zakresem w func (), to tablica nie zostałaby poprawnie przeanalizowana w func () B. Odkomentuj pierwszy wiersz w func () i otrzymasz ten wynik:

main A[0]: ick/blick/flick
main B[0]: ick  [1]: blick  [2]: flick
func A[0]: ick/blick/flick
func B[0]: ick/blick/flick

To jest to, co powinieneś uzyskać, jeśli IFS wyszedł poza zakres.

O wiele lepszym rozwiązaniem IMHO jest rezygnacja ze zmiany lub polegania na IFS na poziomie globalnym / lokalnym. Zamiast tego spawnuj nową powłokę i baw się z IFS. Na przykład, jeśli wywołasz func () w main () w następujący sposób, przekazując tablicę jako ciąg znaków z separatorem pól ukośnika do tyłu:

func $(IFS='\'; echo "${m_args[*]}")

... ta zmiana na IFS nie zostanie odzwierciedlona w func (). Tablica zostanie przekazana jako ciąg:

ick\blick\flick

... ale wewnątrz func () IFS nadal będzie miał wartość „/” (jak ustawiono w main ()), chyba że zostanie zmieniony lokalnie w func ().

Więcej informacji na temat izolowania zmian w IFS można znaleźć pod następującymi linkami:

Jak przekonwertować zmienną tablicy bash na ciąg rozdzielany znakami nowej linii?

Bash ciąg do tablicy za pomocą IFS

Wskazówki i porady dotyczące ogólnego programowania skryptów powłoki - patrz „UWAGA użycia podpowłoki ...”

Markeissler
źródło
naprawdę interesujące ...
iruvar,
„Bash string to array with IFS” IFS=$'\n' declare -a astr=(...)idealne dzięki!
Aquarius Power
1

Ten fragment z pytania:

IFS=$'\n' a=($str)

jest interpretowany jako dwa oddzielne przypisania zmiennych globalnych ocenianych od lewej do prawej i jest równoważny z:

IFS=$'\n'; a=($str)

lub

IFS=$'\n'
a=($str)

To wyjaśnia zarówno, dlaczego zmieniono globalny IFS, jak i dlaczego podział słów $strna elementy tablicowe przeprowadzono przy użyciu nowej wartości IFS.

Możesz ulec pokusie użycia podpowłoki, aby ograniczyć efekt IFSmodyfikacji w następujący sposób:

str="value 0:value 1"
a=( old values )
( # Following code runs in a subshell
 IFS=":"
 a=($str)
 printf 'Subshell IFS: %q\n' "${IFS}"
 echo "Subshell: a[0]='${a[0]}' a[1]='${a[1]}'"
)
printf 'Parent IFS: %q\n' "${IFS}"
echo "Parent: a[0]='${a[0]}' a[1]='${a[1]}'"

ale szybko zauważysz, że modyfikacja ajest również ograniczona do podpowłoki:

Subshell IFS: :
Subshell: a[0]='value 0' a[1]='value 1'
Parent IFS: $' \t\n'
Parent: a[0]='old' a[1]='values'

Następnie miałbyś ochotę zapisać / przywrócić IFS, korzystając z rozwiązania z poprzedniej odpowiedzi @msw lub spróbować użyć funkcji local IFSwewnętrznej, jak sugeruje @helpermethod. Ale wkrótce zauważasz, że masz wiele kłopotów, zwłaszcza jeśli jesteś autorem biblioteki, który musi być odporny na źle działające skrypty wywołujące:

  • Co jeśli IFSpoczątkowo był rozbrojony?
  • Co jeśli działamy z set -u(aka set -o nounset)?
  • Co się stanie, jeśli IFSutworzono opcję tylko do odczytu declare -r IFS?
  • Co się stanie, jeśli będę potrzebować mechanizmu zapisu / przywracania do pracy z rekurencją i / lub wykonywaniem asynchronicznym (takim jak program trapobsługi)?

Proszę nie zapisywać / przywracać IFS. Zamiast tego trzymaj się tymczasowych modyfikacji:

  • Aby ograniczyć modyfikację zmiennej do pojedynczego polecenia, wbudowanego lub wywołania funkcji, użyj IFS="value" command.

    • Aby odczytać wiele zmiennych, dzieląc na określony znak ( :użyty poniżej jako przykład), użyj:

      IFS=":" read -r var1 var2 <<< "$str"
    • Aby odczytać tablicę, użyj (zrób to zamiast array_var=( $str )):

      IFS=":" read -r -a array_var <<< "$str"
  • Ogranicz efekty modyfikacji zmiennej do podpowłoki.

    • Aby wyprowadzić elementy tablicy oddzielone przecinkiem:

      (IFS=","; echo "${array[*]}")
    • Aby przechwycić to w ciągu:

      csv="$(IFS=","; echo "${array[*]}")"
sls
źródło
0

Najprostszym rozwiązaniem jest pobranie kopii oryginału $IFS, jak np. Odpowiedź msw. Jednak to rozwiązanie nie rozróżnia rozbrojenia IFSod IFSzestawu równego pustemu ciągowi, co jest ważne w wielu aplikacjach. Oto bardziej ogólne rozwiązanie, które ujmuje to rozróżnienie:

# Functions taking care of IFS
set_IFS(){
    if [ -z "${IFS+x}" ]; then
        IFS_ori="__unset__"
    else
        IFS_ori="$IFS"
    fi
    IFS="$1"
}
reset_IFS(){
    if [ "${IFS_ori}" == "__unset__" ]; then
        unset IFS
    else
        IFS="${IFS_ori}"
    fi
}

# Example of use
set_IFS "something_new"
some_program_or_builtin
reset_IFS
jmd_dk
źródło