Skip to content

Commit 64f7fc3

Browse files
authored
Prefix funcs (#134)
Adds what I'm calling prefix funcs. Sometimes it's nice to do something like: list map. lots of commands here.. .. .. end So you know you're getting into a map right way as you read left to right and top to bottom.
1 parent b0e33eb commit 64f7fc3

11 files changed

Lines changed: 274 additions & 113 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Prefix quote syntax (`functionName. ... end`) as an alternative to `(...) functionName`
1213
- `<>` operator for in-place file modification. Reads file to stdin, writes stdout back on success.
1314
Example: `` [sort -u] `file.txt` <> ! ``
1415
- Functions
@@ -50,6 +51,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5051

5152
- Breaking change: `@name` now only reads mshell variables and no longer falls back to environment variables; use `$NAME` for environment access.
5253
- `w`/`we` now accept binary input and write raw bytes to stdout/stderr.
54+
- Renamed `.s` to `stack`, `.def` to `defs`, `.env` to `env`
55+
- Removed `.b` (use `binPaths` instead)
5356

5457
## 0.8.0 - 2025-12-29
5558

doc/base.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
font-weight: bold;
3333
}
3434

35+
.mshellPREFIXQUOTE {
36+
color: #0F4C81;
37+
font-weight: bold;
38+
}
39+
3540
.mshellLOOP, .mshellBREAK, .mshellCONTINUE {
3641
color: #7A2E00;
3742
font-weight: bold;

doc/control-flow.inc.html

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,52 @@ <h1 id="control-flow-continue">continue <a class="section-link" href="#control-f
140140
2
141141
4
142142
5</code></pre>
143+
144+
<h1 id="control-flow-prefix-quote">Prefix Quote Syntax <a class="section-link" href="#control-flow-prefix-quote" aria-label="Permalink">§</a> <a class="back-to-top" href="#control-flow-top">Back to top</a></h1>
145+
146+
<p>
147+
Prefix quotes provide an alternative syntax for applying functions to quotations.
148+
The syntax is <code>functionName. ... end</code>, which is equivalent to <code>(...) functionName</code>.
149+
</p>
150+
151+
<pre>
152+
<code><span class="mshellLINECOMMENT"># Traditional postfix syntax</span>
153+
<span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellINTEGER">1</span> <span class="mshellINTEGER">2</span> <span class="mshellINTEGER">3</span> <span class="mshellINTEGER">4</span> <span class="mshellINTEGER">5</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellLEFT_PAREN">(</span><span class="mshellINTEGER">3</span> <span class="mshellGREATERTHAN">&gt;</span><span class="mshellRIGHT_PAREN">)</span> <span class="mshellLITERAL">filter</span>
154+
155+
<span class="mshellLINECOMMENT"># Prefix quote syntax</span>
156+
<span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellINTEGER">1</span> <span class="mshellINTEGER">2</span> <span class="mshellINTEGER">3</span> <span class="mshellINTEGER">4</span> <span class="mshellINTEGER">5</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellPREFIXQUOTE">filter.</span> <span class="mshellINTEGER">3</span> <span class="mshellGREATERTHAN">&gt;</span> <span class="mshellEND">end</span></code>
157+
</pre>
158+
159+
<p>Prefix quotes work with any function that expects a quotation, including <code>map</code>, <code>filter</code>, <code>each</code>, and user-defined functions:</p>
160+
161+
<pre>
162+
<code><span class="mshellLINECOMMENT"># Double each element</span>
163+
<span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellINTEGER">1</span> <span class="mshellINTEGER">2</span> <span class="mshellINTEGER">3</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellPREFIXQUOTE">map.</span> <span class="mshellINTEGER">2</span> <span class="mshellASTERISK">*</span> <span class="mshellEND">end</span> <span class="mshellLINECOMMENT"># Result: [2 4 6]</span>
164+
165+
<span class="mshellLINECOMMENT"># Print each item</span>
166+
<span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellSTRING">"a"</span> <span class="mshellSTRING">"b"</span> <span class="mshellSTRING">"c"</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellPREFIXQUOTE">each.</span> <span class="mshellLITERAL">wl</span> <span class="mshellEND">end</span></code>
167+
</pre>
168+
169+
<p>Prefix quotes can be chained:</p>
170+
171+
<pre>
172+
<code><span class="mshellLINECOMMENT"># Filter positives, then double them</span>
173+
<span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellMINUS">-</span><span class="mshellINTEGER">1</span> <span class="mshellINTEGER">2</span> <span class="mshellMINUS">-</span><span class="mshellINTEGER">3</span> <span class="mshellINTEGER">4</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellPREFIXQUOTE">filter.</span> <span class="mshellINTEGER">0</span> <span class="mshellGREATERTHAN">&gt;</span> <span class="mshellEND">end</span> <span class="mshellPREFIXQUOTE">map.</span> <span class="mshellINTEGER">2</span> <span class="mshellASTERISK">*</span> <span class="mshellEND">end</span> <span class="mshellLINECOMMENT"># Result: [4 8]</span></code>
174+
</pre>
175+
176+
<p>Prefix quotes can also be nested (one inside another):</p>
177+
178+
<pre>
179+
<code><span class="mshellLINECOMMENT"># For each sublist, filter elements &gt; 5</span>
180+
<span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellINTEGER">1</span> <span class="mshellINTEGER">2</span> <span class="mshellINTEGER">3</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellINTEGER">4</span> <span class="mshellINTEGER">5</span> <span class="mshellINTEGER">6</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellLEFT_SQUARE_BRACKET">[</span><span class="mshellINTEGER">7</span> <span class="mshellINTEGER">8</span> <span class="mshellINTEGER">9</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span><span class="mshellRIGHT_SQUARE_BRACKET">]</span> <span class="mshellPREFIXQUOTE">map.</span> <span class="mshellPREFIXQUOTE">filter.</span> <span class="mshellINTEGER">5</span> <span class="mshellGREATERTHAN">&gt;</span> <span class="mshellEND">end</span> <span class="mshellEND">end</span> <span class="mshellLINECOMMENT"># Result: [[] [6] [7 8 9]]</span></code>
181+
</pre>
182+
183+
<p>
184+
This is also useful to turn boolean operators <code>and</code> and <code>or</code> into
185+
a more traditional lookup infix format:
186+
</p>
187+
188+
<pre>
189+
<code><span class="mshellTRUE">true</span> <span class="mshellPREFIXQUOTE">or.</span> <span class="mshellFALSE">false</span> <span class="mshellEND">end</span> <span class="mshellPREFIXQUOTE">and.</span> <span class="mshellTRUE">true</span> <span class="mshellEND">end</span> <span class="mshellLINECOMMENT"># Like (true | false) &amp; true = true</span>
190+
<span class="mshellEOF"></span></code>
191+
</pre>

doc/functions.inc.html

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,9 @@ <h1 id="functions-built-ins">Built-ins <a class="section-link" href="#functions-
2626
<tr> <th>Name</th> <th>Description</th> <th>Signature</th> </tr>
2727
</thead>
2828
<tbody>
29-
<tr> <td><code>.s</code></td> <td>Print the stack at the current location.</td> <td><code>(--)</code></td> </tr>
30-
<tr> <td><code>.b</code></td> <td>Print paths to all known binaries.</td> <td><code>(--)</code></td> </tr>
31-
<tr> <td><code>.def</code></td> <td>Print available definitions at the current location.</td> <td><code>(--)</code></td> </tr>
32-
<tr> <td><code>.env</code></td> <td>Write all environment variables to stderr in sorted order.</td> <td><code>(--)</code></td> </tr>
29+
<tr> <td><code>stack</code></td> <td>Print the stack at the current location.</td> <td><code>(--)</code></td> </tr>
30+
<tr> <td><code>defs</code></td> <td>Print available definitions at the current location.</td> <td><code>(--)</code></td> </tr>
31+
<tr> <td><code>env</code></td> <td>Write all environment variables to stderr in sorted order.</td> <td><code>(--)</code></td> </tr>
3332
<tr> <td><code>dup</code></td> <td>Duplicate the top stack item.</td> <td><code>(a -- a a)</code></td> </tr>
3433
<tr> <td><code>swap</code></td> <td>Swap the top two stack items.</td> <td><code>(a b -- b a)</code></td> </tr>
3534
<tr> <td><code>drop</code></td> <td>Drop the top stack item.</td> <td><code>(a -- )</code></td> </tr>

mshell/BuiltInList.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ package main
33
// BuiltInList is a lookup set of internal built-in command literals.
44
var BuiltInList = map[string]struct{}{
55
"-rot": {},
6-
".b": {},
7-
".def": {},
8-
".env": {},
9-
".s": {},
106
"/": {},
7+
"defs": {},
8+
"env": {},
9+
"stack": {},
1110
"absPath": {},
1211
"addDays": {},
1312
"append": {},

mshell/Evaluator.go

Lines changed: 106 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,26 @@ MainLoop:
562562
parseQuote := t
563563
q := MShellQuotation{Tokens: parseQuote.Items, StandardInputFile: "", StandardOutputFile: "", StandardErrorFile: "", Variables: context.Variables, MShellParseQuote: parseQuote}
564564
stack.Push(&q)
565+
case *MShellParsePrefixQuote:
566+
prefixQuote := t
567+
// Create a quotation from the items and push onto stack
568+
q := MShellQuotation{Tokens: prefixQuote.Items, StandardInputFile: "", StandardOutputFile: "", StandardErrorFile: "", Variables: context.Variables, MShellParseQuote: nil}
569+
stack.Push(&q)
570+
// Create a token for the function name (strip trailing '.')
571+
lexeme := prefixQuote.StartToken.Lexeme
572+
funcToken := Token{
573+
Line: prefixQuote.StartToken.Line,
574+
Column: prefixQuote.StartToken.Column,
575+
Start: prefixQuote.StartToken.Start,
576+
Lexeme: lexeme[:len(lexeme)-1],
577+
Type: LITERAL,
578+
}
579+
// Recursively evaluate the function call
580+
callStackItem := CallStackItem{MShellParseItem: prefixQuote, Name: funcToken.Lexeme, CallStackType: CALLSTACKDEF}
581+
result := state.Evaluate([]MShellParseItem{funcToken}, stack, context, definitions, callStackItem)
582+
if result.ShouldPassResultUpStack() {
583+
return result
584+
}
565585
case *MShellParseIfBlock:
566586
ifBlock := t
567587
startToken := ifBlock.GetStartToken()
@@ -827,27 +847,19 @@ MainLoop:
827847
}
828848
}
829849

830-
if t.Lexeme == ".s" {
850+
if t.Lexeme == "stack" {
831851
// Print current stack
832852
fmt.Fprint(os.Stderr, stack.String())
833-
} else if t.Lexeme == ".b" {
834-
// Print known binaries
835-
debugList := context.Pbm.DebugList()
836-
for _, item := range debugList.Items {
837-
fmt.Fprintf(os.Stderr, "%s\t%s\n", item.(*MShellList).Items[0].(MShellString).Content, item.(*MShellList).Items[1].(MShellString).Content)
838-
}
839-
840-
// fmt.Fprint(os.Stderr, debugStr)
841853
} else if t.Lexeme == "binPaths" {
842854
stack.Push(context.Pbm.DebugList())
843855

844-
} else if t.Lexeme == ".def" {
856+
} else if t.Lexeme == "defs" {
845857
// Print out available definitions
846858
fmt.Fprint(os.Stderr, "Available definitions:\n")
847859
for _, definition := range definitions {
848860
fmt.Fprintf(os.Stderr, "%s\n", definition.Name)
849861
}
850-
} else if t.Lexeme == ".env" {
862+
} else if t.Lexeme == "env" {
851863
// Print a list of all environment variables, sorted by key
852864
envVars := os.Environ()
853865
slices.Sort(envVars)
@@ -4216,6 +4228,89 @@ MainLoop:
42164228
}
42174229
} else if t.Lexeme == "nullDevice" {
42184230
stack.Push(MShellPath{Path: nullDevice})
4231+
} else if t.Lexeme == "and" || t.Lexeme == "or" {
4232+
// Token Type
4233+
obj1, err := stack.Pop()
4234+
if err != nil {
4235+
return state.FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on an empty stack.\n", t.Line, t.Column, t.Lexeme))
4236+
}
4237+
4238+
obj2, err := stack.Pop()
4239+
if err != nil {
4240+
return state.FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on a stack with only one item.\n", t.Line, t.Column, t.Lexeme))
4241+
}
4242+
4243+
switch obj1.(type) {
4244+
case MShellBool:
4245+
switch obj2.(type) {
4246+
case MShellBool:
4247+
if t.Lexeme == "and" {
4248+
stack.Push(MShellBool{obj2.(MShellBool).Value && obj1.(MShellBool).Value})
4249+
} else {
4250+
stack.Push(MShellBool{obj2.(MShellBool).Value || obj1.(MShellBool).Value})
4251+
}
4252+
default:
4253+
return state.FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '%s' to a %s and %s.\n", t.Line, t.Column, t.Lexeme, obj2.TypeName(), obj1.TypeName()))
4254+
}
4255+
case *MShellQuotation:
4256+
if t.Lexeme == "and" {
4257+
if obj2.(MShellBool).Value {
4258+
result, err := state.EvaluateQuote(*obj1.(*MShellQuotation), stack, context, definitions)
4259+
if err != nil {
4260+
return state.FailWithMessage(err.Error())
4261+
}
4262+
4263+
// Pop the top off the stack
4264+
secondObj, err := stack.Pop()
4265+
if err != nil {
4266+
return state.FailWithMessage(fmt.Sprintf("%d:%d: After executing the quotation in %s, the stack was empty.\n", t.Line, t.Column, t.Lexeme))
4267+
}
4268+
4269+
if result.ShouldPassResultUpStack() {
4270+
return result
4271+
}
4272+
4273+
seconObjBool, ok := secondObj.(MShellBool)
4274+
if !ok {
4275+
return state.FailWithMessage(fmt.Sprintf("%d:%d: Expected a boolean after executing the quotation in %s, received a %s.\n", t.Line, t.Column, t.Lexeme, secondObj.TypeName()))
4276+
}
4277+
4278+
stack.Push(MShellBool{seconObjBool.Value})
4279+
} else {
4280+
stack.Push(MShellBool{false})
4281+
}
4282+
} else {
4283+
if obj2.(MShellBool).Value {
4284+
stack.Push(MShellBool{true})
4285+
} else {
4286+
4287+
result, err := state.EvaluateQuote(*obj1.(*MShellQuotation), stack, context, definitions)
4288+
if err != nil {
4289+
return state.FailWithMessage(err.Error())
4290+
}
4291+
4292+
// Pop the top off the stack
4293+
secondObj, err := stack.Pop()
4294+
if err != nil {
4295+
return state.FailWithMessage(fmt.Sprintf("%d:%d: After executing the quotation in %s, the stack was empty.\n", t.Line, t.Column, t.Lexeme))
4296+
}
4297+
4298+
if result.ShouldPassResultUpStack() {
4299+
return result
4300+
}
4301+
4302+
seconObjBool, ok := secondObj.(MShellBool)
4303+
if !ok {
4304+
return state.FailWithMessage(fmt.Sprintf("%d:%d: Expected a boolean after executing the quotation in %s, received a %s.\n", t.Line, t.Column, t.Lexeme, secondObj.TypeName()))
4305+
}
4306+
4307+
stack.Push(MShellBool{seconObjBool.Value})
4308+
}
4309+
}
4310+
default:
4311+
return state.FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '%s' to a %s and %s.\n", t.Line, t.Column, t.Lexeme, obj2.TypeName(), obj1.TypeName()))
4312+
}
4313+
42194314
} else if t.Lexeme == "return" {
42204315
// Return from the current function
42214316
return EvalResult{
@@ -4225,7 +4320,6 @@ MainLoop:
42254320
ExitCode: 0,
42264321
ExitCalled: false,
42274322
}
4228-
42294323
} else { // last new function
42304324
// If we aren't in a list context, throw an error.
42314325
// Nearly always this is unintended.
@@ -4637,87 +4731,6 @@ MainLoop:
46374731
default:
46384732
return state.FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '-' to a %s and %s.\n", t.Line, t.Column, obj2.TypeName(), obj1.TypeName()))
46394733
}
4640-
} else if t.Type == AND || t.Type == OR { // Token Type
4641-
obj1, err := stack.Pop()
4642-
if err != nil {
4643-
return state.FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on an empty stack.\n", t.Line, t.Column, t.Lexeme))
4644-
}
4645-
4646-
obj2, err := stack.Pop()
4647-
if err != nil {
4648-
return state.FailWithMessage(fmt.Sprintf("%d:%d: Cannot do '%s' operation on a stack with only one item.\n", t.Line, t.Column, t.Lexeme))
4649-
}
4650-
4651-
switch obj1.(type) {
4652-
case MShellBool:
4653-
switch obj2.(type) {
4654-
case MShellBool:
4655-
if t.Type == AND {
4656-
stack.Push(MShellBool{obj2.(MShellBool).Value && obj1.(MShellBool).Value})
4657-
} else {
4658-
stack.Push(MShellBool{obj2.(MShellBool).Value || obj1.(MShellBool).Value})
4659-
}
4660-
default:
4661-
return state.FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '%s' to a %s and %s.\n", t.Line, t.Column, t.Lexeme, obj2.TypeName(), obj1.TypeName()))
4662-
}
4663-
case *MShellQuotation:
4664-
if t.Type == AND {
4665-
if obj2.(MShellBool).Value {
4666-
result, err := state.EvaluateQuote(*obj1.(*MShellQuotation), stack, context, definitions)
4667-
if err != nil {
4668-
return state.FailWithMessage(err.Error())
4669-
}
4670-
4671-
// Pop the top off the stack
4672-
secondObj, err := stack.Pop()
4673-
if err != nil {
4674-
return state.FailWithMessage(fmt.Sprintf("%d:%d: After executing the quotation in %s, the stack was empty.\n", t.Line, t.Column, t.Lexeme))
4675-
}
4676-
4677-
if result.ShouldPassResultUpStack() {
4678-
return result
4679-
}
4680-
4681-
seconObjBool, ok := secondObj.(MShellBool)
4682-
if !ok {
4683-
return state.FailWithMessage(fmt.Sprintf("%d:%d: Expected a boolean after executing the quotation in %s, received a %s.\n", t.Line, t.Column, t.Lexeme, secondObj.TypeName()))
4684-
}
4685-
4686-
stack.Push(MShellBool{seconObjBool.Value})
4687-
} else {
4688-
stack.Push(MShellBool{false})
4689-
}
4690-
} else {
4691-
if obj2.(MShellBool).Value {
4692-
stack.Push(MShellBool{true})
4693-
} else {
4694-
4695-
result, err := state.EvaluateQuote(*obj1.(*MShellQuotation), stack, context, definitions)
4696-
if err != nil {
4697-
return state.FailWithMessage(err.Error())
4698-
}
4699-
4700-
// Pop the top off the stack
4701-
secondObj, err := stack.Pop()
4702-
if err != nil {
4703-
return state.FailWithMessage(fmt.Sprintf("%d:%d: After executing the quotation in %s, the stack was empty.\n", t.Line, t.Column, t.Lexeme))
4704-
}
4705-
4706-
if result.ShouldPassResultUpStack() {
4707-
return result
4708-
}
4709-
4710-
seconObjBool, ok := secondObj.(MShellBool)
4711-
if !ok {
4712-
return state.FailWithMessage(fmt.Sprintf("%d:%d: Expected a boolean after executing the quotation in %s, received a %s.\n", t.Line, t.Column, t.Lexeme, secondObj.TypeName()))
4713-
}
4714-
4715-
stack.Push(MShellBool{seconObjBool.Value})
4716-
}
4717-
}
4718-
default:
4719-
return state.FailWithMessage(fmt.Sprintf("%d:%d: Cannot apply '%s' to a %s and %s.\n", t.Line, t.Column, t.Lexeme, obj2.TypeName(), obj1.TypeName()))
4720-
}
47214734
} else if t.Type == NOT { // Token Type
47224735
obj, err := stack.Pop()
47234736
if err != nil {

0 commit comments

Comments
 (0)