bash regex to match semantic version number - regex

I have the following:
versionNumber=$(sw_vers -productVersion) # Finds version number
versionShort=${versionNumber:0:4} # Cut string to 1 decimal place for calculation
which works when versions are like this:
10.9.2
10.9.5
but it will not match
10.10.3
as it will return only
10.1
but I want the versionShort to be set to
10.10
I am wanting to match the major version, the first dot and the minor version as above.

Regexpless solution - cut off last dot and whatever follows it:
versionShort=${versionNumber%.*}

Regexp solution:
[[ $versionNumber =~ ^[0-9]+\.[0-9]+ ]] && echo "${BASH_REMATCH[0]}"
It will always print first two numbers, for example all these:
10.5
10.5.9
10.5.8.2
Will result in 10.5 output. You can also add an else clause to check if something wrong happened (no match found).
Here is a longer version:
if [[ $versionNumber =~ ^[0-9]+\.[0-9]+ ]]; then
versionShort=${BASH_REMATCH[0]}
else
echo "Something is wrong with your version" >&2
fi

I had a similar question, but I needed access to all 3 segments. I did a bit of research and testing and I found this to work well
product_version=$(sw_vers -productVersion)
semver=( ${product_version//./ } )
major="${semver[0]}"
minor="${semver[1]}"
patch="${semver[2]}"
echo "${major}.${minor}.${patch}"
To answer this question directly, you could
product_version=$(sw_vers -productVersion)
semver=( ${product_version//./ } )
major="${semver[0]}"
minor="${semver[1]}"
patch="${semver[2]}"
versionShort="${major}.${minor}"
or you can use less variables
product_version=$(sw_vers -productVersion)
semver=( ${product_version//./ } )
versionShort="${semver[0]}.${semver[1]}"

https://github.com/fsaintjacques/semver-tool
https://github.com/fsaintjacques/semver-tool/blob/master/src/semver
SEMVER_REGEX="^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(\\-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"
function validate-version {
local version=$1
if [[ "$version" =~ $SEMVER_REGEX ]]; then
# if a second argument is passed, store the result in var named by $2
if [ "$#" -eq "2" ]; then
local major=${BASH_REMATCH[1]}
local minor=${BASH_REMATCH[2]}
local patch=${BASH_REMATCH[3]}
local prere=${BASH_REMATCH[4]}
local build=${BASH_REMATCH[5]}
eval "$2=(\"$major\" \"$minor\" \"$patch\" \"$prere\" \"$build\")"
else
echo "$version"
fi
else
error "version $version does not match the semver scheme 'X.Y.Z(-PRERELEASE)(+BUILD)'. See help for more information."
fi
}

Related

Bash script with regex and capturing group

I'm working on a bash script to rename automatically files on my Synology NAS.
I have a loop for the statement of the files and everything is ok until I want to make my script more efficient with regex.
I have several bits of code which are working like as expected:
filename="${filename//[-_.,\']/ }"
filename="${filename//[éèēěëê]/e}"
But I have this:
filename="${filename//t0/0}"
filename="${filename//t1/1}"
filename="${filename//t2/2}"
filename="${filename//t3/3}"
filename="${filename//t4/4}"
filename="${filename//t5/5}"
filename="${filename//t6/6}"
filename="${filename//t7/7}"
filename="${filename//t8/8}"
filename="${filename//t9/9}"
And, I would like to use captured group to have something like this:
filename="${filename//t([0-9]{1,2})/\1}"
filename="${filename//t([0-9]{1,2})/${BASH_REMATCH[1]}}"
I've been looking for a working syntax without success...
The shell's parameter expansion facility does not support regular expressions. But you can approximate it with something like
filename=$(sed 's/t\([0-9]\)/\1/g' <<<"$filename")
This will work regardless of whether the first digit is followed by additional digits or not, so dropping that requirement simplifies the code.
If you want the last or all t[0-9]{1,2}s replaced:
$ filename='abt1cdt2eft3gh'; [[ "$filename" =~ (.*)t([0-9]{1,2}.*) ]] && filename="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"; echo "$filename"
abt1cdt2ef3gh
$ filename='abt1cdt2eft3gh'; while [[ "$filename" =~ (.*)t([0-9]{1,2}.*) ]]; do filename="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"; done; echo "$filename"
ab1cd2ef3gh
Note that the "replace all" case above would keep iterating until all t[0-9]{1,2}s are changed, even ones that didn't exist in the original input but were being created by the loop, e.g.:
$ filename='abtt123de'; while [[ "$filename" =~ (.*)t([0-9]{1,2}.*) ]]; do filename="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"; echo "$filename"; done
abt123de
ab123de
whereas the sed script in #tripleee's answer would not do that:
$ filename='abtt123de'; filename=$(sed 's/t\([0-9]\)/\1/g' <<<"$filename"); echo "$filename"
abt123de

FreeBSD Bash - Unable to code a condition with regex

I am trying to code a script for pfSense who is based on FreeBSD. The only part left who is giving me trouble is a condition with a regex. The simplified code is this :
RESPONSE='{"port":98989}'
REG='{"port":([0-9]*)}'
if [[ $RESPONSE =~ $REG ]]; then
PORT=${BASH_REMATCH[1]}
fi
With the trace mode enabled, the error returned is the following :
+ RESPONSE='{"port":98989}'
+ REG='{"port":([0-9]*)}'
+ '[[' '{"port":98989}' '=~' '{"port":([0-9]*)}' ]]
./pia-port-v2: [[: not found
I don't understand why the [[ is between single quote in the trace and it is probably why the "not found" error occurs.
Update
It is probably because pfSense's FreeBSD does not support bash and these instructions are bash only. I found that after writing this question and trying to find an answer.
Anybody have an alternative for bourne shell? The goal is to return the port number if the expression match.
I am new to script coding in unix like OS.
In the meantime, I look at grep, but it seems to apply the regex to file input only.
You should be able to use the expr utility to do this, but note that it use Posix basic regexps, which means that you need to backslash your parentheses to make them into captures:
response='{"port":98989}'
reg='{"port":\([0-9]*\)}'
port=$(expr "$response" : "$reg")
expr returns failure if the regex doesn't match, so you could use a shell conditional to test:
port=$(expr "$response" : "$reg") || { echo Failed; }
or
if ! port=$(expr "$response" : "$reg"); then
# Do something on failure
fi
With /bin/sh:
#!/bin/sh
response='{"port":98989}'
case $response in
'{"port":'[0-9]*'}')
port=${response#*:} # {"port":98989} --> 98989}
port=${port%'}'} # 98989} --> 98989
esac
printf 'response %s yields port %s\n' "$response" "$port"
Note that a case statement does not use regular expression but shell filename globbing patterns. Therefore, the pattern will only match a single digit and trigger for bogus strings like {"port":0xxx}.
If the response string is a JSON document:
$ response='{"port":98989}'
$ printf '%s\n' "$response" | jq .port
98989
There is trouble with ' and " when using [[ regexps (sometimes; not always) so I would try this instead (which works fine for me):
#!/bin/bash
REG=\{\"port\"\:\([0-9]\*\)\} # This line is altered
RESPONSE='{"port":98989}'
if [[ $RESPONSE =~ $REG ]]; then
echo funkar
fi

bash regular expression format

My code have problem with compare var with regular expression.
The main problem is problem is here
if [[ “$alarm” =~ ^[0-2][0-9]\:[0-5][0-9]$ ]]
This "if" is never true i dont know why even if i pass to "$alarm" value like 13:00 or 08:19 its always false and write "invalid clock format".
When i try this ^[0-2][0-9]:[0-5][0-9]$ on site to test regular expressions its work for example i compered with 12:20.
I start my script whith command ./alarm 11:12
below is whole code
#!/bin/bash
masa="`date +%k:%M`"
mp3="$HOME/Desktop/alarm.mp3" #change this
echo "lol";
if [ $# != 1 ]; then
echo "please insert alarm time [24hours format]"
echo "example ./alarm 13:00 [will ring alarm at 1:00pm]"
exit;
fi
alarm=$1
echo "$alarm"
#fix me with better regex >_<
if [[ “$alarm” =~ ^[0-2][0-9]\:[0-5][0-9]$ ]]
then
echo "time now $masa"
echo "alarm set to $alarm"
echo "will play $mp3"
else
echo "invalid clock format"
exit;
fi
while [ $masa != $alarm ];do
masa="`date +%k:%M`" #update time
sleep 1 #dont overload the cpu cycle
done
echo $masa
if [ $masa = $alarm ];then
echo ringggggggg
play $mp3 > /dev/null 2> /dev/null &
fi
exit
I can see a couple of issues with your test.
Firstly, it looks like you may be using the wrong kind of double quotes around your variable (“ ”, rather than "). These "fancy quotes" are being concatenated with your variable, which I assume is what causes your pattern to fail to match. You could change them but within bash's extended tests (i.e. [[ instead of [), there's no need to quote your variables anyway, so I would suggest removing them entirely.
Secondly, your regular expression allows some invalid dates at the moment. I would suggest using something like this:
re='^([01][0-9]|2[0-3]):[0-5][0-9]$'
if [[ $alarm =~ $re ]]
I have deliberately chosen to use a separate variable to store the pattern, as this is the most widely compatible way of working with bash regexes.

Shell: Checking if argument exists and matches expression

I'm new to shell scripting and trying to write the ability to check if an argument exists and if it matches an expression. I'm not sure how to write expressions, so this is what I have so far:
#!/bin/bash
if [[ -n "$1"] && [${1#*.} -eq "tar.gz"]]; then
echo "Passed";
else
echo "Missing valid argument"
fi
To run the script, I would type this command:
# script.sh YYYY-MM.tar.gz
I believe what I have is
if the YYYY-MM.tar.gz is not after script.sh it will echo "Missing valid argument" and
if the file does not end in .tar.gz it echo's the same error.
However, I want to also check if the full file name is in YYYY-MM.tar.gz format.
if [[ -n "$1" ]] && [[ "${1#*.}" == "tar.gz" ]]; then
-eq: (equal) for arithmetic tests
==: to compare strings
See: help test
You can also use:
case "$1" in
*.tar.gz) ;; #passed
*) echo "wrong/missing argument $1"; exit 1;;
esac
echo "ok arg: $1"
As long as the file is in the correct YYYY-MM.tar.gz format, it obviously is non-empty and ends in .tar.gz as well. Check with a regular expression:
if ! [[ $1 =~ [0-9]{4}-[0-9]{1,2}.tar.gz ]]; then
echo "Argument 1 not in correct YYYY-MM.tar.gz format"
exit 1
fi
Obviously, the regular expression above is too general, allowing names like 0193-67.tar.gz. You can adjust it to be as specific as you need it to be for your application, though. I might recommend
[1-9][0-9]{3}-([1-9]|10|11|12).tar.gz
to allow only 4-digit years starting with 1000 (support for the first millennium ACE seems unnecessary) and only months 1-12 (no leading zero).

How to check an input string in bash it's in version format (n1.n2.n3)

I've written an script that updates a version on a certain file. I need to check that the input for the user is in version format so I don't finish adding number that are not needed in those important files. The way I have done it is by adding a new value version_check which where I delete my regex pattern and then an if check.
version=$1
version_checked=$(echo $version | sed -e '/[0-9]\+\.[0-9]\+\.[0-9]/d')
if [[ -z $version_checked ]]; then
echo "$version is the right format"
else
echo "$version_checked is not in the right format, please use XX.XX.XX format (ie: 4.15.3)"
exit
fi
That works fine for XX.XX and XX.XX.XX but it also allows XX.XX.XX.XX and XX.XX.XX.XX.XX etc.. so if user makes a mistake it will input wrong data on the file. How can I get the sed regex to ONLY allow 3 pairs of numbers separated by a dot?
Change your regex from:
/[0-9]\+\.[0-9]\+\.[0-9]/
to this:
/^[0-9]*\.[0-9]*\.[0-9]*$/
You can do this with bash pattern matching:
$ for version in 1.2 1.2.3 1.2.3.4; do
printf "%s\t" $version
[[ $version == +([0-9]).+([0-9]).+([0-9]) ]] && echo y || echo n
done
1.2 n
1.2.3 y
1.2.3.4 n
If you need each group of digits to be exactly 2 digits:
[[ $version == [0-9][0-9].[0-9][0-9].[0-9][0-9] ]]