Read-only file system error when calling shell script within Applescript

I’ve written an AppleScript that I can call from the Scripts menu that creates a file from a template, changing the datestring to the current date, and then imports that new file into my database.

This used to work on Monterey, but after reinstalling to Ventura I just can’t get it to work anymore. The applescript trips on the error:

findSed: sh: Macintosh HD:private:var:folders:5f:w1hgv_nd20gb4_dc2jlg9mhm0000gp:T:TemporaryItems:Weekly Results - 2022W51.html: Read-only file system

The relevant code snippet is the following, within the applescript that is called from DTP:


on fileSed(_inputFile, _inputString, _outputFile, _outputString)
	(* 
	make sure we map the locations to POSIX locations
	*)
	
	try
		set _inputPOSIX to POSIX path of _inputFile
		set _outputPOSIX to POSIX path of _outputFile
		set shellscript to "cat \"" & _inputPOSIX & "\"| sed 's/" & _inputString & "/" & _outputPOSIX & "/g' > \"" & _outputFile & "\""
		do shell script shellscript
	on error errMSg
		display dialog "findSed: " & errMSg
	end try
end fileSed

set user_home to (get path to home folder as text)
set tmp_dir to (get path to temporary items from user domain as text)
set _pathName to (user_home & "Library:Application Support:DEVONthink 3:Templates.noindex:Agile:Week Ahead.dtTemplate:English.lproj:YYYYW:Weekly Results.html")
				set _tempName to (tmp_dir & "Weekly Results - " & weekLabel & ".html")
set findString to "YYYYW"
				my fileSed(_pathName, findString, _tempName, weekLabel)
set weekGroup to create record with {name:weekLabel, type:group} in quarterGroup
				import template _tempName to weekGroup

All parameters to fileSed are strings, the other inputs are date related strings:

  • weekLabel : “2022W51”
  • QuarterLabel : “2022Q4”
  • weekGroup and QuarterGroup are groups created prior with the respective labels.

It seems to me that this error is related to the confusing security mechanisms provided by Mac OS in relation to how I can access the temporary folder, but for the life of me I can’t figure out how this should reasonably be done…

Did you check if the folder you’re trying to write to

  • exists
  • and you have writing permissions for it?

Something like
ls "/private/var/folders/5f/w1hgv_nd20gb4_dc2jlg9mhm0000gp/T/T*

in Terminal should tell you. In addition, you might want to try
touch "/private/var/folders/5f/w1hgv_nd20gb4_dc2jlg9mhm0000gp/T/TemporaryItems/foo"

(again in Terminal). If that works, the error message is off.

May I ask what you’re trying to accomplish here? Maybe it’s easier to copy the template into the group and change its name afterwards?

After granting full-disk access permission to Terminal I can indeed ‘touch foo’ and execute an ls. Which indicates that this is indeed permissioning - but what permissions should I give to what process for this to work?

What I am trying to accomplish is create a new diary entry with the date string updated to today’s date. I do this by ‘cat’ ing the file through sed to make the required changes to the file and piping it out to the new file name, then importing that file into my database.

The reason I do it this way is not to pollute the database, or are you suggesting that I might as well create the file the same way in place as is, but then how do I tell the database?

I changed my script to create the new file in the DTP Template directory, but there too it says ‘read only filesystem’. It still looks like a permissions error.

I’d import the template directly into the group and then change the record’s name. Something like

set weekGroup to create record with {name:weekLabel, type:group} in quarterGroup
set newRecord to import template _pathName to weekGroup
set name of newRecord to "Weekly Results - " & weekLabel

I think you don’t need to append ‘.html’ in that case.

Thanks for this, this does indeed fix the filename issue, but within the file there is a datestring that should be updated too - this is why I resorted to using sed because I just couldn’t get it to work in AppleScript

You should use the import command with placeholders in AppleScript. You can find examples of this in our built-in templates.

I have no experience with templates. Maybe @BLUEFROG knows how to modify a text inside the file or how to set it up so that it works automagically.

I believe you should give bash and env Full disk access to use do shell script.
That’s how I set it up.

I’ve looked at the placeholders, but the documentation is rather limited and the values I want to use are not defined by default.
Also, the ScriptEditor doesn’t like the following syntax:

set newRecord to import template _pathName placeholders { |YYYYW|:weekLabel } to weekGroup

it responds with:
‘Expected end of line, etc. but found identifier’

Again, your help was invaluable and I managed to figure it out: the crucial change was

				set _thePlaceHolders to {|YYYYW|:weekLabel}
				set newRecord to import _pathName placeholders _thePlaceHolders to weekGroup

Drop the ‘template’ keyword in the import command.

By way of giving back, please find the final script that creates diary entries with a weekly and daily cadence, organized by quarter/week/day.
The weekly and daily templates use “YYYYW” and “YYYY-MM-DD” placeholders for the date values.

Comments and suggestions for improvement are much appreciated.

(* 
  Create a diary entry with weekly and daily focus, order in quarters and weeks.
  
	Using input from:
  	- https://macscripter.net/viewtopic.php?id=24737
	- https://discourse.devontechnologies.com/t/read-only-file-system-error-when-calling-shell-script-within-applescript/73923/8
	- https://discourse.devontechnologies.com/t/applescript-access-denied-issues-with-import-to/73673
 *)

(*

Subroutine Block

*)

on year_start(theDate)
	copy theDate to yearStart
	set yearStart's month to January
	set yearStart's day to 1
	set yearStart's hours to 0
	set yearStart's minutes to 0
	set yearStart's seconds to 0
	return yearStart
end year_start

on create_quarter(theDate)
	set theThursday to get_nearest_thursday(theDate)
	set _month to month of theThursday as number
	set quarter to round ((_month - 1) / 3 + 1) rounding down
	
	return quarter
end create_quarter

on get_nearest_thursday(theDate)
	copy theDate to theThursday -- use copy to duplicate the variable, otherwise it's just the reference!
	set twd to weekday of theThursday as number
	if twd is 0 then
		set twd to 7
	end if
	
	set theThursday to (theThursday + (5 - twd) * 86400)
	
	return theThursday
end get_nearest_thursday

on get_week_number(theDate)
	-- calculate week number for the given thursday
	set theThursday to get_nearest_thursday(theDate)
	-- copy theThursday to yearStart
	set yearStart to year_start(theThursday)
	set theWeekNumber to round (((theThursday - yearStart) / 86400 + 1) / 7) rounding up
	return theWeekNumber
end get_week_number

on quarter_indicator(theDate)
	set _year to year of theDate
	set _quarter to create_quarter(theDate)
	
	return (_year) & "Q" & (_quarter)
end quarter_indicator

on format00(theNumber)
	return (text -2 thru -1 of ("00" & theNumber))
end format00

on week_indicator(theDate)
	set _year to year of theDate
	set _week to get_week_number(theDate)
	
	return (_year) & "W" & format00(_week) -- create a string with the leading zeros
end week_indicator

on day_indicator(theDate)
	set _year to year of theDate
	set _month to month of theDate as number
	set _day to day of theDate as number
	
	set _ms to format00(_month)
	set _ds to format00(_day)
	
	return ((_year) & (_ms) & (_ds))
end day_indicator

on day_string(theDate)
	set _year to year of theDate
	set _month to month of theDate as number
	set _day to day of theDate as number
	
	set _ms to format00(_month)
	set _ds to format00(_day)
	
	
	return ((_year) & "-" & (_ms) & "-" & (_ds))
end day_string

(*

Actual script starts here

*)
set user_home to (get path to home folder as text)
set targetDate to current date
set qt to quarter_indicator(targetDate)
set wk to week_indicator(targetDate)
set dt to day_indicator(targetDate)
set ds to day_string(targetDate)
-- now start looking at the DEVONthink application and check the groups
tell application id "DNtp"
	if not (exists current database) then error "No database open"
	tell current database
		try
			set quarterLabel to ("/" & qt)
			set quarterGroup to get record at quarterLabel
			if quarterGroup is missing value or (type of quarterGroup is not group) then
				-- the rootGroup does not exist, go and create it
				set quarterGroup to create location quarterLabel
			end if
			-- now we have the quarter for sure, create the week group
			set weekLabel to (wk as text)
			set target to (quarterLabel & "/" & weekLabel)
			set weekGroup to get record at (target)
			if weekGroup is missing value or (type of weekGroup is not group) then
				set _pathName to (user_home & "Library:Application Support:DEVONthink 3:Templates.noindex:Agile:Week Ahead.dtTemplate:English.lproj:YYYYW:Weekly Results.html")
				set weekGroup to create record with {name:weekLabel, type:group} in quarterGroup
				set _thePlaceHolders to {|YYYYW|:weekLabel}
				set newRecord to import _pathName placeholders _thePlaceHolders to weekGroup
				set name of newRecord to "Weekly Results - " & weekLabel
			end if
			-- now create the target record if it does not exist 
			set dayLabel to (dt as text)
			set target to (quarterLabel & "/" & weekLabel & "/" & dayLabel)
			set dayGroup to get record at (target)
			if dayGroup is missing value or (type of dayGroup is not group) then
				set dayString to (ds as text)
				set _pathName to (user_home & "Library:Application Support:DEVONthink 3:Templates.noindex:Agile:Week Ahead.dtTemplate:English.lproj:YYYYW:D1:Daily Results.html")
				set dayGroup to create record with {name:dayLabel, type:group} in weekGroup
				set _thePlaceHolders to {|YYYY-MM-DD|:dayString}
				set newRecord to import _pathName placeholders _thePlaceHolders to dayGroup
				set name of newRecord to "Daily Results - " & dayString
			end if
		on error errMSg
			display dialog "ERROR: " & errMSg
		end try
	end tell
end tell

1 Like

Glad you got it worked out and apologies for the short reply. I was on the road so I didn’t have availability to show an example.

Check out (my magnum opus :wink: ) the Automation chapter in the built-in Help and manual for more fun stuff.
Here specifically related to your situation…

And thanks for sharing your solution with the rest of the class :slight_smile:

Can’t comment on most of the code, except

  • I don’t see why format00 concats “00” with theNumber, the number being some value between 1 and 31. So you get ‘001’ through ‘0031’ and you cut out the last two characters from that with the text -2 thru -1 construct. There’s always a leading zero in that construct that is not needed – text -2 thru -1 of ("0" & theNumber) should (I think) do the same thing.
  • Other things are probably ugly because of the shortcomings of AppleScript.

Like the day_string/day_indicator code, which is mostly copy and paste. I’d probably do something like this (in JavaScript)

function year_month_day(date) {
  return [String(date.getYear()), 
          String(date.getMonth()+1).padStart(2,'0'), 
          String(date.getDate()).padStart(2,'0'))];
}
const day_indicator = year_month_day(date).join(' '); //2022 10 03
const day_string = year_month_day(date).join('-'); //2022-10-03

no need to write your own padding function, since there is one already. No need to write two functions that do nearly the same thing because one can return an array that you then deal with the way you want. Getting the week number is equally awkward in JavaScript, though (don’t quite see why you’ll need the nearest Thursday for that).

  • Why do you use path to home folder if what you really need is path to application support from user domain? You’re only ever referring to the user_home in the statements that get the path of the templates, and those are located in the application support folder.
  • I think the “create_quarter” function is overly complicated. I’d use set q to (month / 3 rounding up), but then I don’t know what this “Thursday” thing is about. For me, all dates in months 1 through 3 fall in the first quarter.
  • What’s the deal with this Thursday? You even use it to figure out the start of the year, which I don’t get. For example in get_week_number, you first determine the “nearest Thursday”. On Jan 1st 2023, the nearest Thursday is Dec 29 2022 (in my calendar. Your function, though, returns Jan 5th 2023). What’s wrong with
    round ((today - firstdayofyear)/7/86400) rounding up?

Thanks for all the comments and feedback, I’ll take them into account and see if I can streamline the code.

Also thanks for pointing out a potential bug in the calculator for Thursday, I’ll take a close look. The origin of the Thursday calculation is ISO 8601 - which describes a portable date and time format (so we avoid the ambiguity of 01-03-2022 between EU and US…)

If I remember correctly, the 00 is used for the case where theNumber is empty.

Neither day nor month can be empty, I think.

And while I’m aware of ISO dates, I don’t see the relation to the Thursday stuff. ISO date is about an unambiguous string representation, while you’re doing calculations.

Thanks for all the help and additional feedback. The ISO8601 standard uses several methods to determine week 01, I’m using the first Thursday of the year as reference, hence the Thursdays.

Below the updated code, with the bug in the date calculation fixed, and the code simplified.

(* 
  Create a diary entry with weekly and daily focus, order in quarters and weeks.
  
	Using input from:
  	- https://macscripter.net/viewtopic.php?id=24737
	- https://discourse.devontechnologies.com/t/read-only-file-system-error-when-calling-shell-script-within-applescript/73923/8
	- https://discourse.devontechnologies.com/t/applescript-access-denied-issues-with-import-to/73673
 *)

(*

Subroutine Block

*)

on year_start(theDate)
	copy theDate to yearStart
	set yearStart's month to January
	set yearStart's day to 1
	set yearStart's hours to 0
	set yearStart's minutes to 0
	set yearStart's seconds to 0
	return yearStart
end year_start

on create_quarter(theDate)
	set theThursday to get_nearest_thursday(theDate)
	set _month to month of theThursday as number
	set quarter to round ((_month - 1) / 3 + 1) rounding down
	return quarter
end create_quarter

on get_nearest_thursday(theDate)
	copy theDate to theThursday -- use copy to duplicate the variable, otherwise it's just the reference!
	set twd to weekday of theThursday as number
	if twd is 1 then
		set twd to 8
	end if
	set theThursday to (theThursday + (5 - twd) * 86400)
	return theThursday
end get_nearest_thursday

on get_week_number(theDate)
	-- calculate week number for the given thursday
	set theThursday to get_nearest_thursday(theDate)
	-- copy theThursday to yearStart
	set yearStart to year_start(theThursday)
	set theWeekNumber to round (((theThursday - yearStart) / 86400 + 1) / 7) rounding up
	return theWeekNumber
end get_week_number

on quarter_indicator(theDate)
	set _year to year of theDate
	set _quarter to create_quarter(theDate)
	return (_year) & "Q" & (_quarter)
end quarter_indicator

on format00(theNumber)
	return (text -2 thru -1 of ("00" & theNumber))
end format00

on week_indicator(theDate)
	set _year to year of theDate
	set _week to get_week_number(theDate)
	return (_year) & "W" & format00(_week) -- create a string with the leading zeros
end week_indicator

on day_formatter(theDate, _sep)
	set _year to year of theDate
	set _month to month of theDate as number
	set _day to day of theDate as number
	
	set _ms to format00(_month)
	set _ds to format00(_day)
	return ((_year) & _sep & (_ms) & _sep & (_ds))
end day_formatter

on day_indicator(theDate)
	return day_formatter(theDate, "")
end day_indicator

on day_string(theDate)
	return day_formatter(theDate, "-")
end day_string

(*
	Some test cases for debugging purposes

*)
set _testDate to date ("01-01-2023")
set ntd to get_nearest_thursday(_testDate)
if (ntd as string) is not "Thursday, 29 December 2022 at 00:00:00" then
	display dialog "Testcase error: " & (_testDate as string) & " " & ntd as string
end if

set _testDate to date ("04-01-2023") -- january 4th should always be part of week 1
set _wn to get_week_number(_testDate)
if _wn is not 1 then
	display dialog "Testcas error: " & (_testDate as string) & " " & _wn as string
end if

(*

Actual script starts here

*)


set user_home to (get path to home folder as text)
set targetDate to current date
set qt to quarter_indicator(targetDate)
set wk to week_indicator(targetDate)
set dt to day_indicator(targetDate)
set ds to day_string(targetDate)
-- now start looking at the DEVONthink application and check the groups
tell application id "DNtp"
	if not (exists current database) then error "No database open"
	tell current database
		try
			set quarterLabel to ("/" & qt)
			set quarterGroup to get record at quarterLabel
			if quarterGroup is missing value or (type of quarterGroup is not group) then
				-- the rootGroup does not exist, go and create it
				set quarterGroup to create location quarterLabel
			end if
			-- now we have the quarter for sure, create the week group
			set weekLabel to (wk as text)
			set target to (quarterLabel & "/" & weekLabel)
			set weekGroup to get record at (target)
			if weekGroup is missing value or (type of weekGroup is not group) then
				set _pathName to (user_home & "Library:Application Support:DEVONthink 3:Templates.noindex:Agile:Week Ahead.dtTemplate:English.lproj:YYYYW:Weekly Results.html")
				set weekGroup to create record with {name:weekLabel, type:group} in quarterGroup
				set _thePlaceHolders to {|YYYYW|:weekLabel}
				set newRecord to import _pathName placeholders _thePlaceHolders to weekGroup
				set name of newRecord to "Weekly Results - " & weekLabel
			end if
			-- now create the target record if it does not exist 
			set dayLabel to (dt as text)
			set target to (quarterLabel & "/" & weekLabel & "/" & dayLabel)
			set dayGroup to get record at (target)
			if dayGroup is missing value or (type of dayGroup is not group) then
				set dayString to (ds as text)
				set _pathName to (user_home & "Library:Application Support:DEVONthink 3:Templates.noindex:Agile:Week Ahead.dtTemplate:English.lproj:YYYYW:D1:Daily Results.html")
				set dayGroup to create record with {name:dayLabel, type:group} in weekGroup
				set _thePlaceHolders to {|YYYY-MM-DD|:dayString}
				set newRecord to import _pathName placeholders _thePlaceHolders to dayGroup
				set name of newRecord to "Daily Results - " & dayString
			end if
		on error errMSg
			display dialog "ERROR: " & errMSg
		end try
	end tell
end tell

oops, to soon:

instead of the user_home use:

set app_support to (get path to application support from user domain as text)

and replace

set _pathName to (app_support & "DEVONthink 3:Templates.noindex:Agile:Week Ahead.dtTemplate:English.lproj:YYYYW:Weekly Results.html")

and

set _pathName to (app_support & "DEVONthink 3:Templates.noindex:Agile:Week Ahead.dtTemplate:English.lproj:YYYYW:D1:Daily Results.html")

1 Like