iRules Style Guide
This article (formatted here in collaboration with and from the notes of F5er Jim_Deucker) features an opinionated way to write iRules, which is an extension to the Tcl language. Tcl has its own style guide for reference, as do other languages like my personal favorite python. From the latter, Guido van Rossum quotes Ralph Waldo Emerson: "A foolish consistency is the hobgoblin of little minds..," or if you prefer, Morpheus from the Matrix: "What you must learn is that these rules are no different than the rules of a computer system. Some of them can be bent. Others can be broken." The point? This is a guide, and if there is a good reason to break a rule...by all means break it!
Editor Settings
Setting a standard is good for many reasons. It's easier to share code amongst colleagues, peers, or the larger community when code is consistent and professional looking. Settings for some tools are provided below, but if you're using other tools, here's the goal:
- indent 4 spaces (no tab characters)
- 100-column goal for line length (120 if you must) but avoid line continuations where possible
- file parameters
- ASCII
- Unix linefeeds (\n)
- trailing whitespace trimmed from the end of each line
- file ends with a linefeed
Visual Studio Code
If you aren't using VSCode, why the heck not? This tool is amazing, and with the F5 Networks iRules extension coupled with The F5 Extension, you get the functionality of a powerful editor along with the connectivity control of your F5 hosts. With code diagnostics and auto formatting based on this very guide, the F5 Networks iRules Extension will make your life easy. Seriously...stop reading and go set up VSCode now.
EditorConfig
For those with different tastes in text editing using an editor that supports EditorConfig:
# 4 space indentation
[*.{irule,irul}]
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
charset = ascii
trim_trailing_whitespace = true
Vim
I'm a vi guy with sys-admin work, but I prefer a full-fledge IDE for development efforts. If you prefer the file editor, however, we've got you covered with these Vim settings:
# in ~/.vimrc file
set tabstop=4
set shiftwidth=4
set expandtab
set fileencoding=ascii
set fileformat=unix
Sublime
There are a couple tools for sublime, but all of them are a bit dated and might require some work to bring them up to speed. Unless you're already a Sublime apologist, I'd choose one of the other two options above.
- sublime-f5-irules (bitwisecook fork, billchurch origin) for editing
- Sublime Highlight for export to RTF/HTML
Guidance
- Watch out for smart-quotes and non-breaking spaces inserted by applications like Microsoft Word as they can silently break code. The VSCode extension will highlight these occurrences and offer a fix automatically, so again, jump on that bandwagon!
- A single iRule has a 64KB limit. If you're reaching that limit it might be time to question
your life choices, I mean,the wisdom of the solution. - Break out your iRules into functional blocks. Try to separate (where possible) security from app functionality from stats from protocol nuances from mgmt access, etc. For example, when the DevCentral team managed the DevCentral servers and infrastructure, we had 13 iRules to handle maintenance pages, masking application error codes and data, inserting scripts for analytics, managing vanity links and other structural rewrites to name a few. With this strategy, priorities for your events are definitely your friend.
- Standardize on "{" placement at the end of a line and not the following line, this causes the least problems across all the BIG-IP versions.
# ### THIS ### if { thing } { script } else { other_script } # ### NOT THIS ### if { thing } { script } else { other_script }
- 4-character indent carried into nested values as well, like in switch.
# ### THIS ### switch -- ${thing} { "opt1" { command } default { command } }
- Comments (as image for this one to preserve line numbers)
- Always comment at the same indent-level as the code (lines 1, 4, 9-10)
- Avoid end-of-line comments (line 11)
- Always hash-space a comment (lines 1, 4, 9-10)
- Leave out the space when commenting out code (line 2)
- switch statements cannot have comments inline with options (line 6)
- Avoid multiple commands on a single line.
# ### THIS ### set host [getfield [HTTP::host] 1] set port [getfield [HTTP::host] 2] # ### NOT THIS ### set host [getfield [HTTP::host] 1]; set port [getfield [HTTP::host] 2]
- Avoid single-line if statements, even for debug logs.
# ### THIS ### if { ${debug} } { log local0. "a thing happened...." } # ### NOT THIS ### if { ${debug} } { log local0. "a thing happened..."}
- Even though Tcl allows a horrific number of ways to communicate truthiness, Always express or store state as 0 or 1
# ### THIS ### set f 0 set t 1 if { ${f} && ${t} } { ... } # ### NOT THIS ### # Valid false values set f_values "n no f fal fals false of off" # Valid true values set t_values "y ye yes t tr tru true on" # Set a single valid, but unpreferred, state set f [lindex ${f_values} [expr {int(rand()*[llength ${f_values}])}]] set t [lindex ${t_values} [expr {int(rand()*[llength ${t_values}])}]] if { ${f} && ${t} } { ... }
- Always use Tcl standard || and && boolean operators over the F5 special and and or operators in expressions, and use parentheses when you have multiple arguments to be explicitly clear on operations.
# ### THIS ### if { ${state_active} && ${level_gold} } { if { (${state} == "IL") || (${state} == "MO") } { pool gold_pool } } # ### NOT THIS ### if { ${state_active} and ${level_gold} } { if { ${state} eq "IL" or ${state} eq "MO" } { pool gold_pool } }
- Always put a space between a closing curly bracket and an opening one.
# ### THIS ### if { ${foo} } { log local0.info "something" } # ### NOT THIS ### if { ${foo} }{ log local0.info "something" }
- Always wrap expressions in curly brackets to avoid double expansion. (Check out a deep dive on the byte code between the two approaches shown in the picture below)
# ### THIS ### set result [expr {3 * 4}] # ### NOT THIS ### set result [expr 3 * 4]
- Always use space separation around variables in expressions such as if statements or expr calls.
- Always wrap your variables in curly brackets when referencing them as well.
# ### THIS ### if { ${host} } { # ### NOT THIS ### if { $host } {
- Terminate options on commands like switch and table with "--" to avoid argument injection if if you're 100% sure you don't need them. The VSCode iRules extension will throw diagnostics for this. See K15650046 for more details on the security exposure.
# ### THIS ### switch -- [whereis [IP::client_addr] country] { "US" { table delete -subtable states -- ${state} } } # ### NOT THIS ### switch [whereis [IP::client_addr] country] { "US" { table delete -subtable states ${state} } }
- Always use a priority on an event, even if you're 100% sure you don't need them. The default is 500 so use that if you have no other starting point.
- Always put a timeout and/or lifetime on table contents. Make sure you really need the table space before settling on that solution, and consider abusing the static:: namespace instead.
- Avoid unexpected scope creep with static:: and table variables by assigning prefixes. Lacking a prefix means if multiple rules set or use the variable changing them becomes a race condition on load or rule update.
when RULE_INIT priority 500 { # ### THIS ### set static::appname_confvar 1 # ### NOT THIS ### set static::confvar 1 }
- Avoid using static:: for things like debug configurations, it's a leading cause of unintentional log storms and performance hits. If you have to use them for a provable performance reason follow the prefix naming rule.
# ### THIS ### when CLIENT_ACCEPTED priority 500 { set debug 1 } when HTTP_REQUEST priority 500 { if { ${debug} } { log local0.debug "some debug message" } } # ### NOT THIS ### when RULE_INIT priority 500 { set static::debug 1 } when HTTP_REQUEST priority 500 { if { ${static::debug} } { log local0.debug "some debug message" } }
- Comments are fine and encouraged, but don't leave commented-out code in the final version.
Wrapping up that guidance with a final iRule putting it all into practice:
when HTTP_REQUEST priority 500 {
# block level comments with leading space
#command commented out
if { ${a} } {
command
}
if { !${a} } {
command
} elseif { ${b} > 2 || ${c} < 3 } {
command
} else {
command
}
switch -- ${b} {
"thing1" -
"thing2" {
# thing1 and thing2 business reason
}
"thing3" {
# something else
}
default {
# default branch
}
}
# make precedence explicit with parentheses
set d [expr { (3 + ${c} ) / 4 }]
foreach { f } ${e} {
# always braces around the lists
}
foreach { g h i } { j k l m n o p q r } {
# so the lists are easy to add to
}
for { set i 0 } { ${i} < 10 } { incr i } {
# clarity of each parameter is good
}
}
What standards do you follow for your iRules coding styles? Drop a comment below!
The most 'fun' thing about breaking the rules is that you first learn what it means to obey them before knowing how to break them.
- Jim_DeuckerEmployee
Juergen_Mang interesting point on the return in switch. It's not really something for the style guide, I have a another document I also wrote that covers much more stuff, around common mistakes and issues.
I'll look at adding a diagnostic for this to the next major version of the vscode-irules extension.
Hi Jason,
let me answer programmatically....
set feeling_today [list "just woke up" "need a coffe" "better" "Awesome, no traffic jam!"] foreach x { another big irule coding project } { set HTTP_RESPONSE "The string with more than 65535 characters cannot be stored in a message." if { $HTTP_RESPONSE contains "65535 characters" } then { lappend feeling_today "Frutstration" set options [expr { int( rand() * 3) }] switch -exact -- $options { "0" { set idea "How about to remove useful comments, so that I can not read my own code in 1 year from now..." lappend feeling_today "Not gonna do this..." } "1" { set idea "How about to shrink variable names, so that nobody except me understands the code..." lappend feeling_today "Not gonna do this..." } "2" { set idea "How about to split the iRule in two files and somehow glue them together?" lappend feeling_today "Life Question: Why is F5 forcing me this?" } default { set solution "A wonder happens and F5 increases the maximum iRule size!" lappend feeling_today "Still frustation because \$options values are just in the range of 0-2...." } } lappend feeling_today "Getting angry..." } }
64k is not a technical limitation - its just a "whatever" decission made in the past. So whats needs to be done that you help me to convince F5 Devs to increase the maximum iRule size?
And please dont tell me to question my life choices. I already did and still continue to code iRules... 😂
Cheers, Kai
Spotted another glitch in the final iRule:
foreach { f } { ${e} } { # always braces around the lists }
If $e is holding your list, then just pass the variable without any quotes or curlys to the loop. Otherwise the loop would just receive the string "${e}" but not the content of it...
Cheers, Kai 😉
Great article. Most things are cosmetical, but some realy important things are mentioned also. I will highlight:
- K15650046: Tcl code injection security exposure
- Always wrap expressions in curly brackets to avoid double expansion
- iRule priorities
- the static:: behavior
I see in in the wild often scripts that violates against these best practices.
What also could be mentioned is: switch cases do not need any "return" or "break" statements, too not fall through. I saw "return" many times errorneous used in this context.
Hi Jason,
you may review your topic 10.), since it provides slightest false information. I assume you accidentially just mixed up || vs. && with eq vs == in on scentence?
"eq" or "ne" is different from "==" or "!=".
"==" or "!=" should only be used for numeric comparsions only
"eq" or "ne" should be used for string comparions.
Examples of the math behind == comparsions:
1 == 1.0 is true 1 == 0001 is true 1 == "0x0000001" is true 1 == "\n\n\n\t\t\t1.e0\n\n\n" is also true 1 == 1.000000000000000031337 is also true 10 == 012 is also true
Beside of this using "eq" to compare "strings" is faster than comparing them with "==" (since no shimmering is involved).
The difference of || / && vs. "or" / "and" are not that huge. I consider the differences as personal preferecens... 😉
Cheers, Kai
- Jim_DeuckerEmployee
Kai_Wilke No, 10. is correct and not talking about string vs auto casting comparison, it's talking about the non-Tcl operators `and` and `or`, which make writing test harnesses hard.
From the |& perspective I agree, but 10.) also mixed up "==" and "eq".
Using "eq" in both examples would be correct, since it should compare pure strings and dont do any math...
Cheers, Kai
- Jim_DeuckerEmployee
Kai_Wilke ah I see you mean in the example, I'll get that tidied up and add a point about "eq"/"ne" vs "=="/"!="