How do I split a string on a delimiter in Bash?

后端 未结 30 2018
萌比男神i
萌比男神i 2020-11-21 04:58

I have this string stored in a variable:

IN=\"bla@some.com;john@home.com\"

Now I would like to split the strings by ; delimite

30条回答
  •  夕颜
    夕颜 (楼主)
    2020-11-21 05:31

    Compatible answer

    There are a lot of different ways to do this in bash.

    However, it's important to first note that bash has many special features (so-called bashisms) that won't work in any other shell.

    In particular, arrays, associative arrays, and pattern substitution, which are used in the solutions in this post as well as others in the thread, are bashisms and may not work under other shells that many people use.

    For instance: on my Debian GNU/Linux, there is a standard shell called dash; I know many people who like to use another shell called ksh; and there is also a special tool called busybox with his own shell interpreter (ash).

    Requested string

    The string to be split in the above question is:

    IN="bla@some.com;john@home.com"
    

    I will use a modified version of this string to ensure that my solution is robust to strings containing whitespace, which could break other solutions:

    IN="bla@some.com;john@home.com;Full Name "
    

    Split string based on delimiter in bash (version >=4.2)

    In pure bash, we can create an array with elements split by a temporary value for IFS (the input field separator). The IFS, among other things, tells bash which character(s) it should treat as a delimiter between elements when defining an array:

    IN="bla@some.com;john@home.com;Full Name "
    
    # save original IFS value so we can restore it later
    oIFS="$IFS"
    IFS=";"
    declare -a fields=($IN)
    IFS="$oIFS"
    unset oIFS
    

    In newer versions of bash, prefixing a command with an IFS definition changes the IFS for that command only and resets it to the previous value immediately afterwards. This means we can do the above in just one line:

    IFS=\; read -a fields <<<"$IN"
    # after this command, the IFS resets back to its previous value (here, the default):
    set | grep ^IFS=
    # IFS=$' \t\n'
    

    We can see that the string IN has been stored into an array named fields, split on the semicolons:

    set | grep ^fields=\\\|^IN=
    # fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name ")
    # IN='bla@some.com;john@home.com;Full Name '
    

    (We can also display the contents of these variables using declare -p:)

    declare -p IN fields
    # declare -- IN="bla@some.com;john@home.com;Full Name "
    # declare -a fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name ")
    

    Note that read is the quickest way to do the split because there are no forks or external resources called.

    Once the array is defined, you can use a simple loop to process each field (or, rather, each element in the array you've now defined):

    # `"${fields[@]}"` expands to return every element of `fields` array as a separate argument
    for x in "${fields[@]}" ;do
        echo "> [$x]"
        done
    # > [bla@some.com]
    # > [john@home.com]
    # > [Full Name ]
    

    Or you could drop each field from the array after processing using a shifting approach, which I like:

    while [ "$fields" ] ;do
        echo "> [$fields]"
        # slice the array 
        fields=("${fields[@]:1}")
        done
    # > [bla@some.com]
    # > [john@home.com]
    # > [Full Name ]
    

    And if you just want a simple printout of the array, you don't even need to loop over it:

    printf "> [%s]\n" "${fields[@]}"
    # > [bla@some.com]
    # > [john@home.com]
    # > [Full Name ]
    

    Update: recent bash >= 4.4

    In newer versions of bash, you can also play with the command mapfile:

    mapfile -td \; fields < <(printf "%s\0" "$IN")
    

    This syntax preserve special chars, newlines and empty fields!

    If you don't want to include empty fields, you could do the following:

    mapfile -td \; fields <<<"$IN"
    fields=("${fields[@]%$'\n'}")   # drop '\n' added by '<<<'
    

    With mapfile, you can also skip declaring an array and implicitly "loop" over the delimited elements, calling a function on each:

    myPubliMail() {
        printf "Seq: %6d: Sending mail to '%s'..." $1 "$2"
        # mail -s "This is not a spam..." "$2" 

    (Note: the \0 at end of the format string is useless if you don't care about empty fields at end of the string or they're not present.)

    mapfile < <(echo -n "$IN") -td \; -c 1 -C myPubliMail
    
    # Seq:      0: Sending mail to 'bla@some.com', done.
    # Seq:      1: Sending mail to 'john@home.com', done.
    # Seq:      2: Sending mail to 'Full Name ', done.
    

    Or you could use <<<, and in the function body include some processing to drop the newline it adds:

    myPubliMail() {
        local seq=$1 dest="${2%$'\n'}"
        printf "Seq: %6d: Sending mail to '%s'..." $seq "$dest"
        # mail -s "This is not a spam..." "$dest" ', done.
    
    

    Split string based on delimiter in shell

    If you can't use bash, or if you want to write something that can be used in many different shells, you often can't use bashisms -- and this includes the arrays we've been using in the solutions above.

    However, we don't need to use arrays to loop over "elements" of a string. There is a syntax used in many shells for deleting substrings of a string from the first or last occurrence of a pattern. Note that * is a wildcard that stands for zero or more characters:

    (The lack of this approach in any solution posted so far is the main reason I'm writing this answer ;)

    ${var#*SubStr}  # drops substring from start of string up to first occurrence of `SubStr`
    ${var##*SubStr} # drops substring from start of string up to last occurrence of `SubStr`
    ${var%SubStr*}  # drops substring from last occurrence of `SubStr` to end of string
    ${var%%SubStr*} # drops substring from first occurrence of `SubStr` to end of string
    

    As explained by Score_Under:

    # and % delete the shortest possible matching substring from the start and end of the string respectively, and

    ## and %% delete the longest possible matching substring.

    Using the above syntax, we can create an approach where we extract substring "elements" from the string by deleting the substrings up to or after the delimiter.

    The codeblock below works well in bash (including Mac OS's bash), dash, ksh, and busybox's ash:

    IN="bla@some.com;john@home.com;Full Name "
    while [ "$IN" ] ;do
        # extract the substring from start of string up to delimiter.
        # this is the first "element" of the string.
        iter=${IN%%;*}
        echo "> [$iter]"
        # if there's only one element left, set `IN` to an empty string.
        # this causes us to exit this `while` loop.
        # else, we delete the first "element" of the string from IN, and move onto the next.
        [ "$IN" = "$iter" ] && \
            IN='' || \
            IN="${IN#*;}"
      done
    # > [bla@some.com]
    # > [john@home.com]
    # > [Full Name ]
    

    Have fun!

提交回复
热议问题