Score:1

list items correctly in bash with a space in their name

bn flag

I have for example following Files:

test1.txt
test2.txt
test space.txt

I use the following command:

output=$(ls -l --time-style=long-iso | awk '{print $8, $6}')

And I get following output:

test1.txt 
test2.txt 
test YYYY-MM-DD

because of the space in its name, I get the wrong output.

Is it possible to get output like this?

"text1.txt" 
"text2.txt" 
"text space.txt"
terdon avatar
cn flag
What output do you want? Just the file names? If so, why are you also printing the date? Please [edit] your question and clarify. And how will you use this? Do you really want it in a single, scalar variable? Wouldn't it be better to have each file as an element in an array?
Organic Marble avatar
us flag
Are you asking about sorting the output?
Score:3
cn flag

This is why you never try to parse the output of ls. ls is designed to print to a terminal not to be used in scripts or to be parsed, there are other tools for that. You are also making it more complex yourself since you are using the -l --time-style=long-iso options but then your desired output is only the file name.

To get the list of files and directories in the current directory into a variable, the easiest way is to just use globs:

$ ls
 test1.txt   test2.txt  'test space.txt'
$ files=( * )
$ echo "Files: ${files[@]}"
Files: test1.txt test2.txt test space.txt
$ echo "The third file is '${files[2]}'"
The third file is 'test space.txt'

To have the file names quoted, do:

$ printf '"%s"\n' "${files[@]}"
"test1.txt"
"test2.txt"
"test space.txt"

As you can see, the $files array holds all of the files, with each of them stored as an element in the array. See here for a description of how bash arrays work, but briefly, you can list the entire array using ${array[@]} and individual entries using ${array[N]} where N is a number between 0 (the first element of the array) and X-1 where X is the number of elements in the array. In the example above, we have 3 elements in $files, so you have ${files[0]}, ${files[1]} and ${files[2]}.

Note that this approach even works with files whose name contains a newline (\n):

$ touch 'a file with a '$'\n''newline!'
$ ls
'a file with a '$'\n''newline!'   test1.txt   test2.txt  'test space.txt'
$ files=( * )
$ echo "File is: ${files[0]}"
File is: a file with a 
newline!

Now, if you also want to keep the file's modification date (which is not what your output shows, but is what I think your awk is attempting to do), you can do something like this:

$ files=( * )
$ for file in "${files[@]}"; do
   date=$(stat -c '%y' -- "$file" | cut -d ' ' -f1)
   printf 'Date: %s File: "%s"\n' "$date" "$file"
done
Date: 2023-08-15 File: "a file with a 
newline!"
Date: 2023-08-15 File: "test1.txt"
Date: 2023-08-15 File: "test2.txt"
Date: 2023-08-15 File: "test space.txt"
Score:2
jp flag

More reliably, find alone with its -printf action can give you the output you want from both ls and awk ... Like so:

find ! -name . -prune -printf '%P %TY-%Tm-%Td\n'

If you need to assign the output to a parameter, then an array would be the most appropriate while reading the output as NULL delimited (safer for unusual file/directory names with e.g. spaces/tabs/newlines) like so:

$ touch normalfile
$ touch 'filewith'$'\n''newline'
$ touch 'filewith'$'\t''tab'
$ touch 'filewith space'
$
$
$ readarray -d '' files < <(find ! -name . -prune -printf '%P %TY-%Tm-%Td\0')
$
$ printf '%s\n' "${files[@]}"
filewith space 2023-08-15
filewith    tab 2023-08-15
normalfile 2023-08-15
filewith
newline 2023-08-15

Notice that find will include hidden files and directories (Those starting with a dot like .filename) in the output, so if those are not desired then you might want to filter them out.

Score:1
cn flag

Please do note that the default separator for awk is a space, so you need to print all the variables, not only $8, but also $9 ... when you want the complete filename.

ls -l --time-style=long-iso test* | awk '{ t=index($0,$7); print substr($0,t+6), $6 }'

It will find the 7th column (the time) which is always 5 positions long, and will print the rest of the line substr($0,t+6), followed by the date $6.

You can make it handle even files whose name contains a newline character if you use the --zero option of ls and set awk's record separator to NULL EDIT2:

luuk@ZES:/mnt/d/TEMP$
luuk@ZES:/mnt/d/TEMP$ touch test$'\n'test.txt
luuk@ZES:/mnt/d/TEMP$ ls -l --time-style=long-iso test*
-rwxrwxrwx 1 luuk luuk 0 2023-08-15 21:04 'test'$'\n''test.txt'
luuk@ZES:/mnt/d/TEMP$ $ ls -l --zero --time-style=long-iso -- * | awk 'BEGIN{RS="\0"}{ t=index($0,$7); print substr($0,t+6), $6 }'
test
test.txt 2023-08-16

/mnt/d/TEMP is my TEMP directory on my Windows11 computer. (the special place for temporary files)

But/Conclusion: This might seem to work, but might also depend on other factors like the fact that this is testen under WSL. So, always TEST, before using anything you find on the internet...

terdon avatar
cn flag
Turns out that recent versions of `ls` have the `--zero` option and that allows this to work with newlines as well! I added that to your answer and removed the first version (no need, your second version is better anyway), and converted my downvote to an upvote.. You might be interested in [my question](https://unix.stackexchange.com/q/754193/22222) on U&L asking how robust this is.
Luuk avatar
cn flag
The `--zero` option, do you have a reference for the version from which it is available ? (It's not in version 5.1.16)
Ed Morton avatar
cr flag
That awk command would fail when the user name or group name is multiple space-separated words, e.g. `-rwxrwxrwx 1 luuk Domain Users 0 2023-08-15 21:04 'test'$'\n''test.txt'`, since then `$7` would not be the time field. You could use `awk -v RS='\0' 'p=index($0,":") { print substr($0,p+4), substr($0,p-13,10) }'` or `awk -v RS='\0' 'match($0,/(.{10}) ..:.. (.*)/,a) { print a[2], a[1] }'` (with gawk) instead since those names can't include `:`s.
terdon avatar
cn flag
Certainly since version 9, @Luuk. But it used to be called `--null`, looks for that one too.
Score:0
bn flag

I tried it with:

output=$(
    while IFS= read -r -d '' file; do
        filename=$(basename "$file")
        echo \"$filename\"
    done < <(find . -maxdepth 1 -type f -print0)
)

Now it works and i get the Output i want

"text1.txt" 
"text2.txt" 
"text space.txt"
hr flag
If *all* that you want is to print a list of filenames, one per line, surrounded by literal double quotes, then you don't need anything more complicated in bash than `printf '"%s"\n' *`. If you want to include hidden filenames, set the bash shell's `dotglob` option.
Score:-1
it flag

Rather than using ls, use stat (man stat). You can output file info in any desired format.

I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.