Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Powershell repl for linux #14

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion examples/interact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ use expectrl::repl::spawn_bash;
#[cfg(unix)]
#[cfg(not(feature = "async"))]
fn main() {
let mut bash = spawn_bash().expect("Error while spawning bash");
use expectrl::{Session, repl, spawn};
let mut bash = repl::spawn_powershell().expect("Error while spawning bash");

// let mut bash = spawn("pwsh").expect("Error while spawning bash");

println!("Now you're in interacting mode");
println!("To return control back to main type CTRL-]");

println!("ECHO IS {}", bash.get_echo().unwrap());

let status = bash.interact().expect("Failed to start interact");

println!("Quiting status {:?}", status);
Expand Down
51 changes: 49 additions & 2 deletions examples/powershell.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use expectrl::{repl::spawn_powershell, ControlCode, Regex};

#[cfg(windows)]
fn main() {
use expectrl::{repl::spawn_powershell, ControlCode, Regex};

let mut p = spawn_powershell().unwrap();

// case 1: execute
Expand Down Expand Up @@ -41,6 +41,53 @@ fn main() {
}

#[cfg(not(windows))]
#[cfg(not(feature = "async"))]
fn main() {

let mut p = spawn_powershell().unwrap();

use std::io::Write;

// case 1: execute
let hostname = p.write_all(b"hostname\r").unwrap();
p.write_all("\u{1b}[11;68R".as_bytes()).unwrap();
let hostname = p.expect("EXPECTED_PROMPT>");
let hostname = p.expect("EXPECTED_PROMPT>").unwrap().before().to_vec();
println!(
"Current hostname: {:?}",
String::from_utf8(hostname).unwrap()
);

// case 2: wait until done, only extract a few infos
p.send_line("wc /etc/passwd").unwrap();
// `exp_regex` returns both string-before-match and match itself, discard first
let lines = p.expect(Regex("[0-9]+")).unwrap();
let words = p.expect(Regex("[0-9]+")).unwrap();
let bytes = p.expect(Regex("[0-9]+")).unwrap();
p.expect_prompt().unwrap(); // go sure `wc` is really done
println!(
"/etc/passwd has {} lines, {} words, {} chars",
String::from_utf8_lossy(lines.first()),
String::from_utf8_lossy(words.first()),
String::from_utf8_lossy(bytes.first()),
);

// case 3: read while program is still executing
p.send_line("ping 8.8.8.8").unwrap(); // returns when it sees "bytes of data" in output
for _ in 0..5 {
// times out if one ping takes longer than 2s
let duration = p.expect(Regex("[0-9. ]+ ms")).unwrap();
println!(
"Roundtrip time: {}",
String::from_utf8_lossy(duration.first())
);
}

p.send_control(ControlCode::EOT).unwrap();
}

#[cfg(not(windows))]
#[cfg(feature = "async")]
fn main() {
panic!("An example doesn't supported on windows")
}
32 changes: 26 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
//! Expectrl a library for running, controlling and communicating with a process.
//! # A tool for automating terminal applications on Unix and on Windows.
//!
//! It supports `async/await`. To use it you should specify a `async` feature.
//! Using the library you can:
//!
//! # Example
//! - Spawn process
//! - Control process
//! - Interact with process's IO(input/output).
//!
//! `expectrl` like original `expect` may shine when you're working with interactive applications.
//! If your application is not interactive you may not find the library the best choise.
//!
//! ## Example
//!
//! An example for interacting via ftp.
//!
//! ```no_run,ignore
//! use expectrl::{spawn, Regex, Eof, WaitStatus};
Expand All @@ -21,14 +30,25 @@
//! p.expect(Eof).unwrap();
//! assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 0));
//! ```
//!
//! *The example inspired by the one in [philippkeller/rexpect].*
//!
//! [For more examples, check the examples directory.](https://github.com/zhiburt/expectrl/tree/main/examples)
//!
//! ## Features
//!
//! - It has an `async` support (To enable them you must turn on an `async` feature).
//! - It supports logging.
//! - It supports interact function.
//! - It has a Windows support.

mod control_code;
mod error;
mod expect;
#[cfg(feature = "log")]
mod log;
pub mod repl;
mod session;
pub mod session;
mod stream;

pub use control_code::ControlCode;
Expand All @@ -43,10 +63,10 @@ pub use conpty::ProcAttr;
pub use ptyprocess::{Signal, WaitStatus};

#[cfg(not(feature = "log"))]
pub type Session = session::Session;
pub use session::Session;

#[cfg(feature = "log")]
pub type Session = log::SessionWithLog;
pub use log::SessionWithLog as Session;

/// Spawn spawnes a new session.
///
Expand Down
2 changes: 1 addition & 1 deletion src/log.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! A wrapper of Session to log a read/write operations
//! A wrapper of [Session] to log a read/write operations

use crate::{error::Error, session::Session};
use std::{
Expand Down
55 changes: 54 additions & 1 deletion src/repl.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::ops::{Deref, DerefMut};
//! This module contains a list of special Sessions that can be spawned.

use crate::{error::Error, session::Found, Session};
use std::ops::{Deref, DerefMut};

#[cfg(unix)]
use std::process::Command;
Expand Down Expand Up @@ -107,6 +108,49 @@ pub fn spawn_powershell() -> Result<ReplSession, Error> {
Ok(powershell)
}

/// Spawn a powershell session.
#[cfg(unix)]
pub fn spawn_powershell() -> Result<ReplSession, Error> {
const DEFAULT_PROMPT: &str = "EXPECTED_PROMPT>";

let mut cmd = Command::new("pwsh");
cmd.args(&["-NoProfile", "-NonInteractive", "-NoLogo"]);

let mut powershell = ReplSession::spawn(cmd, DEFAULT_PROMPT, Some("exit"))?;
// powershell.is_echo_on = true;

// some magic value after which powershell starts working
// use std::io::Write;
// powershell.write_all("\u{1b}[12;1R".as_bytes()).unwrap();

// // https://stackoverflow.com/questions/5725888/windows-powershell-changing-the-command-prompt
// powershell
// .send(format!(
// "function prompt {{ \"{}\"; return \" \" }}",
// DEFAULT_PROMPT
// ))
// .unwrap();

// powershell.write_all(b"\r\r\r").unwrap();

// https://stackoverflow.com/questions/69063656/is-it-possible-to-stop-powershell-wrapping-output-in-ansi-sequences/69063912#69063912
// https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_ansi_terminals?view=powershell-7.2#disabling-ansi-output
// powershell
// let hostname = p.write_all(b"\r").unwrap();
// .send("[System.Environment]::SetEnvironmentVariable(\"TERM\", \"dumb\")")
// .unwrap();
// powershell.send("\r").unwrap();

// powershell
// .send("[System.Environment]::SetEnvironmentVariable(\"TERM\", \"NO_COLOR\")")
// .unwrap();
// powershell.send("\r").unwrap();

// powershell.expect_prompt().unwrap();

Ok(powershell)
}

/// A repl session: e.g. bash or the python shell:
/// you have a prompt where a user inputs commands and the shell
/// which executes them and manages IO streams.
Expand Down Expand Up @@ -222,6 +266,15 @@ impl ReplSession {
Ok(())
}

#[cfg(not(feature = "async"))]
pub fn send<S: AsRef<str>>(&mut self, line: S) -> Result<(), Error> {
self.session.send(line.as_ref())?;
if self.is_echo_on {
self.expect(line.as_ref())?;
}
Ok(())
}

/// Sends line to repl (and flush the output).
///
/// If echo_on=true wait for the input to appear.
Expand Down
5 changes: 5 additions & 0 deletions src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ impl Session {
if let Some(timeout) = self.expect_timeout {
if start.elapsed() > timeout {
self.stream.keep_in_buffer(&buf);
println!("INTERACT{:?}", String::from_utf8_lossy(&buf));
return Err(Error::ExpectTimeout);
}
}
Expand Down Expand Up @@ -346,6 +347,8 @@ impl Session {
return self.status().map_err(nix_error_to_io);
}

println!("OUTPUT {:?}", String::from_utf8_lossy(&buf[..n]));

std::io::stdout().write_all(&buf[..n])?;
std::io::stdout().flush()?;
}
Expand All @@ -361,6 +364,8 @@ impl Session {
return self.status().map_err(nix_error_to_io);
}

println!("INPUT {:?}", String::from_utf8_lossy(&buf[..n]));

for i in 0..n {
// Ctrl-]
if buf[i] == ControlCode::GroupSeparator.into() {
Expand Down