diff options
author | Case Duckworth | 2023-03-15 14:24:10 -0500 |
---|---|---|
committer | Case Duckworth | 2023-03-15 14:24:10 -0500 |
commit | 3319d70e4e8fd89fe6140e95504a718717bd4e80 (patch) | |
tree | 33e9de59e234ae64141e11a8733c1323b1ad21ab | |
parent | Add shin.sh (sh library; acts a little differently to shin.awk/shin) (diff) | |
download | shin-3319d70e4e8fd89fe6140e95504a718717bd4e80.tar.gz shin-3319d70e4e8fd89fe6140e95504a718717bd4e80.zip |
-rw-r--r-- | Makefile | 15 | ||||
-rw-r--r-- | README.md | 35 | ||||
-rwxr-xr-x | shin | 203 | ||||
-rwxr-xr-x | shin.awk | 60 | ||||
-rwxr-xr-x | shin.sh | 119 | ||||
-rwxr-xr-x | shinb | 7 |
6 files changed, 179 insertions, 260 deletions
diff --git a/Makefile b/Makefile index a9401a1..b6efa17 100644 --- a/Makefile +++ b/Makefile | |||
@@ -1,10 +1,12 @@ | |||
1 | NAME = shin | 1 | NAME = shin |
2 | DESC = Include shell scripts in other shell scripts | 2 | DESC = Include shell scripts in other shell scripts |
3 | 3 | ||
4 | INSTALLED = shin shinb | ||
5 | |||
4 | DESTDIR = | 6 | DESTDIR = |
5 | PREFIX = /usr/local | 7 | PREFIX = /usr/local |
6 | 8 | ||
7 | BIN = $(DESTDIR)$(PREFIX)/bin/$(NAME) | 9 | BIN = $(DESTDIR)$(PREFIX)/bin |
8 | 10 | ||
9 | .PHONY: help | 11 | .PHONY: help |
10 | help: | 12 | help: |
@@ -19,13 +21,16 @@ help: | |||
19 | @echo " uninstall Uninstall $(NAME)-related files." | 21 | @echo " uninstall Uninstall $(NAME)-related files." |
20 | 22 | ||
21 | .PHONY: install | 23 | .PHONY: install |
22 | install: $(NAME) | 24 | install: $(INSTALLED) |
23 | install -D $< $(BIN) | 25 | install -Dt $(BIN) $? |
24 | 26 | ||
25 | .PHONY: link | 27 | .PHONY: link |
26 | link: | 28 | link: $(NAME) shinb $(BIN) |
27 | ln -sf $(PWD)/$(NAME) $(BIN) | 29 | ln -sf $(PWD)/$(NAME) $(PWD)/shinb $(BIN) |
28 | 30 | ||
29 | .PHONY: uninstall | 31 | .PHONY: uninstall |
30 | uninstall: | 32 | uninstall: |
31 | rm $(BIN) | 33 | rm $(BIN) |
34 | |||
35 | $(BIN): | ||
36 | mkdir -p $(BIN) | ||
diff --git a/README.md b/README.md index 4b68c96..f6f03d8 100644 --- a/README.md +++ b/README.md | |||
@@ -2,35 +2,24 @@ | |||
2 | ## include shell files | 2 | ## include shell files |
3 | 3 | ||
4 | built to scratch a personal itch. SHIN is an awk(1) script that plops include | 4 | built to scratch a personal itch. SHIN is an awk(1) script that plops include |
5 | files in to .shin files. here's the usage text: | 5 | files in to .shin files. |
6 | 6 | ||
7 | ``` | 7 | recently rewritten. now use it like this: |
8 | SHIN: include shell files in other shell files | ||
9 | USAGE: shin FILE... | ||
10 | |||
11 | FILEs named FILE.shin will be built to FILE.sh in the same directory. | ||
12 | to include files in shin files, use the following comment syntax: | ||
13 | 8 | ||
14 | #< INCLUDE | 9 | ```sh |
10 | #!/bin/sh | ||
15 | 11 | ||
16 | shin will add INCLUDE below that comment, as well as a comment | 12 | . shin # if shin is in $PATH this should Just Work(TM) |
17 | denoting the end of INCLUDE. | ||
18 | ``` | ||
19 | 13 | ||
20 | it's really that simple. and stupid. | 14 | do_stuff_here |
21 | 15 | ||
22 | ## configuration | 16 | shin included_file |
23 | 17 | ||
24 | SHIN will look for include files in the current directory, unless you set the | 18 | other_stuff |
25 | SHINPATH environment variable. $SHINPATH is a colon-separated list of | 19 | ``` |
26 | directories like $PATH or $MANPATH. | ||
27 | |||
28 | ## extras | ||
29 | 20 | ||
30 | `shin.awk` is the plain awk script. because awk is dumb about handling | 21 | you can run the above script itself, or you can "compile" it by running `shin -c <script>`, which will insert `included_file` in the correct spot in the file and remove the `. shin` line (only if it's by itself, which really it should be). |
31 | arguments, `shin` is a shell script that will do `shin.awk`'s work unless you | ||
32 | pass `shin -h`, in which case it will print a helpful message. | ||
33 | 22 | ||
34 | ## license | 23 | ## build a shell file |
35 | 24 | ||
36 | SHIN is licensed under the FAIR license. see COPYING for details. | 25 | use the program `shinb` to build a shell file on the command line. You don't need to include `-c` in the command line when calling `shinb`. |
diff --git a/shin b/shin index 9ba488b..ddd9f0e 100755 --- a/shin +++ b/shin | |||
@@ -1,67 +1,164 @@ | |||
1 | #!/bin/sh | 1 | #!/bin/sh |
2 | # SHIN v. 0/1 | 2 | # SHIN --- include files in shell scripts |
3 | # Copyright (C) Case Duckworth <acdw@acdw.net> | 3 | # Copyright (C) Case Duckworth <acdw@acdw.net> |
4 | # Licensed under the Fair License. See COPYING for details. | ||
5 | 4 | ||
6 | _shin() { | 5 | _shin_check_SHINPATH() { |
7 | awk 'BEGIN { | 6 | printf "%s\n" "${SHINPATH:-.}" | |
8 | if (ENVIRON["SHINPATH"]) split(ENVIRON["SHINPATH"], SHINPATH, ":") | 7 | tr : '\n' | |
9 | else SHINPATH[1] = "." | 8 | while read -r path |
9 | do | ||
10 | if test -f "$path/$1" | ||
11 | then | ||
12 | printf '%s\n' "$path/$1" | ||
13 | return 0 | ||
14 | fi | ||
15 | done | ||
16 | return 1 | ||
10 | } | 17 | } |
11 | FNR == 1 { outfile = FILENAME; sub(/in$/, "", outfile) } | 18 | |
12 | { print($0) > outfile } | 19 | _shin_resolve_file() { |
13 | /^#</ { | 20 | f= |
14 | inclfile = shin_resolve(substr($0, 3)) | 21 | case "$1" in |
15 | while ((getline l < inclfile) > 0) print(l) > outfile | 22 | ~/*) f="$HOME/${1#~/}" ;; |
16 | close(inclfile) | 23 | /*) f="/${1#/}" ;; |
17 | sub(/</, ">", $0) | 24 | *) f="$(_shin_check_SHINPATH "$1")" ;; |
18 | print > outfile | 25 | esac |
26 | |||
27 | if test -f "$f" | ||
28 | then | ||
29 | printf '%s\n' "$f" | ||
30 | elif test -f "$f.sh" | ||
31 | then | ||
32 | printf '%s\n' "$f.sh" | ||
33 | else | ||
34 | return 1 | ||
35 | fi | ||
36 | return 0 | ||
19 | } | 37 | } |
20 | function shin_test(filename) { | 38 | |
21 | if (! system("test -f \"" f "\"")) return filename | 39 | _shin_include() { # _shin_include FILE ... |
22 | print("Cannot find \"" filename "\" in " sp) > (STDERR ? STDERR : "/dev/stderr") | 40 | for lib |
23 | exit 1 | 41 | do |
42 | lib="$(_shin_resolve_file "$lib")" | ||
43 | test -f "$lib" && . "$lib" | ||
44 | done | ||
24 | } | 45 | } |
25 | function shin_resolve(filename) { | 46 | |
26 | if (match(filename, "^/")) return shin_test(filename) | 47 | _shin_cleanup() { |
27 | if (match(filename, "^~")) return shin_test(ENVIRON["HOME"] "/" substr(filename, 2)) | 48 | if "$_shin_rmtemp" && test -f "$_shin_temp" |
28 | sub(/^[ \t]*/, "", filename) | 49 | then |
29 | sub(/[ \t]*$/, "", filename) | 50 | rm -f "$_shin_temp" |
30 | sp = "" | 51 | fi |
31 | for (p in SHINPATH) { | ||
32 | sp = sp (sp ? ", " : "") "\"" SHINPATH[p] "\"" | ||
33 | f = SHINPATH[p] "/" filename | ||
34 | gsub("//", "/", f) | ||
35 | return shin_test(f) | ||
36 | } | ||
37 | }' | ||
38 | } | 52 | } |
39 | 53 | ||
40 | if [ "x$1" = -h ]; then | 54 | _shin_build() ( # _shin_build FILE ... |
41 | cat <<\EOF | 55 | for file |
42 | SHIN: include shell files in other shell files | 56 | do |
43 | USAGE: shin [-h] FILE... | 57 | file="$(_shin_resolve_file "$file")" |
58 | |||
59 | # Determine output file | ||
60 | if test -z "$_shin_output" | ||
61 | then | ||
62 | case "$file" in | ||
63 | # file.shin -> file.sh | ||
64 | *.shin) _shin_output="${file%in}" ;; | ||
65 | # file.sh -> file | ||
66 | *.sh) _shin_output="${file%.sh}" ;; | ||
67 | # file.ext -> file.ext.sh | ||
68 | *) _shin_output="$file.sh" ;; | ||
69 | esac | ||
70 | fi | ||
44 | 71 | ||
45 | FLAGS: | 72 | while read -r line |
46 | -h Show this help and exit. | 73 | do |
74 | case "$line" in | ||
75 | # skip lines where the user sources the library | ||
76 | .*shin) ;; | ||
77 | .*shin.sh) ;; | ||
78 | # where the user invokes shin, replace with file | ||
79 | shin*) # cursed | ||
80 | eval \ | ||
81 | for file in ${line#shin}\; \ | ||
82 | do \ | ||
83 | test -f \"\$file\" \&\& \ | ||
84 | sed \"/^#!/d\" \"\$file\"\; \ | ||
85 | done | ||
86 | ;; | ||
87 | # else, print the line | ||
88 | *) printf '%s\n' "$line" ;; | ||
89 | esac | ||
90 | done < "$file" > "$_shin_temp" | ||
47 | 91 | ||
48 | PARAMETERS: | 92 | ec=$? |
49 | FILE... Input files. FILEs named FILE.shin will be built to | ||
50 | FILE.sh in the same directory. To include files in | ||
51 | shin files, use the following comment syntax: | ||
52 | 93 | ||
53 | #< INCLUDE | 94 | if test "x$_shin_output" = x- |
95 | then # already output to standard out | ||
96 | cat "$_shin_temp" | ||
97 | continue | ||
98 | elif test $ec -ne 0 | ||
99 | then # uh oh we messed up | ||
100 | echo >&2 "shin: errors building." | ||
101 | echo >&2 "shin: part-built file: $_shin_temp" | ||
102 | _shin_rmtemp=false | ||
103 | elif test -f "$_shin_output" && ! "$_shin_force" | ||
104 | then # can't overwrite sumthin without -f | ||
105 | echo >&2 "shin: file already exists: $_shin_output" | ||
106 | echo >&2 "shin: part-built file: $_shin_temp" | ||
107 | _shin_rmtemp=false | ||
108 | else # move the temp file to the output | ||
109 | if mv "$_shin_temp" "$_shin_output" | ||
110 | then | ||
111 | printf >&2 '%s\n' "$_shin_output" | ||
112 | else | ||
113 | echo >&2 "shin: errors moving $_shin_output" | ||
114 | echo >&2 "shin: part-built file: $_shin_temp" | ||
115 | _shin_rmtemp=false | ||
116 | fi | ||
117 | fi | ||
118 | done | ||
119 | ) | ||
54 | 120 | ||
55 | If INCLUDE begins with / or ~, it's taken as a literal | 121 | shin() { |
56 | file name; otherwise SHINPATH will be searched for the | 122 | _shin_func="${_shin_func:-_shin_include}" |
57 | file. SHINPATH is a colon-separated list of paths | 123 | _shin_temp=/tmp/shinf |
58 | like $PATH. If SHINPATH is unset, it defaults to the | 124 | _shin_rmtemp=true |
59 | current directory. | 125 | _shin_force=false |
126 | _shin_debug=false | ||
60 | 127 | ||
61 | If INCLUDE is not found, shin will error and quit. | 128 | trap _shin_cleanup INT EXIT QUIT |
62 | Otherwise, shin will add INCLUDE below that comment, | 129 | |
63 | as well as a comment denoting the end of the INCLUDE. | 130 | while getopts dfcio: OPT |
131 | do | ||
132 | case "$OPT" in | ||
133 | c) _shin_func=_shin_build ;; | ||
134 | i) _shin_func=_shin_include ;; | ||
135 | o) # output file implies build. | ||
136 | _shin_func=_shin_build | ||
137 | _shin_output="$OPTARG" | ||
138 | echo "$_shin_output" | ||
139 | ;; | ||
140 | f) _shin_force=true ;; | ||
141 | d) _shin_debug=true ;; | ||
142 | *) exit 1 ;; | ||
143 | esac | ||
144 | done | ||
145 | shift $((OPTIND - 1)) | ||
146 | OPTIND=0 | ||
147 | |||
148 | if "$_shin_debug" | ||
149 | then | ||
150 | cat >&2 <<EOF | ||
151 | SHIN DEBUGGING: ON | ||
152 | VARIABLES: | ||
153 | _shin_func $_shin_func | ||
154 | _shin_output $_shin_output | ||
155 | _shin_temp $_shin_temp | ||
156 | |||
157 | _shin_rmtemp $_shin_rmtemp | ||
158 | _shin_force $_shin_force | ||
64 | EOF | 159 | EOF |
65 | else | 160 | set -x |
66 | _shin "$@" | 161 | fi |
67 | fi | 162 | |
163 | "$_shin_func" "$@" | ||
164 | } | ||
diff --git a/shin.awk b/shin.awk deleted file mode 100755 index 80c76aa..0000000 --- a/shin.awk +++ /dev/null | |||
@@ -1,60 +0,0 @@ | |||
1 | #!/usr/bin/awk -f | ||
2 | # SHIN: include files in shell scripts | ||
3 | # by Case Duckworth <acdw@acdw.net> | ||
4 | # usage: shin -- FILE.shin... | ||
5 | # each FILE.shin will output to FILE.sh in the same directory | ||
6 | BEGIN { | ||
7 | if (ENVIRON["SHINPATH"]) { | ||
8 | split(ENVIRON["SHINPATH"], SHINPATH, ":") | ||
9 | } else { | ||
10 | SHINPATH[1] = "." | ||
11 | } | ||
12 | } | ||
13 | |||
14 | FNR == 1 { | ||
15 | outfile = FILENAME | ||
16 | sub(/in$/, "", outfile) | ||
17 | } | ||
18 | |||
19 | { | ||
20 | print($0) > outfile | ||
21 | } | ||
22 | |||
23 | /^#</ { | ||
24 | inclfile = shin_resolve(substr($0, 3)) | ||
25 | while (getline l < inclfile) { | ||
26 | print(l) > outfile | ||
27 | } | ||
28 | close(inclfile) | ||
29 | sub(/</, ">", $0) | ||
30 | print > outfile | ||
31 | } | ||
32 | |||
33 | |||
34 | function shin_resolve(filename) | ||
35 | { | ||
36 | if (match(filename, "^/")) { | ||
37 | return shin_test(filename) | ||
38 | } | ||
39 | if (match(filename, "^~")) { | ||
40 | return shin_test(ENVIRON["HOME"] "/" substr(filename, 2)) | ||
41 | } | ||
42 | sub(/^[ \t]*/, "", filename) | ||
43 | sub(/[ \t]*$/, "", filename) | ||
44 | sp = "" | ||
45 | for (p in SHINPATH) { | ||
46 | sp = sp (sp ? ", " : "") "\"" SHINPATH[p] "\"" | ||
47 | f = SHINPATH[p] "/" filename | ||
48 | gsub("//", "/", f) | ||
49 | return shin_test(f) | ||
50 | } | ||
51 | } | ||
52 | |||
53 | function shin_test(filename) | ||
54 | { | ||
55 | if (! system("test -f \"" f "\"")) { | ||
56 | return filename | ||
57 | } | ||
58 | print("Cannot find \"" filename "\" in " sp) > (STDERR ? STDERR : "/dev/stderr") | ||
59 | exit 1 | ||
60 | } | ||
diff --git a/shin.sh b/shin.sh deleted file mode 100755 index 97f79e5..0000000 --- a/shin.sh +++ /dev/null | |||
@@ -1,119 +0,0 @@ | |||
1 | #!/bin/sh | ||
2 | # SHIN --- include files in shell scripts | ||
3 | # Copyright (C) Case Duckworth <acdw@acdw.net> | ||
4 | |||
5 | _shin_check_SHINPATH{) { | ||
6 | printf "%s\n" "${SHINPATH:-.}" | | ||
7 | tr : '\n' | | ||
8 | while read -r path | ||
9 | do | ||
10 | if test -f "$path/$1" | ||
11 | then | ||
12 | printf '%s\n' "$path/$1" | ||
13 | return 0 | ||
14 | fi | ||
15 | done | ||
16 | return 1 | ||
17 | } | ||
18 | |||
19 | _shin_resolve_file(} { | ||
20 | f= | ||
21 | case "$1" in | ||
22 | ~/*) f="$HOME/${1#~/}" ;; | ||
23 | /*) f="/${1#/}" ;; | ||
24 | *) f="$(_shin_check_SHINPATH "$1")" ;; | ||
25 | esac | ||
26 | |||
27 | if test -f "$f" | ||
28 | then | ||
29 | printf '%s\n' "$f" | ||
30 | return 0 | ||
31 | else | ||
32 | return 1 | ||
33 | fi | ||
34 | } | ||
35 | |||
36 | _shin_include() { # _shin_include FILE ... | ||
37 | for lib | ||
38 | do | ||
39 | lib="$(_shin_resolve_file "$lib")" | ||
40 | test -f "$lib" && . "$lib" | ||
41 | done | ||
42 | } | ||
43 | |||
44 | _shin_build() ( # _shin_build FILE ... | ||
45 | temp=/tmp/shinf; trap 'rm "$temp"' INT EXIT QUIT | ||
46 | for file | ||
47 | do | ||
48 | file="$(_shin_resolve_file "$file")" | ||
49 | |||
50 | # Determine output file | ||
51 | if test -z "$outfile" | ||
52 | then | ||
53 | case "$file" in | ||
54 | # file.shin -> file.sh | ||
55 | *.shin) outfile="${file%in}" ;; | ||
56 | # file.sh -> file | ||
57 | *.sh) outfile="${file%.sh}" ;; | ||
58 | # file.ext -> file.ext.sh | ||
59 | *) outfile="$file.sh" ;; | ||
60 | esac | ||
61 | fi | ||
62 | |||
63 | while read -r line | ||
64 | do | ||
65 | case "$line" in | ||
66 | # skip lines where the user sources the library | ||
67 | .*shin) ;; | ||
68 | .*shin.sh) ;; | ||
69 | # where the user invokes shin, replace with file | ||
70 | shin*) # cursed | ||
71 | eval \ | ||
72 | for file in ${line#shin}\; \ | ||
73 | do \ | ||
74 | test -f \"\$file\" \&\& \ | ||
75 | cat \"\$file\"\; \ | ||
76 | done | ||
77 | ;; | ||
78 | # else, print the line | ||
79 | *) printf '%s\n' "$line" ;; | ||
80 | esac | ||
81 | done < "$file" > "$temp" | ||
82 | |||
83 | if test "x$outfile" = x- | ||
84 | then # already output to standard out | ||
85 | cat "$temp" | ||
86 | continue | ||
87 | elif test $? -ne 0 | ||
88 | then # uh oh we messed up | ||
89 | echo >&2 "shin: errors building." | ||
90 | echo >&2 "shin: part-built file: $temp" | ||
91 | elif test -f "$outfile" | ||
92 | then # can't overwrite sumthin | ||
93 | echo >&2 "shin: file already exists: $outfile" | ||
94 | echo >&2 "shin: part-built file: $temp" | ||
95 | else # move the temp file to the output | ||
96 | mv "$temp" "$outfile" | ||
97 | fi | ||
98 | done | ||
99 | ) | ||
100 | |||
101 | shin() { | ||
102 | func=_shin_include | ||
103 | while getopts fio: OPT | ||
104 | do | ||
105 | case "$OPT" in | ||
106 | f) func=_shin_build ;; | ||
107 | i) func=_shin_include ;; | ||
108 | o) # output file implies build. | ||
109 | func=_shin_build | ||
110 | outputfile="$OPTARG" | ||
111 | ;; | ||
112 | *) exit 1 ;; | ||
113 | esac | ||
114 | done | ||
115 | shift $((OPTIND - 1)) | ||
116 | OPTIND=0 | ||
117 | |||
118 | "$func" "$@" | ||
119 | } | ||
diff --git a/shinb b/shinb new file mode 100755 index 0000000..855b153 --- /dev/null +++ b/shinb | |||
@@ -0,0 +1,7 @@ | |||
1 | #!/bin/sh | ||
2 | |||
3 | . shin | ||
4 | |||
5 | _shin_func=_shin_build | ||
6 | |||
7 | shin "$@" | ||