Skip to content

Commit 0a7bac1

Browse files
bors[bot]azdavismatklad
authored
Merge #16
16: Add mktemp_d r=azdavis a=azdavis - Use POSIX date invocations, don't use `--iso` option - This might have been part of what was making tests fail on macOS - I ran `cargo t` on my macOS machine and tests failed because macOS's default `date` doesn't have the `--iso` option - Add changelog entry for 0.1.7, 0.1.8 retroactively - Fix cp doc - It acts like shell's `cp` when `dst` is a directory - Allow acquiring many read locks on the same thread - Update the thread-local cache to track readers as well as the writer - Doc and test internal `gsl` more, note some caveats - Add `mktemp_d`, doc it - Update version and release notes - Ignore `target` files in vscode Closes #12 , can also probably close #7 after this. Co-authored-by: Ariel Davis <[email protected]> Co-authored-by: Aleksey Kladov <[email protected]>
2 parents e3a0ea7 + 3042919 commit 0a7bac1

12 files changed

Lines changed: 231 additions & 34 deletions

File tree

.vscode/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"files.exclude": {
3+
"**/target": true
4+
}
5+
}

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# Changelog
22

3+
## 0.1.9
4+
5+
- `mktemp_d` creates an (insecure, world readable) temporary directory.
6+
- Fix cp docs
7+
8+
## 0.1.8
9+
10+
- Add option to not echo command at all
11+
- Add option to censor command contents when echoing
12+
- Add docs
13+
14+
## 0.1.7
15+
16+
- `cp(foo, bar)` copies `foo` _into_ `bar`, if `bar` is an existing directory.
17+
- Tweak reading API
18+
319
## 0.1.6
420

521
- `.read()` chomps `\r\n` on Windows.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "xshell"
33
description = "Utilities for quick shell scripting in Rust"
44
categories = ["development-tools::build-utils", "filesystem"]
5-
version = "0.1.8"
5+
version = "0.1.9"
66
license = "MIT OR Apache-2.0"
77
repository = "https://github.com/matklad/xshell"
88
authors = ["Aleksey Kladov <[email protected]>"]
@@ -13,4 +13,4 @@ exclude = [".github/", "bors.toml", "rustfmt.toml", "cbench", "mock_bin/"]
1313
[workspace]
1414

1515
[dependencies]
16-
xshell-macros = { version = "=0.1.8", path = "./xshell-macros"}
16+
xshell-macros = { version = "=0.1.9", path = "./xshell-macros" }

cbench/baseline/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::process::Command;
22

33
fn main() {
44
let mut cmd = Command::new("date");
5-
cmd.arg("--iso");
5+
cmd.arg("+%Y-%m-%d");
66
let output = cmd.output().unwrap();
77
if !output.status.success() {
88
panic!("command `{:?}` failed: {}", cmd, output.status);

cbench/ducted/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use duct::cmd;
22

33
fn main() {
4-
let date = cmd!("date", "--iso").read().unwrap();
4+
let date = cmd!("date", "+%Y-%m-%d").read().unwrap();
55
print!("today is: {}\n", date)
66
}

cbench/xshelled/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use xshell::cmd;
22

33
fn main() {
4-
let date = cmd!("date --iso").read().unwrap();
4+
let date = cmd!("date +%Y-%m-%d").read().unwrap();
55
print!("today is: {}\n", date)
66
}

mock_bin/date.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ fn main() {
99

1010
fn try_main() -> io::Result<()> {
1111
let args = std::env::args().collect::<Vec<_>>();
12-
if args != ["date", "--iso"] {
12+
if args != ["date", "+%Y-%m-%d"] {
1313
return Err(io::Error::new(io::ErrorKind::Other, "invalid args"));
1414
}
1515
println!("1982-06-25");

src/fs.rs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use std::path::{Path, PathBuf};
1+
use std::{
2+
path::{Path, PathBuf},
3+
sync::atomic::{AtomicUsize, Ordering},
4+
};
25

36
use crate::{error::fs_err, gsl, Result};
47

@@ -46,9 +49,14 @@ fn _mkdir_p(path: &Path) -> Result<()> {
4649
with_path(path, std::fs::create_dir_all(path))
4750
}
4851

49-
/// Copies the file at `src` into the file at `dst`.
52+
/// Copies `src` into `dst`.
5053
///
51-
/// `dst` must be the path to a file. It will be created if it does not exist.
54+
/// `src` must be a file, but `dst` need not be. If `dst` is an existing
55+
/// directory, `src` will be copied into a file in the `dst` directory whose
56+
/// name is same as that of `src`.
57+
///
58+
/// Otherwise, `dst` is a file or does not exist, and `src` will be copied into
59+
/// it.
5260
pub fn cp(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
5361
_cp(src.as_ref(), dst.as_ref())
5462
}
@@ -81,6 +89,30 @@ pub fn cwd() -> Result<PathBuf> {
8189
with_path(&Path::new("."), std::env::current_dir())
8290
}
8391

92+
/// Creates an empty, world-readable, temporary directory.
93+
///
94+
/// Returns a [`TempDir`] value that provides the path of this temporary
95+
/// directory. When dropped, the temporary directory and all of its contents
96+
/// will be removed.
97+
pub fn mktemp_d() -> Result<TempDir> {
98+
let _guard = gsl::read();
99+
let base = std::env::temp_dir();
100+
mkdir_p(&base)?;
101+
102+
static CNT: AtomicUsize = AtomicUsize::new(0);
103+
104+
let mut n_try = 0u32;
105+
loop {
106+
let cnt = CNT.fetch_add(1, Ordering::Relaxed);
107+
let path = base.join(format!("xshell-tmp-dir-{}", cnt));
108+
match std::fs::create_dir(&path) {
109+
Ok(()) => return Ok(TempDir { path }),
110+
Err(io_err) if n_try == 1024 => return Err(fs_err(path, io_err)),
111+
Err(_) => n_try += 1,
112+
}
113+
}
114+
}
115+
84116
fn with_path<T>(path: &Path, res: Result<T, std::io::Error>) -> Result<T> {
85117
res.map_err(|io_err| fs_err(path.to_path_buf(), io_err))
86118
}
@@ -110,3 +142,22 @@ fn read_dir_aux(path: &Path) -> std::io::Result<Vec<PathBuf>> {
110142
res.sort();
111143
Ok(res)
112144
}
145+
146+
/// A temporary directory.
147+
#[derive(Debug)]
148+
pub struct TempDir {
149+
path: PathBuf,
150+
}
151+
152+
impl TempDir {
153+
/// Returns the path of this temporary directory.
154+
pub fn path(&self) -> &Path {
155+
&self.path
156+
}
157+
}
158+
159+
impl Drop for TempDir {
160+
fn drop(&mut self) {
161+
let _ = rm_rf(&self.path);
162+
}
163+
}

src/gsl.rs

Lines changed: 122 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
//! Global shell lock
1+
//! Global shell lock.
2+
23
use std::{
34
cell::Cell,
45
mem::MaybeUninit,
@@ -7,29 +8,77 @@ use std::{
78
sync::{RwLock, RwLockReadGuard, RwLockWriteGuard},
89
};
910

11+
/// If, on the same thread, there are multiple calls to [`read`] or [`write`],
12+
/// then the `Guard`s returned should be dropped in the reverse order that they
13+
/// were acquired.
14+
///
15+
/// If this is violated, e.g. in
16+
///
17+
/// ```ignore
18+
/// let w1 = write();
19+
/// let _w2 = write();
20+
/// drop(w1);
21+
/// // try to use a global resource with write privileges
22+
/// ```
23+
///
24+
/// then things won't turn out well, because `_w2` doesn't actually contain a
25+
/// lock guard.
26+
#[derive(Debug)]
27+
pub(crate) struct Guard(Option<Repr>);
28+
1029
#[derive(Debug)]
11-
pub(crate) struct Guard {
12-
r_guard: Option<RwLockReadGuard<'static, ()>>,
13-
w_guard: Option<RwLockWriteGuard<'static, ()>>,
30+
enum Repr {
31+
Read(RwLockReadGuard<'static, ()>),
32+
Write(RwLockWriteGuard<'static, ()>),
1433
}
1534

35+
/// Returns a [`Guard`] for write access to global resources.
1636
pub(crate) fn write() -> Guard {
17-
if LOCKED.with(|it| it.get()) {
18-
return Guard { r_guard: None, w_guard: None };
37+
match CACHE.with(Cell::get) {
38+
Cache::Write => {
39+
// this thread (and only this thread) can already write. don't try to
40+
// acquire another write guard.
41+
Guard(None)
42+
}
43+
Cache::Read(readers) => {
44+
assert_eq!(
45+
readers, 0,
46+
"calling write() with an active read guard on the same thread would deadlock"
47+
);
48+
let w_guard = static_rw_lock().write().unwrap_or_else(|err| err.into_inner());
49+
// note that we have a writer.
50+
CACHE.with(|it| it.set(Cache::Write));
51+
Guard(Some(Repr::Write(w_guard)))
52+
}
1953
}
20-
21-
let w_guard = static_rw_lock().write().unwrap_or_else(|err| err.into_inner());
22-
LOCKED.with(|it| it.set(true));
23-
Guard { w_guard: Some(w_guard), r_guard: None }
2454
}
2555

56+
/// Returns a [`Guard`] for read access to global resources.
2657
pub(crate) fn read() -> Guard {
27-
if LOCKED.with(|it| it.get()) {
28-
return Guard { r_guard: None, w_guard: None };
58+
match CACHE.with(Cell::get) {
59+
Cache::Write => {
60+
// this thread (and only this thread) can already write. it's safe
61+
// to allow this thread to read as well, because we won't have
62+
// concurrent reads and writes, because we're only working on this
63+
// thread.
64+
Guard(None)
65+
}
66+
Cache::Read(readers) => {
67+
if readers == 0 {
68+
// this thread has no readers or writers. try to acquire the
69+
// lock for reading.
70+
let r_guard = static_rw_lock().read().unwrap_or_else(|err| err.into_inner());
71+
// note that we now have 1 reader.
72+
CACHE.with(|it| it.set(Cache::Read(1)));
73+
Guard(Some(Repr::Read(r_guard)))
74+
} else {
75+
// this thread can already read. don't try to acquire another
76+
// read guard. also, note that we have another reader.
77+
CACHE.with(|it| it.set(Cache::Read(readers + 1)));
78+
Guard(None)
79+
}
80+
}
2981
}
30-
31-
let r_guard = static_rw_lock().read().unwrap_or_else(|err| err.into_inner());
32-
Guard { w_guard: None, r_guard: Some(r_guard) }
3382
}
3483

3584
fn static_rw_lock() -> &'static RwLock<()> {
@@ -41,15 +90,69 @@ fn static_rw_lock() -> &'static RwLock<()> {
4190
}
4291
}
4392

93+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94+
enum Cache {
95+
Read(usize),
96+
Write,
97+
}
98+
4499
thread_local! {
45-
static LOCKED: Cell<bool> = Cell::new(false);
100+
static CACHE: Cell<Cache> = Cell::new(Cache::Read(0));
46101
}
47102

48103
impl Drop for Guard {
49104
fn drop(&mut self) {
50-
if self.w_guard.is_some() {
51-
LOCKED.with(|it| it.set(false))
105+
match self.0 {
106+
Some(Repr::Read(_)) => CACHE.with(|it| {
107+
let n = match it.get() {
108+
Cache::Read(n) => n,
109+
Cache::Write => unreachable!("had both a reader and a writer"),
110+
};
111+
it.set(Cache::Read(n - 1));
112+
}),
113+
Some(Repr::Write(_)) => CACHE.with(|it| {
114+
assert_eq!(it.get(), Cache::Write);
115+
it.set(Cache::Read(0));
116+
}),
117+
None => {}
52118
}
53-
let _ = self.r_guard;
54119
}
55120
}
121+
122+
#[test]
123+
fn read_write_read() {
124+
eprintln!("get r1");
125+
let r1 = read();
126+
eprintln!("got r1");
127+
let h = std::thread::spawn(|| {
128+
eprintln!("get w1");
129+
let w1 = write();
130+
eprintln!("got w1");
131+
drop(w1);
132+
eprintln!("gave w1");
133+
});
134+
std::thread::sleep(std::time::Duration::from_millis(300));
135+
eprintln!("get r2");
136+
let r2 = read();
137+
eprintln!("got r2");
138+
drop(r1);
139+
eprintln!("gave r1");
140+
drop(r2);
141+
eprintln!("gave r2");
142+
h.join().unwrap();
143+
}
144+
145+
#[test]
146+
fn write_read() {
147+
let _w = write();
148+
let _r = read();
149+
}
150+
151+
#[test]
152+
#[should_panic(
153+
expected = "calling write() with an active read guard on the same thread would deadlock"
154+
)]
155+
fn read_write() {
156+
let _r = read();
157+
let _w = write();
158+
}

src/lib.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
//!
4343
//! ```
4444
//! # use xshell::cmd;
45-
//! let output = cmd!("date --iso").read()?;
45+
//! let output = cmd!("date +%Y-%m-%d").read()?;
4646
//! assert!(output.chars().all(|c| "01234567890-".contains(c)));
4747
//! # Ok::<(), xshell::Error>(())
4848
//! ```
@@ -177,7 +177,7 @@
177177
//!
178178
//! # Implementation details
179179
//!
180-
//! The design is heavily inspired by the Juila language:
180+
//! The design is heavily inspired by the Julia language:
181181
//!
182182
//! * [Shelling Out Sucks](https://julialang.org/blog/2012/03/shelling-out-sucks/)
183183
//! * [Put This In Your Pipe](https://julialang.org/blog/2013/04/put-this-in-your-pipe/)
@@ -190,7 +190,7 @@
190190
//!
191191
//! The `cmd!` macro uses a simple proc-macro internally. It doesn't depend on
192192
//! helper libraries, so the fixed-cost impact on compile times is moderate.
193-
//! Compiling a trivial program with `cmd!("date --iso")` takes one second.
193+
//! Compiling a trivial program with `cmd!("date +%Y-%m-%d")` takes one second.
194194
//! Equivalent program using only `std::process::Command` compiles in 0.25
195195
//! seconds.
196196
//!
@@ -261,7 +261,7 @@ pub use xshell_macros::__cmd;
261261
pub use crate::{
262262
env::{pushd, pushenv, Pushd, Pushenv},
263263
error::{Error, Result},
264-
fs::{cp, cwd, mkdir_p, read_dir, read_file, rm_rf, write_file},
264+
fs::{cp, cwd, mkdir_p, mktemp_d, read_dir, read_file, rm_rf, write_file, TempDir},
265265
};
266266

267267
/// Constructs a [`Cmd`] from the given string.

0 commit comments

Comments
 (0)