252

I've been looking for a solution and found similar questions, only they were attempting to split sentences with spaces between them, and the answers do not work for my situation.

Currently a variable is being set to something a string like this:
ABCDE-123456
and I would like to split that into 2 variables, while eliminating the "-". i.e.:
var1=ABCDE
var2=123456

How is it possible to accomplish this?


This is the solution that worked for me:
var1=$(echo $STR | cut -f1 -d-)
var2=$(echo $STR | cut -f2 -d-)

Is it possible to use the cut command that will work without a delimiter (each character gets set as a variable)?

var1=$(echo $STR | cut -f1 -d?)
var2=$(echo $STR | cut -f1 -d?)
var3=$(echo $STR | cut -f1 -d?)
etc.

Community
  • 1
  • 1
crunchybutternut
  • 2,523
  • 2
  • 14
  • 5

5 Answers5

306

To split a string separated by -, you can use read with IFS:

$ IFS=- read -r var1 var2 <<< ABCDE-123456
$ echo "$var1"
ABCDE
$ echo "$var2"
123456

Edit:

Here is how you can read each individual character into array elements:

$ read -ra foo <<<"$(echo "ABCDE-123456" | sed 's/./& /g')"

Dump the array:

$ declare -p foo
declare -a foo='([0]="A" [1]="B" [2]="C" [3]="D" [4]="E" [5]="-" [6]="1" [7]="2" [8]="3" [9]="4" [10]="5" [11]="6")'

If there are spaces in the string:

$ IFS=$'\v' read -ra foo <<<"$(echo "ABCDE 123456" | sed $'s/./&\v/g')"
$ declare -p foo
declare -a foo='([0]="A" [1]="B" [2]="C" [3]="D" [4]="E" [5]=" " [6]="1" [7]="2" [8]="3" [9]="4" [10]="5" [11]="6")'
Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • 2
    this solution also has the benefit that if delimiter is not present, the `var2` will be empty – Martin Serrano Jan 11 '18 at 04:34
  • The `read` does not work inside loops with input redirects. `read` will pick a wrong file descriptor to read from. – akwky Feb 24 '20 at 10:59
  • I initially gave this answer a plus as an elegant solution, but now figured out it works differently on Bash v3 and v4, thereby doesn't work on macos with pre-installed bash v3. Unfortunatelly I can't downvote the answer now since the vote is locked :( – Jerry Green Nov 11 '20 at 14:30
  • A more general, correct, way: `IFS=- read -r -d '' var1 var2 < <(printf %s "ABCDE-123456")`. The `-r -d ''` and `<(printf %s ...)` are important – Fravadona Jan 21 '22 at 10:22
  • 1
    @akwky: Use an alternate [file descriptor](https://mywiki.wooledge.org/BashFAQ/089). `while read -r line <&3; do ssh_or_something "$line"; done 3 – Dennis Williamson Jun 24 '22 at 00:14
  • @JerryGreen: How does it not work with Bash 3 in MacOS? I'm not seeing a difference. – Dennis Williamson Jun 28 '22 at 13:19
264

If you know it's going to be just two fields, you can skip the extra subprocesses like this, using :

var1=${STR%-*}
var2=${STR#*-}

What does this do? ${STR%-*} deletes the shortest substring of $STR that matches the pattern -* starting from the end of the string. ${STR#*-} does the same, but with the *- pattern and starting from the beginning of the string. They each have counterparts %% and ## which find the longest anchored pattern match. If anyone has a helpful mnemonic to remember which does which, let me know! I always have to try both to remember.

See the bash documentation for more information.

luator
  • 4,769
  • 3
  • 30
  • 51
Matt K
  • 13,370
  • 2
  • 32
  • 51
  • 18
    Plus 1 For knowing your POSIX shell features, avoiding expensive forks and pipes, and the absence of bashisms. – Jens Jan 30 '15 at 15:17
  • 3
    Dunno about "absence of bashisms" considering that this is already moderately cryptic .... if your delimiter is a newline instead of a hyphen, then it becomes even more cryptic. On the other hand, *it works with newlines*, so there's that. – Steven Lu May 01 '15 at 20:19
  • A description of how this actually works would be helpful – K Erlandsson Mar 09 '16 at 11:34
  • Well explained. Thanks. What is official name of this functionality? Is there any documentation for it? – Marek Podyma Aug 09 '16 at 15:51
  • 5
    I've finally found documentation for it: [Shell-Parameter-Expansion](https://www.gnu.org/software/bash/manual/bash.html#Shell-Parameter-Expansion) – Marek Podyma Aug 09 '16 at 15:58
  • 29
    Mnemonic: "#" is to the left of "%" on a standard keyboard, so "#" removes a prefix (on the left), and "%" removes a suffix (on the right). – DS. Jan 13 '17 at 19:56
  • Simplest answer that worked with zero modifications. Thank you! – CodingInCircles May 22 '18 at 14:47
  • this didnt work for me in bash 5.0 or zsh 5.8 – vampiire Dec 20 '21 at 05:24
  • 4
    Another mnemonic, since your keyboard may be different (and some just "feel" the layout, rather than know it): the % symbol is typically encountered *after* a number, e.g. 90%, hence it is a suffix. The # symbol is typically leading comments or even just the first char in hashtags, so it's a common prefix. The purpose of both modifiers is to remove, one just removes a prefix (#), the other removes the suffix (%). – Oliver W. Nov 08 '22 at 21:49
195

If your solution doesn't have to be general, i.e. only needs to work for strings like your example, you could do:

var1=$(echo $STR | cut -f1 -d-)
var2=$(echo $STR | cut -f2 -d-)

I chose cut here because you could simply extend the code for a few more variables...

Rob I
  • 5,627
  • 2
  • 21
  • 28
  • Can you look at my post again and see if you have a solution for the followup question? thanks! – crunchybutternut May 09 '12 at 17:40
  • You can use `cut` to cut characters too! `cut -c1` for example. – Matt K May 09 '12 at 17:59
  • 1
    Although this is very simple to read and write, is a very slow solution because forces you to read twice the same data ($STR) ... if you care of your script performace, the @anubhava solution is much better – FSp Nov 27 '12 at 10:26
  • 1
    Apart from being an ugly last-resort solution, this has a bug: You should absolutely use double quotes in `echo "$STR"` unless you specifically want the shell to expand any wildcards in the string as a side effect. See also http://stackoverflow.com/questions/10067266/when-to-wrap-quotes-around-a-variable – tripleee Jan 25 '16 at 06:47
  • 1
    You're right about double quotes of course, though I did point out this solution wasn't general. However I think your assessment is a bit unfair - for some people this solution may be more readable (and hence extensible etc) than some others, and doesn't completely rely on arcane bash feature that wouldn't translate to other shells. I suspect that's why my solution, though less elegant, continues to get votes periodically... – Rob I Feb 10 '16 at 13:57
54

Sounds like a job for set with a custom IFS.

IFS=-
set $STR
var1=$1
var2=$2

(You will want to do this in a function with a local IFS so you don't mess up other parts of your script where you require IFS to be what you expect.)

tripleee
  • 175,061
  • 34
  • 275
  • 318
  • Nice - I knew about `$IFS` but hadn't seen how it could be used. – Rob I May 09 '12 at 19:20
  • I used triplee's example and it worked exactly as advertised! Just change last two lines to
    myvar1=`echo $1` && myvar2=`echo $2`
    
    if you need to store them throughout a script with several "thrown" variables.
    – Sigg3.net Jun 19 '13 at 08:08
  • 1
    No, [don't use a useless `echo` in backticks](http://partmaps.org/era/unix/award.html#echo). – tripleee Jun 19 '13 at 13:25
  • 4
    This is a really sweet solution if we need to write something that is not Bash specific. To handle `IFS` troubles, one can add `OLDIFS=$IFS` at the beginning before overwriting it, and then add `IFS=$OLDIFS` just after the `set` line. – Daniel Andersson Mar 27 '15 at 06:46
  • @DanielAndersson That's a good and common workaround as well if you don't want to or cannot use a function with a `local IFS` (which isn't entirely portable anyway). – tripleee Mar 27 '15 at 07:00
  • 3
    Maybe add a `set -f` to disable pathname expansion before `set -- $STR`, or it will capture paths files names if `$STR` contains patterns. – Léa Gris Oct 11 '19 at 16:56
  • @DanielAndersson I got your point, but this will do the wrong thing if `IFS` is originally unset. I don't know whether that could happen in a normal bash environment, but I am meaning my comment in a more general sense. Setting a variable to a custom value and restoring it later is surprisingly complicated if it is not known in advance whether or not that variable originally is set. – Binarus Jun 23 '20 at 18:41
  • @Binarus Good point -- https://mywiki.wooledge.org/BashPitfalls #49 has some words about this. In practice I like to use subshells for things like this (the last suggestion in the link), temporary options, etc., but sometimes that is too limiting. – Daniel Andersson Jul 08 '20 at 23:04
  • You can do something like `oldIFS=${IFS-__unset__}` but restoring from that is arguably clunky at best. – tripleee Jul 09 '20 at 05:01
34

Using bash regex capabilities:

re="^([^-]+)-(.*)$"
[[ "ABCDE-123456" =~ $re ]] && var1="${BASH_REMATCH[1]}" && var2="${BASH_REMATCH[2]}"
echo $var1
echo $var2

OUTPUT

ABCDE
123456
anubhava
  • 761,203
  • 64
  • 569
  • 643