Kuba Kuba - 4 months ago 8
Bash Question

Bash script invalid space sign

My goal is it is to gate time in second for last modified file in catalog.

I have catalog with space in name and want to

stat
it with script. With a terminal I simply use:

sudo stat -c %Y /var/www/pre/data/jko/files/My\ Files


and works perfectly.
In my
bash
script i read files one by one from directory
/var/www/pre/data/jko/files/
with (this is inside while loop)

touch tempFile.txt
#ls sort by last modified date
sudo ls -rt /var/www/pre/data/$line/files/ > tempFile.txt
# read newest file
outputFile=$(tail -1 tempFile.txt)
# replace all spaces with "\ " sign
outputFile=$(echo ${outputFile// /"\ "})
outputDirectoryToFile=/var/www/pre/data/$line/files/$outputFile
echo $outputDirectoryToFile
expr `sudo date +%s` - `sudo stat -c %Y $outputDirectoryToFile`


And if i fire this script with
bash script.sh
i got

/var/www/pre/data/jko/files/My\ Files //line from echo
stat: cannot stat '/var/www/pre/data/jko/files/My\': No such file or directory
stat: cannot stat 'Files': No such file or directory
expr: syntax error


Or maybe there is simpler solution

Answer

Correctly, and efficiently, finding the latest file in a directory

{ read -r -d ' ' mtime && IFS= read -r -d '' filename; } \
  < <(find /directory -type f -printf '%T@ %p\0' | sort -z -r -n)

...will put the time in seconds for the most-recently-modified file in the shell variable mtime, and the name of this file in the shell variable filename.

Moreover, it will work even with files with surprising or intentionally malicious names -- filenames with newline literals in their names, filenames with glob characters, etc. I have a more complete explanation of this idiom and why it works here.


Why your original code didn't work

Now, what was wrong with your original code? In short: Literal quotes do not substitute for syntactic quotes. Let's go into what this means.

In /var/www/pre/data/jko/files/My\ Files, the backslash is syntactic: It's shell syntax. When you run stat /var/www/pre/data/jko/files/My\ Files, the result is a syscall that looks like the following:

execv("/usr/bin/stat", "stat", "/var/www/pre/data/jko/files/My Files")

Note how the backslash is gone? That's because the shell consumed it when it was parsing the string.

The following are all exactly identical:

# each of these assigns the same string to the variable named s
s=Hello\ World
s=Hello" "World
s='Hello World'

...and they can be expanded as follows:

# this passes the *exact* contents of that variable as an argument to stat, and thus tries
# to stat a file named Hello World (no literal quotes in the name):
stat "$s"

In the above, the quotes are again syntactic -- they're telling the shell not to split the result of the variable expansion into multiple separate words or evaluate it as a glob -- not literal, in which case they would be passed to stat as part of its argument.


So, what happens when you run s=${s// /'\ '}? You're putting literal backslashes into the data.

Thereafter:

s="Hello World"
s=${s// /'\ '}    # change string to 'Hello\ World'
stat "$s"         # try to stat a file named 'Hello\ World', with the backslash 

What happens if you leave out the syntactic double quotes on the expansion? It's even uglier:

s="Hello World"
s=${s// /'\ '}    # change string to 'Hello\ World'
stat $s           # try to stat a file named 'Hello\', and a second file named 'World'

That's because unquoted expansion doesn't run through all shell parsing steps; it only does field splitting and glob expansion.

Comments