Skip to content

Latest commit

 

History

History
566 lines (425 loc) · 18 KB

File metadata and controls

566 lines (425 loc) · 18 KB

link-cli

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).

WebAssembly Browser Workbench

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.

Documentation

Installation

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
Screenshot 2025-05-16 at 5 48 06 AM

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 single link

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 multiple links

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)

Read all links

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

Named references

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.

Structure formatting

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)

Import database from LiNo

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.lino

exported.lino:

(father: father father)
(mother: mother mother)
(child: father mother)

Export database as LiNo

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.lino

database.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)

Persistent transformation triggers

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))' --after

Use --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.

Database files

--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 single link

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 multiple links

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 single link

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)) ()

Delete multiple links

clink '((1 2) (2 2)) ()' --changes --after

((1: 1 2)) ()
((2: 2 2)) ()

Delete all links

clink '((* *)) ()' --changes --after

((1: 1 2)) ()
((2: 2 2)) ()

Link deduplication

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.

Example 1: Duplicate pair deduplication

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:

  • m and a are 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)

Example 2: Multiple expressions with shared sub-links

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.

Example 3: Different sub-links are not deduplicated

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.

Complete examples:

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 --after
clink '() ((1 2) (2 1))' --changes --after

clink '((($index: $source $target)) (($index: $target $source)))' --changes --after

clink '((1: 2 1) (2: 1 2)) ()' --changes --after

All options and arguments

The 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.

For developers and debugging

Execute from root

dotnet run --project csharp/Foundation.Data.Doublets.Cli -- '(((1: 1 1) (2: 2 2)) ((1: 1 2) (2: 2 1)))' --changes --after

Execute from folder

cd csharp/Foundation.Data.Doublets.Cli
dotnet run -- '(((1: 1 1) (2: 2 2)) ((1: 1 2) (2: 2 1)))' --changes --after

Complete examples:

dotnet 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 --after
dotnet 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 --after

Publish next version:

VERSION=$(awk -F'[<>]' '/<Version>/ {print $3}' csharp/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj) && git tag "v$VERSION" && git push origin "v$VERSION"

Running a Specific Test with Detailed Output

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