├── .markdownlint.json
├── .lefthook.yaml
├── Bakefile.sh
├── .gitattributes
├── .editorconfig
├── .github
└── workflows
│ └── main.yml
├── assets
└── mastodon-logo.svg
├── CONTRIBUTING.md
├── CODE-OF-CONDUCT.md
├── LICENSE
├── README.md
└── bake
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "no-inline-html": false,
3 | "line-length": false
4 | }
5 |
--------------------------------------------------------------------------------
/.lefthook.yaml:
--------------------------------------------------------------------------------
1 | pre-commit:
2 | commands:
3 | lint:
4 | run: './bake lint'
5 |
--------------------------------------------------------------------------------
/Bakefile.sh:
--------------------------------------------------------------------------------
1 | # shellcheck shell=bash
2 |
3 | task.lint() {
4 | pnpx awesome-lint
5 | }
6 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 | *.md linguist-detectable
3 | bake linguist-generated
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.{md,yml,yaml}]
11 | indent_style = space
12 | indent_size = 2
13 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches: ['main']
4 | paths:
5 | - 'README.md'
6 | pull_request:
7 | branches: ['main']
8 | paths:
9 | - 'README.md'
10 | jobs:
11 | lint:
12 | runs-on: 'ubuntu-latest'
13 | steps:
14 | - uses: 'actions/checkout@v3'
15 | with:
16 | fetch-depth: 0
17 | - run: 'npx awesome-lint'
18 |
--------------------------------------------------------------------------------
/assets/mastodon-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribution Guidelines
2 |
3 | Please note that this project is released with a [Contributor Code of Conduct](CODE-OF-CONDUCT.md). By participating in this project you agree to abide by its terms.
4 |
5 | ## Table of Contents
6 |
7 | - [Adding to this list](#adding-to-this-list)
8 | - [Adding something to an awesome list](#adding-something-to-an-awesome-list)
9 |
10 | ## Adding to this list
11 |
12 | Please ensure your pull request adheres to the following guidelines:
13 |
14 | - Search previous suggestions before making a new one, as yours may be a duplicate.
15 | - Make sure the item is useful before submitting.
16 | - Make an individual pull request for each suggestion.
17 | - Use [title-casing](http://titlecapitalization.com) (AP style).
18 | - Use the following format: `[List Name](link) - Description.`
19 | - Link additions should be added to the bottom of the relevant category.
20 | - New categories or improvements to the existing categorization are welcome.
21 | - Check your spelling and grammar.
22 | - Make sure your text editor is set to remove trailing whitespace.
23 | - The pull request and commit should have a useful title.
24 | - The body of your commit message should contain a link to the repository.
25 |
26 | Thank you for your suggestions!
27 |
28 | ## Adding something to an awesome list
29 |
30 | If you have something awesome to contribute to an awesome list, this is how you do it.
31 |
32 | You'll need a [GitHub account](https://github.com/join)!
33 |
34 | 1. Access the awesome list's GitHub page. For example: https://github.com/sindresorhus/awesome
35 | 2. Click on the `README.md` file: 
36 | 3. Now click on the edit icon. 
37 | 4. You can start editing the text of the file in the in-browser editor. Make sure you follow guidelines above. You can use [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/). 
38 | 5. Say why you're proposing the changes, and then click on "Propose file change". 
39 | 6. Submit the [pull request](https://help.github.com/articles/using-pull-requests/)!
40 |
--------------------------------------------------------------------------------
/CODE-OF-CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, caste, color, religion, or sexual
10 | identity and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the overall
26 | community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or advances of
31 | any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email address,
35 | without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at [edwin@kofler.dev](mailto:edwin@kofler.dev).
63 | All complaints will be reviewed and investigated promptly and fairly.
64 |
65 | All community leaders are obligated to respect the privacy and security of the
66 | reporter of any incident.
67 |
68 | ## Enforcement Guidelines
69 |
70 | Community leaders will follow these Community Impact Guidelines in determining
71 | the consequences for any action they deem in violation of this Code of Conduct:
72 |
73 | ### 1. Correction
74 |
75 | **Community Impact**: Use of inappropriate language or other behavior deemed
76 | unprofessional or unwelcome in the community.
77 |
78 | **Consequence**: A private, written warning from community leaders, providing
79 | clarity around the nature of the violation and an explanation of why the
80 | behavior was inappropriate. A public apology may be requested.
81 |
82 | ### 2. Warning
83 |
84 | **Community Impact**: A violation through a single incident or series of
85 | actions.
86 |
87 | **Consequence**: A warning with consequences for continued behavior. No
88 | interaction with the people involved, including unsolicited interaction with
89 | those enforcing the Code of Conduct, for a specified period of time. This
90 | includes avoiding interactions in community spaces as well as external channels
91 | like social media. Violating these terms may lead to a temporary or permanent
92 | ban.
93 |
94 | ### 3. Temporary Ban
95 |
96 | **Community Impact**: A serious violation of community standards, including
97 | sustained inappropriate behavior.
98 |
99 | **Consequence**: A temporary ban from any sort of interaction or public
100 | communication with the community for a specified period of time. No public or
101 | private interaction with the people involved, including unsolicited interaction
102 | with those enforcing the Code of Conduct, is allowed during this period.
103 | Violating these terms may lead to a permanent ban.
104 |
105 | ### 4. Permanent Ban
106 |
107 | **Community Impact**: Demonstrating a pattern of violation of community
108 | standards, including sustained inappropriate behavior, harassment of an
109 | individual, or aggression toward or disparagement of classes of individuals.
110 |
111 | **Consequence**: A permanent ban from any sort of public interaction within the
112 | community.
113 |
114 | ## Attribution
115 |
116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
117 | version 2.1, available at
118 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
119 |
120 | Community Impact Guidelines were inspired by
121 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
122 |
123 | For answers to common questions about this code of conduct, see the FAQ at
124 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
125 | [https://www.contributor-covenant.org/translations][translations].
126 |
127 | [homepage]: https://www.contributor-covenant.org
128 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
129 | [Mozilla CoC]: https://github.com/mozilla/diversity
130 | [FAQ]: https://www.contributor-covenant.org/faq
131 | [translations]: https://www.contributor-covenant.org/translations
132 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | CC0 1.0 Universal
2 |
3 | Statement of Purpose
4 |
5 | The laws of most jurisdictions throughout the world automatically confer
6 | exclusive Copyright and Related Rights (defined below) upon the creator and
7 | subsequent owner(s) (each and all, an "owner") of an original work of
8 | authorship and/or a database (each, a "Work").
9 |
10 | Certain owners wish to permanently relinquish those rights to a Work for the
11 | purpose of contributing to a commons of creative, cultural and scientific
12 | works ("Commons") that the public can reliably and without fear of later
13 | claims of infringement build upon, modify, incorporate in other works, reuse
14 | and redistribute as freely as possible in any form whatsoever and for any
15 | purposes, including without limitation commercial purposes. These owners may
16 | contribute to the Commons to promote the ideal of a free culture and the
17 | further production of creative, cultural and scientific works, or to gain
18 | reputation or greater distribution for their Work in part through the use and
19 | efforts of others.
20 |
21 | For these and/or other purposes and motivations, and without any expectation
22 | of additional consideration or compensation, the person associating CC0 with a
23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
25 | and publicly distribute the Work under its terms, with knowledge of his or her
26 | Copyright and Related Rights in the Work and the meaning and intended legal
27 | effect of CC0 on those rights.
28 |
29 | 1. Copyright and Related Rights. A Work made available under CC0 may be
30 | protected by copyright and related or neighboring rights ("Copyright and
31 | Related Rights"). Copyright and Related Rights include, but are not limited
32 | to, the following:
33 |
34 | i. the right to reproduce, adapt, distribute, perform, display, communicate,
35 | and translate a Work;
36 |
37 | ii. moral rights retained by the original author(s) and/or performer(s);
38 |
39 | iii. publicity and privacy rights pertaining to a person's image or likeness
40 | depicted in a Work;
41 |
42 | iv. rights protecting against unfair competition in regards to a Work,
43 | subject to the limitations in paragraph 4(a), below;
44 |
45 | v. rights protecting the extraction, dissemination, use and reuse of data in
46 | a Work;
47 |
48 | vi. database rights (such as those arising under Directive 96/9/EC of the
49 | European Parliament and of the Council of 11 March 1996 on the legal
50 | protection of databases, and under any national implementation thereof,
51 | including any amended or successor version of such directive); and
52 |
53 | vii. other similar, equivalent or corresponding rights throughout the world
54 | based on applicable law or treaty, and any national implementations thereof.
55 |
56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of,
57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
59 | and Related Rights and associated claims and causes of action, whether now
60 | known or unknown (including existing as well as future claims and causes of
61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum
62 | duration provided by applicable law or treaty (including future time
63 | extensions), (iii) in any current or future medium and for any number of
64 | copies, and (iv) for any purpose whatsoever, including without limitation
65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
66 | the Waiver for the benefit of each member of the public at large and to the
67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver
68 | shall not be subject to revocation, rescission, cancellation, termination, or
69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work
70 | by the public as contemplated by Affirmer's express Statement of Purpose.
71 |
72 | 3. Public License Fallback. Should any part of the Waiver for any reason be
73 | judged legally invalid or ineffective under applicable law, then the Waiver
74 | shall be preserved to the maximum extent permitted taking into account
75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
76 | is so judged Affirmer hereby grants to each affected person a royalty-free,
77 | non transferable, non sublicensable, non exclusive, irrevocable and
78 | unconditional license to exercise Affirmer's Copyright and Related Rights in
79 | the Work (i) in all territories worldwide, (ii) for the maximum duration
80 | provided by applicable law or treaty (including future time extensions), (iii)
81 | in any current or future medium and for any number of copies, and (iv) for any
82 | purpose whatsoever, including without limitation commercial, advertising or
83 | promotional purposes (the "License"). The License shall be deemed effective as
84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the
85 | License for any reason be judged legally invalid or ineffective under
86 | applicable law, such partial invalidity or ineffectiveness shall not
87 | invalidate the remainder of the License, and in such case Affirmer hereby
88 | affirms that he or she will not (i) exercise any of his or her remaining
89 | Copyright and Related Rights in the Work or (ii) assert any associated claims
90 | and causes of action with respect to the Work, in either case contrary to
91 | Affirmer's express Statement of Purpose.
92 |
93 | 4. Limitations and Disclaimers.
94 |
95 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
96 | surrendered, licensed or otherwise affected by this document.
97 |
98 | b. Affirmer offers the Work as-is and makes no representations or warranties
99 | of any kind concerning the Work, express, implied, statutory or otherwise,
100 | including without limitation warranties of title, merchantability, fitness
101 | for a particular purpose, non infringement, or the absence of latent or
102 | other defects, accuracy, or the present or absence of errors, whether or not
103 | discoverable, all to the greatest extent permissible under applicable law.
104 |
105 | c. Affirmer disclaims responsibility for clearing rights of other persons
106 | that may apply to the Work or any use thereof, including without limitation
107 | any person's Copyright and Related Rights in the Work. Further, Affirmer
108 | disclaims responsibility for obtaining any necessary consents, permissions
109 | or other rights required for any use of the Work.
110 |
111 | d. Affirmer understands and acknowledges that Creative Commons is not a
112 | party to this document and has no duty or obligation with respect to this
113 | CC0 or use of the Work.
114 |
115 | For more information, please see
116 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Awesome Mastodon [](https://awesome.re)
2 |
3 | [](https://joinmastodon.org/)
4 |
5 | Mastodon is the world's largest free, open-source, decentralized microblogging network.
6 |
7 | This list is a collection of the best Mastodon resources.
8 |
9 | ## Contents
10 |
11 | - [Official](#official)
12 | - [Instances](#instances)
13 | - [Statistics](#statistics)
14 | - [People](#people)
15 | - [Tools](#tools)
16 | - [Desktop Clients](#desktop-clients)
17 | - [Mobile Clients](#mobile-clients)
18 | - [Libraries](#libraries)
19 | - [Servers](#servers)
20 | - [Guides](#guides)
21 | - [Hosting](#hosting)
22 | - [Similar Projects](#similar-projects)
23 | - [Bots](#bots)
24 |
25 | ## Official
26 |
27 | - [GitHub Repository](https://github.com/tootsuite/mastodon/)
28 | - [Documentation & Help Center](https://docs.joinmastodon.org/)
29 | - [API Documentation](https://docs.joinmastodon.org/client/intro/)
30 |
31 | ## Instances
32 |
33 | - [Official Instance List](https://joinmastodon.org/servers) - Official List of Mastodon instances.
34 | - [mastodonservers.net Instance List](https://mastodonservers.net/) - Larger list of Mastodon instances.
35 | - [instances.social Instance List](https://instances.social/list/) - Autogenerated list of Mastodon instances.
36 | - [mastodon.social](https://mastodon.social/about/) - The largest Mastodon server with over 333k active users and 1.5m registered users.
37 | - [EU Server](https://social.network.europa.eu/about/) - The official server for EU institutions.
38 | - [Vivaldi Social](https://social.vivaldi.net/about/) - Mastodon social hosted by Vivaldi.
39 | - [Mozilla Social](https://mozilla.social/about/) - Mastodon social hosted by Mozilla.
40 | - [Journa.host](https://journa.host/about/) - Mastodon server for journalists.
41 | - [Newsie](https://newsie.social/about/) - Another Mastodon server for journalists.
42 | - [Bots in Space](https://botsin.space/about/) - Mastodon server for bot accounts.
43 |
44 | ## Statistics
45 |
46 | - [The Federation](https://the-federation.info/) - Statistics on protocols, users, and nodes of the Fediverse.
47 | - [FediDB](https://fedidb.org/network/) - Fediverse network statistics.
48 | - [FediBuzz](https://fedi.buzz/) - Observe trends across the Fediverse.
49 | - [Fediverse Graph Visualization](https://www.comeetie.fr/galerie/mapstodon/) - Graph visualization of all complete Fediverse.
50 | - [MastoInstance.Info](https://mastoinstance.info/) - View instance metadata and explore the federation graph.
51 | - [fediverse.party](https://fediverse.party/) - Detailed information about the most popular Fediverse projects and protocols.
52 |
53 | ## People
54 |
55 | - [Academics On Mastodon](https://github.com/nathanlesage/academics-on-mastodon/) - Markdown list of academics on Mastodon, by discipline/group/area.
56 | - [Spreadsheet of Popular accounts](https://docs.google.com/spreadsheets/d/1cpUKkoT1MUn8_xM4usiERn-IdEuh0hXfBrwbbThwGiI/edit#gid=1111869705/) - Google spreadsheet of the most popular Mastodon accounts of the Fediverse.
57 | - [Spreadsheet of Journalist accounts](https://docs.google.com/spreadsheets/d/13No4yxY-oFrN8PigC2jBWXreFCHWwVRTftwP6HcREtA/edit#gid=1320898902/) - Google spreadsheet of various journalists of the Fediverse.
58 |
59 | ## Tools
60 |
61 | - [Toot Scheduler](https://scheduler.mastodon.tools/) - Schedule toots to "toot" later.
62 | - [Mastodon List Manager](https://www.mastodonlistmanager.org/main/) - Create, manager, filter, and search lists of your selected Mastodon users.
63 | - [Mastodon Toot Bookmarklet](https://rknightuk.github.io/mastodon-toot-bookmarklet/) - Bookmarklet to toot the current page.
64 | - [Mastodon - Simplified Federation!](https://addons.mozilla.org/firefox/addon/mastodon-simplified-federation/) - Redirect clicks on remote follow/interaction buttons to your own instance.
65 | - [Mastodon Link](https://github.com/masrly/mastodon-link/) - Adds Mastodon icon links by profile to make following people on other instances easier.
66 | - [Mastodon Redirector](https://github.com/bramus/mastodon-redirector/) - Adds a button to view profiles from other instances on your profile page.
67 | - [mastotool](https://github.com/muesli/mastotool/) - Collection of command-line tools for working with Mastodon accounts.
68 | - [Mastodon Widgets](https://github.com/splitbrain/mastodon-widget) - JavaScript Web Components to embed a Mastodon profile, timeline, follow and share button into any website. Provides an auto-completing instance selector.
69 |
70 | ## Desktop Clients
71 |
72 | - [Official Client List](https://joinmastodon.org/apps/) - Official list of Mastodon clients.
73 | - [Elk](https://github.com/elk-zone/elk/) - A nimble Mastodon web client.
74 | - [Brutaldon](https://gitlab.com/brutaldon/brutaldon/) - A brutalist web interface for Mastodon.
75 | - [phanpy](https://github.com/cheeaun/phanpy) - A minimalistic and opinionated Mastodon web client.
76 | - [Planiverse](https://git.mulligrubs.me/planiverse/) - Minimalist, no-JS Web client for Mastodon.
77 | - [Official Client](https://github.com/mastodon/mastodon-android/) - Official Android client.
78 | - [Whalebird](https://whalebird.social/en/desktop/contents/) - Electron-based Mastodon client.
79 | - [toot](https://github.com/ihabunek/toot/) - Mastodon CLI & TUI in Python.
80 | - [Tokodon](https://apps.kde.org/tokodon/) - Mastodon client made by KDE.
81 | - [hellclient](https://hell.limitedideas.org) - Mastodon CLI readline client written in Go.
82 |
83 | ## Mobile Clients
84 |
85 | - [Megalodon](https://sk22.github.io/megalodon/) - Fork of the official Android client.
86 | - [Fedilab](https://codeberg.org/tom79/Fedilab/) - Fully-featured Android client.
87 | - [Tusky](https://github.com/tuskyapp/Tusky/) - Intuitive Android client.
88 | - [Ivory](https://apps.apple.com/us/app/ivory-for-mastodon-by-tapbots/id6444602274) - Mastodon client for iOS, by the creators of Tweetbot.
89 | - [Toot!](https://apps.apple.com/us/app/toot/id1229021451) - Mastodon client for iOS.
90 | - [SwiftUI](https://github.com/Dimillian/IceCubesApp) - Mastodon client for iOS using SwiftUI.
91 |
92 | ## Libraries
93 |
94 | - [Official Library List](https://docs.joinmastodon.org/client/libraries/) - Official list of Mastodon client libraries.
95 | - [masto.js](https://github.com/neet/masto.js) - Mastodon client for JavaScript.
96 | - [go-mastodon](https://github.com/mattn/go-mastodon) - Mastodon Client for golang.
97 |
98 | ## Servers
99 |
100 | - [Glitch Edition](https://glitch-soc.github.io/docs/) - Mastodon Fork with local posting, toot formatting, improved settings, and more granulate controls.
101 | - [Hometown](https://github.com/hometown-fork/hometown/) - Mastodon Fork with local posting, following lists, customizable max toos length.
102 | - [ecko](https://github.com/magicstone-dev/ecko/) - Mastodon fork with local posting, toot formatting, and more granulate controls.
103 |
104 | ## Guides
105 |
106 | - [What is Mastodon?](https://www.youtube.com/watch?v=IPSbNdBmWKE) - Official video explaining Mastodon.
107 | - [A Beginner's Guide to Mastodon](https://buffer.com/resources/mastodon-social) - An unofficial guide to Mastodon.
108 | - [Fedi.Tips](https://fedi.tips) - An unofficial guide to Mastodon and the Fediverse.
109 |
110 | ## Hosting
111 |
112 | - [Official Hosting Documentation](https://docs.joinmastodon.org/user/run-your-own) - Official page with introduction and list of hosting providers.
113 | - [Masto.host](https://masto.host) - Fully managed Mastodon hosting.
114 | - [toot.io](https://toot.io/mastodon_hosting.html) - Managed Mastodon Hosting, used by ACM, Microsoft Dynamics, etc.
115 | - [Cloudplane](https://cloudplane.org) - Fully manged hosting for open-source apps.
116 | - [elestio](https://elest.io/open-source/mastodon) - Fully-managed Mastodon hosting.
117 |
118 | ## Similar Projects
119 |
120 | - [aether](https://getaether.net) - Peer-to-peer ephemeral public communications.
121 | - [Pleroma](https://pleroma.social/) - Lightweight microblogging platform.
122 | - [GNU social](https://gnusocial.rocks/) - Oldest microblogging platform.
123 | - [Microblog.pub](https://microblog.pub/) - Single-user lightweight microblogging platform.
124 | - [Hubzilla](https://zotlabs.org/page/hubzilla/hubzilla-project/) - Blog/social networks platform with file, contacts and events sharing.
125 | - [Friendica](https://friendi.ca/) - Social network platform.
126 | - [Peertube](https://joinpeertube.org/) - Video sharing platform.
127 | - [FunkWhale](https://funkwhale.audio/) - Audio sharing platform.
128 | - [Plume](https://joinplu.me/) - Blogging platform.
129 | - [WriteFreely](https://writefreely.org/) - Blogging platform.
130 | - [PixelFed](https://pixelfed.org/) - Photograph sharing platform.
131 | - [Misskey Hub](https://misskey-hub.net/en/) - Interplanetary microblogging platform.
132 | - [Lemmy](https://join-lemmy.org/) - Link aggregation platform.
133 | - [Kbin](https://kbin.social) - Link aggregation platform.
134 |
135 | ## Bots
136 |
137 | - [feed2toot](https://gitlab.com/chaica/feed2toot) - Automatically parses RSS feeds, identifies new posts and posts them on Mastodon (Python).
138 | - [aerialbot](https://github.com/doersino/aerialbot) - Respond with geotagged aerial imagery of a random location in the world.
139 | - [rust-trending](https://github.com/pbzweihander/rust-trending) - Post trending Rust repositories on Mastodon (inspired by TrendingGithub).
140 | - [@TrendingBot@mastodon.social](https://mastodon.social/@TrendingBot) - Shows you what's trending on Mastodon.
141 | - [News Bot](https://botsin.space/@newsbot) - Mirrors Twitter accounts on Mastodon (ClojureScript), source available on [GitHub](https://github.com/yogthos/mastodon-bot).
142 | - [@HackerNewsBot@mastodon.social](https://mastodon.social/@HackerNewsBot) - Post Hacker News posts with over 100 upvotes.
143 | - [@launchradar@mastodon.cloud](https://mastodon.cloud/@launchradar) - News about space flight, astronomy and astrophysics.
144 |
--------------------------------------------------------------------------------
/bake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # @name Bake
4 | # @brief Bake: A Bash-based Make alternative
5 | # @description Bake is a dead-simple task runner used to quickly cobble together shell scripts
6 | #
7 | # In a few words, Bake lets you call the following 'print' task with './bake print'
8 | #
9 | # ```bash
10 | # #!/usr/bin/env bash
11 | # task.print() {
12 | # printf '%s\n' 'Contrived example'
13 | # }
14 | # ```
15 | #
16 | # Learn more about it [on GitHub](https://github.com/hyperupcall/bake)
17 |
18 | __global_bake_version='1.11.2'
19 |
20 | if [ "$BAKE_INTERNAL_ONLY_VERSION" = 'yes' ]; then
21 | # This is unused, but keep it here just in case
22 | # shellcheck disable=SC2034
23 | BAKE_INTERNAL_ONLY_VERSION_SUCCESS='yes'
24 |
25 | return 0
26 | fi
27 |
28 | if [ "$0" != "${BASH_SOURCE[0]}" ] && [ "$BAKE_INTERNAL_CAN_SOURCE" != 'yes' ]; then
29 | printf '%s\n' 'Error: This file should not be sourced' >&2
30 | return 1
31 | fi
32 |
33 | # @description Prints `$1` formatted as an error and the stacktrace to standard error,
34 | # then exits with code 1
35 | # @arg $1 string Text to print
36 | bake.die() {
37 | if [ -n "$1" ]; then
38 | __bake_error "$1. Exiting"
39 | else
40 | __bake_error 'Exiting'
41 | fi
42 | __bake_print_big --show-time '<- ERROR'
43 |
44 | __bake_print_stacktrace
45 |
46 | exit 1
47 | }
48 |
49 | # @description Prints `$1` formatted as a warning to standard error
50 | # @arg $1 string Text to print
51 | bake.warn() {
52 | if __bake_is_color; then
53 | printf "\033[1;33m%s:\033[0m %s\n" 'Warn' "$1"
54 | else
55 | printf '%s: %s\n' 'Warn' "$1"
56 | fi
57 | } >&2
58 |
59 | # @description Prints `$1` formatted as information to standard output
60 | # @arg $1 string Text to print
61 | bake.info() {
62 | if __bake_is_color; then
63 | printf "\033[0;34m%s:\033[0m %s\n" 'Info' "$1"
64 | else
65 | printf '%s: %s\n' 'Info' "$1"
66 | fi
67 | }
68 |
69 | # breaking: remove in v2
70 | # @description Dies if any of the supplied variables are empty. Deprecated in favor of 'bake.assert_not_empty'
71 | # @arg $@ string Names of variables to check for emptiness
72 | # @see bake.assert_not_empty
73 | bake.assert_nonempty() {
74 | __bake_internal_warn "Function 'bake.assert_nonempty' is deprecated. Please use 'bake.assert_not_empty' instead"
75 | bake.assert_not_empty "$@"
76 | }
77 |
78 | # @description Dies if any of the supplied variables are empty
79 | # @arg $@ string Names of variables to check for emptiness
80 | bake.assert_not_empty() {
81 | local variable_name=
82 | for variable_name; do
83 | local -n ____variable="$variable_name"
84 |
85 | if [ -z "$____variable" ]; then
86 | bake.die "Failed because variable '$variable_name' is empty"
87 | fi
88 | done; unset -v variable_name
89 | }
90 |
91 | # @description Dies if a command cannot be found
92 | # @arg $1 string Command name to test for existence
93 | bake.assert_cmd() {
94 | local cmd=$1
95 |
96 | if [ -z "$cmd" ]; then
97 | bake.die "Argument must not be empty"
98 | fi
99 |
100 | if ! command -v "$cmd" &>/dev/null; then
101 | bake.die "Failed to find command '$cmd'. Please install it before continuing"
102 | fi
103 | }
104 |
105 | # @description Determine if a flag was passed as an argument
106 | # @arg $1 string Flag name to test for
107 | # @arg $@ string Rest of the arguments to search through
108 | bake.has_flag() {
109 | local flag_name="$1"
110 |
111 | if [ -z "$flag_name" ]; then
112 | bake.die "Argument must not be empty"
113 | fi
114 | if ! shift; then
115 | bake.die 'Failed to shift'
116 | fi
117 |
118 | local -a flags=("$@")
119 | if ((${#flags[@]} == 0)); then
120 | flags=("${__bake_args_userflags[@]}")
121 | fi
122 |
123 | local arg=
124 | for arg in "${flags[@]}"; do
125 | if [ "$arg" = "$flag_name" ]; then
126 | return 0
127 | fi
128 | done; unset -v arg
129 |
130 | return 1
131 | }
132 |
133 | # @description Change the behavior of Bake. See [guide.md](./docs/guide.md) for details
134 | # @arg $1 string Name of config property to change
135 | # @arg $2 string New value of config property
136 | bake.cfg() {
137 | local cfg="$1"
138 | local value="$2"
139 |
140 | # breaking: remove in v2
141 | case $cfg in
142 | stacktrace)
143 | case $value in
144 | yes) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg stacktrace' is deprecated. Instead, use either 'on' or 'off'"; __bake_cfg_stacktrace='on' ;;
145 | no) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg stacktrace' is deprecated. Instead, use either 'on' or 'off'"; __bake_cfg_stacktrace='off' ;;
146 | on|off) __bake_cfg_stacktrace=$value ;;
147 | *) __bake_internal_bigdie "Config property '$cfg' accepts only either 'on' or 'off'" ;;
148 | esac
149 | ;;
150 | big-print)
151 | case $value in
152 | yes|no|on|off) __bake_internal_warn "Passing any once-valid value to 'bake.cfg big-print' is deprecated. Instead, use function comments" ;;
153 | *) __bake_internal_bigdie "Config property '$cfg' accepts only either 'on' or 'off'" ;;
154 | esac
155 | ;;
156 | pedantic-task-cd)
157 | case $value in
158 | yes) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg pedantic-task-cd' is deprecated. Instead, use either 'on' or 'off'"; trap '__bake_trap_debug' 'DEBUG' ;;
159 | no) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg pedantic-task-cd' is deprecated. Instead, use either 'on' or 'off'"; trap - 'DEBUG' ;;
160 | on) trap '__bake_trap_debug' 'DEBUG' ;;
161 | off) trap - 'DEBUG' ;;
162 | *) __bake_internal_bigdie "Config property '$cfg' accepts only either 'on' or 'off'" ;;
163 | esac
164 | ;;
165 | *)
166 | __bake_internal_bigdie "No config property matched '$cfg'"
167 | ;;
168 | esac
169 | }
170 |
171 | # @description Prints stacktrace
172 | # @internal
173 | __bake_print_stacktrace() {
174 | if [ "$__bake_cfg_stacktrace" = 'on' ]; then
175 | if __bake_is_color; then
176 | printf '\033[4m%s\033[0m\n' 'Stacktrace:'
177 | else
178 | printf '%s\n' 'Stacktrace:'
179 | fi
180 |
181 | local i=
182 | for ((i=0; i<${#FUNCNAME[@]}-1; ++i)); do
183 | local __bash_source="${BASH_SOURCE[$i]}"; __bash_source=${__bash_source##*/}
184 | printf '%s\n' " in ${FUNCNAME[$i]} ($__bash_source:${BASH_LINENO[$i-1]})"
185 | done; unset -v i __bash_source
186 | fi
187 | } >&2
188 |
189 | # @description Function that is executed when the 'ERR' event is trapped
190 | # @internal
191 | __bake_trap_err() {
192 | local error_code=$?
193 |
194 | __bake_print_big --show-time '<- ERROR'
195 | __bake_internal_error "Your Bakefile did not exit successfully (exit code $error_code)"
196 | __bake_print_stacktrace
197 |
198 | exit $error_code
199 | } >&2
200 |
201 | __global_bake_trap_debug_current_function=
202 | __bake_trap_debug() {
203 | local current_function="${FUNCNAME[1]}"
204 |
205 | if [[ $current_function != "$__global_bake_trap_debug_current_function" \
206 | && $current_function == task.* ]]; then
207 | if ! cd -- "$BAKE_ROOT"; then
208 | __bake_internal_die "Failed to cd to \$BAKE_ROOT"
209 | fi
210 | fi
211 |
212 | __global_bake_trap_debug_current_function=$current_function
213 | } >&2
214 |
215 | # @description Test whether color should be outputed
216 | # @exitcode 0 if should print color
217 | # @exitcode 1 if should not print color
218 | # @internal
219 | __bake_is_color() {
220 | local fd="1"
221 |
222 | if [ ${NO_COLOR+x} ]; then
223 | return 1
224 | fi
225 |
226 | if [[ $FORCE_COLOR == @(1|2|3) ]]; then
227 | return 0
228 | elif [[ $FORCE_COLOR == '0' ]]; then
229 | return 1
230 | fi
231 |
232 | if [ "$TERM" = 'dumb' ]; then
233 | return 1
234 | fi
235 |
236 | if [ -t "$fd" ]; then
237 | return 0
238 | fi
239 |
240 | return 1
241 | }
242 |
243 | # @description Calls `__bake_internal_error` and terminates with code 1
244 | # @arg $1 string Text to print
245 | # @internal
246 | __bake_internal_die() {
247 | __bake_internal_error "$1. Exiting"
248 | exit 1
249 | }
250 |
251 | # @description Calls `__bake_internal_error` and terminates with code 1. Before
252 | # doing so, it closes with "<- ERROR" big text
253 | # @arg $1 string Text to print
254 | # @internal
255 | __bake_internal_bigdie() {
256 | __bake_print_big '<- ERROR'
257 |
258 | __bake_internal_error "$1. Exiting"
259 | exit 1
260 | }
261 |
262 | # @description Prints `$1` formatted as an internal Bake error to standard error
263 | # @arg $1 Text to print
264 | # @internal
265 | __bake_internal_error() {
266 | if __bake_is_color; then
267 | printf "\033[0;31m%s:\033[0m %s\n" "Error (bake)" "$1"
268 | else
269 | printf '%s: %s\n' 'Error (bake)' "$1"
270 | fi
271 | } >&2
272 |
273 | # @description Prints `$1` formatted as an internal Bake warning to standard error
274 | # @arg $1 Text to print
275 | # @internal
276 | __bake_internal_warn() {
277 | if __bake_is_color; then
278 | printf "\033[0;33m%s:\033[0m %s\n" "Warn (bake)" "$1"
279 | else
280 | printf '%s: %s\n' 'Warn (bake)' "$1"
281 | fi
282 | } >&2
283 |
284 | # @description Prints `$1` formatted as an error to standard error. This is not called because
285 | # I do not wish to surface a public 'bake.error' function. All errors should halt execution
286 | # @arg $1 string Text to print
287 | # @internal
288 | __bake_error() {
289 | if __bake_is_color; then
290 | printf "\033[0;31m%s:\033[0m %s\n" 'Error' "$1"
291 | else
292 | printf '%s: %s\n' 'Error' "$1"
293 | fi
294 | } >&2
295 |
296 |
297 | # @description Tests if the './bake' file should be replaced. It should only
298 | # be replaced if we're not in an interactive Git context
299 | # @internal
300 | __bake_should_replace_bakescript() {
301 | local dir="$BAKE_ROOT"
302 | while [ ! -d "$dir/.git" ] && [[ -n "$dir" ]]; do
303 | dir=${dir%/*}
304 | done
305 |
306 | if [ -d "$dir/.git" ]; then
307 | # ref: https://github.com/git/git/blob/d420dda0576340909c3faff364cfbd1485f70376/wt-status.c#L1749
308 | # ref2: https://github.com/Byron/gitoxide/blob/375051fa97d79f95fa7179b536e616c4aefd88e2/git-repository/src/repository/state.rs#L8
309 | local file=
310 | for file in {rebase-apply/applying,rebase-apply/rebasing,rebase-apply,rebase-merge/interactive,rebase-merge,CHERRY_PICK_HEAD,MERGE_HEAD,BISECT_LOG,REVERT_HEAD}; do
311 | if [ -f "$dir/.git/$file" ]; then
312 | return 1
313 | fi
314 | done; unset -v file
315 | fi
316 |
317 | return 0
318 | }
319 |
320 | # @description Prepares internal variables for time setting
321 | # @internal
322 | __bake_time_prepare() {
323 | if ((BASH_VERSINFO[0] >= 5)); then
324 | __bake_global_timestart=$EPOCHSECONDS
325 | fi
326 | }
327 |
328 | # @description Determines total approximate execution time of a task
329 | # @set string REPLY
330 | # @internal
331 | __bake_time_get_total_pretty() {
332 | unset -v REPLY; REPLY=
333 |
334 | if ((BASH_VERSINFO[0] >= 5)); then
335 | local timediff=$((EPOCHSECONDS - __bake_global_timestart))
336 | if ((timediff < 1)); then
337 | return
338 | fi
339 |
340 | local seconds=$((timediff % 60))
341 | local minutes=$((timediff / 60 % 60))
342 | local hours=$((timediff / 3600 % 60))
343 |
344 | REPLY="${seconds}s"
345 |
346 | if ((minutes > 0)); then
347 | REPLY="${minutes}m $REPLY"
348 | fi
349 |
350 | if ((hours > 0)); then
351 | REPLY="${hours}h $REPLY"
352 | fi
353 | fi
354 | }
355 |
356 | # @description Parses the configuration for functions embeded in comments. This properly
357 | # parses inherited config from the 'init' function
358 | # @set string __bake_config_docstring
359 | # @set array __bake_config_watchexec_args
360 | # @set object __bake_config_map
361 | # @internal
362 | __bake_parse_task_comments() {
363 | local task_name="$1"
364 |
365 | local tmp_docstring=
366 | local -a tmp_watch_args=()
367 | local -A tmp_cfg_map=()
368 | local line=
369 | while IFS= read -r line || [ -n "$line" ]; do
370 | if [[ $line =~ ^[[:space:]]*#[[:space:]](doc|watch|config):[[:space:]]*(.*?)$ ]]; then
371 | local comment_category="${BASH_REMATCH[1]}"
372 | local comment_content="${BASH_REMATCH[2]}"
373 |
374 | if [ "$comment_category" = 'doc' ]; then
375 | tmp_docstring=$comment_content
376 | elif [ "$comment_category" = 'watch' ]; then
377 | readarray -td' ' tmp_watch_args <<< "$comment_content"
378 | tmp_watch_args[-1]=${tmp_watch_args[-1]::-1}
379 | elif [ "$comment_category" = 'config' ]; then
380 | local -a pairs=()
381 | readarray -td' ' pairs <<< "$comment_content"
382 | pairs[-1]=${pairs[-1]::-1}
383 |
384 | # shellcheck disable=SC1007
385 | local pair= key= value=
386 | for pair in "${pairs[@]}"; do
387 | IFS='=' read -r key value <<< "$pair"
388 |
389 | tmp_cfg_map[$key]=${value:-on}
390 | done; unset -v pair
391 | fi
392 | fi
393 |
394 | # function()
395 | if [[ $line =~ ^([[:space:]]*function[[:space:]]*)?(.*?)[[:space:]]*\(\)[[:space:]]*\{ ]]; then
396 | local function_name="${BASH_REMATCH[2]}"
397 |
398 | if [ "$function_name" == task."$task_name" ]; then
399 | __bake_config_docstring=$tmp_docstring
400 |
401 | __bake_config_watchexec_args+=("${tmp_watch_args[@]}")
402 |
403 | local key=
404 | for key in "${!tmp_cfg_map[@]}"; do
405 | __bake_config_map[$key]=${tmp_cfg_map[$key]}
406 | done; unset -v key
407 |
408 | break
409 | elif [ "$function_name" == 'init' ]; then
410 | __bake_config_watchexec_args+=("${tmp_watch_args[@]}")
411 |
412 | local key=
413 | for key in "${!tmp_cfg_map[@]}"; do
414 | __bake_config_map[$key]=${tmp_cfg_map[$key]}
415 | done; unset -v key
416 | fi
417 |
418 | tmp_docstring=
419 | tmp_watch_args=()
420 | tmp_cfg_map=()
421 | fi
422 | done < "$BAKE_FILE"; unset -v line
423 | }
424 |
425 | # @description Nicely prints all 'Bakefile.sh' tasks to standard output
426 | # @internal
427 | __bake_print_tasks() {
428 | local str=$'Tasks:\n'
429 |
430 | local -a task_flags=()
431 | # shellcheck disable=SC1007
432 | local line= task_docstring=
433 | while IFS= read -r line || [ -n "$line" ]; do
434 | # doc
435 | if [[ $line =~ ^[[:space:]]*#[[:space:]]doc:[[:space:]](.*?) ]]; then
436 | task_docstring=${BASH_REMATCH[1]}
437 | fi
438 |
439 | # flag
440 | if [[ $line =~ bake\.has_flag[[:space:]][\'\"]?([[:alnum:]]+) ]]; then
441 | task_flags+=("[--${BASH_REMATCH[1]}]")
442 | fi
443 |
444 | if [[ $line =~ ^([[:space:]]*function[[:space:]]*)?task\.(.*?)\(\)[[:space:]]*\{[[:space:]]*(#[[:space:]]*(.*))? ]]; then
445 | local matched_function_name="${BASH_REMATCH[2]}"
446 | local matched_comment="${BASH_REMATCH[4]}"
447 |
448 | if ((${#task_flags[@]} > 0)); then
449 | str+=" ${task_flags[*]}"$'\n'
450 | fi
451 | task_flags=()
452 |
453 | str+=" -> $matched_function_name"
454 |
455 | if [[ -n "$matched_comment" || -n "$task_docstring" ]]; then
456 | if [ -n "$matched_comment" ]; then
457 | __bake_internal_warn "Adjacent documentation comments are deprecated. Instead, write a comment above 'task.$matched_function_name()' like so: '# doc: $matched_comment'"
458 | task_docstring=$matched_comment
459 | fi
460 |
461 | if __bake_is_color; then
462 | str+=$' \033[3m'"($task_docstring)"$'\033[0m'
463 | else
464 | str+=" ($task_docstring)"
465 | fi
466 | fi
467 |
468 | str+=$'\n'
469 | task_docstring=
470 | fi
471 | done < "$BAKE_FILE"; unset -v line
472 |
473 | if [ -z "$str" ]; then
474 | if __bake_is_color; then
475 | str=$' \033[3mNo tasks\033[0m\n'
476 | else
477 | str=$' No tasks\n'
478 | fi
479 | fi
480 |
481 | printf '%s' "$str"
482 | } >&2
483 |
484 | # @description Prints text that takes up the whole terminal width
485 | # @arg $1 string Text to print
486 | # @internal
487 | __bake_print_big() {
488 | if [ "${__bake_config_map[big-print]}" = 'off' ]; then
489 | return
490 | fi
491 |
492 | if [ "$1" = '--show-time' ]; then
493 | local flag_show_time='yes'
494 | local print_text="$2"
495 | else
496 | local flag_show_time='no'
497 | local print_text="$1"
498 | fi
499 |
500 | __bake_time_get_total_pretty
501 | local time_str="${REPLY:+ ($REPLY) }"
502 |
503 | # shellcheck disable=SC1007
504 | local _stty_height= _stty_width=
505 | read -r _stty_height _stty_width < <(
506 | if stty size &>/dev/null; then
507 | stty size
508 | else
509 | # Only columns is used by Bake, so '20 was chosen arbitrarily
510 | if [ -n "$COLUMNS" ]; then
511 | printf '%s\n' "20 $COLUMNS"
512 | else
513 | printf '%s\n' '20 80'
514 | fi
515 | fi
516 | )
517 |
518 | local separator_text=
519 | # shellcheck disable=SC2183
520 | printf -v separator_text '%*s' $((_stty_width - ${#print_text} - 1))
521 | printf -v separator_text '%s' "${separator_text// /=}"
522 | if [[ "$flag_show_time" == 'yes' && -n "$time_str" ]]; then
523 | separator_text="${separator_text::5}${time_str}${separator_text:5+${#time_str}:${#separator_text}}"
524 | fi
525 | if __bake_is_color; then
526 | printf '\033[1m%s %s\033[0m\n' "$print_text" "$separator_text"
527 | else
528 | printf '%s %s\n' "$print_text" "$separator_text"
529 | fi
530 | } >&2
531 |
532 | # @description Parses the arguments. This also includes setting the the 'BAKE_ROOT'
533 | # and 'BAKE_FILE' variables
534 | # @set REPLY Number of times to shift
535 | # @internal
536 | __bake_parse_args() {
537 | unset -v REPLY; REPLY=
538 | local -i total_shifts=0
539 |
540 | local arg=
541 | for arg; do case $arg in
542 | -f)
543 | BAKE_FILE=$2
544 | if [ -z "$BAKE_FILE" ]; then
545 | __bake_internal_die "A value was not specified for for flag '-f'"
546 | fi
547 | ((total_shifts += 2))
548 | if ! shift 2; then
549 | __bake_internal_die 'Failed to shift'
550 | fi
551 |
552 | if [ ! -e "$BAKE_FILE" ]; then
553 | __bake_internal_die "Specified file '$BAKE_FILE' does not exist"
554 | fi
555 | if [ ! -f "$BAKE_FILE" ]; then
556 | __bake_internal_die "Specified file '$BAKE_FILE' is not actually a file"
557 | fi
558 | ;;
559 | -w)
560 | ((total_shifts += 1))
561 | if ! shift; then
562 | __bake_internal_die 'Failed to shift'
563 | fi
564 |
565 | if [[ ! -v 'BAKE_INTERNAL_NO_WATCH_OVERRIDE' ]]; then
566 | BAKE_FLAG_WATCH='yes'
567 | fi
568 | ;;
569 | -v)
570 | printf '%s\n' "Version: $__global_bake_version"
571 | exit 0
572 | ;;
573 | -h)
574 | local flag_help='yes'
575 | if ! shift; then
576 | __bake_internal_die 'Failed to shift'
577 | fi
578 | ;;
579 | *)
580 | break
581 | ;;
582 | esac done; unset -v arg
583 |
584 | if [ -n "$BAKE_FILE" ]; then
585 | BAKE_ROOT=$(
586 | # shellcheck disable=SC1007
587 | CDPATH= cd -- "${BAKE_FILE%/*}"
588 | printf '%s\n' "$PWD"
589 | )
590 | BAKE_FILE="$BAKE_ROOT/${BAKE_FILE##*/}"
591 | else
592 | if ! BAKE_ROOT=$(
593 | while [ ! -f './Bakefile.sh' ] && [ "$PWD" != / ]; do
594 | if ! cd ..; then
595 | exit 1
596 | fi
597 | done
598 |
599 | if [ "$PWD" = / ]; then
600 | exit 1
601 | fi
602 |
603 | printf '%s' "$PWD"
604 | ); then
605 | __bake_internal_die "Failed to find 'Bakefile.sh'"
606 | fi
607 | BAKE_FILE="$BAKE_ROOT/Bakefile.sh"
608 | fi
609 |
610 | if [ "$flag_help" = 'yes' ]; then
611 | cat <<-"EOF"
612 | Usage: bake [-h|-v] [-w] [-f ] [var=value ...] [args ...]
613 | EOF
614 | __bake_print_tasks
615 | exit
616 | fi
617 |
618 | REPLY=$total_shifts
619 | }
620 |
621 | # @description Main function
622 | # @internal
623 | __bake_main() {
624 | # Environment and configuration boilerplate
625 | set -ETeo pipefail
626 | shopt -s dotglob extglob globasciiranges globstar lastpipe shift_verbose
627 | export LANG='C' LC_CTYPE='C' LC_NUMERIC='C' LC_TIME='C' LC_COLLATE='C' \
628 | LC_MONETARY='C' LC_MESSAGES='C' LC_PAPER='C' LC_NAME='C' LC_ADDRESS='C' \
629 | LC_TELEPHONE='C' LC_MEASUREMENT='C' LC_IDENTIFICATION='C' LC_ALL='C'
630 | trap '__bake_trap_err' 'ERR'
631 | trap ':' 'INT' # Ensure Ctrl-C ends up printing <- ERROR ==== etc.
632 |
633 | declare -ga __bake_args_original=("$@")
634 |
635 | # Parse arguments
636 | # Set `BAKE_{ROOT,FILE,FLAG_WATCH}`
637 | BAKE_ROOT=; BAKE_FILE=; BAKE_FLAG_WATCH=
638 | __bake_parse_args "$@"
639 | if ! shift $REPLY; then
640 | __bake_internal_die 'Failed to shift'
641 | fi
642 |
643 | # Set variables à la Make
644 | # shellcheck disable=SC1007
645 | local __bake_key= __bake_value= __bake_arg=
646 | for __bake_arg; do case $__bake_arg in
647 | *=*)
648 | IFS='=' read -r __bake_key __bake_value <<< "$__bake_arg"
649 |
650 | # If 'key=value' is passed, create global variable $value
651 | declare -g "$__bake_key"
652 | local -n __bake_variable="$__bake_key"
653 | __bake_variable="$__bake_value"
654 |
655 | # If 'key=value' is passed, create global variable $value_key
656 | declare -g "var_$__bake_key"
657 | local -n __bake_variable="var_$__bake_key"
658 | __bake_variable="$__bake_value"
659 |
660 | if ! shift; then
661 | __bake_internal_die 'Failed to shift'
662 | fi
663 | ;;
664 | *)
665 | break
666 | ;;
667 | esac done; unset -v __bake_arg
668 | unset -v __bake_key __bake_value
669 | unset -vn __bake_variable
670 |
671 | local __bake_task="$1"
672 | if [ -z "$__bake_task" ]; then
673 | __bake_internal_error 'No valid task supplied'
674 | __bake_print_tasks
675 | exit 1
676 | fi
677 | if ! shift; then
678 | __bake_internal_die 'Failed to shift'
679 | fi
680 |
681 | declare -ga __bake_args_userflags=("$@")
682 |
683 | declare -g __bake_config_docstring=
684 | declare -ga __bake_config_watchexec_args=()
685 | declare -gA __bake_config_map=(
686 | [stacktrace]='off'
687 | [big-print]='on'
688 | [pedantic-cd]='off'
689 | )
690 |
691 | if [ "$BAKE_FLAG_WATCH" = 'yes' ]; then
692 | if ! command -v watchexec &>/dev/null; then
693 | __bake_internal_die "Executable not found: 'watchexec'"
694 | fi
695 |
696 | __bake_parse_task_comments "$__bake_task"
697 |
698 | # shellcheck disable=SC1007
699 | BAKE_INTERNAL_NO_WATCH_OVERRIDE= exec watchexec "${__bake_config_watchexec_args[@]}" "$BAKE_ROOT/bake" -- "${__bake_args_original[@]}"
700 | else
701 | if ! cd -- "$BAKE_ROOT"; then
702 | __bake_internal_die "Failed to cd"
703 | fi
704 |
705 | # shellcheck disable=SC2097,SC1007,SC1090
706 | __bake_task= source "$BAKE_FILE"
707 |
708 | if declare -f task."$__bake_task" >/dev/null 2>&1; then
709 | __bake_parse_task_comments "$__bake_task"
710 |
711 | __bake_print_big "-> RUNNING TASK '$__bake_task'"
712 |
713 | if declare -f init >/dev/null 2>&1; then
714 | init "$__bake_task"
715 | fi
716 |
717 | __bake_time_prepare
718 |
719 | task."$__bake_task" "${__bake_args_userflags[@]}"
720 |
721 | __bake_print_big --show-time "<- DONE"
722 | else
723 | __bake_internal_error "Task '$__bake_task' not found"
724 | __bake_print_tasks
725 | exit 1
726 | fi
727 | fi
728 | }
729 |
730 | __bake_main "$@"
731 |
--------------------------------------------------------------------------------