diff --git a/Linker_QuickAction.sh b/Linker_QuickAction.sh index d82ebc4..8f4012b 100755 --- a/Linker_QuickAction.sh +++ b/Linker_QuickAction.sh @@ -1,20 +1,39 @@ #!/bin/zsh +# shellcheck shell=bash -# Linker Finder QuickAction -# version 1.0.0 +# Linker Finder QuickAction (workflow) +# version 1.0.1 export LANG=en_US.UTF-8 -export PATH=/usr/bin:/usr/sbin:/bin:/sbin:/usr/local/bin:/opt/local/bin:/opt/sw/bin:/sw/bin:$HOME/.local/bin:$HOME/local/bin:$HOME/bin:$HOME/Developer/bin +export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/opt/local/bin:/opt/sw/bin:/sw/bin:"$HOME"/.local/bin:"$HOME"/local/bin:"$HOME"/bin:"$HOME"/Developer/bin if ! command -v linker &>/dev/null ; then - account=$(id -u) - osascript &>/dev/null << EOT + xattr -c /usr/local/bin/linker 2>/dev/null + xattr -c /opt/local/bin/linker 2>/dev/null + xattr -c /opt/sw/bin/linker 2>/dev/null + xattr -c /sw/bin/linker 2>/dev/null + xattr -c "$HOME"/.local/bin/linker 2>/dev/null + xattr -c "$HOME"/local/bin/linker 2>/dev/null + xattr -c "$HOME"/bin/linker 2>/dev/null + xattr -c "$HOME"/Developer/bin/linker 2>/dev/null + chmod +x /usr/local/bin/linker 2>/dev/null + chmod +x /opt/local/bin/linker 2>/dev/null + chmod +x /opt/sw/bin/linker 2>/dev/null + chmod +x /sw/bin/linker 2>/dev/null + chmod +x "$HOME"/.local/bin/linker 2>/dev/null + chmod +x "$HOME"/local/bin/linker 2>/dev/null + chmod +x "$HOME"/bin/linker 2>/dev/null + chmod +x "$HOME"/Developer/bin/linker 2>/dev/null + if ! command -v linker &>/dev/null ; then + account=$(id -u) + osascript &>/dev/null << EOT beep tell application "System Events" - display notification "Linker main script not found!" with title "Linker [" & "$account" & "]" subtitle "❌ Error: \$PATH" + display notification "Main Linker shell script not found!" with title "Linker QuickAction [" & "$account" & "]" subtitle "❌ Error: \$PATH" end tell EOT - exit + exit + fi fi linker "$@" & diff --git a/README b/README index f2ee6b2..7629ad9 100644 --- a/README +++ b/README @@ -1,4 +1,4 @@ -Linker is a macOS shell script and Finder QuickAction to create relative or absolute symbolic links, hard links, Finder aliases, file clones or legacy file copies +Linker is a macOS shell script and Finder QuickAction to create relative or absolute symbolic links, hard links, Finder aliases, Bookmark files, file clones or legacy file copies Latest release: https://github.com/JayBrown/Linker/releases/latest @@ -16,14 +16,16 @@ Latest release: https://github.com/JayBrown/Linker/releases/latest * execute the following, if the script doesn't run: chmod +x ./linker -* install QuickAction (workflow) with Automator Installer +* Mojave & later: install QuickAction workflow with Automator Installer +* on older systems the QuickAction format is either not supported or might not work properly as a Service workflow, and you need to create your own Service workflow in Automator -In Finder you can just use the QuickAction from the contextual menu. +In Finder you can just use the QuickAction from the contextual menu (or the Service on older systems). You will have the following options on the command-line, for tool integration in file managers like Nimble Commander, or via the Touch Bar using BetterTouchTool: linker - -a : create Finder alias + -a : create Finder Alias + -b : create Bookmark file (same as Finder Alias on newer systems) -c : create cloned file copy (only on APFS) -d : create legacy file copy (true duplicate) -h : create hard link (only on HFS+ volumes) @@ -32,5 +34,5 @@ linker Please note that even the above options will still use the GUI for the user to select the destination directory. -Linker uses a code snippet from StackExchange: https://unix.stackexchange.com/a/85068/222515 +Linker uses a modified code snippet from StackExchange: https://unix.stackexchange.com/a/85068/222515 Linker comes bundled with the alisma tool by EclecticLight: https://eclecticlight.co/downloads diff --git a/linker b/linker index 4041a38..eeaf121 100755 --- a/linker +++ b/linker @@ -1,7 +1,7 @@ #!/bin/zsh # shellcheck shell=bash -# Linker v0.3 +# Linker v0.4 export LANG=en_US.UTF-8 export SYSTEM_VERSION_COMPAT=0 @@ -10,21 +10,32 @@ export PATH=/usr/bin:/usr/sbin:/bin:/sbin:/usr/local/bin:/opt/local/bin:/opt/sw/ uiprocess="Linker" procid="local.lcars.Linker" +# check system version sysvers=$(sw_vers -productVersion) +doublealias=false +noapfs=false vmaj=$(echo "$sysvers" | awk -F"." '{print $1}') if [[ $vmaj -lt 11 ]] ; then vmin=$(echo "$sysvers" | awk -F"." '{print $2}') - if [[ $vmin -lt 13 ]] ; then + if [[ $vmin -lt 13 ]] ; then # no APFS support noapfs=true - else - noapfs=false + if [[ $vmin -eq 12 ]] ; then + vfix=$(echo "$sysvers" | awk -F"." '{print $3}') + if [[ $vfix -lt 6 ]] ; then # potentially buggy support for new Alias format: needs to be separated from Bookmark file + doublealias=true + fi + elif [[ $vmin -lt 12 ]] ; then # old Alias format: needs to be separated from Bookmark file + doublealias=true + fi fi fi +# warning beep _sysbeep () { osascript -e 'beep' &>/dev/null } +# notify function _notify () { osascript &>/dev/null << EOT tell application "System Events" @@ -33,6 +44,7 @@ end tell EOT } +# relative path function (modified) _relpath () { # from here: https://unix.stackexchange.com/a/85068/222515 # both $1 and $2 are absolute paths beginning with / @@ -43,6 +55,7 @@ _relpath () { source=$1 # link location target=$2 # original file + # quick check for same directory if [[ $(dirname "$source") == $(dirname "$target") ]] ; then result=$(basename "$target") result=./$result @@ -65,7 +78,7 @@ _relpath () { ((count++)) done - if [[ $count -eq 1 ]] ; then + if [[ $count -eq 1 ]] ; then # override double-dot result=. fi @@ -87,7 +100,7 @@ _relpath () { fi fi - result=$(echo "$result" | sed "s-^\.\./--" 2>/dev/null) + result=$(echo "$result" | sed "s-^\.\./--" 2>/dev/null) # necessary because link doesn't exist yet! printf '%s\n' "$result" } @@ -146,11 +159,13 @@ if ! [[ $HOMEDIR ]] || ! [[ -d "$HOMEDIR" ]] ; then fi fi +# command-line options hardlink=false symlink=false absolute=false relative=false finderalias=false +bookmark=false duplicate=false fileclone=false cli=false @@ -168,6 +183,14 @@ elif [[ $1 == "-s" ]] ; then absolute=true cli=true shift +elif [[ $1 == "-b" ]] ; then + if $doublealias ; then + bookmark=true + else + finderalias=true + fi + cli=true + shift elif [[ $1 == "-a" ]] ; then finderalias=true cli=true @@ -194,11 +217,13 @@ if ! [[ $* ]] ; then exit fi +# check for multiple files multi=false if [[ $# -gt 1 ]] ; then multi=true fi +# quick check for missing files for filepath in "$@" do if ! [[ -e "$filepath" ]] ; then @@ -208,6 +233,7 @@ do fi done +# check for support directory and presence of alisma exportalisma=false supportdir="$HOMEDIR/Library/Application Support/$procid" bindir="$supportdir/bin" @@ -225,6 +251,7 @@ else fi fi +# export alisma CLI if $exportalisma ; then read -d '' alisma64 <<"EOA" yv66vgAAAAIBAAAHAAAAAwAAQAAAAOJgAAAADgEAAAwAAAAAAAFAAAABHIAAAAAO @@ -3457,6 +3484,7 @@ EOA chmod +x "$bindir/alisma" 2>/dev/null fi +# export additional path & check for availability of alisma export PATH=$PATH:"$bindir" xalisma=true if ! command -v alisma &>/dev/null ; then @@ -3473,7 +3501,15 @@ else xalisma=false fi fi +if $cli ; then # standard alias files can always be created via Finder + if $bookmark && ! $xalisma ; then + _sysbeep & + _notify "⚠️ Bookmark files unsupported!" "alisma CLI is missing!" + exit + fi +fi +# export icon, if necessary icon_loc="$supportdir/Linker.png" if ! [[ -f "$icon_loc" ]] ; then read -d '' icon64 <<"EOI" @@ -10298,69 +10334,134 @@ EOI echo "$icon64" | base64 -d -o "$icon_loc" 2>/dev/null fi +# launched from GUI: select method first if ! $cli ; then - opschoice=$(osascript 2>/dev/null << EOC + if ! $multi ; then + filebase=$(basename "$1") + titleadd="Source: $filebase" + else + titleadd="Sources: $# files" + fi + while true + do # primary option: symlink with relative path + opschoice=$(osascript 2>/dev/null << EOC tell application "System Events" activate set theLogoPath to POSIX file "$icon_loc" - set theOpsChoice to button returned of (display dialog "Please select the file operation." ¬ - buttons {"Cancel", "Other", "Symbolic Link (Relative)"} ¬ + set theOpsChoice to button returned of (display dialog "$titleadd" & return & return & "Please select the file operation." ¬ + buttons {"Quit", "Other", "Symbolic Link (Relative)"} ¬ default button 3 ¬ - cancel button "Cancel" ¬ + cancel button "Quit" ¬ with title "Linker" ¬ with icon file theLogoPath ¬ giving up after 180) end tell EOC - ) - if ! [[ $opschoice ]] ; then - exit - fi - if [[ $opschoice == "Symbolic Link (Relative)" ]] ; then - symlink=true - relative=true - else - if ! $noapfs ; then - opschoice=$(osascript 2>/dev/null </dev/null </dev/null </dev/null </dev/null </dev/null </dev/null </dev/null </dev/null </dev/null </dev/null @@ -10520,7 +10670,7 @@ do else errors=true fi - elif $absolute ; then + elif $absolute ; then # absolute path ln -h -s "$filepath" "$linkloc" &>/dev/null if ! [[ -L "$linkloc" ]] ; then errors=true @@ -10528,35 +10678,53 @@ do else errors=true fi - + + # bookmark file (only on systems older than macOS 10.12.6) + elif $bookmark ; then + alisma -a "$filepath" "$linkloc" &>/dev/null + if ! [[ -f "$linkloc" ]] ; then + errors=true + fi + + # Finder alias elif $finderalias ; then - if $xalisma ; then + if $xalisma ; then # use alisma to create new-style bookmark, but label as alias alisma -a "$filepath" "$linkloc" &>/dev/null - else + else # use Finder if alisma is not present or compatible: will produce bookmark or legacy alias, depending on system version finderquit=false - if ! pgrep "Finder" &>/dev/null ; then + if ! pgrep "Finder" &>/dev/null ; then # launch Finder first open -b com.apple.Finder 2>/dev/null finderquit=true fi - osascript &>/dev/null </dev/null </dev/null </dev/null fi fi if ! [[ -f "$linkloc" ]] ; then errors=true fi - + + # hard link elif $hardlink ; then if $multi ; then - if ! [[ -f "$filepath" ]] ; then + if ! [[ -f "$filepath" ]] ; then # only regular files for hardlinks _sysbeep & _notify "⚠️ Hard link impossible!" "Not a regular file: $filename" fserror=true @@ -10564,7 +10732,7 @@ EOF else device=$(df "$filepath" | awk '/^\//{print $1}') filesystem=$(diskutil info "$device" 2>/dev/null | awk '/File System Personality:/{print substr($0, index($0,$4))}') - if [[ $filesystem == "APFS" ]] ; then + if [[ $filesystem == "APFS" ]] ; then # no hardlinks on APFS _sysbeep & _notify "⚠️ Hard links unsupported!" "APFS: $device" fserror=true @@ -10576,7 +10744,7 @@ EOF if ! [[ -f "$linkloc" ]] ; then errors=true fi - else + else # no hardlinks across devices _sysbeep & _notify "⚠️ No hard links across devices!" "$linkdevice != $device" errors=true @@ -10591,14 +10759,15 @@ EOF if ! [[ -f "$linkloc" ]] ; then errors=true fi - else + else # no hardlinks across devices _sysbeep & _notify "⚠️ No hard links across devices!" "$linkdevice != $device" errors=true hardlinkerror=true fi fi - + + # legacy file copy (cp) : available on all systems elif $duplicate ; then if [[ -d "$filepath" ]] ; then if ! cp -f -r "$filepath" "$linkloc" &>/dev/null ; then @@ -10609,12 +10778,13 @@ EOF errors=true fi fi - + + # cloned file copy (cp -c): available only on APFS elif $fileclone ; then if $multi ; then device=$(df "$filepath" | awk '/^\//{print $1}') filesystem=$(diskutil info "$device" 2>/dev/null | awk '/File System Personality:/{print substr($0, index($0,$4))}') - if [[ $filesystem != "APFS" ]] ; then + if [[ $filesystem != "APFS" ]] ; then # not on non-APFS volumes if ! [[ $filesystem ]] ; then filesystem="n/a" fi @@ -10623,6 +10793,27 @@ EOF fserror=true errors=true else + linkdevice=$(df "$destination" | awk '/^\//{print $1}') + if [[ $linkdevice == "$device" ]] ; then + if [[ -d "$filepath" ]] ; then + if ! cp -f -r -c "$filepath" "$linkloc" &>/dev/null ; then + errors=true + fi + else + if ! cp -f -c "$filepath" "$linkloc" &>/dev/null ; then + errors=true + fi + fi + else # no clones across devices + _sysbeep & + _notify "⚠️ No cloned copies across devices!" "$linkdevice != $device" + errors=true + hardlinkerror=true + fi + fi + else + linkdevice=$(df "$destination" | awk '/^\//{print $1}') + if [[ $linkdevice == "$device" ]] ; then if [[ -d "$filepath" ]] ; then if ! cp -f -r -c "$filepath" "$linkloc" &>/dev/null ; then errors=true @@ -10632,50 +10823,47 @@ EOF errors=true fi fi - fi - else - if [[ -d "$filepath" ]] ; then - if ! cp -f -r -c "$filepath" "$linkloc" &>/dev/null ; then - errors=true - fi - else - if ! cp -f -c "$filepath" "$linkloc" &>/dev/null ; then - errors=true - fi + else # no clones across devices + _sysbeep & + _notify "⚠️ No cloned copies across devices!" "$linkdevice != $device" + errors=true + hardlinkerror=true fi fi fi + # loop notifications if ! $multi ; then linkloc_short="${linkloc/#$HOMEDIR/~}" if ! $errors ; then - _notify "✅ Success: $filename" "$method: $linkloc_short" + _notify "✅ Success: $filename" "$method $linkloc_short" else if ! $hardlinkerror && ! $fserror ; then _sysbeep & - _notify "❌ Error: $filename" "$method_err: $linkloc_short" + _notify "❌ Error: $filename" "$method_err $linkloc_short" fi fi else if $errors ; then _sysbeep & - _notify "❌ Error: $filename" "$method_err: $linkloc_short" + _notify "❌ Error: $filename" "$method_err $linkloc_short" else someok=true fi fi done +# final notifications (only for multiple files) if $multi ; then if ! $errors ; then - _notify "✅ Success!" "$method: $# files" + _notify "✅ Success!" "$method $# files" else if $someok ; then _sysbeep & _notify "☑️ Partial success!" "There was at least one error!" else _sysbeep & - _notify "❌ Errors" "$method_err: $# files" + _notify "❌ Errors" "$method_err $# files" fi fi fi