sort/ls: implement version cmp matching GNU spec

This reimplements version_cmp, which is used in sort and ls to sort
according to versions.
However, it is not bug-for-bug identical with GNU's implementation.
I reported a bug with GNU here:
https://lists.gnu.org/archive/html/bug-coreutils/2021-06/msg00045.html
This implementation does not contain the bugs regarding the handling of
file extensions and null bytes.
This commit is contained in:
Michael Debertol 2021-06-27 12:52:46 +02:00
parent 5fce7ecbaa
commit 233a778963
12 changed files with 423 additions and 360 deletions

16
Cargo.lock generated
View file

@ -1434,21 +1434,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "semver"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver-parser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "sha1"
version = "0.6.0"
@ -2488,7 +2473,6 @@ dependencies = [
"ouroboros",
"rand 0.7.3",
"rayon",
"semver",
"tempfile",
"unicode-width",
"uucore",

View file

@ -14,7 +14,6 @@ extern crate uucore;
extern crate lazy_static;
mod quoting_style;
mod version_cmp;
use clap::{crate_version, App, Arg};
use globset::{self, Glob, GlobSet, GlobSetBuilder};
@ -44,6 +43,7 @@ use term_grid::{Cell, Direction, Filling, Grid, GridOptions};
use unicode_width::UnicodeWidthStr;
#[cfg(unix)]
use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR};
use uucore::{fs::display_permissions, version_cmp::version_cmp};
static ABOUT: &str = "
By default, ls will list the files and contents of any directories on
@ -1256,7 +1256,8 @@ fn sort_entries(entries: &mut Vec<PathData>, config: &Config) {
}
// The default sort in GNU ls is case insensitive
Sort::Name => entries.sort_by(|a, b| a.display_name.cmp(&b.display_name)),
Sort::Version => entries.sort_by(|a, b| version_cmp::version_cmp(&a.p_buf, &b.p_buf)),
Sort::Version => entries
.sort_by(|a, b| version_cmp(&a.p_buf.to_string_lossy(), &b.p_buf.to_string_lossy())),
Sort::Extension => entries.sort_by(|a, b| {
a.p_buf
.extension()
@ -1467,8 +1468,6 @@ fn display_grid(
}
}
use uucore::fs::display_permissions;
fn display_item_long(
item: &PathData,
max_links: usize,

View file

@ -1,306 +0,0 @@
use std::cmp::Ordering;
use std::path::Path;
/// Compare paths in a way that matches the GNU version sort, meaning that
/// numbers get sorted in a natural way.
pub(crate) fn version_cmp(a: &Path, b: &Path) -> Ordering {
let a_string = a.to_string_lossy();
let b_string = b.to_string_lossy();
let mut a = a_string.chars().peekable();
let mut b = b_string.chars().peekable();
// The order determined from the number of leading zeroes.
// This is used if the filenames are equivalent up to leading zeroes.
let mut leading_zeroes = Ordering::Equal;
loop {
match (a.next(), b.next()) {
// If the characters are both numerical. We collect the rest of the number
// and parse them to u64's and compare them.
(Some(a_char @ '0'..='9'), Some(b_char @ '0'..='9')) => {
let mut a_leading_zeroes = 0;
if a_char == '0' {
a_leading_zeroes = 1;
while let Some('0') = a.peek() {
a_leading_zeroes += 1;
a.next();
}
}
let mut b_leading_zeroes = 0;
if b_char == '0' {
b_leading_zeroes = 1;
while let Some('0') = b.peek() {
b_leading_zeroes += 1;
b.next();
}
}
// The first different number of leading zeros determines the order
// so if it's already been determined by a previous number, we leave
// it as that ordering.
// It's b.cmp(&a), because the *largest* number of leading zeros
// should go first
if leading_zeroes == Ordering::Equal {
leading_zeroes = b_leading_zeroes.cmp(&a_leading_zeroes);
}
let mut a_str = String::new();
let mut b_str = String::new();
if a_char != '0' {
a_str.push(a_char);
}
if b_char != '0' {
b_str.push(b_char);
}
// Unwrapping here is fine because we only call next if peek returns
// Some(_), so next should also return Some(_).
while let Some('0'..='9') = a.peek() {
a_str.push(a.next().unwrap());
}
while let Some('0'..='9') = b.peek() {
b_str.push(b.next().unwrap());
}
// Since the leading zeroes are stripped, the length can be
// used to compare the numbers.
match a_str.len().cmp(&b_str.len()) {
Ordering::Equal => {}
x => return x,
}
// At this point, leading zeroes are stripped and the lengths
// are equal, meaning that the strings can be compared using
// the standard compare function.
match a_str.cmp(&b_str) {
Ordering::Equal => {}
x => return x,
}
}
// If there are two characters we just compare the characters
(Some(a_char), Some(b_char)) => match a_char.cmp(&b_char) {
Ordering::Equal => {}
x => return x,
},
// Otherwise, we compare the options (because None < Some(_))
(a_opt, b_opt) => match a_opt.cmp(&b_opt) {
// If they are completely equal except for leading zeroes, we use the leading zeroes.
Ordering::Equal => return leading_zeroes,
x => return x,
},
}
}
}
#[cfg(test)]
mod tests {
use crate::version_cmp::version_cmp;
use std::cmp::Ordering;
use std::path::PathBuf;
#[test]
fn test_version_cmp() {
// Identical strings
assert_eq!(
version_cmp(&PathBuf::from("hello"), &PathBuf::from("hello")),
Ordering::Equal
);
assert_eq!(
version_cmp(&PathBuf::from("file12"), &PathBuf::from("file12")),
Ordering::Equal
);
assert_eq!(
version_cmp(
&PathBuf::from("file12-suffix"),
&PathBuf::from("file12-suffix")
),
Ordering::Equal
);
assert_eq!(
version_cmp(
&PathBuf::from("file12-suffix24"),
&PathBuf::from("file12-suffix24")
),
Ordering::Equal
);
// Shortened names
assert_eq!(
version_cmp(&PathBuf::from("world"), &PathBuf::from("wo")),
Ordering::Greater,
);
assert_eq!(
version_cmp(&PathBuf::from("hello10wo"), &PathBuf::from("hello10world")),
Ordering::Less,
);
// Simple names
assert_eq!(
version_cmp(&PathBuf::from("world"), &PathBuf::from("hello")),
Ordering::Greater,
);
assert_eq!(
version_cmp(&PathBuf::from("hello"), &PathBuf::from("world")),
Ordering::Less
);
assert_eq!(
version_cmp(&PathBuf::from("apple"), &PathBuf::from("ant")),
Ordering::Greater
);
assert_eq!(
version_cmp(&PathBuf::from("ant"), &PathBuf::from("apple")),
Ordering::Less
);
// Uppercase letters
assert_eq!(
version_cmp(&PathBuf::from("Beef"), &PathBuf::from("apple")),
Ordering::Less,
"Uppercase letters are sorted before all lowercase letters"
);
assert_eq!(
version_cmp(&PathBuf::from("Apple"), &PathBuf::from("apple")),
Ordering::Less
);
assert_eq!(
version_cmp(&PathBuf::from("apple"), &PathBuf::from("aPple")),
Ordering::Greater
);
// Numbers
assert_eq!(
version_cmp(&PathBuf::from("100"), &PathBuf::from("20")),
Ordering::Greater,
"Greater numbers are greater even if they start with a smaller digit",
);
assert_eq!(
version_cmp(&PathBuf::from("20"), &PathBuf::from("20")),
Ordering::Equal,
"Equal numbers are equal"
);
assert_eq!(
version_cmp(&PathBuf::from("15"), &PathBuf::from("200")),
Ordering::Less,
"Small numbers are smaller"
);
// Comparing numbers with other characters
assert_eq!(
version_cmp(&PathBuf::from("1000"), &PathBuf::from("apple")),
Ordering::Less,
"Numbers are sorted before other characters"
);
assert_eq!(
// spell-checker:disable-next-line
version_cmp(&PathBuf::from("file1000"), &PathBuf::from("fileapple")),
Ordering::Less,
"Numbers in the middle of the name are sorted before other characters"
);
// Leading zeroes
assert_eq!(
version_cmp(&PathBuf::from("012"), &PathBuf::from("12")),
Ordering::Less,
"A single leading zero can make a difference"
);
assert_eq!(
version_cmp(&PathBuf::from("000800"), &PathBuf::from("0000800")),
Ordering::Greater,
"Leading number of zeroes is used even if both non-zero number of zeros"
);
// Numbers and other characters combined
assert_eq!(
version_cmp(&PathBuf::from("ab10"), &PathBuf::from("aa11")),
Ordering::Greater
);
assert_eq!(
version_cmp(&PathBuf::from("aa10"), &PathBuf::from("aa11")),
Ordering::Less,
"Numbers after other characters are handled correctly."
);
assert_eq!(
version_cmp(&PathBuf::from("aa2"), &PathBuf::from("aa100")),
Ordering::Less,
"Numbers after alphabetical characters are handled correctly."
);
assert_eq!(
version_cmp(&PathBuf::from("aa10bb"), &PathBuf::from("aa11aa")),
Ordering::Less,
"Number is used even if alphabetical characters after it differ."
);
assert_eq!(
version_cmp(&PathBuf::from("aa10aa0010"), &PathBuf::from("aa11aa1")),
Ordering::Less,
"Second number is ignored if the first number differs."
);
assert_eq!(
version_cmp(&PathBuf::from("aa10aa0010"), &PathBuf::from("aa10aa1")),
Ordering::Greater,
"Second number is used if the rest is equal."
);
assert_eq!(
version_cmp(&PathBuf::from("aa10aa0010"), &PathBuf::from("aa00010aa1")),
Ordering::Greater,
"Second number is used if the rest is equal up to leading zeroes of the first number."
);
assert_eq!(
version_cmp(&PathBuf::from("aa10aa0022"), &PathBuf::from("aa010aa022")),
Ordering::Greater,
"The leading zeroes of the first number has priority."
);
assert_eq!(
version_cmp(&PathBuf::from("aa10aa0022"), &PathBuf::from("aa10aa022")),
Ordering::Less,
"The leading zeroes of other numbers than the first are used."
);
assert_eq!(
version_cmp(&PathBuf::from("file-1.4"), &PathBuf::from("file-1.13")),
Ordering::Less,
"Periods are handled as normal text, not as a decimal point."
);
// Greater than u64::Max
// u64 == 18446744073709551615 so this should be plenty:
// 20000000000000000000000
assert_eq!(
version_cmp(
&PathBuf::from("aa2000000000000000000000bb"),
&PathBuf::from("aa002000000000000000000001bb")
),
Ordering::Less,
"Numbers larger than u64::MAX are handled correctly without crashing"
);
assert_eq!(
version_cmp(
&PathBuf::from("aa2000000000000000000000bb"),
&PathBuf::from("aa002000000000000000000000bb")
),
Ordering::Greater,
"Leading zeroes for numbers larger than u64::MAX are handled correctly without crashing"
);
}
}

View file

@ -24,7 +24,6 @@ memchr = "2.4.0"
ouroboros = "0.9.3"
rand = "0.7"
rayon = "1.5"
semver = "0.9.0"
tempfile = "3"
unicode-width = "0.1.8"
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["fs"] }

View file

@ -32,7 +32,6 @@ use numeric_str_cmp::{human_numeric_str_cmp, numeric_str_cmp, NumInfo, NumInfoPa
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use rayon::prelude::*;
use semver::Version;
use std::cmp::Ordering;
use std::env;
use std::ffi::OsStr;
@ -44,6 +43,7 @@ use std::path::Path;
use std::path::PathBuf;
use unicode_width::UnicodeWidthStr;
use uucore::parse_size::{parse_size, ParseSizeError};
use uucore::version_cmp::version_cmp;
use uucore::InvalidEncodingHandling;
const NAME: &str = "sort";
@ -1410,7 +1410,7 @@ fn compare_by<'a>(
general_numeric_compare(a_float, b_float)
}
SortMode::Month => month_compare(a_str, b_str),
SortMode::Version => version_compare(a_str, b_str),
SortMode::Version => version_cmp(a_str, b_str),
SortMode::Default => custom_str_cmp(
a_str,
b_str,
@ -1615,31 +1615,6 @@ fn month_compare(a: &str, b: &str) -> Ordering {
}
}
fn version_parse(a: &str) -> Version {
let result = Version::parse(a);
match result {
Ok(vers_a) => vers_a,
// Non-version lines parse to 0.0.0
Err(_e) => Version::parse("0.0.0").unwrap(),
}
}
fn version_compare(a: &str, b: &str) -> Ordering {
#![allow(clippy::comparison_chain)]
let ver_a = version_parse(a);
let ver_b = version_parse(b);
// Version::cmp is not implemented; implement comparison directly
if ver_a > ver_b {
Ordering::Greater
} else if ver_a < ver_b {
Ordering::Less
} else {
Ordering::Equal
}
}
fn print_sorted<'a, T: Iterator<Item = &'a Line<'a>>>(iter: T, settings: &GlobalSettings) {
let mut writer = settings.out_writer();
for line in iter {
@ -1712,7 +1687,7 @@ mod tests {
let a = "1.2.3-alpha2";
let b = "1.4.0";
assert_eq!(Ordering::Less, version_compare(a, b));
assert_eq!(Ordering::Less, version_cmp(a, b));
}
#[test]

View file

@ -30,6 +30,7 @@ pub use crate::mods::coreopts;
pub use crate::mods::os;
pub use crate::mods::panic;
pub use crate::mods::ranges;
pub use crate::mods::version_cmp;
// * string parsing modules
pub use crate::parser::parse_size;

View file

@ -5,3 +5,4 @@ pub mod coreopts;
pub mod os;
pub mod panic;
pub mod ranges;
pub mod version_cmp;

View file

@ -0,0 +1,361 @@
use std::cmp::Ordering;
/// Compares the non-digit parts of a version.
/// Special cases: ~ are before everything else, even ends ("a~" < "a")
/// Letters are before non-letters
fn version_non_digit_cmp(a: &str, b: &str) -> Ordering {
let mut a_chars = a.chars();
let mut b_chars = b.chars();
loop {
match (a_chars.next(), b_chars.next()) {
(Some(c1), Some(c2)) if c1 == c2 => {}
(None, None) => return Ordering::Equal,
(_, Some('~')) => return Ordering::Greater,
(Some('~'), _) => return Ordering::Less,
(None, Some(_)) => return Ordering::Less,
(Some(_), None) => return Ordering::Greater,
(Some(c1), Some(c2)) if c1.is_ascii_alphabetic() && !c2.is_ascii_alphabetic() => {
return Ordering::Less
}
(Some(c1), Some(c2)) if !c1.is_ascii_alphabetic() && c2.is_ascii_alphabetic() => {
return Ordering::Greater
}
(Some(c1), Some(c2)) => return c1.cmp(&c2),
}
}
}
/// Remove file endings matching the regex (\.[A-Za-z~][A-Za-z0-9~]*)*$
fn remove_file_ending(a: &str) -> &str {
let mut ending_start = None;
let mut prev_was_dot = false;
for (idx, char) in a.char_indices() {
if char == '.' {
if ending_start.is_none() || prev_was_dot {
ending_start = Some(idx);
}
prev_was_dot = true;
} else if prev_was_dot {
prev_was_dot = false;
if !char.is_ascii_alphabetic() && char != '~' {
ending_start = None;
}
} else if !char.is_ascii_alphanumeric() && char != '~' {
ending_start = None;
}
}
if prev_was_dot {
ending_start = None;
}
if let Some(ending_start) = ending_start {
&a[..ending_start]
} else {
a
}
}
pub fn version_cmp(mut a: &str, mut b: &str) -> Ordering {
let str_cmp = a.cmp(b);
if str_cmp == Ordering::Equal {
return str_cmp;
}
// Special cases:
// 1. Empty strings
match (a.is_empty(), b.is_empty()) {
(true, false) => return Ordering::Less,
(false, true) => return Ordering::Greater,
(true, true) => unreachable!(),
(false, false) => {}
}
// 2. Dots
match (a == ".", b == ".") {
(true, false) => return Ordering::Less,
(false, true) => return Ordering::Greater,
(true, true) => unreachable!(),
(false, false) => {}
}
// 3. Two Dots
match (a == "..", b == "..") {
(true, false) => return Ordering::Less,
(false, true) => return Ordering::Greater,
(true, true) => unreachable!(),
(false, false) => {}
}
// 4. Strings starting with a dot
match (a.starts_with('.'), b.starts_with('.')) {
(true, false) => return Ordering::Less,
(false, true) => return Ordering::Greater,
(true, true) => {
// Strip the leading dot for later comparisons
a = &a[1..];
b = &b[1..];
}
_ => {}
}
// Try to strip file extensions
let (mut a, mut b) = match (remove_file_ending(a), remove_file_ending(b)) {
(a_stripped, b_stripped) if a_stripped == b_stripped => {
// If both would be the same after stripping file extensions, don't strip them.
(a, b)
}
stripped => stripped,
};
// 1. Compare leading non-numerical part
// 2. Compare leading numerical part
// 3. Repeat
loop {
let a_numerical_start = a.find(|c: char| c.is_ascii_digit()).unwrap_or(a.len());
let b_numerical_start = b.find(|c: char| c.is_ascii_digit()).unwrap_or(b.len());
let a_str = &a[..a_numerical_start];
let b_str = &b[..b_numerical_start];
match version_non_digit_cmp(a_str, b_str) {
Ordering::Equal => {}
ord => return ord,
}
a = &a[a_numerical_start..];
b = &b[a_numerical_start..];
let a_numerical_end = a.find(|c: char| !c.is_ascii_digit()).unwrap_or(a.len());
let b_numerical_end = b.find(|c: char| !c.is_ascii_digit()).unwrap_or(b.len());
let a_str = a[..a_numerical_end].trim_start_matches('0');
let b_str = b[..b_numerical_end].trim_start_matches('0');
match a_str.len().cmp(&b_str.len()) {
Ordering::Equal => {}
ord => return ord,
}
match a_str.cmp(b_str) {
Ordering::Equal => {}
ord => return ord,
}
a = &a[a_numerical_end..];
b = &b[b_numerical_end..];
if a.is_empty() && b.is_empty() {
// Default to the lexical comparison.
return str_cmp;
}
}
}
#[cfg(test)]
mod tests {
use crate::version_cmp::version_cmp;
use std::cmp::Ordering;
#[test]
fn test_version_cmp() {
// Identical strings
assert_eq!(version_cmp("hello", "hello"), Ordering::Equal);
assert_eq!(version_cmp("file12", "file12"), Ordering::Equal);
assert_eq!(
version_cmp("file12-suffix", "file12-suffix"),
Ordering::Equal
);
assert_eq!(
version_cmp("file12-suffix24", "file12-suffix24"),
Ordering::Equal
);
// Shortened names
assert_eq!(version_cmp("world", "wo"), Ordering::Greater,);
assert_eq!(version_cmp("hello10wo", "hello10world"), Ordering::Less,);
// Simple names
assert_eq!(version_cmp("world", "hello"), Ordering::Greater,);
assert_eq!(version_cmp("hello", "world"), Ordering::Less);
assert_eq!(version_cmp("apple", "ant"), Ordering::Greater);
assert_eq!(version_cmp("ant", "apple"), Ordering::Less);
// Uppercase letters
assert_eq!(
version_cmp("Beef", "apple"),
Ordering::Less,
"Uppercase letters are sorted before all lowercase letters"
);
assert_eq!(version_cmp("Apple", "apple"), Ordering::Less);
assert_eq!(version_cmp("apple", "aPple"), Ordering::Greater);
// Numbers
assert_eq!(
version_cmp("100", "20"),
Ordering::Greater,
"Greater numbers are greater even if they start with a smaller digit",
);
assert_eq!(
version_cmp("20", "20"),
Ordering::Equal,
"Equal numbers are equal"
);
assert_eq!(
version_cmp("15", "200"),
Ordering::Less,
"Small numbers are smaller"
);
// Comparing numbers with other characters
assert_eq!(
version_cmp("1000", "apple"),
Ordering::Less,
"Numbers are sorted before other characters"
);
assert_eq!(
// spell-checker:disable-next-line
version_cmp("file1000", "fileapple"),
Ordering::Less,
"Numbers in the middle of the name are sorted before other characters"
);
// Leading zeroes
assert_eq!(
version_cmp("012", "12"),
Ordering::Less,
"A single leading zero can make a difference"
);
assert_eq!(
version_cmp("000800", "0000800"),
Ordering::Greater,
"Leading number of zeroes is used even if both non-zero number of zeros"
);
// Numbers and other characters combined
assert_eq!(version_cmp("ab10", "aa11"), Ordering::Greater);
assert_eq!(
version_cmp("aa10", "aa11"),
Ordering::Less,
"Numbers after other characters are handled correctly."
);
assert_eq!(
version_cmp("aa2", "aa100"),
Ordering::Less,
"Numbers after alphabetical characters are handled correctly."
);
assert_eq!(
version_cmp("aa10bb", "aa11aa"),
Ordering::Less,
"Number is used even if alphabetical characters after it differ."
);
assert_eq!(
version_cmp("aa10aa0010", "aa11aa1"),
Ordering::Less,
"Second number is ignored if the first number differs."
);
assert_eq!(
version_cmp("aa10aa0010", "aa10aa1"),
Ordering::Greater,
"Second number is used if the rest is equal."
);
assert_eq!(
version_cmp("aa10aa0010", "aa00010aa1"),
Ordering::Greater,
"Second number is used if the rest is equal up to leading zeroes of the first number."
);
assert_eq!(
version_cmp("aa10aa0022", "aa010aa022"),
Ordering::Greater,
"The leading zeroes of the first number has priority."
);
assert_eq!(
version_cmp("aa10aa0022", "aa10aa022"),
Ordering::Less,
"The leading zeroes of other numbers than the first are used."
);
assert_eq!(
version_cmp("file-1.4", "file-1.13"),
Ordering::Less,
"Periods are handled as normal text, not as a decimal point."
);
// Greater than u64::Max
// u64 == 18446744073709551615 so this should be plenty:
// 20000000000000000000000
assert_eq!(
version_cmp("aa2000000000000000000000bb", "aa002000000000000000000001bb"),
Ordering::Less,
"Numbers larger than u64::MAX are handled correctly without crashing"
);
assert_eq!(
version_cmp("aa2000000000000000000000bb", "aa002000000000000000000000bb"),
Ordering::Greater,
"Leading zeroes for numbers larger than u64::MAX are \
handled correctly without crashing"
);
assert_eq!(
version_cmp(" a", "a"),
Ordering::Greater,
"Whitespace is after letters because letters are before non-letters"
);
assert_eq!(
version_cmp("a~", "ab"),
Ordering::Less,
"A tilde is before other letters"
);
assert_eq!(
version_cmp("a~", "a"),
Ordering::Less,
"A tilde is before the line end"
);
assert_eq!(
version_cmp("~", ""),
Ordering::Greater,
"A tilde is after the empty string"
);
assert_eq!(
version_cmp(".f", ".1"),
Ordering::Greater,
"if both start with a dot it is ignored for the comparison"
);
// The following tests are incompatible with GNU as of 2021/06.
// I think that's because of a bug in GNU, reported as https://lists.gnu.org/archive/html/bug-coreutils/2021-06/msg00045.html
assert_eq!(
version_cmp("a..a", "a.+"),
Ordering::Less,
".a is stripped before the comparison"
);
assert_eq!(
version_cmp("a.", "a+"),
Ordering::Greater,
". is not stripped before the comparison"
);
assert_eq!(
version_cmp("a\0a", "a"),
Ordering::Greater,
"NULL bytes are handled comparison"
);
}
}

View file

@ -127,11 +127,7 @@ fn test_months_whitespace() {
#[test]
fn test_version_empty_lines() {
new_ucmd!()
.arg("-V")
.arg("version-empty-lines.txt")
.succeeds()
.stdout_is("\n\n\n\n\n\n\n1.2.3-alpha\n1.2.3-alpha2\n\t\t\t1.12.4\n11.2.3\n");
test_helper("version-empty-lines", &["-V", "--version-sort"]);
}
#[test]

View file

@ -8,4 +8,8 @@
1.2.3-alpha
1.2.3-alpha2
11.2.3
bar2
bar2.0.0
foo0.1
foo1.0
1.12.4

View file

@ -0,0 +1,45 @@
^ no match for key
^ no match for key
^ no match for key
^ no match for key
^ no match for key
^ no match for key
^ no match for key
^ no match for key
^ no match for key
^ no match for key
^ no match for key
^ no match for key
^ no match for key
^ no match for key
1.2.3-alpha
___________
___________
1.2.3-alpha2
____________
____________
11.2.3
______
______
bar2
____
____
bar2.0.0
________
________
foo0.1
______
______
foo1.0
______
______
>>>1.12.4
_________
_________

View file

@ -9,3 +9,7 @@
1.12.4
foo1.0
foo0.1
bar2.0.0
bar2