Skip to content

Commit 2c7c67e

Browse files
authored
Dev (#3)
Add scripts
1 parent ff60881 commit 2c7c67e

10 files changed

Lines changed: 238 additions & 35 deletions

File tree

.envrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
PATH_add .
2+
PATH_add bin
3+
PATH_add scripts

.github/workflows/build.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ jobs:
2222

2323
- name: Package ZIP
2424
run: |
25-
zip -j macOS-alias-utils.${{ github.ref_name }}.zip \
26-
bin/readalias \
27-
bin/mkalias \
25+
zip macOS-alias-utils.${{ github.ref_name }}.zip \
26+
bin/* \
27+
scripts/* \
2828
README.md \
2929
LICENSE
3030

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ DerivedData/
99
.netrc
1010
/Sources/**/generated*
1111
.vscode/launch.json
12+
/temp

README.md

Lines changed: 93 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ on some earlier verions of macOS.
1818
## Installation
1919

2020
Download the [latest release](https://github.com/heckman/macOS-alias-utils/releases/latest)
21-
and put the two universal binaries somewhere on your `PATH`.
21+
and put the two universal binaries in `bin` somewhere on your `PATH`.
22+
23+
The scripts in `scripts`can be installed the same way,
24+
or, if using the `Zsh` shell, they can be used as autoloadable functions
25+
by putting them somewhere on the `fpath`.
26+
(This is how I use them.)
2227

2328
Alternatively, build the binaries from source.
2429
You will neet to have Command Line Tools (CLT) installed,
@@ -29,48 +34,106 @@ From there, run `./build`.
2934
This should produce the `readalias` and `mkalias` binaries
3035
in a `./bin` directory.
3136

32-
## Features
33-
34-
At the moment, `readalias` simply prints the path to the original item
35-
and has no options beyond `--help` and `--version`,
36-
while `mkalias` can be given the `force` ( or `-f`) option
37-
indicating existing files should be overwritten--by default it
38-
is non-destructive.
37+
## Usage
3938

40-
Both respond appropriately to `--version` and `--help` options.
39+
Both binaries respond appropriately to `--version` and `--help` options.
4140
The help output includes descriptions their various exit codes.
4241

43-
### Future features
42+
The help text is also easy to find in their source code.
43+
44+
### readalias
45+
46+
Prints the path to the original file of an alias file.
47+
It takes no options, and has one required argument:
48+
49+
`readalias <alias-file>`
50+
51+
On error, nothing is printed to stdout,
52+
an error message is printed to stderr,
53+
and the exit status is non-zero.
54+
55+
### mkalias
56+
57+
Creates an alias file that points to the path of an original.
58+
It takes two required positional arguments, and one optional
59+
flag, which will cause `<alia-file>` to overwrite any existing file.
60+
Be default, the command will fail if the file already exists.
61+
62+
`mkalias [-f|--force] [--] <path-of-original> <alias-file>`
63+
64+
Never writes to stdout.
65+
On error, an message is printed to stderr,
66+
and the exit status is non-zero.
67+
68+
## Scripts
4469

45-
By default `mkalias` will fail
46-
rather than overwrite and existing file.
47-
The `force` (or `-f`) option will cause existing
48-
files to be clobbbered, although a directory
49-
will still not be overwritten.
70+
These scripts make use of the `readalias` and `mkalias` binaries.
71+
The funcionality provided by these scripts
72+
may eventually be merged into the binaries,
73+
or be made into new binaries themselves.
74+
The format of these scripts is such that
75+
the files can be used as autoloadable functions in Zsh
76+
by simply placeing them in a directory on the `fpath`.
77+
This is how I use them, which is why each script
78+
is a single function called from its body, and why
79+
the two helper functions have their own scripts.
5080

51-
I indend to offer a third behaviour, which,
52-
when the specified name for the alias is taken,
53-
generates a new name in the way the Finder does.
54-
This might become the default behaviour
55-
in which case we will need a new a command-line option
56-
for the fail-on-existing file behaviour.
81+
### isalias
5782

58-
The renaming strategy is this: Use the same name as the original,
59-
if avaialbe, otherwise append ` alias` to the name. If that
60-
is still not unique, then append the smallest integer
61-
greater than one, separated by a space, sufficient to
62-
generate a unique name.
83+
This wraps the `readalias` binary, and produces no output.
84+
It produces a successful exit status (0) if the file provided
85+
is an alias file, even if it is broken. It returns a non-zero
86+
exit status otherwise.
6387

64-
### Edge Cases
88+
### makealias
89+
90+
This forwards all arguments passed to it to the `mkalias` binary,
91+
along with one additional argument: a name generated
92+
using `namealias`, so `<alias-file>` can be unspecified,
93+
in which case a suitable name will be generated.
94+
95+
### namealias
96+
97+
This is a helper used by `makealias`.
98+
Given a path, this will print a name
99+
suitable to be used for an alias file,
100+
using the the same naming strategy used by the Finder.
101+
102+
### nextname
103+
104+
This is a helper used by `namealias`.
105+
It prints a path that is guaranteed (at the moment) not to exist.
106+
It takes the desired path as its only argument.
107+
It prints the path if it does not exist, otherwise
108+
it prints the path appended with a space and number, at least 2,
109+
an only large enough to make the path unique.
110+
111+
## Issues
112+
113+
### Undetermined behaviour
65114

66115
These utilites have only been nominally tested,
67-
and most edge cases have not been explored:
116+
and some edge cases have not been explored:
68117

69118
- Broken Aliases
70119
- Aliases to unmounted external/network volumes
71-
- Creating and Alias to another Alias
72120

73-
## Props where due
121+
### Known edge cases
122+
123+
- Creating an Alias to another Alias file makes a new
124+
Alias pointing to the original of the other Alias.
125+
This is the expected behaviour.
126+
- Arguments that are empty strings will be treated
127+
as `.`, which produces potentially confusing
128+
error messages such as "Error: directory exists:".
129+
130+
## Roadmap
131+
132+
- Incorporate scripts into binaries
133+
- Generate an explicit error when arguments are empty
134+
- Add formal testing
135+
136+
## Acknowledgements
74137

75138
Prior to writing these two utilites,
76139
I was using _alisma_, by Howard Oakley,

Sources/config/package-info.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
public let about = """
22
(macOS-alias-utils v\(versionNumber))
33
Copyright (c) 2026 Erik Ben Heckman
4-
SPDX-License-Identifier: MIT
4+
MIT License
55
<https://github.com/heckman/macOS-alias-utils/>
66
"""

Sources/mkalias/main.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ do {
5454
exit(2)
5555
}
5656
}
57-
guard args.count == 2 else {
57+
guard args.count >= 2 else {
5858
fputs(
5959
"\(usage)\n",
6060
stderr

scripts/isalias

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/bin/zsh
2+
# Part of macOS-alias-utils <https://github.com/heckman/macOS-alias-utils>
3+
# Copyright (c) 2026 Erik Ben Heckman
4+
# MIT License
5+
6+
#: isalias <alias-file>
7+
8+
# Return 0 if <alias-file> is an alias file,
9+
# even if it cannot be resolved.
10+
# Otherwise, return 1.
11+
#
12+
# Produces no output.
13+
#
14+
# This relies on the exit status of `readalias <alias-file>`:
15+
# 0: <alias-file> is an alias file and can be resolved.
16+
# 1: <alias-file> is an alias file but cannot be resolved.
17+
# 2: <alias-file> is not an alias file, does not exist,
18+
# or was not specified.
19+
#
20+
# Requires: [readalias](https://github.com/heckman/macOS-alias-utils).
21+
#;
22+
23+
if (( $+commands[readalias] ))
24+
then
25+
isalias() {
26+
readalias $@ &>/dev/null || ((?==1))
27+
}
28+
else
29+
isalias(){
30+
print -u2 'Error: the `isalias` function requires the `readalias` command'
31+
print -u2 'Get it here: <https://github.com/heckman/macOS-alias-utils>'
32+
return 69 # EX_UNAVAILABLE
33+
}
34+
fi
35+
36+
isalias $@

scripts/makealias

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/zsh
2+
# Part of macOS-alias-utils <https://github.com/heckman/macOS-alias-utils>
3+
# Copyright (c) 2026 Erik Ben Heckman
4+
# MIT License
5+
6+
#: makealias [options] <original> [<alias-file>]
7+
8+
# All arguments (and options) are passed to the `mkalias` command,
9+
# along with one additional argument: an alias name derived
10+
# from the last argument. This means that, unlike the `mkalias`
11+
# command, <alias-file> is optional. I <alias-file> is specified,
12+
# this script will still produce an additional argument, but
13+
# it will be ignored by the `mkalias` command.
14+
#
15+
# This script relies on the fact that the `mkalias` command
16+
# will silently ignore extra arguments.
17+
#;
18+
19+
if (( $+commands[mkalias] ))
20+
then
21+
makealias() {
22+
mkalias $@ "$(namealias ${@[-1]})"
23+
}
24+
else
25+
makealias(){
26+
print -u2 'Error: the `makealias` function requires the `mkalias` command'
27+
print -u2 'Get it here: <https://github.com/heckman/macOS-alias-utils>'
28+
return 69 # EX_UNAVAILABLE
29+
}
30+
fi
31+
32+
makealias $@

scripts/namealias

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/bin/zsh
2+
# Part of macOS-alias-utils <https://github.com/heckman/macOS-alias-utils>
3+
# Copyright (c) 2026 Erik Ben Heckman
4+
# MIT License
5+
6+
#: namealias <original>
7+
8+
if (( $+commands[readalias] ))
9+
then
10+
namealias() {
11+
local ideal f
12+
# If original is an alias, use _its_ original
13+
# otherwise, use the original.
14+
# Then strip the directory from the name.
15+
ideal="${"$(readalias $1 2>/dev/null || echo $1)":t}"
16+
17+
if [[ -e $ideal ]]
18+
then nextname "$ideal alias"
19+
else print -- $ideal
20+
fi
21+
}
22+
else
23+
namealias(){
24+
print -u2 'Error: the `namealias` function requires the `readalias` command'
25+
print -u2 'Get it here: <https://github.com/heckman/macOS-alias-utils>'
26+
return 69 # EX_UNAVAILABLE
27+
}
28+
fi
29+
30+
namealias $@

scripts/nextname

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/bin/zsh
2+
# Part of macOS-alias-utils <https://github.com/heckman/macOS-alias-utils>
3+
# Copyright (c) 2026 Erik Ben Heckman
4+
# MIT License
5+
6+
#: nextname <desired-name>
7+
8+
# If <desired-name> does not exist, print it.
9+
# Otherwise append 2 to the name, and check again.
10+
# Increment the number until an available name is found.
11+
#;
12+
13+
nextname() {
14+
# nothing to do if the name is available
15+
[[ -e $1 ]] || {
16+
print -- $1
17+
return
18+
}
19+
20+
# This limits the number of alternate names that will be checked.
21+
# The loop is quite fast--file existence is not checked within it.
22+
local loop_limit=99999
23+
24+
# populate an set of potentially-conflicting names
25+
local base="$1 "
26+
local -A extant=()
27+
local f
28+
for f ( $base*(N) ) extant[$f]=1
29+
local counter=1
30+
31+
# increment counter until the name is not in the set
32+
while (( ${+extant[$base$((++counter))]} )) {
33+
(( counter > loop_limit )) && return 1
34+
}
35+
print -- $base$counter
36+
}
37+
38+
nextname $@

0 commit comments

Comments
 (0)