diff options
-rwxr-xr-x | shatom | 258 |
1 files changed, 134 insertions, 124 deletions
diff --git a/shatom b/shatom index 48b99ff..3f9adac 100755 --- a/shatom +++ b/shatom | |||
@@ -1,97 +1,105 @@ | |||
1 | #!/bin/sh | 1 | #!/bin/sh |
2 | # generate a Atom feed from a directory, recursively | 2 | # SHATOM: generate a feed from a directory of files |
3 | # using POSIX sh | 3 | # Copyright (C) 2020--2022 Case Duckworth <acdw@acdw.net> |
4 | # AUTHOR: Case Duckworth <acdw@acdw.net> | 4 | # Licensed under the Good Choices License. See COPYING for details. |
5 | # LICENSE: MIT | ||
6 | 5 | ||
7 | usage() { | 6 | ### Entry point |
8 | cat <<END | ||
9 | $0: generate an Atom feed from directories of files | ||
10 | INVOCATION: | ||
11 | $0 [-h] [-c CONFIG] DIRECTORY... | ||
12 | |||
13 | OPTIONS: | ||
14 | -h show this help | ||
15 | -c CONFIG change the CONFIG file. | ||
16 | Default: $PWD/$0.conf.sh | ||
17 | END | ||
18 | } | ||
19 | |||
20 | # FEED VARIABLES | ||
21 | FEED_TITLE=example | ||
22 | FEED_SUBTITLE="an example" | ||
23 | FEED_URL="https://example.com/atom.xml" | ||
24 | SITE_URL="https://example.com" | ||
25 | FEED_ID="${SITE_URL#*//}" | ||
26 | FEED_AUTHOR="nobody" | ||
27 | FEED_COPYRIGHT="(c) 2020 CC-BY-SA $FEED_AUTHOR" | ||
28 | FEED_UPDATED=$(date -u +'%FT%TZ') | ||
29 | |||
30 | # FUNCTIONS | ||
31 | |||
32 | recent_files() { # recent_files DIRECTORY FIND_ARG... | ||
33 | # list files, in order of recency | ||
34 | # BOO: REQUIRES GNU FIND :( :( :( :( | ||
35 | # possibly look into https://unix.stackexchange.com/questions/9247/ | ||
36 | # for more possibilities if you don't have GNU find. | ||
37 | dir="$1" | ||
38 | shift | ||
39 | |||
40 | find "$dir" -maxdepth 1 \ | ||
41 | "$@" \ | ||
42 | -not -name '.*' \ | ||
43 | -printf '%T@\t%p\n' | | ||
44 | sort -nr | | ||
45 | cut -f2 | ||
46 | } | ||
47 | 7 | ||
48 | # ENTRY FUNCTIONS | 8 | usage() { |
9 | cat <<EOF | ||
10 | SHATOM: generate a web feed from directories, recursively | ||
11 | USAGE: shatom -h | ||
12 | shatom [-c CONFIG] [-C DIR] [VAR=VALUE...] DIRECTORY... | ||
49 | 13 | ||
50 | skip_entry() { | 14 | FLAGS: |
51 | return 1 | 15 | -h Show this help and exit. |
52 | } | ||
53 | 16 | ||
54 | entry_url() { | 17 | OPTIONS: |
55 | basename="${1#$DIRECTORY}" | 18 | -c CONFIG Specify shatom's configuration path, a shell |
56 | echo "$FEED_URL/$basename" | 19 | script overriding functions in shatom itself. |
20 | Default: \$PWD/shatom.conf.sh | ||
21 | -C DIR Change the current working directory to DIR | ||
22 | before executing the rest of the script. | ||
23 | |||
24 | PARAMETERS: | ||
25 | VAR=VALUE... Override configuration variables for this | ||
26 | invocation of shatom. These are eval'd, so | ||
27 | be careful! | ||
28 | DIRECTORY... Directory(s) containing feed content. These | ||
29 | can be files as well, but really directories | ||
30 | makes more sense, don't you think? | ||
31 | EOF | ||
32 | exit "${1:-0}" | ||
57 | } | 33 | } |
58 | 34 | ||
59 | entry_title() { | 35 | main() { |
60 | awk '/^#+[ ]\S/{ | 36 | # Command-line flags |
61 | for(i=2;i<=NF;i++) { | 37 | while getopts hc: opt; do |
62 | printf $i; | 38 | case "$opt" in |
63 | if (i!=NF) printf " "; | 39 | h) usage ;; |
64 | } | 40 | c) CONFIG="$OPTARG" ;; |
65 | printf "\n";exit}' "$1" | 41 | *) usage 1 ;; |
42 | esac | ||
43 | done | ||
44 | shift $((OPTIND - 1)) | ||
45 | |||
46 | # Configuration file | ||
47 | if [ -f "${CONFIG:=$PWD/shatom.conf.sh}" ]; then | ||
48 | . "$CONFIG" | ||
49 | fi | ||
50 | |||
51 | # Command-line variable assignment | ||
52 | for arg; do | ||
53 | case "$arg" in | ||
54 | *=*) eval "$arg" ;; | ||
55 | *) break ;; | ||
56 | esac | ||
57 | done | ||
58 | |||
59 | # Finally, default values for variables | ||
60 | FEED_TITLE="${FEED_TITLE:-example}" | ||
61 | FEED_SUBTITLE="${FEED_SUBTITLE:-an example}" | ||
62 | FEED_AUTHOR="${FEED_AUTHOR:-nobody}" | ||
63 | FEED_COPYRIGHT="${FEED_COPYRIGHT:-Copyright (C) $(date +%Y) $FEED_AUTHOR}" | ||
64 | FEED_URL="${FEED_URL:-https://example.com/feed.xml}" | ||
65 | FEED_ID="${FEED_ID:-${FEED_URL#*//}}" | ||
66 | FEED_UPDATED="${FEED_UPDATED:-$(date -u +'%F %TZ')}" | ||
67 | SITE_URL="${SITE_URL:-https://example.com/}" | ||
68 | |||
69 | # Do the damn thing already | ||
70 | feed_header | ||
71 | for DIR; do | ||
72 | recent_files "$DIR" -type f | | ||
73 | while read -r item; do | ||
74 | skip_item "$item" && continue | ||
75 | feed_item "$item" | ||
76 | done | ||
77 | done | ||
78 | feed_footer | ||
66 | } | 79 | } |
67 | 80 | ||
68 | entry_summary() { | 81 | ### Library functions |
69 | return 1 | ||
70 | } | ||
71 | 82 | ||
72 | entry_author() { | 83 | ## File-listing functions |
73 | echo "$FEED_AUTHOR" | ||
74 | } | ||
75 | 84 | ||
76 | entry_content() { | 85 | recent_files() { |
77 | cat "$1" | 86 | # requires stat(1) |
78 | } | 87 | dir="$1" |
88 | shift | ||
79 | 89 | ||
80 | entry_updated() { | 90 | find "$dir"/* -prune "$@" \ |
81 | # requires stat(1). | 91 | -exec stat -c '%Y %N' '{}' + | |
82 | # probably another way. | 92 | sort -k1,1 -nr | |
83 | # possibly using ls(1). | 93 | cut -d' ' -f2- |
84 | stat -c '%y' "$1" | | ||
85 | awk '{ | ||
86 | sub(/\..*/,"",$2); | ||
87 | sub(/[0-9][0-9]/,"&:",$3); | ||
88 | print $1"T"$2$3;}' | ||
89 | } | 94 | } |
90 | 95 | ||
91 | # ATOM FUNCTIONS | 96 | ## Feed-level functions |
97 | # by default, generate Atom feeds | ||
98 | # (https://datatracker.ietf.org/doc/html/rfc4287), but an RSS configuration file | ||
99 | # is included in rss.conf.sh. | ||
92 | 100 | ||
93 | atom_header() { | 101 | feed_header() { |
94 | cat <<END | 102 | cat <<END |
95 | <?xml version="1.0" encoding="utf-8"?> | 103 | <?xml version="1.0" encoding="utf-8"?> |
96 | <feed xmlns="http://www.w3.org/2005/Atom"> | 104 | <feed xmlns="http://www.w3.org/2005/Atom"> |
97 | <title>$FEED_TITLE</title> | 105 | <title>$FEED_TITLE</title> |
@@ -105,60 +113,62 @@ atom_header() { | |||
105 | END | 113 | END |
106 | } | 114 | } |
107 | 115 | ||
108 | atom_footer() { | 116 | feed_footer() { |
109 | cat <<END | 117 | cat <<END |
110 | </feed> | 118 | </feed> |
111 | END | 119 | END |
112 | } | 120 | } |
113 | 121 | ||
114 | atom_entry() { # atom_entry FILE | 122 | feed_item() { |
115 | ENTRY_URL="$(entry_url "$1")" | 123 | ITEM_URL="$(item_url "$1")" |
116 | ENTRY_TITLE="$(entry_title "$1")" | 124 | cat <<END |
117 | ENTRY_SUMMARY="$(entry_summary "$1")" | ||
118 | ENTRY_AUTHOR="$(entry_author "$1")" | ||
119 | ENTRY_CONTENT="$(entry_content "$1")" | ||
120 | ENTRY_UPDATED="$(entry_updated "$1")" | ||
121 | cat <<END | ||
122 | <entry> | 125 | <entry> |
123 | <id>$ENTRY_URL</id> | 126 | <id>$ITEM_URL</id> |
124 | <link rel="alternate" href="$ENTRY_URL" /> | 127 | <link rel="alternate" href="$ITEM_URL" /> |
125 | <title>$ENTRY_TITLE</title> | 128 | <title>$(item_title "$1")</title> |
126 | <summary>$ENTRY_SUMMARY</summary> | 129 | <summary>$(item_summary "$1")</summary> |
127 | <updated>$ENTRY_UPDATED</updated> | 130 | <updated>$(item_updated "$1")</updated> |
128 | <author><name>$ENTRY_AUTHOR</name></author> | 131 | <author>$(item_author)</author> |
129 | <content type="text"><![CDATA[$ENTRY_CONTENT]]></content> | 132 | <content type="text"><![CDATA[$(item_content "$1")]]></content> |
130 | </entry> | 133 | </entry> |
131 | END | 134 | END |
132 | } | 135 | } |
133 | 136 | ||
134 | main() { | 137 | ## Item-level functions |
135 | CONFIGFILE="$PWD/$0.conf.sh" | 138 | # These can all be, and in fact should be, changed in the configuration file. |
136 | case "$1" in | 139 | |
137 | -h) | 140 | skip_item() { |
138 | usage | 141 | return 1 |
139 | exit 0 | 142 | } |
140 | ;; | 143 | |
141 | -c) | 144 | item_url() { |
142 | CONFIGFILE="$2" | 145 | basename="${1#$DIRECTORY}" |
143 | shift 2 | 146 | echo "$FEED_URL/$basename" |
144 | ;; | 147 | } |
145 | esac | 148 | |
146 | 149 | item_title() { | |
147 | if [ -f "CONFIGFILE" ]; then | 150 | basename="${1#$DIRECTORY}" |
148 | . "$CONFIGFILE" | 151 | echo "$basename" |
149 | fi | 152 | } |
150 | 153 | ||
151 | atom_header | 154 | item_summary() { |
152 | for DIR; do | 155 | return 1 |
153 | for entry in $(recent_files "$DIR" -type f); do | 156 | } |
154 | if skip_entry "$entry"; then continue; fi | 157 | |
155 | 158 | item_author() { | |
156 | atom_entry "$entry" | 159 | echo "$FEED_AUTHOR" |
157 | done | 160 | } |
158 | done | 161 | |
159 | atom_footer | 162 | item_content() { |
163 | cat "$1" | ||
164 | } | ||
165 | |||
166 | item_updated() { | ||
167 | # requires stat(1) | ||
168 | stat -c %y "$1" | ||
160 | } | 169 | } |
161 | 170 | ||
162 | if [ $DEBUG ]; then set -x; fi | 171 | ### Execution section |
163 | 172 | ||
164 | main "$@" | 173 | test -n $DEBUG && set -x |
174 | test -n $SOURCE || main "$@" | ||