An LSP for Tcl 8.4-9.1, F5 iRules, F5 iApps and other Tcl dialects A new language server for Tcl 8.4-9.1 and F5 iRules/iApps dialects, written in Python using pygls, provides multi-editor support via stdio and includes 25+ commands, 16 built-in snippets, and an MCP context server exposing 44 analysis tools. The project, hosted on GitHub by bitwisecook, offers installation guides for VS Code, Neovim, Zed, Emacs, Helix, Sublime Text, and JetBrains, with a one-line curl installer for the tcl and f5 CLIs. A language server for Tcl with multi-editor support. The server is written in Python using pygls https://github.com/openlawlibrary/pygls and communicates over stdio, making it compatible with any LSP client. Installation guides: INSTALL-editors.md — step-by-step setup for VS Code, Neovim, Zed, Emacs, Helix, Sublime Text, and JetBrains on macOS Homebrew , Linux Debian/Ubuntu, RHEL/CentOS, Fedora , and Windows. INSTALL-cli.md — the tcl and f5 CLIs, including a one-line curl | sh installer. | Editor | Type | Setup | Unique extras | |---|---|---|---| | .vsix from Releases @irule / @tcl / @tk Copilot chat, 25+ commands Neovim /bitwisecook/tcl-lsp/blob/main/editors/neovim tcl lsp.lua to ~/.config/nvim/server/ Zed /bitwisecook/tcl-lsp/blob/main/editors/zed /tcl-doc and /irule-event slash commands Emacs /bitwisecook/tcl-lsp/blob/main/editors/emacs init.el for eglot or lsp-mode Helix /bitwisecook/tcl-lsp/blob/main/editors/helix ~/.config/helix/languages.toml Sublime Text /bitwisecook/tcl-lsp/blob/main/editors/sublime-text JetBrains /bitwisecook/tcl-lsp/blob/main/editors/jetbrains All editors connect to the same Python LSP server over stdio. The server can be invoked from source uv run python -m server or as a standalone zipapp python3 tcl-lsp-server.pyz . Also documented in INSTALL-editors.md: VS Code-compatible editors load the same .vsix unchanged — Cursor, Windsurf, VSCodium, code-server / Coder, GitHub Codespaces, Gitpod, and Eclipse Theia. Other LSP-capable editors point a generic LSP client at the .pyz — Vim vim-lsp or coc.nvim , Kate, Kakoune, Notepad++, Geany, Lite XL, micro, CudaText, JupyterLab, Doom Emacs, and Spacemacs. File types recognised: .tcl , .tk , .itcl , .tm , .irul , .irule , .iapp , .iappimpl , .impl , .apl , .exp , plus shebang detection for /usr/bin/tclsh , /usr/bin/wish , and /usr/bin/expect . Files named presentation no extension are auto-detected as APL. Per-file tcl-dialect: comment directives pin a specific dialect. The full-featured extension, distributed as a .vsix , bundles the LSP server and provides the richest integration. 25+ commands including: Restart Server, Select Dialect, Apply Safe Quick Fixes, Apply All Optimisations, Open in Tcl Compiler Explorer, Open Tk Preview, Format Document, Minify Document, Insert iRule Event Skeleton, Scaffold Tcl Package Starter, Insert package require , Run Runtime Validation, Translate iRule to F5 XC, Extract iRule from Config, Escape/Unescape Selection, Base64 Encode/Decode Selection. Keyboard shortcuts: Ctrl+Alt+O optimise , Ctrl+Alt+M minify , Ctrl+Alt+E compiler explorer . Status bar: shows the active dialect clickable to change and the extension version. Install: see INSTALL-editors.md /bitwisecook/tcl-lsp/blob/main/INSTALL-editors.md vs-code . Zero-plugin setup on Neovim 0.11+ using the native LSP client. Also works with nvim-lspconfig 0.8+ or a manual FileType autocommand. -- ~/.config/nvim/server/tcl lsp.lua Neovim 0.11+ return { cmd = { "python3", "/path/to/tcl-lsp-server.pyz" }, filetypes = { "tcl" }, settings = { tclLsp = { dialect = "tcl8.6", formatting = { indentSize = 4, maxLineLength = 120 }, }, }, } -- init.lua vim.filetype.add { extension = { tcl = "tcl", irul = "tcl", irule = "tcl" } } vim.lsp.enable "tcl lsp" A full Zed extension that auto-downloads the server zipapp from GitHub Releases on first use and auto-discovers Python 3.10+ on your PATH. Includes 16 built-in snippets tcl-proc , tcl-namespace , tcl-if , irule-http-request , irule-collect-release , etc. , an MCP context server exposing all 44 analysis tools, and slash commands /tcl-doc , /irule-event , /tcl-validate . Install: see INSTALL-editors.md /bitwisecook/tcl-lsp/blob/main/INSTALL-editors.md zed . Works with the built-in eglot client Emacs 29+ or lsp-mode . ;; eglot Emacs 29+ with-eval-after-load 'eglot add-to-list 'eglot-server-programs ' tcl-mode . "python3" "/path/to/tcl-lsp-server.pyz" add-hook 'tcl-mode-hook 'eglot-ensure ;; Settings setq-default eglot-workspace-configuration ' :tclLsp :dialect "tcl8.6" :formatting :indentSize 4 :maxLineLength 120 Minimal TOML configuration in ~/.config/helix/languages.toml . language-server.tcl-lsp command = "python3" args = "/path/to/tcl-lsp-server.pyz" language-server.tcl-lsp.config.tclLsp dialect = "tcl8.6" language name = "tcl" scope = "source.tcl" file-types = "tcl", "tk", "itcl", "tm", "irul", "irule", "iapp" language-servers = "tcl-lsp" A full Sublime Text package .sublime-package that works in two modes: standalone syntax highlighting + 16 snippets + static completions and enhanced full LSP features when the LSP package is installed . Auto-discovers the bundled .pyz server from the package archive. Install: see INSTALL-editors.md /bitwisecook/tcl-lsp/blob/main/INSTALL-editors.md sublime-text . Commands: Select Dialect, Restart Language Server, Format Document, Minify Document, Apply Safe Quick Fixes, Apply All Optimisations. A full IntelliJ Platform plugin .zip for IntelliJ IDEA 2024.1+ and other JetBrains IDEs. Includes a dedicated settings panel Settings Tools Tcl Language Server with toggles for every feature, diagnostic code, and formatting option. Features a Compiler Explorer tool window with JCEF browser for inspecting IR, CFG, SSA, and optimiser output directly inside the IDE. Install: see INSTALL-editors.md /bitwisecook/tcl-lsp/blob/main/INSTALL-editors.md jetbrains . Build from source: make build-editor-jetbrains . Fast syntax feedback fires immediately on every keystroke; deeper semantic, optimiser, and security analysis runs in the background and merges results as each tier completes. Tier 1 instant : syntax errors — missing brace caught on parse proc broken {x { puts $x } Tier 2 background : semantic — arity mismatch flagged after analysis string length "a" "b" ; E003: too many arguments Variables, procs, keywords, and strings are classified using SSA-informed type information, giving richer highlighting than a TextMate grammar alone. The server provides 44 token types beyond the standard LSP set, including sub-token highlighting inside strings. Tokens are cached per top-level chunk so only dirty regions are recomputed after an edit, and the server supports textDocument/semanticTokens/full/delta for bandwidth-efficient incremental updates. namespace eval app { variable count 0 ; 'count' highlighted as variable proc handle {request} { ; 'handle' highlighted as function incr count ; 'incr' highlighted as keyword puts "req: $request" ; '$request' highlighted as variable inside string } } In addition to standard token types keyword, function, variable, string, comment, number, operator, parameter, namespace , the server provides domain-specific token types: | Category | Token types | Example | |---|---|---| Regexp | regexpGroup , regexpCharClass , regexpQuantifier , regexpAnchor , regexpEscape , regexpBackref , regexpAlternation | regexp { \d+ \s+ \w+ } $line — each part gets distinct highlighting | Format strings | formatPercent , formatSpec , formatFlag , formatWidth | format "%- 10.2f" $val — % , - , 10.2 , and f each highlighted | Binary format | binarySpec , binaryCount , binaryFlag | binary scan $data su3 x y z — s , u , and 3 each highlighted | Clock format | clockPercent , clockSpec , clockModifier | clock format $t -format "%Y-%m-%d" — % , Y , m , d each highlighted | Escape sequences | escape | puts "line1\nline2\t${var}" — \n , \t highlighted inside strings | BIG-IP config | object , ipAddress , port , partition , pool , monitor , profile , vlan , fqdn , routeDomain , encrypted , interface | BIG-IP .conf files get object-aware highlighting | Arity errors, unknown subcommands, best-practice violations, and security issues are reported with precise ranges. Diagnostics can be suppressed inline, per-file, per-project, per-editor, or globally — see Suppressing diagnostics suppressing-diagnostics . string frobulate $x ; W001: unknown subcommand 'frobulate' set y expr $a + $b ; W100: unbraced expr double-substitution risk eval $user input ; W101: eval with substituted arguments injection risk catch { error "oops" } ; W302: catch without a result variable Context-aware completions for commands, subcommands, variables, proc names workspace-wide , switch arms, and package require names. string len| ; offers: length, last, ... set name "world" puts $na| ; offers: $name dict | ; offers: create, get, set, exists, ... Hovering on a command, proc call, variable, or operator shows its signature, doc comment, and type information. Multi-line docstrings are supported, and @param , @return , and @brief tags are parsed and displayed as structured markdown. Docstrings can appear above the proc or inside the proc body. @brief Greet a person by name. @param name - Who to greet @return The greeting string proc greet {name} { return "Hello, $name " } greet "Alice" ; hover on 'greet' shows signature + formatted @param/@return docs Jump to the definition of a proc or variable — works across files in the workspace. proc helper {} { return 42 } set x helper ; Ctrl+Click on 'helper' → jumps to proc definition above puts $x ; Ctrl+Click on '$x' → jumps to the set statement Locate every usage of a proc or variable, including inside nested braced script bodies such as if , foreach , and namespace eval . proc add {a b} { expr {$a + $b} } set sum add 1 2 ; ← reference to 'add' puts add 3 4 ; ← reference to 'add' "Find all references" on 'add' highlights all three locations Inspect incoming callers and outgoing callees for any procedure. proc validate {input} { return string is integer $input } proc process {data} { if { validate $data } { store $data } } proc main {} { process "42" } Incoming calls to 'validate': process Outgoing calls from 'process': validate, store Safely rename a proc or variable across all scopes in the file. proc greeting {name} { puts "Hi, $name" } greeting "World" Rename 'greeting' → 'salute' updates the proc definition and all call sites As you type arguments, the server shows the expected parameter list with the active parameter highlighted. proc connect {host port {timeout 30}} { ... } connect "db.local" | ↑ signature help shows: connect host port ?timeout? with 'port' highlighted as the active parameter Inline annotations show inferred types, format-string specifier meanings, and parameter names. set count 42 ; inlay: ': int' set msg format "%s has %d items" $name $count ↑ '%s → string' ↑ '%d → integer' A structured outline of the current file — procs, namespaces, variables — for quick navigation Ctrl+Shift+O / Cmd+Shift+O . namespace eval app { variable config {} ; symbol: app::config variable proc init {} { ... } ; symbol: app::init function proc run {} { init } ; symbol: app::run function } Outline: app namespace → config, init, run Search for procs and variables across all open files in the workspace Ctrl+T / Cmd+T . File: utils.tcl proc ::utils::parse csv {data} { ... } File: main.tcl Type "parse csv" in workspace symbol search → jumps to utils.tcl Collapse proc bodies, control-flow blocks, multi-line comments, and namespace bodies. ── Header comment ── ← foldable Author: ... proc calculate {x} { ← foldable if {$x 0} { ← foldable return expr {$x 2} } } Smart expand/shrink selection by syntactic structure Alt+Shift+→ / Alt+Shift+← . proc greet {name} { puts "Hello $name" } Cursor on 'name' inside puts → expand: "$name" → "Hello $name" → puts command → proc body → proc → file source paths and package require names become clickable links that navigate to the resolved file or package. package require http ; click → opens http package source source lib/utils.tcl ; click → opens lib/utils.tcl Full-document and range formatting with 25 configurable options. Defaults follow the F5 iRules Style Guide. Supports full-document textDocument/formatting and range textDocument/rangeFormatting requests. Before: proc messy { x } { if {$x 0}{return $x } set y expr $x+1 ; puts $y } After formatted : proc messy {x} { if {$x 0} { return $x } set y expr $x + 1 puts $y } Capabilities include indentation spaces or tabs, configurable size , brace placement K&R , expression bracing enforcement, variable bracing $var → ${var} , line-length wrapping, semicolon splitting, single-line body expansion, blank-line normalisation between procs and blocks, comment alignment, trailing whitespace trimming, and line-ending normalisation LF/CRLF/CR . Expression bracing enforcement enforceBracedExpr = true : if {$x 0} { ... } ; ✓ braced if $x 0 { ... } ; → rewritten to: if {$x 0} { ... } Variable bracing enforceBracedVariables = true : puts $name ; → rewritten to: puts ${name} Quick-fix actions are offered for diagnostics that have automated repairs. Refactoring actions are available on selected code. expr $a + $b ; W100 → quick-fix: wrap in braces → expr {$a + $b} catch { error "x" } ; W302 → quick-fix: add result variable → catch { error "x" } result set x expr {$x+1} ; O114 → quick-fix: use incr idiom → incr x Extract to proc — select one or more lines, trigger code actions Ctrl+. , and choose Extract selection into proc . The selected code moves into a new proc with detected variable parameters; the original lines are replaced with a call. The editor places the cursor on the new proc name so you can rename it immediately. Bundled code templates for Tcl structures and iRules event skeletons with secure defaults, collect/release pairs, and common patterns. Type 'proc' + Tab: proc name {args} { body } Type 'when' + Tab iRules : when HTTP REQUEST { handler } Switch between Tcl 8.4/8.5/8.6/9.0, F5 iRules, F5 iApps, and EDA tooling profiles. Tk, tcllib, and stdlib commands activate automatically when their package require appears. F5 iRules metadata follows BIG-IP command/event source data, including profile aliases used by newer namespaces and events, shared TLS helper profiles such as PERSIST , and protocol namespace layer metadata that stays aligned with the enabling profile stack. With dialect = tcl8.6: try { open $path r ; ✓ known in 8.6 } on error {msg} { puts $msg } With dialect = tcl8.5: try { ... } ; W002: command disabled in active dialect try requires 8.6 Full TclOO class hierarchy analysis with method resolution order MRO , class definition tracking, and object-aware introspection. oo::class create Animal { variable name constructor {n} { set name $n } method speak {} { return "$name says ..." } } oo::class create Dog { superclass Animal method speak {} { return " my name says woof " } } Hover on 'Dog' shows class hierarchy: Dog - Animal - oo::object Go-to-definition on 'speak' jumps to the method body Type hierarchy shows Dog as a subtype of Animal Features include class definition and method hover, go-to-definition for methods and constructors, type hierarchy supertypes and subtypes , MRO computation matching C Tcl's algorithm, mixin and filter chain support, private variable and method visibility TIP 500 , and property/configurable support TIP 558 . The VM executes TclOO code with 85% native test conformance against the Tcl 9.0.3 oo.test suite. The server lowers source to an intermediate representation, builds a control-flow graph, converts to SSA form, and runs type inference — all used to power deeper diagnostics and the optimiser. proc fibonacci {n} { set a 0; set b 1 for {set i 0} {$i < $n} {incr i} { set t $b set b expr {$a + $b} set a $t } return $a } IR → CFG → SSA → SCCP → liveness → type inference → bytecode The WASM code generator uses a per-proc var-escape analysis to decide which Tcl variables can stay in fast WASM locals and which must spill to the runtime frame so uplevel , upvar , eval , and dynamic set $name can see them by name. Procs that provably never let a variable escape pay zero frame-sync overhead on interpreter fallbacks. See the design doc /bitwisecook/tcl-lsp/blob/main/docs/design/compiler/var-escape-analysis.md and the KCS note /bitwisecook/tcl-lsp/blob/main/docs/kcs/features/kcs-feature-var-escape-analysis.md for the rules and the interprocedural propagation of callee upvar sources. Twenty-plus optimisation passes detect constant propagation, dead code, redundant computations, loop-invariant hoisting, strength reduction, and idiomatic rewrites — each offered as a quick-fix code action. O102 — constant expression folding: set a 1 set b expr {$a + 2} ; → suggestion: replace with 'set b 3' O114 — incr idiom recognition: set x expr {$x + 1} ; → suggestion: replace with 'incr x' O105 — constant var-ref propagation / redundant computation GVN/CSE : set a expr {$x + $y} set b expr {$x + $y} ; → suggestion: replace with 'set b $a' O106 — loop-invariant code motion LICM : for {set i 0} {$i < $n} {incr i} { set len string length $fixed ; → suggestion: hoist above the loop lappend result $len } Tracks each variable's Tcl internal representation through the SSA type lattice. When a command forces a type conversion "shimmer" , the performance cost is reported — especially inside loops. S100 — single shimmer info : set x "hello" set n llength $x ; 'x' shimmers from STRING → LIST S101 — shimmer inside loop warning : set s "42" for {set i 0} {$i < 1000} {incr i} { set v expr {$s + $i} ; 's' shimmers STRING → INT on every iteration } S102 — type thunking warning : for {set i 0} {$i < 100} {incr i} { set n llength $data ; 'data' shimmers STRING → LIST set data "updated $n" ; 'data' back to STRING — oscillates each iteration } S100–S102 are performance warnings. S110 is a correctness warning for byte-array corruption: binary data a binary format result or an iRules ::payload byte array that is forced through character-string semantics and then written back as bytes silently re-encodes every byte ≥ 0x80 . This is the canonical iRules payload-rewrite bug F5 KB K22406348 https://my.f5.com/manage/s/article/K22406348 . S110 — byte-array corruption warning : when HTTP REQUEST DATA { set body HTTP::payload set body "$body INJECTED" ; byte array decoded to a character string HTTP::payload replace 0 100 $body ; ✗ written back: UTF-8 re-encodes high bytes } Fix — re-binarify before writing back or avoid the string detour : when HTTP REQUEST DATA { set body HTTP::payload set body "$body INJECTED" binary scan $body c - ; forces a byte-array intrep HTTP::payload replace 0 100 $body ; ✓ written byte-for-byte } Plain Tcl — string case folding mangles a byte array directly: set ba binary format c {128 195 255} set up string toupper $ba ; ✗ S110: 0xFF → U+0178 corrupts the bytes Colour-aware data provenance tracking follows untrusted I/O through assignments, interpolation, and phi nodes to dangerous sinks. Commands that produce fixed-type results e.g. string length act as sanitisers. T100 — tainted data in dangerous sink: set input gets stdin eval $input ; ✗ tainted data flows into eval T102 — tainted data in option position: set pat HTTP::uri regexp $pat $string ; ✗ tainted pattern without '--' terminator regexp -- $pat $string ; ✓ safe: '--' prevents option injection IRULE1007 — collect without release side-aware : when HTTP REQUEST { HTTP::collect 1048576 ; ✗ missing matching HTTP::release on client side } Call graph, symbol graph, and data-flow graph are exposed for AI agent consumption — enabling automated code review, impact analysis, and refactoring assistance. proc validate {input} { string is integer $input } proc store {data} { puts $data } proc process {x} { if { validate $x } { store $x } } Call graph query: "who calls validate?" → process Symbol graph query: "variables in process" → x Data-flow query: "trace $x" → parameter → validate → store An interactive webview panel Ctrl+Alt+E / Cmd+Alt+E that visualises the compiler's intermediate representation, control-flow graph, SSA form, optimiser output, Tcl bytecode, and WebAssembly disassembly for the active editor. The WASM tab renders each instruction with its originating Tcl source range click an instruction to place the source cursor inside the expression, substituted command, or post- ; sub-command it compiled from , resolved call targets click call 42 ; ::greet to jump to both the callee's disassembly and its definition , resolved branch targets click br 0 ; loop header foreach to jump to the matching loop open , a labelled block / loop / if for each Tcl construct foreach , while , for , if , catch body , switch arm , a source-line comment above every instruction group, and orthogonal control-flow arrows in the left gutter. The IR, CFG, SSA, bytecode, and WASM tabs each carry an optimiser lens off / on / diff . The diff mode compares the relevant node — IR statement, CFG block, or bytecode instruction — rather than raw text, so byte offsets, source ranges, sequence indices, and tree-connector glyphs that merely shift when the optimiser adds or removes a node are ignored. A single rewrite then shows as a single localised change instead of every following line being flagged. The tcl-explorer CLI and TUI render the same offset-free diff via --opt diff . ┌─────────────────────────────────────────────────┐ │ IR │ CFG │ SSA │ Optimiser │ Bytecode │ ├─────────────────────────────────────────────────┤ │ proc fibonacci {n} │ │ ENTRY: │ │ %0 = param n │ │ %1 = const 0 ; set a 0 │ │ %2 = const 1 ; set b 1 │ │ LOOP: │ │ %3 = phi %1, ENTRY %6, BODY │ │ ... │ └─────────────────────────────────────────────────┘ A live preview panel that extracts the widget hierarchy from Tk source and renders a visual layout — updates in real time as you edit. package require Tk ttk::frame .f ttk::label .f.lbl -text "Name:" ttk::entry .f.ent -textvariable name ttk::button .f.btn -text "OK" -command { puts $name } grid .f.lbl .f.ent .f.btn -padx 5 -pady 5 pack .f Preview panel shows the grid layout with label, entry, and button Open a BIG-IP .conf or .scf file to get syntax highlighting, object navigation, and iRule extraction. BIG-IP config file bigip.conf ltm virtual /Common/my vs { destination /Common/10.0.0.1:443 pool /Common/my pool rules { /Common/my irule ← right-click → "Open iRule in Editor" } } "Extract All iRules to Files..." exports every iRule to separate .tcl files f5 CLI tool with a cleanup verb — find every object the configuration defines but no virtual server or wide-IP references, and emit a tmsh delete script in reverse-topological order so each delete runs only after the objects that reference its target have already been removed. iRule bodies are scanned too pool … , SSL::profile … , class match … , persist … , snatpool … , virtual … , node … , LSN::pool … , STATS:: , ifile … , HTTP::respond ifile … , plus every other iRule command that names a BIG-IP object . Constant-string variables are tracked through set var /Common/foo; pool $var linear copy-propagation, so refs written through local bindings are caught. f5 cleanup samples/bigip/bigip.conf f5 cleanup --keep /Common/critical pool bigip.conf f5 cleanup --json bigip.conf report.json f5 grep verb — find every BIG-IP object related to a given object name or regex, or CIDR by walking the same forward-and-reverse reference graph the cleanup analysis uses. By default the BFS traverses both directions, so a single command surfaces the seed's full neighbourhood: forward edges objects the seed depends on and reverse edges objects that depend on the seed . --cidr switches the seed selector from "match the object's full path" to "match an IP address or CIDR mentioned anywhere inside the object — header, body, or iRule script". Multiple networks may be passed at once as a comma- or whitespace-separated list, and an object qualifies when any IP/CIDR token in its text overlaps any requested network. This catches addresses buried deep inside iRule bodies if { IP::addr IP::client addr equals "10.0.0.5" } , class match … "10.0.0.0/8" , … that a plain path grep can't reach. f5 grep /Common/web pool bigip.conf f5 grep --direction reverse /Common/web1 bigip.conf f5 grep --regex '^/Common/ web|api pool$' bigip.conf f5 grep --json --max-depth 2 web pool bigip.conf f5 grep --cidr 10.0.0.0/8 bigip.conf f5 grep --cidr '10.0.0.0/8, 192.168.0.0/16' bigip.conf f5 grep --no-recurse --cidr 10.0.0.0/8 bigip.conf The related-object BFS is on by default; pass --no-recurse to skip it and return only the objects that directly match the pattern -r / --recurse toggle it explicitly back on . This applies to every match mode: substring, --regex , and --cidr . f5 irule verb group — iRules-specific analysis with event-order and event-info sub-actions, defaulting to the f5-irules dialect: f5 irule event-order samples/irules/policy.irule f5 irule event-info HTTP REQUEST --json f5 is a separate CLI from tcl . The full verb list today : | Group | Verbs | |---|---| | Acquisition | fetch , extract UCS → SCF | | Analysis | stats , graph , explain , diff , grep , cleanup , validate | | Transformation | rename , redact , unredact , encrypt-secrets , decrypt-secrets , pcap-remap , split , merge , convert , tmsh | | Round-trip | pull , push | | iRules | irule event-order , irule event-info , irule lint , irule trace , irule extract | | Misc | completion | Highlights of the newer verbs: - — pull SCF/UCS from a live BIG-IP via iControl REST or SSH system f5 fetch ssh / scp . Credentials resolve from CLI flags, env vars, an XDG hosts.toml , or interactive prompt. - Encrypted UCS — archives saved with tmsh save sys ucs