diff --git a/README.md b/README.md index dd95713c..9082f415 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ count below and mark it as done in this README.md. Thanks! GNU coreutils. They are not 100% compatiable. If you encounter different behaviors, compare against the true GNU coreutils version on the Linux-based tests first. -## Completed (74/109) - 68% done! +## Completed (75/109) - 69% done! | Done | Cmd | Descripton | Windows | | :-----: | --------- | ------------------------------------------------ | ------- | @@ -66,7 +66,7 @@ compare against the true GNU coreutils version on the Linux-based tests first. | | chown | Change file owner and group | | | | chroot | Run a command with a different root directory | | | ✓ | cksum | Print CRC checksum and byte counts | ✓ | -| | comm | Compare two sorted files line by line | ✓ | +| ✓ | comm | Compare two sorted files line by line | ✓ | | | coreutils | Multi-call program | ✓ | | ✓ | cp | Copy files and directories | ✓ | | | csplit | Split a file into context-determined pieces | ✓ | diff --git a/common/common.c.v b/common/common.c.v index 2435e68e..1227757e 100644 --- a/common/common.c.v +++ b/common/common.c.v @@ -14,7 +14,7 @@ fn C.GetConsoleOutputCP() u32 // ref: @@ fn init() { - C.setlocale(C.LC_ALL, ''.str) + C.setlocale(C.LC_ALL, c'') } // is_utf8 returns whether the locale supports UTF-8 or not diff --git a/src/comm/comm.v b/src/comm/comm.v new file mode 100644 index 00000000..2609e14c --- /dev/null +++ b/src/comm/comm.v @@ -0,0 +1,205 @@ +module main + +import os +import io +import common + +struct Settings { + suppress_col1 bool + suppress_col2 bool + suppress_col3 bool + check_order bool + nocheck_order bool + output_delimiter string + zero_terminated bool + show_total bool +} + +fn main() { + mut fp := common.flag_parser(os.args) + fp.application('comm') + fp.description('Compare two sorted files line by line') + fp.version(common.coreutils_version()) + + suppress_col1 := fp.bool('', `1`, false, 'suppress column 1 (lines unique to FILE1)') + suppress_col2 := fp.bool('', `2`, false, 'suppress column 2 (lines unique to FILE2)') + suppress_col3 := fp.bool('', `3`, false, 'suppress column 3 (lines that appear in both files)') + check_order := fp.bool('check-order', 0, false, 'check that the input is correctly sorted, even if all input lines are pairable') + nocheck_order := fp.bool('nocheck-order', 0, false, 'do not check that the input is correctly sorted') + output_delimiter := fp.string('output-delimiter', 0, '\t', 'separate columns with STRING') + zero_terminated := fp.bool('zero-terminated', `z`, false, 'line delimiter is NUL, not newline') + total := fp.bool('total', 0, false, 'output a summary') + + positional_args := fp.finalize() or { + eprintln(err) + println(fp.usage()) + exit(1) + } + + if positional_args.len != 2 { + eprintln('comm: missing operand') + eprintln("Try 'comm --help' for more information.") + exit(1) + } + + settings := Settings{ + suppress_col1: suppress_col1 + suppress_col2: suppress_col2 + suppress_col3: suppress_col3 + check_order: check_order + nocheck_order: nocheck_order + output_delimiter: output_delimiter + zero_terminated: zero_terminated + show_total: total + } + + run(settings, positional_args[0], positional_args[1]) +} + +fn run(settings Settings, file1_path string, file2_path string) { + mut file1 := open_file_or_stdin(file1_path) or { + common.exit_with_error_message('comm', '${err}') + } + defer { + file1.close() + } + + mut file2 := open_file_or_stdin(file2_path) or { + common.exit_with_error_message('comm', '${err}') + } + defer { + file2.close() + } + + mut reader1 := io.new_buffered_reader(reader: file1) + mut reader2 := io.new_buffered_reader(reader: file2) + + delimiter := if settings.zero_terminated { `\0` } else { `\n` } + + mut line1 := read_line(mut reader1, delimiter) or { '' } + mut line2 := read_line(mut reader2, delimiter) or { '' } + mut prev_line1 := '' + mut prev_line2 := '' + + mut col1_count := 0 + mut col2_count := 0 + mut col3_count := 0 + + check_order := !settings.nocheck_order && settings.check_order + + for { + // Check sort order if needed + if check_order { + if line1 != '' && prev_line1 != '' && line1 < prev_line1 { + eprintln('comm: file 1 is not in sorted order') + exit(1) + } + if line2 != '' && prev_line2 != '' && line2 < prev_line2 { + eprintln('comm: file 2 is not in sorted order') + exit(1) + } + } + + // Both files exhausted + if line1 == '' && line2 == '' { + break + } + + // File 2 exhausted or line1 comes before line2 + if line2 == '' || (line1 != '' && line1 < line2) { + if !settings.suppress_col1 { + print_column(1, line1, settings) + } + col1_count++ + prev_line1 = line1 + line1 = read_line(mut reader1, delimiter) or { '' } + } + // File 1 exhausted or line2 comes before line1 + else if line1 == '' || line2 < line1 { + if !settings.suppress_col2 { + print_column(2, line2, settings) + } + col2_count++ + prev_line2 = line2 + line2 = read_line(mut reader2, delimiter) or { '' } + } + // Lines are equal + else { + if !settings.suppress_col3 { + print_column(3, line1, settings) + } + col3_count++ + prev_line1 = line1 + prev_line2 = line2 + line1 = read_line(mut reader1, delimiter) or { '' } + line2 = read_line(mut reader2, delimiter) or { '' } + } + } + + if settings.show_total { + if !settings.suppress_col1 { + print('${col1_count}') + } + if !settings.suppress_col2 { + if !settings.suppress_col1 { + print(settings.output_delimiter) + } + print('${col2_count}') + } + if !settings.suppress_col3 { + if !settings.suppress_col1 || !settings.suppress_col2 { + print(settings.output_delimiter) + } + print('${col3_count}') + } + print('\ttotal\n') + } +} + +fn open_file_or_stdin(path string) !os.File { + if path == '-' { + return os.stdin() + } + return os.open(path)! +} + +fn read_line(mut reader io.BufferedReader, delimiter u8) !string { + line := reader.read_line(delim: delimiter)! + if line.len > 0 && line[line.len - 1] == delimiter { + return line[..line.len - 1] + } + return line +} + +fn print_column(column int, text string, settings Settings) { + mut prefix := '' + + // Add tabs based on which columns are being printed + match column { + 1 { + // Column 1 has no prefix + } + 2 { + // Column 2 has one tab if column 1 is being printed + if !settings.suppress_col1 { + prefix = settings.output_delimiter + } + } + 3 { + // Column 3 has tabs for each unsuppressed column before it + if !settings.suppress_col1 { + prefix += settings.output_delimiter + } + if !settings.suppress_col2 { + prefix += settings.output_delimiter + } + } + else {} + } + + if settings.zero_terminated { + print('${prefix}${text}\0') + } else { + println('${prefix}${text}') + } +} diff --git a/src/comm/comm_test.v b/src/comm/comm_test.v new file mode 100644 index 00000000..692fb229 --- /dev/null +++ b/src/comm/comm_test.v @@ -0,0 +1,180 @@ +import common.testing +import os + +const rig = testing.prepare_rig(util: 'comm') +const executable_under_test = rig.executable_under_test + +fn testsuite_begin() { + rig.assert_platform_util() +} + +fn test_help_and_version() { + rig.assert_help_and_version_options_work() +} + +fn test_comm_basic() { + // Create test files + file1_path := os.temp_dir() + '/comm_test1.txt' + file2_path := os.temp_dir() + '/comm_test2.txt' + + os.write_lines(file1_path, ['apple', 'banana', 'cherry', 'date'])! + os.write_lines(file2_path, ['banana', 'cherry', 'date', 'fig'])! + + // Test basic functionality + res := os.execute('${executable_under_test} ${file1_path} ${file2_path}') + assert res.exit_code == 0 + + // Normalize line endings for cross-platform compatibility + output := res.output.replace('\r\n', '\n') + expected := 'apple\n\t\tbanana\n\t\tcherry\n\t\tdate\n\tfig\n' + assert output == expected + + // Compare with platform util + rig.assert_same_results('${file1_path} ${file2_path}') + + // Cleanup + os.rm(file1_path)! + os.rm(file2_path)! +} + +fn test_comm_suppress_columns() { + file1_path := os.temp_dir() + '/comm_test3.txt' + file2_path := os.temp_dir() + '/comm_test4.txt' + + os.write_lines(file1_path, ['a', 'b', 'c'])! + os.write_lines(file2_path, ['b', 'c', 'd'])! + + // Test -1 flag + res1 := os.execute('${executable_under_test} -1 ${file1_path} ${file2_path}') + assert res1.exit_code == 0 + assert res1.output.replace('\r\n', '\n') == '\tb\n\tc\nd\n' + + // Test -2 flag + res2 := os.execute('${executable_under_test} -2 ${file1_path} ${file2_path}') + assert res2.exit_code == 0 + assert res2.output.replace('\r\n', '\n') == 'a\n\tb\n\tc\n' + + // Test -3 flag + res3 := os.execute('${executable_under_test} -3 ${file1_path} ${file2_path}') + assert res3.exit_code == 0 + assert res3.output.replace('\r\n', '\n') == 'a\n\td\n' + + // Test -12 (show only common) + res12 := os.execute('${executable_under_test} -12 ${file1_path} ${file2_path}') + assert res12.exit_code == 0 + assert res12.output.replace('\r\n', '\n') == 'b\nc\n' + + // Compare with platform util + rig.assert_same_results('-1 ${file1_path} ${file2_path}') + rig.assert_same_results('-2 ${file1_path} ${file2_path}') + rig.assert_same_results('-3 ${file1_path} ${file2_path}') + rig.assert_same_results('-12 ${file1_path} ${file2_path}') + + // Cleanup + os.rm(file1_path)! + os.rm(file2_path)! +} + +fn test_comm_empty_files() { + empty_file := os.temp_dir() + '/comm_empty.txt' + nonempty_file := os.temp_dir() + '/comm_nonempty.txt' + + os.write_file(empty_file, '')! + os.write_lines(nonempty_file, ['hello', 'world'])! + + // Both files empty + res1 := os.execute('${executable_under_test} ${empty_file} ${empty_file}') + assert res1.exit_code == 0 + assert res1.output == '' + + // First file empty + res2 := os.execute('${executable_under_test} ${empty_file} ${nonempty_file}') + assert res2.exit_code == 0 + assert res2.output.replace('\r\n', '\n') == '\thello\n\tworld\n' + + // Second file empty + res3 := os.execute('${executable_under_test} ${nonempty_file} ${empty_file}') + assert res3.exit_code == 0 + assert res3.output.replace('\r\n', '\n') == 'hello\nworld\n' + + // Compare with platform util + rig.assert_same_results('${empty_file} ${empty_file}') + rig.assert_same_results('${empty_file} ${nonempty_file}') + rig.assert_same_results('${nonempty_file} ${empty_file}') + + // Cleanup + os.rm(empty_file)! + os.rm(nonempty_file)! +} + +fn test_comm_stdin() { + // Skip stdin test on Windows due to pipe command issues + $if windows { + return + } + + file_path := os.temp_dir() + '/comm_stdin_test.txt' + stdin_file := os.temp_dir() + '/comm_stdin_content.txt' + + os.write_lines(file_path, ['apple', 'banana'])! + os.write_lines(stdin_file, ['banana', 'cherry'])! + + // Test stdin as first file + res1 := os.execute('cat ${stdin_file} | ${executable_under_test} - ${file_path}') + assert res1.exit_code == 0 + // stdin has: banana, cherry + // file_path has: apple, banana + // So: apple is only in file2 (1 tab), banana is in both (2 tabs), cherry is only in file1 (0 tabs) + expected1 := '\tapple\n\t\tbanana\ncherry\n' + assert res1.output.replace('\r\n', '\n') == expected1 + + // Test stdin as second file + res2 := os.execute('cat ${stdin_file} | ${executable_under_test} ${file_path} -') + assert res2.exit_code == 0 + expected2 := 'apple\n\t\tbanana\n\tcherry\n' + assert res2.output.replace('\r\n', '\n') == expected2 + + // Cleanup + os.rm(file_path)! + os.rm(stdin_file)! +} + +fn test_comm_missing_file() { + // Missing file should produce error + res := os.execute('${executable_under_test} /nonexistent/file1 /nonexistent/file2') + assert res.exit_code == 1 + // Error message varies by platform, but should contain the filename + assert res.output.contains('/nonexistent/file1') || res.output.contains('\\nonexistent\\file1') +} + +fn test_comm_missing_operand() { + // No arguments should produce error + res1 := os.execute('${executable_under_test}') + assert res1.exit_code == 1 + assert res1.output.contains('missing operand') + + // One argument should produce error + res2 := os.execute('${executable_under_test} /tmp/file1') + assert res2.exit_code == 1 + assert res2.output.contains('missing operand') +} + +fn test_comm_delimiter() { + file1_path := os.temp_dir() + '/comm_delim1.txt' + file2_path := os.temp_dir() + '/comm_delim2.txt' + + os.write_lines(file1_path, ['a', 'c'])! + os.write_lines(file2_path, ['b', 'c'])! + + // Test custom delimiter + res := os.execute('${executable_under_test} --output-delimiter="|" ${file1_path} ${file2_path}') + assert res.exit_code == 0 + assert res.output.replace('\r\n', '\n') == 'a\n|b\n||c\n' + + // Compare with platform util (if it supports this option) + // Note: Some platform utils may not support --output-delimiter + + // Cleanup + os.rm(file1_path)! + os.rm(file2_path)! +} diff --git a/src/comm/delete.me b/src/comm/delete.me deleted file mode 100644 index e69de29b..00000000 diff --git a/src/uptime/uptime.c.v b/src/uptime/uptime.c.v index 3ea4dd20..030a33a5 100644 --- a/src/uptime/uptime.c.v +++ b/src/uptime/uptime.c.v @@ -26,7 +26,7 @@ fn C.getloadavg(loadavg [3]f64, nelem int) int fn print_uptime(utmp_buf []C.utmpx) ! { // Get uptime mut uptime := i64(0) - fp := C.fopen(&char('/proc/uptime'.str), &char('r'.str)) + fp := C.fopen(&char(c'/proc/uptime'), &char(c'r')) if !isnil(fp) { buf := []u8{len: 4096} unsafe { diff --git a/src/users/users.v b/src/users/users.v index 3eeed57d..121fa227 100644 --- a/src/users/users.v +++ b/src/users/users.v @@ -9,7 +9,7 @@ const app = common.CoreutilInfo{ // Settings for Utility: users struct Settings { mut: - input_file &char = ''.str + input_file &char = c'' } fn users(settings Settings) {