Kann IFS (Internal Field Separator) als einzelnes Trennzeichen für mehrere aufeinanderfolgende Trennzeichen fungieren?


10

Durch das Parsen eines Arrays mithilfe von IFS mit nicht weißen Leerzeichenwerten werden leere Elemente erstellt.
Selbst das tr -sVerkleinern mehrerer Delims auf ein einziges Delim reicht nicht aus.
Ein Beispiel kann das Problem klarer erklären.
Gibt es eine Möglichkeit, "normale" Ergebnisse durch eine Optimierung von IFS zu erzielen (gibt es eine zugehörige Einstellung, um das Verhalten von IFS zu ändern? .... dh das gleiche Verhalten wie das Standard-Leerzeichen) IFS.

var=" abc  def   ghi    "
echo "============== IFS=<default>"
arr=($var)
for x in ${!arr[*]} ; do
   echo "# arr[$x] \"${arr[x]}\""
done
#
sfi="$IFS" ; IFS=':'
set -f # Disable file name generation (globbing)
       # (This  data won't "glob", but unless globbing     
       #  is actually needed, turn if off, because   
       #  unusual/unexpected combinations of data can glob!
       #  and they can do it in the most obscure ways...  
       #  With IFS, "you're not in Kansas any more! :)  
var=":abc::def:::ghi::::"
echo "============== IFS=$IFS"
arr=($var)
for x in ${!arr[*]} ; do
   echo "# arr[$x] \"${arr[x]}\""
done
echo "============== IFS=$IFS and tr"
arr=($(echo -n "$var"|tr -s "$IFS"))
for x in ${!arr[*]} ; do
   echo "# arr[$x] \"${arr[x]}\""
done
set +f     # enable globbing 
IFS="$sfi" # re-instate original IFS val
echo "============== IFS=<default>"

Hier ist die Ausgabe


============== IFS=<default>
# arr[0] "abc"
# arr[1] "def"
# arr[2] "ghi"
============== IFS=:
# arr[0] ""
# arr[1] "abc"
# arr[2] ""
# arr[3] "def"
# arr[4] ""
# arr[5] ""
# arr[6] "ghi"
# arr[7] ""
# arr[8] ""
# arr[9] ""
============== IFS=: and tr
# arr[0] ""
# arr[1] "abc"
# arr[2] "def"
# arr[3] "ghi"
============== IFS=<default>

Es gibt eine bessere (glaube ich) Antwort auf dieselbe Frage: stackoverflow.com/a/14789518/1765658
F. Hauri

Antworten:


3

Um mehrere (nicht durch Leerzeichen) aufeinanderfolgende Trennzeichen zu entfernen, können zwei (Zeichenfolge / Array) Parametererweiterungen verwendet werden. Der Trick besteht darin, die IFSVariable für die Array-Parametererweiterung auf die leere Zeichenfolge zu setzen .

Dies ist man bashunter Wortaufteilung dokumentiert :

Nicht zitierte implizite Nullargumente, die sich aus der Erweiterung von Parametern ohne Werte ergeben, werden entfernt.

(
set -f
str=':abc::def:::ghi::::'
IFS=':'
arr=(${str})
IFS=""
arr=(${arr[@]})

echo ${!arr[*]}

for ((i=0; i < ${#arr[@]}; i++)); do 
   echo "${i}: '${arr[${i}]}'"
done
)

Gut! Eine einfache und effektive Methode - ohne dass eine Bash-Schleife erforderlich ist und keine Utility-App aufgerufen werden muss - übrigens. Wie Sie "(kein Leerzeichen)" erwähnt haben , möchte ich der Klarheit halber darauf hinweisen, dass es mit jeder Kombination von Trennzeichen, einschließlich Leerzeichen, gut funktioniert.
Peter.O

In meinen Tests IFS=' 'verhält sich die Einstellung (dh ein Leerzeichen) genauso. Ich finde das weniger verwirrend als ein explizites Nullargument ("" oder '') von IFS.
Micha Wiedenmann

Das ist eine schreckliche Lösung, wenn Ihre Daten eingebettete Leerzeichen enthalten. Wenn Ihre Daten 'a bc' anstelle von 'abc' wären, würde IFS = "" 'a' in ein separates Element von 'bc' aufteilen.
Dejay Clayton

5

Von der bashManpage:

Jedes Zeichen in IFS, das kein IFS-Leerzeichen ist, sowie alle benachbarten IFS-Leerzeichen begrenzen ein Feld. Eine Folge von IFS-Leerzeichen wird ebenfalls als Trennzeichen behandelt.

Dies bedeutet, dass IFS-Leerzeichen (Leerzeichen, Tabulatoren und Zeilenumbrüche) nicht wie die anderen Trennzeichen behandelt werden. Wenn Sie mit einem alternativen Trennzeichen genau das gleiche Verhalten erzielen möchten, können Sie mit Hilfe von troder sed: Trennzeichen austauschen.

var=":abc::def:::ghi::::"
arr=($(echo -n $var | sed 's/ /%#%#%#%#%/g;s/:/ /g'))
for x in ${!arr[*]} ; do
   el=$(echo -n $arr | sed 's/%#%#%#%#%/ /g')
   echo "# arr[$x] \"$el\""
done

Das %#%#%#%#%Ding ist ein magischer Wert, um die möglichen Leerzeichen innerhalb der Felder zu ersetzen. Es wird erwartet, dass es "einzigartig" (oder sehr unzusammenhängend) ist. Wenn Sie sicher sind, dass auf den Feldern niemals Platz sein wird, lassen Sie diesen Teil einfach fallen.


@FussyS ... Danke (siehe Änderung in meiner Frage) ... Sie haben mir möglicherweise die Antwort auf meine beabsichtigte Frage gegeben .. und diese Antwort könnte (wahrscheinlich) "Es gibt keine Möglichkeit, IFS dazu zu bringen, sich in der zu verhalten Art und Weise, wie ich möchte "... Ich habe die trBeispiele beabsichtigt , um das Problem zu zeigen ... Ich möchte einen Systemaufruf vermeiden, also werde ich mir eine Bash-Option ansehen, die über die ${var##:}in meinem Kommentar zu Glen's Ansewer erwähnte hinausgeht. Ich werde eine
Weile

Diese Behandlung von IFSist in allen Bourne-Shells gleich, sie ist in POSIX angegeben .
Gilles 'SO - hör auf böse zu sein'

Seit mehr als vier Jahren habe ich diese Frage gestellt. Ich fand, dass die Antwort von @ nazad (veröffentlicht vor über einem Jahr) die einfachste Möglichkeit ist, IFS zu jonglieren, um ein Array mit einer beliebigen Anzahl und Kombination von IFSZeichen als Trennzeichenfolge zu erstellen . Meine Frage wurde am besten von beantwortet jon_d, aber die Antwort von @ nazad zeigt eine raffinierte Möglichkeit, sie IFSohne Schleifen und ohne Dienstprogramm-Apps zu verwenden.
Peter.O

2

Da bash IFS keine interne Möglichkeit bietet, aufeinanderfolgende Trennzeichen als ein einziges Trennzeichen zu behandeln (für Nicht-Leerzeichen-Trennzeichen), habe ich eine All-Bash-Version zusammengestellt (im Gegensatz zur Verwendung eines externen Aufrufs, z. B. tr, awk, sed )

Es kann IFS mit mehreren Zeichen verarbeiten.

Hier sind die Ergebnisse der Ausführungszeit zusammen mit ähnlichen Tests für die trund awkdie auf dieser Q / A-Seite gezeigten Optionen ... Die Tests basieren auf 10000 Iterationen, bei denen nur das Arrray erstellt wurde (ohne E / A) ...

pure bash     3.174s (28 char IFS)
call (awk) 0m32.210s  (1 char IFS) 
call (tr)  0m32.178s  (1 char IFS) 

Hier ist die Ausgabe

# dlm_str  = :.~!@#$%^&()_+-=`}{][ ";></,
# original = :abc:.. def:.~!@#$%^&()_+-=`}{][ ";></,'single*quote?'..123:
# unified  = :abc::::def::::::::::::::::::::::::::::'single*quote?'::123:
# max-w 2^ = ::::::::::::::::
# shrunk.. = :abc:def:'single*quote?':123:
# arr[0] "abc"
# arr[1] "def"
# arr[2] "'single*quote?'"
# arr[3] "123"

Hier ist das Skript

#!/bin/bash

# Note: This script modifies the source string. 
#       so work with a copy, if you need the original. 
# also: Use the name varG (Global) it's required by 'shrink_repeat_chars'
#
# NOTE: * asterisk      in IFS causes a regex(?) issue,     but  *  is ok in data. 
# NOTE: ? Question-mark in IFS causes a regex(?) issue,     but  ?  is ok in data. 
# NOTE: 0..9 digits     in IFS causes empty/wacky elements, but they're ok in data.
# NOTE: ' single quote  in IFS; don't know yet,             but  '  is ok in data.
# 
function shrink_repeat_chars () # A 'tr -s' analog
{
  # Shrink repeating occurrences of char
  #
  # $1: A string of delimiters which when consecutively repeated and are       
  #     considered as a shrinkable group. A example is: "   " whitespace delimiter.
  #
  # $varG  A global var which contains the string to be "shrunk".
  #
# echo "# dlm_str  = $1" 
# echo "# original = $varG" 
  dlms="$1"        # arg delimiter string
  dlm1=${dlms:0:1} # 1st delimiter char  
  dlmw=$dlm1       # work delimiter  
  # More than one delimiter char
  # ============================
  # When a delimiter contains more than one char.. ie (different byte` values),    
  # make all delimiter-chars in string $varG the same as the 1st delimiter char.
  ix=1;xx=${#dlms}; 
  while ((ix<xx)) ; do # Where more than one delim char, make all the same in varG  
    varG="${varG//${dlms:$ix:1}/$dlm1}"
    ix=$((ix+1))
  done
# echo "# unified  = $varG" 
  #
  # Binary shrink
  # =============
  # Find the longest required "power of 2' group needed for a binary shrink
  while [[ "$varG" =~ .*$dlmw$dlmw.* ]] ; do dlmw=$dlmw$dlmw; done # double its length
# echo "# max-w 2^ = $dlmw"
  #
  # Shrik groups of delims to a single char
  while [[ ! "$dlmw" == "$dlm1" ]] ; do
    varG=${varG//${dlmw}$dlm1/$dlm1}
    dlmw=${dlmw:$((${#dlmw}/2))}
  done
  varG=${varG//${dlmw}$dlm1/$dlm1}
# echo "# shrunk.. = $varG"
}

# Main
  varG=':abc:.. def:.~!@#$%^&()_+-=`}{][ ";></,'\''single*quote?'\''..123:' 
  sfi="$IFS"; IFS=':.~!@#$%^&()_+-=`}{][ ";></,' # save original IFS and set new multi-char IFS
  set -f                                         # disable globbing
  shrink_repeat_chars "$IFS" # The source string name must be $varG
  arr=(${varG:1})    # Strip leading dlim;  A single trailing dlim is ok (strangely
  for ix in ${!arr[*]} ; do  # Dump the array
     echo "# arr[$ix] \"${arr[ix]}\""
  done
  set +f     # re-enable globbing   
  IFS="$sfi" # re-instate the original IFS
  #
exit

Tolle Arbeit, interessant +1!
F. Hauri

1

Du kannst es auch mit Gawk machen, aber es ist nicht schön:

var=":abc::def:::ghi::::"
out=$( gawk -F ':+' '
  {
    # strip delimiters from the ends of the line
    sub("^"FS,"")
    sub(FS"$","")
    # then output in a bash-friendly format
    for (i=1;i<=NF;i++) printf("\"%s\" ", $i)
    print ""
  }
' <<< "$var" )
eval arr=($out)
for x in ${!arr[*]} ; do
  echo "# arr[$x] \"${arr[x]}\""
done

Ausgänge

# arr[0] "abc"
# arr[1] "def"
# arr[2] "ghi"

Danke ... Ich schien in meiner Hauptanfrage (geänderte Frage) nicht klar zu sein ... Es ist einfach genug, dies zu tun, indem ich einfach meine $varin ${var##:}... Ich war wirklich auf der Suche nach einer Möglichkeit, IFS selbst zu optimieren. Ich möchte Um dies ohne einen externen Anruf zu tun (ich habe das Gefühl, dass Bash dies effizienter kann als jeder externe), also werde ich auf diesem Weg bleiben. Ihre Methode funktioniert (+1). Soweit Wenn ich die Eingabe ändere, würde ich es lieber mit Bash versuchen als mit awk oder tr (es würde einen Systemaufruf vermeiden), aber ich bin wirklich auf eine IFS-Optimierung aus ...
Peter.O

@fred, wie bereits erwähnt, schlürft IFS nur mehrere aufeinanderfolgende Trennzeichen für den Standard-Leerzeichenwert. Andernfalls führen aufeinanderfolgende Trennzeichen zu überflüssigen leeren Feldern. Ich gehe davon aus, dass ein oder zwei externe Anrufe die Leistung kaum wirklich beeinträchtigen werden.
Glenn Jackman

@glen .. (Du hast gesagt, deine Antwort ist nicht "hübsch" .. Ich denke es ist! :) Allerdings habe ich eine All-Bash-Version (gegen einen externen Aufruf) zusammengestellt und basierend auf 10000 Iterationen, nur das Arrray zu erstellen ( nein I / O) ... bash 1.276s... call (awk) 0m32.210s,,, call (tr) 0m32.178s... Tun Sie das ein paar Mal und Sie könnten denken, Bash ist langsam! ... ist awk in diesem Fall einfacher? ... nicht wenn du das Snippet schon hast :) ... ich werde es später posten; muss jetzt gehen.
Peter.O

Übrigens, bezüglich Ihres Gawk-Skripts ... Ich habe awk im Grunde noch nie verwendet, also habe ich es (und andere) im Detail betrachtet ... Ich kann nicht herausfinden, warum, aber ich werde es erwähnen Das Problem jedenfalls. Wenn Anführungszeichen angegeben werden, werden die Anführungszeichen verloren und an den Stellen zwischen den Anführungszeichen aufgeteilt. Bei ungeraden Anführungszeichenabstürzen stürzt es ab. Hier sind die Testdaten:var="The \"X\" factor:::A single '\"' crashes:::\"One Two\""
Peter.O

-1

Die einfache Antwort lautet: Reduzieren Sie alle Trennzeichen auf eins (das erste).
Das erfordert eine Schleife (die weniger als log(N)mal läuft ):

 var=':a bc::d ef:#$%_+$$%      ^%&*(*&*^
 $#,.::ghi::*::'                           # a long test string.
 d=':@!#$%^&*()_+,.'                       # delimiter set
 f=${d:0:1}                                # first delimiter
 v=${var//["$d"]/"$f"};                    # convert all delimiters to
 :                                         # the first of the delimiter set.
 tmp=$v                                    # temporal variable (v).
 while
     tmp=${tmp//["$f"]["$f"]/"$f"};        # collapse each two delimiters to one
     [[ "$tmp" != "$v" ]];                 # If there was a change
 do
     v=$tmp;                               # actualize the value of the string.
 done

Sie müssen nur noch die Zeichenfolge in einem Trennzeichen korrekt aufteilen und ausdrucken:

 readarray -td "$f" arr < <(printf '%s%s' "$v"'' "$f")
 printf '<%s>' "${arr[@]}" ; echo

set -fIFS muss nicht geändert werden.
Getestet mit Leerzeichen, Zeilenumbrüchen und Glob-Zeichen. Alle Arbeit. Ziemlich langsam (wie eine Shell-Schleife zu erwarten ist).
Aber nur für Bash (Bash 4.4+ wegen der Option -dzum Readarray).


Sch

Eine Shell-Version kann kein Array verwenden. Das einzige verfügbare Array sind die Positionsparameter.
Die Verwendung tr -sist nur eine Zeile (IFS ändert sich im Skript nicht):

 set -f; IFS=$f command eval set -- '$(echo "$var" | tr -s "$d" "[$f*]" )""'

Und drucken Sie es aus:

 printf '<%s>' "$@" ; echo

Immer noch langsam, aber nicht viel mehr.

Der Befehl commandist in Bourne ungültig.
Ruft in zsh commandnur externe Befehle auf und lässt eval fehlschlagen, wenn commandes verwendet wird.
In ksh wird auch mit commandder Wert von IFS im globalen Bereich geändert.
Und commanddie Aufteilung schlägt in mksh-bezogenen Shells (mksh, lksh, posh) fehl. Durch Entfernen des Befehls commandwird der Code auf mehreren Shells ausgeführt. Aber: Durch das Entfernen commandbehält IFS seinen Wert in den meisten Shells (eval ist ein spezielles integriertes Element), außer in bash (ohne Posix-Modus) und zsh im Standardmodus (keine Emulation). Dieses Konzept kann weder mit noch ohne Standard-zsh verwendet werden command.


IFS mit mehreren Zeichen

Ja, IFS kann aus mehreren Zeichen bestehen, aber jedes Zeichen generiert ein Argument:

 set -f; IFS="$d" command eval set -- '$(echo "$var" )""'
 printf '<%s>' "$@" ; echo

Wird ausgegeben:

 <><a bc><><d ef><><><><><><><><><      ><><><><><><><><><
 ><><><><><><ghi><><><><><>

Mit bash können Sie das commandWort weglassen, wenn Sie nicht in der sh / POSIX-Emulation sind. Der Befehl schlägt in ksh93 fehl (IFS behält den geänderten Wert bei). In zsh lässt der Befehl commandzsh versuchen, evaleinen externen Befehl zu finden (den er nicht findet), und schlägt fehl.

Was passiert ist, dass die einzigen IFS-Zeichen, die automatisch auf ein Trennzeichen reduziert werden, IFS-Leerzeichen sind.
Ein Leerzeichen in IFS reduziert alle aufeinander folgenden Leerzeichen zu einem. Eine Registerkarte reduziert alle Registerkarten. Ein Leerzeichen und eine Registerkarte reduzieren die Anzahl der Leerzeichen und / oder Registerkarten auf ein Trennzeichen. Wiederholen Sie die Idee mit Newline.

Um mehrere Trennzeichen zu kollabieren, ist ein wenig Jonglieren erforderlich.
Angenommen, ASCII 3 (0x03) wird in der Eingabe nicht verwendet var:

 var=${var// /$'\3'}                       # protect spaces
 var=${var//["$d"]/ }                      # convert all delimiters to spaces
 set -f;                                   # avoid expanding globs.
 IFS=" " command eval set -- '""$var""'    # split on spaces.
 set -- "${@//$'\3'/ }"                    # convert spaces back.

Die meisten Kommentare zu ksh, zsh und bash (about commandund IFS) gelten hier noch.

Ein Wert von $'\0'wäre bei der Texteingabe weniger wahrscheinlich, aber Bash-Variablen können keine NULs ( 0x00) enthalten.

In sh gibt es keine internen Befehle, um dieselben Zeichenfolgenoperationen auszuführen. Daher ist tr die einzige Lösung für sh-Skripte.


Ja, ich habe geschrieben, dass das OP für die Shell gefragt hat: Bash. In dieser Shell wird IFS nicht beibehalten. Und ja, ist nicht portabel, zum Beispiel zu zsh. @ StéphaneChazelas
NotAnUnixNazi

Im Fall von bash und zsh verhalten sie sich wie von POSIX angegeben, wenn sie als sh
Stéphane Chazelas am

@ StéphaneChazelas (viele) Hinweise zu Einschränkungen jeder Shell hinzugefügt.
NotAnUnixNazi

@ StéphaneChazelas Warum die Abstimmung?
NotAnUnixNazi

Weiß nicht, war ich nicht. Übrigens, ich denke, es gibt hier ein spezielles Q & A über command evalIIRC von Gilles
Stéphane Chazelas
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.