macOS 15.5beta4が配信されましたが、とくに報告ずみのバグは何も直っていません。15.5台では直らないというよりも、バグとして認識していないんじゃないでしょうか?
タグ: 15.0savvy
【続報】macOS 15.5で特定ファイル名パターンのfileをaliasにcastすると100%クラッシュするバグ
特定のパターンのファイル名のパスをaliasにcastする処理で処理系まるごとクラッシュするというmacOS 15.5β版(15.5 Beta(24F5053f))のバグ。
もっと以前から存在していたのかもしれませんが、特定のファイル名だとクラッシュを引き起こす前代未聞のバグがすごすぎて、いろいろ調べてみました。
文字依存する箇所はごくわずかで、いろいろ規則性があることがわかってきました。
・15.5 Beta(24F5053f)の日本語ユーザー環境(Primary LanguageをJapaneseにした状態)で発生。英語環境に変更すると発生しない
・ファイルパスをaliasにcastすると即・クラッシュ
・アルファベットとひらがな/カタカナが混在している必要がある???
・拡張子の種別は関係なく発生
・一定の文字数以上の長さが必要
・決定的な問題を引き起こすのは、濁点/半濁点つきのひらがな/カタカナが入っていること
・当初、記号文字やスペースが入っていることが条件かと思っていたが、これらを除去しても発生
・濁点/半濁点つき文字はファイル名の特定文字数以降に登場する必要がある。冒頭に移動させてもクラッシュは発生しない
・同じ処理内容のJXAでもクラッシュが発生する
・これらのクラッシュを誘発するファイル名がフォルダ名についていてもクラッシュは発生しない
現時点でクラッシュを発生させる最低限のファイル名は、
AAXAAXXあああああああああパ1.txt
であることを確認しています。
追記:
macOS 15.5β3(24F5053j)でも継続して発生中です。
macOS 15.5で特定ファイル名パターンのfileをaliasにcastすると100%クラッシュするバグ
きわめて珍しいパターンのバグに遭遇しました。自分の手元のmacOS 15.5Beta2環境(Apple Silicon Mac x 2)では再現率100%です(FB17285323)。再起動しても再現し、日本語環境で発生する一方で英語環境では発生しません。日本語環境であればJXAでも同様の現象の発生を確認しています。
「AppleScript+Xcode GUIアプリ ソースブック.key」
というファイル名のKeynote書類を、macOS 15.5β(日本語ユーザー環境)+Keynote v14.4で編集。このKeynote書類のボツスライドの移動用の「没.key」という書類をもとの書類の同階層に同じスタイルで作成するAppleScriptをスクリプトメニューから実行したところ、これがクラッシュ。
ねんのために、PagesとNumbersでも同様のテストを行なったところ、予想どおり100%クラッシュ。
これまでのmacOSでは遭遇したことのない現象だったので、原因を深掘り。
最終的に、Keynote/Pages/Numbers v14.4の書類からファイルパス(file)を取得し、そのfileをaliasにcastするとクラッシュするということが分かりました。
追記:Pixelmator Pro、CotEditorでも同様のファイル名に対して処理を行うとクラッシュすることが確認されました。
さらに追記:クラッシュを起こすパターンのファイルをchoose fileして、フルパスをstringにcastし、さらにaliasにcastするとアプリ操作に関係なくクラッシュすることが判明
さらに、ファイル名を短くしたり一部を取り出したりすることで、同様のクラッシュが発生するかどうかを調べてみたところ、
AppleScript+Xcode GUIアプリ ソースブ.key
AppleScript+Xcode GUIアプリ ソースブッ.key
AppleScript+Xcode GUIアプリ ソースブック.key
AAAAAAAA+XXXX GUIアプリ ソースブック.key
XXXXXXXX+XXXX XXXアプリ ソースブック.key
XXXXXXXX_XXXX XXXアプリ ソースブック.key
XXXXXXXX-XXXX XXXアプリ ソースブック.key
XXXXXXXX&XXXX XXXアプリ ソースブック.key
XXXXXXXX+XXXX GUIアプリ ソースブック.key
XXXXXXXX=XXXX XXXアプリ ソースブック.key
のパターンで、以下のAppleScriptを実行すると100%クラッシュが発生。スクリプトエディタでもScript Debuggerでもスクリプトメニューでも100%クラッシュ(実行プログラムのクラッシュ)します。
逆に、
AppleScript+Xcode GUIア.key
AppleScript+Xcode GUIアプ.key
AppleScript+Xcode GUIアプリ.key
AppleScript+Xcode GUIアプリ ソース.key
AppleScript+XCODE GUアプリ ソースブック.key
GUIアプリ ソースブックのコピー.key
ではクラッシュが発生しません(なんでや?)。
なんでこの組み合わせでクラッシュが発生するのか、macOS 10.12の時代に発生していた「日本語IMからのファイル名の入力時に余計なUnicode文字が入ることで、ファイル処理できなくなるバグ」の再現かと考え、このファイル名をテキストエディタにコピー&ペーストで表示させたものの、
とくに謎のゴミ文字が入力されているということはないようです。
ちなみに、「AppleScript+Xcode GUIアプリ ソースブック」は企画検討中の電子書籍です。
AppleScript名:Crash Test Script_Keynote.scpt |
— – Created by: Takaaki Naganoya – Created on: 2025/04/19 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — tell application "Keynote" tell front document set curFile to (file of it) end tell end tell set curFile to curFile as alias |
AppleScript名:Crash Test Script_PAGES.scpt |
— – Created by: Takaaki Naganoya – Created on: 2025/04/19 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — tell application "Pages" tell front document set curFile to (file of it) end tell end tell set curFile to curFile as alias |
AppleScript名:Crash Test Script_Numbers.scpt |
— – Created by: Takaaki Naganoya – Created on: 2025/04/19 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — tell application "Numbers" tell front document set curFile to (file of it) end tell end tell set curFile to curFile as alias |
AppleScript名:Crash Test Script_Pixelmator Pro.scpt |
— – Created by: Takaaki Naganoya – Created on: 2025/04/19 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — tell application "Pixelmator Pro" tell front document set curFile to (file of it) end tell end tell set curFile to curFile as alias |
AppleScript名:Crash Test Script_CotEditor.scpt |
— – Created by: Takaaki Naganoya – Created on: 2025/04/19 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — tell application "CotEditor" tell front document set curFile to (file of it) end tell end tell set curFile to curFile as alias |
AppleScript名:only choose file.scpt |
set aFol to (choose file) as string set anAlias to aFol as alias |
«class isot»が復活していた?
Mac OS X 10.5で、dateオブジェクトをISO日付形式に変換する«class isot»が動作しなくなっていました。
そして、もうこの機能はずっと動かなくなったままなのかと思っていましたが……現在動いていることに気づきました。
「AppleScript最新リファレンス」の初版執筆時には使えないことを確認していたので、OS X 10.11ぐらいの時代には動かなかったようです。
その後、手元の古いMacで確認してみたところ、
macOS 10.13:動く
macOS 10.14:動く
と、割と各バージョンで動いていることを確認。最新版のmacOS 15.5βでも動いています。
set testd to ((current date) as «class isot») as string
–> "2025-04-13T22:28:56"
★Click Here to Open This Script
目下、この機能はPiyomaru Softwareの電子書籍に掲載しているコマンドリファレンスに記載していませんが、記述を復活させるべきか悩ましいところです。
個人的には、Cocoaの機能を用いてISO日付フォーマットとの相互変換を行なっています。
AppleScript名:ISO8601日付文字列を生成 v2.scptd |
use AppleScript version "2.5" use scripting additions use framework "Foundation" set aDate to getDateInternational(2022, 10, 21, 0, 0, 0, "JPT") –―year, month, date, hour, minute, second, time zone abbreviation. –> date "2022年10月21日 金曜日 0:00:00" set bStr to retISO8601DateTimeString(aDate, "JPT") as string –> "2022-10-21T00:00:00+09:00" –NSDate -> ISO8601 Date & Time String on retISO8601DateTimeString(targDate, timeZoneAbbreviation) set theNSDateFormatter to current application’s NSDateFormatter’s alloc()’s init() theNSDateFormatter’s setDateFormat:"yyyy-MM-dd’T’HH:mm:ssZZZZZ" — Five zeds to get a colon in the time offset (except with GMT). theNSDateFormatter’s setTimeZone:(current application’s NSTimeZone’s timeZoneWithAbbreviation:(timeZoneAbbreviation)) return (theNSDateFormatter’s stringFromDate:targDate) as text end retISO8601DateTimeString –Make a GMT Date Object with parameters from a given time zone. on getDateInternational(aYear, aMonth, aDay, anHour, aMinute, aSecond, timeZoneAbbreviation) set theNSCalendar to current application’s NSCalendar’s currentCalendar() theNSCalendar’s setTimeZone:(current application’s NSTimeZone’s timeZoneWithAbbreviation:(timeZoneAbbreviation)) set theDate to theNSCalendar’s dateWithEra:1 |year|:aYear |month|:aMonth |day|:aDay hour:anHour minute:aMinute |second|:aSecond nanosecond:0 return theDate as date end getDateInternational |
AppleScript Dropletのバグっぽい動作が「復活」(macOS 15.5β)
ファイルのドラッグ&ドロップを受け付ける「ドロップレット」の異常動作がmacOS 10.12からずっと続いてきました。AppleScriptドロップレットに対して(Finderから)ファイルをドラッグ&ドロップすると、欠落するものが出てくるという現象です。
昨年、macOS 15.2betaか15.1あたりでこのバグが解消されたように見えました。
「見えました」というのは、一応現象としては観測できつつも、その動作を意図して実現していないんじゃないか、という懸念があったためです。つまり、Appleの現場なりマネージャー級で意思決定が行われた成果ではなさそうだ、と判定。
この動作がmacOS 15.5βで以前と同様の動作に戻っている(バグ的な動作)ことを観測しています。
on open droppedFiles
set fileCount to count of droppedFiles
display dialog “ドロップされたファイルの数: ” & fileCount buttons {“OK”} default button 1
end open
一応、ドラッグ&ドロップされたファイル/フォルダの受付で取りこぼしが出ないように対策は(Scripter側で工夫して)できているのですが、上記のように一般に知られている単純な受信コードでは対処できません。
ChatGPTをはじめとするLLMでは、上記のような単純なコードを出力することを確認しています。そして、Apple側がOSに不具合を発生させた場合にはLLMが出力するコードでは対処できません。
そして、macOSのヘルプメニューから表示できる「AppleScriptヘルプ」に書かれているドロップレットのコードがまともに動かないというのでは、話になりません。
NaturalLanguage.frameworkを用いて日本語テキストの形態素解析を行う
NSLinguisticTaggerの後継であるNaturalLanguage(NLTokenizer)を用いて、日本語テキストの形態素解析を行なってみました。
この手の頭を使わないプログラミングだとChatGPTに丸投げすれば、(デバッグ作業が必要な)AppleScriptのコードが出てくるので、走らせて直して……で、15分ほどでしょうか。
NLTokenizerの処理結果は、間違ってはいないものの、相変わらず固有名詞を無視しまくるので、実用レベルにあるとはいえません。自分でAppleScriptで書いた簡易日本語パーサー「easyJParse」と処理結果が変わらないので、頑張ってNLTokenizerを使う意義はほとんどないといえます(日本語を処理すると品詞情報を返して来ないので余計に……)。
AppleScript名:NaturalLanguage.frameworkを用いて日本語テキストの形態素解析を行う.scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/04/10 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript use framework "Foundation" use framework "NaturalLanguage" use scripting additions set inputText to current application’s NSMutableString’s stringWithString:"私の名前は長野谷です。" — NLTokenizerのインスタンスを作成し、単語単位で設定 set tokenizer to current application’s NLTokenizer’s alloc()’s initWithUnit:(current application’s NLTokenUnitWord) tokenizer’s setString:inputText tokenizer’s setLanguage:(current application’s NLLanguageJapanese) — 解析範囲の取得 set textLength to (inputText’s |length|()) as integer set theRange to current application’s NSMakeRange(0, textLength) — トークンを格納するリスト set tokenList to {} — トークン範囲を1つずつ取得して、文字列を抽出 set currentIndex to tokenizer’s tokenRangeAtIndex:0 repeat while (currentIndex’s location < (textLength)) set subStr to (inputText’s substringWithRange:currentIndex) copy (subStr as string) to end of tokenList — 次のトークン範囲へ進む set nextIndex to (currentIndex’s location) + (currentIndex’s |length|()) if nextIndex ≥ textLength then exit repeat set currentIndex to tokenizer’s tokenRangeAtIndex:nextIndex end repeat return tokenList –> {"私", "の", "名前", "は", "長野", "谷", "です"} |
XIBから作るステータスメニューUI
AppleScriptで作るステータスメニューは、プログラムで動的に作るケースが圧倒的に多いところですが、Xcode上でCocoa Applicationを作成する場合にはXIBで(=XcodeのInterface Builder上で)作ったほうが簡単だし使い勝手がいいので、作成方法を確認しておきました。
▲Interface Builder上でメニューをCocoa binding(Xcode 16.3)
AppleScript名:AppDelegate.applescript |
— — AppDelegate.applescript — statusMenuFromXIB — — Created by Takaaki Naganoya2 on 2025/03/27. — — script AppDelegate property parent : class "NSObject" — IBOutlets property theWindow : missing value property theStatMenu : missing value –xib上で作っておいたメニュー。Cocoa Binding on applicationWillFinishLaunching:aNotification set theIcon to current application’s NSImage’s imageNamed:(current application’s NSImageNameComputer) set statusItem to (current application’s NSStatusBar’s systemStatusBar())’s statusItemWithLength:(current application’s NSVariableStatusItemLength) statusItem’s setTitle:("PIYOMARU") statusItem’s setImage:theIcon statusItem’s setHighlightMode:true statusItem’s setMenu:theStatMenu statusItem’s popUpStatusItemMenu:theStatMenu end applicationWillFinishLaunching: on applicationShouldTerminate:sender — Insert code here to do any housekeeping before your application quits return current application’s NSTerminateNow end applicationShouldTerminate: end script |
iWork Appsがバージョン14.4にアップデート
iWork Apps(Keynote、Pages、Numbers)がバージョン14.4にアップデートしました。各アプリのAppleScript用語辞書に変更はありません。
Pagesで継続して発生している、現在画面上で表示中のページ+2見開きのページ上のオブジェクト情報の取得/操作が行えない現象については、修正されていません。
Apple側はこれをバグとも思っていないようですし、修正するつもりもないのでしょう。この不具合に対処するために、情報取得する対象のページを強制的に表示するよう指示する必要があることでしょう(そんな機能はないので、GUI Scriptingで?)。
Dock Menu
Dock Menuを表示するAppleScriptアプリです。探すと意外と情報がまとまっていないので、掲載しておきます。
Dock Menuについては、スクリプトエディタやスクリプトメニュー、アプレットなどで動作する通常のAppleScriptでは利用できませんが、Xcode上で作成するアプリやCocoa AppleScript Applicationでは利用できます。このあたりは、どのぐらいAppleScriptの実行環境がアプリのイベントをAppleScript側に提供しているかどうかによります。
スクリプトエディタ上で作成するアプレットの一種、Cocoa-AppleScript Appletを用いると、Dock Menuを動的に作ることが可能です(後述)。
–> Download Xcode Project Archive
AppleScript名:AppDelegate.applescript |
— — AppDelegate.applescript — dock menu — — Created by Takaaki Naganoya2 on 2025/04/05. — — script AppDelegate property parent : class "NSObject" — IBOutlets property theWindow : missing value property dockMenu : missing value on applicationWillFinishLaunching:aNotification end applicationWillFinishLaunching: on applicationShouldTerminate:sender return current application’s NSTerminateNow end applicationShouldTerminate: –Dock Menu Enabled on applicationDockMenu:(aNotification) return dockMenu end applicationDockMenu: on clicked:aSender set aTag to (tag of aSender) as integer display dialog (aTag as string) end clicked: end script |
Cocoa-AppleScriptアプレット版はこちらです。macOS 12以降では、Finder上でRosettaを利用して実行するように指定する必要があります。
アプレット本体側のscriptではなく、ランタイム側の「CocoaAppletAppDelegate.scpt」を書き換える必要があります。これを読み返すと、つくづく「いきなりこの内容をScripterに使わせようとしたのは無理があった」と感じます。内容が、macOS 10.7当時のScripterのCocoaへの理解度を考えると難解すぎです。
–> Download Cocoa-AppleScript Applet
AppleScript名:CocoaAppletAppDelegate.scpt |
— — CocoaAppletAppDelegate.applescript — Cocoa-AppleScript Applet — — Copyright 2011 {Your Company}. All rights reserved. — — This application delegate emulates the OSA script applet by loading "main.scpt" from the — "Scripts" folder in the application resources and invoking the traditional run/open/reopen/quit — handlers in response to Cocoa application delegate methods being called. — — This is provided in source form so that you may customize or replace it if your needs go — beyond the basic applet handlers. — — Some of these methods must guard against re-entrancy, because invoking the main.scpt — handler may end up invoking the event handler inherited from the current application, — which calls the application delegate’s method again. script CocoaAppletAppDelegate property parent : class "NSObject" property mainScript : missing value — the applet’s main.scpt property didOpenFiles : false — true = the application opened documents during startup property isOpeningFiles : false — re-entrancy guard: true = in the process of opening files property isReopening : false — re-entrancy guard: true = in the process of re-opening property isQuitting : false — re-entrancy guard: true = in the process of quitting on applicationWillFinishLaunching:aNotification — Insert code here to initialize your application before any files are opened — Emulate an OSA Applet: Load the main script from the Scripts resource folder. try set my mainScript to load script (path to resource "main.scpt" in directory "Scripts") on error errMsg number errNum — Perhaps this should silently fail if it can’t load the script; that way, a Cocoa applet — can just have Cocoa classes and no main.scpt. display alert "Could not load main.scpt" message errMsg & " (" & errNum & ")" as critical end try end applicationWillFinishLaunching: on applicationDidFinishLaunching:aNotification — Insert code here to do startup actions after your application has initialized if mainScript is missing value then return — Emulate an OSA Applet: Invoke the "run" handler. — If we have already opened files during startup, don’t invoke the run handler. if didOpenFiles then return try tell mainScript to run on error errMsg number errNum if errNum is not -128 then display alert "An error occurred while running" message errMsg & " (" & errNum & ")" as critical end if end try — TODO: Read the applet’s "stay open" flag and quit if it’s false or unspecified. — For now, all Cocoa Applets stay open and require the run handler to explicitly quit, — which is arguably more correct for a Cocoa application, anyway. (* if not shouldStayOpen then quit end if *) end applicationDidFinishLaunching: on applicationShouldHandleReopen:sender hasVisibleWindows:flag — Insert code here to perform actions in response to a "reopen" event if mainScript is missing value then return true — Guard against re-entrancy. if not isReopening then set isReopening to true — Emulate an OSA Applet: Invoke the "reopen" handler. If there isn’t one, let the application object — handle reopen (this is different from an OSA applet, which would do nothing if there is no handler; — this way, the application will perform the usual "create untitled document" behavior by default). try tell mainScript to reopen set isReopening to false return false on error errMsg number errNum if errNum is not -128 then display alert "An error occurred while reopening" message errMsg & " (" & errNum & ")" as critical end if end try set isReopening to false end if return true end applicationShouldHandleReopen:hasVisibleWindows: on |application|:sender openFiles:filenames — Insert code here to perform actions in response to an "open documents" event — Remember that we opened files, to avoid invoking the "run" handler later. set didOpenFiles to true — Guard against re-entrancy. if not isOpeningFiles and mainScript is not missing value then set isOpeningFiles to true try — Convert all the filenames from NSStrings to script strings set theFilenameStrings to {} repeat with eachFile in filenames set theFilenameStrings to theFilenameStrings & (eachFile as text) end repeat tell mainScript to open theFilenameStrings set isOpeningFiles to false tell sender to replyToOpenOrPrint:(current application’s NSApplicationDelegateReplySuccess) on error errMsg number errNum if errNum = -128 then tell sender to replyToOpenOrPrint:(current application’s NSApplicationDelegateReplyCancel) else display alert "An error occurred while opening file(s)" message errMsg & " (" & errNum & ")" as critical tell sender to replyToOpenOrPrint:(current application’s NSApplicationDelegateReplyFailure) end if end try set isOpeningFiles to false else tell sender to replyToOpenOrPrint:(current application’s NSApplicationDelegateReplyFailure) end if end |application|:openFiles: on applicationShouldTerminate:sender — Insert code here to do any housekeeping before your application quits — Guard against re-entrancy. if not isQuitting and mainScript is not missing value then set isQuitting to true — Emulate an OSA Applet: Invoke the "quit" handler; if the handler returns, it has fully — handled the quit message and we should not quit, otherwise, it calls "continue quit", — which returns error -10000. try tell mainScript to quit set isQuitting to false return current application’s NSTerminateCancel on error errMsg number errNum — -128 means there is no quit handler — -10000 means the handler did "continue quit" if errNum is not -128 and errNum is not -10000 then display alert "An error occurred while quitting" message errMsg & " (" & errNum & ")" as critical end if end try set isQuitting to false end if return current application’s NSTerminateNow end applicationShouldTerminate: –Dock Menu Enabled on applicationDockMenu:(aNotification) set aMenu to current application’s NSMenu’s alloc()’s init() set aMenuItem to (current application’s NSMenuItem’s alloc()’s initWithTitle:"Dock Menuだよ" action:"actionHandler:" keyEquivalent:"") (aMenuItem’s setTarget:me) (aMenu’s addItem:aMenuItem) return aMenu end applicationDockMenu: on actionHandler:sender set aTag to (tag of sender) as string set aTitle to (title of sender) as string display dialog (aTitle as string) end actionHandler: end script |
Xcode 16.3で用語辞書にdebugコマンドを追加?
Xcode 16.3のAppleScript用語辞書を調べていたら、以前のバージョンから変更が加わっていることを見つけました。もっと前のバージョンから変更されていたのかもしれませんが (Xcode 16.2で変更されていました。16.xで変更?)、XcodeのAppleScript用語辞書は機能が壊滅状態なので、まさかアップデートされるとは思ってもみませんでした。
正直なところ、XcodeのAppleScript用語辞書は「これで何をしろと?」という出来で、よく言って「Apple社内のエンジニアが仕事をしているフリ」、正直なところ「何を実装しているのか作成者が目的を完全に見失っている」ような内容です。命令1つ2つを追加したところで、使い物になりません。XcodeのAppleScript用語辞書まわりは、完全にゼロから作り直さないと、何もできない内容です。
XcodeのAppleScript用語辞書こそ、サンプルが掲載されていないと実装側の考えがわからない内容の代表例です。作業者の知能指数を疑うレベルであり、話になりません。
まして、debugコマンドはAppleScriptで作成したアプリケーションでは使えないコマンドなので、GUI側からも使ったことがありません。
NSIndexSetを作成し、各index要素を取り出す
NSIndexSetを作成し、そこから各インデックス(数値)を取り出すAppleScriptです。
{3, 4, 5, 6, 7}
から構成されるNSIndexSetを作成して処理すると、
{3, 4, 5, 6, 7}
が返ってきます。
NSTableViewで複数行選択時の行インデックスを取得する際に、NSIndexSetで結果が返ってきたので、ChatGPTに書かせてみました。
本Script内でNSIndexSet内の各インデックス数値に対してオフセット加算ができるようにしているのは、このNSTableViewで1行目が0と返ってくる仕様のためで、AppleScriptと合わせるために+1のオフセット加算を行う必要があったためこのようになっています。
初回から動くコードは返してこなかったものの、何回かやりとりしている間に、書き直せばなんとかなりそうだったので、引き継いで手で書いてみました。CocoaのAPI呼び出しはプログラミングではなくパズルみたいなものなので、ChatGPTに向いているといえば向いているのですが、AppleScriptObjCがBlocks構文を呼べないとかいった前提条件が認識されていないようなので、「手で描き直した方が速い」ということに。
NSNotFoundについては、Apple側も真面目に実装している風ではない(これを返すことが徹底されているわけではない、と)ので、あてにできないと感じています。
AppleScript名:NSIndexSetを作成し、各index要素を取り出す.scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/03/30 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" — Yosemite (10.10) or later use framework "Foundation" use scripting additions — サンプルのNSIndexSetを作成(3, 4, 5, 6, 7 を含む) set indexSet to current application’s NSIndexSet’s indexSetWithIndexesInRange:(current application’s NSMakeRange(3, 5)) set aList to getEachIndexFromIndexSet(indexSet, 0) of me –> {3, 4, 5, 6, 7} — インデックスを取得する処理 on getEachIndexFromIndexSet(indexSet, offsetNum) set indexList to {} set currentIndex to indexSet’s firstIndex() — 最初のインデックスを取得 repeat while currentIndex ≠ (current application’s NSNotFound) — 取得したインデックスをAppleScriptのリストに追加 if currentIndex > 9.99999999E+8 then exit repeat set end of indexList to ((currentIndex as number) + offsetNum) — 次のインデックスを取得 set currentIndex to indexSet’s indexGreaterThanIndex:currentIndex end repeat return indexList end getEachIndexFromIndexSet |
AppleScript名:NSIndexSetを作成し、各index要素を取り出す(書き換え後).scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/03/30 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" — Yosemite (10.10) or later use framework "Foundation" use scripting additions — サンプルのNSIndexSetを作成(3, 4, 5, 6, 7 を含む) set indexSet to current application’s NSIndexSet’s indexSetWithIndexesInRange:(current application’s NSMakeRange(3, 5)) set aList to getEachIndexFromIndexSet(indexSet, 0) of me –> {3, 4, 5, 6, 7} — インデックスを取得する処理 on getEachIndexFromIndexSet(indexSet, offsetNum) set indexList to {} set currentIndex to indexSet’s firstIndex() — 最初のインデックスを取得 — 取得したインデックスをAppleScriptのリストに追加 repeat while currentIndex ≤ 9.99999999E+8 set end of indexList to ((currentIndex as number) + offsetNum) — 次のインデックスを取得 set currentIndex to indexSet’s indexGreaterThanIndex:currentIndex end repeat return indexList end getEachIndexFromIndexSet |
Keynoteの現在のスライド上で選択中のテキストをもとに、後続の記事トビラページのタイトルに内容を設定 v2a
Keynote書類の現在表示中のスライド上で選択中のテキストアイテム(ボックス)の内容をもとに、後続の記事トビラページのタイトルを設定するAppleScriptです。Keynote 14.3+macOS 15.4betaで動作確認していますが、とくにバージョン依存した書き方などは行なっていません。
また、本来はiWork汎用オブジェクトの座標によるソート機能をライブラリとして独立させ、バンドル形式のScript書類に入れてあったのですが、Blog掲載のためにフラットなScriptに書き換えています。
本来、Keynoteのようにスライドのインデント(レベル変更)を行って階層構造を形成できるアプリでは、それぞれのスライド(ページ)のレベルを取得したり変更できることが望ましいのですが、Keynoteの機能セットの範囲内ではAppleScriptからそのような操作は行えません。そこで、各スライドのベースレイアウトに何を用いているかによって擬似的にレベルを判定しています。
KeynoteのAppleScript対応機能はiWork Apps中では屈指の対応度を誇っていますが、レイアウトした画像の内容データにアクセスできないのと、各スライドのレベルの取得/変更が行えない点がものすごく残念です。
電子書籍「Cocoa Scripting Course」の作成用に、以前にも作ったことがあるかもしれませんが、ふたたび作ってしまいました。ちょっと大きめの書き捨てScriptです。
▲Keynote書類の章トビラ上で章の記事内容を示すテキストボックスを選択した状態で本AppleScriptを実行。複数のボックスを選択してあっても、座標値でソートして順番を決定
▲各記事のトビラページに、章トビラから取得したテキストを設定。以下、繰り返し
AppleScript名:現在のスライド上で選択中のテキストをもとに、後続の記事トビラページのタイトルに内容を設定 v2a.scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/03/23 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" — Yosemite (10.10) or later use framework "Foundation" use scripting additions –use iwoSortLib : script "iWorkObjSortLib" tell application "Keynote" tell front document –章扉の上にあるテキストアイテム(各記事タイトルが入っているものと想定、複数可能)を取得 set aSelList to selection set bSelList to {} set sCount to count every slide –画面上で選択しておいたオブジェクトのうち、text itemのみを抽出(念のため) repeat with i in aSelList set j to contents of i set aClass to class of j if aClass is text item then set the end of bSelList to j end if end repeat set cSelList to sortIWorkObjectsByPositionAndRetObjRef(bSelList, {"positionX", "positionY"}, {true, true}) of iWorkObjSort –章トビラ上のテキストアイテム(複数可)をループしつつ、中のテキストを行ごとに分解してリスト化 –(連結前にあらかじめX座標をもとにソートしておいたほうがいい??) set textList to {} repeat with i in cSelList set tmpCon to object text of i set tmpList to paragraphs of tmpCon set textList to textList & tmpList end repeat –return textList –章トビラのページの情報を取得 tell current slide set curSlideNum to slide number set curSlideLayout to name of base layout end tell –章トビラの次のページの情報を取得(ここが必ず記事トビラであるという前提のもとに処理) tell slide (curSlideNum + 1) set nextSlideLayout to name of base layout end tell set targSlideList to {} repeat with i from (curSlideNum + 1) to sCount tell slide i set tSTheme to name of base layout if tSTheme = curSlideLayout then –章トビラを検出したら処理終了 exit repeat else if tSTheme = nextSlideLayout then –扉+1ページのスライド(記事カバー)を検出したら記録 set the end of targSlideList to i end if end tell end repeat –return targSlideList set iCount to 1 repeat with i in targSlideList try set tmpT to contents of item iCount of textList tell slide i set object text of default title item of it to tmpT end tell set iCount to iCount + 1 on error return end try end repeat end tell end tell –ライブラリとしてバンドル形式のAppleScript書類に組み込んでいたものをBlog掲載用に展開した script iWorkObjSort property parent : AppleScript use AppleScript use framework "Foundation" use framework "AppKit" use scripting additions script spd property aaSel : {} end script –Keynote上のiWork ObjをXY座標でソートして結果を返す(App Obj情報はitem noだけ) on sortIWorkObjectsByPosition(aaSel, sortLabelLIst, sortDirectionList) tell application "Keynote" set aVer to version if aVer < "12.0" then return tell front document set posList to {} set aCount to 1 repeat with ii in aaSel set jj to contents of ii set bClass to class of jj tell jj set {posX, posY} to position try set tmpStr to object text as string on error set tmpStr to "" end try end tell set the end of posList to {positionX:posX, positionY:posY, objID:aCount, myStr:tmpStr} set aCount to aCount + 1 end repeat –座標データをもとにソート set sortedList to sortRecListByLabel(posList, sortLabelLIst, sortDirectionList) of me end tell end tell return sortedList end sortIWorkObjectsByPosition –Keynote上のiWork ObjをXY座標でソートして結果を返す(App Objだけ返す) on sortIWorkObjectsByPositionIncludingObjRef(aaSel, sortLabelLIst, sortDirectionList) tell application "Keynote" set aVer to version if aVer < "12.0" then return tell front document set posList to {} set aCount to 1 repeat with ii in aaSel set jj to contents of ii set bClass to class of jj tell jj set {posX, posY} to position try set tmpStr to object text as string on error set tmpStr to "" end try end tell set the end of posList to {positionX:posX, positionY:posY, objID:aCount, myStr:tmpStr} set aCount to aCount + 1 end repeat –座標データをもとにソート set sortedList to sortRecListByLabel(posList, sortLabelLIst, sortDirectionList) of me –データを返す配列にiWork Object への参照を含める set sCount to 1 repeat with i from 1 to (length of aaSel) set tmpID to objID of contents of item i of sortedList set tmpObj to contents of item tmpID of aaSel set (item tmpID of sortedList) to (item tmpID of sortedList) & {objRef:tmpObj} end repeat end tell end tell return sortedList end sortIWorkObjectsByPositionIncludingObjRef –Keynote上のiWork ObjをXY座標でソートして結果を返す(App Obj入りのリストを返す) on sortIWorkObjectsByPositionAndRetObjRef(aaSel, sortLabelLIst, sortDirectionList) tell application "Keynote" set aVer to version if aVer < "12.0" then return tell front document set posList to {} set aCount to 1 repeat with ii in aaSel set jj to contents of ii set bClass to class of jj tell jj set {posX, posY} to position try set tmpStr to object text as string on error set tmpStr to "" end try end tell set the end of posList to {positionX:posX, positionY:posY, objID:aCount, myStr:tmpStr} set aCount to aCount + 1 end repeat –座標データをもとにソート set sortedList to sortRecListByLabel(posList, sortLabelLIst, sortDirectionList) of me –データを返す配列にiWork Object への参照を含める set sCount to 1 set retList to {} repeat with i from 1 to (length of aaSel) set tmpID to objID of contents of item i of sortedList set tmpObj to contents of item tmpID of aaSel set the end of retList to tmpObj end repeat end tell end tell return retList end sortIWorkObjectsByPositionAndRetObjRef –リストに入れたレコードを、指定の属性ラベルの値でソート on sortRecListByLabel(aRecList as list, aLabelStr as list, ascendF as list) set aArray to current application’s NSArray’s arrayWithArray:aRecList set aCount to length of aLabelStr set sortDescArray to current application’s NSMutableArray’s new() repeat with i from 1 to aCount set aLabel to (item i of aLabelStr) set aKey to (item i of ascendF) set sortDesc to (current application’s NSSortDescriptor’s alloc()’s initWithKey:aLabel ascending:aKey) (sortDescArray’s addObject:sortDesc) end repeat return (aArray’s sortedArrayUsingDescriptors:sortDescArray) as list end sortRecListByLabel end script |
Numbersで選択範囲のdateの年を+1する
Numbersの書類上で選択中のセル内の日付形式データがあったら、yearを+1(インクリメント)するAppleScriptです。macOS 15.4beta+Numbers 14.3で動作確認していますが、OSバージョンおよびNumbersのバージョンに依存する部分はありません。
ものすごくつまらなくて、ものすごく用途が狭いScriptですが、実際に書いてみたら意外と手間取る感じでした。
当然、+1するだけではなくー1する機能も作ってあり、呼び出し部分を差し替えればー1するようになります。macOS標準搭載のスクリプトメニューに入れて実行することを想定しています。
AppleScript名:選択範囲のdateの年を+1する.scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/03/10 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" use framework "Foundation" use scripting additions tell application "Numbers" tell front document tell active sheet try set theTable to first table whose class of selection range is range set cellList to cell of selection range of theTable set valList to value of cell of selection range of theTable set aLen to length of cellList on error display notification "Numbers: There is no selection" return end try set bValList to {} repeat with i in valList set j to contents of i set aClass to class of j if aClass = date then set j to incremenetYearOf(j) of me end if set the end of bValList to j end repeat repeat with i from 1 to aLen set aVal to contents of item i of bValList set aCell to contents of item i of cellList ignoring application responses set value of aCell to aVal end ignoring end repeat end tell end tell end tell on incremenetYearOf(aDate) set {bYear, bMonth, bDate} to {year of aDate, month of aDate, day of aDate} set bYear to bYear + 1 set {year of aDate, month of aDate, day of aDate} to {bYear, bMonth, bDate} return aDate end incremenetYearOf on decremenetYearOf(aDate) set {bYear, bMonth, bDate} to {year of aDate, month of aDate, day of aDate} set bYear to bYear – 1 set {year of aDate, month of aDate, day of aDate} to {bYear, bMonth, bDate} return aDate end decremenetYearOf |
CotEditor v5.1.1でprint機能に微修正
CotEditorのメンテナーの@1024jp氏の談話によると、
https://x.com/1024jp/status/1896829943587508664
CotEditor v 5.1.0 → 5.1.1 でAppleScript経由のprint機能がうまく動作していなかったのを動作するように修正したとのこと。
ここのところ、macOS側の機能の不全により、AppleScript経由でのprint機能の実行がうまく動作していないケースが発生しており、それに対処したとのこと。
CotEditor v5.1.0でdocumentのcontentsにeditable属性を追加
CotEditorがバージョン5.1.0にアップデートしました(翌日、5.1.1にアップデートしましたが、AS的には差はありません)。
アップデート内容は多岐に渡っていますが、AppleScript的には、documentのcontentsにeditable(boolean)属性が追加されました。
実際に、AppleScriptからこの属性値を操作して、想定どおりの挙動を行うかどうかをチェックしてみました。こうしたテストは開発者側でも行なっているかどうか怪しいところなので(とくにAppleのノーチェックぶりが目に余る)、大事な作業です。
想定どおりの動作を行なっているので、確認OKです!
AppleScript名:v51.0 document editable test.scpt |
tell application "CotEditor" tell front document set editable of it to true set edF to editable –> true set contents of it to "ABC" set editable of it to false set edF to editable –> false set contents of it to "CDE" end tell end tell |
画像をAppleScriptでアスキーアート化
指定の画像をアスキーアート化して、ダイアログ表示するAppleScriptです。
–> Download Script archive with Library
面倒だったのでChatGPTに書かせてみましたが、Retina Display対応や細かい箇所のエラーを取りきれず、途中から自分で書き換えました。
画像のアスキーアート化Scriptは、できることはわかっていつつも、これほど役立たずに時間の無駄なプログラムも珍しいところ。こういうのはChatGPTに書かせるのがお似合いです。
それにしても、けっこうこまめにコメント入れるもんですね。正しい内容かどうかは定かではありませんが……>ChatGPT
AppleScript名:画像をAppleScriptでアスキーアート化.scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/02/26 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" use framework "Foundation" use framework "AppKit" use scripting additions use radioLib : script "displayTextView" property retinaF : missing value — ファイル選択ダイアログで画像を選ぶ set imagePath to POSIX path of (choose file with prompt "画像を選択してください:") — ASCIIアートを生成 set asciiText to convertImageToASCII(imagePath) display text view asciiText main message "ASCII ART" sub message "Sub Message" with properties {font name:"Courier", size:12, width:620, height:620, color:{255, 255, 255}} on convertImageToASCII(imagePath) set asciiChars to "@%#*+=-:. " — 濃い→薄い順の文字列 — Retina用のスケールファクターを取得 set retinaF to (current application’s NSScreen’s mainScreen()’s backingScaleFactor()) as real — 画像を読み込む set imageURL to current application’s NSURL’s fileURLWithPath:imagePath set imageData to current application’s NSImage’s alloc()’s initWithContentsOfURL:imageURL — 論理サイズ(points単位)を取得("size"は予約語なのでエスケープ) set imageSize to imageData’s |size|() set {w_pt, h_pt} to {imageSize’s |width|, imageSize’s |height|} — リサイズ後の幅(文字数)を指定し、論理サイズに基づいてスケール計算 set newWidth to 80 set scaleFactor to newWidth / w_pt set newHeight to round (h_pt * scaleFactor * 0.55) — 文字の縦横比調整 — ピクセル単位のサイズに変換(Retina対応) set pixelWidth to newWidth * retinaF set pixelHeight to newHeight * retinaF set pixelSize to current application’s NSMakeSize(pixelWidth, pixelHeight) — 新しいピクセルサイズでNSImageを作成し、元画像を描画 set resizedImage to current application’s NSImage’s alloc()’s initWithSize:pixelSize resizedImage’s lockFocus() current application’s NSGraphicsContext’s currentContext()’s setImageInterpolation:(current application’s NSImageInterpolationHigh) imageData’s drawInRect:{{0, 0}, pixelSize} fromRect:{{0, 0}, imageSize} operation:(current application’s NSCompositingOperationCopy) fraction:1.0 resizedImage’s unlockFocus() — リサイズ後の画像からNSBitmapImageRepを取得 set resizedRep to current application’s NSBitmapImageRep’s imageRepWithData:(resizedImage’s TIFFRepresentation()) — 各ピクセルをASCII文字に変換(Retina対応のため、retinaFごとにサンプリング) set asciiArt to "" set stepX to retinaF as integer set stepY to retinaF as integer repeat with y from 0 to (pixelHeight – 1) by stepY repeat with x from 0 to (pixelWidth – 1) by stepX set pixelColor to (resizedRep’s colorAtX:(x * retinaF) y:(y * retinaF)) set r to pixelColor’s redComponent() set g to pixelColor’s greenComponent() set b to pixelColor’s blueComponent() set brightness to (r + g + b) / 3 set charIndex to round (brightness * ((count of asciiChars) – 1)) set asciiArt to asciiArt & (character (charIndex + 1) of asciiChars) end repeat set asciiArt to asciiArt & linefeed end repeat return asciiArt end convertImageToASCII |
XcodeのWorkspace Documentをクローズ後、再オープン
XcodeでオープンしているWorkspace Document(.xcodeproj)のパスを取得してクローズし、再度オープンするAppleScriptです。macOS 15.3上のXcode 16.2で動作をテストしました。
Xcodeでオープン中の.applescriptファイルの構文確認を行うために、いろいろ試行錯誤してみたものの、Xcodeでオープン中にはファイルへの書き込み権限を取得できない雰囲気が濃厚。Xcodeのエディタで表示中のコンテンツ(.applescript)の本文に文字列を突っ込んでみても、Xcodeに蹴られます。
# AppleScriptからの制御が封じられているので、まっとうな手口が使えません
これに対策するために、Xcode Projectをいったんクローズして、構文確認を行うとよさそうだったので、試作品を作ってみたものです。
AppleScript名:XcodeのWorkspace Documentをクローズ後、再オープン.scptd |
— – Created by: Takaaki Naganoya – Created on: 2025/02/21 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" — Yosemite (10.10) or later use framework "Foundation" use scripting additions tell application "Xcode" set xcVer to version tell workspace document 1 set xWSprojPath to path close end tell end tell –delay 1 tell application "Xcode" open ((POSIX file xWSprojPath) as alias) end tell |
AppleScript Explored 最新OS&Xcode対応 日本語版?
個人的な資料として、Shane Stanleyの電子書籍「AppleScript Explored」を日本語訳して使っておりました。部分的にmacOS最新版+Xcode最新版にスクリーンキャプチャを入れ替えて。
最初の版から、購入して読んでいましたが……実にいい本です。「もうちょっと画面キャプチャが多いとわかりやすいのに」とは思っていましたが、自分には書けないタイプの本です。
# そのフィードバックをもとに、画面キャプチャ主体の本「AppleScript+XcodeでつくるMacアプリ Xcode 14対応」を出したわけで
冗談半分でShaneに「日本語訳版を出してもいい?」と聞いてみたところ、快諾。えっ? いいの?!(^ー^;;;
いまXcodeで作っているAppleScriptのプロジェクトがあるのですが、こうした資料が充実していないと本当に辛いので、日本語版を出せることは個人的に素直に嬉しいです。
ただ、本書がOS X 10.8ぐらいの時代に書かれたという時代背景があり、そのあたりの状況認識を大幅に変更する必要はあると思います。当時はShaneがAppleScriptObjC Explorerを作っていたので、そうしたツールの存在を前提として書かれた部分もあるので、macOS 15時代+Xcode 16環境に合わせてアップデートする必要もあることでしょう。
Xcodeでオープン中のAppleScriptのフルパスを返す
Xcodeでオープン中のXcode Projectで表示中のファイルのフルパスを返すAppleScriptです。Xcodeで表示中のAppleScript書類(テキストベースの.applescript)をコンパイルして、省略表記した部分をすべて展開して元ファイルに書き戻すAppleScriptを書こうとして作ったものです。
Xcode.appのAppleScript用語辞書は、実用性がないというべきなのか、編集中のファイルに対する補助機能を作ろうとしても編集中のファイルのファイル名が取得できないとか、selectionの取得もできないとか、いまひとつ「使えない」という印象です(仕事をしているフリでもしてるんだろうか?>担当者)。
XcodeのAppleScript用語辞書には、日常的な作業を便利にするために必要とされる基礎的な機能が何もないので、「これでどうしろと?」と、途方に暮れてしまう出来です。
ただ、強引にファイル名の情報を取得しつつ(Windowのnameから取得)、Xcode Projectのルートフォルダからファイル検索することで、どうにかファイルのフルパスを取得してみました。取得したファイル名のファイルがProject内に複数存在する可能性もあるため、そのような場合にはどのファイルかを選択するように書いてみました(実にナンセンスな処理です)。
本Scriptでは、AppleScriptのプロジェクトに対して利用することを前提にしているため「.applescript」ファイルを取得する仕様になっていますが、別に「.m」でも「.swift」でも抽出できるように変更するのは容易です。
AppleScript名:Xcodeでオープン中のAppleScriptのフルパスを返す.scpt |
— – Created by: Takaaki Naganoya – Created on: 2025/02/14 — – Copyright © 2025 Piyomaru Software, All Rights Reserved — use AppleScript use scripting additions use framework "Foundation" set targString to " " & string id 8212 & " " –コピペやURL Events経由で受け渡すと壊れる tell application "Xcode" set xcVer to version —オープン中のファイル名(余計な文字列つき)を取得 tell window 1 set aStr to name end tell –Xcode Project書類のパスを取得 tell document 1 set aPrjPath to path of it end tell end tell set a1Str to parseFileNameFromXCodeWindowName(aStr, targString) of me if a1Str does not end with ".applescript" then return set pathString to current application’s NSString’s stringWithString:(aPrjPath) set parentFol to (pathString’s stringByDeletingLastPathComponent()) as string set aList to retFullPathUnderAFolderWithRecursiveFilterByFileName(parentFol, a1Str) of me if length of aList > 1 then set aRes to first item of (choose from list aList) else set aRes to first item of aList end if return aRes –> "/Users/me/Documents/testXC162/testXC162/AppDelegate.applescript" on parseFileNameFromXCodeWindowName(aStr, aDelim) set aCount to retFrequency(aStr, aDelim) of me set aRes to parseByDelim(aStr, aDelim) of me return item 2 of aRes end parseFileNameFromXCodeWindowName –指定文字列内の指定キーワードの出現回数を取得する on retFrequency(origText, aKeyText) set aRes to parseByDelim(origText, aKeyText) of me return ((count every item of aRes) – 1) end retFrequency on parseByDelim(aData, aDelim) set curDelim to AppleScript’s text item delimiters set AppleScript’s text item delimiters to aDelim set dList to text items of aData set AppleScript’s text item delimiters to curDelim return dList end parseByDelim on retFullPathUnderAFolderWithRecursiveFilterByFileName(aFol, aFileName) set anArray to current application’s NSMutableArray’s array() set aPath to current application’s NSString’s stringWithString:aFol set dirEnum to current application’s NSFileManager’s defaultManager()’s enumeratorAtPath:aPath repeat set aName to (dirEnum’s nextObject()) if aName = missing value then exit repeat set aFullPath to aPath’s stringByAppendingPathComponent:aName anArray’s addObject:aFullPath end repeat set thePred to current application’s NSPredicate’s predicateWithFormat_("lastPathComponent == %@", aFileName) set bArray to anArray’s filteredArrayUsingPredicate:thePred return bArray as list end retFullPathUnderAFolderWithRecursiveFilterByFileName |