Dynamic Function Generation in Zsh

Dynamic Function Generation in Zsh

Bill Gates the tech tycoon allegedly said "I choose a lazy person to do a hard job. Because a lazy person will find an easy way to do it."

I like consider myself to be a lazy person. I'm always looking for a way to work less.

It might sound silly but I don't want to go downstairs to grab a snack, only to have to head back downstairs 20 minutes later just to refill my drink, so I'll finish up my drink and do both at the same time. And again to avoid going downstairs again I'll make sure the door is locked, and the lights are turned out. Sounds simple right?

But not everyone makes these small changes. I've seen some people make 7 trips up and down stairs to do what 1 trip should have completed. You could say they're working hard, but did they really need to make 7 trips out of what could have been a single trip? Then they could have used that extra time for something else.

I'm often bombarded with some form of work from all directions at the office and home, and sometimes there is simply no way to complete it all in any reasonable time without me creating some kind of automation to reduce the workload.

Did you know the menial tasks I do everyday cost me time? Even opening "Google" in my browser can be cut down by several seconds.

Because of my career in technology, I often find I'm working in one or more terminal windows. And I like to work from my Mac and only ssh into a remote system when needed. So most of those terminal windows are local. You could argue one way is superior to the other but that isn't what I'm here to do.

I've been working on my DotFiles for years, today they consist of about 50 files, containing almost 9000 lines of code. I have them divided into different sections. One file's job is to load those sections with pre_ files coming first to setup a few things, followed by most of the scripts loading, followed by loading and executing any post_ files. I'll touch on why I use this later, but needless to say it's probably more complicated than most other people's DotFiles.

Let's start with websites. There are a number of websites I regularly use, and the act of switching to the browser, opening up a new tab, typing the url, and pressing enter is more than I'm willing to do most of the time.

So what if I could just type "google" at the terminal? And it take me to the website? Actually I can with a small alias like this.

alias google='open -a "Google Chrome" "https://www.google.com"'        
No alt text provided for this image
The Google search page ready to accept my query.

I could even make the alias shorter while keeping the full name alias like this.

alias goog='google'
alias g='google'        

Now if I type any of those three from the command line, it opens Google in my default web browser (opening my browser if needed). Ready for me to type my search. Granted, these are simple aliases, and I and most other techies have a number of them. I'm not going to go into the specifics of bash/zsh scripting, but I will explain some of the more complicated parts of what I'm going to share later on.

Now what if I want to define a lot of these websites? I have to define an alias for each. At first that's not a big deal, 5 or 20 websites doesn't take that much time. But what if I want a lot more? At that point I start thinking about automation, and you'll see why as this gets more complicated.

The following will let me define a list of alias names and website links to open in my browser.

#!/bin/zsh

# To use: source <thisfile>

# I keep this first like in a file called GoLinks_pre.sh which loads first
export go_mylinks=()


# This is kept in Golinks.sh which loads second
# <thing>
go_mylinks=(${go_mylinks[@]}
? ? 'amazon:https://www.amazon.com'
? ? 'google:https://www.google.com'
)


alias g='google'
alias goog='google'


# The rest of this is kept in GoLinks_post.sh which loads last.
function _golinks_setup() {
? ? local app link go_l f
? ? for go_l in ${go_mylinks}; do
? ? ? ? if [[ "a${go_l}" =~ ":" ]]; then
? ? ? ? ? ? app=$(echo "${${${go_l}%%:*}}") # Bash subsitution for "cut -f1 -d:"
? ? ? ? ? ? link=$(echo "${${go_l}#*:}")? ? # Bash subsitution for "cut -f2- -d:"
? ? ? ? ? ? alias ${app}="open -a 'Google Chrome' \"${link}\""
? ? ? ? fi
? ? done
}


_golinks_setup        

Although so long as you keep all three sections one after the other like above they can exist in the same file. Like I said, I have MANY functions and aliases in my DotFiles so I keep different scripts in different areas. In my case, I have multiple "GoLinks_xxxx.sh" that load between "GoLinks_pre.sh" and "GoLinks_post.sh". Each of the files contain different GoLinks, one for work, one for personal, etc.

You might be asking...what is a GoLink. I learned that time from my work at LinkedIn, when I joined I was taught LinkedIn has alot of these "GoLinks" for your browser. Such as https://go/wiki takes you to the mail wiki page. I understand GoLinks originated at Google, and when a Google engineer moved to LinkedIn that he continued the tradition. I continued to use the term because originally I first wrote those web script to open those go links from the command line in my browser by simply typing go/wiki at the command line just like in the examples I've provided here.

Note that almost everything I write is a function or an alias, I don't really write many "traditional" bash scripts designed to be run like "myscript.sh my arguments". Thats a personal choice of mine. When I have a long complicated program I may use bash, but more often I'll choose Python or Java. But for my DotFiles few of my scripts run more than 30-50 lines, most are under 20.

Now that's cool we can open web pages, but it's kinda limiting....I mean why stop there? Why can't I go ahead and enter my search from the command line?

That requires tweaking things a bit...you see in an alias I can't process arguments, aliases are a bit limiting. But functions can't be defined with variable arguments or the parameters they take will be defined at definition time and not at runtime. So for example $@ will contain what the function call of "_weblinks_setup" contained, when really we want it to contain what is in the $@ of a call to "google". So we'll need to switch to functions...and use a bit of a hack to dynamically define them with variables that can be populated on function run.


#!/bin/zsh


# To use: source <thisfile>


# I keep this first like in a file called GoLinks_pre.sh which loads first
export go_hardlinks=()
export go_mylinks=()
export go_searchlinks=()
alias url='open -a "Google Chrome"'
function urlencode () {
? ? if [[ ${#@} -eq 0 ]]
? ? then
? ? ? ? echo $(python -c "from urllib.request import quote;import sys; print(quote('\n'.join(sys.stdin.readlines())))")
? ? else
? ? ? ? echo $(python -c "from urllib.request import quote;print(quote('''${*}'''))")
? ? fi
}


# This is kept in Golinks.sh which loads second


# LinkedIn Go Links go/<thing>
# Note these are provided for example only and will not work outside of the LinkedIn internal network
# Now you could setup your own DNS or URL redirect for "go" and redirect it to whatever you wanted. For example bit.ly links.
go_hardlinks=(${go_hardlinks[@]}
? ? 'helpin' 'questions' 'wiki' # Research
)


# <thing>
go_mylinks=(${go_mylinks[@]}
? ? # Web tools
? ? 'gmail:https://mail.google.com'


? ? # Conversions and Experimentation
? ? 'epochconverter:https://www.epochconverter.com'
? ? 'jsfiddle:https://jsfiddle.net'
? ? 'regex101:https://regex101.com'
? ? 'urlencoder:https://meyerweb.com/eric/tools/dencoder'
? ? 'keycodes:https://keycode.info'
? ? 'keycodechart:https://www.foreui.com/articles/Key_Code_Table.htm'
)


# <thing> [params]
go_searchlinks=($go_searchlinks[@]
? ? # Shopping
? ? 'amazon:https://www.amazon.com###/s?url=search-alias%3Daps&field-keywords=$$$'


? ? # Web Searches
? ? 'google:https://www.google.com###/search?q=$$$'
? ? 'google_img:https://www.google.com###/search?q=$$$&tbm=isch'


? ? # Fun
? ? 'httpcat:https://http.cat/###$$$'


? ? # Research
? ? 'colorhex:https://www.color-hex.com###/color/$$$'
? ? 'macapp:https://macappstore.org/###$$$/'
? ? 'nasdaq:https://www.nasdaq.com###/symbol/$$$/real-time'
? ? 'rfc:https://tools.ietf.org###/html/$$$'
? ? 'stackoverflow:https://stackoverflow.com###/search?q=$$$'
)


alias g='google'
alias goog='google'
alias so='stackoverflow'


function _golinks_setup() {
? ? local go_links app link go_l f
? ? go_links=(${go_hardlinks[@]} ${go_mylinks[@]} ${go_searchlinks[@]})
? ? for go_l in ${go_links}; do
? ? ? ? if [[ "a${go_l}" =~ ":" ]]; then
? ? ? ? ? ? app=$(echo "${${${go_l}%%:*}}")
? ? ? ? ? ? link=$(echo "${${go_l}#*:}")
? ? ? ? ? ? if [[ "a${go_l}" =~ '\$\$\$' ]]; then
? ? ? ? ? ? ? ? go_l=${${go_l//\\&/&}//&/\\&} # Escape ambersand, as it expands unintentionally in functions.
? ? ? ? ? ? ? ? app=$(echo "${${${go_l}%%:*}}") # Bash subsitution for "cut -f1 -d:"
? ? ? ? ? ? ? ? link=$(echo "${${go_l}#*:}")? ? # Bash subsitution for "cut -f2- -d:"
? ? ? ? ? ? ? ? # Note the regex requires 4x$ for a match of 3x. Otherwise we keep 1 $.
? ? ? ? ? ? ? ? # Define function template and static variables
? ? ? ? ? ? ? ? if [[ "a${go_l}" =~ '###' ]]; then
? ? ? ? ? ? ? ? ? ? f='function ${app}() { if [[ $[#] -eq 0 ]]; then url "${link_s}";else;url "${link}";fi }' # Template function
? ? ? ? ? ? ? ? ? ? f=$(echo "${f}" | sed 's@${link_s}@'"${link%###*}"'@')
? ? ? ? ? ? ? ? else
? ? ? ? ? ? ? ? ? ? f='function ${app}() { if [[ $[#] -eq 0 ]]; then echo "Error: $0 requires at least 1 argument (search term)";return 1;fi;url "${link}" }' # Template function
? ? ? ? ? ? ? ? fi
? ? ? ? ? ? ? ? eval $(echo $f | sed 's@${link}@'"${link//\#\#\#/}"'@' | sed 's/\$\$\$/$(urlencode "${@}")/')
? ? ? ? ? ? else
? ? ? ? ? ? ? ? app=$(echo "${${${go_l}%%:*}}")
? ? ? ? ? ? ? ? link=$(echo "${${go_l}#*:}")
? ? ? ? ? ? ? ? alias ${app}="url \"${link}\"" # Define custom alias
? ? ? ? ? ? fi
? ? ? ? else
? ? ? ? ? ? alias go/${go_l}="url \"https://go/${go_l}\"" # Define simple alias. Only works at Google and LinkedIn
? ? ? ? fi
? ? done
}


_golinks_setup        

So if I load this up with something like "source DynamicAliasesAndFunctions.sh", and type "google wat lady meme" in the command line. I get this:

No alt text provided for this image

So let me explain a few of these lines

# Extract the app name from tuple, the thing someone types on the command line
app=$(echo "${${${go_l}%%:*}}")

# Extract URL from tuple
link=$(echo "${${go_l}#*:}")

# Check if $$$ exist in the URL, if so we'll define a function
# providing the user the option of providing paramaters.
if [[ "a${go_l}" =~ '\$\$\$' ]]; then
  # Escape ambersand, as it expands unintentionally in functions.
  go_l=${${go_l//\\&/&}//&/\\&} 
  # Note the regex requires 4x$ for a match of 3x.
  # Otherwise we keep 1 $.
  # Define function template and static variables

  # "###" in a URL indicates where the "static" part of the URL ends,
  # and the search part begins
  # For example 'google:https://www.google.com###/search?q=$$$'
  #     Defines the static url as "https://www.google.com"
  #     and the search URL as "https://www.google.com/search?q=<query>"
  # If you don't specify ### in a search URL, then paramaters are
  # required.
  if [[ "a${go_l}" =~ '###' ]]; then

    # We define a template function here with single qoutes toshell avoid expansion
    # Using "${app}", "${link_s}", and "${link}" as variables we will replace with sed later.
    f='function ${app}() { if [[ $[#] -eq 0 ]]; then url "${link_s}";else;url "${link}";fi }' # Template function

    # Here we replace the static link variable link_s with the short URL (removing the ### and everything after)
    f=$(echo "${f}" | sed 's@${link_s}@'"${link%###*}"'@')
  else
    # This would define a search function that requires paramaters, otherwise identical to the above.
    f='function ${app}() { if [[ $[#] -eq 0 ]]; then echo "Error: $0 requires at least 1 argument (search term)";return 1;fi;url "${link}" }' # Template function
  fi

  # This part the full URL, and the $$$ with the users search term.
  eval $(echo $f | sed 's@${link}@'"${link//\#\#\#/}"'@' | sed 's/\$\$\$/$(urlencode "${@}")/')
else

  # Custom URL alias we did earlier in the previous example.
  alias ${app}="url \"${link}\"" # Define custom alias
fi        

? ? ? ? ? ? ?

I hope this was helpful to everyone, please send me any questions, comments, recommend improvements, or just say hi. Hopefully I'll do more of these articles in the future as I have a lot of knowledge to share.

Do you have some cool scripts/dotfiles to share? I'm always interested in learning more and seeing some of the cool stuff other people build so send them my way.

Have a great day!

Edit: I corrected an error in _golinks_setup() that would cause a failed expansion.

#sre #hacking #zsh #bash #scripting #url #automation #lazy #terminal #commandline

要查看或添加评论,请登录

Joshua Briefman的更多文章

社区洞察

其他会员也浏览了