I have a string:
one_two_three_four_five
I need to save in a variable A
value two
and in variable B
value four
from the above string
I am using ksh.
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems. It only takes a minute to sign up.
Sign up to join this communityUse 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
Beware that if $s
contains newline characters, that will return a multiline string that contains the 2nd/4th field in each line of $s
, not the 2nd/4th field in $s
.
$ echo $FILE
my_user/my_folder/file.csv
$ A="$(cut -d'/' -f2 <<<"$FILE")"
$ echo $A
[file]*
Do you know what's happening here?
Dec 5, 2018 at 11:55
echo "${s##*_}"
May 28, 2019 at 21:45
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')
awk -F_ '{print $NF}' <<< 'one_two_3_4_five'
May 28, 2019 at 21:38
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 for strings that don't contain newline characters is to use the read
builtin.
IFS=_ read -r first second third fourth trail <<'EOF'
one_two_three_four_five
EOF
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.
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.
Sep 26, 2016 at 16:23
user/my_folder/[this_is_my_file]*
? What I obtain when I follow these steps is [this_is_my_file]*
Dec 5, 2018 at 12:20
/
.
Dec 5, 2018 at 17:11
Here string
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 ("$@").
Heredoc
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.
Bash 4.4+
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.
Arrays
For shells that have arrays may be as simple as:
(tested working in attsh, lksh, mksh, ksh, and bash (not zsh))
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
IFS=$oldIFS
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:
set -F; IFS=_; arr=( $(echo $string) )
echo "two=${arr[2]} four=${arr[4]}"
With zsh
you could split the string (on _
) into an array:
non_empty_elements=(${(s:_:)string})
all_elements=("${(@s:_:)string}")
and then access each/any element via array index:
print -r -- ${all_elements[4]}
Keep in mind that in zsh
(like most other shells, but unlike ksh
/bash
) array indices start at 1.
Or directly in one expansion:
print -r -- "${${(@s:_:)string}[4]}"
set -f
? I'm not using read
/IFS
. Try my solutions with a string like *_*_*
or whatever...
Sep 26, 2016 at 19:16
zsh
, that would rather be set -o noglob
. The -f
option in zsh
's default emulation mode, is the same as in csh, not sh, it's about starting zsh
without reading conf files. set -f
to disable globbing would only work with sh/ksh emulation (that would be the emulations where you would need to disable globbing indeed, but you'd only use those modes to interpret sh/ksh code, so it doesn't really apply here, and you'd need to change the code further anyway to work around the other misfeatures of sh/ksh)
Jun 10, 2020 at 6:24
set -f
. Not for zsh, I am not saying to use set -f
in zsh.
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
With due respect to everyone who have posted excellent answers, I wonder if we are over-engineering this problem. Three simple lines to just answer the question asked without generalizing:
str="one_two_three_four_five"
<-- create a string
A=$(echo $str | awk -F_ '{print $2}')
<-- tell awk to use _ as the delimiter and assign the second field to A
B=$(echo $str | awk -F_ '{print $4}')
<-- tell awk to use _ as the delimiter and assign the fourth field to B
You can then use the variables as usual. Here is an example:
$ echo "The value of A is: $A; the value of B is: $B"
The value of A is: two; the value of B is: four
$
str='*'
or str='-n _-o _-p '
or str=$'a_b\n_c_d_e'
.
Jun 10, 2020 at 6:35
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=$(printf '%s\n' "${this_str}" | awk -F_ '{print $1}')
B=$(printf '%s\n' "${this_str}" | awk -F_ '{print $2}')
C=$(printf '%s\n' "${this_str}" | awk -F_ '{print $3}')
... and so on...
That assumes ${this_str}
doesn't contain newline characters, or it would return the first _
in each line of the contents of the variable instead of the first field in the contents of the variable.