From fbf35747fcd3c471ae853a3db1b37c4991c3fa9a Mon Sep 17 00:00:00 2001 From: Mitchell Paulus Date: Thu, 18 Jun 2026 16:23:33 -0500 Subject: [PATCH] Add UUID built ins --- CHANGELOG.md | 6 +++++ doc/functions.inc.html | 2 ++ doc/mshell.md | 2 ++ mshell/BuiltInList.go | 2 ++ mshell/Evaluator.go | 50 +++++++++++++++++++++++++++++++++++ mshell/TypeBuiltins.go | 2 ++ tests/success/uuid.msh | 2 ++ tests/success/uuid.msh.stdout | 2 ++ 8 files changed, 68 insertions(+) create mode 100644 tests/success/uuid.msh create mode 100644 tests/success/uuid.msh.stdout diff --git a/CHANGELOG.md b/CHANGELOG.md index 459de0c..2c53bc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Functions + - `uuid`: Generate a random (version 4) UUID per RFC 9562 as a canonical + lowercase hyphenated string. `( -- str)` + - `uuid7`: Generate a time-ordered (version 7) UUID per RFC 9562, whose leading + bits encode a Unix millisecond timestamp so values sort chronologically. + `( -- str)` - `modTime`: Return a file's last modification time as a `datetime`, the one file timestamp portable across operating systems and filesystems. Returns a `Maybe` (`None` when the file is missing or cannot be stat'd). `(str|path -- Maybe[datetime])` diff --git a/doc/functions.inc.html b/doc/functions.inc.html index 139474e..24f3982 100644 --- a/doc/functions.inc.html +++ b/doc/functions.inc.html @@ -62,6 +62,8 @@

Built-ins str]] -- ) runtime Get the current OS runtime (GOOS). (-- str) hostname Get the current hostname, or unknown on failure. (-- str) + uuid Generate a random (version 4) UUID as a canonical lowercase hyphenated string. (-- str) + uuid7 Generate a time-ordered (version 7) UUID. The leading bits encode a Unix millisecond timestamp, so values sort chronologically. (-- str) parseCsv Parse CSV input (path or string) into a list of rows. (path|str -- [[str]]) toGrid Build a Grid from a table of string rows. The first row supplies column headers and remaining rows become string-valued data rows. ([[str]] -- Grid) gridValues Extract Grid or GridView cell values as row-major lists, without a header row and without coercing cell types. (Grid|GridView -- [[a]]) diff --git a/doc/mshell.md b/doc/mshell.md index 0b2411e..ef9d5c8 100644 --- a/doc/mshell.md +++ b/doc/mshell.md @@ -973,6 +973,8 @@ end wl # Output: 11 - `tuw`: Shorthand for `(tjoin) map uw` `([[str]] -- )` - `runtime`: Get the current OS runtime. This is the output of the GOOS environment variable. Common possible values are `linux`, `windows`, and `darwin`. `( -- str)` - `hostname`: Get the current OS hostname. On failure to get, puts 'unknown' on the stack. `( -- str)` +- `uuid`: Generate a random (version 4) UUID per RFC 9562, as a canonical lowercase hyphenated string (e.g. `9a9fc320-8284-440d-a740-d038cf95b667`). `( -- str)` +- `uuid7`: Generate a time-ordered (version 7) UUID per RFC 9562. The first 48 bits are a Unix millisecond timestamp, so the values sort chronologically; the rest is random. `( -- str)` - `parseCsv`: Parse a CSV file into a list of lists of strings. Input can be a path/literal file name, or the string contents itself. (`path|str -- [[str]])` - `toGrid`: Build a Grid from a list of string rows. The first row supplies column headers and remaining rows become string-valued data rows. (`[[str]] -- Grid`) - `gridValues`: Extract Grid or GridView cell values as row-major lists, without a header row and without coercing cell types. (`Grid|GridView -- [[a]]`) diff --git a/mshell/BuiltInList.go b/mshell/BuiltInList.go index 7d0eae8..ab67151 100644 --- a/mshell/BuiltInList.go +++ b/mshell/BuiltInList.go @@ -199,6 +199,8 @@ var BuiltInList = map[string]struct{}{ "updateCol": {}, "uniq": {}, "upper": {}, + "uuid": {}, + "uuid7": {}, "urlEncode": {}, "utcToCst": {}, "utf8Bytes": {}, diff --git a/mshell/Evaluator.go b/mshell/Evaluator.go index de73fd4..e106e76 100644 --- a/mshell/Evaluator.go +++ b/mshell/Evaluator.go @@ -5,6 +5,7 @@ import ( "archive/zip" "bufio" "bytes" + crand "crypto/rand" "crypto/sha256" "encoding/csv" "encoding/base64" @@ -4363,6 +4364,43 @@ func ParseJsonObjToMshell(jsonObj any) MShellObject { // "\\\\?\\UNC\\server\\share\\dir" -> "dir" // "\\\\?\\Volume{GUID}\\Windows\\Temp" -> "Windows\\Temp" // "\\\\.\\COM1" -> "" (device path, no remainder) +// formatUuid renders 16 bytes as the canonical lowercase hyphenated UUID string +// (8-4-4-4-12), e.g. "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx". +func formatUuid(b [16]byte) string { + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) +} + +// NewUuidV4 generates a random (version 4) UUID as defined by RFC 9562. +func NewUuidV4() (string, error) { + var b [16]byte + if _, err := crand.Read(b[:]); err != nil { + return "", err + } + b[6] = (b[6] & 0x0f) | 0x40 // Version 4 + b[8] = (b[8] & 0x3f) | 0x80 // Variant 10 + return formatUuid(b), nil +} + +// NewUuidV7 generates a time-ordered (version 7) UUID as defined by RFC 9562. +// The first 48 bits are a Unix timestamp in milliseconds, making the values +// sort chronologically; the remaining bits are random. +func NewUuidV7() (string, error) { + var b [16]byte + if _, err := crand.Read(b[:]); err != nil { + return "", err + } + ms := time.Now().UnixMilli() + b[0] = byte(ms >> 40) + b[1] = byte(ms >> 32) + b[2] = byte(ms >> 24) + b[3] = byte(ms >> 16) + b[4] = byte(ms >> 8) + b[5] = byte(ms) + b[6] = (b[6] & 0x0f) | 0x70 // Version 7 + b[8] = (b[8] & 0x3f) | 0x80 // Variant 10 + return formatUuid(b), nil +} + func StripVolumePrefix(p string) string { if runtime.GOOS != "windows" { return p @@ -10233,6 +10271,18 @@ func (state *EvalState) evaluateToken(t Token, stack *MShellStack, context Execu } else { stack.Push(MShellString{host}) } + } else if t.Lexeme == "uuid" { + s, err := NewUuidV4() + if err != nil { + return state.FailWithMessage(fmt.Sprintf("%d:%d: Failed to generate a UUID: %s\n", t.Line, t.Column, err.Error())) + } + stack.Push(MShellString{s}) + } else if t.Lexeme == "uuid7" { + s, err := NewUuidV7() + if err != nil { + return state.FailWithMessage(fmt.Sprintf("%d:%d: Failed to generate a UUID: %s\n", t.Line, t.Column, err.Error())) + } + stack.Push(MShellString{s}) } else if t.Lexeme == "removeWindowsVolumePrefix" { obj, err := stack.Pop() if err != nil { diff --git a/mshell/TypeBuiltins.go b/mshell/TypeBuiltins.go index c3d3f12..1e6e244 100644 --- a/mshell/TypeBuiltins.go +++ b/mshell/TypeBuiltins.go @@ -506,6 +506,8 @@ func builtinSigsByName(arena *TypeArena, names *NameTable) map[NameId][]QuoteSig r.reg(name, "(str str -- int)") } r.reg("hostname", "( -- str)") + r.reg("uuid", "( -- str)") + r.reg("uuid7", "( -- str)") r.reg("pwd", "( -- path)") r.reg("args", "( -- [str])") r.reg("md5", "(str | path | bytes -- str)") diff --git a/tests/success/uuid.msh b/tests/success/uuid.msh new file mode 100644 index 0000000..0e98ae6 --- /dev/null +++ b/tests/success/uuid.msh @@ -0,0 +1,2 @@ +uuid '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' reMatch ("uuid ok") ("uuid BAD") iff wl +uuid7 '^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' reMatch ("uuid7 ok") ("uuid7 BAD") iff wl diff --git a/tests/success/uuid.msh.stdout b/tests/success/uuid.msh.stdout new file mode 100644 index 0000000..3464c77 --- /dev/null +++ b/tests/success/uuid.msh.stdout @@ -0,0 +1,2 @@ +uuid ok +uuid7 ok