Impostazione di IFS per una singola istruzione


42

So che è possibile impostare un valore IFS personalizzato per l'ambito di un singolo comando / incorporato. C'è un modo per impostare un valore IFS personalizzato per una singola istruzione ?? Apparentemente no, dato che in base al sotto il valore IFS globale è influenzato quando questo viene tentato

#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

Risposte:


39

In alcune shell (incluso bash):

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

(con bash, è possibile omettere l' commandemulazione if / if di sh / POSIX). Ma attenzione che quando si utilizzano variabili non quotate, in genere è anche necessario set -f, e nella maggior parte delle shell non esiste un ambito locale per questo.

Con zsh, puoi fare:

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

$=PATHè forzare la suddivisione delle parole che non viene eseguita per impostazione predefinita in zsh(non si fa neppure il gomito sull'espansione variabile, quindi non è necessario set -fse non in emulazione).

(){...}(o function {...}) sono chiamate funzioni anonime e in genere vengono utilizzate per impostare un ambito locale. con altre shell che supportano l'ambito locale nelle funzioni, potresti fare qualcosa di simile con:

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

Per implementare un ambito locale per variabili e opzioni nelle shell POSIX, è anche possibile utilizzare le funzioni fornite su https://github.com/stephane-chazelas/misc-scripts/blob/master/locvar.sh . Quindi puoi usarlo come:

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

(a proposito, non è valido dividere in $PATHquesto modo sopra tranne che zshcome in altre shell, IFS è delimitatore di campo, non separatore di campo).

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

Sono solo due incarichi, uno dopo l'altro, proprio come a=1 b=2.

Una nota di spiegazione su var=value cmd:

Nel:

var=value cmd arg

L'esecuzione della shell /path/to/cmdin un nuovo processo e passaggi cmde argin argv[]e var=valuein envp[]. Non si tratta in realtà di un'assegnazione di variabili, ma più di passare le variabili d'ambiente al comando eseguito . Nella shell Bourne o Korn, con set -k, puoi persino scriverlo cmd var=value arg.

Ora, ciò non si applica ai builtin o alle funzioni che non vengono eseguite . Nella shell Bourne, in var=value some-builtin, varfinisce per essere impostato in seguito, proprio come da var=valuesolo. Ciò significa ad esempio che il comportamento di var=value echo foo(che non è utile) varia a seconda che echosia incorporato o meno.

POSIX e / o kshcambiato quello in quanto quel comportamento Bourne si verifica solo per una categoria di builtin chiamati builtin speciali . evalè un builtin speciale, readnon lo è. Per builtin non speciale, var=value builtinimposta varsolo per l'esecuzione del builtin che lo fa comportare in modo simile a quando viene eseguito un comando esterno.

Il commandcomando può essere usato per rimuovere l' attributo speciale di quei builtin speciali . Ciò che POSIX ha trascurato però è che per evale i .builtin, ciò significherebbe che le shell dovrebbero implementare uno stack variabile (anche se non specifica i comandi di limitazione dell'ambito localo typeset), perché potresti fare:

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

O anche:

a=1 command eval myfunction

con myfunctionessere una funzione che utilizza o imposta $ae potenzialmente chiama command eval.

Questo è stato davvero un aspetto negativo perché ksh(su cui si basa principalmente la specifica) non lo ha implementato (e AT&T kshe zshancora non lo fanno), ma oggigiorno, tranne quei due, la maggior parte delle shell lo implementano. Il comportamento varia tra le conchiglie anche se in cose come:

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

anche se. L'utilizzo localsu shell che lo supportano è un modo più affidabile per implementare l'ambito locale.


Stranamente, IFS=: command eval …imposta IFSsolo per la durata di eval, come richiesto da POSIX, in trattino, pdksh e bash, ma non in ksh 93u. È insolito vedere ksh come il dispari-non conforme-one-out.
Gilles 'SO- smetti di essere malvagio' il

12

Salvataggio e ripristino standard presi da "The Unix Programming Environment" di Kernighan e Pike:

#!/bin/sh
old_IFS=$IFS
IFS="something_new"
some_program_or_builtin
IFS=${old_IFS}

2
grazie e +1. Sì, sono a conoscenza di questa opzione, ma vorrei sapere se esiste un'opzione "più pulita" se sai cosa intendo
iruvar,

Potresti incepparlo su una riga con punti e virgola, ma non penso che sia più pulito. Potrebbe essere bello se tutto ciò che volevi esprimere avesse un supporto sintattico speciale, ma probabilmente dovremmo imparare carpenteria o sumptin invece di scrivere codice;)
msw,

9
Che non riesce a ripristinare $IFScorrettamente se precedentemente non era impostato.
Stéphane Chazelas,

2
Se non è impostato, Bash lo considera come $'\t\n'' ', come spiegato qui: wiki.bash-hackers.org/syntax/expansion/…
davide

2
@davide, sarebbe $' \t\n'. lo spazio deve essere il primo come viene utilizzato "$*". Si noti che è lo stesso in tutte le shell tipo Bourne.
Stéphane Chazelas,

8

Inserisci il tuo script in una funzione e invoca quella funzione passando gli argomenti della riga di comando. Poiché l'IFS è definito locale, le modifiche apportate non influiscono sull'IFS globale.

main() {
  local IFS='/'

  # the rest goes here
}

main "$@"

6

Per questo comando:

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

Esiste una soluzione alternativa: dare al primo compito ( IFS=$'\n') un comando da eseguire (una funzione):

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

Ciò consentirà all'IFS nell'ambiente di chiamare split, ma non verrà conservato nell'ambiente attuale.

Questo evita anche l'uso sempre rischioso di eval.


In ksh93 e mksh e bash e zsh in modalità POSIX, ciò lascia comunque $IFSimpostato su in $'\n'seguito, come richiesto da POSIX.
Stéphane Chazelas,

4

La risposta proposta da @helpermethod è sicuramente un approccio interessante. Ma è anche un po 'una trappola perché in BASH l'ambito della variabile locale si estende dal chiamante alla funzione chiamata. Pertanto, impostando IFS in main (), il valore persisterà nelle funzioni chiamate da main (). Ecco un esempio:

#!/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 "$@"

E l'output ...

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

Se IFS dichiarato in main () non fosse ancora nell'ambito in func (), l'array non sarebbe stato analizzato correttamente in func () B. Scomponi la prima riga in func () e otterrai questo output:

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

Qual è ciò che dovresti ottenere se IFS fosse uscito dal campo di applicazione.

Una soluzione di gran lunga migliore IMHO, è quella di rinunciare a cambiare o fare affidamento su IFS a livello globale / locale. Invece, genera una nuova shell e giocherella con IFS. Ad esempio, se dovessi chiamare func () in main () come segue, passando l'array come stringa con un separatore del campo di barra all'indietro:

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

... la modifica a IFS non si rifletterà in func (). L'array verrà passato come una stringa:

ick\blick\flick

... ma all'interno di func () l'IFS sarà comunque "/" (come impostato in main ()) a meno che non sia cambiato localmente in func ().

Ulteriori informazioni sull'isolamento delle modifiche all'IFS sono disponibili ai seguenti collegamenti:

Come faccio a convertire una variabile di array bash in una stringa delimitata da newline?

Bash stringa per array con IFS

Suggerimenti e consigli per la programmazione generale di script di shell - Vedi "NOTA l'uso di sub-shell ..."


davvero interessante ...
Iruvar,

"Bash string to array with IFS" IFS=$'\n' declare -a astr=(...)perfetto grazie!
Aquarius Power il

1

Questo frammento della domanda:

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

viene interpretato come due assegnazioni di variabili globali separate valutate da sinistra a destra ed equivale a:

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

o

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

Questo spiega sia il motivo per cui il globale è IFSstato modificato, sia il motivo per cui la suddivisione delle parole $strin elementi dell'array è stata eseguita utilizzando il nuovo valore di IFS.

Potresti essere tentato di utilizzare una subshell per limitare l'effetto della IFSmodifica in questo modo:

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]}'"

ma noterai rapidamente che la modifica di aè limitata anche alla subshell:

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

Successivamente, verrai tentato di salvare / ripristinare IFS utilizzando la soluzione di questa risposta precedente di @msw o di provare a utilizzare local IFSuna funzione interna come suggerito da @helpermethod. Ma molto presto, ti accorgi di essere nei guai di ogni genere, specialmente se sei un autore di biblioteche che deve essere robusto contro il comportamento errato degli script invocanti:

  • E se IFSinizialmente non fosse stato impostato?
  • E se stiamo correndo con set -u(aka set -o nounset)?
  • E se IFSfosse stato reso di sola lettura tramite declare -r IFS?
  • Cosa succede se ho bisogno del meccanismo di salvataggio / ripristino per funzionare con ricorsione e / o esecuzione asincrona (come un trapgestore)?

Si prega di non salvare / ripristinare IFS. Invece, attenersi alle modifiche temporanee:

  • Per limitare la modifica della variabile a un singolo comando, chiamata incorporata o funzione, utilizzare IFS="value" command.

    • Per leggere in più variabili suddividendo un carattere specifico ( :utilizzato di seguito come esempio), utilizzare:

      IFS=":" read -r var1 var2 <<< "$str"
    • Per leggere in un array usa (fai questo invece di array_var=( $str )):

      IFS=":" read -r -a array_var <<< "$str"
  • Limitare gli effetti della modifica della variabile a una subshell.

    • Per generare gli elementi di un array separati da virgola:

      (IFS=","; echo "${array[*]}")
    • Per catturarlo in una stringa:

      csv="$(IFS=","; echo "${array[*]}")"

0

La soluzione più semplice è quella di prendere una copia dell'originale $IFS, come ad esempio la risposta di msw. Tuttavia, questa soluzione non distingue tra un set IFSe un IFSset pari alla stringa vuota, che è importante per molte applicazioni. Ecco una soluzione più generale che cattura questa distinzione:

# 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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.