bb.sh 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860
  1. #!/usr/bin/env bash
  2. # BashBlog, a simple blog system written in a single bash script
  3. # Copyright: Carles Fenollosa <carles.fenollosa@bsc.es>, 2011-2013
  4. # With contributions from many others:
  5. # https://github.com/carlesfe/bashblog/contributors
  6. #########################################################################################
  7. #
  8. # README
  9. #
  10. #########################################################################################
  11. #
  12. # This is a very basic blog system
  13. #
  14. # Basically it asks the user to create a text file, then converts it into a .html file
  15. # and then rebuilds the index.html and feed.rss.
  16. #
  17. # Comments are supported via external service (Disqus).
  18. # Markdown syntax is supported via third party library (e.g. Gruber's Markdown.pl)
  19. #
  20. # This script is standalone, it doesn't require any other file to run
  21. #
  22. # Files that this script generates:
  23. # - main.css (inherited from my web page) and blog.css (blog-specific stylesheet)
  24. # - one .html for each post
  25. # - index.html (regenerated each run)
  26. # - feed.rss (regenerated each run)
  27. # - all_posts.html (regenerated each run)
  28. # - it also generates temporal files, which are removed afterwards
  29. #
  30. # It generates valid html and rss files, so keep care to use valid xhtml when editing a post
  31. #
  32. # There are many loops which iterate on '*.html' so make sure that the only html files
  33. # on this folder are the blog entries and index.html and all_posts.html. Drafts must go
  34. # into drafts/ and any other *.html file should be moved out of the way
  35. #
  36. # Read more: https://github.com/cfenollosa/bashblog
  37. #########################################################################################
  38. #
  39. # LICENSE
  40. #
  41. #########################################################################################
  42. #
  43. # This program is free software: you can redistribute it and/or modify
  44. # it under the terms of the GNU General Public License as published by
  45. # the Free Software Foundation, either version 3 of the License, or
  46. # (at your option) any later version.
  47. #
  48. # This program is distributed in the hope that it will be useful,
  49. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  50. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  51. # GNU General Public License for more details.
  52. #
  53. # You should have received a copy of the GNU General Public License
  54. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  55. #########################################################################################
  56. #
  57. # CHANGELOG
  58. #
  59. #########################################################################################
  60. #
  61. # 2.0.2 Fixed bug when $body_begin_file was empty
  62. # Added extra line in the footer linking to the github project
  63. # 2.0.1 Allow personalized header/footer files
  64. # 2.0 Added Markdown support
  65. # Fully support BSD date
  66. # 1.6.4 Fixed bug in localized dates
  67. # 1.6.3 Now supporting BSD date
  68. # 1.6.2 Simplified some functions and variables to avoid duplicated information
  69. # 1.6.1 'date' fix when hours are 1 digit.
  70. # 1.6.0 Disqus comments. External configuration file. Check of 'date' command version.
  71. # 1.5.1 Misc bugfixes and parameter checks
  72. # 1.5 Đurađ Radojičić (djura-san) refactored some code and added flexibility and i18n
  73. # 1.4.2 Now issues are handled at Github
  74. # 1.4.1 Some code refactoring
  75. # 1.4 Using twitter for comments, improved 'rebuild' command
  76. # 1.3 'edit' command
  77. # 1.2.2 Feedburner support
  78. # 1.2.1 Fixed the timestamps bug
  79. # 1.2 'list' command
  80. # 1.1 Draft and preview support
  81. # 1.0 Read http://is.gd/Bkdoru
  82. #########################################################################################
  83. #
  84. # CODE
  85. #
  86. #########################################################################################
  87. #
  88. # As usual with bash scripts, scroll all the way to the bottom for the main routine
  89. # All other functions are declared above main.
  90. # Global variables
  91. # It is recommended to perform a 'rebuild' after changing any of this in the code
  92. # Config file. Any settings "key=value" written there will override the
  93. # global_variables defaults. Useful to avoid editing bb.sh and having to deal
  94. # with merges in VCS
  95. global_config=".config"
  96. # This function will load all the variables defined here. They might be overriden
  97. # by the 'global_config' file contents
  98. global_variables() {
  99. global_software_name="BashBlog"
  100. global_software_version="2.0.2"
  101. # Blog title
  102. global_title="My fancy blog"
  103. # The typical subtitle for each blog
  104. global_description="A blog about turtles and carrots"
  105. # The public base URL for this blog
  106. global_url="http://example.com/blog"
  107. # Your name
  108. global_author="John Smith"
  109. # You can use twitter or facebook or anything for global_author_url
  110. global_author_url="http://twitter.com/example"
  111. # Your email
  112. global_email="john@smith.com"
  113. # CC by-nc-nd is a good starting point, you can change this to "&copy;" for Copyright
  114. global_license="CC by-nc-nd"
  115. # If you have a Google Analytics ID (UA-XXXXX), put it here.
  116. # If left empty (i.e. "") Analytics will be disabled
  117. global_analytics=""
  118. # Leave this empty (i.e. "") if you don't want to use feedburner,
  119. # or change it to your own URL
  120. global_feedburner=""
  121. # Change this to your username if you want to use twitter for comments
  122. global_twitter_username=""
  123. # Change this to your disqus username to use disqus for comments
  124. global_disqus_username=""
  125. # Blog generated files
  126. # index page of blog (it is usually good to use "index.html" here)
  127. index_file="index.html"
  128. number_of_index_articles="8"
  129. # global archive
  130. archive_index="all_posts.html"
  131. # feed file (rss in this case)
  132. blog_feed="feed.rss"
  133. number_of_feed_articles="10"
  134. # personalized header and footer (only if you know what you're doing)
  135. # DO NOT name them .header.html, .footer.html or they will be overwritten
  136. # leave blank to generate them, recommended
  137. header_file=""
  138. footer_file=""
  139. # extra content to add just after we open the <body> tag
  140. # and before the actual blog content
  141. body_begin_file=""
  142. # Localization and i18n
  143. # "Comments?" (used in twitter link after every post)
  144. template_comments="Comments?"
  145. # "View more posts" (used on bottom of index page as link to archive)
  146. template_archive="View more posts"
  147. # "Back to the index page" (used on archive page, it is link to blog index)
  148. template_archive_index_page="Back to the index page"
  149. # "Subscribe" (used on bottom of index page, it is link to RSS feed)
  150. template_subscribe="Subscribe"
  151. # "Subscribe to this page..." (used as text for browser feed button that is embedded to html)
  152. template_subscribe_browser_button="Subscribe to this page..."
  153. # "Tweet" (used as twitter text button for posting to twitter)
  154. template_twitter_button="Tweet"
  155. template_twitter_comment="&lt;Type your comment here but please leave the URL so that other people can follow the comments&gt;"
  156. # The locale to use for the dates displayed on screen (not for the timestamps)
  157. date_format="%B %d, %Y"
  158. date_locale="C"
  159. # Markdown location. Trying to autodetect by default.
  160. # The invocation must support the signature 'markdown_bin in.html > out.md'
  161. markdown_bin="$(which Markdown.pl)"
  162. }
  163. # Check for the validity of some variables
  164. # DO NOT EDIT THIS FUNCTION unless you know what you're doing
  165. global_variables_check() {
  166. [[ "$header_file" == ".header.html" ]] &&
  167. echo "Please check your configuration. '.header.html' is not a valid value for the setting 'header_file'" &&
  168. exit
  169. [[ "$footer_file" == ".footer.html" ]] &&
  170. echo "Please check your configuration. '.footer.html' is not a valid value for the setting 'footer_file'" &&
  171. exit
  172. }
  173. # Test if the markdown script is working correctly
  174. test_markdown() {
  175. [[ -z "$markdown_bin" ]] && return 1
  176. [[ -z "$(which diff)" ]] && return 1
  177. in="/tmp/md-in-$(echo $RANDOM).md"
  178. out="/tmp/md-out-$(echo $RANDOM).html"
  179. good="/tmp/md-good-$(echo $RANDOM).html"
  180. echo -e "line 1\n\nline 2" > $in
  181. echo -e "<p>line 1</p>\n\n<p>line 2</p>" > $good
  182. $markdown_bin $in > $out 2> /dev/null
  183. diff $good $out &> /dev/null # output is irrelevant, we'll check $?
  184. if [[ $? -ne 0 ]]; then
  185. rm -f $in $good $out
  186. return 1
  187. fi
  188. rm -f $in $good $out
  189. return 0
  190. }
  191. # Parse a Markdown file into HTML and return the generated file
  192. markdown() {
  193. out="$(echo $1 | sed 's/md$/html/g')"
  194. while [ -f "$out" ]; do out="$(echo $out | sed 's/\.html$/\.'$RANDOM'\.html')"; done
  195. $markdown_bin $1 > $out
  196. echo $out
  197. }
  198. # Prints the required google analytics code
  199. google_analytics() {
  200. [[ -z "$global_analytics" ]] && return
  201. echo "<script type=\"text/javascript\">
  202. var _gaq = _gaq || [];
  203. _gaq.push(['_setAccount', '"$global_analytics"']);
  204. _gaq.push(['_trackPageview']);
  205. (function() {
  206. var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
  207. ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
  208. var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  209. })();
  210. </script>"
  211. }
  212. # Prints the required code for disqus comments
  213. disqus_body() {
  214. [[ -z "$global_disqus_username" ]] && return
  215. echo '<div id="disqus_thread"></div>
  216. <script type="text/javascript">
  217. /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
  218. var disqus_shortname = '\'$global_disqus_username\''; // required: replace example with your forum shortname
  219. /* * * DONT EDIT BELOW THIS LINE * * */
  220. (function() {
  221. var dsq = document.createElement("script"); dsq.type = "text/javascript"; dsq.async = true;
  222. dsq.src = "//" + disqus_shortname + ".disqus.com/embed.js";
  223. (document.getElementsByTagName("head")[0] || document.getElementsByTagName("body")[0]).appendChild(dsq);
  224. })();
  225. </script>
  226. <noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
  227. <a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>'
  228. }
  229. # Prints the required code for disqus in the footer
  230. disqus_footer() {
  231. [[ -z "$global_disqus_username" ]] && return
  232. echo '<script type="text/javascript">
  233. /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
  234. var disqus_shortname = '\'$global_disqus_username\''; // required: replace example with your forum shortname
  235. /* * * DONT EDIT BELOW THIS LINE * * */
  236. (function () {
  237. var s = document.createElement("script"); s.async = true;
  238. s.type = "text/javascript";
  239. s.src = "//" + disqus_shortname + ".disqus.com/count.js";
  240. (document.getElementsByTagName("HEAD")[0] || document.getElementsByTagName("BODY")[0]).appendChild(s);
  241. }());
  242. </script>'
  243. }
  244. # Edit an existing, published .html file while keeping its original timestamp
  245. # Please note that this function does not automatically republish anything, as
  246. # it is usually called from 'main'.
  247. #
  248. # 'edit' is kind of an advanced function, as it leaves to the user the responsibility
  249. # of editing an html file
  250. #
  251. # $1 the file to edit
  252. edit() {
  253. timestamp="$(date -r $1 +'%Y%m%d%H%M')"
  254. $EDITOR "$1"
  255. touch -t $timestamp "$1"
  256. }
  257. # Adds the code needed by the twitter button
  258. #
  259. # $1 the post URL
  260. twitter() {
  261. [[ -z "$global_twitter_username" ]] && return
  262. if [[ -z "$global_disqus_username" ]]; then
  263. echo "<p id='twitter'>$template_comments&nbsp;"
  264. else
  265. echo "<p id='twitter'><a href=\"$1#disqus_thread\">$template_comments</a> &nbsp;"
  266. fi
  267. echo "<a href=\"https://twitter.com/share\" class=\"twitter-share-button\" data-text=\"$template_twitter_comment\" data-url=\"$1\""
  268. echo " data-via=\"$global_twitter_username\""
  269. echo ">$template_twitter_button</a> <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=\"//platform.twitter.com/widgets.js\";fjs.parentNode.insertBefore(js,fjs);}}(document,\"script\",\"twitter-wjs\");</script>"
  270. echo "</p>"
  271. }
  272. # Adds all the bells and whistles to format the html page
  273. # Every blog post is marked with a <!-- entry begin --> and <!-- entry end -->
  274. # which is parsed afterwards in the other functions. There is also a marker
  275. # <!-- text begin --> to determine just the beginning of the text body of the post
  276. #
  277. # $1 a file with the body of the content
  278. # $2 the output file
  279. # $3 "yes" if we want to generate the index.html,
  280. # "no" to insert new blog posts
  281. # $4 title for the html header
  282. # $5 original blog timestamp
  283. create_html_page() {
  284. content="$1"
  285. filename="$2"
  286. index="$3"
  287. title="$4"
  288. timestamp="$5"
  289. # Create the actual blog post
  290. # html, head
  291. cat ".header.html" > "$filename"
  292. echo "<title>$title</title>" >> "$filename"
  293. google_analytics >> "$filename"
  294. echo "</head><body>" >> "$filename"
  295. # stuff to add before the actual body content
  296. [[ -n "$body_begin_file" ]] && cat "$body_begin_file" >> "$filename"
  297. # body divs
  298. echo '<div id="divbodyholder">' >> "$filename"
  299. echo '<div class="headerholder"><div class="header">' >> "$filename"
  300. # blog title
  301. echo '<div id="title">' >> "$filename"
  302. cat .title.html >> "$filename"
  303. echo '</div></div></div>' >> "$filename" # title, header, headerholder
  304. echo '<div id="divbody"><div class="content">' >> "$filename"
  305. file_url="$(sed 's/.rebuilt//g' <<< $filename)" # Get the correct URL when rebuilding
  306. # one blog entry
  307. if [[ "$index" == "no" ]]; then
  308. echo '<!-- entry begin -->' >> "$filename" # marks the beginning of the whole post
  309. echo '<h3><a class="ablack" href="'$global_url/$file_url'">' >> "$filename"
  310. # remove possible <p>'s on the title because of markdown conversion
  311. echo "$(echo "$title" | sed 's/<\/*p>//g')" >> "$filename"
  312. echo '</a></h3>' >> "$filename"
  313. if [[ "$timestamp" == "" ]]; then
  314. echo '<div class="subtitle">'$(LC_ALL=$date_locale date +"$date_format")' &mdash; ' >> "$filename"
  315. else
  316. echo '<div class="subtitle">'$(LC_ALL=$date_locale date +"$date_format" --date="$timestamp") ' &mdash; ' >> "$filename"
  317. fi
  318. echo "$global_author</div>" >> "$filename"
  319. echo '<!-- text begin -->' >> "$filename" # This marks the text body, after the title, date...
  320. fi
  321. cat "$content" >> "$filename" # Actual content
  322. if [[ "$index" == "no" ]]; then
  323. echo '<!-- text end -->' >> "$filename"
  324. twitter "$global_url/$file_url" >> "$filename"
  325. echo '<!-- entry end -->' >> "$filename" # absolute end of the post
  326. fi
  327. echo '</div>' >> "$filename" # content
  328. # Add disqus commments except for index and all_posts pages
  329. if [[ ${filename%.*.*} != "index" && ${filename%.*.*} != "all_posts" ]]; then
  330. disqus_body >> "$filename"
  331. fi
  332. # page footer
  333. cat .footer.html >> "$filename"
  334. # close divs
  335. echo '</div></div>' >> "$filename" # divbody and divbodyholder
  336. disqus_footer >> "$filename"
  337. echo '</body></html>' >> "$filename"
  338. }
  339. # Parse the plain text file into an html file
  340. parse_file() {
  341. # Read for the title and check that the filename is ok
  342. title=""
  343. while read line; do
  344. if [[ "$title" == "" ]]; then
  345. # set title and
  346. # remove extra <p> and </p> added by markdown
  347. title=$(echo "$line" | sed 's/<\/*p>//g')
  348. filename="$(echo $title | tr [:upper:] [:lower:])"
  349. filename="$(echo $filename | sed 's/\ /-/g')"
  350. filename="$(echo $filename | tr -dc '[:alnum:]-')" # html likes alphanumeric
  351. filename="$filename.html"
  352. content="$filename.tmp"
  353. # Check for duplicate file names
  354. while [ -f "$filename" ]; do
  355. suffix="$RANDOM"
  356. filename="$(echo $filename | sed 's/\.html/'$suffix'\.html/g')"
  357. done
  358. else
  359. echo "$line" >> "$content"
  360. fi
  361. done < "$1"
  362. # Create the actual html page
  363. create_html_page "$content" "$filename" no "$title"
  364. rm "$content"
  365. }
  366. # Manages the creation of the text file and the parsing to html file
  367. # also the drafts
  368. write_entry() {
  369. fmt="html"; f="$2"
  370. [[ "$2" == "-m" ]] && fmt="md" && f="$3"
  371. if [[ "$fmt" == "md" ]]; then
  372. test_markdown
  373. if [[ "$?" -ne 0 ]]; then
  374. echo "Markdown is not working, please use HTML. Press a key to continue..."
  375. fmt="html"
  376. read
  377. fi
  378. fi
  379. if [[ "$f" != "" ]]; then
  380. TMPFILE="$f"
  381. if [[ ! -f "$TMPFILE" ]]; then
  382. echo "The file doesn't exist"
  383. delete_includes
  384. exit
  385. fi
  386. # check if TMPFILE is markdown even though the user didn't specify it
  387. extension="${TMPFILE##*.}"
  388. [[ "$extension" == "md" ]] && fmt="md"
  389. else
  390. TMPFILE=".entry-$RANDOM.$fmt"
  391. echo "Title on this line" >> "$TMPFILE"
  392. echo "" >> "$TMPFILE"
  393. [[ "$fmt" == "html" ]] && echo -n "<p>" >> "$TMPFILE"
  394. echo -n "The rest of the text file is " >> "$TMPFILE"
  395. [[ "$fmt" == "html" ]] && echo -n "an <b>html</b> " >> "$TMPFILE"
  396. [[ "$fmt" == "md" ]] && echo -n "a **Markdown** " >> "$TMPFILE"
  397. echo -n "blog post. The process will continue as soon as you exit your editor" >> "$TMPFILE"
  398. [[ "$fmt" == "html" ]] && echo "</p>" >> "$TMPFILE"
  399. fi
  400. chmod 600 "$TMPFILE"
  401. post_status="E"
  402. while [ "$post_status" != "p" ] && [ "$post_status" != "P" ]; do
  403. $EDITOR "$TMPFILE"
  404. if [[ "$fmt" == "md" ]]; then
  405. html_from_md="$(markdown "$TMPFILE")"
  406. parse_file "$html_from_md"
  407. rm "$html_from_md"
  408. else
  409. parse_file "$TMPFILE" # this command sets $filename as the html processed file
  410. fi
  411. chmod 600 "$filename"
  412. echo -n "Preview? (Y/n) "
  413. read p
  414. if [[ "$p" != "n" ]] && [[ "$p" != "N" ]]; then
  415. chmod 644 "$filename"
  416. echo "Open $global_url/$filename in your browser"
  417. fi
  418. echo -n "[P]ost this entry, [E]dit again, [D]raft for later? (p/E/d) "
  419. read post_status
  420. if [[ "$post_status" == "d" ]] || [[ "$post_status" == "D" ]]; then
  421. mkdir -p "drafts/"
  422. chmod 700 "drafts/"
  423. title="$(head -n 1 $TMPFILE)"
  424. title="$(echo $title | tr [:upper:] [:lower:])"
  425. title="$(echo $title | sed 's/\ /-/g')"
  426. title="$(echo $title | tr -dc '[:alnum:]-')"
  427. draft="drafts/$title.$fmt"
  428. while [ -f "$draft" ]; do draft="drafts/$title-$RANDOM.$fmt"; done
  429. mv "$TMPFILE" "$draft"
  430. chmod 600 "$draft"
  431. rm "$filename"
  432. delete_includes
  433. echo "Saved your draft as '$draft'"
  434. exit
  435. fi
  436. if [[ "$post_status" == "e" ]] || [[ "$post_status" == "E" ]]; then
  437. rm "$filename" # Delete the html file as it will be generated again
  438. fi
  439. done
  440. rm "$TMPFILE"
  441. chmod 644 "$filename"
  442. echo "Posted $filename"
  443. }
  444. # Create an index page with all the posts
  445. all_posts() {
  446. echo -n "Creating an index page with all the posts "
  447. contentfile="$archive_index.$RANDOM"
  448. while [ -f "$contentfile" ]; do
  449. contentfile="$archive_index.$RANDOM"
  450. done
  451. echo "<h3>All posts</h3>" >> "$contentfile"
  452. echo "<ul>" >> "$contentfile"
  453. for i in $(ls -t *.html); do
  454. if [[ "$i" == "$index_file" ]] || [[ "$i" == "$archive_index" ]]; then continue; fi
  455. echo -n "."
  456. # Title
  457. title="$(awk '/<h3><a class="ablack" href=".+">/, /<\/a><\/h3>/{if (!/<h3><a class="ablack" href=".+">/ && !/<\/a><\/h3>/) print}' $i)"
  458. echo -n '<li><a href="'$global_url/$i'">'$title'</a> &mdash;' >> "$contentfile"
  459. # Date
  460. date="$(LC_ALL=$date_locale date -r "$i" +"$date_format")"
  461. echo " $date</li>" >> "$contentfile"
  462. done
  463. echo ""
  464. echo "</ul>" >> "$contentfile"
  465. echo '<div id="all_posts"><a href="'$global_url'">'$template_archive_index_page'</a></div>' >> "$contentfile"
  466. create_html_page "$contentfile" "$archive_index.tmp" yes "$global_title &mdash; All posts"
  467. mv "$archive_index.tmp" "$archive_index"
  468. chmod 644 "$archive_index"
  469. rm "$contentfile"
  470. }
  471. # Generate the index.html with the content of the latest posts
  472. rebuild_index() {
  473. echo -n "Rebuilding the index "
  474. newindexfile="$index_file.$RANDOM"
  475. contentfile="$newindexfile.content"
  476. while [ -f "$newindexfile" ]; do
  477. newindexfile="$index_file.$RANDOM"
  478. contentfile="$newindexfile.content"
  479. done
  480. # Create the content file
  481. n=0
  482. for i in $(ls -t *.html); do # sort by date, newest first
  483. if [[ "$i" == "$index_file" ]] || [[ "$i" == "$archive_index" ]]; then continue; fi
  484. if [[ "$n" -ge "$number_of_index_articles" ]]; then break; fi
  485. awk '/<!-- entry begin -->/, /<!-- entry end -->/' "$i" >> "$contentfile"
  486. echo -n "."
  487. n=$(( $n + 1 ))
  488. done
  489. if [[ "$global_feedburner" == "" ]]; then
  490. echo '<div id="all_posts"><a href="'$archive_index'">'$template_archive'</a> &mdash; <a href="'$blog_feed'">'$template_subscribe'</a></div>' >> "$contentfile"
  491. else
  492. echo '<div id="all_posts"><a href="'$archive_index'">'$template_archive'</a> &mdash; <a href="'$global_feedburner'">Subscribe</a></div>' >> "$contentfile"
  493. fi
  494. echo ""
  495. create_html_page "$contentfile" "$newindexfile" yes "$global_title"
  496. rm "$contentfile"
  497. mv "$newindexfile" "$index_file"
  498. chmod 644 "$index_file"
  499. }
  500. # Displays a list of the posts
  501. list_posts() {
  502. ls *.html &> /dev/null
  503. [[ $? -ne 0 ]] && echo "No posts yet. Use 'bb.sh post' to create one" && return
  504. lines=""
  505. n=1
  506. for i in $(ls -t *.html); do
  507. if [[ "$i" == "$index_file" ]] || [[ "$i" == "$archive_index" ]]; then continue; fi
  508. line="$n # $(awk '/<h3><a class="ablack" href=".+">/, /<\/a><\/h3>/{if (!/<h3><a class="ablack" href=".+">/ && !/<\/a><\/h3>/) print}' $i) # $(LC_ALL=$date_locale date -r $i +"date_format")"
  509. lines="${lines}""$line""\n" # Weird stuff needed for the newlines
  510. n=$(( $n + 1 ))
  511. done
  512. echo -e "$lines" | column -t -s "#"
  513. }
  514. # Generate the feed file
  515. make_rss() {
  516. echo -n "Making RSS "
  517. rssfile="$blog_feed.$RANDOM"
  518. while [ -f "$rssfile" ]; do rssfile="$blog_feed.$RANDOM"; done
  519. echo '<?xml version="1.0" encoding="UTF-8" ?>' >> "$rssfile"
  520. echo '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">' >> "$rssfile"
  521. echo '<channel><title>'$global_title'</title><link>'$global_url'</link>' >> "$rssfile"
  522. echo '<description>'$global_description'</description><language>en</language>' >> "$rssfile"
  523. echo '<lastBuildDate>'$(date +"%a, %d %b %Y %H:%M:%S %z")'</lastBuildDate>' >> "$rssfile"
  524. echo '<pubDate>'$(date +"%a, %d %b %Y %H:%M:%S %z")'</pubDate>' >> "$rssfile"
  525. echo '<atom:link href="'$global_url/$blog_feed'" rel="self" type="application/rss+xml" />' >> "$rssfile"
  526. n=0
  527. for i in $(ls -t *.html); do
  528. if [[ "$i" == "$index_file" ]] || [[ "$i" == "$archive_index" ]]; then continue; fi
  529. [[ "$n" -ge "$number_of_feed_articles" ]] && break # max 10 items
  530. echo -n "."
  531. echo '<item><title>' >> "$rssfile"
  532. echo "$(awk '/<h3><a class="ablack" href=".+">/, /<\/a><\/h3>/{if (!/<h3><a class="ablack" href=".+">/ && !/<\/a><\/h3>/) print}' $i)" >> "$rssfile"
  533. echo '</title><description><![CDATA[' >> "$rssfile"
  534. echo "$(awk '/<!-- text begin -->/, /<!-- entry end -->/{if (!/<!-- text begin -->/ && !/<!-- entry end -->/) print}' $i)" >> "$rssfile"
  535. echo "]]></description><link>$global_url/$i</link>" >> "$rssfile"
  536. echo "<guid>$global_url/$i</guid>" >> "$rssfile"
  537. echo "<dc:creator>$global_author</dc:creator>" >> "$rssfile"
  538. echo '<pubDate>'$(date -r "$i" +"%a, %d %b %Y %H:%M:%S %z")'</pubDate></item>' >> "$rssfile"
  539. n=$(( $n + 1 ))
  540. done
  541. echo '</channel></rss>' >> "$rssfile"
  542. echo ""
  543. mv "$rssfile" "$blog_feed"
  544. chmod 644 "$blog_feed"
  545. }
  546. # generate headers, footers, etc
  547. create_includes() {
  548. echo '<h1 class="nomargin"><a class="ablack" href="'$global_url'">'$global_title'</a></h1>' > ".title.html"
  549. echo '<div id="description">'$global_description'</div>' >> ".title.html"
  550. if [[ -f "$header_file" ]]; then cp "$header_file" .header.html
  551. else
  552. echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' > ".header.html"
  553. echo '<html xmlns="http://www.w3.org/1999/xhtml"><head>' >> ".header.html"
  554. echo '<meta http-equiv="Content-type" content="text/html;charset=UTF-8" />' >> ".header.html"
  555. echo '<link rel="stylesheet" href="main.css" type="text/css" />' >> ".header.html"
  556. echo '<link rel="stylesheet" href="blog.css" type="text/css" />' >> ".header.html"
  557. if [[ "$global_feedburner" == "" ]]; then
  558. echo '<link rel="alternate" type="application/rss+xml" title="'$template_subscribe_browser_button'" href="'$blog_feed'" />' >> ".header.html"
  559. else
  560. echo '<link rel="alternate" type="application/rss+xml" title="'$template_subscribe_browser_button'" href="'$global_feedburner'" />' >> ".header.html"
  561. fi
  562. fi
  563. if [[ -f "$footer_file" ]]; then cp "$footer_file" .footer.html
  564. else
  565. protected_mail="$(echo "$global_email" | sed 's/@/\&#64;/g' | sed 's/\./\&#46;/g')"
  566. echo '<div id="footer">'$global_license '<a href="'$global_author_url'">'$global_author'</a> &mdash; <a href="mailto:'$protected_mail'">'$protected_mail'</a><br/>' >> ".footer.html"
  567. echo 'Generated with <a href="https://github.com/cfenollosa/bashblog">bashblog</a>, a single bash script to easily create blogs like this one</div>' >> ".footer.html"
  568. fi
  569. }
  570. # Delete the temporarily generated include files
  571. delete_includes() {
  572. rm ".title.html" ".footer.html" ".header.html"
  573. }
  574. # Create the css file from scratch
  575. create_css() {
  576. # To avoid overwriting manual changes. However it is recommended that
  577. # this function is modified if the user changes the blog.css file
  578. if [[ ! -f "blog.css" ]]; then
  579. # blog.css directives will be loaded after main.css and thus will prevail
  580. echo '#title{font-size: x-large;}
  581. a.ablack{color:black !important;}
  582. li{margin-bottom:8px;}
  583. ul,ol{margin-left:24px;margin-right:24px;}
  584. #all_posts{margin-top:24px;text-align:center;}
  585. .subtitle{font-size:small;margin:12px 0px;}
  586. .content p{margin-left:24px;margin-right:24px;}
  587. h1{margin-bottom:12px !important;}
  588. #description{font-size:large;margin-bottom:12px;}
  589. h3{margin-top:42px;margin-bottom:8px;}
  590. h4{margin-left:24px;margin-right:24px;}
  591. #twitter{line-height:20px;vertical-align:top;text-align:right;font-style:italic;color:#333;margin-top:24px;font-size:14px;}' > blog.css
  592. fi
  593. # If there is a style.css from the parent page (i.e. some landing page)
  594. # then use it. This directive is here for compatibility with my own
  595. # home page. Feel free to edit it out, though it doesn't hurt
  596. if [[ -f "../style.css" ]] && [[ ! -f "main.css" ]]; then
  597. ln -s "../style.css" "main.css"
  598. elif [[ ! -f "main.css" ]]; then
  599. echo 'body{font-family:Georgia,"Times New Roman",Times,serif;margin:0;padding:0;background-color:#F3F3F3;}
  600. #divbodyholder{padding:5px;background-color:#DDD;width:874px;margin:24px auto;}
  601. #divbody{width:776px;border:solid 1px #ccc;background-color:#fff;padding:0px 48px 24px 48px;top:0;}
  602. .headerholder{background-color:#f9f9f9;border-top:solid 1px #ccc;border-left:solid 1px #ccc;border-right:solid 1px #ccc;}
  603. .header{width:800px;margin:0px auto;padding-top:24px;padding-bottom:8px;}
  604. .content{margin-bottom:45px;}
  605. .nomargin{margin:0;}
  606. .description{margin-top:10px;border-top:solid 1px #666;padding:10px 0;}
  607. h3{font-size:20pt;width:100%;font-weight:bold;margin-top:32px;margin-bottom:0;}
  608. .clear{clear:both;}
  609. #footer{padding-top:10px;border-top:solid 1px #666;color:#333333;text-align:center;font-size:small;font-family:"Courier New","Courier",monospace;}
  610. a{text-decoration:none;color:#003366 !important;}
  611. a:visited{text-decoration:none;color:#336699 !important;}
  612. blockquote{background-color:#f9f9f9;border-left:solid 4px #e9e9e9;margin-left:12px;padding:12px 12px 12px 24px;}
  613. blockquote img{margin:12px 0px;}
  614. blockquote iframe{margin:12px 0px;}' > main.css
  615. fi
  616. }
  617. # Regenerates all the single post entries, keeping the post content but modifying
  618. # the title, html structure, etc
  619. rebuild_all_entries() {
  620. echo -n "Rebuilding all entries "
  621. for i in *.html; do # no need to sort
  622. if [[ "$i" == "$index_file" ]] || [[ "$i" == "$archive_index" ]] || [[ "$i" == "$footer_file" ]] || [[ "$i" == "$header_file" ]]; then continue; fi
  623. contentfile=".tmp.$RANDOM"
  624. while [ -f "$contentfile" ]; do contentfile=".tmp.$RANDOM"; done
  625. echo -n "."
  626. # Get the title and entry, and rebuild the html structure from scratch (divs, title, description...)
  627. title="$(awk '/<h3><a class="ablack" href=".+">/, /<\/a><\/h3>/{if (!/<h3><a class="ablack" href=".+">/ && !/<\/a><\/h3>/) print}' $i)"
  628. awk '/<!-- text begin -->/, /<!-- text end -->/{if (!/<!-- text begin -->/ && !/<!-- text end -->/) print}' "$i" >> "$contentfile"
  629. # Original post timestamp
  630. timestamp="$(LC_ALL=$date_locale date -r $i +"%a, %d %b %Y %H:%M:%S %z" )"
  631. create_html_page "$contentfile" "$i.rebuilt" no "$title" "$timestamp"
  632. # keep the original timestamp!
  633. timestamp="$(LC_ALL=$date_locale date -r $i +'%Y%m%d%H%M')"
  634. mv "$i.rebuilt" "$i"
  635. chmod 644 "$i"
  636. touch -t $timestamp "$i"
  637. rm "$contentfile"
  638. done
  639. echo ""
  640. }
  641. # Displays the help
  642. function usage() {
  643. echo "$global_software_name v$global_software_version"
  644. echo "Usage: $0 command [filename]"
  645. echo ""
  646. echo "Commands:"
  647. echo " post [-m] [filename] insert a new blog post, or the FILENAME of a draft to continue editing it"
  648. echo " use '-m' to edit the post as Markdown text"
  649. echo " edit [filename] edit an already published .html file. Never edit manually a published .html file,"
  650. echo " always use this function as it keeps the original timestamp "
  651. echo " and rebuilds whatever indices are needed"
  652. echo " rebuild regenerates all the pages and posts, preserving the content of the entries"
  653. echo " reset deletes blog-generated files. Use with a lot of caution and back up first!"
  654. echo " list list all entries. Useful for debug"
  655. echo ""
  656. echo "For more information please open $0 in a code editor and read the header and comments"
  657. }
  658. # Delete all generated content, leaving only this script
  659. reset() {
  660. echo "Are you sure you want to delete all blog entries? Please write \"Yes, I am!\" "
  661. read line
  662. if [[ "$line" == "Yes, I am!" ]]; then
  663. rm .*.html *.html *.css *.rss &> /dev/null
  664. echo
  665. echo "Deleted all posts, stylesheets and feeds."
  666. echo "Kept your old '.backup.tar.gz' just in case, please delete it manually if needed."
  667. else
  668. echo "Phew! You dodged a bullet there. Nothing was modified."
  669. fi
  670. }
  671. # Detects if GNU date is installed
  672. date_version_detect() {
  673. date --version >/dev/null 2>&1
  674. if [[ $? -ne 0 ]]; then
  675. # date utility is BSD. Test if gdate is installed
  676. if gdate --version >/dev/null 2>&1 ; then
  677. date() {
  678. gdate "$@"
  679. }
  680. else
  681. # BSD date
  682. date() {
  683. if [[ "$1" == "-r" ]]; then
  684. # Fall back to using stat for 'date -r'
  685. format=$(echo $3 | sed 's/\+//g')
  686. stat -f "%Sm" -t "$format" "$2"
  687. elif [[ $(echo $@ | grep '\-\-date') ]]; then
  688. # convert between dates using BSD date syntax
  689. /bin/date -j -f "%a, %d %b %Y %H:%M:%S %z" "$(echo $2 | sed 's/\-\-date\=//g')" "$1"
  690. else
  691. # acceptable format for BSD date
  692. /bin/date -j "$@"
  693. fi
  694. }
  695. fi
  696. fi
  697. }
  698. # Main function
  699. # Encapsulated on its own function for readability purposes
  700. #
  701. # $1 command to run
  702. # $2 file name of a draft to continue editing (optional)
  703. do_main() {
  704. # Detect if using BSD date or GNU date
  705. date_version_detect
  706. # Load default configuration, then override settings with the config file
  707. global_variables
  708. [[ -f "$global_config" ]] && source "$global_config" &> /dev/null
  709. global_variables_check
  710. # Check for $EDITOR
  711. [[ -z "$EDITOR" ]] &&
  712. echo "Please set your \$EDITOR environment variable" && exit
  713. # Check for validity of argument
  714. [[ "$1" != "reset" ]] && [[ "$1" != "post" ]] && [[ "$1" != "rebuild" ]] && [[ "$1" != "list" ]] && [[ "$1" != "edit" ]] &&
  715. usage && exit
  716. [[ "$1" == "list" ]] &&
  717. list_posts && exit
  718. if [[ "$1" == "edit" ]]; then
  719. if [[ $# -lt 2 ]] || [[ ! -f "$2" ]]; then
  720. echo "Please enter a valid html file to edit"
  721. exit
  722. fi
  723. fi
  724. # Test for existing html files
  725. ls *.html &> /dev/null
  726. [[ $? -ne 0 ]] && [[ "$1" == "rebuild" ]] &&
  727. echo "Can't find any html files, nothing to rebuild" && exit
  728. # We're going to back up just in case
  729. ls *.html &> /dev/null
  730. [[ $? -eq 0 ]] &&
  731. tar cfz ".backup.tar.gz" *.html &&
  732. chmod 600 ".backup.tar.gz"
  733. [[ "$1" == "reset" ]] &&
  734. reset && exit
  735. create_includes
  736. create_css
  737. [[ "$1" == "post" ]] && write_entry "$@"
  738. [[ "$1" == "rebuild" ]] && rebuild_all_entries
  739. [[ "$1" == "edit" ]] && edit "$2"
  740. rebuild_index
  741. all_posts
  742. make_rss
  743. delete_includes
  744. }
  745. #
  746. # MAIN
  747. # Do not change anything here. If you want to modify the code, edit do_main()
  748. #
  749. do_main $*