rpm conditional with substring? - if-statement

I have a spec file that uses a conditional like
%if "%{pkgname}" = "wobble"
Requires: extra-thing
....
%endif
and now need to treat wobble-thing, wobble-otherthing and any other wobble* as satisfying the same conditon. Easy, you'd think.
I can't find any way to do it without giving up on the awfulness that is spec files and preprocessing the file.
Unfortunately preprocessing the file won't fly in this context, it'll upturn a whole build chain that expects to be able to just rpmbuild the spec.
There's lots of undocumented magic in rpm and rpmbuild, like the ${error:blah} and %{expand:..othermacros...} and %global stuff. And even simple relational operations like %if %{value} < 42 don't seem to actually be documented anywhere.
Does anyone know of a string-prefix or string-infix pattern matching operator?
I'm looking for the spec equivalent of bash's if [[ "${VAR}" == *"substring"* ]]; then construct.
Edit: To be clear, the reason I'm not just using shell conditionals is that I need to affect rpm metadata. I thought it was obvious that I'd use shell if if that was an option. I've edited to show more clearly above.
Edit: To help other people find this, this is about string pattern matching in rpm. Complex conditionals in rpm. Conditional sections in spec files. String prefix, infix or suffix operators and tests in rpmbuild.

You cannot use regexp or wildcards. But you can use "or".
%if "%{pkgname}" == "wobble" || "%{pkgname}" == "wobble-thing"
..
%endif
or you can do the evaluation in the shell
%global yourmacro %(/usr/bin/perl ...%{pkgname}... )
where /usr/bin/perl ... can be any script and yourmacro is set to value of stdout of this script.

Actually, you can do it without needing Lua, and you can use exactly the if [[ ... ]] construct in BASH that you mentioned wanting to use in your original question.
To illustrate how, let's look at the header section of the specfile for the tmux RPM I use. It must build unmodified on any host running RHEL/CentOS versions 6, 7, and 8 (and/or any other rebuilds, like Oracle's). This is what I have:
%global name tmux
%global version 3.1b
%global release 1%{?dist}
%global _hardened_build 1
Summary: A terminal multiplexer
Name: %{name}
Version: %{version}
Release: %{release}
# Mostly ISC-licensed, but some files in compat/ are 2-/3-clause BSD.
License: ISC/BSD
URL: https://tmux.github.io/
Source0: https://github.com/tmux/%{name}/releases/download/%{version}/%{name}-%{version}.tar.gz
Source1: bash_completion_tmux.sh
BuildRequires: %(/bin/bash -fc 'if [[ %{name} == tmux* ]]; then echo make ; else echo nomake ; fi') %(/bin/bash -fc 'if [[ %{name} == wobble* ]]; then echo wobble ; fi')
BuildRequires: gcc, ncurses-devel, %{expand:%(/bin/bash -c 'if [[ %{?rhel}%{!?rhel:9} -le 6 ]]; then echo libevent2-devel ; else echo libevent-devel ; fi')}
Requires: %{expand:%(/bin/bash -c 'if [[ %{?rhel}%{!?rhel:9} -le 6 ]]; then echo libevent2 ; else echo libevent ; fi')} >= 2.0
The tricky part with this RPM is that RHEL6 supplies the libevent package, but it's version 1.4.13, which is too old for tmux to build against successfully. (It requires libevent 2.x.) The good news, though, is that RHEL6 actually has both versions! The libevent2 RPM has version 2.0.21, which is recent enough to work for tmux. But I can't just list libevent2-devel as my build dependency; that package is not available for RHEL 7 or 8 because their default libevent RPM is version 2 already. Nor can I list libevent-devel as the build dependency because, while the libevent and libevent2 packages can be installed side-by-side, the libevent-devel and libevent2-devel RPMs conflict.
As you can see in my specfile above, my solution to this challenge is to use RPM macros to inject either the string libevent-devel or libevent2-devel into the BuildRequires: header value based on the BASH-based comparison of the value of the %{rhel} macro (to which /etc/rpm/macros.dist assigns the value of 6, 7, or 8 on RHEL 6, 7, or 8, respectively). For RHEL6 (and prior, theoretically...RHEL5 is dead, and RHEL4 and earlier are super-duper-dead!), the latter value is used whereas the former value is used for RHEL7 and RHEL8.
I also added a couple contrived string-matching macro invocations just to illustrate that it works correctly. My actual specfile contains neither the first BuildRequires: line nor the final Requires: line (because it's unnecessary/redundant); I added them specifically for this exercise to demonstrate that the conditionals work for both build-time and run-time dependencies.
How do I know they worked? I checked:
$ rpm -qp --qf '[%|SOURCERPM?{%25{=NEVRA}}:{%21{=NEVR}.src}|.rpm: %{REQUIREFLAGS:deptype}: %{REQUIRENEVRS}\n]' tmux-3.1b-1.el6.x86_64.rpm tmux-3.1b-1.el6.src.rpm tmux-3.1b-1.el8.x86_64.rpm tmux-3.1b-1.el8.src.rpm | fgrep 'manual:'
tmux-3.1b-1.el6.x86_64.rpm: manual: libevent2 >= 2.0
tmux-3.1b-1.el6.src.rpm: manual: make
tmux-3.1b-1.el6.src.rpm: manual: gcc
tmux-3.1b-1.el6.src.rpm: manual: ncurses-devel
tmux-3.1b-1.el6.src.rpm: manual: libevent2-devel
tmux-3.1b-1.el8.x86_64.rpm: manual: libevent >= 2.0
tmux-3.1b-1.el8.src.rpm: manual: gcc
tmux-3.1b-1.el8.src.rpm: manual: libevent-devel
tmux-3.1b-1.el8.src.rpm: manual: make
tmux-3.1b-1.el8.src.rpm: manual: ncurses-devel
Note that the correct libevent2 >= 2.0 dependency (with the 2 on the end) shows up in the RHEL6 RPM; the RHEL8 RPM, on the other hand, shows libevent >= 2.0 (without the 2 on the end), again exactly as it should. Additionally, a build dependency on make shows up in both SRPMs, and both nomake and wobble are absent, proving that the pattern match conditionals on tmux* and wobble* worked exactly as they should.

You can indeed use Lua scripting, though it requires some odd incantations. Here's how to add a starts_with function-like macro to your rpm spec file that you can use in %if conditions.
# Define the Lua starts_with function we want to expose
%{lua:
function starts_with(str, start)
return str:sub(1, #start) == start
end
}
# Define the rpm parametric macro starts_with(str,prefix) that
# calls Lua and maps "false"=>"0" and "true"=>"1"
#
# Note that we need to inject the parameters %1 and %2 to a
# string-quoted version of the Lua macro, then expand the whole
# thing.
#
%define starts_with(str,prefix) (%{expand:%%{lua:print(starts_with(%1, %2) and "1" or "0")}})
# Finally we can use the parametric macro
#
%if %{starts_with "wobble-boo" "wobble"}
Requires: wobble
%endif
What happens here is that:
%{starts_with "wobble-boo", "wobble}
expands to
%{expand:%%{lua:print(starts_with("wobble-boo", "wobble") and "1" or "0")}}
which expands to
%{lua:print(starts_with("wobble-boo", "wobble") and "1" or "0")}
which executes the Lua function starts_with, which tests if the left-anchore substring of "str" that's the same length as "start" is equal to "start". If that's true it returns "1"; if it's false it returns "0". That's because rpm doesn't recognise false as false.
So what we're doing here is calling a Lua function from a parametric rpm macro and adapting the return value.
Nifty. Painful that rpm needs this kind of hack for a simple task like this, and that it's almost totally undocumented. But nifty.

Here is a way to do it in the spec using %define.
%global original_string SomeLongStringHere
%global matching_string LongString
%global nomatch_string NoMatchHere
%define matchTwoStrings(n:) \
string_one=%1 \
string_two=%2 \
if [[ "${string_one}" = *"${string_two}"* ]]; then \
echo "It matches" \
else \
echo "It doesn't match" \
fi \
%{nil}
# Then somewhere later in the .spec, e.g., I used the %post section
%post
%matchTwoStrings "%{original_string}" "%{matching_string}"
%matchTwoStrings "%{original_string}" "%{nomatch_string}"
Because this is in the %post section, it will print out during the .rpm installation.
It matches
It doesn't match

Related

How to programmatically extract the Python version in a shell script?

With the python --version as input in the terminal, I'd like to confirm (also in the shell in the form of shell programming) whether any python versions installed are 3 or higher. For example,
Python 3.2.1
Would yield True or some form of confirmation, where as
python2.2p1 or Python 2.1.4 would yield False.
I've tried using regex through sed or grep, but can't get it.
Note, this is a different matter from what some think to be a duplicate of, as I'm asking the shell programming code that can automate confirmation of python3 or higher being installed, not a way to manually check it (just entering python --version).
With the python --version as input in the terminal, I'd like to confirm (also in the shell in the form of shell programming) whether any python versions installed are 3 or higher.
If the python command launches any version of cpython, then the output of python --version (to its stderr, not stdout) will have the form:
Python X.Y.Z
You can test that in a shell script like so:
if python --version 2>&1 | grep -q '^Python 3\.'; then
# It's python 3 ...
else
# It's not python 3; maybe not installed
fi
Note that the output you originally presented in the question indicated that Python was not installed at all. The above approach will execute the else branch in that event. Note also that it is possible for Python to be installed with a different name -- for example, on the system where I am typing this, python is Python 2.7.5, but Python 3.4.8 is available via the command python3. You could extend the above to test some possible alternative names for Python 3, but you cannot safely, reliably, or efficiently perform an exhaustive test for whether Python 3 is installed at all, under any name or path.
The easiest way to do this in Bash is using sort -V.
To solve the problem of comparing version strings in general in bash, here is an simple compare() function that accepts two arguments a and b and returns true if a <= b. And then some unit tests showing its validity for various Python version inputs:
# test.sh
compare() {
local a=$1 ; local b=$2
[ "$( (echo "$a" ; echo "$b") | sort -V | head -1)" == "$a" ]
}
test22p1() {
compare "2.2p1" "3.0.0"
assertTrue "$?"
}
test214() {
compare "2.1.4" "3.0.0"
assertTrue "$?"
}
test300() {
compare "3.0.0" "3.0.0"
assertTrue "$?"
}
test372() {
compare "3.7.2" "3.0.0"
assertFalse "$?"
}
. shunit2
Output:
▶ bash test.sh
test22p1
test214
test300
test372
Ran 4 tests.
OK
(Those unit tests assume that shunit2 is installed of course.)
About the function:
It just echoes $a then $b on two separate lines, pipes into sort -V, and takes the head. If the head is equal to the left-hand side, true is returned ; otherwise if it is equal to the right-hand side, false is returned.
In your question you mentioned you really want to know if Python 3 or greater is installed, so you could modify it and have something like this instead:
python3_installed() {
local a b
a=$(python --version 2>&1 | perl -pe 's/python *//i') ; b=3
[ "$( (echo "$a" ; echo "$b") | sort -V | head -1)" == "$b" ]
}
This function will compute the actual version of Python installed, and compare it to "3" using sort -V and return false unless 3 or greater is installed.
Note use of Perl there to do a case-insensitive regex search and replace. (Not all sed's have the case-insensitive ability.)
The great thing about doing it this way is you can then have readable code when you call it, like:
if python3_installed ; then
# yes it is!
else
# ...
fi
Finally, according to the docs for sort -V (from the BSD manual; POSIX doesn't specify a -V option, but most sorts seem to have it):
`-V, --version-sort`
Sort version numbers. The input lines are treated as file names in form PREFIX VERSION SUFFIX, where SUFFIX matches the regular expression (.([A-Za-z~][A-Za-z0-9~]*)?)*. The files are compared by their prefixes and versions (leading zeros are ignored in version numbers, see example below). If an input string does not match the pattern, then it is compared using the byte compare function. All string comparisons are performed in C locale, the locale environment setting is ignored.
Here is sample awk script to do the job:
echo $(python --version) | awk 'BEGIN{found="false"}/python-3/{found="true"}END{print found}'

Effective way of detecting X11 vs Wayland, preferrably with CMake

So I've done some Google searching and this is something that has very little knowledge out there. What would be an effective and foolproof way of detecting whether X11 or Wayland is in use, preferrably at compile-time and with CMake? I need to apply this to a C++ project of mine.
The accepted answer is very inaccurate and dangerous. It just runs loginctl to dump a large list of user-sessions and greps every line with a username or other string that matches the current user's name, which can lead to false positives and multiple matching lines. Calling whoami is also wasteful. So it's harmful, and inaccurate.
Here's a much better way to get the user's session details, by querying your exact username's details and grabbing their 1st session scope's id.
This is a Bash/ZSH-compatible one-liner solution:
if [ "$(loginctl show-session $(loginctl user-status $USER | grep -E -m 1 'session-[0-9]+\.scope' | sed -E 's/^.*?session-([0-9]+)\.scope.*$/\1/') -p Type | grep -ic "wayland")" -ge 1 ]; then
echo "Wayland!"
else
echo "X11"
fi
I really wish that loginctl had a "list all sessions just for a specific user", but it doesn't, so we have to resort to these tricks. At least my trick is a LOT more robust and should always work!
I assume you want to evaluate the display server during compile time, when calling CMake, instead of for every compilation. That's how CMake works and hot it should be used. One downside is, that you have to re-run CMake for every changed display server.
There is currently no default way to detect the running display server. Similar, there is no default code snippet to evaluate the display server by CMake. Just pick one way of detecting the display server that manually works for you or your environment respectively.
Call this code from CMake and store the result in a variable and use it for your C++ code.
For example loginctl show-session $(loginctl | grep $(whoami) | awk '{print $1}') -p Type works for me. The resulting CMake check is
execute_process(
"loginctl show-session $(loginctl | grep $(whoami) | awk '{print $1}') -p Type"
OUTPUT_VARIABLE result_display_server)
if ("${resulting_display_server}" EQUALS "Type=x11")
set(display_server_x11 TRUE)
else()
set(display_server_x11 FALSE)
endif()
Probably you have to fiddle around with the condition and check for Type=wayland or similar to get it properly working in your environment.
You can use display_server_x11 and write it into a config.h file to use it within C++ code.

Using regex in git shell when checking out multiple branches

If I have the following git shell command:
for branch in `git branch -a | grep remotes | grep -v master | sed 's/^.*old\/\(.*\)/\1/g'`; do git branch --track $branch remotes/old/$branch; done
This checks out every single remote branch that exists on the old remote and tracks them using the same name that they have on that remote. However, what if I wanted to slightly change the name that the local branches are checked out has?
What if I have the following remote branches:
release/1.2.1.0
release/1.2.1.1
And I want to check them out under the same parent folder release but I only want the last 3 digits in the version number. So I want my local branches to be:
release/2.1.0
release/2.1.1
I have a simple javascript regex that matches the last 3 digits of the version string: (?:\d\.)(\d.*)
This uses a non-matching group to toss out the first digit followed by the period. The question is, how do I apply that regex to the $branch variable in the git shell bash script above?
First, avoid git branch for loops like this. The correct tool here is git for-each-ref, which is designed to work with scripting languages (git branch is aimed at users and the output format may change in the future, for instance).
To loop over all remote-tracking branches, simply tell for-each-ref to scan the remote-tracking branch namespace. Since you want, more specifically, the remote named old, you can do that very easily by adding /old as well:
git for-each-ref refs/remotes/old
The output here defaults to a triple of objectname objecttype refname. We only care about the refname part (and we can use the :short modifier to drop refs/remotes/ as well, if we like, although we still need to drop the old/ too so we could get away without the modifier). Thus we want to include --format=%(refname:short).
Moving on to bash, bash has built-in regular expression support. Its RE syntax is not quite the same, though, so your existing RE must change. Here is one that probably works for your needs:
bash$ x=1.2.3.4
bash$ [[ $x =~ ([0-9]\.)([0-9.]+) ]] && echo ${BASH_REMATCH[2]}
2.3.4
(There is a bit of subtlety here: using $x changes the way the =~ match applies, which in our case is probably good. As an old school Unix person I generally prefer using expr myself, but in this case I might resort to doing this in Python, which has Perl-style REs, and Javascript/ECMAscript REs are modeled on Perl's. But all that is more or less irrelevant. The most important is that this RE is slightly sub-par as a version number matcher. For instance, it matches strings like "1.3..6". We're safe in that these are invalid branch names—double dots are verboten since they would conflict with the set subtraction syntax in gitrevisions—but it's generally a bit sloppy; with some work we could come up with a tighter expression. It also fails to match revisions starting with two or more digits, but your original RE did as well, so I left that in on purpose.)
Reading in a loop in shell, using -r is generally wise (see Etan Reisner's comment), although in this case we could omit it safely since git controls branch names. I will use it in the example just for form's sake.
Putting these all together:
warn() {
echo "warning: $#" 1>&2
}
# Given an input name release/\d\.(\d|\.)+, make
# a local branch named release/\2 (more or less).
make_local_release_branch() {
local relnum newname
relnum=${1#old/release/}
[[ $relnum =~ ([0-9]\.)([0-9.]+) ]] || {
warn "remote-tracking branch $1 does not conform to name style, ignored"
return
}
newname=release/${BASH_REMATCH[2]}
git rev-parse -q --verify refs/heads/$newname >/dev/null && {
warn "branch $newname already exists, remote-tracking branch $1 ignored"
return
}
git branch --track $branch $1
}
git for-each-ref --format='%(refname:short)' refs/remotes/old |
while read -r rmtbranch; do
case $rmtbranch in
old/release/[0-9]*) make_local_release_branch "$rmtbranch";;
*) warn "skipping remote branch $rmtbranch -- not old/release/[digit]";;
esac
done
(this whole thing is entirely untested).

Advanced pattern matching in Makefile

Is it possible to create a Makefile pattern matching with two or three varying patterns? I'm using Gnu make.
In my current set-up, in simplified form, I'm using two Bash for-loops in order to convert a certain set of files to another set, and to create the final result file. Example:
#!/bin/bash
XMIN=$1
XMAX=$2
YMIN=$3
YMAX=$4
z=$5
FINAL_LIST=
for y in `seq $YMIN $YMAX`;
do
SOURCE_LIST=
echo Processing column $y
for x in `seq $XMIN $XMAX`;
do
# Convert from file source/something_${x}_${y}_${z} to
# target/something_else_$${x}_${y}_${z}
echo Processing X ${x} Y ${y} with Z ${z}
# do_something
SOURCE_LIST+="target/something_else_$${x}_${y}_${z} "
done
# Create something for this line
echo Processing ${SOURCE_LIST} target_line_${y}_${z}
# process the line
FINAL_LIST+="target_line_${y}_${z} "
done
# Finally, compose the final thing
echo Process the final result: ${FINAL_LIST} result_${z}
# process the final result
# We're done
I would like to make this more effectively with Makefile, as it would allow me to execute things in parallel, and also it would take care, that "line results" are re-generated only when something changes for that particular line.
I'm already using Makefile to convert single datafiles to another format, with simple pattern matching. Makefile is very good in handling my base of >500k datafiles - it can very fast detect changed source files and execute the conversion only for the changed datafiles.
The problem here is that I don't know, how to make Makefile patterns with more than one varying pattern. Following is an easy pattern:
%.target : %.source
# do something
But I don't know, whether the following would be possible (as pseudocode):
<var_pat_Z>_<var_pat_Y>.target: <var_pat_Z>_<var_pat_Y>.source
# do something else
It is not necessary to implement this with Makefile, but I would still need to find a way to detect changed source files, and the capability to execute things in parallel. Currently I'm handling those detections in my bash scripts, and the parallelization by executing bash scripts in parallel with Gnu parallel command. Anyway, that is most likely not the optimal way.
If I understood your question correctly, you have a bunch of *.source files, and want a rule that turns each into a *.target file, while picking two sub-strings from whatever the * expands to.
Why not pick the stem in $* apart at the underscore? Here's a solution.
If you have these files
$ ls *.source
1_1.source
1_2.source
1_3.source
a_b.source
foo_bar.source
then running this GNUmakefile's default target
# all should depend on all targets for which a source exists.
all: $(shell echo *.source | sed 's/source/target/g')
%.target: %.source
#z="$*" y="$*"; \
z=$${z%%_*} y=$${y##*_}; \
echo z=$$z y=$$y
will give you
$ gmake
z=1 y=1
z=1 y=2
z=1 y=3
z=a y=b
z=foo y=bar

Conditional statement using rexep with bash

I want to use the "conditional regexp" structure from bash, present since the third version of bash (circa 2004).
It's supposed to go like this:
if [[ $string =~ $regexp ]]; then
#do smthg
else
#do somthg else
fi
So here's my code, following this structure, and its role is to check if the name contained in SSID is presend in the output from iw dev wlan0 link :
if [[ $(iw dev wlan0 link) =~ $SSID+ ]]; then
#do sthming
else
echo "wrong network"
fi
For some reason that i can't decipher, this statement works pretty well if I run it right into the bash shell, like
if [[ $(iw dev wlan0 link) =~ $SSID+ ]]; then echo found; else echo not found; fi
But if i run it inside the script its contained, it'll spit out:
scripts/ssidchecker.sh: 22: [[: not found
22 being the line of the "fi" keyword. The strangest thing is that it will always execute the code contained in the "else" statement
Is "not found" meant to indicate me that the regexp dind't find anything in that string? Is it a real error message?
Starting Note: Answer created with input from comments in question, especially in response of the last comment.
First, what is [[? It is a shell keyword.
samveen#maverick:~$ type [[
[[ is a shell keyword
This means that [[ is an internal keyword of bash, not a command and thus will not work with other shells. Thus, your error output
scripts/ssidchecker.sh: 22: [[: not found
means that the shell you're using is probably
Not bash
A version of bash older than 2.02 ([[ introduced in Bash-2.02)
Given that 2.02 is a really really old version (pre Y2K), all this just points to the fact that the shell you're using to run the script is probably not /bin/bash, instead probably being /bin/sh which is the most common path used for Bourne shell scripts shebang (the #!) line.
Please change that to /bin/bash or explicitly run bash scripts/ssidchecker.sh and you're good to go.
As to why the else section was always executed, the [[ command not being found is the same as a failure (non-zero return value), from the viewpoint of if. Thus the else section is executed.
samveen#maverick:~$ /bin/whosyourdaddy
-bash: /bin/whosyourdaddy: No such file or directory
samveen#maverick:~$ echo $?
127
As a side note on portability, the bash hackers wiki also says the following about the [[ keyword:
Amongst the major "POSIX-shell superset languages" (for lack of a
better term) which do have [[, the test expression compound command is
one of the very most portable non-POSIX features. Aside from the =~
operator, almost every major feature is consistent between Ksh88,
Ksh93, mksh, Zsh, and Bash. Ksh93 also adds a large number of unique
pattern matching features not supported by other shells including
support for several different regex dialects, which are invoked using
a different syntax from Bash's =~, though =~ is still supported by ksh
and defaults to ERE.
Thus your script will probably fail on the =~ even if the shell supports [[, in case the shell isn't bash but supports [[.