** xref:xlat/index.adoc[Dynamic Expansion]
*** xref:xlat/alternation.adoc[Alternation Syntax]
+*** xref:xlat/conversion.adoc[Data Conversion]
*** xref:xlat/deprecated.adoc[Deprecated Functions]
*** xref:xlat/file.adoc[File handling]
*** xref:xlat/function.adoc[Function Syntax]
+*** xref:xlat/hash.adoc[Hashing]
+*** xref:xlat/interpreter.adoc[Interpreter State and Debugging]
*** xref:xlat/log.adoc[Logging Functions]
*** xref:xlat/protocol.adoc[Protocol Encoding and Decoding]
+*** xref:xlat/string.adoc[String Handling]
*** xref:xlat/builtin.adoc[Built-in Expansions]
*** xref:xlat/character.adoc[Single Letter Expansions]
*** xref:xlat/attribute.adoc[Attribute References]
```
====
-== Interpreter State
-
-The state of the interpreter can be queried via the
-`%interpeter(<name>)` expansion. The individual expansions are
-documented below.
-
-Each expansion given here can be prefixed with one or more dot (`.`)
-characters. These dots allow the expansion to refer to the current
-request via a `name`, or the parent request via `.name`. If there is
-no parent, the expansion returns the string `<underflow>`.
-
-=== %interpeter('module')
-
-The current module being executed. If the expansions is done in an
-`unlang` statement and outside of any module, it returns the name of
-the previous module which was executed.
-
-=== %interpeter('processing_stage')
-
-Which section of a virtual server is processing the request.
-
-=== %interpeter('rcode')
-
-The current interpreter return code, e.g. `handle`, or `ok`, etc.
-
-=== %interpeter('server')
-
-The name of the virtual server which is running the request.
-
== Server Configuration
=== %config(<key>)
Module rlm_exec.shell_escape = yes
```
-=== %client(<key>)
-
-Refers to a variable that was defined in the client section for the
-current client. See the sections `client { ... }` in `clients.conf`.
-
-.Return: _string_
-
-.Example
-
-[source,unlang]
-----
-"The client ipaddr is %client(ipaddr)"
-----
-
-.Output
-
-```
-The client ipaddr is 192.168.5.9
-```
-
-=== %debug(<level>)
-
-Dynamically change the debug level to something high, recording the old level.
-
-.Return: _string_
-
-.Example
-
-[source,unlang]
-----
-recv Access-Request {
- if (&request.User-Name == "bob") {
- "%debug(4)"
- } else {
- "%debug(0)"
- }
- ...
-}
-----
-
-.Output (_extra informations only for that condition_)
-
-```
-...
-(0) recv Access-Request {
-(0) if (&request.User-Name == "bob") {
-(0) EXPAND %debug(4)
-(0) --> 2
-(0) } # if (&request.User-Name == "bob") (...)
-(0) filter_username {
-(0) if (&State) {
-(0) ...
-(0) }
-...
-```
-
-=== %debug_attr(<list:[index]>)
-
-Print to debug output all instances of current attribute, or all attributes in a list.
-expands to a zero-length string.
-
-.Return: _string_
-
-.Example
-
-[source,unlang]
-----
-recv Access-Request {
- if (&request.User-Name == "bob") {
- "%debug_attr(request[*])"
- }
- ...
-}
-----
-
-.Output
-
-```
-...
-(0) recv Access-Request {
-(0) if (&request.User-Name == "bob") {
-(0) Attributes matching "request[*]"
-(0) &request.User-Name = bob
-(0) &request.User-Password = hello
-(0) &request.NAS-IP-Address = 127.0.1.1
-(0) &request.NAS-Port = 1
-(0) &request.Message-Authenticator = 0x9210ee447a9f4c522f5300eb8fc15e14
-(0) EXPAND %debug_attr(request[*])
-(0) } # if (&request.User-Name == "bob") (...)
-...
-```
-
-=== %interpreter(<state>)
-
-Get information about the interpreter state.
-
-[options="header,autowidth"]
-|===
-| State | Description
-| `name` | Name of the instruction.
-| `type` | Unlang type.
-| `depth` | How deep the current stack is.
-| `line` | Line number of the current section.
-| `filename` | Filename of the current section.
-|===
-
-.Return: _string_
-
-.Example
-
-[source,unlang]
-----
-"Failure in test at line %interpreter(...filename):%interpreter(...line)"
-----
-
-.Output
-
-```
-Failure in test at line /path/raddb/sites-enaled/default:231
-```
-
-== String manipulation
-
-=== %concat(<&ref:[idx]>, <delim>)
-
-Used to join two or more attributes, separated by an optional delimiter.
-
-.Return: _string_
-
-In most cases, `%concat(...)` is only useful inside of a dynamically
-expanded string. If you need to concatenate strings together in a policy, just use `+`.
-
-.Example
-
-[source,unlang]
-----
-&control += {
- &Tmp-String-0 = "aaa"
- &Tmp-String-0 = "bb"
- &Tmp-String-0 = "c"
-}
-
-&reply += {
- &Reply-Message = "%concat(%{control.Tmp-String-0[*]}, ', ')"
- &Reply-Message = "%concat(%{control.Tmp-String-0[*]}, ',')"
-}
-----
-
-.Output
-
-```
-aaa, bb, c
-aaa,bb,c
-```
-
-.Using "+"
-[source,unlang]
-----
-string foo
-
-&foo += { "a", "c", "c", "d" } # abcd
-
-&foo += &control.Tmp-String-0[*]
-----
-
-
-=== %explode(<&ref>, <delim>)
-
-Split an string into multiple new strings based on a delimiter.
-
-This expansion is the opposite of `%concat( ... )`.
-
-.Return: _the number exploded list of strings_.
-
-.Example
-
-[source,unlang]
-----
-&control.Tmp-String-0 := "bob.toba@domain.com"
-
-&control.Tmp-String-1 := "%explode(&control.Tmp-String-0, '@')"
-
-&reply.Reply-Message := "Welcome %{control.Tmp-String-1[0]}"
-----
-
-.Output
-
-```
-Welcome bob.toba
-```
-
-=== %lpad(<string>, <val>, <char>)
-
-Left-pad a string.
-
-.Return: _string_
-
-.Example
-
-[source,unlang]
-----
-&control.Tmp-String-0 := "123"
-
-&reply.Reply-Message := "Maximum should be %lpad(%{control.Tmp-String-0}, 11, '0')"
-----
-
-.Output
-
-```
-Maximum should be 00000000123
-```
-
-=== %rpad(<string>, <val>, <char>)
-
-Right-pad a string.
-
-.Return: _string_
-
-.Example
-
-[source,unlang]
-----
-&control.Tmp-String-0 := "123"
-
-&reply.Reply-Message := "Maximum should be %rpad(%{control.Tmp-String-0}, 11, '0')"
-----
-
-.Output
-
-```
-Maximum should be 12300000000
-```
-
-=== %pairs(<list>.[*])
-
-Serialize attributes as comma-delimited string.
-
-.Return: _string_
-
-.Example
-
-[source,unlang]
-----
-&control.Tmp-String-0 := { "This is a string", "This is another one" }
-&reply.Reply-Message := "Serialize output: %pairs(&control.[*])"
-----
-
-.Output
-
-```
-Serialize output: Tmp-String-0 = "\"This is a string\", Tmp-String-0 = \"This is another one\""
-```
-
-=== %randstr( ...)
-
-Get random string built from character classes.
-
-.Return: _string_
-
-.Example
-
-[source,unlang]
-----
-&reply.Reply-Message := "The random string output is %randstr(aaaaaaaa}"
-----
-
-.Output
-
-```
-The random string output is 4Uq0gPyG
-```
-
-=== %tolower( ... )
-
-Dynamically expands the string and returns the lowercase version of
-it. This definition is only available in version 2.1.10 and later.
-
-.Return: _string_
-
-.Example
-
-[source,unlang]
-----
-&control.Tmp-String-0 := "CAIPIRINHA"
-&reply.Reply-Message := "tolower of %{control.Tmp-String-0} is %tolower(%{control.Tmp-String-0})"
-----
-
-.Output
-
-```
-tolower of CAIPIRINHA is caipirinha
-```
-
-=== %toupper( ... )
-
-Dynamically expands the string and returns the uppercase version of
-it. This definition is only available in version 2.1.10 and later.
-
-.Return: _string_
-
-.Example
-
-[source,unlang]
-----
-&control.Tmp-String-0 := "caipirinha"
-&reply.Reply-Message := "toupper of %{control.Tmp-String-0} is " + %toupper(%{control.Tmp-String-0})
-----
-
-.Output
-
-```
-toupper of caipirinha is CAIPIRINHA
-```
-
-== Data Conversion
-
-=== %base64.encode( ... )
-
-Encode a string using Base64.
-
-.Return: _string_
-
-.Example
-
-[source,unlang]
-----
-&control.Tmp-String-0 := "Caipirinha"
-&reply.Reply-Message := "The base64 of %{control.Tmp-String-0} is %base64.encode(%{control.Tmp-String-0})"
-----
-
-.Output
-
-```
-The base64 of foo is Q2FpcGlyaW5oYQ==
-```
-
-=== %base64.decode( ... )
-
-Decode a string previously encoded using Base64.
-
-.Return: _string_
-
-.Example
-
-[source,unlang]
-----
-&control.Tmp-String-0 := "Q2FpcGlyaW5oYQ=="
-&reply.Reply-Message := "The base64.decode of %{control.Tmp-String-0} is %base64.decode(%{control.Tmp-String-0})"
-----
-
-.Output
-
-```
-The base64.decode of Q2FpcGlyaW5oYQ== is Caipirinha
-```
-
-=== %bin( ... )
-
-Convert string to binary.
-
-.Return: _octal_
-
-.Example
-
-[source,unlang]
-----
-&control.Tmp-String-0 := "10"
-&reply.Reply-Message := "The %{control.Tmp-String-0} in binary is %bin(%{control.Tmp-String-0})"
-----
-
-.Output
-
-```
-The 10 in binary is \020
-```
-
-=== %hex( ... )
-
-Convert to hex.
-
-.Return: _string_
-
-.Example
-
-[source,unlang]
-----
-&control.Tmp-String-0 := "12345"
-&reply.Reply-Message := "The value of %{control.Tmp-String-0} in hex is %hex(%{control.Tmp-String-0})"
-----
-
-.Output
-
-```
-The value of 12345 in hex is 3132333435
-```
-
-=== %urlquote( ... )
-
-Quote URL special characters.
-
-.Return: _string_.
-
-.Example
-
-[source,unlang]
-----
-&control.Tmp-String-0 := "http://example.org/"
-&reply += {
- &Reply-Message = "The urlquote of %{control.Tmp-String-0} is %urlquote(%{control.Tmp-String-0})"
-}
-----
-
-.Output
-
-```
-The urlquote of http://example.org/ is http%3A%2F%2Fexample.org%2F
-```
-
-=== %urlunquote( ... )
-
-Unquote URL special characters.
-
-.Return: _string_.
-
-.Example
-
-[source,unlang]
-----
-&control.Tmp-String-0 := "http%%3A%%2F%%2Fexample.org%%2F" # Attention for the double %.
-&reply += {
- &Reply-Message = "The urlunquote of %{control.Tmp-String-0} is %urlunquote(%{control.Tmp-String-0})"
-}
-----
-
-.Output
-
-```
-The urlunquote of http%3A%2F%2Fexample.org%2F is http://example.org/
-```
-
-== Hashing and Encryption
-
-=== %hmacmd5(<shared_key> <string>)
-
-Generate `HMAC-MD5` of string.
-
-.Return: _octal_
-
-.Example
-
-[source,unlang]
-----
-&control.Tmp-String-0 := "mykey"
-&control.Tmp-String-1 := "Caipirinha"
-&reply.control.Tmp-Octets-0 := "%hmacmd5(%{control.Tmp-String-0} %{control.Tmp-String-1})"
-
-&reply += {
- &Reply-Message = "The HMAC-MD5 of %{control.Tmp-String-1} in octets is %{control.Tmp-Octets-0}"
- &Reply-Message = "The HMAC-MD5 of %{control.Tmp-String-1} in hex is %hex(control.Tmp-Octets-0)"
-}
-----
-
-.Output
-
-```
-The HMAC-MD5 of Caipirinha in octets is \317}\264@K\216\371\035\304\367\202,c\376\341\203
-The HMAC-MD5 of Caipirinha in hex is 636f6e74726f6c3a546d702d4f63746574732d30
-```
-
-=== %hmacsha1(<shared_key>, <string>)
-
-Generate `HMAC-SHA1` of string.
-
-.Return: _octal_
-
-.Example
-
-[source,unlang]
-----
-&control.Tmp-String-0 := "mykey"
-&control.Tmp-String-1 := "Caipirinha"
-&control.Tmp-Octets-0 := "%hmacsha1(%{control.Tmp-String-0}, %{control.Tmp-String-1})"
-
-&reply += {
- &Reply-Message = "The HMAC-SHA1 of %{control.Tmp-String-1} in octets is %{control.Tmp-Octets-0}"
- &Reply-Message = "The HMAC-SHA1 of %{control.Tmp-String-1} in hex is %hex(control.Tmp-Octets-0}"
-}
-----
-
-.Output
-
-```
-The HMAC-SHA1 of Caipirinha in octets is \311\007\212\234j\355\207\035\225\256\372ʙ>R\"\341\351O)
-The HMAC-SHA1 of Caipirinha in hex is 636f6e74726f6c3a546d702d4f63746574732d30
-```
-
-=== %md5( ... )
-
-Dynamically expands the string and performs an MD5 hash on it. The
-result is binary data.
-
-.Return: _binary data_
-
-.Example
-
-[source,unlang]
-----
-&control.Tmp-String-0 := "Caipirinha"
-&reply += {
- &Reply-Message = "md5 of %{control.Tmp-String-0} is octal=%md5(%{control.Tmp-String-0})"
- &Reply-Message = "md5 of %{control.Tmp-String-0} is hex=%hex(%md5(%{control.Tmp-String-0}))"
-}
-----
-
-.Output
-
-```
-md5 of Caipirinha is octal=\024\204\013md||\230\243\3472\3703\330n\251
-md5 of Caipirinha is hex=14840b6d647c7c98a3e732f833d86ea9
-```
-
-=== Other Hashing Functions
-
-The following hashes are supported for all versions of OpenSSL.
-
-* `%md2( ... }`
-* `%md4( ... }`
-* `%md5( ... }`
-* `%sha1( ... }`
-* `%sha224( ... }`
-* `%sha256( ... }`
-* `%sha384( ... }`
-* `%sha512( ... }`
-
-The following hashes are supported for when OpenSSL 1.1.1 or greater
-is installed. This version adds support for the `sha3` and `blake`
-families of digest functions.
-
-* `%blake2s_256( ... )`
-* `%blake2b_512( ... )`
-* `%sha2_224( ... )`
-* `%sha2_256( ... )`
-* `%sha2_384( ... )`
-* `%sha2_512( ... )`
-* `%sha3_224( ... )`
-* `%sha3_256( ... )`
-* `%sha3_384( ... )`
-* `%sha3_512( ... )`
-
-.Return: _octal_
-
-.Example
-
-[source,unlang]
-----
-&control.Tmp-String-0 := "Caipirinha"
-&reply += {
- &Reply-Message = "The md5 of %{control.Tmp-String-0} in octal is %md5(%{control.Tmp-String-0}}"
- &Reply-Message = "The md5 of %{control.Tmp-String-0} in hex is %hex(%md5(%{control.Tmp-String-0}}}"
-}
-----
-
-.Output
-
-```
-The md5 of Caipirinha in octal is \024\204\013md||\230\243\3472\3703\330n\251
-The md5 of Caipirinha in hex is 14840b6d647c7c98a3e732f833d86ea9
-```
-
== Miscellaneous Expansions
=== %{0}+..+%{32}
--- /dev/null
+= Data Conversion
+
+The following functions perform conversion to/from different types of data encoding.
+
+== %base64.encode( ... )
+
+Encode a string using Base64.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+&control.Tmp-String-0 := "Caipirinha"
+&reply.Reply-Message := "The base64 of %{control.Tmp-String-0} is %base64.encode(%{control.Tmp-String-0})"
+----
+
+.Output
+
+```
+The base64 of foo is Q2FpcGlyaW5oYQ==
+```
+
+== %base64.decode( ... )
+
+Decode a string previously encoded using Base64.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+&control.Tmp-String-0 := "Q2FpcGlyaW5oYQ=="
+&reply.Reply-Message := "The base64.decode of %{control.Tmp-String-0} is %base64.decode(%{control.Tmp-String-0})"
+----
+
+.Output
+
+```
+The base64.decode of Q2FpcGlyaW5oYQ== is Caipirinha
+```
+
+== %bin( ... )
+
+Convert string to binary.
+
+.Return: _octal_
+
+.Example
+
+[source,unlang]
+----
+&control.Tmp-String-0 := "10"
+&reply.Reply-Message := "The %{control.Tmp-String-0} in binary is %bin(%{control.Tmp-String-0})"
+----
+
+.Output
+
+```
+The 10 in binary is \020
+```
+
+== %hex( ... )
+
+Convert to hex.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+&control.Tmp-String-0 := "12345"
+&reply.Reply-Message := "The value of %{control.Tmp-String-0} in hex is %hex(%{control.Tmp-String-0})"
+----
+
+.Output
+
+```
+The value of 12345 in hex is 3132333435
+```
+
+== %urlquote( ... )
+
+Quote URL special characters.
+
+.Return: _string_.
+
+.Example
+
+[source,unlang]
+----
+&control.Tmp-String-0 := "http://example.org/"
+&reply += {
+ &Reply-Message = "The urlquote of %{control.Tmp-String-0} is %urlquote(%{control.Tmp-String-0})"
+}
+----
+
+.Output
+
+```
+The urlquote of http://example.org/ is http%3A%2F%2Fexample.org%2F
+```
+
+== %urlunquote( ... )
+
+Unquote URL special characters.
+
+.Return: _string_.
+
+.Example
+
+[source,unlang]
+----
+&control.Tmp-String-0 := "http%%3A%%2F%%2Fexample.org%%2F" # Attention for the double %.
+&reply += {
+ &Reply-Message = "The urlunquote of %{control.Tmp-String-0} is %urlunquote(%{control.Tmp-String-0})"
+}
+----
+
+.Output
+
+```
+The urlunquote of http%3A%2F%2Fexample.org%2F is http://example.org/
+```
+
+// Copyright (C) 2023 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// This documentation was developed by Network RADIUS SAS.
--- /dev/null
+= Hashing
+
+The following functions perform hashing.
+
+Note that the server supports insecure hashing methods such as MD5 and
+SHA1. These functions are here for historical compatibility and
+completeness.
+
+== %hmacmd5(<shared_key>, <string>)
+
+Generate `HMAC-MD5` of string.
+
+.Return: _octal_
+
+.Example
+
+[source,unlang]
+----
+&control.Tmp-String-0 := "mykey"
+&control.Tmp-String-1 := "Caipirinha"
+&reply.control.Tmp-Octets-0 := "%hmacmd5(%{control.Tmp-String-0} %{control.Tmp-String-1})"
+
+&reply += {
+ &Reply-Message = "The HMAC-MD5 of %{control.Tmp-String-1} in octets is %{control.Tmp-Octets-0}"
+ &Reply-Message = "The HMAC-MD5 of %{control.Tmp-String-1} in hex is %hex(control.Tmp-Octets-0)"
+}
+----
+
+.Output
+
+```
+The HMAC-MD5 of Caipirinha in octets is \317}\264@K\216\371\035\304\367\202,c\376\341\203
+The HMAC-MD5 of Caipirinha in hex is 636f6e74726f6c3a546d702d4f63746574732d30
+```
+
+== %hmacsha1(<shared_key>, <string>)
+
+Generate `HMAC-SHA1` of string.
+
+.Return: _octal_
+
+.Example
+
+[source,unlang]
+----
+&control.Tmp-String-0 := "mykey"
+&control.Tmp-String-1 := "Caipirinha"
+&control.Tmp-Octets-0 := "%hmacsha1(%{control.Tmp-String-0}, %{control.Tmp-String-1})"
+
+&reply += {
+ &Reply-Message = "The HMAC-SHA1 of %{control.Tmp-String-1} in octets is %{control.Tmp-Octets-0}"
+ &Reply-Message = "The HMAC-SHA1 of %{control.Tmp-String-1} in hex is %hex(control.Tmp-Octets-0}"
+}
+----
+
+.Output
+
+```
+The HMAC-SHA1 of Caipirinha in octets is \311\007\212\234j\355\207\035\225\256\372ʙ>R\"\341\351O)
+The HMAC-SHA1 of Caipirinha in hex is 636f6e74726f6c3a546d702d4f63746574732d30
+```
+
+== %md5( ... )
+
+Dynamically expands the string and performs an MD5 hash on it. The
+result is binary data.
+
+.Return: _binary data_
+
+.Example
+
+[source,unlang]
+----
+&control.Tmp-String-0 := "Caipirinha"
+&reply += {
+ &Reply-Message = "md5 of %{control.Tmp-String-0} is octal=%md5(%{control.Tmp-String-0})"
+ &Reply-Message = "md5 of %{control.Tmp-String-0} is hex=%hex(%md5(%{control.Tmp-String-0}))"
+}
+----
+
+.Output
+
+```
+md5 of Caipirinha is octal=\024\204\013md||\230\243\3472\3703\330n\251
+md5 of Caipirinha is hex=14840b6d647c7c98a3e732f833d86ea9
+```
+
+=== Other Hashing Functions
+
+The following hashes are supported for all versions of OpenSSL.
+
+* `%md2( ... }`
+* `%md4( ... }`
+* `%md5( ... }`
+* `%sha1( ... }`
+* `%sha224( ... }`
+* `%sha256( ... }`
+* `%sha384( ... }`
+* `%sha512( ... }`
+
+The following hashes are supported for when OpenSSL 1.1.1 or greater
+is installed. This version adds support for the `sha3` and `blake`
+families of digest functions.
+
+* `%blake2s_256( ... )`
+* `%blake2b_512( ... )`
+* `%sha2_224( ... )`
+* `%sha2_256( ... )`
+* `%sha2_384( ... )`
+* `%sha2_512( ... )`
+* `%sha3_224( ... )`
+* `%sha3_256( ... )`
+* `%sha3_384( ... )`
+* `%sha3_512( ... )`
+
+.Return: _octal_
+
+.Example
+
+[source,unlang]
+----
+&control.Tmp-String-0 := "Caipirinha"
+&reply += {
+ &Reply-Message = "The md5 of %{control.Tmp-String-0} in octal is %md5(%{control.Tmp-String-0}}"
+ &Reply-Message = "The md5 of %{control.Tmp-String-0} in hex is %hex(%md5(%{control.Tmp-String-0}}}"
+}
+----
+
+.Output
+
+```
+The md5 of Caipirinha in octal is \024\204\013md||\230\243\3472\3703\330n\251
+The md5 of Caipirinha in hex is 14840b6d647c7c98a3e732f833d86ea9
+```
+
--- /dev/null
+= Interpreter
+
+The following functions allow inspection and/or manipulation of the `unlang` interpreter as it is running.
+
+== Debug Functions
+
+The following functions allow changing the debug level, or printing out specific lists of attributes.
+
+=== %debug(<level>)
+
+Dynamically change the debug level to something high, recording the old level.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+recv Access-Request {
+ if (&request.User-Name == "bob") {
+ "%debug(4)"
+ } else {
+ "%debug(0)"
+ }
+ ...
+}
+----
+
+.Output (_extra informations only for that condition_)
+
+```
+...
+(0) recv Access-Request {
+(0) if (&request.User-Name == "bob") {
+(0) EXPAND %debug(4)
+(0) --> 2
+(0) } # if (&request.User-Name == "bob") (...)
+(0) filter_username {
+(0) if (&State) {
+(0) ...
+(0) }
+...
+```
+
+=== %debug_attr(<list:[index]>)
+
+Print to debug output all instances of current attribute, or all attributes in a list.
+expands to a zero-length string.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+recv Access-Request {
+ if (&request.User-Name == "bob") {
+ "%debug_attr(request[*])"
+ }
+ ...
+}
+----
+
+.Output
+
+```
+...
+(0) recv Access-Request {
+(0) if (&request.User-Name == "bob") {
+(0) Attributes matching "request[*]"
+(0) &request.User-Name = bob
+(0) &request.User-Password = hello
+(0) &request.NAS-IP-Address = 127.0.1.1
+(0) &request.NAS-Port = 1
+(0) &request.Message-Authenticator = 0x9210ee447a9f4c522f5300eb8fc15e14
+(0) EXPAND %debug_attr(request[*])
+(0) } # if (&request.User-Name == "bob") (...)
+...
+```
+
+== State
+
+The state of the interpreter can be queried via the
+`%interpeter(<name>)` expansion. The individual expansions are
+documented below.
+
+Each expansion given here can be prefixed with one or more dot (`.`)
+characters. These dots allow the expansion to refer to the current
+request via a `name`, or the parent request via `.name`. If there is
+no parent, the expansion returns the string `<underflow>`.
+
+For example `'filename'` is the name of the current file being
+executed, while `'..filename'` is the filename of the parent request.
+
+== %interpeter('filename')
+
+Which filename is currently being executed.
+
+== %interpeter('line')
+
+The line number in the current file being executed.
+
+== %interpeter('processing_stage')
+
+Which section of a virtual server is processing the request.
+
+
+== %interpeter('module')
+
+The current module being executed. If the expansions is done in an
+`unlang` statement and outside of any module, it returns the name of
+the previous module which was executed.
+
+== %interpeter('processing_stage')
+
+Which section of a virtual server is processing the request.
+
+== %interpeter('rcode')
+
+The current interpreter return code, e.g. `handle`, or `ok`, etc.
+
+== %interpeter('server')
+
+The name of the virtual server which is running the request.
+
+=== %client(<key>)
+
+Refers to a variable that was defined in the client section for the
+current client. See the sections `client { ... }` in `clients.conf`.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+"The client ipaddr is %client(ipaddr)"
+----
+
+.Output
+
+```
+The client ipaddr is 192.168.5.9
+```
+
+// Copyright (C) 2023 Network RADIUS SAS. Licenced under CC-by-NC 4.0.
+// This documentation was developed by Network RADIUS SAS.
--- /dev/null
+= String manipulation
+
+The following functions perform string manipulation.
+
+== %concat(<&ref:[idx]>, <delim>)
+
+Used to join two or more attributes, separated by an optional delimiter.
+
+.Return: _string_
+
+In most cases, `%concat(...)` is only useful inside of a dynamically
+expanded string. If you need to concatenate strings together in a policy, just use `+`.
+
+.Example
+
+[source,unlang]
+----
+&control += {
+ &Tmp-String-0 = "aaa"
+ &Tmp-String-0 = "bb"
+ &Tmp-String-0 = "c"
+}
+
+&reply += {
+ &Reply-Message = "%concat(%{control.Tmp-String-0[*]}, ', ')"
+ &Reply-Message = "%concat(%{control.Tmp-String-0[*]}, ',')"
+}
+----
+
+.Output
+
+```
+aaa, bb, c
+aaa,bb,c
+```
+
+.Using "+"
+[source,unlang]
+----
+string foo
+
+&foo += { "a", "c", "c", "d" } # abcd
+
+&foo += &control.Tmp-String-0[*]
+----
+
+== %explode(<&ref>, <delim>)
+
+Split an string into multiple new strings based on a delimiter.
+
+This expansion is the opposite of `%concat( ... )`.
+
+.Return: _the number exploded list of strings_.
+
+.Example
+
+[source,unlang]
+----
+&control.Tmp-String-0 := "bob.toba@domain.com"
+
+&control.Tmp-String-1 := "%explode(&control.Tmp-String-0, '@')"
+
+&reply.Reply-Message := "Welcome %{control.Tmp-String-1[0]}"
+----
+
+.Output
+
+```
+Welcome bob.toba
+```
+
+== %lpad(<string>, <val>, <char>)
+
+Left-pad a string.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+&control.Tmp-String-0 := "123"
+
+&reply.Reply-Message := "Maximum should be %lpad(%{control.Tmp-String-0}, 11, '0')"
+----
+
+.Output
+
+```
+Maximum should be 00000000123
+```
+
+== %rpad(<string>, <val>, <char>)
+
+Right-pad a string.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+&control.Tmp-String-0 := "123"
+
+&reply.Reply-Message := "Maximum should be %rpad(%{control.Tmp-String-0}, 11, '0')"
+----
+
+.Output
+
+```
+Maximum should be 12300000000
+```
+
+== %pairs(<list>.[*])
+
+Serialize attributes as comma-delimited string.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+&control.Tmp-String-0 := { "This is a string", "This is another one" }
+&reply.Reply-Message := "Serialize output: %pairs(&control.[*])"
+----
+
+.Output
+
+```
+Serialize output: Tmp-String-0 = "\"This is a string\", Tmp-String-0 = \"This is another one\""
+```
+
+== %randstr( ...)
+
+Get random string built from character classes.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+&reply.Reply-Message := "The random string output is %randstr(aaaaaaaa}"
+----
+
+.Output
+
+```
+The random string output is 4Uq0gPyG
+```
+
+== %tolower( ... )
+
+Dynamically expands the string and returns the lowercase version of
+it. This definition is only available in version 2.1.10 and later.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+&control.Tmp-String-0 := "CAIPIRINHA"
+&reply.Reply-Message := "tolower of %{control.Tmp-String-0} is %tolower(%{control.Tmp-String-0})"
+----
+
+.Output
+
+```
+tolower of CAIPIRINHA is caipirinha
+```
+
+== %toupper( ... )
+
+Dynamically expands the string and returns the uppercase version of
+it. This definition is only available in version 2.1.10 and later.
+
+.Return: _string_
+
+.Example
+
+[source,unlang]
+----
+&control.Tmp-String-0 := "caipirinha"
+&reply.Reply-Message := "toupper of %{control.Tmp-String-0} is " + %toupper(%{control.Tmp-String-0})
+----
+
+.Output
+
+```
+toupper of caipirinha is CAIPIRINHA
+```
+