Skip to content

Some tips on RPM conditional macros

Admittedly, I hadn't been messing around with spec files too much, but recently I had to and I found some things that bit me and make me spend some time trying to figure out. Here I'm talking about RPM version 4.x, which is still the default in most (all?) distros.

Specifically, my troubles were with the %if conditional macro, whose exact behavior and syntax is sparsely documented and may appear to do strange things at times, so I'll just put my findings here to remember them (and hopefully they may perhaps be useful to others).

It all started when I naively tried to use this conditional to check whether a macro was defined (as most documentation that can be found on the net seems to suggest):

%if %{mymacro}
# ... do something
%endif

When the macro was defined, that worked; when it was not defined, I was getting the dreaded error

error: parse error in expression
error: /usr/src/foo/foo.spec:25: parseExpressionBoolean returns -1

A similar situation happened with this test for equality:

%if %{mymacro} == 100
# ... do something
%else
# ... do something else
%endif

When the macro was undefined, I was expecting either the whole %if/%else block to be skipped, or at least the %else branch to be executed (since something that's undefined is clearly not equal to 100). What was happening instead, was again the same "parse error in expression" error as before.

Trying to make it work, I did this:

%if "%{mymacro}" == "100"
# ... do something
%else
# ... do something else
%endif

Which appeared to work indeed - but as we'll see it wasn't working for the reason I thought, and could even produce false matches under some special circumstances (see below).

It still wasn't satisfactory, because I felt I wasn't really understanding what was going on, so I had that feeling that it all was working by chance. And still, I hadn't found a way to check something as simple as whether a macro is defined or not.

Finally, it occurred to me to add the following line at the very beginning of the %prep section in the spec file to see what was going on:

echo "the value of mymacro is --%{mymacro}--"

That helped a lot, because I discovered that if the macro is defined the expansion is

the value of mymacro is --100--

but when the macro is undefined, this is what happens:

the value of mymacro is --%{mymacro}--

that is, it is left untouched (rather than removed). So this is the reason why adding double quotes to the test seems to work: when the macro is undefined, the test executed is

"%{mymacro}" == "100"

(double quotes included) which is false. However, should one one day need to test against the literal string %{mymacro}, the test would actually succeed when the macro is undefined! Let's confirm this:

# spec file
%if "%{mymacro}" == "%%{mymacro}"
echo "equality test succeeded"
%endif

%% is used to put a literal percent sign.

If we invoke the spec file without defining %{mymacro}, the test succeeds:

$ rpmbuild -ba foo.spec
...
+ echo 'mymacro is --%{mymacro}--'
mymacro is --%{mymacro}--
+ echo 'equality test succeeded'
equality test succeeded
...

Granted, this is not very likely to happen in practice, but it still looks somewhat not clean.

What's needed, then, is a real way to check whether a macro is defined or not. We can't rely on the equality test we used above to determine that either, because the macro may have been defined, and have the literal value %{mymacro} assigned to it. Again this is very unlikely, but it's always better to make things in a clean way when possible.

The solution: conditional expansion

(I don't know if that is the right definition for this feature). There's a useful tool that the macro language used in the spec files has, and it's the %{?mymacro:value1} and %{!?mymacro:value2} syntax. The result of the expansion of those macros is value1 if %{mymacro} is defined, and value2 if %{mymacro} is undefined, respectively. If the condition they check is false, they expand to nothing.

Since the macro processor is recursive, this allows for the conditional definition of macros, for example:

%{?onemacro:%define anothermacro 100}

That defines the macro %{anothermacro} only if %{onemacro} is defined. But let's focus on the task at hand: determining whether a macro is defined (and after that, possibly do further tests on its value).

If we only need to check whether a macro is defined or not, these idioms seem to work:

%if %{?mymacro:1}%{!?mymacro:0}
# ... %{mymacro} is defined
%else
# ... %{mymacro} is not defined
%endif

The above expands either to 1 or 0 depending on whether %{mymacro} is defined or not.
This also seems to work:

%if 0%{?mymacro:1}
# ... %{mymacro} is defined
%else
# ... %{mymacro} is not defined
%endif

That expands to either 01 or 0, again corresponding to true or false for the %if.

If instead, we need to check that a macro has a specific value, this should work:

%if "%{?mymacro:%{mymacro}}%{!?mymacro:0}" == "somevalue"
# ... %{mymacro} is defined and has value "somevalue"
%endif

That expands to either the actual value of the macro, or 0 (in double quotes), either of which can safely be compared to "somevalue". If there can be no spaces in the values, the double quotes can be omitted. Obviously, the value returned when the macro is not defined doesn't have to be 0; it can be any value that's not somevalue.

Putting it all together, if we need to differentiate between the macro being unset or being set to a specific value, we can do this:

%if 0%{?mymacro:1}
# ... %{mymacro} is defined
%if "%{mymacro}" == "somevalue"
# ... %{mymacro} is defined and has value somevalue
%else
# ... %{mymacro} is defined, but has a value != somevalue
%endif
%else
# ... %{mymacro} is not defined
%endif

Again, if values can have no spaces, the double quotes can be omitted.

More elaborate variation of conditional expansion can be found in the official rpm documentation. For example, they use some sort of "function-like" macros as follows:

# Check if symbol is defined.
# Example usage: %if %{defined with_foo} && %{undefined with_bar} ...
%defined()      %{expand:%%{?%{1}:1}%%{!?%{1}:0}}
%undefined()    %{expand:%%{?%{1}:0}%%{!?%{1}:1}}

%{1} expands to the first "argument" passed to the macro. So when doing %{defined with_foo} what is done is actually

%{expand:%{?with_foo:1}%{!?with_foo:0}}

"expand" is like "eval", so the above effectively re-evaluates the part after expand: and eventually expands to either 1 or 0 depending on whether %{with_foo} was defined or not.

Update 27/09/2011: after some tests it looks like in conditional expansion the macro expands to its value by default if it's defined and to nothing if it's not, so the idiom

%{?mymacro:%{mymacro}}

can also be written just as

%{?mymacro}

Update 07/03/2017: thanks to Mihai for sending the following useful additions:

You're over-complicating things. The canonical way is to use something like

%if 0%{?var} <op> <val>

The only caveat is that if %{var} is not defined, this will essentially decay
into a check like:

%if 0 <op> <val>

which is problematic if your op is a logical operator.

For example:

%if 0%{?waldner} >= 23

works fine, whether %{waldner} is defined or not, but

%if 0%{?waldner} < 42

will evaluate to true if %{waldner} is either *really* less than 42, *or* undefined.

In that case, something like

%if 0%{?waldner} && 0%{?waldner} < 42

is more appropriate, as it will catch the undefined value early.

A lesser-known feature of RPM (I don't know why, but it's rarely ever used,
despite even "ancient" systems like SLE 11 or RHEL 5 supporting it) is grouping
of expressions.

With that, a statement such as

%if ( 0%{?waldner} && 0%{?waldner} < 42 ) || 0%{?ionic} >= 23

can be composed.

Running local script remotely (with arguments)

So, we have a script we want or need to run on loads of remote servers, but we don't want to copy it to every server, to avoid a maintenance nightmare. (Let's not focus on why we may find ourselves in that situation; it may be because there is no choice, or whatever reason. Agreed it's undesirable. That's life.)
Another use case is as follows: we need to develop a script that will run on a machine where the editing tools aren't as good as those we are used to; so we want to develop on our familiar local machine, but once in a while, while developing it, we need to run it on the target machine (presumably to test it).

If the code to run is short and simple, we can just put it inline:

$ ssh user@remote 'our code here'

However this quickly becomes difficult to type, prone to errors, and proper quoting can be a nightmare too. So let's assume that the script is stored in a file.

Method 1: stdin

Well, ssh runs a shell on the remote host, so why not do

$ ssh user@remote < local.sh

Sure, that works and looks easy right? But things start to change if we need to pass arguments to the script.

Let's use the following example script (real ones, of course, will be much more complex):

# local.sh
printf 'Argument is __%s__\n' "$@"

This code is representative of the task, because it lets us check that arguments are seen correctly by the script even when it runs remotely. This is the critical part; if that works, we don't have to worry about the rest of the code; that will mostly "just work" (with some caveats, noted at the end).

So, since the remote shell is reading stdin anyway, this should also work:

$ ssh user@remote 'bash' < local.sh
Argument is ____

And so should this (but doesn't):

$ ssh user@remote 'bash /dev/stdin' < local.sh
bash: /dev/stdin: No such device or address

What's happening here? Let's try to find out:

$ ssh user@remote 'ls -l /dev/stdin' < local.sh
lrwxrwxrwx 1 root root 15 2011-07-08 10:45 /dev/stdin -> /proc/self/fd/0
$ ssh user@remote 'ls -l /proc/self/fd/0' < local.sh
lrwx------ 1 user users 64 2011-08-10 11:04 /proc/self/fd/0 -> socket:[28463861]

So stdin exists but it's connected to a UNIX socket (this is part of how ssh sets things up when connecting). Why is it failing?

$ ssh user@remote 'strace bash /dev/stdin' < local.sh
...
open("/dev/stdin", O_RDONLY)            = -1 ENXIO (No such device or address)
...

Doing some research, it appears that Linux disallows open()ing a socket, and returns ENXIO when that is attempted. (And yes, "bash < /dev/stdin" fails equally). Can we work around that? Let's see if cat works:

$ ssh user@remote cat < local.sh 
# local.sh
printf 'Argument is __%s__\n' "$@"

Predictably, it works since cat just reads its stdin (which is set up before it is run) without explicitly attempting to open() it. So we can use this to accomplish our goal:

$ ssh user@remote 'cat | bash /dev/stdin' < local.sh
Argument is ____

This trick turns bash's stdin into a pipe (rather than a socket), which it can thus open successfully.

Update 05/04/2013: on recent enough versions of ssh, stdin is a pipe and not a socket, so the simpler version

$ ssh user@remote 'bash /dev/stdin' < local.sh

does actually work, and can be used in place of the more complicated cat | bash /dev/stdin in the following examples.

Now, this may just look like a fancy way of rewriting the original attempt, but it has an important advantage: since /dev/stdin looks like the name of the script to run to the remote shell, it allows us to specify arguments after it, as follows:

$ ssh user@remote 'cat | bash /dev/stdin arg1 arg2 arg3' < local.sh
Argument is __arg1__
Argument is __arg2__
Argument is __arg3__

(Thanks to Stéphane Chazelas and Marcel Bruinsma for suggesting the above ideas during an old discussion on comp.unix.shell).

We're almost done. So far, we are hardcoding the arguments in the single-quoted string; it would be nice to have a way of putting variables there. We can create a wrapper script that does the hard work for us:

#!/bin/bash
# runremote.sh
# usage: runremote.sh remoteuser remotehost arg1 arg2 ...

realscript=local.sh
user=$1
host=$2
shift 2

ssh $user@$host 'cat | bash /dev/stdin' "$@" < "$realscript"

The expansion of "$@" is replaced by the actual arguments. Let's run it:

$ runremote.sh user remote arg1 arg2 arg3
Argument is __arg1__
Argument is __arg2__
Argument is __arg3__
$ runremote.sh user remote arg1 "arg2 with spaces" arg3
Argument is __arg1__
Argument is __arg2__
Argument is __with__
Argument is __spaces__
Argument is __arg3__

Ok, so it's not perfect yet. The problem is that with ssh, the supplied command string is (re)evaluated by the remote shell, and that turns what is meant to be a single argument "arg2 with spaces" into three separate arguments. For the same reason, there may also be problems with other characters that are special to the shell like globbing characters, escapes and quotes. So, the wrapper script needs to escape the arguments it's given before putting them into the command string for the remote ssh. Since we want the wrapper to be transparent, and want to be able to supply arbitrarily complex arguments, the task can rapidly become an escaping nightmare, which is one of the things we wanted to avoid in the first place.
Fortunately, bash has just the right feature for this: the builtin printf command supports the %q specifier:

%q     causes printf to output the corresponding argument in a format that can be reused as shell input.

Let's try it:

$ printf '%q\n' "argument with space"
argument\ with\ space
$ printf '%q\n' "argument with 'single quotes'"
argument\ with\ \'single\ quotes\'
$ printf '%q\n' 'argument with "double quotes"'
argument\ with\ \"double\ quotes\"
$ printf '%q\n' 'argument with *? glob and $ other ` special { chars'
argument\ with\ \*\?\ glob\ and\ \$\ other\ \`\ special\ \{\ chars
$ foo=$(printf '%q\n' 'argument with *? glob and $ other ` special { chars')
$ echo "$foo"
argument\ with\ \*\?\ glob\ and\ \$\ other\ \`\ special\ \{\ chars
$ eval echo "$foo"
argument with *? glob and $ other ` special { chars

Looks good. Since arguments to a script can't be modified directly, we can use an array to store and modify them, then use the special "${array[@]}" construct to pass them (which behaves the same as "$@"). We can also generalize the wrapper to accept the name of the local script to run remotely, so:

#!/bin/bash
# runremote.sh
# usage: runremote.sh localscript remoteuser remotehost arg1 arg2 ...

realscript=$1
user=$2
host=$3
shift 3

declare -a args

count=0
for arg in "$@"; do
  args[count]=$(printf '%q' "$arg")
  count=$((count+1))
done

ssh $user@$host 'cat | bash /dev/stdin' "${args[@]}" < "$realscript"

Let's try it:

$ runremote.sh local.sh user remote 'arg1 with spaces and "quotes"' 'arg2 with *? glob and $ other ` special { chars'
Argument is __arg1 with spaces and "quotes"__
Argument is __arg2 with *? glob and $ other ` special { chars__

Now runremote.sh can be used to run a local script remotely with arbitrary arguments. Of course, quoting and/or escaping must still be done correctly locally if needed, so that the script sees the intended number of arguments.

Method 2: stdin, revisited

As a variation of the previous method, we could have the wrapper prepend some code to the local script so it magically finds its arguments already set, for example something like this:

#!/bin/bash
# runremote.sh
# usage: runremote.sh localscript remoteuser remotehost arg1 arg2 ...

realscript=$1
user=$2
host=$3
shift 3

# escape the arguments
declare -a args

count=0
for arg in "$@"; do
  args[count]=$(printf '%q' "$arg")
  count=$((count+1))
done

{
  printf '%s\n' "set -- ${args[*]}"
  cat "$realscript"
} | ssh $user@$host "cat | bash /dev/stdin"

Note the "${args[*]}" expansion, which should not normally be used, but here it's useful as it expands as a single argument (whereas "${args[@]}" would expand to multiple arguments, each of which would be formatted by printf's format specifier - not what we want here).
Again this works:

$ runremote.sh local.sh user remote 'arg1 with spaces and "quotes"' 'arg2 with *? glob and $ other ` special { chars'
Argument is __arg1 with spaces and "quotes"__
Argument is __arg2 with *? glob and $ other ` special { chars__

Update 19/02/2012: thanks to Joseph's comment I realized that using /dev/stdin with this approach isn't needed at all, since we don't pass any argument directly on the command line. So the code could be changed to directly invoke the shell on the remote system:

#!/bin/bash
# runremote.sh (revised, not dependent upon /dev/stdin)
# usage: runremote.sh localscript remoteuser remotehost arg1 arg2 ...

realscript=$1
user=$2
host=$3
shift 3

# escape the arguments
declare -a args

count=0
for arg in "$@"; do
  args[count]=$(printf '%q' "$arg")
  count=$((count+1))
done

{
  printf '%s\n' "set -- ${args[*]}"
  cat "$realscript"
} | ssh $user@$host "bash -s"

This makes it possible to use this approach even on systems that don't have the special /dev/stdin file (or equivalent). (The -s switch seems to be optional with bash, as it will read from stdin anyway, but it may be required with other shells). Thanks Joseph!

Method 3: copy-and-execute

This uses a different approach; the wrapper just copies the file to the remote machine, and runs it:

#!/bin/bash
# runremote.sh
# usage: runremote.sh localscript remoteuser remotehost arg1 arg2 ...

realscript=$1
user=$2
host=$3
shift 3

# escape the arguments
declare -a args

count=0
for arg in "$@"; do
  args[count]=$(printf '%q' "$arg")
  count=$((count+1))
done

scp -q "$realscript" "$user"@"$host":/some/where
ssh $user@$host bash "/some/where/$realscript" "${args[@]}"

This does work, however in my opinion is less preferable because it leaves the file around on the remote machine; ok, the wrapper could be changed to remove it after it's run, but it still looks less clean than the other methods (also, it needs a place where to save the file remotely, and it makes multiple ssh connections every time, one to copy the file, one to run it, and optionally another to delete it).
However it does have the advantage that the script's standard input remains available (see Caveats below).

Caveats

The first thing to note is that the scripts that we are going to run, even if they reside on the local machine, need to behave correctly on the remote machine, so all the paths, commands invoked, temporary files and other references have to be valid on the remote machine, not the local one. Also the features that it uses must be supported by the remote shell that you invoke. This may seem obvious, but it is easily overlooked, especially if the script is being developed on the local machine.

The second thing to consider is that, if the script is run through the remote shell's standard input, it can't use commands that read from unredirected standard input; if it did, those commands would swallow part or all of the script themselves. So ensure that all such commands have their stdin appropriately redirected, or alternatively, use method three above ("copy-and-execute").

Update 26/08/2011: the method that uses /dev/stdin works fine with Perl too (mostly, the same caveats apply). So now the runremote.sh script can be made even more general by accepting an argument that specifies the command interpreter to run on the remote machine:

#!/bin/bash
# runremote.sh
# usage: runremote.sh localscript interpreter remoteuser remotehost arg1 arg2 ...

realscript=$1
interpreter=$2
user=$3
host=$4
shift 4

declare -a args

count=0
for arg in "$@"; do
  args[count]=$(printf '%q' "$arg")
  count=$((count+1))
done

ssh $user@$host "cat | ${interpreter} /dev/stdin" "${args[@]}" < "$realscript"

Poor man’s HTTP VPN

There is a program floating on the net called httptunnel which is essentially a tool to forward arbitrary TCP connections over HTTP, similar for example to SSH's port forwarding capabilities (except, of course, that SSH carries the data over the SSH channel, while httptunnel puts the data into what appears to be "normal" HTTP traffic).
Now such a tool has a great potential, and as usual, it can be used for many purposes, both legitimate and questionable. So before going further, let me fully quote the contents of the file DISCLAIMER that comes with httptunnel:

I hereby disclaim all responsibility for this hack. If it backfires on
you in any way whatsoever, that's the breaks. Not my fault. If you
don't understand the risks inherent in doing this, don't do it. If you
use this hack and it allows vicious vandals to break into your
company's computers and costs you your job and your company millions
of dollars, well that's just tough nuggies. Don't come crying to me.

What we will do here is create a full fledged VPN over HTTP using httptunnel, which means that the potential is even greater, and the above disclaimer holds even more. Let me state it again: you are the sole responsible for your actions. Keep this well in mind.

The way httptunnel works is in a client-server fashion: the server (the hts program) listens on an arbitrary TCP port (8888 by default) and when a connection comes in from the client part, it forwards it to an arbitrary host and port (specified on the command line). For example:

server # hts -F 127.0.0.1:110 12345    # listen on port 12345 and forward to port 22 on local host

The client part (the htc program) opens a port on the host on which it runs, and also connects to the server where hts is running. For example:

client # htc -F 11223 192.168.4.18:12345    # listen on port 11223 and connect to server at 192.168.4.18, port 12345

The result of the above example configuration is that if you connect to port 11223 on the client, the connection is forwarded to port 110 on the server over an HTTP channel which runs from the client to the server's port 12345.

(for those familiar with SSH's port forwarding, the result is simlar to running

ssh -L11223:127.0.0.1:110 user@192.168.4.18

on the client machine except, as said, that SSH uses SSH - ie, a TCP connection to the server's port 22 by default - to carry the data).

No need to mention that hts server could be run on a different and, um, more "friendly" port, and the htc client supports some extra options, among which the use of an HTTP proxy (read the help or man pages).

Now how do we exploit httptunnel to carry raw IP traffic? This is where the mighty socat comes in. IP traffic and VPN, in Linux, almost invariably means a tun/tap interface, which socat supports. Additionally, we note that both hts and htc support reading and writing from/to stdin/stdout (instead of a TCP port), with the -s option. Putting it all together, here's what we run on the server:

server # socat 'EXEC:hts -s' 'TUN:10.0.0.1/24,iff-up'

After this, socat runs hts (which listens on port 8888 by default) and connects hts's stdin and stdout to the tun interface. On the client, we run

client # socat 'EXEC:htc -s 192.168.4.18' 'TUN:10.0.0.2/24,iff-up'

where 192.168.4.18 is the address of the machine running socat+hts. To run the server on a different port, or use other options (both on the server and the client), modify the hts/htc command lines in socat's EXEC part accordingly.

After this is done, verify that traffic can be sent between 10.0.0.1 and 10.0.0.2, and the (pseudo) VPN link is ready. With appropriate setup (routes on the client, forwarding/NAT etc. on the server), the client can direct part or all of its IP traffic over the httptunnel link.

CAVEATS: this is a "kind of" VPN, traffic obviously is NOT encrypted. Also, httptunnel has NO authentication mechanisms, so if somebody discovered your listening hts, they could run socat+htc on their machine and send traffic through and (more worryingly) TO your server when you're not using it. Finally, in many networks there are restrictions that are not meant to be circumvented; make sure that whatever you do, you are allowed to do it. Let's repeat it one more time: YOU ARE RESPONSIBLE. Think carefully. Be reasonable.

Ruby fun

Still learning...

Note that this code works only with ruby 1.8.x.

Copy and distribution of the code published in this page, with or without
modification, are permitted in any medium without royalty provided the copyright
notice (see bottom of page) and this notice are preserved.
split,Map=";!94B(KE*N8L.1=","gc_~qJ9h2mH_SiU)ubVkUJO~|LjIs{SKxe=zF]}dPcx4}s{"
mAp=(chr="length".length)*10-27;maP=(1847664772/461916193-1)/3-54567*7/381969
split_=([:EBPebKtoGRSf0IsjPodZZUqnL]&["*:i,Ypa(g!l9iwW5%[+3f(!R5j,(-m"]).to_s
puts=split.split(split_).map{|map|(Map[map[maP]-mAp]-chr).chr}.join;puts puts

Obfuscated awk

This started as a pastime during a rainy afternoon, and ended up being included by Arnold Robbins in the GNU awk manual:

Copy and distribution of the code published in this page, with or without
modification, are permitted in any medium without royalty provided the copyright
notice (see bottom of page) and this notice are preserved.
awk 'BEGIN{O="~"~"~";o="=="=="==";o+=+o;x=O""O;while(X++<=x+o+o)c=c"%c";
printf c,(x-O)*(x-O),x*(x-o)-o,x*(x-O)+x-O-o,+x*(x-O)-x+o,X*(o*o+O)+x-O,
X*(X-x)-o*o,(x+X)*o*o+o,x*(X-x)-O-O,x-O+(O+o+X+x)*(o+O),X*X-X*(x-O)-x+O,
O+X*(o*(o+O)+O),+x+O+X*o,x*(x-o),(o+X+x)*o*o-(x-O-O),O+(X-x)*(X+O),x-O}'

To see what it does, just run it (hint: it's not different from other pieces of obfuscated code published here).