From 1da67b1a4af20e7d03b427bb24f5cd45b893caa6 Mon Sep 17 00:00:00 2001 From: monstermichl Date: Sat, 21 Jun 2025 00:14:58 +0200 Subject: [PATCH 01/10] Make README.md examples more Batch friendly --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 77b10e8..df44934 100644 --- a/README.md +++ b/README.md @@ -157,17 +157,17 @@ for i := 0; i < len(s); i++ { ### Programs ```golang // Programs are called by stating the program name preceded by an @. -@ls("-a") +@dir("/b") ``` ```golang // Similar to Bash/Batch, the output can be piped into another program. -@ls("-a") | @sort() +@dir("/b") | @sort("/r") ``` ```golang // To capture the output, just assign the call chain to variables. -stdout, stderr, code := @ls("-a") | @sort() +stdout, stderr, code := @dir("/b") | @sort("/r") ``` ### Imports From b4cd64a14325134fcf069b57c0f827769496dfb8 Mon Sep 17 00:00:00 2001 From: monstermichl Date: Sat, 21 Jun 2025 11:06:43 +0200 Subject: [PATCH 02/10] Remove main.go --- main.go | 98 --------------------------------------------------------- 1 file changed, 98 deletions(-) delete mode 100644 main.go diff --git a/main.go b/main.go deleted file mode 100644 index 92bab69..0000000 --- a/main.go +++ /dev/null @@ -1,98 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/monstermichl/typeshell/converters/bash" - "github.com/monstermichl/typeshell/converters/batch" - "github.com/monstermichl/typeshell/transpiler" -) - -const ( - typeBatch string = "batch" - typeBash string = "bash" -) - -var convMapping = map[string]transpiler.Converter{ - typeBatch: batch.New(), - typeBash: bash.New(), -} - -type options struct { - in string - out string - converters []transpiler.Converter -} - -func parseOptions() options { - args := os.Args - options := options{} - types := []string{} - - for k := range convMapping { - types = append(types, k) - } - - for i := 1; i < (len(args) - 1); i += 2 { - cSwitch := args[i] - cValue := args[i+1] - - switch cSwitch { - case "-i", "--in": - // Make sure input file exists. - if stat, err := os.Stat(cValue); err != nil { - panic(fmt.Errorf("input file %s doesn't exist", cValue)) - } else if stat.IsDir() { - panic(fmt.Errorf("input %s is not a file", cValue)) - } - options.in = cValue - case "-o", "--out": - // Make sure output path exists. - if stat, err := os.Stat(cValue); err != nil { - panic(fmt.Errorf("output path %s doesn't exist", cValue)) - } else if !stat.IsDir() { - panic(fmt.Errorf("output path %s is not a directory", cValue)) - } - options.out = cValue - case "-t", "--type": - conv, ok := convMapping[cValue] - - if !ok { - panic(fmt.Errorf("unknown converter type %s. Allowed types are %s", cValue, strings.Join(types, ", "))) - } - options.converters = append(options.converters, conv) - default: - panic(fmt.Errorf("unknown option %s", cSwitch)) - } - } - - if len(options.in) == 0 { - panic("no input file provided (-i/--in)") - } else if len(options.out) == 0 { - panic("no output directory provided (-o/--out)") - } else if len(options.converters) == 0 { - panic("no output type provided (-t/--type)") - } - return options -} - -func main() { - options := parseOptions() - t := transpiler.New() - - for _, conv := range options.converters { - in := options.in - dump, err := t.Transpile(in, conv) - - if err != nil { - panic(err) - } - file := filepath.Base(in) - file = file[0 : len(file)-len(filepath.Ext(in))] // Remove extension. - - os.WriteFile(filepath.Join(options.out, fmt.Sprintf("%s.%s", file, conv.Extension())), []byte(dump), 0777) - } -} From d125df86c3d4ddc7dd7371a3bc6a2b61f896653d Mon Sep 17 00:00:00 2001 From: monstermichl Date: Sat, 5 Jul 2025 11:54:51 +0200 Subject: [PATCH 03/10] Replace character descriptions by actual characters in some error messages --- parser/parser.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/parser/parser.go b/parser/parser.go index 5db172e..8edb1df 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -1355,7 +1355,7 @@ func (p *Parser) evaluateFunctionDefinition(ctx context) (Statement, error) { closingBrace := p.eat() if closingBrace.Type() != lexer.CLOSING_ROUND_BRACKET { - return nil, p.expectedError("closing bracket", closingBrace) + return nil, p.expectedError(`")"`, closingBrace) } } returnTypeToken := p.peek() @@ -1994,7 +1994,7 @@ func (p *Parser) evaluateSingleExpression(ctx context) (Expression, error) { closingToken := p.eat() if closingToken.Type() != lexer.CLOSING_ROUND_BRACKET { - return nil, p.expectedError("closing bracket", closingToken) + return nil, p.expectedError(`")"`, closingToken) } // Handle slice instantiation. @@ -2300,7 +2300,7 @@ func (p *Parser) evaluateArguments(typeName string, name string, params []Variab openingBraceToken := p.eat() if openingBraceToken.Type() != lexer.OPENING_ROUND_BRACKET { - return nil, p.expectedError("opening bracket", openingBraceToken) + return nil, p.expectedError(`"("`, openingBraceToken) } nextToken := p.peek() args := []Expression{} @@ -2347,7 +2347,7 @@ func (p *Parser) evaluateArguments(typeName string, name string, params []Variab tokenType := nextToken.Type() if !slices.Contains([]lexer.TokenType{lexer.COMMA, lexer.CLOSING_ROUND_BRACKET}, tokenType) { - err = p.expectedError("comma or closing bracket", nextToken) + err = p.expectedError(`"," or ")"`, nextToken) break } else if tokenType == lexer.COMMA { p.eat() @@ -2370,7 +2370,7 @@ func (p *Parser) evaluateArguments(typeName string, name string, params []Variab closingBraceToken := p.eat() if closingBraceToken.Type() != lexer.CLOSING_ROUND_BRACKET { - return nil, p.expectedError("closing bracket", closingBraceToken) + return nil, p.expectedError(`")"`, closingBraceToken) } return args, nil } From 240a5a3bfe068e8f7a91e5795c6983dc2a43512a Mon Sep 17 00:00:00 2001 From: monstermichl Date: Sat, 5 Jul 2025 12:10:57 +0200 Subject: [PATCH 04/10] Consider plural in error message --- parser/parser.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/parser/parser.go b/parser/parser.go index 8edb1df..54433a6 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -1076,7 +1076,16 @@ func (p *Parser) evaluateVarDefinition(ctx context) (Statement, error) { // Check if the amount of values is equal to the amount of variable names. if valuesTypesLen != variablesLen { - return nil, p.atError(fmt.Sprintf("got %d initialisation values but %d variables", valuesTypesLen, variablesLen), nextToken) + pluralInit := "" + pluralValues := "" + + if valuesTypesLen != 1 { + pluralInit = "s" + } + if variablesLen != 1 { + pluralValues = "s" + } + return nil, p.atError(fmt.Sprintf("got %d initialisation value%s but %d variable%s", valuesTypesLen, pluralInit, variablesLen, pluralValues), nextToken) } // If a type has been specified, make sure the returned types fit this type. From 34dfab40e4737bbe2c05db681f34fea533c66246 Mon Sep 17 00:00:00 2001 From: monstermichl Date: Sat, 5 Jul 2025 13:00:49 +0200 Subject: [PATCH 05/10] Fix permissions (from hex to oct) --- tests/builtin.go | 4 ++-- tests/helpers.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/builtin.go b/tests/builtin.go index 807542b..948b246 100644 --- a/tests/builtin.go +++ b/tests/builtin.go @@ -49,7 +49,7 @@ func testCopySuccess(t *testing.T, transpilerFunc transpilerFunc) { func testReadSuccess(t *testing.T, transpilerFunc transpilerFunc) { file := "read-test.txt" content := "Hello World" - os.WriteFile(file, []byte(content), 0x777) + os.WriteFile(file, []byte(content), 0700) defer os.Remove(file) transpilerFunc(t, ` @@ -156,7 +156,7 @@ func testCopyInFunctionSuccess(t *testing.T, transpilerFunc transpilerFunc) { func testReadInFunctionSuccess(t *testing.T, transpilerFunc transpilerFunc) { file := "read-test.txt" content := "Hello World" - os.WriteFile(file, []byte(content), 0x777) + os.WriteFile(file, []byte(content), 0700) defer os.Remove(file) transpilerFunc(t, ` diff --git a/tests/helpers.go b/tests/helpers.go index 4d435d6..74c24c4 100644 --- a/tests/helpers.go +++ b/tests/helpers.go @@ -33,7 +33,7 @@ func transpileFunc(t *testing.T, source sourceCallout, targetFileName string, co if err == nil { var code string - err = os.WriteFile(file, []byte(src), 0x777) + err = os.WriteFile(file, []byte(src), 0700) require.Nil(t, err) code, err = trans.Transpile(file, converter) @@ -42,7 +42,7 @@ func transpileFunc(t *testing.T, source sourceCallout, targetFileName string, co // If transpilation was successful, run the code. if err == nil { targetFile := filepath.Join(dir, targetFileName) - err = os.WriteFile(targetFile, []byte(code), 0x777) + err = os.WriteFile(targetFile, []byte(code), 0700) require.Nil(t, err) cmd := exec.Command(targetFile) From 5c641e01dfe59de5223f78a005ef39d125db3090 Mon Sep 17 00:00:00 2001 From: monstermichl Date: Sat, 5 Jul 2025 13:14:54 +0200 Subject: [PATCH 06/10] Add support for execution of non-local executables by specifying them via string literal (https://github.com/monstermichl/TypeShell/issues/35) --- README.md | 11 ++++++++--- parser/parser.go | 7 +++++-- tests/appcall_linux_test.go | 27 +++++++++++++++++++++++++++ tests/appcall_windows_test.go | 27 +++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index df44934..49939ff 100644 --- a/README.md +++ b/README.md @@ -154,14 +154,14 @@ for i := 0; i < len(s); i++ { } ``` -### Programs +### Programs/Scripts ```golang -// Programs are called by stating the program name preceded by an @. +// Programs/Scripts are called by stating the name preceded by an @. @dir("/b") ``` ```golang -// Similar to Bash/Batch, the output can be piped into another program. +// Similar to Bash/Batch, the output can be piped into another program/script. @dir("/b") | @sort("/r") ``` @@ -170,6 +170,11 @@ for i := 0; i < len(s); i++ { stdout, stderr, code := @dir("/b") | @sort("/r") ``` +```golang +// To specify the path to a program/script, a string literal is used. +@`helper\dir.bat`("/b") // Equivalent to @"helper\\dir.bat"("/b") +``` + ### Imports TypeShell does not support import of packages like Go does, but it supports single file imports. If an imported script is not a [standard "library" script](https://github.com/monstermichl/TypeShell/tree/main/std), an alias needs to be defined. diff --git a/parser/parser.go b/parser/parser.go index 54433a6..a816fd1 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -2447,8 +2447,11 @@ func (p *Parser) evaluateAppCall(ctx context) (Call, error) { nextToken = p.eat() name := nextToken.Value() - if nextToken.Type() != lexer.IDENTIFIER { - return nil, p.expectedError("program identifier", nextToken) + switch nextToken.Type() { + case lexer.IDENTIFIER, lexer.STRING_LITERAL: + // Nothing to do, those cases are valid. + default: + return nil, p.expectedError("program identifier or string literal", nextToken) } args, err := p.evaluateArguments("program", name, nil, ctx) diff --git a/tests/appcall_linux_test.go b/tests/appcall_linux_test.go index 991447c..f503714 100644 --- a/tests/appcall_linux_test.go +++ b/tests/appcall_linux_test.go @@ -2,6 +2,8 @@ package tests import ( "fmt" + "os" + "path" "testing" "github.com/stretchr/testify/require" @@ -33,6 +35,31 @@ func TestLsCallPipeToGrepCallSuccess(t *testing.T) { }) } +func TestBashFileFromSubDirCallCallPipeToGrepCallSuccess(t *testing.T) { + transpileBashFunc(t, func(dir string) (string, error) { + subdir := path.Join(dir, "subdir") + err := os.Mkdir(subdir, 0700) + + if err != nil { + return "", err + } + bashFile := path.Join(subdir, "bash.sh") + err = os.WriteFile(bashFile, []byte("ls $1"), 0700) + + if err != nil { + return "", err + } + return ` + ` + fmt.Sprintf(`var stdout, stderr, code = @"%s"("..") | @grep(".tsh")`, bashFile) + ` + + print(stdout, code) + `, nil + }, func(output string, err error) { + require.Nil(t, err) + require.Equal(t, "test.tsh 0", output) + }) +} + func TestLsCallFail(t *testing.T) { transpileBashFunc(t, func(dir string) (string, error) { return ` diff --git a/tests/appcall_windows_test.go b/tests/appcall_windows_test.go index 0c02000..20881c6 100644 --- a/tests/appcall_windows_test.go +++ b/tests/appcall_windows_test.go @@ -2,6 +2,8 @@ package tests import ( "fmt" + "os" + "path" "strings" "testing" @@ -34,6 +36,31 @@ func TestDirCallPipeToFindstrCallSuccess(t *testing.T) { }) } +func TestBatFileFromSubDirCallPipeToFindstrCallSuccess(t *testing.T) { + transpileBatchFunc(t, func(dir string) (string, error) { + subdir := path.Join(dir, "subdir") + err := os.Mkdir(subdir, 0700) + + if err != nil { + return "", err + } + batFile := path.Join(subdir, "bat.bat") + err = os.WriteFile(batFile, []byte("dir /b %1"), 0700) + + if err != nil { + return "", err + } + return ` + ` + fmt.Sprintf(`var stdout, stderr, code = @"%s"("..") | @findstr(".tsh")`, strings.ReplaceAll(strings.ReplaceAll(batFile, `/`, `\`), `\`, `\\`)) + ` + + print(stdout, code) + `, nil + }, func(output string, err error) { + require.Nil(t, err) + require.Equal(t, "test.tsh 0", output) + }) +} + func TestDirCallFail(t *testing.T) { transpileBatchFunc(t, func(dir string) (string, error) { return ` From 01846a6d1ae5132351779e885cd513726d68bef9 Mon Sep 17 00:00:00 2001 From: monstermichl Date: Sat, 5 Jul 2025 13:21:28 +0200 Subject: [PATCH 07/10] Remove dir passing in tests --- tests/appcall_linux_test.go | 4 ++-- tests/appcall_windows_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/appcall_linux_test.go b/tests/appcall_linux_test.go index f503714..c16e389 100644 --- a/tests/appcall_linux_test.go +++ b/tests/appcall_linux_test.go @@ -44,13 +44,13 @@ func TestBashFileFromSubDirCallCallPipeToGrepCallSuccess(t *testing.T) { return "", err } bashFile := path.Join(subdir, "bash.sh") - err = os.WriteFile(bashFile, []byte("ls $1"), 0700) + err = os.WriteFile(bashFile, []byte("ls .."), 0700) if err != nil { return "", err } return ` - ` + fmt.Sprintf(`var stdout, stderr, code = @"%s"("..") | @grep(".tsh")`, bashFile) + ` + ` + fmt.Sprintf(`var stdout, stderr, code = @"%s"() | @grep(".tsh")`, bashFile) + ` print(stdout, code) `, nil diff --git a/tests/appcall_windows_test.go b/tests/appcall_windows_test.go index 20881c6..482c3eb 100644 --- a/tests/appcall_windows_test.go +++ b/tests/appcall_windows_test.go @@ -45,13 +45,13 @@ func TestBatFileFromSubDirCallPipeToFindstrCallSuccess(t *testing.T) { return "", err } batFile := path.Join(subdir, "bat.bat") - err = os.WriteFile(batFile, []byte("dir /b %1"), 0700) + err = os.WriteFile(batFile, []byte("dir /b .."), 0700) if err != nil { return "", err } return ` - ` + fmt.Sprintf(`var stdout, stderr, code = @"%s"("..") | @findstr(".tsh")`, strings.ReplaceAll(strings.ReplaceAll(batFile, `/`, `\`), `\`, `\\`)) + ` + ` + fmt.Sprintf(`var stdout, stderr, code = @"%s"() | @findstr(".tsh")`, strings.ReplaceAll(strings.ReplaceAll(batFile, `/`, `\`), `\`, `\\`)) + ` print(stdout, code) `, nil From e96132c667ce89bdb9e4761b535806b0a9eda8f5 Mon Sep 17 00:00:00 2001 From: monstermichl Date: Sat, 5 Jul 2025 13:24:46 +0200 Subject: [PATCH 08/10] Test commit to find test issue --- tests/appcall_windows_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/appcall_windows_test.go b/tests/appcall_windows_test.go index 482c3eb..9e76409 100644 --- a/tests/appcall_windows_test.go +++ b/tests/appcall_windows_test.go @@ -51,7 +51,7 @@ func TestBatFileFromSubDirCallPipeToFindstrCallSuccess(t *testing.T) { return "", err } return ` - ` + fmt.Sprintf(`var stdout, stderr, code = @"%s"() | @findstr(".tsh")`, strings.ReplaceAll(strings.ReplaceAll(batFile, `/`, `\`), `\`, `\\`)) + ` + ` + fmt.Sprintf(`var stdout, stderr, code = @"%s"()`, strings.ReplaceAll(strings.ReplaceAll(batFile, `/`, `\`), `\`, `\\`)) + ` print(stdout, code) `, nil From d37d1ebba769e5d5103060a4b37c375407282ec1 Mon Sep 17 00:00:00 2001 From: monstermichl Date: Sat, 5 Jul 2025 13:30:38 +0200 Subject: [PATCH 09/10] Fix passed test directory (https://github.com/monstermichl/TypeShell/issues/35) --- tests/appcall_linux_test.go | 4 ++-- tests/appcall_windows_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/appcall_linux_test.go b/tests/appcall_linux_test.go index c16e389..94a05c3 100644 --- a/tests/appcall_linux_test.go +++ b/tests/appcall_linux_test.go @@ -44,13 +44,13 @@ func TestBashFileFromSubDirCallCallPipeToGrepCallSuccess(t *testing.T) { return "", err } bashFile := path.Join(subdir, "bash.sh") - err = os.WriteFile(bashFile, []byte("ls .."), 0700) + err = os.WriteFile(bashFile, []byte("ls $1"), 0700) if err != nil { return "", err } return ` - ` + fmt.Sprintf(`var stdout, stderr, code = @"%s"() | @grep(".tsh")`, bashFile) + ` + ` + fmt.Sprintf(`var stdout, stderr, code = @"%s"("%s") | @grep(".tsh")`, bashFile, dir) + ` print(stdout, code) `, nil diff --git a/tests/appcall_windows_test.go b/tests/appcall_windows_test.go index 9e76409..16d6490 100644 --- a/tests/appcall_windows_test.go +++ b/tests/appcall_windows_test.go @@ -45,13 +45,13 @@ func TestBatFileFromSubDirCallPipeToFindstrCallSuccess(t *testing.T) { return "", err } batFile := path.Join(subdir, "bat.bat") - err = os.WriteFile(batFile, []byte("dir /b .."), 0700) + err = os.WriteFile(batFile, []byte("dir /b %1"), 0700) if err != nil { return "", err } return ` - ` + fmt.Sprintf(`var stdout, stderr, code = @"%s"()`, strings.ReplaceAll(strings.ReplaceAll(batFile, `/`, `\`), `\`, `\\`)) + ` + ` + fmt.Sprintf(`var stdout, stderr, code = @"%s"("%s") | @findstr(".tsh")`, strings.ReplaceAll(strings.ReplaceAll(batFile, `/`, `\`), `\`, `\\`), strings.ReplaceAll(dir, `\`, `\\`)) + ` print(stdout, code) `, nil From 5e5695922d44be4a047873f20f46466bf87f62d4 Mon Sep 17 00:00:00 2001 From: monstermichl Date: Sat, 5 Jul 2025 17:08:14 +0200 Subject: [PATCH 10/10] Escape exclamation marks in string literals (Batch) (https://github.com/monstermichl/TypeShell/issues/33) --- converters/batch/converter.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/converters/batch/converter.go b/converters/batch/converter.go index 93426cc..2d11ebc 100644 --- a/converters/batch/converter.go +++ b/converters/batch/converter.go @@ -85,8 +85,13 @@ func forLabel(count int) string { func (c *converter) StringToString(value string) string { c.addLf() + // Escape all "!". + value = strings.ReplaceAll(value, "!", "^!") + // Replace "\n" with Batch newline value. - return strings.ReplaceAll(value, "\n", "!LF!") + value = strings.ReplaceAll(value, "\n", "!LF!") + + return value } func (c *converter) Dump() (string, error) {