7
\$\begingroup\$

I rewrote echo from the manpage to learn Rust, and got the following:

// echo in rust - display a line of text
use std::env;

static HELP: &'static str = "\
echo the STRING(s) to standard output.\n\
-n     do not output the trailing newline\n\
-e     enable intepretation of backslash escapes\n\
-E     disable intepretation of backslash escapes (default)\n\
--help display this help and exit

If -e is in effect, the following sequence are recognized:

  \\\\   backslash
  \\a   alert (BEL)
  \\b   backspace
  \\c   produce no futher output
  \\e   escape
  \\f   form feed
  \\n   new line
  \\r   carriage return
  \\t   horizontal tab
  \\v   vertical tab";

fn main() {
    let mut args: Vec<String> = env::args().skip(1).collect();

    let mut nl: bool = true; // the "-n" flag can be used to indicate no new line
    let mut escape: bool = false; // Disable interpreation of backslashes
    let mut args_count: usize = 0; // first argument is binary name

    for stuff in &args {
        if !stuff.starts_with("-") {
            break;
        }
        if stuff == "-n" {
            nl = false;
            args_count += 1;
        }
        if stuff == "-e" {
            escape = true;
            args_count += 1;
        }
        if stuff == "-E" {
            escape = false;
            args_count += 1;
        }
        if stuff == "--help" {
            println!("{}", HELP);
            return;
        }
    }

    if escape {
        for stuff in &mut args[args_count..] {
            *stuff = stuff.replace("\\\\", "\\"); // backslashe
            *stuff = stuff.replace("\\a", ""); // alert (BEL)
            *stuff = stuff.replace("\\b", ""); // backspace
            *stuff = stuff.replace("\\c", ""); // produce no further output
            *stuff = stuff.replace("\\e", ""); // escape
            *stuff = stuff.replace("\\f", ""); // form feed
            *stuff = stuff.replace("\\n", ""); // new line
            *stuff = stuff.replace("\\r", ""); // carriage return
            *stuff = stuff.replace("\\t", ""); // horizontal tab
            *stuff = stuff.replace("\\v", ""); // vertical tab
        }
    }

    print!("{}", args[args_count..].join(" "));
    if nl {
        println!();
    }
}

I then had a look at previously asked questions on this topic, and learnt of skip from here.

I would particularly welcome feedback on string manipulation, notably on the argument parsing (i.e. here, I expect known flags before data to echo), and the if escape block.

Could the argument parsing be converted to a match statement, whilst retaining the arg_count increment and the break condition?

Another thing I'm unsure of is that I'm first collecting args and then checking each element within the vector and eventually joining it, using a bunch of for loops.

Is there a more idiomatic way to do so, such as vector subtraction or...?

Lastly, my manpage for echo mentions support for decoding bytes:

\0NNN  byte with octal value NNN (1 to 3 digits)
\xHH   byte with hexadecimal value HH (1 to 2 digits)

Would you have pointers on how to do this type of operation?

\$\endgroup\$
2
  • 1
    \$\begingroup\$ As described in Rust Echo Command Implementation, you really should not collect all of the input as a vector as it's very wasteful. \$\endgroup\$
    – Shepmaster
    Oct 9, 2019 at 14:14
  • 1
    \$\begingroup\$ @Shepmaster That can even make the program crash in case of infinite input. \$\endgroup\$
    – Boiethios
    Oct 9, 2019 at 14:17

1 Answer 1

2
\$\begingroup\$

Thanks to @Shepmaster's comment referring to one of his previous answers, I was able to make the process nicer, by using env::args as an Iterator.

By doing so, it became much easier to switch to using a match statement, and avoids all the string concatenation I mentioned in my question.

I've also managed to add the conversion from hex and octal values to chars by making use of std::u8::from_str_radix in conjunction with std::string::String::trim_start_matches

I therefore now have the following:

// echo in rust - display a line of text
use std::env;
use std::u8;

static HELP: &'static str = "\
echo the STRING(s) to standard output.\n\
-n     do not output the trailing newline\n\
-e     enable intepretation of backslash escapes\n\
-E     disable intepretation of backslash escapes (default)\n\
--help display this help and exit

If -e is in effect, the following sequence are recognized:

  \\\\    backslash
  \\a    alert (BEL)
  \\b    backspace
  \\c    produce no futher output
  \\e    escape
  \\f    form feed
  \\n    new line
  \\r    carriage return
  \\t    horizontal tab
  \\v    vertical tab
  \\0NNN byte with octal value NNN (1 to 3 digits)
  \\xHH  byte with hexadecimal value HH (1 to 2 digits)
";

fn main() {
    let mut nl: bool = true; // the "-n" flag can be used to indicate no new line
    let mut escape: bool = false; // Disable interpretation of backslashes
    let mut args_done = false; // Argument parsing done
    let mut first = true; // Correct the printing spacing

    for arg in env::args().skip(1) {
        if !args_done {
            match arg.as_ref() {
                "-n" => nl = false,
                "-e" => escape = true,
                "-E" => escape = false,
                "--help" => print!("{}", HELP),
                _ => args_done = true,
            }
        }

        if args_done {
            let mut datum: String = arg;
            if escape {
                datum = datum.replace("\\\\", "\\"); // backslash
                datum = datum.replace("\\a", ""); // alert (BEL)
                datum = datum.replace("\\b", ""); // backspace
                datum = datum.replace("\\c", ""); // produce no further output
                datum = datum.replace("\\e", ""); // escape
                datum = datum.replace("\\f", ""); // form feed
                datum = datum.replace("\\n", ""); // new line
                datum = datum.replace("\\r", ""); // carriage return
                datum = datum.replace("\\t", ""); // horizontal tab
                datum = datum.replace("\\v", ""); // vertical tab

                if datum.starts_with("\\x") && 3 <= datum.len() && datum.len() <= 4 {
                    // Hex values with at most 2 digits
                    let value = datum.trim_start_matches("\\x");
                    let chr = u8::from_str_radix(value, 16).unwrap() as char;
                    datum = chr.to_string();
                }

                if datum.starts_with("\\0") && 3 <= datum.len() && datum.len() <= 5 {
                    // Octal values with at most 3 digits
                    let value = datum.trim_start_matches("\\0");

                    let chr = u8::from_str_radix(value, 8);
                    // The maximum octal value for a byte is 377.
                    // Check that this conversion was successful.
                    if chr.is_ok() {
                        let x = chr.unwrap() as char;
                        datum = x.to_string();
                    }
                }
            }
            if first {
                print!("{}", datum);
                first = false;
            } else {
                print!(" {}", datum);
            }
        }
    }
    if nl {
        println!();
    }
}

This now adds an extra first flag for correct formatting, but it's okay for what is done here.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.