Skip to content
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
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ feat_common_core = [
"mesg",
"mountpoint",
"nologin",
"rename",
"renice",
"rev",
"setpgid",
Expand Down Expand Up @@ -116,6 +117,7 @@ mcookie = { optional = true, version = "0.0.1", package = "uu_mcookie", path = "
mesg = { optional = true, version = "0.0.1", package = "uu_mesg", path = "src/uu/mesg" }
mountpoint = { optional = true, version = "0.0.1", package = "uu_mountpoint", path = "src/uu/mountpoint" }
nologin = { optional = true, version = "0.0.1", package = "uu_nologin", path = "src/uu/nologin" }
rename = { optional = true, version = "0.0.1", package = "uu_rename", path = "src/uu/rename" }
renice = { optional = true, version = "0.0.1", package = "uu_renice", path = "src/uu/renice" }
rev = { optional = true, version = "0.0.1", package = "uu_rev", path = "src/uu/rev" }
setpgid = { optional = true, version = "0.0.1", package = "uu_setpgid", path = "src/uu/setpgid" }
Expand Down
19 changes: 19 additions & 0 deletions src/uu/rename/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "uu_rename"
version = "0.0.1"
edition = "2021"
description = "rename ~ (uutils) Rename files by substring replacement"

[lib]
path = "src/rename.rs"

[[bin]]
name = "rename"
path = "src/main.rs"

[dependencies]
clap = { workspace = true }
uucore = { workspace = true }

[target.'cfg(unix)'.dependencies]
libc = { workspace = true }
7 changes: 7 additions & 0 deletions src/uu/rename/rename.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# rename

```
rename [options] <substring> <replacement> <file>...
```

Rename files.
60 changes: 60 additions & 0 deletions src/uu/rename/src/argv.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// This file is part of the uutils util-linux package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

//! Where getopt would have stopped reading options.
//!
//! rename(1) permutes options with operands, so a flag is recognized anywhere
//! before a `--` - after all three operands, or between the substring and the
//! replacement. `POSIXLY_CORRECT` turns that off, and its mere presence does
//! it whatever the value: scanning stops at the first argument that is not an
//! option, and everything from there on is a filename however much it looks
//! like a flag.
//!
//! clap has no such concept, so rather than teach it one the argv is
//! terminated where getopt would have stopped and clap is handed the result.
use std::env;
use std::ffi::{OsStr, OsString};

const TERMINATOR: &str = "--";

pub(crate) fn collect_getopt_argv(args: impl uucore::Args) -> Vec<OsString> {
terminate_at_first_operand(args.collect(), env::var_os("POSIXLY_CORRECT").is_some())
}

/// A bare `-` is an operand and not an option, so it stops the scan like any
/// other name. `--` is longer than one unit and so answers true here, which is
/// why the caller tests for it first.
fn is_option(arg: &OsStr) -> bool {
// ASCII survives this on every platform: the encoded form keeps ASCII bytes
// as themselves, which is all a leading `-` needs.
matches!(arg.as_encoded_bytes(), [b'-', _, ..])
}

fn terminate_at_first_operand(mut argv: Vec<OsString>, posixly_correct: bool) -> Vec<OsString> {
if !posixly_correct {
return argv;
}

let mut operand_at = None;
// argv[0] is the utility's own name and is never scanned.
for (index, arg) in argv.iter().enumerate().skip(1) {
if arg == TERMINATOR {
// getopt consumes this one itself, so there is nothing to insert
// and nothing after it to protect.
return argv;
}
if !is_option(arg) {
operand_at = Some(index);
break;
}
}

if let Some(index) = operand_at {
argv.insert(index, OsString::from(TERMINATOR));
}

argv
}
95 changes: 95 additions & 0 deletions src/uu/rename/src/encoding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// This file is part of the uutils util-linux package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

//! The bridge between OsStr and the code units the engine works on.
//!
//! Both directions are lossless: bytes on unix, UTF-16 units on Windows. The
//! path separator is 0x2F in either encoding, so the scope rule needs no
//! special casing. `\` is deliberately not a separator here: rename(1)
//! documents `/`, and Rust accepts `/` on Windows too.
//!
//! Only unix and windows are covered; a third target does not build.
//!
//! Writing a name is done here rather than through `uucore::display::OsWrite`
//! because that trait refuses a Windows name that is not valid Unicode, with
//! `io::ErrorKind::InvalidData`, where a report has to print whatever the name
//! actually is - and because it has no impl for stderr.

#[cfg(unix)]
mod imp {
use std::ffi::{OsStr, OsString};
use std::io::{self, Write};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::Path;

pub(crate) type Unit = u8;
pub(crate) const SEP: Unit = b'/';

pub(crate) fn units(s: &OsStr) -> Vec<Unit> {
s.as_bytes().to_vec()
}

pub(crate) fn os_string(units: Vec<Unit>) -> OsString {
OsString::from_vec(units)
}

/// Filenames are written raw. A name holding an invalid byte is emitted as
/// that byte, which is what C util-linux does and what makes its -v output
/// round-trip through a shell.
pub(crate) fn write_os(w: &mut impl Write, s: &OsStr) -> io::Result<()> {
w.write_all(s.as_bytes())
}

pub(crate) fn symlink(target: &OsStr, link: &Path) -> io::Result<()> {
std::os::unix::fs::symlink(target, link)
}
}

#[cfg(windows)]
mod imp {
use std::ffi::{OsStr, OsString};
use std::io::{self, Write};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::path::Path;

pub(crate) type Unit = u16;
pub(crate) const SEP: Unit = b'/' as u16;

pub(crate) fn units(s: &OsStr) -> Vec<Unit> {
s.encode_wide().collect()
}

pub(crate) fn os_string(units: Vec<Unit>) -> OsString {
OsString::from_wide(&units)
}

/// There is no raw byte form of a Windows filename, so a lone surrogate is
/// replaced here. Unlike the unix arm this is lossy, and only for output.
pub(crate) fn write_os(w: &mut impl Write, s: &OsStr) -> io::Result<()> {
w.write_all(s.to_string_lossy().as_bytes())
}

/// Windows splits the call in two and needs the privilege or Developer
/// Mode to make either. Which one is right depends on what the target
/// names, and a relative target is stored relative to the LINK, so it is
/// resolved from the link's own directory - not from the process working
/// directory, which would answer about a different object and leave a link
/// Windows reports as broken. The -o guard resolves relative to the
/// process instead, because that side has C util-linux to conform to and
/// this side has none.
///
/// A target that does not resolve is treated as a file, and whatever the
/// OS then says is passed back to the caller rather than pre-empted here.
pub(crate) fn symlink(target: &OsStr, link: &Path) -> io::Result<()> {
let base = link.parent().unwrap_or_else(|| Path::new(""));
if base.join(target).is_dir() {
std::os::windows::fs::symlink_dir(target, link)
} else {
std::os::windows::fs::symlink_file(target, link)
}
}
}

pub(crate) use imp::{os_string, symlink, units, write_os, Unit, SEP};
109 changes: 109 additions & 0 deletions src/uu/rename/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// This file is part of the uutils util-linux package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

//! Per-operand diagnostics.
//!
//! Each variant keeps the path it complains about as an `OsString` and writes
//! it out as the bytes it is made of. A name that is not valid UTF-8 is exactly
//! the kind of name people reach for rename to fix, and C util-linux prints it
//! raw on stderr just as it does on stdout; rendering it through a `String`
//! would replace it with U+FFFD.
//!
//! That is also why these do not implement `Display`. `Formatter` writes
//! `&str`, so the diagnostic and a lossless name are mutually exclusive.
//!
//! The trailing " (os error NN)" that Rust appends to an io::Error is stripped
//! back off: C util-linux prints the bare strerror text and nothing else.

use std::ffi::OsString;
use std::io::{self, Write};

use uucore::error::strip_errno;

use crate::output::Output;

#[derive(Debug)]
pub(crate) enum RenameError {
NotAccessible {
path: OsString,
source: io::Error,
},

NotASymlink {
path: OsString,
},

RenameFailed {
old: OsString,
new: OsString,
source: io::Error,
},

/// The two halves of a symlink rewrite report separately, as they do for
/// C util-linux: a link in a directory that denies writes fails at the
/// unlink, and says so.
UnlinkFailed {
path: OsString,
source: io::Error,
},

/// This one names the target it tried to create, so an empty target prints
/// two spaces between "to" and "failed".
SymlinkFailed {
path: OsString,
new: OsString,
source: io::Error,
},

WriteFailed {
source: io::Error,
},
}

impl RenameError {
/// The `rename: ` prefix is written here rather than by `show_error!`,
/// which renders through `format!` and cannot carry a name losslessly.
pub(crate) fn report<W: Write>(&self, out: &mut Output<W>) {
out.write_bytes(uucore::util_name().as_bytes());
out.write_bytes(b": ");

match self {
Self::NotAccessible { path, source } => {
out.write_os(path);
out.write_bytes(b": not accessible: ");
out.write_bytes(strip_errno(source).as_bytes());
}
Self::NotASymlink { path } => {
out.write_os(path);
out.write_bytes(b": not a symbolic link");
}
Self::RenameFailed { old, new, source } => {
out.write_os(old);
out.write_bytes(b": rename to ");
out.write_os(new);
out.write_bytes(b" failed: ");
out.write_bytes(strip_errno(source).as_bytes());
}
Self::UnlinkFailed { path, source } => {
out.write_os(path);
out.write_bytes(b": unlink failed: ");
out.write_bytes(strip_errno(source).as_bytes());
}
Self::SymlinkFailed { path, new, source } => {
out.write_os(path);
out.write_bytes(b": symlinking to ");
out.write_os(new);
out.write_bytes(b" failed: ");
out.write_bytes(strip_errno(source).as_bytes());
}
Self::WriteFailed { source } => {
out.write_bytes(b"write error: ");
out.write_bytes(strip_errno(source).as_bytes());
}
}

out.write_bytes(b"\n");
}
}
1 change: 1 addition & 0 deletions src/uu/rename/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uucore::bin!(uu_rename);
Loading
Loading