diff --git a/crates/bashkit/src/builtins/ls.rs b/crates/bashkit/src/builtins/ls.rs index c7757e64..67a52ad3 100644 --- a/crates/bashkit/src/builtins/ls.rs +++ b/crates/bashkit/src/builtins/ls.rs @@ -79,6 +79,10 @@ impl Builtin for Ls { let mut output = String::new(); let multiple_paths = paths.len() > 1 || opts.recursive; + // Separate file and directory arguments (like real ls) + let mut file_args: Vec<(&str, crate::fs::Metadata)> = Vec::new(); + let mut dir_args: Vec<(usize, &str, std::path::PathBuf)> = Vec::new(); + for (i, path_str) in paths.iter().enumerate() { let path = resolve_path(ctx.cwd, path_str); @@ -93,37 +97,44 @@ impl Builtin for Ls { )); } - // Check if it's a file or directory let metadata = ctx.fs.stat(&path).await?; if metadata.file_type.is_file() { - // Single file - just list it - let name = Path::new(path_str) - .file_name() - .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_else(|| path_str.to_string()); - - if opts.long { - output.push_str(&format_long_entry(&name, &metadata, opts.human)); - } else { - output.push_str(&name); - output.push('\n'); - } + file_args.push((path_str, metadata)); } else { - // Directory - if let Err(e) = list_directory( - &ctx, - &path, - path_str, - &mut output, - &opts, - multiple_paths, - i > 0, - ) - .await - { - return Ok(ExecResult::err(format!("ls: {}\n", e), 2)); - } + dir_args.push((i, path_str, path)); + } + } + + // Sort file arguments by time if -t, preserving original paths + if opts.sort_by_time { + file_args.sort_by(|a, b| b.1.modified.cmp(&a.1.modified)); + } + + // Output file arguments first (preserving path as given by user) + for (path_str, metadata) in &file_args { + if opts.long { + output.push_str(&format_long_entry(path_str, metadata, opts.human)); + } else { + output.push_str(path_str); + output.push('\n'); + } + } + + // Then output directory listings + for (i, path_str, path) in &dir_args { + if let Err(e) = list_directory( + &ctx, + path, + path_str, + &mut output, + &opts, + multiple_paths, + *i > 0 || !file_args.is_empty(), + ) + .await + { + return Ok(ExecResult::err(format!("ls: {}\n", e), 2)); } } diff --git a/crates/bashkit/tests/spec_cases/bash/ls.test.sh b/crates/bashkit/tests/spec_cases/bash/ls.test.sh new file mode 100644 index 00000000..c146b9d4 --- /dev/null +++ b/crates/bashkit/tests/spec_cases/bash/ls.test.sh @@ -0,0 +1,42 @@ +### ls_file_preserves_path +# ls with file arguments should preserve the full path in output +mkdir -p /tmp/lsdir +echo x > /tmp/lsdir/a.md +echo y > /tmp/lsdir/b.md +ls /tmp/lsdir/a.md /tmp/lsdir/b.md +### expect +/tmp/lsdir/a.md +/tmp/lsdir/b.md +### end + +### ls_file_preserves_path_sorted_by_time +# ls -t with file arguments should preserve the full path in output +mkdir -p /tmp/lstdir +echo x > /tmp/lstdir/a.md +sleep 0.01 +echo y > /tmp/lstdir/b.md +ls -t /tmp/lstdir/a.md /tmp/lstdir/b.md +### expect +/tmp/lstdir/b.md +/tmp/lstdir/a.md +### end + +### ls_directory_shows_filenames_only +# ls on a directory should show filenames only, not full paths +mkdir -p /tmp/lsdironly +echo x > /tmp/lsdironly/file1.txt +echo y > /tmp/lsdironly/file2.txt +ls /tmp/lsdironly +### expect +file1.txt +file2.txt +### end + +### ls_single_file_preserves_path +# ls with a single file argument should preserve the full path +mkdir -p /tmp/lssingle +echo x > /tmp/lssingle/test.txt +ls /tmp/lssingle/test.txt +### expect +/tmp/lssingle/test.txt +### end