From cdfb7e20b672f6cbdfa9350e6bbc6191dcd558b8 Mon Sep 17 00:00:00 2001 From: MarcosAndradeV Date: Wed, 15 Jan 2025 08:18:08 -0300 Subject: [PATCH] Improve CLI and remove rere.py --- Makefile | 6 -- chs/src/cli.rs | 64 ++++++++++++++++++ chs/src/main.rs | 50 +++++--------- rere.py | 173 ------------------------------------------------ test.list | 8 --- 5 files changed, 82 insertions(+), 219 deletions(-) create mode 100644 chs/src/cli.rs delete mode 100755 rere.py delete mode 100644 test.list diff --git a/Makefile b/Makefile index d2e553b..33e5579 100644 --- a/Makefile +++ b/Makefile @@ -4,12 +4,6 @@ build: release: cargo build --release --bin chs -test: build - ./rere.py replay test.list - -record: build - ./rere.py record test.list - chsc: release help: diff --git a/chs/src/cli.rs b/chs/src/cli.rs new file mode 100644 index 0000000..a481df4 --- /dev/null +++ b/chs/src/cli.rs @@ -0,0 +1,64 @@ +use std::{env::Args, process::ExitCode}; + +pub const COMMANDS: &[Command] = &[ + Command { + name: "help", + descripition: "Print this message", + run: |program, _| { + usage(program); + ExitCode::SUCCESS + }, + }, + Command { + name: "compile", + descripition: "Compile a program: chs compile ", + run: |program, args| { + if let Some(file_path) = args.next() { + ExitCode::SUCCESS + } else { + eprintln!("Expect file path."); + ExitCode::FAILURE + } + }, + }, + Command { + name: "parse", + descripition: "Parse a program and it's AST: chs parse ", + run: |program, args| { + if let Some(file_path) = args.next() { + match chs_ast::parse_file(file_path) { + Ok(ast) => { + println!("{ast}"); + ExitCode::SUCCESS + } + Err(err) => { + eprintln!("{err}"); + ExitCode::FAILURE + } + } + } else { + eprintln!("Expect file path."); + ExitCode::FAILURE + } + }, + }, +]; + +pub struct Command { + pub name: &'static str, + pub descripition: &'static str, + pub run: fn(&str, &mut Args) -> ExitCode, +} + +pub fn usage(program: &str) { + println!("USAGE: {program} [OPTIONS]"); + println!("COMMANDS:"); + + for ele in COMMANDS.iter() { + println!( + " {name} {descripition}", + name = ele.name, + descripition = ele.descripition, + ); + } +} diff --git a/chs/src/main.rs b/chs/src/main.rs index 2840d5e..d4bcc5e 100644 --- a/chs/src/main.rs +++ b/chs/src/main.rs @@ -1,38 +1,24 @@ #![allow(unused)] -use std::{env, process::exit}; +use std::{env, process::{exit, ExitCode}}; -fn main() { - let mut argv = env::args(); - let program = argv.next().expect("Program always provided."); - let cmd = argv.next().unwrap_or_else(|| usage_err(&program, "Expect Command")); - match cmd.as_str() { - "help" => usage(&program), - "com"|"compile" => {} - "parse" => { - let file_path = argv.next().unwrap_or_else(|| msg_err("ERROR: File not provided.")); - let ast = chs_ast::parse_file(file_path).unwrap_or_else(|err| msg_err(err)); - println!("{ast}"); +use cli::{usage, COMMANDS}; +mod cli; + +fn main() -> ExitCode { + let mut args = env::args(); + let program = args.next().expect("Program always provided."); + if let Some(cmd) = args.next() { + if let Some(cmd) = COMMANDS.iter().find(|c| c.name == cmd) { + (cmd.run)(&program, &mut args) + } else { + println!("Invalid command."); + usage(&program); + ExitCode::FAILURE } - c => usage_err(&program, format!("Invalid Command \"{c}\"")) + } else { + println!("Expect command."); + usage(&program); + ExitCode::FAILURE } } - -fn usage(program: &str) { - println!("USAGE: {program} [OPTIONS]"); - println!("COMMANDS:"); - println!(" help - Show this message."); - println!(" com|compile - Compile a program: chs com "); - println!(" parse - Parse a program and print its AST: chs parse "); -} - -fn usage_err(program: &str, err: impl ToString) -> ! { - eprintln!("ERROR: {}", err.to_string()); - usage(&program); - exit(1); -} - -fn msg_err(err: impl ToString) -> ! { - eprintln!("{}", err.to_string()); - exit(1); -} diff --git a/rere.py b/rere.py deleted file mode 100755 index b8f260c..0000000 --- a/rere.py +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2024 Alexey Kutepov - -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: - -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -import sys -import os -import subprocess -from difflib import unified_diff -from typing import List, BinaryIO, Tuple, Optional - -def read_blob_field(f: BinaryIO, name: bytes) -> bytes: - line = f.readline() - field = b':b ' + name + b' ' - assert line.startswith(field), field - assert line.endswith(b'\n') - size = int(line[len(field):-1]) - blob = f.read(size) - assert f.read(1) == b'\n' - return blob - -def read_int_field(f: BinaryIO, name: bytes) -> int: - line = f.readline() - field = b':i ' + name + b' ' - assert line.startswith(field) - assert line.endswith(b'\n') - return int(line[len(field):-1]) - -def write_int_field(f: BinaryIO, name: bytes, value: int): - f.write(b':i %s %d\n' % (name, value)) - -def write_blob_field(f: BinaryIO, name: bytes, blob: bytes): - f.write(b':b %s %d\n' % (name, len(blob))) - f.write(blob) - f.write(b'\n') - -def capture(shell: str) -> dict: - print(f"CAPTURING: {shell}") - process = subprocess.run(['sh', '-c', shell], capture_output = True) - return { - 'shell': shell, - 'returncode': process.returncode, - 'stdout': process.stdout, - 'stderr': process.stderr, - } - -def load_list(file_path: str) -> list[str]: - with open(file_path) as f: - return [line.strip() for line in f] - -def dump_snapshots(file_path: str, snapshots: list[dict]): - with open(file_path, "wb") as f: - write_int_field(f, b"count", len(snapshots)) - for snapshot in snapshots: - write_blob_field(f, b"shell", bytes(snapshot['shell'], 'utf-8')) - write_int_field(f, b"returncode", snapshot['returncode']) - write_blob_field(f, b"stdout", snapshot['stdout']) - write_blob_field(f, b"stderr", snapshot['stderr']) - -def load_snapshots(file_path: str) -> list[dict]: - snapshots = [] - with open(file_path, "rb") as f: - count = read_int_field(f, b"count") - for _ in range(count): - shell = read_blob_field(f, b"shell") - returncode = read_int_field(f, b"returncode") - stdout = read_blob_field(f, b"stdout") - stderr = read_blob_field(f, b"stderr") - snapshot = { - "shell": shell, - "returncode": returncode, - "stdout": stdout, - "stderr": stderr, - } - snapshots.append(snapshot) - return snapshots - -if __name__ == "__main__": - program_name, *argv = sys.argv - - if len(argv) == 0: - print(f'Usage: {program_name} ') - print('ERROR: no subcommand is provided') - exit(1) - subcommand, *argv = argv - - if subcommand == 'record': - if len(argv) == 0: - print(f'Usage: {program_name} {subcommand} ') - print('ERROR: no test.list is provided') - exit(1) - test_list_path, *argv = argv - - snapshots = [capture(shell.strip()) for shell in load_list(test_list_path)] - dump_snapshots(f'{test_list_path}.bi', snapshots) - elif subcommand == 'replay': - if len(argv) == 0: - print(f'Usage: {program_name} {subcommand} ') - print('ERROR: no test.list is provided') - exit(1) - test_list_path, *argv = argv - - shells = load_list(test_list_path) - snapshots = load_snapshots(f'{test_list_path}.bi') - - if len(shells) != len(snapshots): - print(f"UNEXPECTED: Amount of shell commands in f{test_list_path}") - print(f" EXPECTED: {len(snapshots)}") - print(f" ACTUAL: {len(shells)}") - print(f"NOTE: You may want to do `{program_name} record {test_list_path}` to update {test_list_path}.bi") - exit(1) - - for (shell, snapshot) in zip(shells, snapshots): - print(f"REPLAYING: {shell}") - snapshot_shell = snapshot['shell'].decode('utf-8') - if shell != snapshot_shell: - print(f"UNEXPECTED: shell command") - print(f" EXPECTED: {snapshot_shell}") - print(f" ACTUAL: {shell}") - print(f"NOTE: You may want to do `{program_name} record {test_list_path}` to update {test_list_path}.bi") - exit(1) - process = subprocess.run(['sh', '-c', shell], capture_output = True); - failed = False - if process.returncode != snapshot['returncode']: - print(f"UNEXPECTED: return code") - print(f" EXPECTED: {snapshot['returncode']}") - print(f" ACTUAL: {process.returncode}") - failed = True - if process.stdout != snapshot['stdout']: - # TODO: support binary outputs - a = snapshot['stdout'].decode('utf-8').splitlines(keepends=True) - b = process.stdout.decode('utf-8').splitlines(keepends=True) - print(f"UNEXPECTED: stdout") - for line in unified_diff(a, b, fromfile="expected", tofile="actual"): - print(line, end='') - failed = True - if process.stderr != snapshot['stderr']: - a = snapshot['stderr'].decode('utf-8').splitlines(keepends=True) - b = process.stderr.decode('utf-8').splitlines(keepends=True) - print(f"UNEXPECTED: stderr") - for line in unified_diff(a, b, fromfile="expected", tofile="actual"): - print(line, end='') - failed = True - if failed: - exit(1) - print('OK') - elif subcommand == "update": - if len(argv) == 0: - print(f'Usage: {program_name} {subcommand} ') - print('ERROR: no test.list is provided') - exit(1) - test_list_path, *argv = argv - update_test_list(test_list_path) - else: - print(f'ERROR: unknown subcommand {subcommand}'); - exit(1); diff --git a/test.list b/test.list deleted file mode 100644 index 99b9c2b..0000000 --- a/test.list +++ /dev/null @@ -1,8 +0,0 @@ -./target/debug/chs -./target/debug/chs help -./target/debug/chs com tests/hello.chs -./target/debug/chs com tests/infix_ops.chs -./target/debug/chs com tests/prefix_ops.chs -./target/debug/chs com tests/post_call.chs -./target/debug/chs com tests/assign.chs -./target/debug/chs com tests/records.chs