├── .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: ![Step 2 Click on README.md](https://cloud.githubusercontent.com/assets/170270/9402920/53a7e3ea-480c-11e5-9d81-aecf64be55eb.png) 36 | 3. Now click on the edit icon. ![Step 3 - Click on Edit](https://cloud.githubusercontent.com/assets/170270/9402927/6506af22-480c-11e5-8c18-7ea823530099.png) 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/). ![Step 4 - Edit the file](https://cloud.githubusercontent.com/assets/170270/9402932/7301c3a0-480c-11e5-81f5-7e343b71674f.png) 38 | 5. Say why you're proposing the changes, and then click on "Propose file change". ![Step 5 - Propose Changes](https://cloud.githubusercontent.com/assets/170270/9402937/7dd0652a-480c-11e5-9138-bd14244593d5.png) 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 [![Awesome](https://awesome.re/badge.svg)](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 | --------------------------------------------------------------------------------