Compare commits

..

9 Commits

8 changed files with 339 additions and 19 deletions

152
README.md
View File

@ -1,11 +1,139 @@
# Menu Helper # Menu-Helper
A program to manage a database of recipes and help you to pick out meals based A program to manage a database of recipes and help you to pick out meals based
on filters of ingredients and tags. on filters of ingredients and tags.
## Usage ## Usage
Ensure the `XDG_DATA_HOME` variable is set (e.g. to `$HOME/.local/share`). Ensure the `XDG_DATA_HOME` variable is set (e.g. to `$HOME/.local/share`) and
that you have installed the SQLite3 library.
Upon first execution of any command, the program will automatically create the
database.
### Adding New Recipes
The first thing you're probably going to want to do is to add a new recipe to
your database. If this database hasn't been created already then the program
will do it automatically. This is done via the `add` subcommand, which will
query you about the different attributes you want for your recipe, looking
something like the following:
```console
$ menu-helper add
Name: Linguine Scampi
Description: A lemony Italian pasta dish.
Ingredients (comma separated): linguine,shrimp,garlic,parsley,lemon
Tags (comma separated): italian,lunch
Creating database in /home/nicolas/.local/share/menu-helper/recipes.db
```
This will have created your recipe within the database. That last line there is
merely informative, telling you that the database did not exist and it is not
being created; if you had a database already and it isn't being found, ensure
that your `XDG_DATA_HOME` environment variable is properly set.
### Querying Recipes
#### Filtering
Once a recipe or two have been added to your database you may now query them
filtering based on ingredients and tags. This is done via the `list` command,
which takes two kinds of arguments, both optional:
- `-i <list>`: Comma-separated list of the ingredients to look for.
- `-t <list>`: Comma-separated list of the tags to look for.
If neither is specified then all recipes will be listed with their respective
ID, name, and description:
```console
$ menu-helper list
1 | Linguine Scampi | A lemony Italian pasta dish.
2 | Garlic Soup | A simple monastic soup for cold winters.
```
However, when one of these arguments is used it filters recipes to only show
those which include __all__ the ingredients and tags specified:
```console
$ menu-helper list -i linguine
1 | Linguine Scampi | A lemony Italian pasta dish.
```
#### Recipe Information
The IDs shown in the queries above now become useful for the rest of
Menu-Helper functionality. If you wish to query all stored information about a
given recipe, this is where you can use the `info` subcommand with the relevant
ID:
```console
$ menu-helper info 2
Name: Garlic Soup
Description: A simple monastic soup for cold winters.
ID: 2
Ingredients:
- garlic
- bread
- egg
Tags:
- soup
- dinner
- simple
```
### Removing Recipes
If you end up desiring to remove a recipe for whatever reason, you can do so by
using the `del` subcommand with the recipe's corresponding ID:
```console
$ menu-helper del 2
$ menu-helper list
1 | Linguine Scampi | A lemony Italian pasta dish.
```
### Modifying Recipe Ingredients/Tags
If there are ingredients/tags which you forgot to add to a recipe, or that you
added erringly, you can correct this with the following commands:
- `add-ingr <id> <list>`: Add list of comma-separated ingredients `list` to
recipe with ID `id`.
- `rm-ingr <id> <list>`: Remove list of comma-separated ingredients `list` from
recipe with ID `id`.
- `add-tag <id> <list>`: Add list of comma-separated tags `list` to recipe with
ID `id`.
- `rm-tag <id> <list>`: Remove list of comma-separated tags `list` from recipe
with ID `id`.
For example, we forgot to add the useful tag to our first recipe (Linguine
Scampi) that it is a pasta dish. We can do this with the following command:
```console
$ menu-helper add-tag 1 pasta
$ menu-helper info 1
Name: Linguine Scampi
Description: A lemony Italian pasta dish.
ID: 1
Ingredients:
- linguine
- shrimp
- garlic
- parsley
- lemon
Tags:
- italian
- lunch
- pasta
```
## Building ## Building
@ -15,7 +143,25 @@ To build the program you will require the following dependencies:
- SQLite3 C/C++ library - SQLite3 C/C++ library
- Make - Make
Once installed, compile the project with the `make` command. Once installed, compile the project with the `make` command. To install simply
run the `make install` command, optionally appending `PREFIX=...` to change the
default directory of installation (i.e. `/usr/local/...`).
## Contributing
If you find any issues, feel free to report them on GitHub or send me an E-Mail
(see my website/profile for the address). I will add these issues to my personal
Gitea page and (unless specified otherwise) mention you as the person who found
the issue.
For patches/pull requests, if you open a PR on GitHub I will likely not merge
directly but instead apply the patches locally (via Git patches, conserving
authorship), push them to my Gitea repository, which will finally be mirrored to
GitHub. However, you can save me a bit of work by just sending me the Git
patches directly (via E-Mail).
If you're looking for a way to contribute, consider having a look at my [To-Do
list](/TODO.md) for the project.
## License ## License

10
TODO.md Normal file
View File

@ -0,0 +1,10 @@
# To-Do List
- [ ] Add more safeguards to avoid bad usage.
- [ ] Create a man page.
- [ ] Add more documentation to `help` subcommand.
- [ ] Add import/export functionality.
- [ ] Properly align output columns from `list` subcommand.
- [ ] Add feature for editing recipe name and description.
- [ ] Name
- [ ] Description

View File

@ -25,18 +25,26 @@
enum cmd_id { enum cmd_id {
CMD_UNKNOWN = 0, CMD_UNKNOWN = 0,
CMD_ADD, CMD_ADD,
CMD_LIST,
CMD_DEL, CMD_DEL,
CMD_LIST,
CMD_INFO, CMD_INFO,
CMD_ADD_INGR,
CMD_RM_INGR,
CMD_ADD_TAG,
CMD_RM_TAG,
CMD_HELP, CMD_HELP,
CMD_VERSION, CMD_VERSION,
}; };
static const std::map<enum cmd_id, std::vector<std::string>> commands = { static const std::map<enum cmd_id, std::vector<std::string>> commands = {
{ CMD_ADD, {"add", "new"} }, { CMD_ADD, {"add", "new"} },
{ CMD_LIST, {"list", "ls"} },
{ CMD_DEL, {"del", "rm"} }, { CMD_DEL, {"del", "rm"} },
{ CMD_LIST, {"list", "ls"} },
{ CMD_INFO, {"info", "i"} }, { CMD_INFO, {"info", "i"} },
{ CMD_ADD_INGR, {"add-ingr"} },
{ CMD_RM_INGR, {"rm-ingr"} },
{ CMD_ADD_TAG, {"add-tag"} },
{ CMD_RM_TAG, {"rm-tag"} },
{ CMD_HELP, {"help", "-h", "--help"} }, { CMD_HELP, {"help", "-h", "--help"} },
{ CMD_VERSION, {"version", "-v", "--version"} }, { CMD_VERSION, {"version", "-v", "--version"} },
}; };
@ -55,9 +63,13 @@ static inline void print_help(void) {
std::cout << "COMMANDS:\n" std::cout << "COMMANDS:\n"
"\tadd, new Add a new recipe to the database.\n" "\tadd, new Add a new recipe to the database.\n"
"\tlist, ls List recipes with filters.\n"
"\tdel, rm Delete recipe by ID.\n" "\tdel, rm Delete recipe by ID.\n"
"\tlist, ls List recipes with filters.\n"
"\tinfo Show recipe information.\n" "\tinfo Show recipe information.\n"
"\tadd-ingr Add ingredient to a recipe.\n"
"\trm-ingr Remove ingredient from a recipe.\n"
"\tadd-tag Add tag to a recipe.\n"
"\trm-tag Remove tag from a recipe.\n"
"\thelp, -h, --help Show this help information.\n" "\thelp, -h, --help Show this help information.\n"
"\tversion, -v, --version Show version information.\n" "\tversion, -v, --version Show version information.\n"
<< std::endl; << std::endl;

View File

@ -167,3 +167,117 @@ int cmd_info(const int id) {
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }
int cmd_add_ingr(const int recipe_id, const char *ingredients) {
db db;
std::vector<std::string> ingr_list = split(ingredients, ",");
db.open();
if(not db.recipe_exists(recipe_id)) {
std::cerr << "Recipe with ID " << recipe_id << " does not exist." << std::endl;
db.close();
return EXIT_FAILURE;
}
for(auto &i : ingr_list) {
int ingr_id;
trim(i);
if(not db.ingredient_exists(i))
ingr_id = db.add_ingredient(i);
else
ingr_id = db.get_ingredient_id(i);
db.conn_recipe_ingredient(recipe_id, ingr_id);
}
db.close();
return EXIT_SUCCESS;
}
int cmd_rm_ingr(const int recipe_id, const char *ingredients) {
db db;
std::vector<std::string> ingr_list = split(ingredients, ",");
db.open();
if(not db.recipe_exists(recipe_id)) {
std::cerr << "Recipe with ID " << recipe_id << " does not exist." << std::endl;
db.close();
return EXIT_FAILURE;
}
for(auto &i : ingr_list) {
int ingr_id;
trim(i);
if(not db.ingredient_exists(i)) {
std::cerr << "Could not find ingredient '" << i << "'. Skipping!" << std::endl;
continue;
}
ingr_id = db.get_ingredient_id(i);
db.disconn_recipe_ingredient(recipe_id, ingr_id);
}
db.close();
return EXIT_SUCCESS;
}
int cmd_add_tag(const int recipe_id, const char *tags) {
db db;
std::vector<std::string> tag_list = split(tags, ",");
db.open();
if(not db.recipe_exists(recipe_id)) {
std::cerr << "Recipe with ID " << recipe_id << " does not exist." << std::endl;
db.close();
return EXIT_FAILURE;
}
for(auto &i : tag_list) {
int tag_id;
trim(i);
if(not db.tag_exists(i))
tag_id = db.add_tag(i);
else
tag_id = db.get_ingredient_id(i);
db.conn_recipe_tag(recipe_id, tag_id);
}
db.close();
return EXIT_SUCCESS;
}
int cmd_rm_tag(const int recipe_id, const char *tags) {
db db;
std::vector<std::string> tag_list = split(tags, ",");
db.open();
if(not db.recipe_exists(recipe_id)) {
std::cerr << "Recipe with ID " << recipe_id << " does not exist." << std::endl;
db.close();
return EXIT_FAILURE;
}
for(auto &i : tag_list) {
int tag_id;
trim(i);
if(not db.tag_exists(i)) {
std::cerr << "Could not find tag '" << i << "'. Skipping!" << std::endl;
continue;
}
tag_id = db.get_tag_id(i);
db.disconn_recipe_tag(recipe_id, tag_id);
}
db.close();
return EXIT_SUCCESS;
}

View File

@ -21,3 +21,7 @@ int cmd_add(void);
int cmd_list(int argc, char *argv[]); int cmd_list(int argc, char *argv[]);
int cmd_delete(int argc, char *argv[]); int cmd_delete(int argc, char *argv[]);
int cmd_info(const int id); int cmd_info(const int id);
int cmd_add_ingr(const int recipe_id, const char *ingredients);
int cmd_rm_ingr(const int recipe_id, const char *ingredients);
int cmd_add_tag(const int recipe_id, const char *tags);
int cmd_rm_tag(const int recipe_id, const char *tags);

View File

@ -42,7 +42,7 @@ void db::open(void) {
db_path += "/recipes.db"; db_path += "/recipes.db";
if(not std::filesystem::exists(db_path)) { if(not std::filesystem::exists(db_path)) {
std::cout << "Creating database: " << db_path << std::endl; std::cout << "Creating database in " << db_path << std::endl;
new_db = true; new_db = true;
} }
@ -101,7 +101,7 @@ int db::add_recipe(const std::string &name, const std::string &description) {
if(not sqlite_db) if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__)); throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("INSERT INTO recipes(name,description) VALUES('{}','{}');", name, description).c_str(), if(sqlite3_exec(sqlite_db, std::format("INSERT OR IGNORE INTO recipes(name,description) VALUES('{}','{}');", name, description).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) { nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error("Failed to insert new recipe into database."); throw std::runtime_error("Failed to insert new recipe into database.");
} }
@ -251,7 +251,7 @@ int db::add_ingredient(const std::string &name) {
if(not sqlite_db) if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__)); throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("INSERT INTO ingredients(name) VALUES(lower('{}'));", name).c_str(), if(sqlite3_exec(sqlite_db, std::format("INSERT OR IGNORE INTO ingredients(name) VALUES(lower('{}'));", name).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) { nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to instert ingredient '{}'.", name)); throw std::runtime_error(std::format("Failed to instert ingredient '{}'.", name));
} }
@ -280,7 +280,7 @@ int db::add_tag(const std::string &name) {
if(not sqlite_db) if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__)); throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("INSERT INTO tags(name) VALUES('{}');", name).c_str(), if(sqlite3_exec(sqlite_db, std::format("INSERT OR IGNORE INTO tags(name) VALUES('{}');", name).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) { nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to insert tag '{}'", name)); throw std::runtime_error(std::format("Failed to insert tag '{}'", name));
} }
@ -305,24 +305,44 @@ std::vector<std::string> db::get_recipe_tags(const int id) {
return tags; return tags;
} }
void db::conn_recipe_ingredient(int recipe_id, int ingredient_id) { void db::conn_recipe_ingredient(const int recipe_id, const int ingredient_id) {
if(not sqlite_db) if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__)); throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("INSERT INTO recipe_ingredient(recipe_id, ingredient_id) VALUES({},{});", recipe_id, ingredient_id).c_str(), if(sqlite3_exec(sqlite_db, std::format("INSERT OR IGNORE INTO recipe_ingredient(recipe_id, ingredient_id) VALUES({},{});", recipe_id, ingredient_id).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) { nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to connect recipe with ID {} to ingredient with ID {}", throw std::runtime_error(std::format("Failed to connect recipe with ID {} to ingredient with ID {}",
recipe_id, ingredient_id)); recipe_id, ingredient_id));
} }
} }
void db::conn_recipe_tag(int recipe_id, int tag_id) { void db::disconn_recipe_ingredient(const int recipe_id, const int ingredient_id) {
if(not sqlite_db) if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__)); throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("INSERT INTO recipe_tag(recipe_id, tag_id) VALUES({},{});", recipe_id, tag_id).c_str(), if(sqlite3_exec(sqlite_db, std::format("DELETE FROM recipe_ingredient WHERE recipe_id={} AND ingredient_id={};", recipe_id, ingredient_id).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to disconnect recipe with ID {} from ingredient with ID {}.", recipe_id, ingredient_id));
}
}
void db::conn_recipe_tag(const int recipe_id, const int tag_id) {
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("INSERT OR IGNORE INTO recipe_tag(recipe_id, tag_id) VALUES({},{});", recipe_id, tag_id).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) { nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to connect recipe with ID {} to tag with ID {}", throw std::runtime_error(std::format("Failed to connect recipe with ID {} to tag with ID {}",
recipe_id, tag_id)); recipe_id, tag_id));
} }
} }
void db::disconn_recipe_tag(const int recipe_id, const int tag_id) {
if(not sqlite_db)
throw std::runtime_error(std::format("{}: Database not open! Please contact a developer.", __PRETTY_FUNCTION__));
if(sqlite3_exec(sqlite_db, std::format("DELETE FROM recipe_tag WHERE recipe_id={} AND tag_id={};", recipe_id, tag_id).c_str(),
nullptr, nullptr, nullptr) not_eq SQLITE_OK) {
throw std::runtime_error(std::format("Failed to disconnect recipe with ID {} from tag with ID {}.", recipe_id, tag_id));
}
}

View File

@ -94,6 +94,8 @@ public:
return (get_tag_id(name) > 0); return (get_tag_id(name) > 0);
} }
void conn_recipe_ingredient(int recipe_id, int ingredient_id); void conn_recipe_ingredient(const int recipe_id, const int ingredient_id);
void conn_recipe_tag(int recipe_id, int tag_id); void disconn_recipe_ingredient(const int recipe_id, const int ingredient_id);
void conn_recipe_tag(const int recipe_id, const int tag_id);
void disconn_recipe_tag(const int recipe_id, const int tag_id);
}; };

View File

@ -41,15 +41,27 @@ int main(int argc, char *argv[]) {
case CMD_ADD: case CMD_ADD:
ret = cmd_add(); ret = cmd_add();
break; break;
case CMD_LIST:
ret = cmd_list(argc - 1, argv + 1);
break;
case CMD_DEL: case CMD_DEL:
ret = cmd_delete(argc - 2, argv + 2); ret = cmd_delete(argc - 2, argv + 2);
break; break;
case CMD_LIST:
ret = cmd_list(argc - 1, argv + 1);
break;
case CMD_INFO: case CMD_INFO:
ret = cmd_info(std::stoi(argv[2])); ret = cmd_info(std::stoi(argv[2]));
break; break;
case CMD_ADD_INGR:
ret = cmd_add_ingr(std::stoi(argv[2]), argv[3]);
break;
case CMD_RM_INGR:
ret = cmd_rm_ingr(std::stoi(argv[2]), argv[3]);
break;
case CMD_ADD_TAG:
ret = cmd_add_tag(std::stoi(argv[2]), argv[3]);
break;
case CMD_RM_TAG:
ret = cmd_rm_tag(std::stoi(argv[2]), argv[3]);
break;
case CMD_HELP: case CMD_HELP:
print_help(); print_help();
break; break;