Bash substring separated by spaces - bash

I've a string like this
var="--env=test --arg=foo"
I've tried to use substring ${var#*=} to get for example test but don't find a way to separate spaces. Any idea or should I use cut?

You can use BASH regex:
var="--env=test --arg=foo"
[[ $var =~ =([^ ]+) ]] && echo "${BASH_REMATCH[1]}"
test
Using extglob, you can do this in even shorter code:
shopt -s extglob
echo "${var//+( *|+([! ])=)}"
test

You could use an array:
$ var="--env=test --arg=foo"
$ arr=($var)
$ printf "%s\n" "${arr[#]}"
--env=test
--arg=foo
where tokens in var are split by IFS (which defaults to: space, tab, newline). If you want to split only by space, just set IFS=' ':
IFS=' ' arr=($var)

util-linux (which should be part of any Linux distribution) has built in support for getopts command line parsing.
Usage in your case:
var="blub --env=test --arg=foo"
eval set -- $(getopt --longoptions env:,arg: -- $var)
while true ; do
case "$1" in
--arg)
echo "Arg is $2"
shift 2
;;
--env)
echo "Env is $2"
shift 2
;;
--) shift ; break ;;
esac
done

Related

How to concatenate the arguments and store it in a variable in unix? [duplicate]

This question already has answers here:
Concatenate all arguments and wrap them with double quotes
(6 answers)
Closed 5 years ago.
I would like to concatenate all the arguments passed to my bash script except the flag.
So for example, If the script takes inputs as follows:
./myBashScript.sh -flag1 exampleString1 exampleString2
I want the result to be "exampleString1_exampleString2"
I can do this for a predefined number of inputs (i.e. 2), but how can i do it for an arbitrary number of inputs?
function concatenate_args
{
string=""
for a in "$#" # Loop over arguments
do
if [[ "${a:0:1}" != "-" ]] # Ignore flags (first character is -)
then
if [[ "$string" != "" ]]
then
string+="_" # Delimeter
fi
string+="$a"
fi
done
echo "$string"
}
# Usage:
args="$(concatenate_args "$#")"
This is an ugly but simple solution:
echo $* | sed -e "s/ /_/g;s/[^_]*_//"
You can also use formatted strings to concatenate args.
# assuming flag is first arg and optional
flag=$1
[[ $1 = ${1#-} ]] && unset $flag || shift
concat=$(printf '%s_' ${#})
echo ${concat%_} # to remove the trailing _
nJoy!
Here's a piece of code that I'm actually proud of (it is very shell-style I think)
#!/bin/sh
firsttime=yes
for i in "$#"
do
test "$firsttime" && set -- && unset firsttime
test "${i%%-*}" && set -- "$#" "$i"
done
IFS=_ ; echo "$*"
I've interpreted your question so as to remove all arguments beginning with -
If you only want to remove the beginning sequence of arguments beginnnig with -:
#!/bin/sh
while ! test "${1%%-*}"
do
shift
done
IFS=_ ; echo "$*"
If you simply want to remove the first argument:
#!/bin/sh
shift
IFS=_ ; printf %s\\n "$*"
flag="$1"
shift
oldIFS="$IFS"
IFS="_"
the_rest="$*"
IFS="$oldIFS"
In this context, "$*" is exactly what you're looking for, it seems. It is seldom the correct choice, but here's a case where it really is the correct choice.
Alternatively, simply loop and concatenate:
flag="$1"
shift
the_rest=""
pad=""
for arg in "$#"
do
the_rest="${the_rest}${pad}${arg}"
pad="_"
done
The $pad variable ensures that you don't end up with a stray underscore at the start of $the_rest.
#!/bin/bash
paramCat () {
for s in "$#"
do
case $s in
-*)
;;
*)
echo -n _${s}
;;
esac
done
}
catted="$(paramCat "$#")"
echo ${catted/_/}

parse and expand interval

In my script I need to expand an interval, e.g.:
input: 1,5-7
to get something like the following:
output: 1,5,6,7
I've found other solutions here, but they involve python and I can't use it in my script.
Solution with Just Bash 4 Builtins
You can use Bash range expansions. For example, assuming you've already parsed your input you can perform a series of successive operations to transform your range into a comma-separated series. For example:
value1=1
value2='5-7'
value2=${value2/-/..}
value2=`eval echo {$value2}`
echo "input: $value1,${value2// /,}"
All the usual caveats about the dangers of eval apply, and you'd definitely be better off solving this problem in Perl, Ruby, Python, or AWK. If you can't or won't, then you should at least consider including some pipeline tools like tr or sed in your conversions to avoid the need for eval.
Try something like this:
#!/bin/bash
for f in ${1//,/ }; do
if [[ $f =~ - ]]; then
a+=( $(seq ${f%-*} 1 ${f#*-}) )
else
a+=( $f )
fi
done
a=${a[*]}
a=${a// /,}
echo $a
Edit: As #Maxim_united mentioned in the comments, appending might be preferable to re-creating the array over and over again.
This should work with multiple ranges too.
#! /bin/bash
input="1,5-7,13-18,22"
result_str=""
for num in $(tr ',' ' ' <<< "$input"); do
if [[ "$num" == *-* ]]; then
res=$(seq -s ',' $(sed -n 's#\([0-9]\+\)-\([0-9]\+\).*#\1 \2#p' <<< "$num"))
else
res="$num"
fi
result_str="$result_str,$res"
done
echo ${result_str:1}
Will produce the following output:
1,5,6,7,13,14,15,16,17,18,22
expand_commas()
{
local arg
local st en i
set -- ${1//,/ }
for arg
do
case $arg in
[0-9]*-[0-9]*)
st=${arg%-*}
en=${arg#*-}
for ((i = st; i <= en; i++))
do
echo $i
done
;;
*)
echo $arg
;;
esac
done
}
Usage:
result=$(expand_commas arg)
eg:
result=$(expand_commas 1,5-7,9-12,3)
echo $result
You'll have to turn the separated words back into commas, of course.
It's a bit fragile with bad inputs but it's entirely in bash.
Here's my stab at it:
input=1,5-7,10,17-20
IFS=, read -a chunks <<< "$input"
output=()
for chunk in "${chunks[#]}"
do
IFS=- read -a args <<< "$chunk"
if (( ${#args[#]} == 1 )) # single number
then
output+=(${args[*]})
else # range
output+=($(seq "${args[#]}"))
fi
done
joined=$(sed -e 's/ /,/g' <<< "${output[*]}")
echo $joined
Basically split on commas, then interpret each piece. Then join back together with commas at the end.
A generic bash solution using the sequence expression `{x..y}'
#!/bin/bash
function doIt() {
local inp="${#/,/ }"
declare -a args=( $(echo ${inp/-/..}) )
local item
local sep
for item in "${args[#]}"
do
case ${item} in
*..*) eval "for i in {${item}} ; do echo -n \${sep}\${i}; sep=, ; done";;
*) echo -n ${sep}${item};;
esac
sep=,
done
}
doIt "1,5-7"
Should work with any input following the sample in the question. Also with multiple occurrences of x-y
Use only bash builtins
Using ideas from both #Ansgar Wiechers and #CodeGnome:
input="1,5-7,13-18,22"
for s in ${input//,/ }
do
if [[ $f =~ - ]]
then
a+=( $(eval echo {${s//-/..}}) )
else
a+=( $s )
fi
done
oldIFS=$IFS; IFS=$','; echo "${a[*]}"; IFS=$oldIFS
Works in Bash 3
Considering all the other answers, I came up with this solution, which does not use any sub-shells (but one call to eval for brace expansion) or separate processes:
# range list is assumed to be in $1 (e.g. 1-3,5,9-13)
# convert $1 to an array of ranges ("1-3" "5" "9-13")
IFS=,
local range=($1)
unset IFS
list=() # initialize result list
local r
for r in "${range[#]}"; do
if [[ $r == *-* ]]; then
# if the range is of the form "x-y",
# * convert to a brace expression "{x..y}",
# * using eval, this gets expanded to "x" "x+1" … "y" and
# * append this to the list array
eval list+=( {${r/-/..}} )
else
# otherwise, it is a simple number and can be appended to the array
list+=($r)
fi
done
# test output
echo ${list[#]}

Escape accumulated arguments in a shell script?

With a view to a bug in git, at the moment git-submodule.sh reads (reordered):
[iterating over command line arguments]
--reference)
case "$2" in '') usage ;; esac
reference="--reference=$2"
shift
;;
--reference=*)
reference="$1"
shift
;;
[...]
if test -n "$reference"
then
git-clone $quiet "$reference" -n "$url" "$path" --separate-git-dir "$gitdir"
else
git-clone $quiet -n "$url" "$path" --separate-git-dir "$gitdir"
fi ||
die "$(eval_gettext "Clone of '\$url' into submodule path '\$path' failed")"
This uses only the last argument given by --reference. I now want to enhance this so that all --reference options are passed on to git-clone. This is trivial for trivial arguments (reference="$reference --reference=$2"), but my mind boggles when thinking about arguments containing white space, quote or shell meta characters.
What is the best practice to escape such accumulated arguments?
Best practice would be to use a bash array:
declare -a references
#...
--reference)
case ... esac
references+=("--reference=$2")
shift
;;
--reference=*)
references+=("$1")
shift
;;
#...
# no need to test the array for emptiness
git-clone $quiet "${references[#]}" -n "$url" "$path" --separate-git-dir "$gitdir"
However, the referenced script uses /bin/sh instead.

assinging "-n" string to a variable doesn't work

$ OPTION="-n"
$ echo $OPTION
$
Nothing happens. I expected this.
$ OPTION="-n"
$ echo $OPTION
-n
$
Why is this?
-n is a parameter to echo, which means the trailing newline is suppressed. The fact that there's no empty line between $ echo $OPTION and the following $ means that $OPTION is properly set to -n.
If you put something else in front of $OPTION, the echo will work as you expect it to. echo only interprets words at the beginning of the command as options. As soon as it finds a non-option word ("OPTION", in this case), all words that follow are treated as literals, and not parsed as options to echo.
$ echo OPTION is set to $OPTION
OPTION is set to -n
$
Remember that the shell expands environment variables before the command is executed. Thus:
option="-n"
echo $option
becomes
echo -n ""
With the value of $option being interpreted as a parameter for the echo command. If you were using Kornshell (which is 95% similar to BASH), you could have used the builtin print command instead:
option="-n"
print -- "$option"
Unfortunately, BASH doesn't have the print command, and using the double dash in the BASH echo command will print out a double dash -- not what you want.
Instead, you'll have to use the printf command which is a bit slower than echo:
option="-n"
printf -- "$option\n" #Must include the \n to make a new line!
Of course, if you had this, you'd be in trouble:
option="%d"
printf -- "$option\n"
To get around that:
option="%d"
printf "%s\n", "$option"
By the way, you have the same problems with test:
option="-n"
if [ "$option" -eq "-n" ] #Won't work!
This is why you'll see people do this:
if [ "x$option" -eq "x-n" ] #Will work
To get the desired result, you could do this:
$ OUTPUT='-n'
$ echo -en ${OUTPUT}\\n

shell matching a pattern using a case-statement which is stored inside a variable

I am trying to match a pattern with a case-statement where the pattern is stored inside a variable. Here is a minimal example:
PATTERN="foo|bar|baz|bla"
case "foo" in
${PATTERN})
printf "matched\n"
;;
*)
printf "no match\n"
;;
esac
Unfortunately the "|" seems to be escaped (interestingly "*" or "?" are not). How do I get this to work, i.e. to match "foo"? I need to store the pattern in a variable because it is constructed dynamically. This needs to work on a POSIX compatible shell.
It is possible to match a sub-string in a string without spawning a sub-process (such as grep) using the only POSIX compliant methods of sh(1p), as defined in Section 2.6.2, Parameter Expansion.
Here is a convenience function:
# Note:
# Unlike a regular expression, the separator *must* enclose the pattern;
# and it could be a multi chars.
isin() {
PATTERN=${2:?a pattern is required}
SEP=${3:-|}
[ -z "${PATTERN##*${SEP}${1}${SEP}*}" ]
}
Examples:
for needle in foo bar; do
isin "$needle" "|hello|world|foo|" && echo "found: $needle"
done
# using ";" as separator
for needle in foo bar; do
isin "$needle" ";hello;world;foo;" \; && echo "found: $needle"
done
# using the string "RS" as separator
for needle in foo bar; do
isin "$needle" "RShelloRSworldRSfooRS" RS && echo "found: $needle"
done
You can mix this solution with the case statement if you want both of the worlds:
PATTERN="|foo bar|baz|bla|"
case "$needle" in
xyz) echo "matched in a static part" ;;
*)
if [ -z "${PATTERN##*|${needle}|*}" ]; then
echo "$needle matched $PATTERN"
else
echo "not found"
fi
esac
Note
Sometimes it is good to remember you could do your whole script in awk(1p), which is also POSIX, but I believe this is for another answer.
This should work:
PATTERN="foo|bar|baz|bla"
shopt -s extglob
case "foo" in
#($(echo $PATTERN)))
printf "matched\n"
;;
*)
printf "no match\n"
;;
esac
Your pattern is in fact a list of patterns, and the separator | must be given literally. Your only option seems to be eval. However, try to avoid that if you can.
"You can't get there from here"
I love using case for pattern matching but in this situation you're running past the edge of what bourne shell is good for.
there are two hacks to solve this problem:
at the expense of a fork, you could use egrep
pattern="this|that|those"
if
echo "foo" | egrep "$pattern" > /dev/null 2>&1
then
echo "found"
else
echo "not found"
fi
You can also do this with only built-ins using a loop. Depending on the situation, this may make your code run a billion times slower, so be sure you understand what's going on with the code.
pattern="this|that|those"
IFS="|" temp_pattern="$pattern"
echo=echo
for value in $temp_pattern
do
case foo
in
"$list") echo "matched" ; echo=: ; break ;;
esac
done
$echo not matched
This is clearly a horror show, an example of how shell scripts can quickly spin out of control if you try to make do anything even a little bit off the map..
Some versions of expr (e.g. GNU) allow pattern matching with alternation.
PATTERN="foo\|bar\|baz"
VALUE="bar"
expr "$VALUE" : "$PATTERN" && echo match || echo no match
Otherwise, I'd use a tool like awk:
awk -v value="foo" -v pattern="$PATTERN" '
BEGIN {
if (value ~ pattern) {
exit 0
} else {
exit 1
}
}'
or more tersely:
awk -v v="foo" -v p="$PATTERN" 'BEGIN {exit !(v~p)}'
You can use it like this:
PATTERN="foo|bar|baz"
VALUE=oops
matches() { awk -v v="$1" -v p="$2" 'BEGIN {exit !(v~p)}'; }
if matches "$VALUE" "$PATTERN"; then
echo match
else
echo no match
fi
You can use regex matching in Bash:
PATTERN="foo|bar|baz|bla"
if [[ "foo" =~ $PATTERN ]]
then
printf "matched\n"
elif . . .
. . .
elif . . .
. . .
else
printf "no match\n"
fi
This obviates the need for escaping or anything else:
PATTERN="foo bar baz bla"
case "foo" in
${PATTERN// /|})
printf "matched\n"
;;
*)
printf "no match\n"
;;
esac

Resources