Skip to content

CLI raw commands

Daniel Frantík edited this page Oct 10, 2026 · 1 revision

CLI raw commands

tik4net lets you drive a CLI connection in either of two dialects:

  • the API dialect, translated for you — /ip/address/print with ?interface=ether1, the same command and parameters you would send over the binary API. tik4net turns it into a RouterOS command line, runs it and parses the answer. This is the standard way: the code is portable across all eleven transports, and the ADO.NET-like API and the O/R mapper work this way. What each call becomes on the wire is described in Command translation on non-API transports.
  • the CLI's own native dialect, sent unchanged — :put [/ip address print as-value where interface=ether1], exactly what you would type at the router's terminal. This page is about this one.

The native dialect works on all five CLI transports — Telnet, Ssh, MacTelnet, WinboxCli and WinboxCliMac — because all five end at the same RouterOS console. A line that works on one works on the other four, so only the Create…Connection() call differs.

When to use it

Stay with the API dialect for CRUD — it is less work, portable, and its error handling knows which verb it sent. Go native when you want something the API dialect cannot say:

  • scripting — :foreach, :len, :local, arithmetic, find with any condition the console accepts;
  • free text — /export, or anything else that is not a table;
  • a modifier or a menu tik4net does not model;
  • a command copied from a MikroTik forum post or from your own terminal history, used as it is.

A practical example: blocklist housekeeping

The program keeps an address list of offenders. It adds entries, reads them back, extends the timeout of a whole subnet in one script line, counts the result, prints the list as a person would see it, and cleans up. Pick the transport on the command line; nothing below Open knows which one it got.

// ==========================================================================
// TASK       maintain a firewall address list ("blocklist") in native CLI syntax:
//            add → read → bulk update by script → count → print as text → clean up
// LEVEL      Raw — CallCommandSync (sentences) and CreateRawCommand (ITikCommand)
// TRANSPORT  telnet | ssh | mactelnet | winboxcli | winboxclimac   (args[0])
// ==========================================================================
// ON THE ROUTER: the service for the transport you pick, and a user with
//   read,write plus the transport's policy (telnet / ssh / winbox).
//   Only rows with list=tik4net-blocklist are touched, and they are removed at the end.
//
// OUTPUT     (RouterOS 7.24.5 — identical on all five transports, apart from the ids and times)
//            added 192.0.2.10 as *58
//            added 192.0.2.11 as *59
//            added 198.51.100.7 as *5A
//            *58  192.0.2.10  timeout=23h59m59s  comment=ssh brute force
//            *59  192.0.2.11  timeout=23h59m59s  comment=ssh brute force
//            *5A  198.51.100.7  timeout=23h59m59s  comment=ssh brute force
//            entries extended to 7d: 2
//            Flags: D - DYNAMIC
//            Columns: LIST, ADDRESS, CREATION-TIME, TIMEOUT
//            #   LIST               ADDRESS       CREATION-TIME        TIMEOUT
//            ;;; ssh brute force
//            0 D tik4net-blocklist  192.0.2.10    2026-10-10 13:47:20  6d23h59m59s
//            ...
//            2 D tik4net-blocklist  198.51.100.7  2026-10-10 13:47:21  23h59m59s
//            refused as expected: no such item (4)
//
// WATCH OUT  An entry with a timeout is DYNAMIC (the D flag), and /export leaves dynamic
//            entries out — which is why step 5 prints the list instead of exporting it.
//            The two MAC-layer transports find the router's MAC by MNDP. Where that fails, pass
//            it: setup.CreateMacTelnetConnection("AA:BB:CC:DD:EE:FF").
// ==========================================================================

using System;
using System.Linq;
using tik4net;
using tik4net.Telnet;
using tik4net.Ssh;
using tik4net.MacTelnet;
using tik4net.WinboxCli;
using tik4net.WinboxCliMac;

class Program
{
    const string Host     = "192.168.88.1";
    const string User     = "admin";
    const string Password = "";
    const string List     = "tik4net-blocklist";

    static ITikCliConnection Open(TikConnectionSetup setup, string transport) => transport switch
    {
        "telnet"       => setup.CreateTelnetConnection(),
        "ssh"          => setup.CreateSshConnection(),
        "mactelnet"    => setup.CreateMacTelnetConnection(),     // router MAC found by MNDP
        "winboxcli"    => setup.CreateWinboxCliConnection(),
        "winboxclimac" => setup.CreateWinboxCliMacConnection(),
        _ => throw new ArgumentException("unknown transport: " + transport),
    };

    static void Main(string[] args)
    {
        var setup = new TikConnectionSetup(Host, User, Password);
        using (ITikCliConnection cli = Open(setup, args.Length > 0 ? args[0] : "telnet"))
        {
            // 1. ADD — ':put [ … ]' is what makes the console print the new row's .id.
            //    Without it, add prints nothing at all.
            foreach (string address in new[] { "192.0.2.10", "192.0.2.11", "198.51.100.7" })
            {
                var reply = cli.CallCommandSync(
                    ":put [/ip firewall address-list add list=" + List + " address=" + address
                    + " timeout=1d comment=\"ssh brute force\"]");
                string id = reply.OfType<ITikDoneSentence>().Single().GetResponseWord();
                Console.WriteLine("added " + address + " as " + id);
            }

            // 2. READ — as-value output becomes one ITikReSentence per row.
            //    'detail' asks for every field, not just the summary columns.
            foreach (var row in cli.CallCommandSync(
                         ":put [/ip firewall address-list print detail as-value where list=" + List + "]")
                     .OfType<ITikReSentence>())
                Console.WriteLine(row.GetId() + "  " + row.GetResponseField("address")
                                  + "  timeout=" + row.GetResponseFieldOrDefault("timeout", "-")
                                  + "  comment=" + row.GetResponseFieldOrDefault("comment", "<unset>"));

            // 3. BULK UPDATE BY SCRIPT — one line, one round trip, any condition the console
            //    understands ('in' tests subnet membership). The API dialect has no such sentence.
            cli.CreateRawCommand(
                    ":foreach i in=[/ip firewall address-list find where list=" + List
                    + " and address in 192.0.2.0/24] do={ /ip firewall address-list set $i timeout=7d }")
               .ExecuteNonQuery();

            // 4. A COMPUTED VALUE — ExecuteScalar returns what the line printed.
            string count = cli.CreateRawCommand(
                    ":put [:len [/ip firewall address-list find where list=" + List + " and timeout>3d]]")
                .ExecuteScalar();
            Console.WriteLine("entries extended to 7d: " + count);

            // 5. FREE TEXT — a print without as-value is the table a person would see, and comes
            //    back whole, as text. (/export is read the same way.)
            Console.WriteLine(cli.CreateRawCommand("/ip firewall address-list print where list=" + List)
                                 .ExecuteScalar());

            // 6. A REFUSAL — raised as the same exception type the portable levels use.
            try
            {
                cli.CreateRawCommand("/ip firewall address-list remove numbers=*FFFFFF").ExecuteNonQuery();
            }
            catch (TikNoSuchItemException ex)
            {
                Console.WriteLine("refused as expected: " + ex.Message);
            }

            // 7. CLEAN UP — one line for the whole list.
            cli.CreateRawCommand("/ip firewall address-list remove [find where list=" + List + "]")
               .ExecuteNonQuery();
        }
    }
}

Two entry points

The program uses both, on purpose. They send the same line; they differ in what they hand back.

CallCommandSync / CallCommandAsync CreateRawCommand(line)
Where it is ITikRawSentenceConnection — which ITikCliConnection is an extension on ITikConnection
What comes back sentences: one ITikReSentence per as-value row, then an ITikDoneSentence; any other output is the ret word of the !done an ITikCommand: ExecuteList · ExecuteScalar · ExecuteNonQuery
Read rows write the :put [ … as-value] yourself the same, or pass wrapAsValue: true
A router error throws throws
Best for reading rows; code shared with the Low-level API scalars, free text, writes

Several rows given to CallCommandSync are joined with one space, so CallCommandSync(":put [/interface print", "as-value]") and the one-piece line are the same command.

On a connection whose transport is chosen at run time, check connection.Supports(TikConnectionCapability.RawCommand). The binary API (Api / ApiSsl) has the same two entry points, but its native dialect is API sentence words, not CLI text — see Low-level API. REST and WinBox native have no command language, so raw CLI is not offered there; on the WinBox channel use WinBox CLI, or address WinBox native with WinBox's own names.

What the router does not tell you

Raw means the line goes out as you wrote it — including its mistakes. These are the ones that do not fail: the command is accepted and the answer is wrong or empty. The API dialect avoids every one of them for you.

Symptom Cause Fix
a read returns no rows a bare print as-value — the console prints as-value output only inside a script wrap it in :put [ … ], or wrapAsValue: true
a filter matches nothing an unquoted /, : or . in a where value; ="true" on a boolean quote values; booleans are yes / no
a filter matches everything a field name the menu does not have, such as the other RouterOS version's name check the name — CLI tips and tricks
a field is missing the summary columns; a secret without show-sensitive; counters without stats detail, show-sensitive, a separate stats read
a value reads back changed $ or \ inside double quotes — substituted and unescaped by the router escape them: \$, \\
a set or remove "succeeds" on a row that is gone [find …] that matches nothing is not an error address by numbers=*N, which answers no such item (4)

The full list, with the measurements behind it, is CLI tips and tricks.

See also

Clone this wiki locally