clink (CLInk cLINK), a CLI tool to manipulate links using single substitution operation.
It is based on associative theory (also in ru) and Links Notation (also in ru)
It uses C# implementation of a links data store (see also in ru).
clink can run in the browser through the Rust query processor compiled to
WebAssembly. The React workbench mirrors the current link set into
doublets-web, the WebAssembly
package built from doublets-rs.
- Live demo: https://link-foundation.github.io/link-cli/
- Browser app documentation: README-WASM.md
- Implementation notes: WEBASSEMBLY_IMPLEMENTATION.md
- docs/REQUIREMENTS.md: implemented and planned requirements collected from issues and PR comments.
- docs/ARCHITECTURE.md: repository layout, major components, dependencies, storage files, and CI.
- docs/HOW-IT-WORKS.md: deeper explanation of query processing, references, import/export, triggers, and the WebAssembly workbench.
- docs/case-studies/issue-71/README.md: evidence and analysis behind this documentation refresh.
This CLI tool can be installed globally as clink using single command (that will work if you have .NET installed):
dotnet tool install --global clink
The NuGet tool is the C# implementation and exposes the complete production
command surface, including persistent transformation triggers. The Rust
implementation under rust/ mirrors the core query engine, named references,
LiNo import/export, structure formatting, and the WebAssembly workbench API.
Persistent transformation trigger CLI options currently exist only in the C#
tool.
This tool provides all CRUD operations for links using single substitution operation (ru) which is turing complete.
Each operations split into two parts:
(matching pattern)
(substitution pattern)
When match pattern and substitution pattern are essensially the same we get no changes (no operation), it may seem like it does not any write, but it actually does the read operation.
For example when --changes option is enabled this operation:
((1: 1 1)) ((1: 1 1))
will output:
((1: 1 1)) ((1: 1 1))
That is change of 1-st link with start (source) at itself and end (target) at itself to itself. Meaning no change, but as match pattern applies only to the link with 1 as index, 1 as source and 1 as target, this "no change" can be used as read query.
Creation is just a replacement of nothing to something:
() ((1 1))
Where first () is just empty sequence of links, that symbolizes nothing. And ((1 1)) is a sequence of link with 1 as a start and 1 as end, the index is undefined so it for database to decide actual available id (index).
Deletion is just a replacement of something to nothing:
((1 1)) ()
Where ((1 1)) is a sequence of match patterns, with a single pattern for a link with 1 as a start and 1 as end, the index is undefined, meaning it can be any index. It will match only existing link, if no such link found there will be no match. Last () is just empty sequence of links, that symbolizes nothing. We don't have matched link on the right side, meaning it will be effectively deleted.
And the update is substitution itself, obviously.
((1: 1 1)) ((1: 1 2))
In that case we have a link with 1-st id on both sides, meaning it is not deleted and not created, it is changed. In this particular example with change the target of the link (its ending) to 2. 2 is ofcourse id of another link. In here we have only links, nothing else.
Create link with 1 as source and 1 as target.
clink '() ((1 1))' --changes --after→
() ((1: 1 1))
(1: 1 1)
Create link with 2 as source and 2 as target.
clink '() ((2 2))' --changes --after→
() ((2: 2 2))
(1: 1 1)
(2: 2 2)
Create two links at the same time: (1 1) and (2 2).
clink '() ((1 1) (2 2))' --changes --after→
() ((2: 2 2))
() ((1: 1 1))
(1: 1 1)
(2: 2 2)
clink '((($i: $s $t)) (($i: $s $t)))' --changes --after→
((1: 1 1)) ((1: 1 1))
((2: 2 2)) ((2: 2 2))
(1: 1 1)
(2: 2 2)
Where $i stands for variable named i, that stands for index. $s is for source and $t is for target.
A short version of read operation will also work:
clink '((($i:)) (($i:)))' --changes
String references can name links. Names are persisted in a companion
<database-name>.names.links file and are rendered in output whenever a link
has a name. Missing named references are rejected by default; use
--auto-create-missing-references when the query should create missing names as
self-referential point links.
Create a named child link from named father and mother references:
clink --db family.links --auto-create-missing-references '() ((child: father mother))' --changes --after→
((father: 0 0)) ((father: father father))
((mother: 0 0)) ((mother: mother mother))
() ((child: father mother))
(father: father father)
(mother: mother mother)
(child: father mother)
Read the named link without changing it:
clink --db family.links '(((child: father mother)) ((child: father mother)))' --changes→
((child: father mother)) ((child: father mother))
Update the named link by swapping its source and target:
clink --db family.links '((child: father mother)) ((child: mother father))' --changes --after→
((child: father mother)) ((child: mother father))
(father: father father)
(mother: mother mother)
(child: mother father)
Delete the named link:
clink --db family.links '((child: mother father)) ()' --changes --after→
((3: mother father)) ()
(father: father father)
(mother: mother mother)
The deleted link no longer has the child name when the deletion change is
printed because deleting a link also removes its name mapping.
Variables work with named references too. Starting from a database where child
still points to father mother, this query reads every link and writes it back
with source and target swapped:
clink --db family.links '((($index: $source $target)) (($index: $target $source)))' --changes --after→
((father: father father)) ((father: father father))
((mother: mother mother)) ((mother: mother mother))
((child: father mother)) ((child: mother father))
(father: father father)
(mother: mother mother)
(child: mother father)
If a name contains spaces, parentheses, colons, or quotes, the exporter quotes it in LiNo output.
Use --structure <id> to render the left branch of a link recursively. The
formatter keeps the current link index and stops recursion when it would revisit
a link.
clink --db family.links --structure 3→
(child: (father: father father) mother)
For numbered links:
clink --db structure.links --structure 4→
(4: (3: (2: (1: 1 1) 2) 1) 2)
Use --in, --import, or --lino-input to read a .lino file before the
query runs. Import accepts one complete two-value link definition per non-empty
line.
(father: father father)
(mother: mother mother)
(child: father mother)
clink --db imported.links --import family.lino --export exported.linoexported.lino:
(father: father father)
(mother: mother mother)
(child: father mother)
Use --out or --export to write the complete database to a .lino file after the query is processed. The older --lino-output option is also accepted.
clink --auto-create-missing-references '() ((child: father mother))' --export database.linodatabase.lino:
(father: father father)
(mother: mother mother)
(child: father mother)
When links do not have names, exported references are plain link numbers:
(1: 1 1)
(2: 1 2)
Store a query as a trigger with --always to apply it after later write operations:
clink --db graph.links --always '(((1: 1 1)) ((1: 1 2)))'
clink --db graph.links --auto-create-missing-references '() ((1: 1 1))' --afterUse --once for a trigger that deletes itself after the first successful application, and --never to remove matching stored triggers:
clink --db graph.links --once '(((1: 1 1)) ((1: 1 2)))'
clink --db graph.links --never '(((1: 1 1)) ((1: 1 2)))'Triggers are stored as binary links using the structure (Always ((Condition ...) (Substitution ...))) or (Once ((Condition ...) (Substitution ...))). By default they are kept in a companion <database-name>.triggers.links file, such as graph.triggers.links for graph.links. Use --triggers-file path/to/triggers.links to choose a different companion file, --triggers to enable trigger evaluation explicitly, or --embed-triggers to store trigger links in the main database file.
--db selects the primary links database file. With the default database name,
the CLI uses these files:
| File | Purpose |
|---|---|
db.links |
Primary link triples. |
db.names.links |
Named-reference sidecar used by NamedTypesDecorator. |
db.triggers.links |
Persistent transformation trigger sidecar when trigger support is enabled. |
Use --triggers-file to point trigger storage somewhere else, or
--embed-triggers to store trigger links in the main database. Names are stored
separately so the primary links database remains numeric.
Update link with index 1 and source 1 and target 1, changing target to 2.
clink '((1: 1 1)) ((1: 1 2))' --changes --after→
((1: 1 1)) ((1: 1 2))
(1: 1 2)
(2: 2 2)
Update link with index 1 and source 1 and target 1, changing target to 2.
clink '((1: 1 1) (2: 2 2)) ((1: 1 2) (2: 2 1))' --changes --after→
((1: 1 1)) ((1: 1 2))
((2: 2 2)) ((2: 2 1))
(1: 1 2)
(2: 2 1)
Delete link with source 1 and target 2:
clink '((1 2)) ()' --changes --after→
((1: 1 2)) ()
(2: 2 2)
Delete link with source 2 and target 2:
clink '((2 2)) ()' --changes --after→
((2: 2 2)) ()
clink '((1 2) (2 2)) ()' --changes --after→
((1: 1 2)) ()
((2: 2 2)) ()
clink '((* *)) ()' --changes --after→
((1: 1 2)) ()
((2: 2 2)) ()
When creating nested links, identical sub-links are automatically deduplicated. This means if the same link pattern appears multiple times, it will only be created once and reused.
Create a nested structure where (m a) appears twice:
clink '() (((m a) (m a)))' --after→
(m: m m)
(a: a a)
(3: m a)
(4: 3 3)
In this example:
mandaare named self-referencing links(m a)is created once with index 3- The outer link
((m a) (m a))has index 4, pointing to link 3 twice (source=3, target=3)
clink '(((m a) (m a))) (((p a) (p a)))' --after→
(p: p p)
(a: a a)
(3: p a)
(4: 3 3)
The update operation replaces the structure, but note that a is reused between expressions.
clink '() (((m a) (a m)))' --after→
(m: m m)
(a: a a)
(3: m a)
(4: a m)
(5: 3 4)
Since (m a) and (a m) are different links, they are both created. The outer link references both of them.
clink '() ((1 1) (2 2))' --changes --after
clink '((1: 1 1) (2: 2 2)) ((1: 1 2) (2: 2 1))' --changes --after
clink '((1 2) (2 1)) ()' --changes --afterclink '() ((1 2) (2 1))' --changes --after
clink '((($index: $source $target)) (($index: $target $source)))' --changes --after
clink '((1: 2 1) (2: 1 2)) ()' --changes --afterThe C# NuGet tool supports every option below. The Rust CLI currently supports the core query, storage, output, import/export, and structure options; trigger options are C#-only for now.
| Parameter | Type | Default Value | Aliases | Description |
|---|---|---|---|---|
--db |
string | db.links |
--data-source, --data, -d |
Path to the links database file |
--query |
string | None | --apply, --do, -q |
LiNo query for CRUD operation |
query (positional) |
string | None | N/A | LiNo query for CRUD operation (provided as the first positional argument) |
--trace |
bool | false |
-t |
Enable trace (verbose output) |
--auto-create-missing-references |
bool | false |
None | Create missing numeric and named references as self-referential point links |
--structure |
uint? | None | -s |
ID of the link to format its structure |
--before |
bool | false |
-b |
Print the state of the database before applying changes |
--changes |
bool | false |
-c |
Print the changes applied by the query |
--after |
bool | false |
--links, -a |
Print the state of the database after applying changes |
--in |
string | None | --import, --lino-input |
Read and import a LiNo file before query execution |
--out |
string | None | --export, --lino-output |
Write the complete database as a LiNo file |
--always |
bool | false |
None | Store the query as an always-on persistent transformation trigger |
--once |
bool | false |
None | Store the query as a one-shot persistent transformation trigger |
--never |
bool | false |
None | Remove stored persistent transformation triggers matching the query |
--triggers |
bool | false |
None | Enable persistent transformation triggers for the command |
--triggers-file |
string | <db>.triggers.links |
None | Path to the persistent transformation trigger links database |
--embed-triggers |
bool | false |
None | Store persistent transformation triggers in the main links database |
The query can be passed as the first positional argument or through --query,
--apply, or --do. In the Rust CLI, --query takes precedence when both
--query and a positional query are provided.
dotnet run --project csharp/Foundation.Data.Doublets.Cli -- '(((1: 1 1) (2: 2 2)) ((1: 1 2) (2: 2 1)))' --changes --aftercd csharp/Foundation.Data.Doublets.Cli
dotnet run -- '(((1: 1 1) (2: 2 2)) ((1: 1 2) (2: 2 1)))' --changes --afterdotnet run --project csharp/Foundation.Data.Doublets.Cli -- '() ((1 1) (2 2))' --changes --after
dotnet run --project csharp/Foundation.Data.Doublets.Cli -- '((1: 1 1) (2: 2 2)) ((1: 1 2) (2: 2 1))' --changes --after
dotnet run --project csharp/Foundation.Data.Doublets.Cli -- '((1 2) (2 1)) ()' --changes --afterdotnet run --project csharp/Foundation.Data.Doublets.Cli -- '() ((1 2) (2 1))' --changes --after
dotnet run --project csharp/Foundation.Data.Doublets.Cli -- '((($index: $source $target)) (($index: $target $source)))' --changes --after
dotnet run --project csharp/Foundation.Data.Doublets.Cli -- '((1: 2 1) (2: 1 2)) ()' --changes --afterVERSION=$(awk -F'[<>]' '/<Version>/ {print $3}' csharp/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj) && git tag "v$VERSION" && git push origin "v$VERSION"To run a specific test (e.g., DeleteAllLinksByIndexTest) with detailed output, use:
dotnet test --filter "FullyQualifiedName=Foundation.Data.Doublets.Cli.Tests.Tests.AdvancedMixedQueryProcessor.DeleteAllLinksByIndexTest" --logger "console;verbosity=detailed"
This will execute only the specified test and show detailed logs in the console.
Short version:
dotnet test --filter DeleteAllLinksByIndexTest -v n