Skip to content

Commit b117805

Browse files
committed
Add alias management commands for runfile and gist
Introduces an alias system for both runfile and gist tools, enabling users to create, list, delete, and rename aliases for remote refs via a new `alias` command. Adds the `AliasCommands` class to handle alias operations and updates CLI help and argument parsing to support alias management. Aliases are stored in the global `.netconfig` file and can be used in place of full refs. Documentation and launch profiles are updated accordingly. Also adds `Tmds.DBus.Protocol` as a dependency and improves ref web link generation.
1 parent 0c6d618 commit b117805

11 files changed

Lines changed: 367 additions & 0 deletions

File tree

readme.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,56 @@ The last download etag is used to avoid downloading on each run.
135135

136136
<!-- #gist -->
137137

138+
## Aliases
139+
140+
<!-- #aliases -->
141+
142+
`runfile` and `gist` share the same alias store, so a name created with one tool works with the other.
143+
Aliases are stored in your global `.netconfig` file (`~/.netconfig` on Linux/macOS, `%USERPROFILE%\.netconfig` on Windows).
144+
145+
### Creating an alias
146+
147+
Pass `--dnx-alias` (or `--alias`) when running a ref:
148+
149+
```
150+
dnx TOOL REF --dnx-alias NAME
151+
```
152+
153+
Examples:
154+
155+
```
156+
dnx runfile kzu/sandbox@main:run.cs --dnx-alias sandbox
157+
dnx gist kzu/0ac826dc7de666546aaedd38e5965381 --dnx-alias demo
158+
```
159+
160+
After that, use the alias instead of the full ref:
161+
162+
```
163+
dnx TOOL NAME
164+
```
165+
166+
Re-running with the same alias name updates the ref it points to.
167+
168+
### Managing aliases
169+
170+
```
171+
[dnx] TOOL alias
172+
[dnx] TOOL alias delete NAME
173+
[dnx] TOOL alias rename OLD NEW
174+
```
175+
176+
`TOOL` is `runfile` or `gist`.
177+
178+
| Command | Description |
179+
| --- | --- |
180+
| `alias` | List all configured aliases and the refs they point to. Refs link to the source on the web; `github.com/` is omitted when it is the default host. |
181+
| `alias delete NAME` | Remove an alias. |
182+
| `alias rename OLD NEW` | Rename an alias, keeping the same ref. Fails if `NEW` already exists. |
183+
184+
Running a tool with no arguments shows the usage help followed by the alias table when any aliases are configured.
185+
186+
<!-- #aliases -->
187+
138188
# Dogfooding
139189

140190
[![CI Version](https://img.shields.io/endpoint?url=https://shields.kzu.app/vpre/gist/main&label=nuget.ci&color=brightgreen)](https://pkg.kzu.app/index.json)

src/Core/AliasCommands.cs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
using DotNetConfig;
2+
using Spectre.Console;
3+
4+
namespace Devlooped;
5+
6+
public static class AliasCommands
7+
{
8+
public const string CommandName = "alias";
9+
internal const string Section = "runfile";
10+
11+
public static int Execute(string[] args, Config config)
12+
{
13+
if (args.Length == 0)
14+
return List(config);
15+
16+
if (args[0] is "delete")
17+
return Delete(args, config);
18+
19+
if (args[0] is "rename")
20+
return Rename(args, config);
21+
22+
ShowUsage();
23+
return 1;
24+
}
25+
26+
/// <summary>Writes the alias table when any are configured. Returns whether a table was written.</summary>
27+
public static bool WriteTableIfAny(Config config)
28+
{
29+
var aliases = GetAliases(config);
30+
if (aliases.Length == 0)
31+
return false;
32+
33+
AnsiConsole.WriteLine();
34+
AnsiConsole.WriteLine("Aliases:");
35+
36+
WriteTable(aliases);
37+
return true;
38+
}
39+
40+
static int List(Config config)
41+
{
42+
var aliases = GetAliases(config);
43+
if (aliases.Length == 0)
44+
{
45+
AnsiConsole.MarkupLine(":information_source: [grey]No aliases configured. Use[/] [yellow]--dnx-alias[/] [grey]to create one.[/]");
46+
return 0;
47+
}
48+
49+
AnsiConsole.MarkupLine($"[grey]Configured ref aliases[/] [dim]({aliases.Length})[/]:");
50+
WriteTable(aliases);
51+
return 0;
52+
}
53+
54+
static int Delete(string[] args, Config config)
55+
{
56+
if (args.Length < 2 || string.IsNullOrWhiteSpace(args[1]))
57+
{
58+
AnsiConsole.MarkupLine(":cross_mark: [red]Missing alias name.[/]");
59+
ShowUsage();
60+
return 1;
61+
}
62+
63+
var name = args[1];
64+
if (config.GetString(Section, name) is null)
65+
{
66+
AnsiConsole.MarkupLine($":cross_mark: [red]Alias[/] [cyan bold]{Markup.Escape(name)}[/] [red]not found.[/]");
67+
return 1;
68+
}
69+
70+
config.Unset(Section, name);
71+
AnsiConsole.MarkupLine($":check_mark_button: [green]Deleted alias[/] [cyan bold]{Markup.Escape(name)}[/][green].[/]");
72+
return 0;
73+
}
74+
75+
static int Rename(string[] args, Config config)
76+
{
77+
if (args.Length < 3 || string.IsNullOrWhiteSpace(args[1]) || string.IsNullOrWhiteSpace(args[2]))
78+
{
79+
AnsiConsole.MarkupLine(":cross_mark: [red]Missing alias name.[/]");
80+
ShowUsage();
81+
return 1;
82+
}
83+
84+
var oldName = args[1];
85+
var newName = args[2];
86+
87+
if (string.Equals(oldName, newName, StringComparison.OrdinalIgnoreCase))
88+
{
89+
AnsiConsole.MarkupLine($":information_source: [grey]Alias[/] [cyan bold]{Markup.Escape(oldName)}[/] [grey]already has that name.[/]");
90+
return 0;
91+
}
92+
93+
if (config.GetString(Section, oldName) is not { } @ref)
94+
{
95+
AnsiConsole.MarkupLine($":cross_mark: [red]Alias[/] [cyan bold]{Markup.Escape(oldName)}[/] [red]not found.[/]");
96+
return 1;
97+
}
98+
99+
if (config.GetString(Section, newName) is not null)
100+
{
101+
AnsiConsole.MarkupLine($":cross_mark: [red]Alias[/] [cyan bold]{Markup.Escape(newName)}[/] [red]already exists.[/]");
102+
return 1;
103+
}
104+
105+
config.SetString(Section, newName, @ref).Unset(Section, oldName);
106+
AnsiConsole.MarkupLine($":check_mark_button: [green]Renamed alias[/] [cyan bold]{Markup.Escape(oldName)}[/] [green]to[/] [cyan bold]{Markup.Escape(newName)}[/][green].[/]");
107+
return 0;
108+
}
109+
110+
static (string Name, string Ref)[] GetAliases(Config config) =>
111+
config
112+
.Where(entry => entry.Section == Section && entry.Subsection == null)
113+
.Select(entry => entry.Variable)
114+
.Distinct(StringComparer.OrdinalIgnoreCase)
115+
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
116+
.Select(name => (Name: name, Ref: config.GetString(Section, name)!))
117+
.Where(alias => alias.Ref != null)
118+
.ToArray();
119+
120+
static void WriteTable((string Name, string Ref)[] aliases)
121+
{
122+
var table = new Table()
123+
.RoundedBorder()
124+
.BorderColor(Color.Grey23)
125+
.AddColumn(new TableColumn("[lime bold]:label: Alias[/]").LeftAligned())
126+
.AddColumn(new TableColumn("[lime bold]:link: Ref[/]").LeftAligned());
127+
128+
foreach (var (name, @ref) in aliases)
129+
table.AddRow($"[cyan bold]{Markup.Escape(name)}[/]", FormatRefLink(@ref));
130+
131+
AnsiConsole.Write(table);
132+
}
133+
134+
static string FormatRef(string @ref) =>
135+
@ref.StartsWith("github.com/", StringComparison.OrdinalIgnoreCase)
136+
? @ref["github.com/".Length..]
137+
: @ref;
138+
139+
static string FormatRefLink(string @ref)
140+
{
141+
var display = Markup.Escape(FormatRef(@ref));
142+
return RemoteRef.TryParse(@ref, out var parsed)
143+
? $"[link={parsed.ToWebUrl()}]{display}[/]"
144+
: $"[link]{display}[/]";
145+
}
146+
147+
static void ShowUsage() =>
148+
AnsiConsole.MarkupLine(
149+
"""
150+
[grey]Usage:[/]
151+
[cyan bold]alias[/] [grey]List configured ref aliases.[/]
152+
[cyan bold]alias delete[/] [yellow]NAME[/] [grey]Delete a ref alias.[/]
153+
[cyan bold]alias rename[/] [yellow]OLD[/] [yellow]NEW[/] [grey]Rename a ref alias.[/]
154+
""");
155+
}

src/Core/RemoteRefExtensions.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,35 @@ public static class RemoteRefExtensions
99
public string TempPath => Path.Join(GetTempRoot(), location.Host ?? "github.com", location.Owner, location.Project ?? "", location.Repo, location.Ref ?? "main");
1010

1111
public string EnsureTempPath() => Directory.CreateUserDirectory(location.TempPath);
12+
13+
public string ToWebUrl()
14+
{
15+
var host = location.Host ?? "github.com";
16+
var reference = location.Ref ?? "main";
17+
18+
return host switch
19+
{
20+
"gist.github.com" => $"https://gist.github.com/{location.Owner}/{location.Repo}",
21+
"gitlab.com" => location.Path != null
22+
? $"https://gitlab.com/{location.Owner}/{location.Repo}/-/blob/{reference}/{location.Path}"
23+
: $"https://gitlab.com/{location.Owner}/{location.Repo}",
24+
"dev.azure.com" => ToAzureDevOpsWebUrl(location),
25+
_ => location.Path != null
26+
? $"https://github.com/{location.Owner}/{location.Repo}/blob/{reference}/{location.Path}"
27+
: $"https://github.com/{location.Owner}/{location.Repo}/tree/{reference}",
28+
};
29+
}
30+
}
31+
32+
static string ToAzureDevOpsWebUrl(RemoteRef location)
33+
{
34+
var project = location.Project ?? location.Repo;
35+
var url = $"https://dev.azure.com/{location.Owner}/{project}/_git/{location.Repo}";
36+
if (location.Path != null)
37+
url += $"?path=/{location.Path}";
38+
if (location.Ref != null)
39+
url += location.Path != null ? $"&version=GB{location.Ref}" : $"?version=GB{location.Ref}";
40+
return url;
1241
}
1342

1443
/// <summary>Obtains the temporary directory root, e.g., <c>/tmp/dotnet/runfile/</c>.</summary>

src/Tests/Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
1212
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.7" />
1313
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.7" />
14+
<PackageReference Include="Tmds.DBus.Protocol" Version="0.93.0" />
1415
<PackageReference Include="xunit" Version="2.9.3" />
1516
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" PrivateAssets="all" />
1617
</ItemGroup>

src/gist/Program.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
}
3636

3737
var config = Config.Build(Config.GlobalLocation);
38+
if (args.Length > 0 && args[0] == AliasCommands.CommandName)
39+
Environment.Exit(AliasCommands.Execute(args[1..], config));
40+
3841
if (args.Length > 0 && config.GetString("runfile", args[0]) is string aliased)
3942
args = [aliased, .. args[1..]];
4043

@@ -57,6 +60,12 @@
5760
$"""
5861
Usage:
5962
[grey][[dnx]][/] [lime]{ThisAssembly.Project.ToolCommandName}[/] [grey][[OPTIONS]][/] [bold]<gistRef>[/] [grey italic][[<appArgs>...]][/]
63+
[grey][[dnx]][/] [lime]{ThisAssembly.Project.ToolCommandName}[/] [bold]alias[/] [[delete NAME]]
64+
65+
Commands:
66+
[bold]alias[/] List configured ref aliases.
67+
[bold]alias delete[/] NAME Delete a ref alias.
68+
[bold]alias rename[/] OLD NEW Rename a ref alias.
6069
6170
Arguments:
6271
[bold]<GIST_REF>[/] Reference to gist file to run, with format [yellow]owner/gist[[@commit]][[:path]][/]
@@ -77,6 +86,8 @@
7786
[bold]--dnx-debug[/] Launch the debugger before running.
7887
[bold]--dnx-force[/] Force download, skipping ETag checking.
7988
""");
89+
if (args.Length == 0)
90+
AliasCommands.WriteTableIfAny(config);
8091
return;
8192
}
8293

src/gist/gist.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<PackageReference Include="Spectre.Console" Version="0.55.2" />
3131
<PackageReference Include="ThisAssembly" Version="2.1.2" PrivateAssets="all" />
3232
<PackageReference Include="System.CommandLine" Version="2.0.7" />
33+
<PackageReference Include="Tmds.DBus.Protocol" Version="0.93.0" />
3334
</ItemGroup>
3435

3536
<ItemGroup Condition="'$(Pkggit-credential-manager)' != ''">

src/gist/readme.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,58 @@ The last download etag is used to avoid downloading on each run.
5454

5555
<!-- #gist -->
5656
<!-- ../../readme.md#gist -->
57+
58+
## Aliases
59+
60+
<!-- include ../../readme.md#aliases -->
61+
<!-- #aliases -->
62+
63+
`runfile` and `gist` share the same alias store, so a name created with one tool works with the other.
64+
Aliases are stored in your global `.netconfig` file (`~/.netconfig` on Linux/macOS, `%USERPROFILE%\.netconfig` on Windows).
65+
66+
### Creating an alias
67+
68+
Pass `--dnx-alias` (or `--alias`) when running a ref:
69+
70+
```
71+
dnx TOOL REF --dnx-alias NAME
72+
```
73+
74+
Examples:
75+
76+
```
77+
dnx runfile kzu/sandbox@main:run.cs --dnx-alias sandbox
78+
dnx gist kzu/0ac826dc7de666546aaedd38e5965381 --dnx-alias demo
79+
```
80+
81+
After that, use the alias instead of the full ref:
82+
83+
```
84+
dnx TOOL NAME
85+
```
86+
87+
Re-running with the same alias name updates the ref it points to.
88+
89+
### Managing aliases
90+
91+
```
92+
[dnx] TOOL alias
93+
[dnx] TOOL alias delete NAME
94+
[dnx] TOOL alias rename OLD NEW
95+
```
96+
97+
`TOOL` is `runfile` or `gist`.
98+
99+
| Command | Description |
100+
| --- | --- |
101+
| `alias` | List all configured aliases and the refs they point to. Refs link to the source on the web; `github.com/` is omitted when it is the default host. |
102+
| `alias delete NAME` | Remove an alias. |
103+
| `alias rename OLD NEW` | Rename an alias, keeping the same ref. Fails if `NEW` already exists. |
104+
105+
Running a tool with no arguments shows the usage help followed by the alias table when any aliases are configured.
106+
107+
<!-- #aliases -->
108+
<!-- ../../readme.md#aliases -->
57109
<!-- include https://github.com/devlooped/.github/raw/main/osmf.md -->
58110
## Open Source Maintenance Fee
59111

src/runfile/Program.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
}
3636

3737
var config = Config.Build(Config.GlobalLocation);
38+
if (args.Length > 0 && args[0] == AliasCommands.CommandName)
39+
Environment.Exit(AliasCommands.Execute(args[1..], config));
40+
3841
if (args.Length > 0 && config.GetString("runfile", args[0]) is string aliased)
3942
args = [aliased, .. args[1..]];
4043

@@ -52,6 +55,12 @@
5255
$"""
5356
Usage:
5457
[grey][[dnx]][/] [lime]{ThisAssembly.Project.ToolCommandName}[/] [grey][[OPTIONS]][/] [bold]<repoRef>[/] [grey italic][[<appArgs>...]][/]
58+
[grey][[dnx]][/] [lime]{ThisAssembly.Project.ToolCommandName}[/] [bold]alias[/] [[delete NAME]]
59+
60+
Commands:
61+
[bold]alias[/] List configured ref aliases.
62+
[bold]alias delete[/] NAME Delete a ref alias.
63+
[bold]alias rename[/] OLD NEW Rename a ref alias.
5564
5665
Arguments:
5766
[bold]<REPO_REF>[/] Reference to remote file to run, with format [yellow][[host/]]owner/repo[[@ref]][[:path]][/]
@@ -74,6 +83,8 @@
7483
[bold]--dnx-debug[/] Launch the debugger before running.
7584
[bold]--dnx-force[/] Force download, skipping ETag checking.
7685
""");
86+
if (args.Length == 0)
87+
AliasCommands.WriteTableIfAny(config);
7788
return;
7889
}
7990

src/runfile/Properties/launchSettings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
"help": {
44
"commandName": "Project"
55
},
6+
"alias": {
7+
"commandName": "Project",
8+
"commandLineArgs": "alias"
9+
},
610
"args": {
711
"commandName": "Project",
812
"commandLineArgs": "--aot kzu/runfile@v1 dotnet rocks"

0 commit comments

Comments
 (0)