{"id":16310668,"url":"https://github.com/nkh/bash-rd","last_synced_at":"2025-04-14T04:42:46.822Z","repository":{"id":193505350,"uuid":"688556033","full_name":"nkh/bash-rd","owner":"nkh","description":"Remote data display, KV store","archived":false,"fork":false,"pushed_at":"2024-11-14T17:20:19.000Z","size":245,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-02-16T09:41:58.732Z","etag":null,"topics":["debug","display","kv","kv-store","monitor","remote","watch"],"latest_commit_sha":null,"homepage":"","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nkh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-09-07T15:36:11.000Z","updated_at":"2024-11-14T17:20:23.000Z","dependencies_parsed_at":"2023-12-19T10:07:19.957Z","dependency_job_id":"fbec76db-dca7-4678-a636-ef7bd354511c","html_url":"https://github.com/nkh/bash-rd","commit_stats":{"total_commits":4,"total_committers":1,"mean_commits":4.0,"dds":0.0,"last_synced_commit":"cd39c2797c93babce19311e70a35efa085c44bed"},"previous_names":["nkh/bash-rd"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nkh%2Fbash-rd","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nkh%2Fbash-rd/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nkh%2Fbash-rd/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nkh%2Fbash-rd/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nkh","download_url":"https://codeload.github.com/nkh/bash-rd/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248824662,"owners_count":21167343,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["debug","display","kv","kv-store","monitor","remote","watch"],"created_at":"2024-10-10T21:42:43.179Z","updated_at":"2025-04-14T04:42:46.773Z","avatar_url":"https://github.com/nkh.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"% rd(1) | General Commands Manual\n# NAME\n\n\trd - display data remotely, also acts as a KV store\n\n![UI](https://github.com/nkh/bash-rd/blob/main/media/rd.gif)\n\nWhat's happening in the video above?\n\n```\nLayout:\n\n.------------.-----------.\n| Terminal 1 | Server    |\n|            |           |\n.------------.-----------.\n| Terminal 2 | Relay +   |\n|            | formatter |\n'------------'-----------'\n```\n\nThe server, which echoes all data.\n\nThe relay, which only displays what looks like a log entry.\n\nMultiple commands are run in Terminal 1 to send data to the serer.\n\n*cat* is used in terminal 2, giving use a \"cheap\" terminal, also check the utilities if you have rlwrap installed.\n\n- terminal 1: sends text to server, server echoes it, relay does nothing (twice)\n- terminal 2: sends text to server, server echoes it, relay does nothing\n- terminal 2: sends a log entry, server echoes it, relay echoes it\n- terminal 1: change server formatter dynamically to a table which can show the value of variables x and y\n- terminal 1: set value of x in KV store, table is updated \n- terminal 2: idem\n- terminal 2: set value of y\n- terminal 1: send 'quit' command to server, relay also shuts down\n\n# SYNOPSIS\n\n\trd CONNECTION_TYPE [FORMATTER]\n\n\trd --connect ID\n\n# DESCRIPTION\n\n*rd* displays data in another terminal, on another computer, in a web page.\n\n*rd* is the client, it sends data to a server.\n\n*rd* is also the server, it echoes the data send by a client but you can provide custom formatters.\n\n# SECURITY\n\nWith *rd* running on a port you may open a security hole, If you are not sure about what you're doing, don't use rd!\n\nOptions *-f, -i* do not open ports.\n\n## Remote input and output\n\nIf you need to send data to  server or get output from a server, understand the consequences first.\n\n### Data in\n\nBest is to not accept data from anyone but you or processes you control (including controlling the input).\n\n- have a watertight firewall\n- open the port to your server only and close it when done\n- proxy the connection and filter the traffic, netcat, ncat, socat can help you\n\n### Data out\n\nThere are two possibilities, serve a web page or relay the server's output. Both are documented below.\n\n# CONNECTION_TYPE\n\n## Server\n\nStart *rd* as a server, it will display where you can connect.\n\n### Server local\n\n| Type        | Option   | Argument |\n| ----------- | -------- | -------- |\n| random fifo | -f       |          |\n| user ID     | -i       | ID       |\n\n### Server networked\n\n| Type        | Option   | Argument              |\n| ----------- | -------- | --------------------- |\n| port        | -p       | port                  |\n| web server  | -w       | port, formatter[, id] |\n\n## Client\n\n| Type          | Option   | Argument     |\n| -----------   | -------- | --------     |\n| connect       | -c       | ID           |\n| net connect   | -n       | port         |\n| net connect   | -N       | address port |\n| relay         | -r       | ID           |\n| network relay | -R       | port         |\n\n# Examples\n\n### No formatter \n\nData is echoed by *rd*\n\n```\nterminal 1 $\u003e rd -f\nrd: ID: XXXXXX\n\nterminal 2 $\u003e program | rd -c XXXXXX\n\nterminal 3 $\u003e interactive_program \u003e(rd -c XXXXXX)\n\nterminal 4 $\u003e interactive_program 3\u003e(rd -c XXXXXX) # chose which file descriptor you use to send data to rd\n\n```\n\n### With a formatter\n\nYou decide how the data is used and displayed\n\n```\nterminal 1 $\u003e rd -f my_formatter # my_formatter will be called by the server\nrd: ID: XXXXXX\n\nterminal 2 $\u003e bash_script | rd -c XXXXXX\n```\n\n## Random FIFO\n\n```\nterminal 1 $\u003e rd -f\nrd: ID: XXXXXX # XXXXXX is where the server can be reached\n\nterminal 2 $\u003e cat \u003e \u003e(rd -c XXXXXX)\n```\n\n## User ID\n\nYou can give an alphanumeric ID (or generate it, ex: https://github.com/fnichol/names).\n\n```\nterminal 1 $\u003e rd -i my_id \nrd: ID: my_id\n\nterminal 2 $\u003e cat | rd -c my_id\n```\n\n## Networked\n\n### Local port\n\n```\nterminal 1 $\u003e rd -p my_port\nrd: port: my_port\n\nterminal 2 $\u003e cat | rd -n my_port\n\nterminal 3 $\u003e cat \u003e \u003e(rd -N host my_port)\n```\n\n### Web page\n\n```\nterminal 1 $\u003e rd -w my_port web_table XXX\nrd: ID: XXX\n\n# set variable x and generate a page via formatter 'web_table'\nterminal 2 $\u003e rd -c XXX '=:x=1'\n\n# get page\n$\u003e firefox http:/localhost:my_port\n$\u003e curl localhost:my_port 2\u003e\u0026- | cat \n```\n\n# CONTROL COMMANDS\n\n*rd* accepts these control commands.\n\n| Prefix | Action        | Description                                                     |\n| ------ | ------------- | --------------------------------------------------------------- |\n| q:     | quit          | stop the server                                                 |\n| v:     | verbatim      | allows you to send data that looks like a command               |\n| s:     | semaphore     | increment the server semaphore, needs to be 0 to quit           |\n| c:     | clear         | clear server's terminal                                         |\n| t:     | timestamp     | add timestamp to default renderer                               |\n| e:     | when          | when to run the formatter. 0: always, 1: never, nothing: now    |\n| k:     | clean up data | use it if rd complains about existing data                      |\n| f:     | set formatter | dynamically set the formatter                                   |\n| r:     | relay output  | format r:location:[pid]                                         |\n|        |               |                                                                 |\n| =:     | key=value;... | send a list of key=values to server                             |\n|        |               | the key must match [\\_a-zA-Z0-9] immediately followed by '='    |\n|        |               | the value can contain spaces, it's evaluated withing quotes     |\n|        |               | multiple KV can be send simultaneously if they are separated    |\n|        |               | by a ';'. No space is allowed between ';' and the next key      |\n\n## Key-Value store\n\n*rd* stores your data, on the server, the data is available to the formatters.\n\n```bash\necho '=:key=value;other_key=value' | rd -c ID\n```\n\n# USAGE\n\n## Multiple clients to one server\n\nSee video at top of README.md.\n\nBy using the semaphore command, each client can quit and the server will quit when the last client quits.\n\n## Connect to multiple servers\n\nYou can start multiple server, each getting specific data or using a specific formatter.\n\n```bash\nterminal 1 $\u003e rd -i server1\n\nterminal 2 $\u003e rd -i server2 my_formatter\n\nterminal 3 $\u003e my_script 3\u003e \u003e(rd -c server1) 4\u003e \u003e(rd -c server2)\n```\n\n*my_script*, or equivalent command in Perl, C, Go, ... \n\n```bash\nserver1_fd=3 # the file descriptor you chose to use to send data to on the above command line\nserver2_fd=4 # send data to different servers\n\n# will echo the data\necho \"data send to server_1\" \u003e\u0026$server1_fd\n\n# will run the formatter\necho -e \"data send to server_2 with \\0 binary\" \u003e\u0026$server2_fd\n\n```\n\n## Send identical data to multiple servers\n\n```\ncommand | tee \u003e(rd -c ID) \u003e \u003e(rd -n port)\n```\n\n## Send data inside a pipe\n\n```\ncommand | tee \u003e(rd -c ID) | other_command\n```\n\n## Usage in Bash\n\n```bash\nls --color | rd -c id\n\n\u003c\u003c\u003c'c:' rd -n port\n\ntree -C -d \u003e \u003e(rd -c id)\n\nrd -n 1234 \"some data\"\n```\n\n## Usage in Perl\n\n```perl\nopen my $fh, \"\u003e\u0026=\", 3 ; \n$fh-\u003eautoflush(1) ;\n\nprint $fh \"c:\\n\" ; # clear\nprint $fh DumpTree $data, ...\n\n```\n\n# FORMATTERS \n\nA formatter is an external program that formats the data received by the server.\n\nWithout a formatter the data is echoed.\n\nFormatters also get the stored key/value in their environment.\n\n*rd* variables of interest:\n- $rd_counter, number of received lines\n- $rd_line, the last received data\n- $rd_fs, this sessions cache\n\nThings you can do:\n\n- render specific variables in a table \n- parse variable for content and location to display `echo \"key:/x,y/value\"`\n- show data structures\n...\n\n## Templating\n\nA nice way to present data is with the use of templates.\n\nI like *bash-tpl*, a templating engine that generates bash scripts.\n\n[Bash-TPL](https://github.com/TekWizely/bash-tpl)\n\n## Generating a web page\n\nYou can use any HTML generator you like in your formatter, the HTML must be put in *$rd_web_page*.\n\n### example HTML generator in Bash\n\n```bash\nname1=\"$(printf \"%8s\" \"$x\")\"\nname2=\"$(printf \"%8s\" \"$y\")\"\n\ncat \u003e $rd_web_page \u003c\u003c EOP\nHTTP/1.1 200 OK\nContent-Type: text/html; charset=utf-8\n\n\u003chtml\u003e\n\t\u003cbody\u003e\n\t\t\u003cpre\u003e\n+---+---------+\n| k | v       |\n+---+---------+\n| x |$name1 |\n| y |$name2 |\n+-------------+\n\t\t\u003c/pre\u003e\n\t\u003c/body\u003e\n\u003c/html\u003e\nEOP\n```\n\n# DATA STRUCTURES\n\nSay you have a JSON data structure with embedded new line, you want to see it formatted by *jq*.\n\nFirst you'll need a specialized formatter that takes a variable and pass it to *jq*. \n\nTo send a variable with embedded new lines we need to encode it, the simplest is in base64.\n\nOn the client side:\n\n```bash\n# set the key 'json' to the encoded json\nprintf 'json=%s\\n' \"$(\u003c\u003c\u003c\"$json\" base64 -w 0)\"\n```\n\nOn the formatter side:\n\n```bash\n\u003c\u003c\u003c\"$json\" base64 --decode | jq\n```\n\nSee the *examples* directory for the full example.\n\n# LOG\n\nKeep a log of all traffic in a file, note the final ':' after the file name.\n\n```bash\necho 'r:log_file:' | rd -c id\n```\n\n# RELAYS\n\nGet a server's output to another terminal, the relay will close when the server closes.\n\n```bash\nrd -r id\n```\n\n## Relay with a custom formatter\n\n```\nrd -r id log4j\n```\n\n## Relay rd running on a port\n\n```bash\nrd -R port\n```\n\n## Relay rd running on another host\n\nIf you can ssh to the other host:\n\n```bash\nssh me@host rd -r id \n```\n\n### Securely relaying to a client on the internet\n\nOn the server:\n\n```bash\n# generate private key\nopenssl genrsa -out server.key 2048 \n\n## generate server certificate\nopenssl req -new -key server.key -x509 -days 1 -out server.crt\n\n## generate server pem\ncat server.key server.crt \u003e server.pem \nrm server.key\n```\n\nOn the client:\n\n```bash\nopenssl genrsa -out client.key 2048 \nopenssl req -new -key client.key -x509 -days 1 -out client.crt\ncat client.key client.crt \u003e client.pem \nrm client.key\n```\n\n**** Exchange ONLY .cert files ****, via mail etc ..., safe as they are encrypted\n\nOn server:\n\n```bash\n# start rd server\nrd -i 1234\n```\n\nTo forward the data send by server on port 1234 via en encrypted and authenticated connection\n\nNote that we:\n- setup an unidirectional tunnel with -u\n- fix the version of ssl to 1.3\n- accept a single connection and then close\n\n```bash\n# start a secure relay\nrd -r 1234 | socat -u - ssl-l:1234,reuseaddr,cert=server.pem,cafile=client.crt,verify=1,openssl-min-proto-version=TLS1.3,openssl-max-proto-version=TLS1.3\n```\n\nOn client connect using ssl and output to terminal \n\n```bash\n# on the other end of the tunnel\nsocat -u ssl:localhost:1234,cert=client.pem,cafile=server.crt,openssl-min-proto-version=TLS1.3,openssl-max-proto-version=TLS1.3 -\n```\n\nClosing the server closes:\n- the relay, and its sub processes\n- the server side tls proxy\n- the client side proxy\n- the client side end point (SDTOUT in the code above)\n\n***Note:*** relaying has a noticeable impact on the server performance as it needs\nsend every line to all relays in a separate process. Let me know if performance is\na problem.\n\n### LAST WORD OF CAUTION\n\nIf you are not used to open application to internet traffic, or even your intranet, talk to someone knowledgeable or your IT department first.\n\nOffering data as read only via a web service is simple and safe, still anyone with the address can see the data.\n\nThe same amount of caution must be used when passing data to a formatter, filter the input, use chroot, running in pods, ...\n\n# UTILITIES (don't miss these)\n\n## rdc\n\nA bash script that's equivalent to \"rd -c\", it uses the excellent *rlwrap* readline wrapper which make it easier to use rd as a shell. \n\n## trd\n\n* opens *rd* as a server in a *tmux* pane.\n\nYou can pass the same arguments to *trd* as you can pass to *rd*, without argument *trd* will generated an ID and copy it to the clipboard.\n\n\n# CUSTOM FORMATTERS\n\nSay you want to log 'log4j' style with colors (log4j is just hyped syslog).\n\nYou'll need:\n- a set of log4j functions\n- a log4j formatter\n\n## Levels and colors\n\n```bash\ndeclare -A log4j_levels=( ['DEBUG']=0 ['INFO']=1 ['NOTICE']=2 ['WARN']=3 ['ERROR']=4 ['CRIT']=5 ['ALERT']=6 ['EMERG']=7)\nlog4j_colors=(            '35'        '2;34'     '2;32'       '33'       '31'        '4;31'     '5;31'      '101;93')\n\n```\n\n## log4j functions for bash scripts\n\n```bash\n# destination, a file descriptor, where messages are send, stderr by default\nlog4j_sink=2\n\n# a function to set the destination\nlog4j_sink()  { log4j_sink=\"$1\" ; }\n\n# minimum message level to display, pass a string 'DEBUG', 'INFO', ... as argument\nlog4j_level() { log4j_level=${log4j_levels[$1]} ; echo \"log4j_level=$log4j_level\" \u003e$log4j_sink ; }\n\n# send message to destination\nlog4j_send()    { echo \"$*\" \u003e\u0026$log4j_sink ; }\n\n# finally create log functions corresponding to the levels\nfor l in \"${!log4j_levels[@]}\" ; do eval \"log_${l,,}() { log4j_send '$l:' \\\"\\$@\\\" ; }\" ; done\n```\n\n## Formatter\n\n```bash\n# is it a log entry\n[[ \"$rd_line\" =~ ^(.*):(.*) ]] \u0026\u0026 level_name=\"${BASH_REMATCH[1]}\" \u0026\u0026 [[ \"$log4j_levels[$level_name]\" ]] \u0026\u0026\n\t{\n\t# what's its level ?\n\tlevel=${log4j_levels[$level_name]}\n\t\n\t# display message if level high enough\n\t(( level \u003e= log4j_level )) \u0026\u0026 { echo -ne \"\\e[${log4j_colors[$level]}m$level_name:\\e[m\" ; echo ${BASH_REMATCH[2]} ; }\n\t}\n```\n\nThe complete code is in *examples/log4j*.\n\nTo get access to the function you source the file, the same file can be used as a formatter.\n\n```bash\nrd -f examples/log4j\n```\n\n![LOG4J](https://github.com/nkh/bash-rd/blob/main/media/log4j.png)\n\nAlert is there but the screenshot can't capture blinking text.\n\n# DATABASE\n\nUsing a database instead for the KV store has some advantages\n- persistence, if you need it \n- complex records\n- queries\n\n## Instrument a program, here a bash script, to debug it\n\nWe'll be using the excellent *sqlite3*. We first create a *formatter*.\n\n```bash\n[[ \"$sql_db\" ]] \u0026\u0026 [[ \"$rd_line\" =~ ^sql:(.*) ]] \u0026\u0026 \n\t{\n\tquery=\"${BASH_REMATCH[1]}\" ;\n\tsqlite3 \"$sql_db\" \u003c\u003c\u003c\"$query\"\n\t}\n...\n```\n\nIn our program:\n\n```bash\n# execute sql commands directly\nsqlite3 my_db 'create table my_table(key text, value int);'\n\n# setup variable in KV store (they are passed to the formatter)\necho \"sql_db=my_db\"\necho \"sql_mode=.mode box\"\n\n# or via the formatter which acts as a sqlite3 proxy\n\necho \"sql:insert into my_table values('date','$(date)');\"\necho 'sql:insert into my_table values(\"x\",0);'\n\n\necho 'sql:select * from my_table;'\n\n```\n\nThe complete example is in *examples/sqlite3*.\n\n## A DB convenience wrapper\n\nFor a quick debugging session, with few changes, you can use the code found in *examples/db_variables* that let's you write this:\n\n```bash\nrddb_setup my_db 0\n\n# run some code\na=1\nrddb_set 'a' $a\n\n# run some code\nb=\"some b value\"\nrddb_set 'b' \"$b\"\n\n# code that overrides a\n$a=42\nrddb_set 'a' \"$a\" \n\n# display a table with the variables\nrddb_show \"title\"\n```\n\nIt should be noted that each call to rddb_set start multiple processes:\n- the sqlite formatter\n- sqlite3 which\n\t- opens the db\n\t- opens your rc file\n\t- executes the SQL\n\nIn other words don't put this a loop.\n\nIf you really must run in a loop:\n- use only one db process in your application and use the sqlite3 formatter only when you need it\n\n# TABLE FORMATTED KEY/VALUE[/comment]\n\nUse a table to present variables.\n\nThe *ptt* formatter uses *Text::Table::Tiny* which can output tables in multiple formats.\n\nYou set Key-Values in *rd* and when you need a table you just send a command to *ptt*.\n\n```bash\n# start a server\nrd -i 1234 ptt # use ptt as a formatter\n```\n\n```bash\n# start client\nrd -c 1234 \"=:rd_ptt_header=variable content comment\" # optional\n\n# set 'a' then send a command to ptt with list of variables to display: ptt:variable[ comment];variable; variable[ comment] ...\nrd -c 1234 \"=:a=1\"\n\u003c\u003c\u003c\"ptt:a;b\" rd -c 1234 \n\n# set 'TEST' (color coded)\nrd -c 1234 \"=:TEST=${RED}test${RESET}\"\n\n# you can also send a comment to ptt\n\u003c\u003c\u003c\"ptt_comment:display a set of variables from now on\" rd -c 1234 \n\n# give ptt a list of variable to display ptt_set:variable[ comment];variable; variable[ comment] ...\n# every time you send something to rd, the table will be displayed\n\u003c\u003c\u003c\"ptt_set:TEST;HOME my home\" rd -c 1234 \n\n# you can use ptt_reset to clear the variable set\n\u003c\u003c\u003c\"ptt_set:TEST;HOME my home\" rd -c 1234 \n\n```\n\n![PTT](https://github.com/nkh/bash-rd/blob/main/media/rd_ptt.png)\n\n# REAL LIFE EXAMPLES\n\n## tdiff\n\n*tdiff* is an interactive program I wrote; I had a problem while adding a function, exactly the type of problems rd can help with.\n\n![TDIFF](https://github.com/nkh/bash-rd/blob/main/media/rd_tdiff.png)\n\nFirst understand what was going wrong.\n\nYes it's easy to take a piece of bash code and run it in the shell (Bash you rock); but you need the data ... you already have.\nIf the data was large I'd redirect it in file and use it but this case was simple enough.\nThe possibility to use the interactive program to pick and chose which data is also a plus.\n\nWhat does the code do?\n\n```bash\n\n# glyphs is a cache of, well, glyphs\n# we create a cache because this function is called often and starting a subshell\n# to run sed would be time expensive in a loop\n\n# the first character, a glyph, is extracted from a string which can contain colors\n\n[[ \"${glyphs[$2]}\" ]] || glyph[$2]=\"$(\u003c\u003c\u003c\"${lines[$2]}\" sed -e 's/\\x1B\\[[0-9;]\\{1,\\}[A-Za-z]//g' -e 's/^\\(.\\).*/\\1/' )\"\n```\n\nSome tooling later:\n\n```bash\n \necho cache: \"${glyphs[$2]}\" \u003e\u00263\necho line: ${lines[$2]} \u003e\u00263\necho line: $(\u003c\u003c\u003c\"${lines[$2]}\" show_control_characters) \u003e\u00263\n\n[[ \"${glyphs[$2]}\" ]] || glyph[$2]=\"$(\u003c\u003c\u003c\"${lines[$2]}\" sed -e 's/\\x1B\\[[0-9;]\\{1,\\}[A-Za-z]//g' -e 's/^\\(.\\).*/\\1/' )\"\n\nglyph=\"${glyphs[$2]}\"\n\n# show the glyph\necho glyph: $glyph \u003e\u00263\n```\n\nTwo noteworthy remarks:\n- echo is redirected to fd 3, which is itself redirected to *rd* on the command line\n\t- leave sdtderr for errors\n- adding extra commands in the pipeline to rd let me display ANSI codes in the data\n\n# DEPENDENCIES\n\nnetcat\n\nsocat\n\n# INSTALL\n\nSee *INSTALL.md*.\n\n# SEE ALSO\n\n[Bash-TPL](https://github.com/TekWizely/bash-tpl)\n\n[bashlog](https://github.com/Zordrak/bashlog)\n\nThere's a niece bash db project here https://blog.dhampir.no/content/bashdb-a-single-dynamic-database-table-for-bash-scripts; the only drawback is its speed.\n\n# AUTHOR\n\n\tKhemir Nadim ibn Hamouda\n\thttps://github.com/nkh\n\tCPAN ID: NKH\n    \n# LICENCE\n\n\t© Nadim Khemir 2024, Artistic licence 2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnkh%2Fbash-rd","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnkh%2Fbash-rd","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnkh%2Fbash-rd/lists"}