Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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])`
Expand Down
2 changes: 2 additions & 0 deletions doc/functions.inc.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ <h1 id="functions-built-ins">Built-ins <a class="section-link" href="#functions-
<tr> <td><code>tuw</code></td> <td>Shorthand for <code>(tjoin) map uw</code>.</td> <td><code>([[<span class="sig-type sig-type-str">str</span>]] -- )</code></td> </tr>
<tr> <td><code>runtime</code></td> <td>Get the current OS runtime (<code>GOOS</code>).</td> <td><code>(-- <span class="sig-type sig-type-str">str</span>)</code></td> </tr>
<tr> <td><code>hostname</code></td> <td>Get the current hostname, or <code>unknown</code> on failure.</td> <td><code>(-- <span class="sig-type sig-type-str">str</span>)</code></td> </tr>
<tr> <td><code>uuid</code></td> <td>Generate a random (version 4) UUID as a canonical lowercase hyphenated string.</td> <td><code>(-- <span class="sig-type sig-type-str">str</span>)</code></td> </tr>
<tr> <td><code>uuid7</code></td> <td>Generate a time-ordered (version 7) UUID. The leading bits encode a Unix millisecond timestamp, so values sort chronologically.</td> <td><code>(-- <span class="sig-type sig-type-str">str</span>)</code></td> </tr>
<tr> <td><code>parseCsv</code></td> <td>Parse CSV input (path or string) into a list of rows.</td> <td><code>(<span class="sig-type sig-type-path">path</span>|<span class="sig-type sig-type-str">str</span> -- [[<span class="sig-type sig-type-str">str</span>]])</code></td> </tr>
<tr> <td><code>toGrid</code></td> <td>Build a Grid from a table of string rows. The first row supplies column headers and remaining rows become string-valued data rows.</td> <td><code>([[<span class="sig-type sig-type-str">str</span>]] -- Grid)</code></td> </tr>
<tr> <td><code>gridValues</code></td> <td>Extract Grid or GridView cell values as row-major lists, without a header row and without coercing cell types.</td> <td><code>(Grid|GridView -- [[a]])</code></td> </tr>
Expand Down
2 changes: 2 additions & 0 deletions doc/mshell.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]]`)
Expand Down
2 changes: 2 additions & 0 deletions mshell/BuiltInList.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ var BuiltInList = map[string]struct{}{
"updateCol": {},
"uniq": {},
"upper": {},
"uuid": {},
"uuid7": {},
"urlEncode": {},
"utcToCst": {},
"utf8Bytes": {},
Expand Down
50 changes: 50 additions & 0 deletions mshell/Evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"archive/zip"
"bufio"
"bytes"
crand "crypto/rand"
"crypto/sha256"
"encoding/csv"
"encoding/base64"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions mshell/TypeBuiltins.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand Down
2 changes: 2 additions & 0 deletions tests/success/uuid.msh
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions tests/success/uuid.msh.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
uuid ok
uuid7 ok