local testimony = require("testimony") local pipeline = require("shell.utils.pipeline") local testify = testimony.new("== pipeline parsing ==") testify:that("double-quoted < and > are not treated as redirection", function() local result = pipeline.parse('git commit --amend --author="Your Name " -S --no-edit') testimony.assert_not_nil(result) testimony.assert_equal(1, #result) testimony.assert_equal("git", result[1].cmd) testimony.assert_nil(result[1].input_file) testimony.assert_nil(result[1].output_file) testimony.assert_equal({ "commit", "--amend", "--author=Your Name ", "-S", "--no-edit", }, result[1].args) end) testify:that("single-quoted < and > are not treated as redirection", function() local result = pipeline.parse("echo 'foo < bar > baz'") testimony.assert_not_nil(result) testimony.assert_equal(1, #result) testimony.assert_equal("echo", result[1].cmd) testimony.assert_nil(result[1].input_file) testimony.assert_nil(result[1].output_file) testimony.assert_equal({ "foo < bar > baz" }, result[1].args) end) testify:that("curly-quoted < and > are not treated as redirection", function() local result = pipeline.parse("echo {{foo < bar > baz}}") testimony.assert_not_nil(result) testimony.assert_equal(1, #result) testimony.assert_equal("echo", result[1].cmd) testimony.assert_nil(result[1].input_file) testimony.assert_nil(result[1].output_file) testimony.assert_equal({ "foo < bar > baz" }, result[1].args) end) testify:that("unquoted < still works for input redirection", function() local result = pipeline.parse("cmd < infile") testimony.assert_not_nil(result) testimony.assert_equal(1, #result) testimony.assert_equal("cmd", result[1].cmd) testimony.assert_equal("infile", result[1].input_file) end) testify:that("unquoted > still works for output redirection", function() local result = pipeline.parse("cmd > outfile") testimony.assert_not_nil(result) testimony.assert_equal(1, #result) testimony.assert_equal("cmd", result[1].cmd) testimony.assert_equal("outfile", result[1].output_file) end) testify:that("middle pipeline segments with quoted < > are not rejected", function() local result = pipeline.parse('echo hello | grep "< >" | cat') testimony.assert_not_nil(result) testimony.assert_equal(3, #result) testimony.assert_equal("echo", result[1].cmd) testimony.assert_equal("grep", result[2].cmd) testimony.assert_equal("cat", result[3].cmd) end) testify:that('--flag="value with spaces" is parsed as a single arg', function() local cmd, args = pipeline.parse_cmdline('cmd --flag="value with spaces" other') testimony.assert_equal("cmd", cmd) testimony.assert_equal({ "--flag=value with spaces", "other" }, args) end) testify:that("combined input and output redirection works", function() local result = pipeline.parse("cmd < infile > outfile") testimony.assert_not_nil(result) testimony.assert_equal(1, #result) testimony.assert_equal("cmd", result[1].cmd) testimony.assert_equal("infile", result[1].input_file) testimony.assert_equal("outfile", result[1].output_file) end) -- Append redirect tests testify:that(">> parses as append stdout redirect", function() local result = pipeline.parse("cmd >> outfile") testimony.assert_not_nil(result) testimony.assert_equal(1, #result) testimony.assert_equal("cmd", result[1].cmd) testimony.assert_equal("outfile", result[1].output_file) testimony.assert_true(result[1].output_append) end) testify:that(">> without space before filename works", function() local result = pipeline.parse("cmd >>outfile") testimony.assert_not_nil(result) testimony.assert_equal("outfile", result[1].output_file) testimony.assert_true(result[1].output_append) end) testify:that("> without space before filename works", function() local result = pipeline.parse("cmd >outfile") testimony.assert_not_nil(result) testimony.assert_equal("outfile", result[1].output_file) testimony.assert_false(result[1].output_append) end) -- Stderr redirect tests testify:that("err> parses as stderr truncate redirect", function() local result = pipeline.parse("cmd err> errfile") testimony.assert_not_nil(result) testimony.assert_equal(1, #result) testimony.assert_equal("cmd", result[1].cmd) testimony.assert_equal("errfile", result[1].stderr_file) testimony.assert_false(result[1].stderr_append) end) testify:that("err>> parses as stderr append redirect", function() local result = pipeline.parse("cmd err>> errfile") testimony.assert_not_nil(result) testimony.assert_equal(1, #result) testimony.assert_equal("cmd", result[1].cmd) testimony.assert_equal("errfile", result[1].stderr_file) testimony.assert_true(result[1].stderr_append) end) -- Combined all> redirect tests testify:that("all> redirects both stdout and stderr to same file", function() local result = pipeline.parse("cmd all> logfile") testimony.assert_not_nil(result) testimony.assert_equal(1, #result) testimony.assert_equal("cmd", result[1].cmd) testimony.assert_equal("logfile", result[1].output_file) testimony.assert_equal("logfile", result[1].stderr_file) testimony.assert_false(result[1].output_append) testimony.assert_true(result[1].all_redirect) end) testify:that("all>> redirects both stdout and stderr in append mode", function() local result = pipeline.parse("cmd all>> logfile") testimony.assert_not_nil(result) testimony.assert_equal("logfile", result[1].output_file) testimony.assert_equal("logfile", result[1].stderr_file) testimony.assert_true(result[1].output_append) testimony.assert_true(result[1].stderr_append) testimony.assert_true(result[1].all_redirect) end) -- Separate stdout and stderr on same command testify:that("separate stdout and stderr redirects on same command", function() local result = pipeline.parse("cmd > outfile err> errfile") testimony.assert_not_nil(result) testimony.assert_equal(1, #result) testimony.assert_equal("cmd", result[1].cmd) testimony.assert_equal("outfile", result[1].output_file) testimony.assert_equal("errfile", result[1].stderr_file) end) testify:that("append stdout with truncate stderr", function() local result = pipeline.parse("cmd >> outfile err> errfile") testimony.assert_not_nil(result) testimony.assert_equal("outfile", result[1].output_file) testimony.assert_true(result[1].output_append) testimony.assert_equal("errfile", result[1].stderr_file) testimony.assert_false(result[1].stderr_append) end) -- Combined input, output, and stderr redirects testify:that("combined < >> err> on first command", function() local result = pipeline.parse("cmd < infile >> outfile err> errfile") testimony.assert_not_nil(result) testimony.assert_equal("cmd", result[1].cmd) testimony.assert_equal("infile", result[1].input_file) testimony.assert_equal("outfile", result[1].output_file) testimony.assert_true(result[1].output_append) testimony.assert_equal("errfile", result[1].stderr_file) end) -- Conflict detection tests testify:that("all> with err> is a conflict error", function() local result, err = pipeline.parse("cmd all> file err> errfile") testimony.assert_nil(result) testimony.assert_not_nil(err) end) testify:that("err> with all> is a conflict error", function() local result, err = pipeline.parse("cmd err> errfile all> file") testimony.assert_nil(result) testimony.assert_not_nil(err) end) testify:that(">> with all> is a conflict error", function() local result, err = pipeline.parse("cmd >> file all> log") testimony.assert_nil(result) testimony.assert_not_nil(err) end) testify:that("duplicate stdout redirects are an error", function() local result, err = pipeline.parse("cmd > file >> file2") testimony.assert_nil(result) testimony.assert_not_nil(err) end) testify:that("redirect with no filename is an error", function() local result, err = pipeline.parse("cmd >") testimony.assert_nil(result) testimony.assert_not_nil(err) end) -- Middle pipeline redirect tests testify:that("middle pipeline command can have output redirect", function() local result = pipeline.parse("echo hello | grep hello > matches.txt | cat") testimony.assert_not_nil(result) testimony.assert_equal(3, #result) testimony.assert_equal("grep", result[2].cmd) testimony.assert_equal("matches.txt", result[2].output_file) end) testify:that("middle pipeline command can have stderr redirect", function() local result = pipeline.parse("cmd1 | cmd2 err> errors.log | cmd3") testimony.assert_not_nil(result) testimony.assert_equal(3, #result) testimony.assert_equal("errors.log", result[2].stderr_file) end) testify:that("stdin redirect on non-first command is an error", function() local result, err = pipeline.parse("cmd1 | cmd2 < infile | cmd3") testimony.assert_nil(result) testimony.assert_not_nil(err) end) -- Quote tests for new redirect tokens testify:that("double-quoted err> is not treated as redirect", function() local result = pipeline.parse('echo "err> not redirect"') testimony.assert_not_nil(result) testimony.assert_equal(1, #result) testimony.assert_nil(result[1].stderr_file) end) testify:that("single-quoted all> is not treated as redirect", function() local result = pipeline.parse("echo 'all> not redirect'") testimony.assert_not_nil(result) testimony.assert_nil(result[1].output_file) testimony.assert_nil(result[1].stderr_file) end) testify:that("duplicate err> redirects report correct error", function() local result, err = pipeline.parse("cmd err> file1 err> file2") testimony.assert_nil(result) testimony.assert_equal("multiple stderr redirections", err) end) testify:that("lone } inside {{ }} does not break curly quoting", function() local result = pipeline.parse("echo {{foo } bar > baz}}") testimony.assert_not_nil(result) testimony.assert_equal(1, #result) testimony.assert_equal("echo", result[1].cmd) testimony.assert_nil(result[1].output_file) testimony.assert_equal({ "foo } bar > baz" }, result[1].args) end) testify:that("lone } inside {{ }} does not split pipeline on |", function() local result = pipeline.parse("echo {{a } | b}}") testimony.assert_not_nil(result) testimony.assert_equal(1, #result) testimony.assert_equal("echo", result[1].cmd) testimony.assert_equal({ "a } | b" }, result[1].args) end) -- Word boundary tests testify:that("stderr> does not match as err> redirect", function() local result = pipeline.parse("cmd stderr> file") testimony.assert_not_nil(result) -- stderr> should be parsed as > redirect with stderr as part of command/args testimony.assert_equal("file", result[1].output_file) testimony.assert_nil(result[1].stderr_file) end) testify:conclude()