package builtin import ( "strings" "testing" ) // --- IsSedInPlaceEdit --- func TestIsSedInPlaceEdit(t *testing.T) { tests := []struct { cmd string want bool }{ {"sed -i 's/foo/bar/' file.txt", true}, {"sed -i '' 's/foo/bar/' file.txt", true}, // macOS 空后缀 {"sed -i.bak 's/foo/bar/' file.txt", true}, // 带 .bak 后缀 {"sed -E -i 's/foo+/bar/' file.txt", true}, // 扩展正则 {"sed -i -E 's/foo+/bar/' file.txt", true}, // -E 在 -i 后 {"sed 's/foo/bar/' file.txt", false}, // 无 -i {"sed -i 'p' file.txt", false}, // 非替换操作 {"sed -i 's/foo/bar/g' f1 f2", false}, // 多文件 {"cat file.txt", false}, // 非 sed {"sed -i -e 's/a/b/' -e 's/c/d/' file", false}, // 多表达式 } for _, tt := range tests { got := IsSedInPlaceEdit(tt.cmd) if got != tt.want { t.Errorf("IsSedInPlaceEdit(%q) = %v, want %v", tt.cmd, got, tt.want) } } } // --- ParseSedEditCommand --- func TestParseSedEditCommand_Basic(t *testing.T) { info := ParseSedEditCommand("sed -i 's/foo/bar/' file.txt") if info == nil { t.Fatal("expected non-nil") } if info.Pattern != "foo" { t.Errorf("pattern: got %q, want %q", info.Pattern, "foo") } if info.Replacement != "bar" { t.Errorf("replacement: got %q, want %q", info.Replacement, "bar") } if info.Flags != "" { t.Errorf("flags: got %q, want empty", info.Flags) } if info.FilePath != "file.txt" { t.Errorf("filePath: got %q, want %q", info.FilePath, "file.txt") } if info.ExtendedRegex { t.Error("should not be extended regex") } } func TestParseSedEditCommand_GlobalFlag(t *testing.T) { info := ParseSedEditCommand("sed -i 's/old/new/g' /tmp/file") if info == nil { t.Fatal("expected non-nil") } if info.Flags != "g" { t.Errorf("flags: got %q, want %q", info.Flags, "g") } } func TestParseSedEditCommand_ExtendedRegex(t *testing.T) { info := ParseSedEditCommand("sed -E -i 's/foo+/bar/' file.go") if info == nil { t.Fatal("expected non-nil") } if !info.ExtendedRegex { t.Error("should be extended regex") } } func TestParseSedEditCommand_EscapedDelimiter(t *testing.T) { // 模式和替换中包含转义的 / info := ParseSedEditCommand(`sed -i 's/foo\/bar/baz\/qux/' file.txt`) if info == nil { t.Fatal("expected non-nil") } if info.Pattern != `foo\/bar` { t.Errorf("pattern: got %q, want %q", info.Pattern, `foo\/bar`) } if info.Replacement != `baz\/qux` { t.Errorf("replacement: got %q, want %q", info.Replacement, `baz\/qux`) } } func TestParseSedEditCommand_QuotedSpaceInPattern(t *testing.T) { // 模式含空格,用单引号包裹 info := ParseSedEditCommand("sed -i 's/hello world/hi/' file.txt") if info == nil { t.Fatal("expected non-nil") } if info.Pattern != "hello world" { t.Errorf("pattern: got %q, want %q", info.Pattern, "hello world") } } func TestParseSedEditCommand_MacOSEmptySuffix(t *testing.T) { // macOS: sed -i '' 's/foo/bar/' info := ParseSedEditCommand("sed -i '' 's/foo/bar/' file.txt") if info == nil { t.Fatal("expected non-nil for macOS style") } if info.Pattern != "foo" { t.Errorf("pattern: got %q", info.Pattern) } } func TestParseSedEditCommand_InvalidFlagDenied(t *testing.T) { // e 标志(执行)应被拒绝 info := ParseSedEditCommand("sed -i 's/foo/bar/e' file.txt") if info != nil { t.Error("expected nil for dangerous e flag") } } // --- ApplySedSubstitution --- func TestApplySedSubstitution_SimpleReplace(t *testing.T) { info := &SedEditInfo{Pattern: "foo", Replacement: "bar", Flags: "g"} result := ApplySedSubstitution("foo and foo", info) if result != "bar and bar" { t.Errorf("got %q, want %q", result, "bar and bar") } } func TestApplySedSubstitution_FirstOnly(t *testing.T) { // 无 g 标志:只替换第一次 info := &SedEditInfo{Pattern: "foo", Replacement: "bar", Flags: ""} result := ApplySedSubstitution("foo and foo", info) if result != "bar and foo" { t.Errorf("got %q, want %q", result, "bar and foo") } } func TestApplySedSubstitution_AmpersandFullMatch(t *testing.T) { // & 在替换字符串中 = 全匹配 info := &SedEditInfo{Pattern: "foo", Replacement: "&bar", Flags: "g"} result := ApplySedSubstitution("foo baz", info) if result != "foobar baz" { t.Errorf("got %q, want %q", result, "foobar baz") } } func TestApplySedSubstitution_EscapedAmpersand(t *testing.T) { // \& 在替换字符串中 = 字面量 & info := &SedEditInfo{Pattern: "foo", Replacement: `\&bar`, Flags: "g"} result := ApplySedSubstitution("foo baz", info) if result != "&bar baz" { t.Errorf("got %q, want %q", result, "&bar baz") } } func TestApplySedSubstitution_MixedAmpersand(t *testing.T) { // 同时含 \& (字面量) 和 & (全匹配) info := &SedEditInfo{Pattern: "foo", Replacement: `\& &`, Flags: "g"} result := ApplySedSubstitution("foo baz", info) if result != "& foo baz" { t.Errorf("got %q, want %q", result, "& foo baz") } } func TestApplySedSubstitution_Newline(t *testing.T) { // \n 转换为换行符 info := &SedEditInfo{Pattern: "foo", Replacement: `foo\nbar`, Flags: "g"} result := ApplySedSubstitution("foo", info) if result != "foo\nbar" { t.Errorf("got %q, want %q", result, "foo\nbar") } } func TestApplySedSubstitution_CaptureGroup(t *testing.T) { // \1 引用捕获组 info := &SedEditInfo{Pattern: `\(hello\)`, Replacement: `\1 world`, Flags: "g"} result := ApplySedSubstitution("hello", info) if result != "hello world" { t.Errorf("got %q, want %q", result, "hello world") } } func TestApplySedSubstitution_ExtendedCaptureGroup(t *testing.T) { // ERE 模式下的 (hello) 捕获组 info := &SedEditInfo{ Pattern: `(hello)`, Replacement: `\1 world`, Flags: "g", ExtendedRegex: true, } result := ApplySedSubstitution("hello", info) if result != "hello world" { t.Errorf("got %q, want %q", result, "hello world") } } func TestApplySedSubstitution_BREMetachars(t *testing.T) { // BRE 模式:+ 是字面量,\+ 是元字符 info := &SedEditInfo{Pattern: `foo\+`, Replacement: "bar", Flags: "g"} result := ApplySedSubstitution("foo fooo foooo", info) // \+ in BRE = one or more → should match fooo and foooo but not foo if !strings.Contains(result, "bar") { t.Errorf("BRE \\+ should match one-or-more: got %q", result) } } func TestApplySedSubstitution_InvalidPattern(t *testing.T) { // 无效正则:不崩溃,返回原内容 info := &SedEditInfo{Pattern: "[invalid", Replacement: "bar", Flags: "g"} original := "some content" result := ApplySedSubstitution(original, info) if result != original { t.Errorf("invalid pattern should return original: got %q", result) } } func TestApplySedSubstitution_RandomSalt_Idempotent(t *testing.T) { // 多次调用结果一致(random salt 不影响功能) info := &SedEditInfo{Pattern: "foo", Replacement: "&bar", Flags: "g"} input := "foo baz foo" result1 := ApplySedSubstitution(input, info) result2 := ApplySedSubstitution(input, info) result3 := ApplySedSubstitution(input, info) if result1 != result2 || result2 != result3 { t.Errorf("non-deterministic: %q, %q, %q", result1, result2, result3) } if result1 != "foobar baz foobar" { t.Errorf("got %q, want %q", result1, "foobar baz foobar") } } func TestApplySedSubstitution_LiteralDollar(t *testing.T) { // $ 在 sed 替换字符串中是字面量(行尾锚点只在模式中有效) info := &SedEditInfo{Pattern: "price", Replacement: "cost $100", Flags: "g"} result := ApplySedSubstitution("the price", info) if result != "the cost $100" { t.Errorf("got %q, want %q", result, "the cost $100") } } // --- 边界情况 --- func TestParseSedSubstExpression_EdgeCases(t *testing.T) { tests := []struct { input string wantPattern string wantRepl string wantFlags string wantOK bool }{ {"foo/bar/g", "foo", "bar", "g", true}, {"foo//g", "foo", "", "g", true}, // 空替换字符串 {"/bar/", "", "bar", "", true}, // 空模式 {"foo/bar", "", "", "", false}, // 缺少第二个 / {`foo\/bar/baz/`, `foo\/bar`, "baz", "", true}, // 转义分隔符 } for _, tt := range tests { pattern, repl, flags, ok := parseSedSubstExpression(tt.input) if ok != tt.wantOK { t.Errorf("parseSedSubstExpression(%q) ok=%v, want %v", tt.input, ok, tt.wantOK) continue } if !ok { continue } if pattern != tt.wantPattern { t.Errorf("parseSedSubstExpression(%q) pattern=%q, want %q", tt.input, pattern, tt.wantPattern) } if repl != tt.wantRepl { t.Errorf("parseSedSubstExpression(%q) repl=%q, want %q", tt.input, repl, tt.wantRepl) } if flags != tt.wantFlags { t.Errorf("parseSedSubstExpression(%q) flags=%q, want %q", tt.input, flags, tt.wantFlags) } } }