59

I have a string:

one_two_three_four_five

I need to save in a variable A value two and in variable B value fourfrom the above string

82

Use cut with _ as the field delimiter and get desired fields:

A="$(cut -d'_' -f2 <<<'one_two_three_four_five')"
B="$(cut -d'_' -f4 <<<'one_two_three_four_five')"

You can also use echo and pipe instead of Here string:

A="$(echo 'one_two_three_four_five' | cut -d'_' -f2)"
B="$(echo 'one_two_three_four_five' | cut -d'_' -f4)"

Example:

$ s='one_two_three_four_five'

$ A="$(cut -d'_' -f2 <<<"$s")"
$ echo "$A"
two

$ B="$(cut -d'_' -f4 <<<"$s")"
$ echo "$B"
four
  • Is there any alternative? I am using ksh (not bsh) and it returns ksh: syntax error: `<' unexpected – Alex Sep 25 '16 at 23:14
  • @Alex Check my edits. – heemayl Sep 25 '16 at 23:24
  • Nice answer, I have a little question: what happen if your variable "$s" is a path folder. When I try to cut a path folder I do like following: ` $FILE=my_user/my_folder/[file]*` $ echo $FILE my_user/my_folder/file.csv $ A="$(cut -d'/' -f2 <<<"$FILE")" $ echo $A [file]* Do you know what's happening here? – Henry Navarro Dec 5 '18 at 11:55
15

Using only POSIX sh constructs, you can use parameter substitution constructs to parse one delimiter at a time. Note that this code assumes that there is the requisite number of fields, otherwise the last field is repeated.

string='one_two_three_four_five'
remainder="$string"
first="${remainder%%_*}"; remainder="${remainder#*_}"
second="${remainder%%_*}"; remainder="${remainder#*_}"
third="${remainder%%_*}"; remainder="${remainder#*_}"
fourth="${remainder%%_*}"; remainder="${remainder#*_}"

Alternatively, you can use an unquoted parameter substitution with wildcard expansion disabled and IFS set to the delimiter character (this only works if the delimiter is a single non-whitespace character or if any whitespace sequence is a delimiter).

string='one_two_three_four_five'
set -f; IFS='_'
set -- $string
second=$2; fourth=$4
set +f; unset IFS

This clobbers the positional parameters. If you do this in a function, only the function's positional parameters are affected.

Yet another approach is to use the read builtin.

IFS=_ read -r first second third fourth trail <<'EOF'
one_two_three_four_five
EOF
  • The use of unset IFS doesn't return IFS to default. If after that someone does OldIFS="$IFS" will have a null value inside OldIFS. Also, it is assuming that the previous value of IFS is the default, which is very possible (and useful) to not be. The only correct solution is to store in old="$IFS" and later restore with IFS="$old". Or ... use a sub-shell (...). Or, better yet, read my answer. – sorontar Sep 26 '16 at 16:09
  • @sorontar unset IFS doesn't restore IFS to the default value, but it returns field splitting to the default effect. Yes, it's a limitation, but usually an acceptable one in practice. The problem with a subshell is that we need to get data out of it. I do show a solution that doesn't change the state at the end, with read. (It works in POSIX shells, but IIRC not in the Bourne shell because it would run the read in a subshell due to the here-document.) Using <<< as in you answer is a variant that works only in ksh/bash/zsh. – Gilles Sep 26 '16 at 16:23
  • I don't see a problem even with att or heirloom shell about a subshell. All shells tested (including the old bourne) provide the correct value in the main shell. – sorontar Sep 26 '16 at 19:16
  • What happen if my path is something like user/my_folder/[this_is_my_file]*? What I obtain when I follow these steps is [this_is_my_file]* – Henry Navarro Dec 5 '18 at 12:20
  • @HenryNavarro This output doesn't correspond to any of the code snippets in my answer. None of them do anything special on /. – Gilles Dec 5 '18 at 17:11
13

Wanted to see an awk answer, so here's one:

A=$(awk -F_ '{print $2}' <<< 'one_two_three_four_five')
B=$(awk -F_ '{print $4}' <<< 'one_two_three_four_five')
6

The simplest way (for shells with <<<) is:

 IFS='_' read -r a second a fourth a <<<"$string"

Using a temporal variable $a instead of $_ because one shell complains.

In a full script:

 string='one_two_three_four_five'
 IFS='_' read -r a second a fourth a <<<"$string"
 echo "$second $fourth"

No IFS changing, not issues with set -f (Pathname expansion) No changes to the positional parameters ("$@").


For a solution portable to all shells (yes, all POSIX included) without changing IFS or set -f, use the (a bit more complex) heredoc equivalent:

string='one_two_three_four_five'

IFS='_' read -r a second a fourth a <<-_EOF_
$string
_EOF_

echo "$second $fourth"

Understand that this solutions (both the here-doc and the use of <<< will remove all trailing newlines.
And that this is designed to a "one liner" variable content.
Solutions for multi-liners are possible but need more complex constructs.


A very simple solution is possible in bash version 4.4

readarray -d _ -t arr <<<"$string"

echo "array ${arr[1]} ${arr[3]}"   # array numbers are zero based.

There is no equivalent for POSIX shells, as many POSIX shells do not have arrays.

For shells that have arrays may be as simple as:
(tested working in attsh, lksh, mksh, ksh, and bash)

set -f; IFS=_; arr=($string)

But with a lot of additional plumbing to keep and reset variables and options:

string='one_* *_three_four_five'

case $- in
    *f*) noglobset=true; ;;
    *) noglobset=false;;
esac

oldIFS="$IFS"

set -f; IFS=_; arr=($string)

if $noglobset; then set -f; else set +f; fi

echo "two=${arr[1]} four=${arr[3]}"

In zsh, arrays start in 1, and doesn't split string by default.
So some changes need to be done to get this working in zsh.

  • solutions that use read are simple as long as OP doesn't want to extract the 76th and 127th elements from a long string... – don_crissti Sep 26 '16 at 16:25
  • @don_crissti Well, yes, of course, but a similar construct: readarray could be easier to use for that situation. – sorontar Sep 26 '16 at 16:36
  • @don_crissti I also added an array solution for shells that do have arrays. For POSIX shells, well, not having arrays, positional parameters up to 127 elements is not a "simple" solution by any measure. – sorontar Sep 26 '16 at 18:55
2

With zsh you could split the string (on _) into an array:

elements=(${(s:_:)string})

and then access each/any element via array index:

print -r ${elements[4]}

Keep in mind that in zsh (unlike ksh/bash) array indices start at 1.

  • Please remember to add set -f warning to the first solution. ... asterisks * maybe? – sorontar Sep 26 '16 at 19:08
  • @sorontar - why do you think I need set -f ? I'm not using read/IFS. Try my solutions with a string like *_*_* or whatever... – don_crissti Sep 26 '16 at 19:16
  • Not for zsh, but the user asked for a ksh solution, so, he may try to use it in that shell. A warning will help him avoid the problem. – sorontar Sep 26 '16 at 19:19
1

Is a python solution allowed?

# python -c "import sys; print sys.argv[1].split('_')[1]" one_two_three_four_five
two

# python -c "import sys; print sys.argv[1].split('_')[3]" one_two_three_four_five
four
  • No. bad bad answet – Raj Kumar May 9 at 7:51
0

Another awk example; simpler to understand.

A=\`echo one_two_three_four_five | awk -F_ '{print $1}'\`  
B=\`echo one_two_three_four_five | awk -F_ '{print $2}'\`  
C=\`echo one_two_three_four_five | awk -F_ '{print $3}'\`  
... and so on...  

Can be used with variables also.
Suppose:
this_str="one_two_three_four_five"
Then the following works:
A=`echo ${this_str} | awk -F_ '{print $1}'`
B=`echo ${this_str} | awk -F_ '{print $2}'`
C=`echo ${this_str} | awk -F_ '{print $3}'`
... and so on...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.