From cb9d448f378b348f1f8df8f47d4614dd1cf3dd29 Mon Sep 17 00:00:00 2001 From: Toshimaru Date: Mon, 12 Jan 2026 13:45:10 +0900 Subject: [PATCH 1/2] [ci skip] WIP From a1068eaf3c6d625b338487c97bfaa8aaea14a7f3 Mon Sep 17 00:00:00 2001 From: Toshimaru Date: Mon, 12 Jan 2026 13:45:46 +0900 Subject: [PATCH 2/2] WIP --- cmd/root.go | 66 +++++++++++++++++++ cmd/root_test.go | 129 ++++++++++++++++++++++++++++++++++++++ cmd/testdata/bashscript | 2 + cmd/testdata/pythonscript | 2 + testscript/bashscript | 2 + testscript/pythonscript | 2 + 6 files changed, 203 insertions(+) create mode 100644 cmd/testdata/bashscript create mode 100644 cmd/testdata/pythonscript create mode 100644 testscript/bashscript create mode 100644 testscript/pythonscript diff --git a/cmd/root.go b/cmd/root.go index bb45f91..b54bfd0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -77,6 +77,9 @@ func cmdMain(cmd *cobra.Command, args []string) (err error) { cmd.PrintErrln("Error:", err) return err } + if lexer == nil { + lexer = detectShebang(data) + } if lexer == nil { lexer = lexers.Analyse(string(data)) } @@ -91,6 +94,10 @@ func cmdMain(cmd *cobra.Command, args []string) (err error) { } if language == "" { lexer = lexers.Match(filename) + if lexer == nil { + lexer = lexers.Analyse(string(data)) + // lexer = detectShebang((data)) + } } printData(&data, cmd, lexer) } @@ -101,6 +108,65 @@ func cmdMain(cmd *cobra.Command, args []string) (err error) { return } +func detectShebang(data []byte) chroma.Lexer { + if len(data) < 2 || data[0] != '#' || data[1] != '!' { + return nil + } + + // Find the end of the first line + endIdx := bytes.IndexByte(data, '\n') + if endIdx == -1 { + endIdx = len(data) + } + + shebangLine := bytes.TrimSpace(data[2:endIdx]) // Skip "#!" + + // Parse interpreter from shebang + var interpreter string + if bytes.Contains(shebangLine, []byte("/env ")) { + // Handle "/usr/bin/env python3" format + parts := bytes.Fields(shebangLine) + if len(parts) >= 2 { + interpreter = string(parts[1]) + } + } else { + // Handle "/bin/bash" format + parts := bytes.Split(shebangLine, []byte("/")) + if len(parts) > 0 { + lastPart := parts[len(parts)-1] + // Remove any arguments after space + if idx := bytes.IndexByte(lastPart, ' '); idx != -1 { + lastPart = lastPart[:idx] + } + interpreter = string(lastPart) + } + } + + // Map interpreter to lexer + interpreterMap := map[string]string{ + "bash": "bash", + "sh": "bash", + "zsh": "bash", + "ksh": "bash", + "python": "python", + "python3": "python", + "python2": "python", + "ruby": "ruby", + "perl": "perl", + "node": "javascript", + "nodejs": "javascript", + "php": "php", + "lua": "lua", + "awk": "awk", + } + + if lexerName, ok := interpreterMap[interpreter]; ok { + return lexers.Get(lexerName) + } + + return nil +} + func printData(data *[]byte, cmd *cobra.Command, lexer chroma.Lexer) { out := cmd.OutOrStdout() if number { diff --git a/cmd/root_test.go b/cmd/root_test.go index 0197bcd..c52d3b0 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -353,3 +353,132 @@ func _unhighlightedGoCode() string { } return unhighlightedGoCode } + +func TestDetectShebang(t *testing.T) { + t.Run("Bash shebang", func(t *testing.T) { + data := []byte("#!/bin/bash\necho 'hello'") + lexer := detectShebang(data) + assert.NotNil(t, lexer) + assert.Contains(t, lexer.Config().Name, "Bash") + }) + + t.Run("Python3 with env", func(t *testing.T) { + data := []byte("#!/usr/bin/env python3\nprint('hello')") + lexer := detectShebang(data) + assert.NotNil(t, lexer) + assert.Contains(t, lexer.Config().Name, "Python") + }) + + t.Run("Ruby shebang", func(t *testing.T) { + data := []byte("#!/usr/bin/ruby\nputs 'hello'") + lexer := detectShebang(data) + assert.NotNil(t, lexer) + assert.Contains(t, lexer.Config().Name, "Ruby") + }) + + t.Run("Node with env", func(t *testing.T) { + data := []byte("#!/usr/bin/env node\nconsole.log('hello')") + lexer := detectShebang(data) + assert.NotNil(t, lexer) + assert.Contains(t, lexer.Config().Name, "JavaScript") + }) + + t.Run("Sh shebang", func(t *testing.T) { + data := []byte("#!/bin/sh\necho 'hello'") + lexer := detectShebang(data) + assert.NotNil(t, lexer) + assert.Contains(t, lexer.Config().Name, "Bash") + }) + + t.Run("Perl shebang", func(t *testing.T) { + data := []byte("#!/usr/bin/perl\nprint 'hello'") + lexer := detectShebang(data) + assert.NotNil(t, lexer) + assert.Contains(t, lexer.Config().Name, "Perl") + }) + + t.Run("No shebang", func(t *testing.T) { + data := []byte("echo 'hello'") + lexer := detectShebang(data) + assert.Nil(t, lexer) + }) + + t.Run("Comment but not shebang", func(t *testing.T) { + data := []byte("# This is a comment\necho 'hello'") + lexer := detectShebang(data) + assert.Nil(t, lexer) + }) + + t.Run("Unknown interpreter", func(t *testing.T) { + data := []byte("#!/bin/unknowninterpreter\necho 'hello'") + lexer := detectShebang(data) + assert.Nil(t, lexer) + }) + + t.Run("Shebang with arguments", func(t *testing.T) { + data := []byte("#!/bin/bash -e\necho 'hello'") + lexer := detectShebang(data) + assert.NotNil(t, lexer) + assert.Contains(t, lexer.Config().Name, "Bash") + }) + + t.Run("Empty file", func(t *testing.T) { + data := []byte("") + lexer := detectShebang(data) + assert.Nil(t, lexer) + }) +} + +func TestShebangFileDetection(t *testing.T) { + setupTerminalMock(t) + + t.Run("Bash script without extension", func(t *testing.T) { + var o, e bytes.Buffer + rootCmd.SetArgs([]string{"testdata/bashscript"}) + rootCmd.SetOut(&o) + rootCmd.SetErr(&e) + err := rootCmd.Execute() + + assert.NoError(t, err) + assert.Empty(t, e.String()) + assert.NotEmpty(t, o.String()) + // Should be syntax highlighted as bash + assert.Contains(t, o.String(), "[38;5;") + }) + + t.Run("Python script without extension", func(t *testing.T) { + t.Cleanup(resetStrings) + var o, e bytes.Buffer + rootCmd.SetArgs([]string{"testdata/pythonscript"}) + rootCmd.SetOut(&o) + rootCmd.SetErr(&e) + err := rootCmd.Execute() + + assert.NoError(t, err) + assert.Empty(t, e.String()) + assert.NotEmpty(t, o.String()) + // Should be syntax highlighted as python + assert.Contains(t, o.String(), "[38;5;") + }) +} + +func TestShebangFromStdin(t *testing.T) { + setupTerminalMock(t) + + t.Run("Bash script from stdin", func(t *testing.T) { + t.Cleanup(resetStrings) + i := bytes.NewBufferString("#!/bin/bash\necho 'hello'") + var o, e bytes.Buffer + rootCmd.SetArgs([]string{}) + rootCmd.SetIn(i) + rootCmd.SetOut(&o) + rootCmd.SetErr(&e) + err := rootCmd.Execute() + + assert.NoError(t, err) + assert.Empty(t, e.String()) + assert.NotEmpty(t, o.String()) + // Should be syntax highlighted + assert.Contains(t, o.String(), "[38;5;") + }) +} diff --git a/cmd/testdata/bashscript b/cmd/testdata/bashscript new file mode 100644 index 0000000..060a12e --- /dev/null +++ b/cmd/testdata/bashscript @@ -0,0 +1,2 @@ +#!/bin/bash +echo "hello from bash" diff --git a/cmd/testdata/pythonscript b/cmd/testdata/pythonscript new file mode 100644 index 0000000..ffaee47 --- /dev/null +++ b/cmd/testdata/pythonscript @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +print("hello from python") diff --git a/testscript/bashscript b/testscript/bashscript new file mode 100644 index 0000000..060a12e --- /dev/null +++ b/testscript/bashscript @@ -0,0 +1,2 @@ +#!/bin/bash +echo "hello from bash" diff --git a/testscript/pythonscript b/testscript/pythonscript new file mode 100644 index 0000000..ffaee47 --- /dev/null +++ b/testscript/pythonscript @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +print("hello from python")