use std::{os::unix::process::CommandExt, process::ExitCode};
fn main() -> ExitCode {
let bad_usage = ExitCode::from(2);
let mut args = std::env::args().skip(1);
if args.next() != Some("-c".to_string()) {
eprintln!("Expected usage: shell -c <argument>");
return bad_usage;
}
let Some(cmd) = args.next() else {
eprintln!("Missing argument for argument '-c'");
return bad_usage;
};
if args.next().is_some() {
eprintln!("Too many arguments passed");
return bad_usage;
}
let Some(mut cmd) = parse_cmd(&cmd) else {
eprintln!("Bad command");
return bad_usage;
};
let Some(mut program) = cmd.drain(0..1).next() else {
eprintln!("Bad command");
return bad_usage;
};
if program == "git" {
let Some(subcommand) = cmd.drain(0..1).next() else {
eprintln!("Bad command");
return bad_usage;
};
program.push('-');
program.push_str(&subcommand);
}
let mut args = Vec::new();
let git_cmds = ["git-receive-pack", "git-upload-archive", "git-upload-pack"];
if git_cmds.contains(&program.as_str()) {
if cmd.len() != 1 {
eprintln!("Bad command");
return bad_usage;
}
let repository = cmd[0].trim_start_matches('/');
args.push(repository);
} else if program == "git-lfs-authenticate" {
program.clear();
program.push_str("gitolfs3-authenticate");
if cmd.len() != 2 {
eprintln!("Bad command");
return bad_usage;
}
let repository = cmd[0].trim_start_matches('/');
args.push(repository);
args.push(&cmd[1]);
} else {
eprintln!("Unknown command");
return bad_usage;
}
let e = std::process::Command::new(program).args(args).exec();
eprintln!("Error: {e}");
ExitCode::FAILURE
}
fn parse_cmd(mut cmd: &str) -> Option<Vec<String>> {
let mut args = Vec::<String>::new();
cmd = cmd.trim_matches(is_posix_space);
while !cmd.is_empty() {
if cmd.starts_with('\'') {
let (arg, remaining) = parse_sq(cmd)?;
args.push(arg);
cmd = remaining.trim_start_matches(is_posix_space);
} else if let Some((arg, remaining)) = cmd.split_once(is_posix_space) {
args.push(arg.to_owned());
cmd = remaining.trim_start_matches(is_posix_space);
} else {
args.push(cmd.to_owned());
cmd = "";
}
}
Some(args)
}
fn is_posix_space(c: char) -> bool {
// Form feed: 0x0c
// Vertical tab: 0x0b
c == ' ' || c == '\x0c' || c == '\n' || c == '\r' || c == '\t' || c == '\x0b'
}
fn parse_sq(s: &str) -> Option<(String, &str)> {
#[derive(PartialEq, Eq)]
enum SqState {
Quoted,
Unquoted { may_escape: bool },
UnquotedEscaped,
}
let mut result = String::new();
let mut state = SqState::Unquoted { may_escape: false };
let mut remaining = "";
for (i, c) in s.char_indices() {
match state {
SqState::Unquoted { may_escape: false } => {
if c != '\'' {
return None;
}
state = SqState::Quoted
}
SqState::Quoted => {
if c == '\'' {
state = SqState::Unquoted { may_escape: true };
continue;
}
result.push(c);
}
SqState::Unquoted { may_escape: true } => {
if is_posix_space(c) {
remaining = &s[i..];
break;
}
if c != '\\' {
return None;
}
state = SqState::UnquotedEscaped;
}
SqState::UnquotedEscaped => {
if c != '\\' && c != '!' {
return None;
}
result.push(c);
state = SqState::Unquoted { may_escape: false };
}
}
}
if state != (SqState::Unquoted { may_escape: true }) {
return None;
}
Some((result, remaining))
}