urlendec 0.1.0

A command-line url encoder/decoder
use anyhow::{Context, Result};
use clap::Parser;
use log::debug;
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use urlencoding;

enum InputType {
    File,
    String,
    Stdio,
}

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[arg(short, long, default_value = "false")]
    decode: bool,

    #[arg(short = 's', long = "string", default_value = "", group = "input")]
    input_string: String,

    #[arg(short, long, default_value = "-", group = "input")]
    input_file: String,

    #[arg(short, long, default_value = "-")]
    output_file: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    env_logger::init();

    let cli = Cli::parse();

    debug!(
        "input arguments: decode='{}'  input_string='{}'  input_file='{}'  output_file='{}'",
        cli.decode, cli.input_string, cli.input_file, cli.output_file
    );

    let reader = create_reader(&cli)?;
    let mut writer = create_writer(&cli)?;

    for line in reader.lines() {
        let output = encode_decode(&cli, &line?)?;

        writer.write_fmt(format_args!("{}\n", output))?;
    }
    writer.flush()?;

    Ok(())
}

fn encode_decode(cli: &Cli, input: &String) -> Result<String> {
    Ok(if cli.decode {
        debug!("decode input: '{}'", input);
        urlencoding::decode(&input)?.to_string()
    } else {
        debug!("encode input: '{}'", input);
        urlencoding::encode(&input).to_string()
    })
}

fn create_writer(cli: &Cli) -> Result<Box<dyn Write>> {
    debug!("creating writer for output file '{}'", cli.output_file);
    let writer: Box<dyn Write>;

    if cli.output_file == "-" {
        writer = Box::new(BufWriter::new(io::stdout().lock()));
    } else {
        writer = Box::new(BufWriter::new(
            OpenOptions::new()
                .create(true)
                .write(true)
                .truncate(true)
                .open(&cli.output_file)
                .with_context(|| format!("could not create output file `{}`", cli.output_file))?,
        ));
    }
    Ok(writer)
}

fn create_reader(cli: &Cli) -> Result<Box<dyn BufRead + '_>> {
    Ok(match get_input_type(&cli) {
        InputType::File => Box::new(BufReader::new(
            File::open(&cli.input_file)
                .with_context(|| format!("could not read file `{}`", cli.input_file))?,
        )),
        InputType::Stdio => Box::new(BufReader::new(io::stdin().lock())),
        InputType::String => Box::new(BufReader::new(cli.input_string.as_bytes())),
    })
}

fn get_input_type(cli: &Cli) -> InputType {
    if cli.input_string.is_empty() {
        if cli.input_file == "-" {
            InputType::Stdio
        } else {
            InputType::File
        }
    } else {
        InputType::String
    }
}