From b65a9425ebeeb9dc56a91736b7e43c5cf3f74d36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:29:25 +0000 Subject: [PATCH 01/12] build(deps): bump golang.org/x/image from 0.35.0 to 0.38.0 Bumps [golang.org/x/image](https://github.com/golang/image) from 0.35.0 to 0.38.0. - [Commits](https://github.com/golang/image/compare/v0.35.0...v0.38.0) --- updated-dependencies: - dependency-name: golang.org/x/image dependency-version: 0.38.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 11 ++++------- go.sum | 24 ++++++++++++------------ 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index 2ae9f64f9..f7a38c58f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/lmorg/murex -go 1.24.0 +go 1.25.0 require ( github.com/Knetic/govaluate v3.0.0+incompatible @@ -17,8 +17,8 @@ require ( github.com/mattn/go-sqlite3 v1.14.33 github.com/pelletier/go-toml v1.9.5 github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee - golang.org/x/sys v0.40.0 - golang.org/x/text v0.33.0 + golang.org/x/sys v0.41.0 + golang.org/x/text v0.35.0 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.44.3 ) @@ -35,10 +35,7 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/stretchr/testify v1.11.1 // indirect golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect - golang.org/x/image v0.35.0 // indirect - golang.org/x/mod v0.32.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/tools v0.41.0 // indirect + golang.org/x/image v0.38.0 // indirect modernc.org/libc v1.67.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index 574e673a4..790e18c66 100644 --- a/go.sum +++ b/go.sum @@ -59,20 +59,20 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I= -golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE= +golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From 52dee73daec83f9d17a200a1a0274cfc132fbc6d Mon Sep 17 00:00:00 2001 From: ugurtafrali Date: Wed, 8 Apr 2026 02:06:50 +0300 Subject: [PATCH 02/12] Fix #987: Unable to successfully execute su root --- lang/define_unmarshal.go | 4 +++- lang/define_unmarshal_test.go | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lang/define_unmarshal.go b/lang/define_unmarshal.go index 89012ce50..5dc0a5663 100644 --- a/lang/define_unmarshal.go +++ b/lang/define_unmarshal.go @@ -47,8 +47,10 @@ func UnmarshalDataBuffered(parent *Process, b []byte, dataType string) (any, err fork := parent.Fork(F_BACKGROUND | F_CREATE_STDIN | F_NO_STDOUT | F_NO_STDERR) defer fork.Kill() - _, err := fork.Stdin.Write(b) + fork.Stdin.Open() defer fork.Stdin.Close() + + _, err := fork.Stdin.Write(b) if err != nil { return nil, fmt.Errorf("cannot write value to unmarshaller's buffer: %s", err.Error()) } diff --git a/lang/define_unmarshal_test.go b/lang/define_unmarshal_test.go index b649f24b0..ec5d9b42c 100644 --- a/lang/define_unmarshal_test.go +++ b/lang/define_unmarshal_test.go @@ -5,11 +5,33 @@ import ( "testing" _ "github.com/lmorg/murex/builtins" + "github.com/lmorg/murex/debug" "github.com/lmorg/murex/lang" "github.com/lmorg/murex/lang/types" "github.com/lmorg/murex/test/count" ) +func TestUnmarshalDataBufferedDebug(t *testing.T) { + count.Tests(t, 1) + + input := `["a","b","c"]` + + lang.InitEnv() + + debug.Enabled = true + defer func() { debug.Enabled = false }() + + v, err := lang.UnmarshalDataBuffered(lang.ShellProcess, []byte(input), types.Json) + if err != nil { + t.Error(err) + return + } + + if fmt.Sprintf("%v", v) != "[a b c]" { + t.Errorf("unexpected output: %v", v) + } +} + func TestUnmarshalArrayJsonString(t *testing.T) { count.Tests(t, 1) From 953ba2e70acbbdd2edacbf6884ba365c3e8ed31b Mon Sep 17 00:00:00 2001 From: ugurtafrali Date: Wed, 8 Apr 2026 02:08:06 +0300 Subject: [PATCH 03/12] Close stdin after write in unmarshal Fixes #987 --- lang/define_unmarshal.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lang/define_unmarshal.go b/lang/define_unmarshal.go index 5dc0a5663..ee8ee252d 100644 --- a/lang/define_unmarshal.go +++ b/lang/define_unmarshal.go @@ -48,12 +48,12 @@ func UnmarshalDataBuffered(parent *Process, b []byte, dataType string) (any, err defer fork.Kill() fork.Stdin.Open() - defer fork.Stdin.Close() - _, err := fork.Stdin.Write(b) + fork.Stdin.Close() if err != nil { return nil, fmt.Errorf("cannot write value to unmarshaller's buffer: %s", err.Error()) } + v, err := UnmarshalData(fork.Process, dataType) if err != nil { return nil, fmt.Errorf("cannot unmarshal buffer: %s", err.Error()) From 3dd904079a04915904b9581c0eb246f0f4652b23 Mon Sep 17 00:00:00 2001 From: lawrence3699 Date: Mon, 13 Apr 2026 18:23:29 +1000 Subject: [PATCH 04/12] Fix panic in comparison operators with non-comparable types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When comparing a non-comparable type (e.g. array) with a number using <, >, <=, or >= in non-strict mode, expGtLt panicked with "interface conversion: interface {} is float64, not string" because compareTypes() can return mismatched Go types — the non-comparable side is JSON-marshaled to a string while the comparable side retains its original type (e.g. float64). Add a safe type assertion in the string comparison branch, converting the right-hand value to a string via types.ConvertGoType when it is not already a string. Fixes #982 --- lang/expressions/exp06.go | 12 ++++++- lang/expressions/exp06_test.go | 59 ++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/lang/expressions/exp06.go b/lang/expressions/exp06.go index 7efe2eb3d..38b96a5c2 100644 --- a/lang/expressions/exp06.go +++ b/lang/expressions/exp06.go @@ -31,7 +31,17 @@ func expGtLt(tree *ParserT, compareFloat ltGtFT, compareString ltGtST) error { value = compareFloat(convertNumber(lv), convertNumber(rv)) case string: - value = compareString(lv.(string), rv.(string)) + rvStr, ok := rv.(string) + if !ok { + v, convErr := types.ConvertGoType(rv, types.String) + if convErr != nil { + return raiseError(tree.expression, tree.currentSymbol(), 0, fmt.Sprintf( + "cannot %s with %s types", tree.currentSymbol().key, left.Primitive, + )) + } + rvStr = v.(string) + } + value = compareString(lv.(string), rvStr) default: return raiseError(tree.expression, tree.currentSymbol(), 0, fmt.Sprintf( diff --git a/lang/expressions/exp06_test.go b/lang/expressions/exp06_test.go index 607b91e96..56bbb3485 100644 --- a/lang/expressions/exp06_test.go +++ b/lang/expressions/exp06_test.go @@ -181,3 +181,62 @@ func TestExpLessThanOrEqual(t *testing.T) { testExpression(t, tests, true) } + +// TestExpCompareNonComparableStrict is a regression test for +// https://github.com/lmorg/murex/issues/982 +// Comparing a non-comparable type (e.g. array) with a number +// should return an error, not panic. +func TestExpCompareNonComparableStrict(t *testing.T) { + tests := []expressionTestT{ + { + Expression: `%[1,2,3] < 2`, + Error: true, + }, + { + Expression: `2 < %[1,2,3]`, + Error: true, + }, + { + Expression: `%[1,2,3] > 2`, + Error: true, + }, + { + Expression: `%[1,2,3] <= 2`, + Error: true, + }, + { + Expression: `%[1,2,3] >= 2`, + Error: true, + }, + } + + testExpression(t, tests, true) +} + +// TestExpCompareNonComparableNonStrict is a regression test for +// https://github.com/lmorg/murex/issues/982 +// In non-strict mode, comparing a non-comparable type with a number +// previously panicked with "interface conversion: interface {} is float64, +// not string". After the fix, this should not panic. +func TestExpCompareNonComparableNonStrict(t *testing.T) { + tests := []expressionTestT{ + { + Expression: `%[1,2,3] < 2`, + Expected: false, + }, + { + Expression: `2 > %[1,2,3]`, + Expected: true, + }, + { + Expression: `%[1,2,3] > 2`, + Expected: true, + }, + { + Expression: `2 < %[1,2,3]`, + Expected: false, + }, + } + + testExpression(t, tests, false) +} From 87a514968291177a67fa8f38b48c7645730bb89a Mon Sep 17 00:00:00 2001 From: Laurence Morgan Date: Tue, 14 Apr 2026 22:49:44 +0100 Subject: [PATCH 05/12] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lang/expressions/exp06_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lang/expressions/exp06_test.go b/lang/expressions/exp06_test.go index 56bbb3485..f38c464e3 100644 --- a/lang/expressions/exp06_test.go +++ b/lang/expressions/exp06_test.go @@ -236,6 +236,22 @@ func TestExpCompareNonComparableNonStrict(t *testing.T) { Expression: `2 < %[1,2,3]`, Expected: false, }, + { + Expression: `%[1,2,3] <= 2`, + Expected: false, + }, + { + Expression: `2 >= %[1,2,3]`, + Expected: true, + }, + { + Expression: `%[1,2,3] >= 2`, + Expected: true, + }, + { + Expression: `2 <= %[1,2,3]`, + Expected: false, + }, } testExpression(t, tests, false) From 456ea8c664815994f8d31c3584ab428273735b71 Mon Sep 17 00:00:00 2001 From: Laurence Morgan Date: Wed, 8 Apr 2026 10:11:49 +0100 Subject: [PATCH 06/12] reintroduce force-tty integrations --- app/app.go | 2 +- config/defaults/profile_preload.mx | 4 ++++ go.mod | 10 +++++----- go.sum | 14 ++++++++++++++ integrations/dnf_linux.mx | 1 + integrations/su_linux.mx | 1 + integrations/sudo_posix.mx | 1 + version.svg | 2 +- 8 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 integrations/dnf_linux.mx create mode 100644 integrations/sudo_posix.mx diff --git a/app/app.go b/app/app.go index 869f09722..ba5fb1aae 100644 --- a/app/app.go +++ b/app/app.go @@ -17,7 +17,7 @@ const Name = "murex" const ( Major = 7 Minor = 2 - Revision = 1001 + Revision = 1003 ) var ( diff --git a/config/defaults/profile_preload.mx b/config/defaults/profile_preload.mx index 6d5cb6357..6f663bf2b 100644 --- a/config/defaults/profile_preload.mx +++ b/config/defaults/profile_preload.mx @@ -28,4 +28,8 @@ function define-force-tty-func (CMD: str) { summary $CMD ${man-summary $CMD} } } + + if { man-summary $CMD } then { + summary $CMD man-summary() + } } \ No newline at end of file diff --git a/go.mod b/go.mod index 2ae9f64f9..b68d9070b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/lmorg/murex -go 1.24.0 +go 1.25.0 require ( github.com/Knetic/govaluate v3.0.0+incompatible @@ -17,7 +17,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.33 github.com/pelletier/go-toml v1.9.5 github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee - golang.org/x/sys v0.40.0 + golang.org/x/sys v0.43.0 golang.org/x/text v0.33.0 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.44.3 @@ -36,9 +36,9 @@ require ( github.com/stretchr/testify v1.11.1 // indirect golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect golang.org/x/image v0.35.0 // indirect - golang.org/x/mod v0.32.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/tools v0.41.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/tools v0.44.0 // indirect modernc.org/libc v1.67.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index 574e673a4..91687ae75 100644 --- a/go.sum +++ b/go.sum @@ -63,16 +63,30 @@ golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I= golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/integrations/dnf_linux.mx b/integrations/dnf_linux.mx new file mode 100644 index 000000000..f34bc0d47 --- /dev/null +++ b/integrations/dnf_linux.mx @@ -0,0 +1 @@ +define-force-tty-func dnf \ No newline at end of file diff --git a/integrations/su_linux.mx b/integrations/su_linux.mx index a4c832f66..7eea09f2c 100644 --- a/integrations/su_linux.mx +++ b/integrations/su_linux.mx @@ -20,3 +20,4 @@ autocomplete set su %[{ IncManPage: true }] +define-force-tty-func su diff --git a/integrations/sudo_posix.mx b/integrations/sudo_posix.mx new file mode 100644 index 000000000..9e7ecc9df --- /dev/null +++ b/integrations/sudo_posix.mx @@ -0,0 +1 @@ +define-force-tty-func sudo \ No newline at end of file diff --git a/version.svg b/version.svg index 5f65855f6..95e5efe22 100644 --- a/version.svg +++ b/version.svg @@ -1 +1 @@ -Version: 7.2.1001Version7.2.1001 +Version: 7.2.1003Version7.2.1003 From 5a64ae3f0f1d0c6605b773ef931d87ffe2d6aee4 Mon Sep 17 00:00:00 2001 From: Laurence Morgan Date: Tue, 14 Apr 2026 23:09:15 +0100 Subject: [PATCH 07/12] fix bug with `define-force-tty-func` summary --- app/app.go | 2 +- config/defaults/profile_preload.mx | 24 +++++++++++++----------- version.svg | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/app.go b/app/app.go index 6dba33958..06f767757 100644 --- a/app/app.go +++ b/app/app.go @@ -17,7 +17,7 @@ const Name = "murex" const ( Major = 7 Minor = 2 - Revision = 1005 + Revision = 1006 ) var ( diff --git a/config/defaults/profile_preload.mx b/config/defaults/profile_preload.mx index 6f663bf2b..e24ca367a 100644 --- a/config/defaults/profile_preload.mx +++ b/config/defaults/profile_preload.mx @@ -14,22 +14,24 @@ if { os darwin && %[ /opt/homebrew/bin/brew ] -> f +x } then { function define-force-tty-func (CMD: str) { # Fixes programs which expect STDERR to be a TTY - if { which $CMD } then { - function $CMD "{ - # Wrapper script around `$CMD` + !if { which $CMD } then { + return + } + + function $CMD "{ + # Wrapper script around `$CMD` - config set proc strict-arrays false - config set proc force-tty true + config set proc strict-arrays false + config set proc force-tty true - exec $CMD \@PARAMS - }" + exec $CMD \@PARAMS + }" - trypipe { - summary $CMD ${man-summary $CMD} - } + trypipe { + summary $CMD ${man-summary $CMD} } if { man-summary $CMD } then { - summary $CMD man-summary() + summary $CMD man-summary($cmd) } } \ No newline at end of file diff --git a/version.svg b/version.svg index 2ba7cfe39..01a2f63a5 100644 --- a/version.svg +++ b/version.svg @@ -1 +1 @@ -Version: 7.2.1005Version7.2.1005 +Version: 7.2.1006Version7.2.1006 From e7e6d64c586eb09c374e656c3ed28668882aff7e Mon Sep 17 00:00:00 2001 From: Laurence Morgan Date: Tue, 14 Apr 2026 23:12:46 +0100 Subject: [PATCH 08/12] revert dumb mistake (duplicated code) --- app/app.go | 2 +- config/defaults/profile_preload.mx | 6 +----- version.svg | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/app.go b/app/app.go index 06f767757..5ada228be 100644 --- a/app/app.go +++ b/app/app.go @@ -17,7 +17,7 @@ const Name = "murex" const ( Major = 7 Minor = 2 - Revision = 1006 + Revision = 1007 ) var ( diff --git a/config/defaults/profile_preload.mx b/config/defaults/profile_preload.mx index e24ca367a..88e3af275 100644 --- a/config/defaults/profile_preload.mx +++ b/config/defaults/profile_preload.mx @@ -28,10 +28,6 @@ function define-force-tty-func (CMD: str) { }" trypipe { - summary $CMD ${man-summary $CMD} - } - - if { man-summary $CMD } then { - summary $CMD man-summary($cmd) + summary $CMD man-summary($CMD) } } \ No newline at end of file diff --git a/version.svg b/version.svg index 01a2f63a5..d86857055 100644 --- a/version.svg +++ b/version.svg @@ -1 +1 @@ -Version: 7.2.1006Version7.2.1006 +Version: 7.2.1007Version7.2.1007 From 9800fa3a618537fb88d5ffbccbbfb612da883cee Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Tue, 12 May 2026 12:18:00 -0700 Subject: [PATCH 09/12] fix: avoid panic comparing arrays with numbers in expressions expGtLt did a direct type assertion rv.(string) when the left operand was a string, which panicked with an interface conversion error when compareTypes returned a numeric right operand (e.g. `@PWD < 2`). Resolves #982 --- lang/expressions/exp06.go | 13 ++++++++++++- lang/expressions/exp06_test.go | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lang/expressions/exp06.go b/lang/expressions/exp06.go index 7efe2eb3d..189a9d421 100644 --- a/lang/expressions/exp06.go +++ b/lang/expressions/exp06.go @@ -31,7 +31,7 @@ func expGtLt(tree *ParserT, compareFloat ltGtFT, compareString ltGtST) error { value = compareFloat(convertNumber(lv), convertNumber(rv)) case string: - value = compareString(lv.(string), rv.(string)) + value = compareString(convertString(lv), convertString(rv)) default: return raiseError(tree.expression, tree.currentSymbol(), 0, fmt.Sprintf( @@ -65,3 +65,14 @@ func convertNumber(v any) float64 { } return f.(float64) } + +func convertString(v any) string { + if s, ok := v.(string); ok { + return s + } + s, err := types.ConvertGoType(v, types.String) + if err != nil { + return fmt.Sprint(v) + } + return s.(string) +} diff --git a/lang/expressions/exp06_test.go b/lang/expressions/exp06_test.go index 607b91e96..b08b26623 100644 --- a/lang/expressions/exp06_test.go +++ b/lang/expressions/exp06_test.go @@ -181,3 +181,26 @@ func TestExpLessThanOrEqual(t *testing.T) { testExpression(t, tests, true) } + +// TestExpGtLtArrayVsNumber is a regression test for +// https://github.com/lmorg/murex/issues/982 where comparing an +// (uncomparable) array against a number panicked with an interface +// conversion error instead of returning a clean result. +func TestExpGtLtArrayVsNumber(t *testing.T) { + tests := []expressionTestT{ + { + Expression: `%[a b c] < 2`, + Expected: false, + }, + { + Expression: `%[a b c] > 2`, + Expected: true, + }, + { + Expression: `%[a b c] >= 2`, + Expected: true, + }, + } + + testExpression(t, tests, false) +} From 23acddc32cbb69750a5dd4f143f9f0482e03d203 Mon Sep 17 00:00:00 2001 From: Laurence Morgan Date: Sun, 17 May 2026 14:43:05 +0100 Subject: [PATCH 10/12] core: support for `!$(vars)` --- app/app.go | 4 +- docs/apis/lang.UnmarshalData.md | 4 +- docs/parser/README.md | 19 +++--- docs/parser/scalar.md | 11 ++++ docs/user-guide/operators-and-tokens.md | 19 +++--- gen/includes/expr-operators-tokens.inc.md | 19 +++--- gen/parser/variables_doc.yaml | 11 ++++ go.mod | 4 +- go.sum | 2 - integrations/aspell_any.mx | 4 +- lang/define_unmarshal_test.go | 5 +- lang/expressions/parse_array.go | 23 +++++++ lang/expressions/parse_array_test.go | 23 +++++++ lang/expressions/parse_expression.go | 13 ++++ lang/expressions/parse_object.go | 31 +++++++++ lang/expressions/parse_object_test.go | 10 +++ lang/expressions/parse_quote%_test.go | 5 ++ lang/expressions/parse_quotes.go | 21 ++++++ lang/expressions/parse_statement.go | 31 +++++++++ lang/expressions/parse_statement_test.go | 14 ++++ lang/expressions/parse_vars.go | 43 ++++++++++++ lang/expressions/parse_vars_exec_test.go | 79 +++++++++++++++++++++++ lang/expressions/parse_vars_test.go | 5 ++ lang/process.go | 11 ++++ version.svg | 2 +- 25 files changed, 375 insertions(+), 38 deletions(-) diff --git a/app/app.go b/app/app.go index 5ada228be..8cf373bf5 100644 --- a/app/app.go +++ b/app/app.go @@ -16,8 +16,8 @@ const Name = "murex" // Format of version string should be "$(Major).$(Minor).$(Revision) ($Branch)" const ( Major = 7 - Minor = 2 - Revision = 1007 + Minor = 3 + Revision = 1008 ) var ( diff --git a/docs/apis/lang.UnmarshalData.md b/docs/apis/lang.UnmarshalData.md index 7a959ee0e..15b2d6dfc 100644 --- a/docs/apis/lang.UnmarshalData.md +++ b/docs/apis/lang.UnmarshalData.md @@ -85,11 +85,13 @@ func UnmarshalDataBuffered(parent *Process, b []byte, dataType string) (any, err fork := parent.Fork(F_BACKGROUND | F_CREATE_STDIN | F_NO_STDOUT | F_NO_STDERR) defer fork.Kill() + fork.Stdin.Open() _, err := fork.Stdin.Write(b) - defer fork.Stdin.Close() + fork.Stdin.Close() if err != nil { return nil, fmt.Errorf("cannot write value to unmarshaller's buffer: %s", err.Error()) } + v, err := UnmarshalData(fork.Process, dataType) if err != nil { return nil, fmt.Errorf("cannot unmarshal buffer: %s", err.Error()) diff --git a/docs/parser/README.md b/docs/parser/README.md index 8858b562a..3a7201f09 100644 --- a/docs/parser/README.md +++ b/docs/parser/README.md @@ -56,15 +56,16 @@ parsed by the following rules of operation Order of operations: 1. expression or statement discovery 2. sub-shells / sub-expressions -3. multiplication / division (expressions only) -4. addition / subtraction (expressions only) -5. immutable merge -6. comparisons, eg greater than (expressions only) -7. logical and (sub-expressions only) -8. logical or (sub-expressions only) -9. elvis (expressions only) -10. assign (expressions only) -11. _left_ to _right_ +3. variable stage (expressions only) +4. multiplication / division (expressions only) +5. addition / subtraction (expressions only) +6. immutable merge +7. comparisons, eg greater than (expressions only) +8. logical and (sub-expressions only) +9. logical or (sub-expressions only) +10. elvis (expressions only) +11. assign (expressions only) +12. _left_ to _right_ ### Expression Or Statement Discovery diff --git a/docs/parser/scalar.md b/docs/parser/scalar.md index f0d9e5f35..b2aefc721 100644 --- a/docs/parser/scalar.md +++ b/docs/parser/scalar.md @@ -39,6 +39,17 @@ $(scalar) _Enclosed_ syntax supports any unicode characters however the variable name needs to be surrounded by parenthesis. See examples below. +### Logical-Not Syntax (expressions) + +You can negate a variable's logical value in expressions with: + +``` +!$(variable) +``` + +This syntax resolves the variable's truthy / falsy value and then returns +the inverse (`true` becomes `false`, and `false` becomes `true`). + ## Examples diff --git a/docs/user-guide/operators-and-tokens.md b/docs/user-guide/operators-and-tokens.md index 975f1353b..46c5fe576 100644 --- a/docs/user-guide/operators-and-tokens.md +++ b/docs/user-guide/operators-and-tokens.md @@ -58,15 +58,16 @@ parsed by the following rules of operation Order of operations: 1. expression or statement discovery 2. sub-shells / sub-expressions -3. multiplication / division (expressions only) -4. addition / subtraction (expressions only) -5. immutable merge -6. comparisons, eg greater than (expressions only) -7. logical and (sub-expressions only) -8. logical or (sub-expressions only) -9. elvis (expressions only) -10. assign (expressions only) -11. _left_ to _right_ +3. variable stage (expressions only) +4. multiplication / division (expressions only) +5. addition / subtraction (expressions only) +6. immutable merge +7. comparisons, eg greater than (expressions only) +8. logical and (sub-expressions only) +9. logical or (sub-expressions only) +10. elvis (expressions only) +11. assign (expressions only) +12. _left_ to _right_ ### Expression Or Statement Discovery diff --git a/gen/includes/expr-operators-tokens.inc.md b/gen/includes/expr-operators-tokens.inc.md index c8c95271a..5fd1581d3 100644 --- a/gen/includes/expr-operators-tokens.inc.md +++ b/gen/includes/expr-operators-tokens.inc.md @@ -54,15 +54,16 @@ parsed by the following rules of operation Order of operations: 1. expression or statement discovery 2. sub-shells / sub-expressions -3. multiplication / division (expressions only) -4. addition / subtraction (expressions only) -5. immutable merge -6. comparisons, eg greater than (expressions only) -7. logical and (sub-expressions only) -8. logical or (sub-expressions only) -9. elvis (expressions only) -10. assign (expressions only) -11. _left_ to _right_ +3. variable stage (expressions only) +4. multiplication / division (expressions only) +5. addition / subtraction (expressions only) +6. immutable merge +7. comparisons, eg greater than (expressions only) +8. logical and (sub-expressions only) +9. logical or (sub-expressions only) +10. elvis (expressions only) +11. assign (expressions only) +12. _left_ to _right_ ### Expression Or Statement Discovery diff --git a/gen/parser/variables_doc.yaml b/gen/parser/variables_doc.yaml index 01baad60d..9328f59cd 100644 --- a/gen/parser/variables_doc.yaml +++ b/gen/parser/variables_doc.yaml @@ -40,6 +40,17 @@ _Enclosed_ syntax supports any unicode characters however the variable name needs to be surrounded by parenthesis. See examples below. + + ### Logical-Not Syntax (expressions) + + You can negate a variable's logical value in expressions with: + + ``` + !$(variable) + ``` + + This syntax resolves the variable's truthy / falsy value and then returns + the inverse (`true` becomes `false`, and `false` becomes `true`). Examples: |- {{ include "gen/includes/parser-var-tokens.inc.md" }} diff --git a/go.mod b/go.mod index d2d5ae17f..1d73fe895 100644 --- a/go.mod +++ b/go.mod @@ -35,10 +35,10 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/stretchr/testify v1.11.1 // indirect golang.org/x/image v0.39.0 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/sync v0.20.0 // indirect golang.org/x/tools v0.44.0 // indirect modernc.org/libc v1.72.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) + +replace github.com/lmorg/readline/v4 => ./local/readline diff --git a/go.sum b/go.sum index 838b93ea7..ac2925d18 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,6 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/lmorg/apachelogs v0.0.0-20161115121556-e5f3eae677ad h1:TQcz4T52CwRmLT4KexBSPAL2XxAWKlsbpPyQ1hg9T50= github.com/lmorg/apachelogs v0.0.0-20161115121556-e5f3eae677ad/go.mod h1:ZCbRp0gZDkH+2t/flBuXpT4+FyrVodEONI0l5jUwYH0= -github.com/lmorg/readline/v4 v4.2.2 h1:aR1buzcMO525Omo7fS3H4ecUuONPv3nIENKHcTgrESg= -github.com/lmorg/readline/v4 v4.2.2/go.mod h1:Zdp/tnRZl0eTssyikMGSp61wOZwxJLw/CtK2sE2f9e8= github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= diff --git a/integrations/aspell_any.mx b/integrations/aspell_any.mx index 55ccb9079..3514cc860 100644 --- a/integrations/aspell_any.mx +++ b/integrations/aspell_any.mx @@ -1,3 +1,3 @@ -if { which aspell } then { +if { which aspell && !$(MXTTY) } then { config set shell spellcheck-enabled true -} \ No newline at end of file +} diff --git a/lang/define_unmarshal_test.go b/lang/define_unmarshal_test.go index ec5d9b42c..81ea6fd55 100644 --- a/lang/define_unmarshal_test.go +++ b/lang/define_unmarshal_test.go @@ -19,7 +19,10 @@ func TestUnmarshalDataBufferedDebug(t *testing.T) { lang.InitEnv() debug.Enabled = true - defer func() { debug.Enabled = false }() + t.Cleanup(func() { + lang.WaitForProcessCleanup() + debug.Enabled = false + }) v, err := lang.UnmarshalDataBuffered(lang.ShellProcess, []byte(input), types.Json) if err != nil { diff --git a/lang/expressions/parse_array.go b/lang/expressions/parse_array.go index 0d8deaf57..3a283b16f 100644 --- a/lang/expressions/parse_array.go +++ b/lang/expressions/parse_array.go @@ -146,6 +146,29 @@ func (tree *ParserT) parseArray(exec bool) ([]rune, *primitives.DataType, error) slice = append(slice, v) } + case '!': + if tree.nextChar() == '$' { + r, _, _, fn, err := tree.parseVarLogicalNotExpr(exec) + if err != nil { + return nil, nil, err + } + + if exec { + v, err := fn() + if err != nil { + return nil, nil, err + } + slice = append(slice, v.Value) + } else { + slice = append(slice, string(r)) + } + + continue + } + + value := tree.parseArrayBareword() + slice = append(slice, formatArrayValue(value)) + case '~': // tilde home, err := tree.parseVarTilde(exec) diff --git a/lang/expressions/parse_array_test.go b/lang/expressions/parse_array_test.go index cbf510605..5341faa45 100644 --- a/lang/expressions/parse_array_test.go +++ b/lang/expressions/parse_array_test.go @@ -90,6 +90,11 @@ func TestParseArray(t *testing.T) { expected: `[1,2,null]`, pos: 20, }, + { + input: "%[!$(TestParseArray)]", + expected: `[true]`, + pos: 19, + }, { input: "%[@TestParseArray]", expected: `[]`, @@ -261,3 +266,21 @@ func TestParseArrayNestedExpr(t *testing.T) { testParserObject(t, tests) } + +func TestParseArrayNot(t *testing.T) { + tests := []test.MurexTest{ + { + Block: `TestParseArrayNotTrue = true; %[!$(TestParseArrayNotTrue), !$(TestParseArrayNotTrue)]`, + Stdout: `[false,false]`, + }, + { + Block: `TestParseArrayNotFalse = false; %[!$(TestParseArrayNotFalse), !$(TestParseArrayNotFalse)]`, + Stdout: `[true,true]`, + }, + { + Block: `TestParseArrayNotTrue = true; TestParseArrayNotFalse = false; %[!$(TestParseArrayNotTrue), !$(TestParseArrayNotFalse)]`, + Stdout: `[false,true]`, + }, + } + test.RunMurexTests(tests, t) +} diff --git a/lang/expressions/parse_expression.go b/lang/expressions/parse_expression.go index 2641a9242..e1a3bfcc0 100644 --- a/lang/expressions/parse_expression.go +++ b/lang/expressions/parse_expression.go @@ -123,6 +123,19 @@ func (tree *ParserT) parseExpression(exec, incLogicalOps bool) error { // not like tree.appendAst(symbols.NotLike) tree.charPos++ + case '$': + runes, v, mxDt, fn, err := tree.parseVarLogicalNotExpr(exec) + if err != nil { + return err + } + + if exec { + dt := primitives.NewFunction(fn) + tree.appendAstWithPrimitive(symbols.Scalar, dt, runes...) + } else { + dt := primitives.NewScalar(mxDt, v) + tree.appendAstWithPrimitive(symbols.Scalar, dt, runes...) + } default: // might be a function if !isBareChar(tree.nextChar()) { diff --git a/lang/expressions/parse_object.go b/lang/expressions/parse_object.go index 71eed8387..58f49f899 100644 --- a/lang/expressions/parse_object.go +++ b/lang/expressions/parse_object.go @@ -171,6 +171,37 @@ func (tree *ParserT) parseObject(exec bool) ([]rune, *primitives.DataType, error } } + case '!': + if tree.nextChar() == '$' { + r, _, _, fn, err := tree.parseVarLogicalNotExpr(exec) + if err != nil { + return nil, nil, err + } + + if exec { + val, err := fn() + if err != nil { + return nil, nil, err + } + err = o.UpdateInterface(val.Value) + if err != nil { + return nil, nil, err + } + } else { + err = o.UpdateInterface(string(r)) + if err != nil { + return nil, nil, err + } + } + + continue + } + + err := o.AppendRune(r) + if err != nil { + return nil, nil, err + } + case '~': // tilde home, err := tree.parseVarTilde(exec) diff --git a/lang/expressions/parse_object_test.go b/lang/expressions/parse_object_test.go index b2f1a4b50..013d508e0 100644 --- a/lang/expressions/parse_object_test.go +++ b/lang/expressions/parse_object_test.go @@ -77,6 +77,16 @@ func TestParseObject(t *testing.T) { expected: `{"a":null,"b":[]}`, pos: 13, }, + { + input: `%{a:!$(a)}`, + expected: `{"a":true}`, + pos: 8, + }, + { + input: `%{!$(a):b}`, + expected: `{"true":"b"}`, + pos: 8, + }, { input: `%{nan:-}`, expected: `{"NaN":"-"}`, diff --git a/lang/expressions/parse_quote%_test.go b/lang/expressions/parse_quote%_test.go index caddd48cd..4c3dec202 100644 --- a/lang/expressions/parse_quote%_test.go +++ b/lang/expressions/parse_quote%_test.go @@ -89,6 +89,11 @@ func TestParseQuoteParen(t *testing.T) { expected: `foo--bar`, pos: 3, }, + { + input: `%(foo-!$(b)-bar)`, + expected: `foo-true-bar`, + pos: 2, + }, { input: `%(foo-\$bar-bar)`, expected: `foo-\-bar`, diff --git a/lang/expressions/parse_quotes.go b/lang/expressions/parse_quotes.go index d07c811b5..33fb16276 100644 --- a/lang/expressions/parse_quotes.go +++ b/lang/expressions/parse_quotes.go @@ -5,6 +5,7 @@ import ( "github.com/lmorg/murex/lang" "github.com/lmorg/murex/lang/expressions/symbols" + "github.com/lmorg/murex/lang/types" "github.com/lmorg/murex/utils/ansi" ) @@ -148,6 +149,26 @@ func (tree *ParserT) parseStringInfix(qEnd rune, exec bool) ([]rune, error) { } } + case r == '!' && tree.nextChar() == '$': + r, _, _, fn, err := tree.parseVarLogicalNotExpr(exec) + if err != nil { + return nil, err + } + + if exec { + val, err := fn() + if err != nil { + return nil, err + } + s, err := types.ConvertGoType(val.Value, types.String) + if err != nil { + return nil, err + } + value = append(value, []rune(s.(string))...) + } else { + value = append(value, r...) + } + case r == '~': // tilde home, err := tree.parseVarTilde(exec) diff --git a/lang/expressions/parse_statement.go b/lang/expressions/parse_statement.go index 7beabf745..eb2b6005e 100644 --- a/lang/expressions/parse_statement.go +++ b/lang/expressions/parse_statement.go @@ -444,6 +444,37 @@ func (tree *ParserT) parseStatement(exec bool) error { } } + case '!': + tree.statement.validFunction = false + switch { + case tree.nextChar() == '$': + var tokenise bool + tokenise = tree.tokeniseScalar() + execScalar := exec && tokenise + value, _, _, fn, err := tree.parseVarLogicalNotExpr(execScalar) + if err != nil { + return raiseError(tree.expression, nil, tree.charPos, err.Error()) + } + + if execScalar { + val, err := fn() + if err != nil { + return err + } + s, err := types.ConvertGoType(val.Value, types.String) + if err != nil { + return err + } + appendToParam(tree, []rune(s.(string))...) + tree.statement.canHaveZeroLenStr = true + } else { + appendToParam(tree, value...) + } + + default: + appendToParam(tree, r) + } + case '@': tree.statement.validFunction = false prev := tree.prevChar() diff --git a/lang/expressions/parse_statement_test.go b/lang/expressions/parse_statement_test.go index fed2b5c47..4c9a73291 100644 --- a/lang/expressions/parse_statement_test.go +++ b/lang/expressions/parse_statement_test.go @@ -252,6 +252,13 @@ func TestParseStatement(t *testing.T) { }, Exec: false, }, + { + Statement: `echo !$(bob)`, + Args: []string{ + "echo", "!$(bob)", + }, + Exec: false, + }, { Statement: `echo $bob`, Args: []string{ @@ -259,6 +266,13 @@ func TestParseStatement(t *testing.T) { }, Exec: true, }, + { + Statement: `echo !$(bob)`, + Args: []string{ + "echo", "true", + }, + Exec: true, + }, { Statement: `echo "$bob"`, Args: []string{ diff --git a/lang/expressions/parse_vars.go b/lang/expressions/parse_vars.go index a70b3cf9a..67a0fca96 100644 --- a/lang/expressions/parse_vars.go +++ b/lang/expressions/parse_vars.go @@ -35,6 +35,49 @@ func (tree *ParserT) parseVarScalarExpr(exec, execScalars bool) ([]rune, any, st return runes, v, mxDt, nil, err } +func (tree *ParserT) parseVarLogicalNotExpr(exec bool) ([]rune, any, string, primitives.FunctionT, error) { + startPos := tree.charPos + + if tree.nextChar() != '$' || tree.charPos+2 >= len(tree.expression) || tree.expression[tree.charPos+2] != '(' { + return nil, nil, "", nil, raiseError( + tree.expression, nil, tree.charPos, "expecting variable token '$(' after logical-not token '!'") + } + + tree.charPos += 2 + start := tree.charPos + + for tree.charPos++; tree.charPos < len(tree.expression); tree.charPos++ { + r := tree.expression[tree.charPos] + if r == ')' { + path := tree.expression[start+1 : tree.charPos] + runes := tree.expression[startPos : tree.charPos+1] + + if !exec { + return runes, nil, types.Boolean, nil, nil + } + + fn := func() (*primitives.Value, error) { + value, _, err := tree.getVar(path, varAsValue) + if err != nil { + return nil, err + } + + s, err := types.ConvertGoType(value, types.String) + if err != nil { + return nil, err + } + + return &primitives.Value{Value: !types.IsTrueString(s.(string), 0), DataType: types.Boolean}, nil + } + + return runes, nil, types.Boolean, fn, nil + } + } + + return nil, nil, "", nil, raiseError( + tree.expression, nil, tree.charPos, "expecting closing parenthesis, ')', after logical-not variable reference") +} + func (tree *ParserT) parseVarScalar(exec, execScalars bool, strOrVal varFormatting) ([]rune, any, string, error) { if tree.nextChar() == '(' { tree.charPos++ diff --git a/lang/expressions/parse_vars_exec_test.go b/lang/expressions/parse_vars_exec_test.go index 8da0cf9c1..fb3ed339d 100644 --- a/lang/expressions/parse_vars_exec_test.go +++ b/lang/expressions/parse_vars_exec_test.go @@ -60,6 +60,85 @@ func TestParseVarsScalar(t *testing.T) { test.RunMurexTests(tests, t) } +func TestParseVarsLogicalNot(t *testing.T) { + tests := []test.MurexTest{ + { + Block: `TestParseVarsLogicalNot0=true;!$(TestParseVarsLogicalNot0)`, + Stdout: `false`, + ExitNum: 1, + }, + { + Block: `TestParseVarsLogicalNot1=false;!$(TestParseVarsLogicalNot1)`, + Stdout: `true`, + }, + { + Block: `TestParseVarsLogicalNot2=1;!$(TestParseVarsLogicalNot2)`, + Stdout: `false`, + ExitNum: 1, + }, + { + Block: `TestParseVarsLogicalNot3=0;!$(TestParseVarsLogicalNot3)`, + Stdout: `true`, + }, + } + + test.RunMurexTests(tests, t) +} + +func TestParseVarsLogicalNotStatement(t *testing.T) { + tests := []test.MurexTest{ + { + Block: `TestParseVarsLogicalNotStatement0=true;out !$(TestParseVarsLogicalNotStatement0)`, + Stdout: "false\n", + }, + { + Block: `TestParseVarsLogicalNotStatement1=false;out !$(TestParseVarsLogicalNotStatement1)`, + Stdout: "true\n", + }, + { + Block: `TestParseVarsLogicalNotStatement2=1;out !$(TestParseVarsLogicalNotStatement2)`, + Stdout: "false\n", + }, + { + Block: `TestParseVarsLogicalNotStatement3=0;out !$(TestParseVarsLogicalNotStatement3)`, + Stdout: "true\n", + }, + } + + test.RunMurexTests(tests, t) +} + +func TestParseVarsLogicalNotBuilders(t *testing.T) { + tests := []test.MurexTest{ + { + Block: `TestParseVarsLogicalNotBuilders0=true;%(!$(TestParseVarsLogicalNotBuilders0))`, + Stdout: `false`, + }, + { + Block: `TestParseVarsLogicalNotBuilders1=false;%(!$(TestParseVarsLogicalNotBuilders1))`, + Stdout: `true`, + }, + { + Block: `TestParseVarsLogicalNotBuilders2=true;%[!$(TestParseVarsLogicalNotBuilders2)]`, + Stdout: `[false]`, + }, + { + Block: `TestParseVarsLogicalNotBuilders3=false;%[!$(TestParseVarsLogicalNotBuilders3)]`, + Stdout: `[true]`, + }, + { + Block: `TestParseVarsLogicalNotBuilders4=true;%{a:!$(TestParseVarsLogicalNotBuilders4)}`, + Stdout: `{"a":false}`, + }, + { + Block: `TestParseVarsLogicalNotBuilders5=false;%{a:!$(TestParseVarsLogicalNotBuilders5)}`, + Stdout: `{"a":true}`, + }, + } + + test.RunMurexTests(tests, t) +} + func TestParseVarsArray(t *testing.T) { tests := []test.MurexTest{ { diff --git a/lang/expressions/parse_vars_test.go b/lang/expressions/parse_vars_test.go index 48ba918ab..cb0289498 100644 --- a/lang/expressions/parse_vars_test.go +++ b/lang/expressions/parse_vars_test.go @@ -20,6 +20,11 @@ func TestParseVarsScalarSymbol(t *testing.T) { expected: `$foo[[/1]]`, pos: -1, }, + { + input: `!$(foo)`, + expected: `!$(foo)`, + pos: -1, + }, }, } diff --git a/lang/process.go b/lang/process.go index 5c47ccf1d..7166abd57 100644 --- a/lang/process.go +++ b/lang/process.go @@ -6,6 +6,7 @@ import ( "io" "os" "strings" + "sync" "time" "github.com/lmorg/murex/app" @@ -67,8 +68,16 @@ var ( // ShellExitNum is for when running murex in interactive shell mode ShellExitNum int + + // processCleanupWG tracks asynchronous process cleanup work. + processCleanupWG sync.WaitGroup ) +// WaitForProcessCleanup waits for asynchronous process cleanup goroutines to finish. +func WaitForProcessCleanup() { + processCleanupWG.Wait() +} + func DefineFunction(name string, fn func(*Process) error, StdoutDataType string) { GoFunctions[name] = fn MethodStdout.Define(name, StdoutDataType) @@ -453,7 +462,9 @@ func deregisterProcess(p *Process) { ForegroundProc.Set(p.Next) } + processCleanupWG.Add(1) go func() { + defer processCleanupWG.Done() p.State.Set(state.AwaitingGC) GlobalFIDs.Deregister(p.Id) if p.HasJobId.Get() { diff --git a/version.svg b/version.svg index d86857055..ee7244217 100644 --- a/version.svg +++ b/version.svg @@ -1 +1 @@ -Version: 7.2.1007Version7.2.1007 +Version: 7.3.1008Version7.3.1008 From b23edb729628094b8eb398674c75a16f167b3a61 Mon Sep 17 00:00:00 2001 From: tiymat <138939221+tiymat@users.noreply.github.com> Date: Fri, 12 Jun 2026 23:07:07 -0230 Subject: [PATCH 11/12] Update 'which' not to search CWD when cmd doesn't contain a separator --- utils/which/which.go | 38 +++++++++++++++++++++++--------------- utils/which/which_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/utils/which/which.go b/utils/which/which.go index caaefb617..0ba0d7040 100644 --- a/utils/which/which.go +++ b/utils/which/which.go @@ -2,29 +2,37 @@ package which import ( "os" + "path/filepath" + "strings" "github.com/lmorg/murex/utils/consts" ) // Which works similarly to the UNIX command with the same name. -// If the executable is now found in $PATH then a zero length string is returned. +// If the executable is not found in $PATH then a zero length string is returned. func Which(cmd string) string { - _, err := os.Stat(cmd) - if !os.IsNotExist(err) { - return cmd - } - - envPath := os.Getenv("PATH") - - for _, path := range SplitPath(envPath) { - filepath := path + consts.PathSlash + cmd - _, err := os.Stat(filepath) - if !os.IsNotExist(err) { - return filepath + if strings.ContainsRune(cmd, '/') || strings.ContainsRune(cmd, filepath.Separator) { + // cmd is explicitly a path, resolve and return the absolute path if possible + fi, err := os.Stat(cmd) + if err == nil && fi.Mode().IsRegular() { + if abs, err := filepath.Abs(cmd); err == nil { + return abs + } + return cmd + } + return "" + } else { + // cmd is not explicitly a path, just search $PATH, don't attempt to reoslve absolute path + for _, path := range SplitPath(os.Getenv("PATH")) { + fullPath := path + consts.PathSlash + cmd + fi, err := os.Stat(fullPath) + if err == nil && fi.Mode().IsRegular() { + return fullPath + } } - } - return "" + return "" + } } // WhichIgnoreFail will always return a best guess of the executable diff --git a/utils/which/which_test.go b/utils/which/which_test.go index 793950e81..2c552f463 100644 --- a/utils/which/which_test.go +++ b/utils/which/which_test.go @@ -23,3 +23,29 @@ func TestWhich(t *testing.T) { t.Log("$PATH: " + os.Getenv("PATH")) } } + +func TestWhichDirInCwd(t *testing.T) { + count.Tests(t, 1) + + tmp := t.TempDir() + // make a dir with the same name as a real executable + if err := os.Mkdir(tmp+"/go", 0755); err != nil { + t.Fatal(err) + } + + old, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer os.Chdir(old) + + if err := os.Chdir(tmp); err != nil { + t.Fatal(err) + } + + result := Which("go") + + if result == "go" || result == "" { + t.Errorf("Which(\"go\") returned %q; expected the resultolved $PATH path", result) + } +} From faae1937815b3ae22d4b73e8266a8fe4f3b6af01 Mon Sep 17 00:00:00 2001 From: tiymat <138939221+tiymat@users.noreply.github.com> Date: Mon, 29 Jun 2026 02:19:03 -0230 Subject: [PATCH 12/12] chore: use exec.LookPath in "which" builtin --- utils/which/which.go | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/utils/which/which.go b/utils/which/which.go index 0ba0d7040..4af354bcc 100644 --- a/utils/which/which.go +++ b/utils/which/which.go @@ -2,10 +2,9 @@ package which import ( "os" + "os/exec" "path/filepath" "strings" - - "github.com/lmorg/murex/utils/consts" ) // Which works similarly to the UNIX command with the same name. @@ -21,18 +20,14 @@ func Which(cmd string) string { return cmd } return "" - } else { - // cmd is not explicitly a path, just search $PATH, don't attempt to reoslve absolute path - for _, path := range SplitPath(os.Getenv("PATH")) { - fullPath := path + consts.PathSlash + cmd - fi, err := os.Stat(fullPath) - if err == nil && fi.Mode().IsRegular() { - return fullPath - } - } + } + // cmd is not explicitly a path, just search $PATH + path, err := exec.LookPath(cmd) + if err != nil { return "" } + return path } // WhichIgnoreFail will always return a best guess of the executable