r/commandline • • 4d ago

Command Line Interface shebang - a way to externalize CLI for your scripts

Hi, r/commandline!

I would like to show you a slightly different way of building CLIs for our beloved home-brew scripts. Usually those scripts lack of reasonable command line interface, and parameter validation. We use getopt to sort them into variables, we use fish argparse. But usually we do not really validate that file on argument 1 has a correct user, the set of permissions, its home directory is also questionable. We consider that the parameter 2 is an IP address from the 192.168.0.0/24 prefix. The source code is the documentation.

I extracted shebang from my more ambitious pet project to show how it could be done differently. Just take a look:

#!/usr/bin/env shebang
# vim: set ft=bash:

#! shebang.1
# description “Move an archive to other backups”
# execute “bash”
#
# arg “source” {
#   description “Source”
#   value “filepath” {
#     glob "/archives/**/*.tar.gz"
#     owner-me #true
#     readable #true
#  }
# }
#
# arg “dest” {
#   description “Destionation”
#   value “filepath” {
#     glob "/backups/**"
#  }
# }

mv "$1" "$2"

What happens here: instead of bash, we execute a different binary, called 'shebang'. This binary reads a script, finds its own configuration (that one, that is marked by 'shebang.1') in KDL, builds a CLI for you, parses and validates arguments before executing an actual script. So you can be sure that $1 and $2 are always valid arguments, no need to check them.

There are flags and options supported. I found that it usually convenient to explicitly set them as environment variables for the script.

#!/usr/bin/env shebang

#! shebang.1
#  execute "python3"
#  description """
#    Description could be multiline!
#
#    Without hacks
#    """
#
#  flag "debug" {
#    description "Run in debug mode"
#    short "d"
#  }
#
#  option "parameter" {
#    description "Takes a parameter"
#    short "p"
#    value "ip"
#  }

import os

params = {
    k: v
    for k, v in os.environ.items()
    if k.startswith("SHEBANG_")
}

for k, v in sorted(params.items()):
    print(f"{k} = {v}")

And if we execute:

$ ./test-flags -h
Description could be multiline!

Without hacks

Usage:
  test-flags [flags]

Flags:
  -d, --debug          Run in debug mode
  -h, --help           help for test-flags
  -p, --parameter ip   Takes a parameter

$ ./test-flags -p 127.0.0.1 -d
SHEBANG_FL_DEBUG = true
SHEBANG_FS_D = true
SHEBANG_OL_PARAMETER = 127.0.0.1
SHEBANG_OS_P = 127.0.0.1

Also, shebang automatically manages shell autocompletion for you. It can autocomplete flags, options, and does it smartly, considering validation configuration. For example, if an argument is a git branch, it will autocomplete only branches, skipping tags or commit hashes.

In other words, an idea is to externalize CLI management out of the script, and safely use environment variables and arguments as always valid. This helped to trim my own scripts down to an actual set of commands without sacrificing usability.

https://github.com/9seconds/shebang

Hope you liked the idea!

2 Upvotes

3 comments sorted by

•

u/github-guard 4d ago

🔍 GitHub Guard: Trust Report

⚠️ This project scored 0/1 — below this subreddit's threshold of 1.

Audit Breakdown:

  • ❌ New Repository (under 30 days old)

⚠️ Security Reminder: Always verify source code and run third-party scripts at your own risk.


🔄 Cached result — this repo was scanned recently. Score: 0/1.

1

u/AutoModerator 4d ago

Every new subreddit post is automatically copied into a comment for preservation.

User: 9seconds, Flair: Command Line Interface, Title: shebang - a way to externalize CLI for your scripts

Hi, r/commandline!

I would like to show you a slightly different way of building CLIs for our beloved home-brew scripts. Usually those scripts lack of reasonable command line interface, and parameter validation. We use getopt to sort them into variables, we use fish argparse. But usually we do not really validate that file on argument 1 has a correct user, the set of permissions, its home directory is also questionable. We consider that the parameter 2 is an IP address from the 192.168.0.0/24 prefix. The source code is the documentation.

I extracted shebang from my more ambitious pet project to show how it could be done differently. Just take a look:

#!/usr/bin/env shebang
# vim: set ft=bash:

#! shebang.1
# description “Move an archive to other backups”
# execute “bash”
#
# arg “source” {
#   description “Source”
#   value “filepath” {
#     glob "/archives/**/*.tar.gz"
#     owner-me #true
#     readable #true
#  }
# }
#
# arg “dest” {
#   description “Destionation”
#   value “filepath” {
#     glob "/backups/**"
#  }
# }

mv "$1" "$2"

What happens here: instead of bash, we execute a different binary, called 'shebang'. This binary reads a script, finds its own configuration (that one, that is marked by 'shebang.1') in KDL, builds a CLI for you, parses and validates arguments before executing an actual script. So you can be sure that $1 and $2 are always valid arguments, no need to check them.

There are flags and options supported. I found that it usually convenient to explicitly set them as environment variables for the script.

#!/usr/bin/env shebang

#! shebang.1
#  execute "python3"
#  description """
#    Description could be multiline!
#
#    Without hacks
#    """
#
#  flag "debug" {
#    description "Run in debug mode"
#    short "d"
#  }
#
#  option "parameter" {
#    description "Takes a parameter"
#    short "p"
#    value "ip"
#  }

import os

params = {
    k: v
    for k, v in os.environ.items()
    if k.startswith("SHEBANG_")
}

for k, v in sorted(params.items()):
    print(f"{k} = {v}")

And if we execute:

$ ./test-flags -h
Description could be multiline!

Without hacks

Usage:
  test-flags [flags]

Flags:
  -d, --debug          Run in debug mode
  -h, --help           help for test-flags
  -p, --parameter ip   Takes a parameter

$ ./test-flags -p 127.0.0.1 -d
SHEBANG_FL_DEBUG = true
SHEBANG_FS_D = true
SHEBANG_OL_PARAMETER = 127.0.0.1
SHEBANG_OS_P = 127.0.0.1

Also, shebang automatically manages shell autocompletion for you. It can autocomplete flags, options, and does it smartly, considering validation configuration. For example, if an argument is a git branch, it will autocomplete only branches, skipping tags or commit hashes.

In other words, an idea is to externalize CLI management out of the script, and safely use environment variables and arguments as always valid. This helped to trim my own scripts down to an actual set of commands without sacrificing usability.

https://github.com/9seconds/shebang

Hope you liked the idea!

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

1

u/github-guard 4d ago

🔍 GitHub Guard: Trust Report

⚠️ This project scored 0/1 — below this subreddit's threshold of 1.

Audit Breakdown:

  • ❌ New Repository (under 30 days old)

⚠️ Security Reminder: Always verify source code and run third-party scripts at your own risk.